package auth import ( "context" "encoding/json" "log" "sync" "armature/database" "armature/store" ) // EveryoneGroupID is the stable ID of the implicit "Everyone" group seeded in // migration 002. Every authenticated user receives its permissions without an // explicit membership row. const EveryoneGroupID = "00000000-0000-0000-0000-000000000001" // AdminsGroupID is the stable ID of the "Admins" system group seeded in // migration 002. Members receive surface.admin.access and all platform // permissions. Replaces the legacy users.role = 'admin' check. const AdminsGroupID = "00000000-0000-0000-0000-000000000002" // Permission constants — domain.action convention. const ( PermSurfaceAdminAccess = "surface.admin.access" // full admin panel access (replaces role check) PermExtensionUse = "extension.use" // use installed extensions PermExtensionInstall = "extension.install" // install/manage extension packages PermWorkflowCreate = "workflow.create" // create workflow definitions PermWorkflowSubmit = "workflow.submit" // submit to public workflows PermAdminView = "admin.view" // read-only admin panel access PermTokenUnlimited = "token.unlimited" // bypass token budgets ) // KernelPermissions is the static set of platform permission strings. var KernelPermissions = []string{ PermSurfaceAdminAccess, PermExtensionUse, PermExtensionInstall, PermWorkflowCreate, PermWorkflowSubmit, PermAdminView, PermTokenUnlimited, } // AllPermissions is the kernel permissions. For the complete set including // extension-declared permissions, use AllPermissionsWithExtensions(). // Kept as a var for backward compatibility with EnsureAdminsGroup and // other call sites that only need kernel permissions. var AllPermissions = KernelPermissions // ── Extension Permission Registry ──────────── var ( extPermsMu sync.RWMutex extPerms = make(map[string][]string) // packageID → declared user permissions ) // RegisterExtensionPermissions registers user-facing permissions declared by // an extension package. Called on install and at boot for active packages. func RegisterExtensionPermissions(packageID string, perms []string) { extPermsMu.Lock() extPerms[packageID] = perms extPermsMu.Unlock() } // UnregisterExtensionPermissions removes user-facing permissions declared by // an extension package. Called on uninstall. func UnregisterExtensionPermissions(packageID string) { extPermsMu.Lock() delete(extPerms, packageID) extPermsMu.Unlock() } // AllPermissionsWithExtensions returns kernel + extension-declared permissions. func AllPermissionsWithExtensions() []string { extPermsMu.RLock() defer extPermsMu.RUnlock() result := make([]string, len(KernelPermissions)) copy(result, KernelPermissions) for _, perms := range extPerms { result = append(result, perms...) } return result } // AllPermissionsGrouped returns permissions grouped by source. // "kernel" key holds platform permissions; other keys are package IDs. func AllPermissionsGrouped() map[string][]string { extPermsMu.RLock() defer extPermsMu.RUnlock() result := map[string][]string{ "kernel": KernelPermissions, } for pkgID, perms := range extPerms { result[pkgID] = perms } return result } // ── Resolution ────────────────────────────── // ResolvePermissions returns the effective permission set for a user. // Unions permissions from all groups the user is a member of (including Everyone). func ResolvePermissions(ctx context.Context, stores store.Stores, userID string) (map[string]bool, error) { perms := make(map[string]bool) groups, err := stores.Groups.ListForUser(ctx, userID) if err != nil { return perms, err } for _, g := range groups { for _, p := range g.Permissions { perms[p] = true } } return perms, nil } // EnsureEveryoneGroup adds a user to the Everyone group (idempotent). // Called on every user creation path so that Everyone membership is explicit. func EnsureEveryoneGroup(ctx context.Context, stores store.Stores, userID string) { _ = stores.Groups.AddMember(ctx, EveryoneGroupID, userID, userID) } // EnsureAdminsGroup creates the Admins system group if it does not exist. // Handles the case where migration 002 was applied before the Admins INSERT // was added (no new migrations pre-MVP — edits in place). // Uses raw SQL because the store's Create() overwrites the ID. func EnsureAdminsGroup(ctx context.Context, stores store.Stores) { if _, err := stores.Groups.GetByID(ctx, AdminsGroupID); err == nil { return // already exists } permsJSON, _ := json.Marshal(AllPermissions) db := database.DB if db == nil { return } _, err := db.ExecContext(ctx, ` INSERT OR IGNORE INTO groups (id, name, description, scope, created_by, source, permissions) VALUES (?, 'Admins', 'Full platform access — replaces legacy admin role.', 'global', NULL, 'system', ?)`, AdminsGroupID, string(permsJSON)) if err != nil { // Postgres variant _, err = db.ExecContext(ctx, ` INSERT INTO groups (id, name, description, scope, created_by, source, permissions) VALUES ($1, 'Admins', 'Full platform access — replaces legacy admin role.', 'global', NULL, 'system', $2::jsonb) ON CONFLICT (id) DO NOTHING`, AdminsGroupID, string(permsJSON)) if err != nil { log.Printf("⚠ EnsureAdminsGroup: %v", err) } } } // AddToAdminsGroup adds a user to the Admins group (idempotent). // Ensures the Admins group exists before attempting membership. func AddToAdminsGroup(ctx context.Context, stores store.Stores, userID string) { EnsureAdminsGroup(ctx, stores) _ = stores.Groups.AddMember(ctx, AdminsGroupID, userID, userID) } // RemoveFromAdminsGroup removes a user from the Admins group. func RemoveFromAdminsGroup(ctx context.Context, stores store.Stores, userID string) { _ = stores.Groups.RemoveMember(ctx, AdminsGroupID, userID) }