Changeset 0.24.2 (#158)

This commit is contained in:
2026-03-07 19:58:43 +00:00
parent b6cc4df6e7
commit ea082e2016
24 changed files with 954 additions and 119 deletions

View File

@@ -681,31 +681,52 @@ COMMENT ON COLUMN groups.permissions IS 'Array of permission strings granted to
COMMENT ON COLUMN groups.allowed_models IS 'Array of model_id strings this group can use. NULL = unrestricted.';
```
### The Everyone Group
Rather than hardcoding base permissions in application code, the system
seeds a special **Everyone** group at migration time. Every authenticated
user implicitly receives its permissions without explicit membership.
Properties:
- `id` = `00000000-0000-0000-0000-000000000001` (stable, well-known)
- `name` = `"Everyone"`
- `scope` = `"global"`
- `source` = `"system"` (guards against deletion)
The Everyone group is editable in the admin UI — an org that wants
locked-down defaults empties it; an open install leaves it permissive.
`DefaultUserPerms` via `global_config` (previously in ROADMAP) is
superseded by this approach: it was a one-time init value anyway.
The migration seeds the group; subsequent changes are live data.
`Delete` on a `source=system` group returns `400 Bad Request`. The guard
lives in the store layer, not the handler, so it can't be bypassed.
### Permission Resolution
A user's effective permissions are the **union** of all their groups'
permissions, plus base permissions that every active user gets.
A user's effective permissions are the **union** of the Everyone group's
permissions plus all their explicitly assigned groups' permissions.
```go
// server/auth/permissions.go
// Base permissions granted to all active users (no group needed)
var basePermissions = map[string]bool{
"model.use": true,
"kb.read": true,
"channel.create": true,
}
const EveryoneGroupID = "00000000-0000-0000-0000-000000000001"
// ResolvePermissions returns the effective permission set for a user.
// Always includes the Everyone group regardless of membership.
// Admin users bypass this entirely — callers should check role first.
func ResolvePermissions(ctx context.Context, stores store.Stores, userID string) (map[string]bool, error) {
perms := make(map[string]bool)
// Start with base permissions
for k, v := range basePermissions {
perms[k] = v
// Always apply Everyone group
everyone, err := stores.Groups.GetByID(ctx, EveryoneGroupID)
if err == nil {
for _, p := range everyone.Permissions {
perms[p] = true
}
}
// Union all group permissions
// Union all explicit group memberships
groups, err := stores.Groups.ListForUser(ctx, userID)
if err != nil {
return perms, err
@@ -723,6 +744,21 @@ func ResolvePermissions(ctx context.Context, stores store.Stores, userID string)
Admin users (`role=admin`) bypass permission checks entirely, same
as they bypass team membership checks today.
### BYOK Budget Bypass
Token budget enforcement only applies when the completion draws from a
**team or global provider**. If `providerScope == "personal"` the request
is the user's own money — budget pre-flight is skipped entirely. The
`providerScope` value is already resolved by `resolveConfig()` before
the completion handler begins streaming.
### Anonymous Sessions
v0.24.3 session participants carry `SessionClaims`, not `UserClaims`.
They never enter group-based permission resolution. Their capability is
bounded to their channel and the persona bound to it by the workflow
definition. Do not conflate the two paths.
### Middleware Helper
```go
@@ -813,9 +849,15 @@ Group edit form gains:
### What Ships
- Migration 019 (group permissions, token budgets, model allowlist)
- `server/auth/permissions.go` (resolution logic)
- `middleware/permissions.go` (`RequirePermission()`)
- Migration 020 (group permissions, token budgets, model allowlist, Everyone group seed)
- `server/auth/permissions.go` (constants, resolution logic, budget resolution)
- `middleware/permissions.go` (`RequirePermission()` with request-context cache)
- Permission-gated handler routes
- Token budget pre-flight in completion handler (skipped for personal/BYOK providers)
- Model access filtering in model list endpoint
- Admin UI: group permissions editor, user effective permissions view
- Both Postgres and SQLite store implementations
- Everyone group: seeded in migration, guarded from deletion in store layer
- Permission-gated handler routes
- Token budget pre-flight in completion handler
- Model access filtering in model list endpoint

View File

@@ -1430,15 +1430,15 @@ group infrastructure.
Depends on: auth abstraction (v0.24.0). Parallel to v0.24.1.
See [DESIGN-0.24.0.md](DESIGN-0.24.0.md) §v0.24.2 for full spec.
- [ ] Permission constants (`domain.action` convention, code-level enum)
- [ ] `permissions` JSONB column on groups
- [ ] `ResolvePermissions()`: union of group permissions + `DefaultUserPerms`
- [ ] `RequirePermission()` middleware
- [ ] Token budgets: per-group daily/monthly ceilings, enforced in completion handler
- [ ] Model access control: per-group model allowlists
- [ ] Admin UI: permission checklist, budget fields, model picker on group edit
- [ ] `DefaultUserPerms` override via `global_config` key `default_permissions`
- [ ] Migration 020: permissions, budgets, allowed_models on groups
- [x] Permission constants (`domain.action` convention, code-level enum)
- [x] `permissions` JSONB column on groups
- [x] `ResolvePermissions()`: union of group permissions + Everyone group
- [x] `RequirePermission()` middleware
- [x] Token budgets: per-group daily/monthly ceilings, enforced in completion handler
- [x] Model access control: per-group model allowlists
- [x] Admin UI: permission checklist, budget fields, model picker on group edit
- [x] Everyone group supersedes `DefaultUserPerms` (seeded in migration, editable in admin)
- [x] Migration 020: permissions, budgets, allowed_models on groups
---