poc: Embedded OpenFGA for fine-grained authorization#335
poc: Embedded OpenFGA for fine-grained authorization#335edsonmichaque wants to merge 9 commits intomainfrom
Conversation
Add pkg/authz/ package with embedded OpenFGA server for fine-grained relationship-based authorization. This replaces the current boolean/JOIN-based access control model with a Zanzibar-style authorization system. Phase 1 delivers infrastructure with no behavioral changes: - model.fga: Authorization model with 13 types covering all resource relationships (system, user, group, catalogue, data_catalogue, tool_catalogue, llm, datasource, tool, app, chat, plugin, plugin_resource, submission, filter) - store.go: Embedded in-memory OpenFGA server with Check, ListObjects, WriteTuples - sync.go: FullSync scans 12 GORM tables to populate all relationship tuples - middleware.go: Gin middleware (RequireSystemAdmin, RequireCanUse, RequireRelation) - 18 integration tests verifying every relationship path in 57ms Addresses roadmap items from features/UserManagement.md: - Fine-grained permissions within groups (read, write, admin) - Group hierarchies for nested permissions - Resource ownership models - App sharing (features/Apps.md)
|
This pull request introduces a proof-of-concept for a new fine-grained authorization system using an embedded OpenFGA engine. It replaces the current authorization mechanism, which relies on boolean flags like The system is designed to be decoupled and incrementally adoptable. An Key components include:
Files Changed AnalysisThe changes are substantial, with over 2,800 lines of new code, but are well-encapsulated within a new
Architecture & Impact Assessment
Component Interaction Diagramgraph TD
subgraph "Startup"
DB[(GORM DB)] --|Reads 12 tables in batches|--> FullSync("authz/openfga.FullSync")
FullSync --|Populates graph|--> FGAStore{Embedded OpenFGA Store}
end
subgraph "Runtime"
ServiceLayer[Service Layer] --|e.g., user added to group|--> DBWrite("1. DB Write")
ServiceLayer --|Calls syncer method|--> Syncer("2. authz.Syncer")
Syncer --|Grants/revokes relationship|--> FGAStore
end
subgraph "API Request"
Request[HTTP Request] --> Middleware("authz.Middleware")
Middleware --|"Check(user, can_use, llm:5)"|--> Authorizer("authz.Authorizer")
Authorizer --|Evaluates against model & data|--> FGAStore
FGAStore --|Allowed/Denied|--> Authorizer
Authorizer --> Middleware
Middleware --|"Abort or c.Next()"|--> APIHandler
end
Scope Discovery & Context Expansion
Metadata
Powered by Visor from Probelabs Last updated: 2026-03-08T13:34:56.198Z | Triggered by: pr_updated | Commit: 68ded61 💡 TIP: You can chat with Visor using |
\n\n
Architecture Issues (5)
Performance Issues (2)
Quality Issues (2)
Powered by Visor from Probelabs Last updated: 2026-03-08T13:34:20.116Z | Triggered by: pr_updated | Commit: 68ded61 💡 TIP: You can chat with Visor using |
…tecture - Add OPENFGA_ENABLED env var toggle (disabled by default, legacy auth used) - Add NoopAuthorizer that always allows when OpenFGA is disabled - Add NewFromEnv() factory that creates Store or NoopAuthorizer based on config - Add ListObjectsStr() for types with non-numeric composite IDs (plugin_resource) - Change ListObjects() to return error (not silent skip) on non-numeric IDs - Add shadow mode middleware (ShadowCheckAdmin, ShadowCheckResource, ShadowCheckOwnership) that logs discrepancies without blocking requests - Add Enabled() to Authorizer interface for runtime feature detection - 27 tests passing (was 18)
- Rename Tuple → Relationship (Subject/Relation/Resource fields) - Rename WriteTuples/DeleteTuples → Grant/Revoke/GrantAndRevoke - Rename ListObjects → ListResources, ListObjectsStr → ListResourcesByName - Rename CheckStr → CheckByName, ResourceStr → ResourceByName - Rename helper functions: UserStr → SubjectUser, GroupStr → SubjectGroup, ObjectStr → ResourceID, ParseObjectID → ParseResourceNumericID, ParseObjectStr → ParseResourceID - Add ResourceByName() with colon validation to prevent injection - Add validateID() security check in plugin resource sync - Use first-colon delimiter in ParseResourceID (not last) for safety - Remove OpenFGA references from comments, logs, and variable names - Add detailed comments to model.fga for every type and relation - Middleware uses domain-neutral resourceType parameter naming - Shadow mode logs use "authz" instead of "openfga" in field names
Split the authorization package into abstract interface and implementation: - authz/ — Authorizer interface, Relationship type, helpers, NoopAuthorizer, middleware, shadow mode. No OpenFGA imports. - authz/openfga/ — Store implementation, model.fga, sync, config (NewFromEnv). All OpenFGA-specific code isolated here. This allows swapping the authorization backend without touching the interface or middleware. Consumers import authz for the interface and authz/openfga only at the composition root.
Syncer translates service-layer mutations into Grant/Revoke calls, keeping the authorization store in sync with the database after startup. Methods cover all mutation points: - User lifecycle: OnUserCreated, OnUserUpdated, OnUserDeleted - Group membership: OnUserAddedToGroup, OnUserRemovedFromGroup, OnGroupMembersReplaced - Catalogue assignments: OnCatalogueAssignedToGroup, OnCatalogueRemovedFromGroup - Resource-catalogue: OnResourceAddedToCatalogue, OnResourceRemovedFromCatalogue - Ownership: OnOwnershipSet, OnOwnershipChanged - App sharing: OnAppShared, OnAppUnshared - Chat groups: OnChatGroupAssigned, OnChatGroupRemoved - Submissions: OnSubmissionCreated, OnReviewerAssigned - Plugins: OnPluginInstalled, OnPluginResourceGroupAssigned/Removed All methods are no-ops when the authorizer is disabled.
Remove implementation-specific details from the abstract authz package: - Package doc no longer references OpenFGA or OPENFGA_ENABLED - Remove SubjectGroupMembers (#member is OpenFGA userset syntax) - Decouple middleware and shadow mode from models.User via UserIDFromContext - Remove domain-specific middleware (RequireSystemAdmin, RequireSSOAdmin, RequireCanUse, RequireCanAdmin) — keep only generic RequireRelation - Parameterize all shadow check functions instead of hardcoding relations
FullSync: - Stream database rows in batches (1000 rows) using keyset pagination - Grant relationships incrementally per batch instead of accumulating all - Consolidate 3 user queries in collectSystemRels into a single query - Extract reusable syncJoinTable/syncOwnerTable helpers to reduce duplication - Peak memory usage is now constant regardless of database size ListResources pagination: - Add ListResourcesPage and ListResourcesByNamePage to Authorizer interface - OpenFGA ListObjects has no native cursor pagination, so implementation fetches all results, sorts for stable ordering, and applies client-side windowing with offset-based tokens - Results from ListResources/ListResourcesByName remain bounded by the server's configured OPENFGA_LIST_OBJECTS_MAX_RESULTS (default 1000)
- Add 16 missing Syncer tests: OnUserRemovedFromGroup, OnCatalogueRemovedFromGroup, OnResourceAddedToCatalogue, OnResourceRemovedFromCatalogue, OnOwnershipSet, OnAppShared, OnAppUnshared, OnChatGroupAssigned, OnChatGroupRemoved, OnSubmissionCreated, OnReviewerAssigned, OnPluginInstalled, OnPluginResourceGroupAssigned, OnPluginResourceGroupRemoved, OnUserUpdated SSO promotion/demotion - Add 7 middleware tests: RequireRelation allowed/denied/no-user/invalid-id/error, RequireRelationByName allowed/denied - Add 6 shadow mode tests: ShadowCheck never-blocks/disabled/no-user, ShadowCheckResource never-blocks, ShadowCheckOwnership never-blocks/stores-result - Add noop pagination test All 21 Syncer methods now have test coverage (75 total tests).
Integration: - Add Authz and AuthzSyncer fields to services.Service - Add SetAuthorizer() method (follows existing setter pattern) - Close authorization backend in Service.Cleanup() User service hooks: - CreateUser: sync system membership, admin/SSO roles, group memberships - UpdateUser: detect admin/SSO promotion/demotion, sync group changes - DeleteUser: revoke all system-level roles - UpdateGroupUsers: sync group membership replacements Group service hooks: - AddUserToGroup / RemoveUserFromGroup - AddCatalogue/DataCatalogue/ToolCatalogue to group (and remove) Also: - Add SubjectGroupMembers to openfga package (OpenFGA-specific #member userset syntax needed for group-based app sharing) - Sort results in ListResourcesByNamePage for deterministic pagination
|
|
Summary
Proof-of-concept for embedded OpenFGA as the fine-grained authorization backend for AI Studio. Replaces the current boolean flags (
IsAdmin) and SQL JOIN-based group filtering with a unified relationship graph (Zanzibar-style ReBAC).Authorizerinterface with domain-neutral terminology (Subject/Relation/Resource), fully decoupled from OpenFGA specificsauthz/for interface + middleware (no OpenFGA imports),authz/openfga/for the implementationSyncertranslates service-layer mutations intoGrant/Revokecalls in real-time — no stale permissionsOPENFGA_ENABLEDenv var — when disabled,NoopAuthorizerdefers to legacy authRequireSystemAdmin,RequireSSOAdmin,RequireRelation,RequireCanUse,RequireCanAdminValidateID()and first-colon parsingAuthorization model access chain
Package structure
Syncer — incremental permission updates
The
Syncerkeeps the authorization store in sync after startup by providingmethods for every permission-relevant mutation in the service layer:
OnUserCreated,OnUserUpdated,OnUserDeletedOnUserAddedToGroup,OnUserRemovedFromGroup,OnGroupMembersReplacedOnCatalogueAssignedToGroup,OnCatalogueRemovedFromGroupOnResourceAddedToCatalogue,OnResourceRemovedFromCatalogueOnOwnershipSet,OnOwnershipChangedOnAppShared,OnAppUnsharedOnChatGroupAssigned,OnChatGroupRemovedOnSubmissionCreated,OnReviewerAssignedOnPluginInstalled,OnPluginResourceGroupAssigned/RemovedAll methods are no-ops when the authorizer is disabled.
Migration plan (6 phases)
Test plan
authz/andauthz/openfga/