The three-mode auth boundary
Follow one request from token to authorized handler across all three providers, then into the Entra-only tenant/role gate and the data edge — doc to code and back.
backend/src/lib/auth/roles.ts59 lines · resolveRoles L25–58
Outline 3 symbols
- AppRole type export
- parseGuidList function
- resolveRoles function export
1// Role resolution from Entra group memberships. Reads the admin /
2// member group ID allowlists from KV via getConfig, falling back to
3// process.env on older installs (getConfig handles env-first priority).
4//
5// KV value format accepts comma-separated GUIDs with optional
6// `# display-name` comments, matching the install configurator's
7// surface (see installAuth.ts:isInAdminGroup). The comment is stripped
8// before comparison so operators can self-document the KV value.
9//
10// Gap #1 in docs/issues/azure-migration/036-marketplace-install-gaps.md.
11
12import { getConfig } from "../config.js";
13
14export type AppRole = "TenantAdmin" | "Member";
15
16function parseGuidList(raw: string): Set<string> {
17 return new Set(
18 raw
19 .split(",")
20 .map((value) => value.split("#")[0].trim().toLowerCase())
21 .filter(Boolean),
22 );
23}
24
25export async function resolveRoles(groups: string[]): Promise<AppRole[]> {
26 const [adminRaw, memberRaw] = await Promise.all([
27 getConfig("entra-admin-group-ids").catch(() => ""),
28 getConfig("entra-member-group-ids").catch(() => ""),
29 ]);
30
31 const adminGroupIds = parseGuidList(adminRaw);
32 const memberGroupIds = parseGuidList(memberRaw);
33
34 const userGroupsLower = groups.map((g) => g.toLowerCase());
35
36 const matchedAdmin = userGroupsLower.some((group) => adminGroupIds.has(group));
37 if (matchedAdmin) {
38 return ["TenantAdmin", "Member"];
39 }
40
41 // Empty member-group means "no restriction on who can use Mike beyond
42 // tenant membership." Tenant membership has already been verified by
43 // the token's `tid` claim before this function runs, so granting
44 // Member here is safe. Avoids forcing operators who legitimately want
45 // "anyone in my tenant" to invent an artificial group. The /install
46 // 'Users (who can use Mike)' row makes this default explicit in copy.
47 // Closes 040 Entry 7 fix A.
48 if (memberGroupIds.size === 0) {
49 return ["Member"];
50 }
51
52 const matchedMember = userGroupsLower.some((group) => memberGroupIds.has(group));
53 if (matchedMember) {
54 return ["Member"];
55 }
56
57 return [];
58}
59