Skip to content

[bug] host-cloudflare: AccountProvider.me omits orgRole, so the console hides every workspace mutation from admins #1958

Description

@BobzTH

Executor version: v1.6.8
Surface: self-hosted on Cloudflare Workers (apps/host-cloudflare), behind Cloudflare Access
Integration: 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:

POST /api/oauth/start  {"clientOwner":"org","owner":"org","integration":"cloudflare-api",...}
→ 200 {"status":"redirect","authorizationUrl":"https://..."}

So the principal is an admin and the API authorises the write. Only the console hides it.

Cause

principalFromAccessClaims computes the role correctly (apps/host-cloudflare/src/auth/cloudflare-access.ts):

const isAdmin = email.length > 0 && config.adminEmails.includes(email.toLowerCase());
orgRole: isAdmin ? "admin" : "member",

But AccountProvider.me (apps/host-cloudflare/src/account/account-provider.ts) never returns it:

me: (headers) => ... Effect.succeed({
  user: { id, email, name, avatarUrl },
  organization: { id, name, slug },
})   // no orgRole / orgRoleModel

Confirmed against a live deployment — GET /api/account/me returns 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.me on host-cloudflare should surface orgRole (and orgRoleModel) from the verified Access principal, so the console can enable workspace mutations for admins — matching the cloud and self-host behaviour.

Steps to reproduce

  1. Deploy apps/host-cloudflare at v1.6.8 behind Cloudflare Access, with ADMIN_EMAILS=you@example.com.
  2. Sign in through Access with that exact email.
  3. GET /api/account/me → your email and organization are correct; no role is returned.
  4. Open an integration that has a workspace connection → its row menu shows only Check now.
  5. POST /api/oauth/start with owner: "org" from that same session → 200 with an authorizationUrl, 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_name and carry no email, so isAdmin can 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, including executor.mcp.addServer and connection reconnects from inside execute. An ADMIN_COMMON_NAMES allowlist alongside ADMIN_EMAILS would restore that; happy to open it as a separate feature request if you'd prefer.

Activity

  1. Stormix commented on Sep 11, 2026

    @Stormix

    I reproduced this on v1.6.8 (eaa1f3a57), self-hosted on Cloudflare Workers behind Access. My email was in ADMIN_EMAILS, but Add connection only showed Personal. 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/members returns no membership from which the UI can determine the current user’s role.

    I patched apps/host-cloudflare/src/account/account-provider.ts to authenticate through its existing principalFrom(headers) helper and return the current principal as a member, with role: principal.orgRole ?? "member" and isCurrentUser: true.

    After deploying that change, Workspace ownership became available for connections and OAuth apps.

  2. ray-amjad commented on Sep 17, 2026

    @ray-amjad

    Confirming this on v1.6.8 with the Cloudflare host behind Access, with the signed-in email in ADMIN_EMAILS. The root cause is listMembers, not me:

    • The console decides admin status in useIsTenantAdmin (packages/react/src/multiplayer/use-admin-nav.tsx) by finding the member row with isCurrentUser: true in /api/account/members.
    • apps/host-cloudflare/src/account/account-provider.ts hardcodes listMembers: () => Effect.succeed({ members: [] }), so that row never exists.
    • So useCanCreateWorkspaceConnections() is always false: 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. listMembers resolves the Access principal with the existing principalFrom(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/members returns 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.

  3. cheetahbyte commented on Oct 1, 2026

    @cheetahbyte

    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?

  4. RhysSullivan commented on Oct 8, 2026

    @RhysSullivan
    Collaborator

    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

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions