241 lines
5.2 KiB
Go
241 lines
5.2 KiB
Go
package permission
|
|
|
|
import "strings"
|
|
|
|
// Role is a platform-level role carried in OIDC groups.
|
|
type Role string
|
|
|
|
const (
|
|
RoleAdmin Role = "admin"
|
|
RoleUser Role = "user"
|
|
RoleService Role = "service"
|
|
)
|
|
|
|
// Resource is a suite module protected by resource-scoped permissions.
|
|
type Resource string
|
|
|
|
const (
|
|
ResourceDrive Resource = "drive"
|
|
ResourcePhotos Resource = "photos"
|
|
ResourceContacts Resource = "contacts"
|
|
ResourceCalendar Resource = "calendar"
|
|
)
|
|
|
|
// Level is a resource permission with read < write < admin ordering.
|
|
type Level int
|
|
|
|
const (
|
|
LevelRead Level = iota + 1
|
|
LevelWrite
|
|
LevelAdmin
|
|
)
|
|
|
|
func (l Level) String() string {
|
|
switch l {
|
|
case LevelRead:
|
|
return "read"
|
|
case LevelWrite:
|
|
return "write"
|
|
case LevelAdmin:
|
|
return "admin"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
func ParseLevel(s string) (Level, bool) {
|
|
switch strings.ToLower(strings.TrimSpace(s)) {
|
|
case "read":
|
|
return LevelRead, true
|
|
case "write":
|
|
return LevelWrite, true
|
|
case "admin":
|
|
return LevelAdmin, true
|
|
default:
|
|
return 0, false
|
|
}
|
|
}
|
|
|
|
func levelRank(l Level) int {
|
|
return int(l)
|
|
}
|
|
|
|
var suiteResources = []Resource{
|
|
ResourceContacts,
|
|
ResourceCalendar,
|
|
ResourceDrive,
|
|
ResourcePhotos,
|
|
}
|
|
|
|
func hasAnyResourcePermission(groups []string) bool {
|
|
for _, g := range groups {
|
|
g = strings.ToLower(strings.TrimSpace(g))
|
|
for _, resource := range suiteResources {
|
|
if strings.HasPrefix(g, string(resource)+":") {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// WithSuiteDefaults grants standard suite read/write when the token carries no
|
|
// resource-scoped groups. Mail endpoints stay open; CardDAV/CalDAV modules rely
|
|
// on this until Authentik emits explicit RBAC groups on every account.
|
|
func WithSuiteDefaults(groups []string) []string {
|
|
if hasAnyResourcePermission(groups) {
|
|
return groups
|
|
}
|
|
|
|
defaults := []string{
|
|
string(RoleUser),
|
|
string(ResourceContacts) + ":write",
|
|
string(ResourceCalendar) + ":write",
|
|
string(ResourceDrive) + ":write",
|
|
string(ResourcePhotos) + ":write",
|
|
}
|
|
|
|
seen := make(map[string]struct{}, len(groups)+len(defaults))
|
|
out := make([]string, 0, len(groups)+len(defaults))
|
|
for _, g := range groups {
|
|
g = strings.TrimSpace(g)
|
|
if g == "" {
|
|
continue
|
|
}
|
|
key := strings.ToLower(g)
|
|
if _, ok := seen[key]; ok {
|
|
continue
|
|
}
|
|
seen[key] = struct{}{}
|
|
out = append(out, g)
|
|
}
|
|
for _, g := range defaults {
|
|
key := strings.ToLower(g)
|
|
if _, ok := seen[key]; ok {
|
|
continue
|
|
}
|
|
seen[key] = struct{}{}
|
|
out = append(out, g)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// AdminScope is a fine-grained admin API permission with read < write ordering.
|
|
type AdminScope int
|
|
|
|
const (
|
|
AdminScopeRead AdminScope = iota + 1
|
|
AdminScopeWrite
|
|
)
|
|
|
|
// DefaultAdminScope is the scope assumed when an endpoint requires full admin API access.
|
|
const DefaultAdminScope = AdminScopeWrite
|
|
|
|
const (
|
|
GroupAdminRead = "admin:read"
|
|
GroupAdminWrite = "admin:write"
|
|
)
|
|
|
|
func (s AdminScope) String() string {
|
|
switch s {
|
|
case AdminScopeRead:
|
|
return "read"
|
|
case AdminScopeWrite:
|
|
return "write"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
func ParseAdminScope(s string) (AdminScope, bool) {
|
|
switch strings.ToLower(strings.TrimSpace(s)) {
|
|
case "read":
|
|
return AdminScopeRead, true
|
|
case "write":
|
|
return AdminScopeWrite, true
|
|
default:
|
|
return 0, false
|
|
}
|
|
}
|
|
|
|
func adminScopeRank(s AdminScope) int {
|
|
return int(s)
|
|
}
|
|
|
|
// HasAdminScope reports whether groups grant at least the required admin API scope.
|
|
// Platform admins (admin or role:admin) satisfy any scope for backwards compatibility.
|
|
// admin:write implies admin:read; admin:read does not imply write.
|
|
func HasAdminScope(groups []string, required AdminScope) bool {
|
|
if HasRole(groups, RoleAdmin) {
|
|
return true
|
|
}
|
|
|
|
max := AdminScope(0)
|
|
|
|
for _, g := range groups {
|
|
g = strings.ToLower(strings.TrimSpace(g))
|
|
switch g {
|
|
case GroupAdminRead:
|
|
if adminScopeRank(AdminScopeRead) > adminScopeRank(max) {
|
|
max = AdminScopeRead
|
|
}
|
|
case GroupAdminWrite:
|
|
if adminScopeRank(AdminScopeWrite) > adminScopeRank(max) {
|
|
max = AdminScopeWrite
|
|
}
|
|
}
|
|
}
|
|
|
|
return adminScopeRank(max) >= adminScopeRank(required)
|
|
}
|
|
|
|
// HasRole reports whether groups grant the given platform role.
|
|
func HasRole(groups []string, role Role) bool {
|
|
want := string(role)
|
|
for _, g := range groups {
|
|
g = strings.ToLower(strings.TrimSpace(g))
|
|
if g == want || g == "role:"+want {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// HasAnyRole reports whether groups grant at least one of the roles.
|
|
func HasAnyRole(groups []string, roles ...Role) bool {
|
|
for _, role := range roles {
|
|
if HasRole(groups, role) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// HasPermission reports whether groups grant at least the required level on resource.
|
|
// Platform admins bypass resource checks. Higher levels satisfy lower ones.
|
|
func HasPermission(groups []string, resource Resource, required Level) bool {
|
|
if HasRole(groups, RoleAdmin) {
|
|
return true
|
|
}
|
|
|
|
want := string(resource)
|
|
max := Level(0)
|
|
|
|
for _, g := range groups {
|
|
g = strings.ToLower(strings.TrimSpace(g))
|
|
prefix := want + ":"
|
|
if !strings.HasPrefix(g, prefix) {
|
|
continue
|
|
}
|
|
level, ok := ParseLevel(strings.TrimPrefix(g, prefix))
|
|
if !ok {
|
|
continue
|
|
}
|
|
if levelRank(level) > levelRank(max) {
|
|
max = level
|
|
}
|
|
}
|
|
|
|
return levelRank(max) >= levelRank(required)
|
|
}
|