package auth import ( "net/http" "time" . "kjol/basic" "kjol/l4g" ) // Permissions are plain strings, aggregated per request by the app's // PermissionResolver into a single map. The engine does not care where they come // from — a user's direct grants, the groups they belong to, an organization // role, a claim snapshot from an SSO login — only whether the map contains what // a route requires. // // Two families of check live here, and the difference matters: // // - Authorized / AuthorizedExact / AuthorizedAny are pure predicates, for // display logic. They are expected to fail routinely (most users are not // admins, and every "admin only" menu asks on every page), so they log // nothing. // // - Require / RequireExact / RequireAny are middleware. A failure here means a // request actually reached a route the caller may not have, so it is logged. // // The pairing is deliberate: gate the link with Authorized so the request is // never made, and gate the route with Require so it cannot be made anyway. // Authorized reports whether the principal holds every listed permission. // The super permission satisfies any requirement. func (a *Authenticator) Authorized(p Principal, requirements ...string) bool { if a.hasSuper(p) { return true } return holdsAll(p, requirements) } // AuthorizedExact reports whether the principal holds every listed permission, // with no super-permission bypass. // // Use it where the question is "does this user really have X" rather than "may // this user do X" — e.g. showing an admin their own effective grants, where the // wildcard would otherwise make every box look ticked. func (a *Authenticator) AuthorizedExact(p Principal, requirements ...string) bool { return holdsAll(p, requirements) } // AuthorizedAny reports whether the principal holds at least one of the listed // permissions. The super permission satisfies it. // // This is for resources read by several pages whose own permissions differ — a // product-options list fetched by both the inventory editor and the sales // terminal — where requiring all of them would lock out callers who legitimately // hold only one. func (a *Authenticator) AuthorizedAny(p Principal, anyOf ...string) bool { if a.hasSuper(p) { return true } for _, requirement := range anyOf { if p.Permissions[requirement] { return true } } return false } // Require gates a handler on the caller holding every listed permission, // answering 403 otherwise. Mount it inside LoadContext. func (a *Authenticator) Require(h http.HandlerFunc, requirements ...string) http.HandlerFunc { return a.gate(h, func(p Principal) bool { return a.Authorized(p, requirements...) }, requirements) } // RequireExact gates a handler on the caller holding every listed permission, // with no super-permission bypass. func (a *Authenticator) RequireExact(h http.HandlerFunc, requirements ...string) http.HandlerFunc { return a.gate(h, func(p Principal) bool { return a.AuthorizedExact(p, requirements...) }, requirements) } // RequireAny gates a handler on the caller holding at least one of the listed // permissions. func (a *Authenticator) RequireAny(h http.HandlerFunc, anyOf ...string) http.HandlerFunc { return a.gate(h, func(p Principal) bool { return a.AuthorizedAny(p, anyOf...) }, anyOf) } // gate is the shared middleware body: check, log-and-deny, or serve. func (a *Authenticator) gate(h http.HandlerFunc, allow func(Principal) bool, requirements []string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { p := PrincipalFrom(r) if !allow(p) { a.logDenial(r, p, requirements) if isAPIRequest(r) { respondJSONError(w, http.StatusForbidden, "Forbidden: insufficient permissions") return } w.WriteHeader(http.StatusForbidden) return } h(w, r) } } // logDenial records a failed route authorization. // // Unlike a failed display check, this means a request was actually issued for a // route the caller cannot have: either a user is missing access they need, or // someone is probing. Both are worth seeing, so this is the one authorization // path that logs. func (a *Authenticator) logDenial(r *http.Request, p Principal, requirements []string) { entry := l4g.Entry{ Category: l4g.CATEGORY_AUTH, LogType: l4g.TYPE_WARN, Content: MakePtr("Failed Endpoint Authorization"), StructuredContent: l4g.Serialize(struct { Route string RequiredPermissions []string CurrentPermissions map[string]bool Authenticated bool }{ Route: r.URL.Path, RequiredPermissions: requirements, CurrentPermissions: p.Permissions, Authenticated: p.Authenticated, }), Timestamp: time.Now(), } // Anonymous denials carry no user to attribute the entry to. if p.Authenticated { entry.AppUserID = MakePtr(p.Session.UserID) entry.OrgID = p.Session.OrgID entry.IdentityID = MakePtr(p.Session.ID) } l4g.Write(entry) } // hasSuper reports whether the principal holds the app's wildcard permission. func (a *Authenticator) hasSuper(p Principal) bool { return a.cfg.SuperPermission != "" && p.Permissions[a.cfg.SuperPermission] } // holdsAll reports whether the principal holds every listed permission. func holdsAll(p Principal, requirements []string) bool { for _, requirement := range requirements { if !p.Permissions[requirement] { return false } } return true }