docs / access model

Access model

How Fred decides who may do what: Keycloak identity, relationship-based authorization (ReBAC / OpenFGA) as the single enforcement mechanism, and a display-only role catalog for the UI.

Authorization in one line

Authentication is a Keycloak JWT (OIDC), validated on every request. Authorization is ReBAC (OpenFGA), and only ReBAC — evaluated at every endpoint against the caller's relationships to the object (their team, the resource owner, the tag hierarchy).

Roles are not the enforcement layer. The capability flags further down are display-only: the frontend uses them to gate which menus and buttons a user sees. They are never used to allow or deny an operation on the server — that is done exclusively by a ReBAC check. (The former RBAC enforcement path was removed under AUTHZ-01.)

Source: fred-core security/authorization.py · security/rebac/schema.fga

Identity & claims

A validated token is decoded into a KeycloakUser:

Token claimKeycloakUser fieldUse
subuidStable user identity key
preferred_usernameusernameDisplay and tracing
emailemailUser profile
resource_access[client].rolesrolesIdentity marker only — used to recognize service_agent (the evaluation worker); org/team roles are never derived from this claim

There is no groups claim on KeycloakUser. Team membership is not derived from the JWT at all — platform_admin/platform_observer and team_admin/team_editor/team_analyst/team_member are stored OpenFGA relations, granted through the bootstrap endpoint or an explicit admin action (see ReBAC below).

Source: fred-core security/oidc.py (decode_jwt) · security/structure.py (KeycloakUser)

ReBAC — the enforcement model

Fred's OpenFGA schema defines object types, the relations users/teams can hold on them, and the permissions those relations grant. Every protected endpoint resolves to one of these checks.

Organization

Relations: platform_admin, platform_observer (= [user] or platform_admin). Both are stored tuples only — granted through the one-time root bootstrap (POST /bootstrap/platform-admin) or an explicit admin action — never derived from a Keycloak role or group. Global capabilities are organization permissions:

PermissionGranted to
can_create_team · can_list_all_teams · can_delete_team · can_rescue_team_adminplatform_admin
can_edit_agent_class_pathplatform_admin
can_manage_platform · can_administer_users · can_run_benchmarkplatform_admin
can_observe_platformplatform_observer
AUTHZ-05 removed the legacy Keycloak admin/editor/viewer bridge and the "any connected user" organization tier (can_create_agent, can_read_kpi, can_read_logs, can_read_metrics, can_read_opensearch, can_read_content, can_process_content) outright — those call sites now rely on authentication alone, not an organization-level ReBAC check. A separate can_read_kpi_global was also retired as a duplicate of can_observe_platform.

Team

team_admin and team_editor are orthogonal, not hierarchical: team_admin has full governance authority (membership, roles) and no agent/prompt authority by default; team_editor is the reverse. team_analyst is a separate, narrower role for evaluation work. A public marker grants read-only team-info visibility to anyone, never data/agent/conversation access.

Agent, tag, document, resource

Source: fred-core security/rebac/schema.fga · security/rebac/rebac_engine.py

Frontend capability flags

There is no server-side "display catalog" table anymore. permission_catalog.py and its static ROLE_CAPABILITIES role→resource matrix were removed under AUTHZ-05 — they mirrored Keycloak role names (admin/editor/viewer) that no longer exist. The frontend now derives capability booleans directly, per request, from the same OpenFGA relations described above.

Two hooks, no matrix

The only role-string check left anywhere in this path is service_agent — an identity marker on the JWT (KeycloakUser.roles), not a ReBAC relation, that lets the evaluation worker act within a scoped team_id.

Source: fred-core security/structure.py (SERVICE_AGENT_ROLE) · control-plane teams/service.py, product/schemas.py · frontend rework/core/hooks/useUserCapabilities.ts, useTeamCapabilities.ts

Endpoint enforcement

Authentication is a Keycloak JWT everywhere. Authorization is a ReBAC check keyed to the surface:

SurfaceAuthorization (ReBAC)
control-plane · teamsTeam permissions — can_read, can_update_info, can_read_members, can_administer_*
control-plane · platform, import/export, agent class pathOrganization can_manage_platform
control-plane · global KPIsOrganization can_observe_platform
control-plane · sessions & historyTeam + owner scoping — session.user_id == uid and matching team_id
control-plane · evaluationsTeam can_update_agents / can_read
fred-agents · execute / streamPod-side team can_read on a mandatory team_id, per request
knowledge-flow · tags, documents, resourcesObject-level tag / document / resource permissions
knowledge-flow · KPIs, opensearch statisticsOrganization can_observe_platform
knowledge-flow · logs, task statusAuthentication only — no ReBAC check (AUTHZ-05 removed the "any connected user" organization tier)
health / readinessPublic — no auth

Source: control-plane teams/service.py, product/service.py, evaluations/api.py · knowledge-flow features/* · fred-runtime app/agent_app.py

Execution authorization

Agent execution is team-scoped and authorized at the pod. There is no signed grant or capability token: the control plane issues none. Identity is the Keycloak JWT, and the agent pod runs an OpenFGA check on the caller's team_id for every request before executing. The ExecutionGrantAction values (execute, resume) are action labels, not tokens.

Source: fred-sdk contracts/execution.py · fred-runtime app/agent_app.py

ReBAC-disabled mode

When OIDC is off or ReBAC is disabled, Fred loads a no-op engine: has_permission() returns True, and lookups return a "disabled" marker so services skip object-level filtering. The deployment is effectively open — intended for local development, never production.

Source: fred-core security/rebac/noop_engine.py · security/rebac/rebac_factory.py