Skip to content

fix(api): restore default org roles in Better Auth access control#35

Open
natori-hrj wants to merge 1 commit into
oblien:mainfrom
natori-hrj:fix/org-default-roles
Open

fix(api): restore default org roles in Better Auth access control#35
natori-hrj wants to merge 1 commit into
oblien:mainfrom
natori-hrj:fix/org-default-roles

Conversation

@natori-hrj

Copy link
Copy Markdown

Problem

A self-hosted instance cannot invite anyone. On a fresh install, the owner of the auto-created workspace gets:

POST /api/auth/organization/invite-member
403 {"code":"YOU_ARE_NOT_ALLOWED_TO_INVITE_USERS_TO_THIS_ORGANIZATION"}

The UI shows a failed toast and no invitation row is written.

Cause

Better Auth 1.5.4 resolves the organization role table as:

// node_modules/better-auth/dist/plugins/organization/has-permission.mjs
let acRoles = { ...input.options.roles || defaultRoles };

options.roles || defaultRoles is evaluated before the spread, so supplying roles REPLACES the plugin's defaults rather than extending them.

apps/api/src/lib/auth.ts registers only restricted:

roles: {
  // The plugin's default roles still apply (owner/admin/member);
  restricted: RESTRICTED_ROLE,
},

That comment's premise does not hold — owner, admin and member end up with no statements at all, so every hasPermission check denies them.

Blast radius — lines matching hasPermission in the org plugin's routes:

route file matches
crud-access-control.mjs 8
crud-team.mjs 6
crud-invites.mjs 3
crud-members.mjs 3
crud-org.mjs 3

Fix

Spread defaultRoles back in so restricted is genuinely a fourth role, and replace the comment with one that documents the replace-not-extend behaviour.

Verification

Self-hosted mode (CLOUD_MODE=false) on Postgres, built with docker compose up -d --build. The same image was rebuilt from the parent commit to measure the "before" column, so both columns are observed rather than inferred. Each row is POST /api/auth/organization/invite-member by the sole workspace member, with that member's role set to the value in column 1:

member role before after expected
owner 403 200 allowed
admin 403 200 allowed
member 403 403 denied
restricted 403 403 denied
unauthenticated 401 401 denied

After the fix, owner and admin write an invitation row (status = pending), and list-members / list-invitations return 200 for the owner.

member and restricted still answer {"error":"Forbidden"} — that is middleware/better-auth-shield.ts rejecting them before the plugin runs, not the plugin's own YOU_ARE_NOT_ALLOWED_…. So the shield's role policy is unchanged by this patch, and restricted keeps zero plugin-side permissions.

bun install --frozen-lockfile and bun run --cwd apps/api lint (the CI typecheck) both pass.

Notes

No test is included. The two tests that reference lib/auth (github.auth.test.ts, github.controller.test.ts) vi.mock it rather than loading the real module — it builds betterAuth() at module scope with the DB adapter — so there is no existing pattern in apps/api/test for exercising this config. Happy to add a test if you'd like a particular shape.

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.

1 participant