Files
pocket-id/backend/internal/authz/scope.go
T

150 lines
5.1 KiB
Go

package authz
// Scope is a permission that a principal must hold to call a route
// Keys follow the resource:action pattern and are valid RFC 6749 scope tokens so they can later appear in API key records and OAuth access tokens unchanged
type Scope string
// Account scopes cover the caller's own account and are held by every signed-in user
const (
AccountRead Scope = "account:read"
AccountWrite Scope = "account:write"
AccountPasskeys Scope = "account:passkeys"
AccountAPIKeys Scope = "account:api-keys"
AccountApps Scope = "account:apps"
AccountAuditLogs Scope = "account:audit-logs"
AccountSession Scope = "account:session"
AccountPasskeysEnroll Scope = "account:passkeys:enroll"
AccountAPIKeysCreate Scope = "account:api-keys:create"
)
// Admin scopes cover other users' data and the instance configuration
const (
UsersRead Scope = "users:read"
UsersWrite Scope = "users:write"
GroupsRead Scope = "groups:read"
GroupsWrite Scope = "groups:write"
OidcClientsRead Scope = "oidc-clients:read"
OidcClientsWrite Scope = "oidc-clients:write"
APIsRead Scope = "apis:read"
APIsWrite Scope = "apis:write"
ConfigRead Scope = "config:read"
ConfigWrite Scope = "config:write"
AuditLogsRead Scope = "audit-logs:read"
)
// Category groups scopes by whose data they reach
type Category int
const (
// CategoryAccount scopes act on the caller's own account
CategoryAccount Category = iota + 1
// CategoryAdmin scopes act on other users or on the instance
CategoryAdmin
)
// PrincipalKind identifies the kind of credential a principal authenticated with
// Kinds are bit flags so a scope can list every kind that may hold it
type PrincipalKind uint8
const (
// KindSession is a browser session established by signing in to Pocket ID
KindSession PrincipalKind = 1 << iota
// KindAPIKey is a personal API key sent in the X-API-Key header
KindAPIKey
// KindOAuthUser is an OAuth access token issued to a client acting on behalf of a user
KindOAuthUser
// KindOAuthClient is an OAuth access token issued to a client acting as itself through the client credentials grant
KindOAuthClient
)
// delegated lists the kinds that act for a user, which is every kind except a client acting as itself
const delegated = KindSession | KindAPIKey | KindOAuthUser
type definition struct {
scope Scope
category Category
grantableTo PrincipalKind
}
// catalog is the complete list of scopes
// grantableTo restricts which credential kinds can ever hold a scope, independent of the user's role
// Session-only scopes guard actions that must never be reachable with a long-lived or third-party credential, such as enrolling passkeys or minting API keys
var catalog = []definition{
{AccountRead, CategoryAccount, delegated},
{AccountWrite, CategoryAccount, delegated},
{AccountPasskeys, CategoryAccount, delegated},
{AccountAPIKeys, CategoryAccount, delegated},
{AccountApps, CategoryAccount, delegated},
{AccountAuditLogs, CategoryAccount, delegated},
{AccountSession, CategoryAccount, KindSession},
{AccountPasskeysEnroll, CategoryAccount, KindSession},
{AccountAPIKeysCreate, CategoryAccount, KindSession},
{UsersRead, CategoryAdmin, delegated},
{UsersWrite, CategoryAdmin, delegated},
{GroupsRead, CategoryAdmin, delegated},
{GroupsWrite, CategoryAdmin, delegated},
{OidcClientsRead, CategoryAdmin, delegated},
{OidcClientsWrite, CategoryAdmin, delegated},
{APIsRead, CategoryAdmin, delegated},
{APIsWrite, CategoryAdmin, delegated},
{ConfigRead, CategoryAdmin, delegated},
{ConfigWrite, CategoryAdmin, delegated},
{AuditLogsRead, CategoryAdmin, delegated},
}
var definitions = indexCatalog(catalog)
func indexCatalog(entries []definition) map[Scope]definition {
index := make(map[Scope]definition, len(entries))
for _, entry := range entries {
index[entry.scope] = entry
}
return index
}
// Known reports whether the scope is part of the catalog
func (s Scope) Known() bool {
_, ok := definitions[s]
return ok
}
// GrantableTo reports whether a principal of the given kind can ever hold the scope
func (s Scope) GrantableTo(kind PrincipalKind) bool {
return definitions[s].grantableTo&kind != 0
}
// ScopeSet is an unordered set of scopes
type ScopeSet map[Scope]struct{}
// NewScopeSet creates a set containing the given scopes
func NewScopeSet(scopes ...Scope) ScopeSet {
set := make(ScopeSet, len(scopes))
for _, scope := range scopes {
set[scope] = struct{}{}
}
return set
}
// Has reports whether the set contains the scope
func (s ScopeSet) Has(scope Scope) bool {
_, ok := s[scope]
return ok
}
// UserScopes returns the scopes a user holds when authenticated with a credential of the given kind
// The admin flag stands in for roles: admins hold every scope and other users hold the account scopes
func UserScopes(isAdmin bool, kind PrincipalKind) ScopeSet {
set := make(ScopeSet, len(catalog))
for _, entry := range catalog {
if entry.grantableTo&kind == 0 {
continue
}
if entry.category == CategoryAdmin && !isAdmin {
continue
}
set[entry.scope] = struct{}{}
}
return set
}