Feat v0.7.7 api tokens ext permissions (#61)
All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / e2e-smoke (push) Has been skipped
CI/CD / test-frontend (push) Successful in 6s
CI/CD / test-runners (push) Has been skipped
CI/CD / test-go-pg (push) Successful in 2m48s
CI/CD / test-sqlite (push) Successful in 2m49s
CI/CD / build-and-deploy (push) Successful in 28s

Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #61.
This commit is contained in:
2026-04-02 19:11:47 +00:00
committed by xcaliber
parent e02b13dc12
commit e4f0bdbd36
34 changed files with 1769 additions and 58 deletions

13
server/auth/hash.go Normal file
View File

@@ -0,0 +1,13 @@
package auth
import (
"crypto/sha256"
"encoding/hex"
)
// HashToken returns the hex-encoded SHA-256 hash of a token string.
// Used by both the token creation handler and the auth middleware.
func HashToken(token string) string {
h := sha256.Sum256([]byte(token))
return hex.EncodeToString(h[:])
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"log"
"sync"
"armature/database"
"armature/store"
@@ -30,9 +31,8 @@ const (
PermTokenUnlimited = "token.unlimited" // bypass token budgets
)
// AllPermissions is the complete set of valid permission strings.
// Used for validation in handlers and rendering checkboxes in admin UI.
var AllPermissions = []string{
// KernelPermissions is the static set of platform permission strings.
var KernelPermissions = []string{
PermSurfaceAdminAccess,
PermExtensionUse,
PermExtensionInstall,
@@ -42,6 +42,63 @@ var AllPermissions = []string{
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.

View File

@@ -0,0 +1,81 @@
package auth
import (
"testing"
)
func TestRegisterExtensionPermissions(t *testing.T) {
// Clean state
extPermsMu.Lock()
extPerms = make(map[string][]string)
extPermsMu.Unlock()
RegisterExtensionPermissions("image-gen", []string{"image-gen.use", "image-gen.admin"})
all := AllPermissionsWithExtensions()
found := 0
for _, p := range all {
if p == "image-gen.use" || p == "image-gen.admin" {
found++
}
}
if found != 2 {
t.Errorf("expected 2 extension permissions, found %d in %v", found, all)
}
// Kernel permissions should also be present
for _, kp := range KernelPermissions {
foundKernel := false
for _, p := range all {
if p == kp {
foundKernel = true
break
}
}
if !foundKernel {
t.Errorf("kernel permission %q missing from AllPermissionsWithExtensions()", kp)
}
}
}
func TestUnregisterExtensionPermissions(t *testing.T) {
extPermsMu.Lock()
extPerms = make(map[string][]string)
extPermsMu.Unlock()
RegisterExtensionPermissions("image-gen", []string{"image-gen.use"})
UnregisterExtensionPermissions("image-gen")
all := AllPermissionsWithExtensions()
for _, p := range all {
if p == "image-gen.use" {
t.Error("unregistered permission should not appear")
}
}
}
func TestAllPermissionsGrouped(t *testing.T) {
extPermsMu.Lock()
extPerms = make(map[string][]string)
extPermsMu.Unlock()
RegisterExtensionPermissions("chat", []string{"chat.send", "chat.admin"})
RegisterExtensionPermissions("notes", []string{"notes.edit"})
grouped := AllPermissionsGrouped()
if len(grouped["kernel"]) != len(KernelPermissions) {
t.Errorf("expected %d kernel permissions, got %d", len(KernelPermissions), len(grouped["kernel"]))
}
if len(grouped["chat"]) != 2 {
t.Errorf("expected 2 chat permissions, got %d", len(grouped["chat"]))
}
if len(grouped["notes"]) != 1 {
t.Errorf("expected 1 notes permission, got %d", len(grouped["notes"]))
}
// Clean up
extPermsMu.Lock()
extPerms = make(map[string][]string)
extPermsMu.Unlock()
}