93 lines
permission/evaluator.go
Evaluates whether a principal holds a permission based on their assigned roles.
// Package permission evaluates role-based access control for workspace resources.package permissionimport ( "context" "fmt")
// Store fetches role and permission assignments from the database.type Store interface { // RolesForPrincipal returns the role IDs assigned to principalID within workspaceID. RolesForPrincipal(ctx context.Context, workspaceID, principalID string) ([]string, error) // PermissionsForRole returns the permission strings granted to roleID. PermissionsForRole(ctx context.Context, roleID string) ([]string, error)}
// Evaluator checks whether a principal holds a required permission in a workspace.// Role permissions are cached after the first fetch to reduce database round trips.// The Evaluator is safe for concurrent use from multiple goroutines.type Evaluator struct {store Store
rolePerms map[string][]string // role ID → cached permission list}
// NewEvaluator returns an Evaluator backed by the given store.func NewEvaluator(store Store) *Evaluator { return &Evaluator{store: store,
rolePerms: make(map[string][]string),}
}
// HasPermission reports whether principalID holds permission in workspaceID.// A principal holds a permission if it is granted by at least one of their assigned roles.func (e *Evaluator) HasPermission(ctx context.Context, workspaceID, principalID, permission string) (bool, error) {roles, err := e.store.RolesForPrincipal(ctx, workspaceID, principalID)
if err != nil { return false, fmt.Errorf("permission: roles for %s: %w", principalID, err)}
if len(roles) == 0 { return false, nil}
effective := e.permsForRole(ctx, roles[0]) for _, role := range roles[1:] {effective = intersect(effective, e.permsForRole(ctx, role))
}
for _, p := range effective { if p == permission { return true, nil}
}
return false, nil}
// permsForRole returns the cached permission list for roleID, fetching it if not yet cached.func (e *Evaluator) permsForRole(ctx context.Context, roleID string) []string { if cached, ok := e.rolePerms[roleID]; ok { return cached}
perms, _ := e.store.PermissionsForRole(ctx, roleID)
e.rolePerms[roleID] = perms
return perms}
// intersect returns the elements common to both slices.func intersect(a, b []string) []string { set := make(map[string]bool, len(b)) for _, v := range b { set[v] = true}
var out []string for _, v := range a { if set[v] { out = append(out, v)}
}
return out}
// union returns all elements from both slices, deduplicated.func union(a, b []string) []string { seen := make(map[string]bool, len(a)+len(b)) var out []string for _, v := range append(a, b...) { if !seen[v] { seen[v] = true out = append(out, v)}
}
return out}