Permissions Reference
Honeyframe is in the middle of a transition between two authorization layers:
require_role(*roles)(legacy) — flat per-user role string stored onhubstudio.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()againsthubstudio.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 — Legacy roles
The role column on hubstudio.users holds one of:
| Role | Typical use |
|---|---|
admin | Full org access. Bypasses all permission checks via the legacy shim. |
management | Read/manage business-side resources (dashboards, reports). |
editor | Create and modify datasets, dashboards, recipes. |
viewer | Read-only. |
cs_staff | Customer-success scope (vertical-specific, e.g. healthcare 360). |
Endpoints gate on roles via:
@router.post("/some-endpoint", dependencies=[Depends(require_role("admin", "editor"))])
The role string lives on the user record and is checked directly — no group lookup happens.
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)
The real catalog is small — only the strings below are referenced by require_permission(...) calls in the product backend as of v0.0.x. Treat any other string as planned-but-not-wired.
org.admin — full administrative access to the organization. Required for all /api/groups mutations (create, update, delete, add/remove members, set/unset permissions).
dashboard.edit — modify a dashboard's tiles, layout, or settings. Used as require_permission("dashboard.edit", "dashboard_id") so the check is per-dashboard.
feature.agent_builder — example of a generic capability gate. The feature.* prefix denotes a non-resource permission used to flag whether a user can access a product surface.
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 hubstudio.group_permissions looks like:
INSERT INTO hubstudio.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 hubstudio.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.