Skip to content

feat(auth): centralized RBAC — roles, permissions, and route protection#4230

Open
Another-DevX wants to merge 12 commits into
ava-labs:masterfrom
Voyager-Ship:feat/roles-and-permissions
Open

feat(auth): centralized RBAC — roles, permissions, and route protection#4230
Another-DevX wants to merge 12 commits into
ava-labs:masterfrom
Voyager-Ship:feat/roles-and-permissions

Conversation

@Another-DevX

@Another-DevX Another-DevX commented May 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Replaces the scattered custom_attributes.includes(role) pattern with a centralized
Role-Based Access Control system. All authorization now flows through a single
permission model backed by a UserRole DB table instead of a string array on User.

Implementation

Permission model (lib/auth/rolePermissions.ts)

Single source of truth for all roles. Each role maps to a list of
{ resource, action } pairs. Resources follow a namespace:subnamespace
convention (badge:nft, etc.). Actions are read | write | delete | manage | admin | export | assign.
manage implies all CRUD actions; resource "*" matches any resource.
getPermissionsFromRoles() is memoized per process to avoid rebuilding sets on
every request.

Route manifest (lib/auth/routeManifest.ts)

Declarative map of URL prefixes to required resources. The middleware in
proxy.ts reads this map to enforce auth at the edge before any handler runs.
Mixed routes (e.g. POST /api/events is public, but PUT requires auth) are
handled at the handler level via withAuthPermission / withAuthResource HOFs.

Database

New UserRole table (user_id, role, expires_at, granted_by) replaces
custom_attributes: String[] on User. The JWT callback now reads roles
exclusively from UserRole rows. Migration includes a backfill that populates
UserRole from existing custom_attributes values so no user loses access.
Composite index on (user_id, expires_at) covers the query in the JWT callback.

Callsite migration

  • Deleted lib/auth/permissions.ts; replaced hasAnyRole / withAuthRole with hasPermission()
  • Removed User.custom_attributes merge from JWT callback
  • Added isHackathonJudge and canEvaluateHackathon helpers to lib/auth/roles.ts
  • Migrated 13 API routes and 14 UI files from .includes(role) to hasPermission()
  • Legacy helpers (hasShowcaseRole, hasHackathonEditorRole, etc.) kept but marked @deprecated

Role additions & permission corrections

  • Added Team1-Leader, Team1-member, T1-Technical with builder_insights:read
  • Added showcase:export action; hackathonCreator now gets showcase:export
  • /api/projects/export and ShowCaseCard guard updated to showcase:export
  • platform:admin added to superadmin and devrel for strictly internal gates
  • badge/console-migrate restricted to platform:admin
  • Ownership check on PUT / PATCH /api/events/[id] — creators can only edit their own hackathons
  • admin/user-roles: Zod validation, expires_at > now check, anti-escalation guard, structured JSON audit logs
  • CORS: proxy.ts uses NEXTAUTH_URL allowlist instead of wildcard *
  • evaluate/* returns 403 on permission failures (was 401)
  • custom_attributes excluded from judge API responses

Docs

docs/auth/roles-and-permissions.md — role→permission table, action semantics,
route manifest usage, and Admin API reference.

@vercel

vercel Bot commented May 25, 2026

Copy link
Copy Markdown

Someone is attempting to deploy a commit to the Ava Labs Team on Vercel.

A member of the Team first needs to authorize it.

@Another-DevX Another-DevX force-pushed the feat/roles-and-permissions branch from 46e0f50 to 2edc509 Compare May 25, 2026 20:32
@Another-DevX Another-DevX changed the title [WIP] roles and permissions [WIP] feat(auth): centralized RBAC — roles, permissions, and route protection May 27, 2026
@Another-DevX Another-DevX changed the title [WIP] feat(auth): centralized RBAC — roles, permissions, and route protection feat(auth): centralized RBAC — roles, permissions, and route protection May 27, 2026
@Another-DevX Another-DevX changed the title feat(auth): centralized RBAC — roles, permissions, and route protection [WIP] feat(auth): centralized RBAC — roles, permissions, and route protection May 27, 2026
@dihnometry

Copy link
Copy Markdown
Contributor

Permission Test Script

Automated permission/role testing script for route-level access control.
Runs a series of HTTP requests against the running app and verifies that
each role is correctly allowed or denied on every protected route.

Requirements

  • Node.js 18+
  • The app running locally (yarn dev)

Setup

Create a .env.permissions file in the project root:

BASE_URL=http://localhost:3000
DEVREL_EMAIL=your-superadmin@example.com
DUMMY_EMAIL=your-dummy@example.com
DUMMY_USER_ID=the-dummy-account-db-id

The script uses two accounts:

  • Superadmin (DEVREL_EMAIL): must have devrel or superadmin role.
    Used to assign and remove roles from the dummy account.
  • Dummy (DUMMY_EMAIL): a plain account with no roles. Roles are
    temporarily assigned to it during each test cycle.

Running the script

node scripts/permission_test.js

The script will prompt for OTP codes as needed. Since OTPs are generated
on demand by /api/send-otp, check the Next.js server logs for the code
each time it is requested.

How it works

  1. Authenticates as the superadmin.
  2. For each role in the test matrix, runs a full cycle:
    • Assigns the role to the dummy account
    • Prompts for a new OTP and re-authenticates the dummy account
      (required because role changes are not reflected in an existing
      JWT session — the dummy must log in again to get a token with
      the updated roles)
    • Runs all route tests with the dummy session
    • Removes the role from the dummy account
  3. Also runs a cycle with no role assigned, to verify that
    unauthenticated requests are correctly rejected.

Test logic

Each route is tested from two angles:

  • Should allow — a request made with a session that has permission.
    Any response other than 401/403 is a pass. The payload is
    intentionally invalid; what matters is that the auth layer does not
    block the request.
  • Should deny — a request made with a session that lacks permission.
    Only 401 or 403 is a pass.

UI routes (/showcase, /events/new, etc.) are tested via GET with
redirect: manual. A 307 redirect to / is treated as a deny.

@Another-DevX Another-DevX force-pushed the feat/roles-and-permissions branch 4 times, most recently from d004098 to 5e85866 Compare June 2, 2026 23:19
Anotherdev and others added 8 commits June 2, 2026 18:24
…all callsites

- Remove User.custom_attributes merge from JWT callback; roles now from UserRole rows only
- Delete lib/auth/permissions.ts; all helpers inlined as hasPermission() calls
- Remove hasAnyRole and withAuthRole; add isHackathonJudge and canEvaluateHackathon to roles.ts
- Add platform:admin permission to superadmin and devrel for strictly devrel-only gates
- Add builder_insights:write permission to builder_insights role
- Migrate 13 API routes and 14 UI files from .includes(role) to hasPermission()
- Update docs/auth/roles-and-permissions.md to reflect new data flow and permission map

Signed-off-by: Anotherdev <anotherdev@MacBook-Pro-de-Anotherdev.local>
- Add Team1-Leader, Team1-member, T1-Technical roles with builder_insights:read
- Add showcase:export action type and permission to hackathonCreator
- Fix export showcase gate (was platform:admin, now showcase:export)
- Update ShowCaseCard and /api/projects/export to use showcase:export

Signed-off-by: Anotherdev <anotherdev@MacBook-Pro-de-Anotherdev.local>
Signed-off-by: Anotherdev <joseluismanco37@gmail.com>
- judge: replace judge:write with judge:assign; only devrel/superadmin/
  team1-admin can add or remove judges from hackathons
- events/[id]: add ownership check in PUT/PATCH — hackathonCreator can
  only edit their own hackathons unless actor has hackathon:manage
- console-migrate: restrict to platform:admin (was badge:manage, which
  included badge_admin unintentionally)
- UI pages /events/new and /hackathons/new: relax guard from platform:admin
  to hackathon:write so hackathonCreator can reach them
- admin-panel page: add ownership check matching the API guard
- judges page: change guard from platform:admin to judge:assign
- admin/user-roles: add Zod validation, expires_at > now check,
  anti-escalation (cannot grant roles with wider permissions than own),
  and structured JSON audit logs on assign/revoke
- proxy.ts: replace CORS wildcard with NEXTAUTH_URL allowlist
- routeManifest: add /api/evaluate and /api/evaluate/* (judge:read)
- rolePermissions: add builder_insights to Resource union, drop | string,
  add assign action, replace action:* with action:manage in superadmin/devrel,
  memoize getPermissionsFromRoles
- evaluate/*: fix 401 → 403 on permission failure
- schema: add @@index([user_id, expires_at]) on UserRole
- docs: fix merge claim, usertole typo, update role/action tables,
  remove custom_attributes from judges API response

Signed-off-by: Anotherdev <joseluismanco37@gmail.com>
- Rename Resource type "hackathon" → "event" across all RBAC consumers
  (rolePermissions.ts, routeManifest.ts, roles.ts, all route handlers,
   edit pages, hackathon/event list components, UserButton)
- Fix routeManifest: /api/projects/export now maps to resource "showcase"
  instead of "hackathon" (aligns with actual handler check showcase:export)
- Docs: add "How to Extend the System" section to roles-and-permissions.md
  covering how to add new roles, resources, and actions with code examples

Signed-off-by: Anotherdev <joseluismanco37@gmail.com>
…issions

Signed-off-by: Anotherdev <joseluismanco37@gmail.com>
Replace getServerSession(AuthOptions) + manual permission checks with
withAuthPermission / withAuth from lib/protectedRoute:

- create: withAuthPermission({ resource: notification, action: write })
  also removes unnecessary getUserById DB call
- get: withAuth (session-only check)
- read: withAuth (session-only check)

Signed-off-by: Anotherdev <joseluismanco37@gmail.com>
@Another-DevX Another-DevX force-pushed the feat/roles-and-permissions branch 2 times, most recently from 7898a10 to 0de5d1d Compare June 2, 2026 23:43
…honCreator

Add explicit action override support to RouteConfig so the proxy
does not always infer the action from the HTTP method. Set
action: "export" on the /api/projects/export manifest entry and
replace withAuth + manual hasPermission check in the handler
with withAuthPermission({ resource: "showcase", action: "export" }).
@Another-DevX Another-DevX changed the title [WIP] feat(auth): centralized RBAC — roles, permissions, and route protection feat(auth): centralized RBAC — roles, permissions, and route protection Jun 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants