This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/CHANGESET.md
gobha 8c53f61b71 Changeset 0.37.1 (#213)
Co-authored-by: gobha <jasafpro@gmail.com>
Co-committed-by: gobha <jasafpro@gmail.com>
2026-03-20 22:15:57 +00:00

12 KiB

CS-0.37.1 — Permission Audit & Enforcement

Date: 2026-03-20 Scope: Backend permission enforcement + new self-service permissions endpoint CI Impact: Adds new test file with 12 test functions. Existing tests unaffected.


Summary

Problem

  • 8 protected routes enforced only Auth() (logged-in check) but not RequirePermission(perm). Any authenticated user could call them regardless of group permissions.
  • No user-facing permissions endpoint — the frontend has zero ability to know what the user is allowed to do.
  • The Everyone group seeds with model.use, kb.read, channel.create, masking the enforcement gaps in most deployments.
  • All existing task tests run as admin, which bypasses RequirePermission entirely — permission denial was never tested.

Fix

  1. Add RequirePermission middleware to 8 routes in main.go
  2. Mirror the same 8 fixes in the integration test harness
  3. Add GET /profile/permissions endpoint (new handler file)
  4. Add GET /profile/permissions to route registration + test harness
  5. Add 12 permission enforcement integration tests (deny → grant → allow)

New Files (drop-in)

  • server/handlers/profile_permissions.go
  • server/handlers/perm_enforcement_test.go

Edited Files (str_replace patches below)

  • server/main.go — 9 route edits
  • server/handlers/integration_test.go — 9 route edits (mirror)
  • docs/ICD/profile.md — add new endpoint documentation

Patch Instructions

Apply each str_replace in order. Each section shows the exact old string and the exact replacement.

1. main.go — Completions (model.use)

Old:

		protected.POST("/chat/completions", comp.Complete)

New:

		protected.POST("/chat/completions", middleware.RequirePermission(auth.PermModelUse, stores), comp.Complete)

2. main.go — Summarize (model.use)

Old:

		protected.POST("/channels/:id/summarize", summarize.Summarize)

New:

		protected.POST("/channels/:id/summarize", middleware.RequirePermission(auth.PermModelUse, stores), summarize.Summarize)

3. main.go — Generate Title (model.use)

Old:

		protected.POST("/channels/:id/generate-title", titleH.GenerateTitle)

New:

		protected.POST("/channels/:id/generate-title", middleware.RequirePermission(auth.PermModelUse, stores), titleH.GenerateTitle)

4. main.go — KB Search (kb.read)

Old:

		protected.POST("/knowledge-bases/:id/search", kbH.SearchKB)

New:

		protected.POST("/knowledge-bases/:id/search", middleware.RequirePermission(auth.PermKBRead, stores), kbH.SearchKB)

5. main.go — KB Update (kb.write)

Old:

		protected.PUT("/knowledge-bases/:id", kbH.UpdateKB)

New:

		protected.PUT("/knowledge-bases/:id", middleware.RequirePermission(auth.PermKBWrite, stores), kbH.UpdateKB)

6. main.go — KB Delete (kb.write)

Old:

		protected.DELETE("/knowledge-bases/:id", kbH.DeleteKB)

New:

		protected.DELETE("/knowledge-bases/:id", middleware.RequirePermission(auth.PermKBWrite, stores), kbH.DeleteKB)

7. main.go — KB Document Delete (kb.write)

Old:

		protected.DELETE("/knowledge-bases/:id/documents/:docId", kbH.DeleteDocument)

New:

		protected.DELETE("/knowledge-bases/:id/documents/:docId", middleware.RequirePermission(auth.PermKBWrite, stores), kbH.DeleteDocument)

8. main.go — KB Rebuild (kb.write)

Old:

		protected.POST("/knowledge-bases/:id/rebuild", kbH.RebuildKB)

New:

		protected.POST("/knowledge-bases/:id/rebuild", middleware.RequirePermission(auth.PermKBWrite, stores), kbH.RebuildKB)

9. main.go — New route: GET /profile/permissions

Insert AFTER the existing profile routes block. Find:

		protected.GET("/settings", settings.GetSettings)
		protected.PUT("/settings", settings.UpdateSettings)

Insert immediately after:

		// Permission bootstrap (v0.37.1) — self-service resolved permissions
		permH := handlers.NewProfilePermissionsHandler(stores)
		protected.GET("/profile/permissions", permH.GetMyPermissions)

10. integration_test.go — Mirror: Completions (model.use)

Old:

	protected.POST("/chat/completions", completions.Complete)

New:

	protected.POST("/chat/completions", middleware.RequirePermission(authpkg.PermModelUse, stores), completions.Complete)

11. integration_test.go — Mirror: KB Search (kb.read)

Old:

	protected.POST("/knowledge-bases/:id/search", kbH.SearchKB)

New:

	protected.POST("/knowledge-bases/:id/search", middleware.RequirePermission(authpkg.PermKBRead, stores), kbH.SearchKB)

12. integration_test.go — Mirror: KB Update (kb.write)

Old:

	protected.PUT("/knowledge-bases/:id", kbH.UpdateKB)

New:

	protected.PUT("/knowledge-bases/:id", middleware.RequirePermission(authpkg.PermKBWrite, stores), kbH.UpdateKB)

13. integration_test.go — Mirror: KB Delete (kb.write)

Old:

	protected.DELETE("/knowledge-bases/:id", kbH.DeleteKB)

New:

	protected.DELETE("/knowledge-bases/:id", middleware.RequirePermission(authpkg.PermKBWrite, stores), kbH.DeleteKB)

14. integration_test.go — Mirror: KB Doc Delete (kb.write)

Old:

	protected.DELETE("/knowledge-bases/:id/documents/:docId", kbH.DeleteDocument)

New:

	protected.DELETE("/knowledge-bases/:id/documents/:docId", middleware.RequirePermission(authpkg.PermKBWrite, stores), kbH.DeleteDocument)

15. integration_test.go — Mirror: KB Rebuild (kb.write)

Old:

	protected.POST("/knowledge-bases/:id/rebuild", kbH.RebuildKB)

New:

	protected.POST("/knowledge-bases/:id/rebuild", middleware.RequirePermission(authpkg.PermKBWrite, stores), kbH.RebuildKB)

16. integration_test.go — New route: GET /profile/permissions

Find this block in setupHarness:

	// Settings / Profile
	settings := NewSettingsHandler(stores, nil)

Insert BEFORE it:

	// Profile permissions (v0.37.1)
	permH := NewProfilePermissionsHandler(stores)
	protected.GET("/profile/permissions", permH.GetMyPermissions)

17. Existing KB Permission Test Update

The existing TestIntegration_KB_PermissionEnforcement test (line ~2808) tests kb.create denial. It still passes because it relies on TruncateAll wiping the Everyone group. No changes needed.

However, note that this test's comment says:

// (TruncateAll wipes the Everyone group, so no default permissions)

This is the only reason it works. On a live system with an intact Everyone group seeded with kb.read, the denial would not trigger for kb.read routes. The new perm_enforcement_test.go tests do NOT rely on Everyone group presence/absence — they work from a clean slate.


ICD Update

Add to docs/ICD/profile.md before the closing ---:

### Get My Permissions

\```
GET /profile/permissions
\```

**Auth:** Authenticated user

Returns the current user's resolved permission set, contributing group
IDs, team memberships, and UI-relevant policies. Admin users receive all
permissions.

**Response:**

\```json
{
  "permissions": ["model.use", "kb.read", "channel.create"],
  "groups": ["group-id-1", "00000000-0000-0000-0000-000000000001"],
  "teams": [
    { "id": "uuid", "name": "Engineering", "my_role": "member" }
  ],
  "policies": {
    "allow_user_byok": false,
    "allow_user_personas": false,
    "allow_raw_model_access": true,
    "kb_direct_access": true
  }
}
\```

| Field | Type | Description |
|-------|------|-------------|
| `permissions` | string[] | Resolved permission set (union of all groups) |
| `groups` | string[] | Contributing group IDs (always includes Everyone) |
| `teams` | object[] | User's active team memberships with role |
| `policies` | object | Boolean policy flags relevant to UI feature gating |

Audit Summary

Routes Now Enforcing RequirePermission

Route Permission Was Now
POST /chat/completions model.use Auth() only RequirePermission
POST /channels/:id/summarize model.use Auth() only RequirePermission
POST /channels/:id/generate-title model.use Auth() only RequirePermission
POST /knowledge-bases/:id/search kb.read Auth() only RequirePermission
PUT /knowledge-bases/:id kb.write Auth() only RequirePermission
DELETE /knowledge-bases/:id kb.write Auth() only RequirePermission
DELETE /knowledge-bases/:id/documents/:docId kb.write Auth() only RequirePermission
POST /knowledge-bases/:id/rebuild kb.write Auth() only RequirePermission

Routes Already Correctly Enforced (no changes)

Route Permission
POST /channels channel.create
POST /channels/:id/participants channel.invite
POST /personas persona.create
PUT /personas/:id persona.manage
DELETE /personas/:id persona.manage
POST /knowledge-bases kb.create
POST /knowledge-bases/:id/documents kb.write
POST /workflows workflow.create
PATCH /workflows/:id workflow.create
DELETE /workflows/:id workflow.create
All workflow stage routes workflow.create
POST /tasks task.create
PUT /tasks/:id task.create
DELETE /tasks/:id task.create
POST /tasks/:id/run task.create
POST /tasks/:id/kill task.create
Team tasks (scoped) task.create
All admin routes RequireAdmin()
Team-scoped routes RequireTeamAdmin()

Routes Correctly Using Auth() Only (no permission needed)

Read-only data for the authenticated user: channels list, messages, notes, projects, profile, settings, models/enabled, notifications, extensions, groups/mine, teams/mine, presence, folders, workspaces, files, memories, packages, surfaces, usage, search, export/me.

Everyone Group Default Permissions

The migration seeds Everyone with: model.use, kb.read, channel.create.

These three permissions mean that on a default deployment, the new enforcement on completions (model.use) and KB search (kb.read) will not change behavior — users already have these via Everyone.

The enforcement matters when an admin removes permissions from Everyone or creates restricted groups. Without these route-level gates, removing model.use from Everyone did nothing — users could still call completions.

New Tests

12 test functions in perm_enforcement_test.go:

Test Permission Pattern
TestPermEnforce_ModelUse_Completions model.use deny → grant → allow
TestPermEnforce_ChannelCreate channel.create deny → grant → allow
TestPermEnforce_ChannelInvite channel.invite deny → grant → allow
TestPermEnforce_KBCreate kb.create deny → grant → allow
TestPermEnforce_KBSearch kb.read deny → grant → allow
TestPermEnforce_KBWrite kb.write deny → grant → allow
TestPermEnforce_PersonaCreate persona.create deny → grant → allow
TestPermEnforce_PersonaManage persona.manage deny → grant → allow
TestPermEnforce_WorkflowCreate workflow.create deny → grant → allow
TestPermEnforce_TaskCreate task.create deny → grant → allow
TestPermEnforce_ProfilePermissions_NoGroups empty perms
TestPermEnforce_ProfilePermissions_WithGrant granted perms appear
TestPermEnforce_ProfilePermissions_Admin admin gets all

Not Covered (test harness limitation)

Summarize and GenerateTitle routes are not in the integration test harness (setupHarness). They share model.use with completions, which IS tested. Route-level wiring is applied in main.go. Full coverage comes when the test harness is extended to include these routes.