Skip to content

feat(core): organization roles for organization-owned records - #683

Open
seanspeaks wants to merge 6 commits into
claude/internal-mcp-05-scope-ownershipfrom
claude/internal-mcp-05b-org-roles
Open

seanspeaks wants to merge 6 commits into
claude/internal-mcp-05-scope-ownershipfrom
claude/internal-mcp-05b-org-roles

Conversation

@seanspeaks

@seanspeaks seanspeaks commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Only an organization's admins can manage what the organization owns; members can use it. This PR is stacked on #682 (entity scopes), the second of three.

What it adds

  • authSource on User, set explicitly by each AuthenticateUser branch: shared-secret (x-frigg headers), adopter-jwt or bearer-token. It is never inferred from a null role.

  • canManageOrganization:

    • A shared-secret caller manages. It already holds the app's secret and could mint any user.
    • A token caller manages only when its role is in user.orgAdminRoles (default ['ADMIN'], case-insensitive). A token with no role claim is a MEMBER, so a missing role is never treated as admin.
  • user.orgAdminRoles is validated at app-definition load and in the schema.

  • Gates return 403 ORG_ADMIN_REQUIRED on:

    • creating an org-owned entity or integration;
    • deleting an entity;
    • integration PATCH and DELETE (v1 and v2);
    • credential delete and reauthorize.

    They run through one shared assertCanManage (modules/use-cases/manage-access.js) and the ACCESS constants.

Behaviour change

Adopters using adopter-JWT or bearer tokens whose tokens carry no role claim lose org-level management until they send a role in orgAdminRoles, or configure it. Header (shared-secret) callers are unaffected.

Test plan

  • Core suite (no-op global setup): 14 suites / 61 tests fail, the same set as base by name; 3421 pass.
  • packages/schemas: 199 pass.
  • ESLint: 0 errors.
  • 41 of 41 mutations killed. These cover the auth source on each branch, shared-secret trust, a missing role, case-insensitivity, an unknown source, configured roles, orgAdminRoles validation, every gate and assertCanManage.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6


Generated by Claude Code


Note

High Risk
Changes authorization for organization-owned resources and alters behavior for adopter-JWT/bearer callers without role claims; misconfiguration of orgAdminRoles or gateBearerTokens could block legitimate admins or leave tokens over-privileged.

Overview
Introduces organization admin roles so members can use organization-owned entities, integrations, and credentials, but only admins (or trusted callers) can create, change, or delete them.

Authentication & policy: AuthenticateUser now sets explicit authSource on User (shared-secret, adopter-jwt, bearer-token). Scope visibility derives canManageOrg via canManageOrganization (token role vs stored membership orgRole, default MEMBER when missing). App config adds user.orgAdminRoles (default ['ADMIN']) and optional user.orgAdminGateBearerTokens. List/access metadata now returns manage vs use for organization rows.

Enforcement: Shared assertCanManage returns 403 ORG_ADMIN_REQUIRED when access is not manage. Applied on org-level authorize/create, entity delete, integration PATCH/DELETE (v1/v2 via { manage: true }), and credential delete/reauthorize (scopePolicy.accessToOwnedBy). Adopter-JWT callers without a role claim lose org-level management unless their membership role qualifies; Frigg bearer tokens stay ungated by default; shared-secret callers still manage.

Docs, schema, and types updated; broad test coverage for gates and role resolution.

Reviewed by Cursor Bugbot for commit 8a84ab1. Bugbot is set up for automated code reviews on this repo. Configure here.

📦 Published PR as canary version: 2.0.0--canary.683.8a84ab1.0

✨ Test out this PR locally via:

npm install @friggframework/admin-scripts@2.0.0--canary.683.8a84ab1.0
npm install @friggframework/core@2.0.0--canary.683.8a84ab1.0
npm install @friggframework/devtools@2.0.0--canary.683.8a84ab1.0
npm install @friggframework/eslint-config@2.0.0--canary.683.8a84ab1.0
npm install @friggframework/prettier-config@2.0.0--canary.683.8a84ab1.0
npm install @friggframework/schemas@2.0.0--canary.683.8a84ab1.0
npm install @friggframework/serverless-plugin@2.0.0--canary.683.8a84ab1.0
npm install @friggframework/test@2.0.0--canary.683.8a84ab1.0
npm install @friggframework/ui@2.0.0--canary.683.8a84ab1.0
# or 
yarn add @friggframework/admin-scripts@2.0.0--canary.683.8a84ab1.0
yarn add @friggframework/core@2.0.0--canary.683.8a84ab1.0
yarn add @friggframework/devtools@2.0.0--canary.683.8a84ab1.0
yarn add @friggframework/eslint-config@2.0.0--canary.683.8a84ab1.0
yarn add @friggframework/prettier-config@2.0.0--canary.683.8a84ab1.0
yarn add @friggframework/schemas@2.0.0--canary.683.8a84ab1.0
yarn add @friggframework/serverless-plugin@2.0.0--canary.683.8a84ab1.0
yarn add @friggframework/test@2.0.0--canary.683.8a84ab1.0
yarn add @friggframework/ui@2.0.0--canary.683.8a84ab1.0

… §3 permissions)

Anyone in an organization may use its entities, integrations and credentials;
only an organization admin may create, change or delete them.

- User carries an explicit auth source (shared-secret | adopter-jwt |
  bearer-token) set by AuthenticateUser. A shared-secret caller is the app's
  own backend and manages. A token caller has the role the token carries, or
  MEMBER: a missing role never means admin.
- user.orgAdminRoles (default ['ADMIN'], case-insensitive) names the admin
  roles; validated at app-definition load and in the schema.
- Gates (403 ORG_ADMIN_REQUIRED): organization entity and integration create,
  entity delete, integration PATCH/DELETE (v1 and v2), credential delete and
  re-authorize. One shared check, assertCanManage; access comes from
  describeRecord or scopePolicy.accessToOwnedBy for credentials.
- Docs: entity-scopes guide, organization-memberships guide, core README.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Code review skipped — your organization has no extra usage available to pay for this review.

If your organization's extra usage balance is empty, an organization admin can add extra usage credits at claude.ai/admin-settings/usage. If its monthly spend limit was reached, an admin can raise it on the same page. If neither applies, contact Anthropic support.

Once extra usage is available, someone with write access to this repository can comment @claude review on this pull request to trigger a review.

@seanspeaks seanspeaks added release Create a release when this pr is merged prerelease This change is available in a prerelease. labels Oct 16, 2026 — with Claude

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f45da33. Configure here.

…ernal-mcp-05b-org-roles

Brings in the DocumentDB list drain, the v1 POST /api/entity owner
resolution and the owner-built v1 entity modules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
With the owner of POST /api/entity resolved through
AuthorizeEntityForUser.resolveOwnership (merged from 05), the 05b gate in
resolveOwnerForScope applies to it as it does to /api/authorize: a member
token, or a token with no role, gets 403 ORG_ADMIN_REQUIRED for an
organization type before any credential or module is touched. An admin
token may create it, and a member may still create their own entity.
Closes the open Bugbot thread on #683.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
…earer tokens keep pre-roles behaviour

05b treated every caller whose auth source carries no role as MEMBER.
Frigg bearer tokens never carry one, so in an organization-primary app
(default scope organization) a Frigg-UI user could no longer connect
anything, a regression for existing adopters. And
OrganizationMembership.orgRole was stored but never read.

canManageOrganization now decides:
- shared secret: always (unchanged);
- Frigg bearer token: always, as before roles existed, unless the app sets
  user.orgAdminGateBearerTokens: true (new, validated at load and in the
  schema); then judged like the rest;
- otherwise the token's role claim when it carries one, else the stored
  orgRole of the caller's membership of the active organization (the
  legacy pointer reads as MEMBER), else MEMBER.

GetUserFromAdopterJwt puts the membership's stored role on the User
(User#getMembershipRole). Docs, types and the schema say the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
@sonarqubecloud

Copy link
Copy Markdown

@cursor

cursor Bot commented Oct 24, 2026

Copy link
Copy Markdown

Frigg 2.0.1 review (Stack A) — COMMENT only

Verdict (would): would approve

Create/update/delete gates for org-scoped resources via canManageOrg + assertCanManage look correct on v1 and v2 (including ownerOf(..., { manage: true })). No proven cross-org access bug.

Blocking

None.

Should-fix

  1. Sticky ADMIN when JWT omits role after demotion — Prisma membership update: {} never refreshes orgRole; JWT path treats missing claim as orgRole=null and falls back to the stored membership role.
    Failure: User first logs in with org_role=ADMIN (persisted). IdP later issues tokens without a role claim after demotion → still manages org entities/credentials/integrations. Docs imply demotion takes effect when the token carries the new role; omitting the claim preserves first-login ADMIN forever.
    Fix: Update orgRole on every successful JWT membership ensure when the claim is present, and treat “claim absent” as MEMBER (or document hard that demotion must send org_role=MEMBER).
  2. Bearer tokens manage org by default — Product-profile frigg init / docs should set user.orgAdminGateBearerTokens: true whenever adopterJwt is the real role source; otherwise any /user/login bearer is an org admin.

Nits

  • Shared-secret always canManageOrg is correct for a BFF.

No formal approve / request-changes on this review.

);
user.setOrgRole(orgRole);
// What the membership row says: the role when the token carries none.
user.setMembershipRole(membership?.orgRole ?? null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should-fix — sticky ADMIN when the JWT omits org_role after demotion

orgRole from a missing claim is null, then setMembershipRole(membership?.orgRole ?? null) falls back to the stored row. Prisma membership upsert uses update: {} (role never refreshed on later logins).

Scenario: User first authenticates with org_role=ADMIN (persisted). IdP later demotes them and issues tokens without a role claim → membershipRole stays ADMIN → still manages org entities/credentials/integrations.

Please either (a) update orgRole on every successful JWT membership ensure when the claim is present, and treat “claim absent” as MEMBER, or (b) document hard that demotion must send org_role=MEMBER and that omitting the claim preserves the first-login role forever.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

prerelease This change is available in a prerelease. release Create a release when this pr is merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants