Repository navigation
[bug] host-cloudflare: AccountProvider.me omits orgRole, so the console hides every workspace mutation from admins #1958
Description
Activity
I reproduced this on v1.6.8 (
eaa1f3a57), self-hosted on Cloudflare Workers behind Access. My email was inADMIN_EMAILS, but Add connection only showedPersonal. My agent found a different approach, so I'm not 100% sure which one is the right fix for this:The UI’s admin check reads the account member list, and the Cloudflare provider hardcodes:
listMembers: () => Effect.succeed({ members: [] }),
So
/api/account/membersreturns no membership from which the UI can determine the current user’s role.I patched
apps/host-cloudflare/src/account/account-provider.tsto authenticate through its existingprincipalFrom(headers)helper and return the current principal as a member, withrole: principal.orgRole ?? "member"andisCurrentUser: true.After deploying that change, Workspace ownership became available for connections and OAuth apps.
Confirming this on v1.6.8 with the Cloudflare host behind Access, with the signed-in email in
ADMIN_EMAILS. The root cause islistMembers, notme:- The console decides admin status in
useIsTenantAdmin(packages/react/src/multiplayer/use-admin-nav.tsx) by finding the member row withisCurrentUser: truein/api/account/members. apps/host-cloudflare/src/account/account-provider.tshardcodeslistMembers: () => Effect.succeed({ members: [] }), so that row never exists.- So
useCanCreateWorkspaceConnections()is alwaysfalse: the Saved to picker in Add connection is hidden and every new connection is Personal. The server already allows workspace writes for allowlisted admins (orgWriteAccessForPrincipal), so this is a UI-only gap.
The fix from the previous comment works for us.
listMembersresolves the Access principal with the existingprincipalFrom(headers)helper and returns it as the only member:listMembers: (headers) => principalFrom(headers).pipe( Effect.flatMap((principal) => principal ? Effect.succeed({ members: [ { id: principal.accountId, userId: principal.accountId, email: principal.email, name: principal.name, avatarUrl: principal.avatarUrl, role: principal.orgRole === "admin" ? "admin" : "member", status: "active", lastActiveAt: null, isCurrentUser: true, }, ], }) : Effect.fail(new AccountUnauthorized()), ), ),
It reports only the caller, never the rest of the Access directory, and the role matches what the API gate already grants, so non-admins still get Personal only. After deploying,
/api/account/membersreturns one active admin row, Add connection shows Personal and Workspace, and a Workspace connection created through it is visible to other principals.Test (
apps/host-cloudflare/src/account/account-provider.test.ts):const listMembers = (cfg: CloudflareConfig) => Effect.gen(function* () { const provider = yield* AccountProvider; return yield* provider.listMembers({}); }).pipe(Effect.provide(cloudflareAccountProvider(cfg))); it.effect("reports the caller as the current member with its gate role", () => Effect.gen(function* () { const result = yield* listMembers({ ...config, enableDevAuth: true }); expect(result.members).toHaveLength(1); expect(result.members[0]).toMatchObject({ role: "admin", status: "active", isCurrentUser: true }); }), ); it.effect("rejects a request with no Access assertion", () => Effect.gen(function* () { const error = yield* Effect.flip(listMembers(config)); expect(error._tag).toBe("AccountUnauthorized"); }), );
Happy to open a PR if that helps.
- The console decides admin status in
Still affected by this on my Cloudflare deployment: my email is in
ADMIN_EMAILS, but adding an integration shows “Requires a
workspace admin.” I see #2089 is still open. Is anything blocking it from being merged?We're clearing the backlog ahead of the v2 launch, so we're closing this. If it still applies to v2, please open a new issue or PR against v2.
Sent from my Claude
Executor version: v1.6.8
Surface: self-hosted on Cloudflare Workers (
apps/host-cloudflare), behind Cloudflare AccessIntegration: any workspace-owned connection (observed on
cloudflare-api,cloudflare-bindings)What happened
After upgrading to v1.6.8, an administrator (email listed in
ADMIN_EMAILS) can no longer perform any workspace mutation from the console. On a workspace-owned connection the row menu offers only Check now — Edit, Reconnect and Remove are all hidden. Personal connections keep the full menu.The server does not agree with the UI. From the same authenticated browser session:
So the principal is an admin and the API authorises the write. Only the console hides it.
Cause
principalFromAccessClaimscomputes the role correctly (apps/host-cloudflare/src/auth/cloudflare-access.ts):But
AccountProvider.me(apps/host-cloudflare/src/account/account-provider.ts) never returns it:Confirmed against a live deployment —
GET /api/account/mereturns the correct email and organization, and no role field of any kind.Per #1919, "Missing role data under the organization model fails closed", so the console treats every principal as a member and hides workspace mutations from everyone, admins included. Cloud derives roles from WorkOS and self-host from Better Auth; the Cloudflare host appears to have been missed.
What I expected
AccountProvider.meonhost-cloudflareshould surfaceorgRole(andorgRoleModel) from the verified Access principal, so the console can enable workspace mutations for admins — matching the cloud and self-host behaviour.Steps to reproduce
apps/host-cloudflareat v1.6.8 behind Cloudflare Access, withADMIN_EMAILS=you@example.com.GET /api/account/me→ your email and organization are correct; no role is returned.POST /api/oauth/startwithowner: "org"from that same session →200with anauthorizationUrl, proving the server treats you as an admin.The practical effect is that an expired workspace OAuth connection cannot be repaired through the UI at all, because Reconnect and Remove are both hidden. Rolling the Worker back to the pre-1.6.8 version restores the buttons.
Related note (possibly separate)
Cloudflare Access service tokens authenticate with
common_nameand carry noemail, soisAdmincan never be true for them — the unit test asserts this directly (// a token is a member, not an admin). On an Access-gated single-tenant deployment this means every machine identity permanently loses workspace writes, includingexecutor.mcp.addServerand connection reconnects from insideexecute. AnADMIN_COMMON_NAMESallowlist alongsideADMIN_EMAILSwould restore that; happy to open it as a separate feature request if you'd prefer.