Permissions Reference
Honeyframe is in the middle of a transition between two authorization layers:
require_role(*roles)(legacy) — flat per-user role string stored onhoneyframe.users.role. Most existing endpoints still gate on this.require_permission("<resource>.<action>", target_param?)(new) — group-based check resolved byservices/permissions.user_has_permission()againsthoneyframe.group_permissions. New endpoints are written against this; old ones are migrated opportunistically.
Both layers are honored simultaneously. The resolver allows a request when any of the following is true (in order):
user.is_superadmin == trueuser.role == 'admin'(legacy compatibility shim)- The user belongs to a group with a matching
group_permissionsrow (specifictarget_idortarget_id IS NULL)
If none match, the request returns 403 Forbidden with a JSON body of { "error": "permission_denied", "permission": "...", "target_id": "..." }.
Layer 1 — Roles
The role column on honeyframe.users holds one of (v0.0.54+):
| Role | Typical use |
|---|---|
admin | Full org access. Bypasses all permission checks via the resolver shim. |
designer | Power user / flow author. Builds datasets, recipes, dashboards, agents. |
analyst | Visual-only consumer. Reads dashboards, builds dashboards/queries, no ingestion edits. |
viewer | Read-only. |
The legacy require_role() gate was retired in PaaS in v0.0.54 — all 557 router callsites now go through require_permission() (see below). require_role() is kept defined only for SaaS vertical routers until they get their own migration.
The role == 'admin' shim in the resolver stays as a permanent per-request optimization (Administrators groups hold org.admin via the v0.0.54 seed; the shortcut just saves a DB roundtrip). Implicit-grant shims also map designer/analyst/viewer onto their seeded permissions so tenants where user_groups hasn't been backfilled yet still resolve correctly.
Layer 2 — Permission strings
Every permission Honeyframe checks at this layer is a string of the form <resource>.<action>. The resolver lives at services/permissions.user_has_permission() and is invoked via the FastAPI dependency require_permission(permission_type, target_param=None).
Catalog (today)
Every gate in the migrated PaaS routers (v0.0.54+) resolves to one of the strings below.
org.admin — full administrative access to the organization. Required for all /api/groups mutations + every administration surface (audit, branding, connectors before v0.0.55, infrastructure, invites, lineage, notifications, org_backup, org_oidc, org_saml, org_smtp, org_usage, org_webhooks, organizations, plugins, projects, site_auth, storage, system_health, users).
project.edit — designer-or-better paths: every endpoint that previously gated on the legacy ("admin", "designer") tuple (publishing, dashboards, dataset readwrite, chat, column_lineage, data_dictionary, datasets, flow_ai, knowledge, knowledge_bases, lineage, sync_recipes).
project.view — broad-read paths previously gated on OVERVIEW_ROLES (analytics overview, etc.). Held by analyst + viewer org-wide via the v0.0.54 seed.
dashboard.edit — modify a dashboard's tiles, layout, or settings. Used as require_permission("dashboard.edit", "dashboard_id") so the check is per-dashboard.
dashboard.view — view a dashboard and run its cards.
connector.read / connector.edit — split off org.admin in v0.0.55 so designers can manage data sources without full org-admin. See Connectors → Permission model.
dataset.read / dataset.readwrite — per-dataset authorization tier.
feature.<feature_name> — generic capability gate. feature.agent_builder and feature.chat are the wired entries today.
Catalog (planned)
The resolver's docstring documents the intended schema as the migration off require_role proceeds. These strings are reserved — implementations should use them rather than inventing new shapes.
| Permission | Scope | Meaning |
|---|---|---|
org.admin | org | Full org-level admin (already wired). |
project.admin / project.edit / project.view | object (project_id) | Per-project authorization tier. |
dashboard.view / dashboard.edit | object (dashboard_id) | Per-dashboard authorization tier (dashboard.edit already wired). |
dataset.read / dataset.readwrite | object (dataset_id) | Per-dataset authorization tier. |
feature.<feature_name> | org | Generic capability gate. feature.agent_builder is the only one wired today. |
Granting a permission
A row in honeyframe.group_permissions looks like:
INSERT INTO honeyframe.group_permissions (group_id, permission_type, target_id)
VALUES (
42, -- the group
'dashboard.edit', -- the permission string
'7' -- the dashboard_id this grant applies to
);
Set target_id to NULL to grant the permission organization-wide (any dashboard, any project, etc.). The resolver matches a specific target_id first and falls back to NULL grants.
Group memberships live in honeyframe.user_groups. A user inherits the union of all permissions across every group they belong to.
Resolution order
When require_permission("dashboard.edit", "dashboard_id") runs on a request to DELETE /api/dashboards/7:
- The dependency reads
dashboard_id=7from the path. user_has_permission(user, "dashboard.edit", "7")is called.- Resolver returns
trueif any of:user.is_superadminuser.role == 'admin'- Some group the user belongs to has a row in
group_permissionswithpermission_type='dashboard.edit'and (target_id='7'ORtarget_id IS NULL).
- Otherwise the request returns
403.
Migrating from require_role to require_permission
When converting an endpoint:
- Identify which roles previously had access (
require_role("admin", "editor")→ admins and editors). - Pick a
<resource>.<action>string from the catalog above. Use the planned schema if possible; only mint a new string if no existing one fits. - Replace the dependency:
require_role(...)→require_permission("dataset.readwrite", "dataset_id"). - Seed the corresponding
group_permissionsrows for groups the legacy roles used to map to (admin→org.admin; resource roles →<resource>.<action>per the new strings). - Keep
require_rolein place during the transition; the resolver'srole == 'admin'shim means migrated endpoints still pass for legacy admins.