factor out kjol auth
This commit is contained in:
153
go/auth/authz.go
Normal file
153
go/auth/authz.go
Normal file
@@ -0,0 +1,153 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user