Skip to content

Commit 2bcd4e9

Browse files
committed
Report the Cloudflare Access principal on /account/members
The console infers workspace-admin from that list, so an empty response locked edit/delete even when ADMIN_EMAILS granted admin.
1 parent d27e673 commit 2bcd4e9

2 files changed

Lines changed: 80 additions & 5 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { describe, expect, it } from "@effect/vitest";
2+
import { Effect } from "effect";
3+
4+
import { AccountUnauthorized } from "@executor-js/api";
5+
import { AccountProvider } from "@executor-js/api/server";
6+
7+
import type { CloudflareConfig } from "../config";
8+
import { cloudflareAccountProvider } from "./account-provider";
9+
10+
const config = (overrides: Partial<CloudflareConfig> = {}): CloudflareConfig => ({
11+
accessTeamDomain: "team.cloudflareaccess.com",
12+
accessAud: "aud-tag",
13+
accessNameClaim: "name",
14+
accessGroupsClaim: "groups",
15+
adminEmails: ["admin@example.com"],
16+
organizationId: "default",
17+
organizationName: "Default",
18+
organizationSlug: "default",
19+
secretKey: "x".repeat(32),
20+
allowLocalNetwork: false,
21+
webBaseUrl: "https://localhost",
22+
enableDevAuth: true,
23+
...overrides,
24+
});
25+
26+
describe("cloudflareAccountProvider.listMembers", () => {
27+
it.effect("reports the Access principal so the console can see ADMIN_EMAILS", () =>
28+
Effect.gen(function* () {
29+
const provider = yield* AccountProvider;
30+
const { members } = yield* provider.listMembers({});
31+
expect(members).toEqual([
32+
{
33+
id: "dev",
34+
userId: "dev",
35+
email: "admin@example.com",
36+
name: "Dev",
37+
avatarUrl: null,
38+
role: "admin",
39+
status: "active",
40+
lastActiveAt: null,
41+
isCurrentUser: true,
42+
},
43+
]);
44+
}).pipe(Effect.provide(cloudflareAccountProvider(config()))),
45+
);
46+
47+
it.effect("refuses when Access did not authenticate the request", () =>
48+
Effect.gen(function* () {
49+
const provider = yield* AccountProvider;
50+
const error = yield* provider.listMembers({}).pipe(Effect.flip);
51+
expect(error).toBeInstanceOf(AccountUnauthorized);
52+
}).pipe(Effect.provide(cloudflareAccountProvider(config({ enableDevAuth: false })))),
53+
);
54+
});

‎apps/host-cloudflare/src/account/account-provider.ts‎

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ import type { CloudflareConfig } from "../config";
1717
// uses), reading the `Cf-Access-Jwt-Assertion` header off the request.
1818
//
1919
// Single-tenant + Access-managed: members, roles, and API keys live in
20-
// Cloudflare Access, NOT in the app. The shell hides the API-keys footer and
21-
// shows no members page, so those methods are never reached from the UI; they
22-
// return empty (reads) or a clear "managed by Cloudflare Access" error (writes)
23-
// to satisfy the provider shape.
20+
// Cloudflare Access, NOT in the app. Writes stay refused. `listMembers` still
21+
// has to return the current Access principal — the console infers admin from
22+
// that list (`isCurrentUser` + role), and an empty list fail-closes every
23+
// workspace-admin action even when `ADMIN_EMAILS` granted `orgRole: "admin"`.
2424
// ---------------------------------------------------------------------------
2525

2626
const NOT_IN_APP = "Managed by Cloudflare Access, not in the app.";
@@ -66,7 +66,28 @@ export const cloudflareAccountProvider = (
6666
listOrgApiKeys: () => Effect.succeed({ apiKeys: [] }),
6767
createOrgApiKey: () => forbiddenWrite,
6868
revokeOrgApiKey: () => forbiddenWrite,
69-
listMembers: () => Effect.succeed({ members: [] }),
69+
listMembers: (headers) =>
70+
principalFrom(headers).pipe(
71+
Effect.flatMap((principal) =>
72+
principal
73+
? Effect.succeed({
74+
members: [
75+
{
76+
id: principal.accountId,
77+
userId: principal.accountId,
78+
email: principal.email.length > 0 ? principal.email : null,
79+
name: principal.name,
80+
avatarUrl: principal.avatarUrl,
81+
role: principal.orgRole === "admin" ? "admin" : "member",
82+
status: "active",
83+
lastActiveAt: null,
84+
isCurrentUser: true,
85+
},
86+
],
87+
})
88+
: Effect.fail(new AccountUnauthorized()),
89+
),
90+
),
7091
listRoles: () => Effect.succeed({ roles: [] }),
7192
inviteMember: () => forbiddenWrite,
7293
removeMember: () => forbiddenWrite,

0 commit comments

Comments
 (0)