Lewati ke konten utama
Versi: v0.0.27

Permissions Reference

Honeyframe is in the middle of a transition between two authorization layers:

  • require_role(*roles) (legacy) — flat per-user role string stored on hubstudio.users.role. Most existing endpoints still gate on this.
  • require_permission("<resource>.<action>", target_param?) (new) — group-based check resolved by services/permissions.user_has_permission() against hubstudio.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):

  1. user.is_superadmin == true
  2. user.role == 'admin' (legacy compatibility shim)
  3. The user belongs to a group with a matching group_permissions row (specific target_id or target_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:

RoleTypical use
adminFull org access. Bypasses all permission checks via the legacy shim.
managementRead/manage business-side resources (dashboards, reports).
editorCreate and modify datasets, dashboards, recipes.
viewerRead-only.
cs_staffCustomer-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.

PermissionScopeMeaning
org.adminorgFull org-level admin (already wired).
project.admin / project.edit / project.viewobject (project_id)Per-project authorization tier.
dashboard.view / dashboard.editobject (dashboard_id)Per-dashboard authorization tier (dashboard.edit already wired).
dataset.read / dataset.readwriteobject (dataset_id)Per-dataset authorization tier.
feature.<feature_name>orgGeneric 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:

  1. The dependency reads dashboard_id=7 from the path.
  2. user_has_permission(user, "dashboard.edit", "7") is called.
  3. Resolver returns true if any of:
    • user.is_superadmin
    • user.role == 'admin'
    • Some group the user belongs to has a row in group_permissions with permission_type='dashboard.edit' and (target_id='7' OR target_id IS NULL).
  4. Otherwise the request returns 403.

Migrating from require_role to require_permission

When converting an endpoint:

  1. Identify which roles previously had access (require_role("admin", "editor") → admins and editors).
  2. 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.
  3. Replace the dependency: require_role(...)require_permission("dataset.readwrite", "dataset_id").
  4. Seed the corresponding group_permissions rows for groups the legacy roles used to map to (adminorg.admin; resource roles → <resource>.<action> per the new strings).
  5. Keep require_role in place during the transition; the resolver's role == 'admin' shim means migrated endpoints still pass for legacy admins.