@tumbaland/backend-core 1.42.0 → 1.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/apiKeys/middleware.d.ts.map +1 -1
  2. package/dist/apiKeys/middleware.js +18 -9
  3. package/dist/apiKeys/middleware.js.map +1 -1
  4. package/dist/groups/client.d.ts +15 -0
  5. package/dist/groups/client.d.ts.map +1 -1
  6. package/dist/groups/client.js +1 -0
  7. package/dist/groups/client.js.map +1 -1
  8. package/dist/groups/index.d.ts +1 -0
  9. package/dist/groups/index.d.ts.map +1 -1
  10. package/dist/groups/index.js +5 -1
  11. package/dist/groups/index.js.map +1 -1
  12. package/dist/groups/membership.d.ts +28 -0
  13. package/dist/groups/membership.d.ts.map +1 -0
  14. package/dist/groups/membership.js +122 -0
  15. package/dist/groups/membership.js.map +1 -0
  16. package/dist/middleware/authMiddleware.d.ts.map +1 -1
  17. package/dist/middleware/authMiddleware.js +9 -6
  18. package/dist/middleware/authMiddleware.js.map +1 -1
  19. package/dist/testing/serviceTestSetup.d.ts +2 -0
  20. package/dist/testing/serviceTestSetup.d.ts.map +1 -0
  21. package/dist/testing/serviceTestSetup.js +18 -0
  22. package/dist/testing/serviceTestSetup.js.map +1 -0
  23. package/package.json +1 -1
  24. package/src/apiKeys/middleware.test.ts +68 -0
  25. package/src/apiKeys/middleware.ts +20 -10
  26. package/src/groups/client.ts +1 -1
  27. package/src/groups/index.ts +2 -0
  28. package/src/groups/membership.test.ts +84 -0
  29. package/src/groups/membership.ts +133 -0
  30. package/src/middleware/authMiddleware.test.ts +9 -3
  31. package/src/middleware/authMiddleware.ts +9 -6
  32. package/src/testing/serviceTestSetup.ts +17 -0
  33. package/testing.js +4 -3
  34. package/dist/testing/allowAllSessions.d.ts +0 -2
  35. package/dist/testing/allowAllSessions.d.ts.map +0 -1
  36. package/dist/testing/allowAllSessions.js +0 -13
  37. package/dist/testing/allowAllSessions.js.map +0 -1
  38. package/src/testing/allowAllSessions.ts +0 -11
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/apiKeys/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyB,cAAc,EAAY,MAAM,SAAS,CAAC;AAS1E,OAAO,EAKL,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,MAAM,EACZ,MAAM,SAAS,CAAC;AAGjB;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,iFAAiF;IACjF,OAAO,EAAE,YAAY,CAAC;IACtB,qDAAqD;IACrD,QAAQ,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC;IAC1B,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;OAMG;IACH,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,MAAM,CAAC,EAAE,aAAa,CAAC;SACxB;KACF;CACF;AAgJD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,iBAAiB,GAAI,GAAG,gBAAgB,WAAW,EAAE,KAAG,cAwElE,CAAC;AAEJ;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,YAAY,GAAI,GAAG,gBAAgB,WAAW,EAAE,KAAG,cAiB7D,CAAC;AAEJ;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,EAAE,cASzB,CAAC"}
1
+ {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/apiKeys/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyB,cAAc,EAAY,MAAM,SAAS,CAAC;AAS1E,OAAO,EAKL,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,MAAM,EACZ,MAAM,SAAS,CAAC;AAIjB;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,iFAAiF;IACjF,OAAO,EAAE,YAAY,CAAC;IACtB,qDAAqD;IACrD,QAAQ,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC;IAC1B,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;OAMG;IACH,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,MAAM,CAAC,EAAE,aAAa,CAAC;SACxB;KACF;CACF;AAyJD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,iBAAiB,GAAI,GAAG,gBAAgB,WAAW,EAAE,KAAG,cAwElE,CAAC;AAEJ;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,YAAY,GAAI,GAAG,gBAAgB,WAAW,EAAE,KAAG,cAiB7D,CAAC;AAEJ;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,EAAE,cASzB,CAAC"}
@@ -13,6 +13,7 @@ const crypto_1 = require("./crypto");
13
13
  const service_1 = require("./service");
14
14
  const types_1 = require("./types");
15
15
  const session_1 = require("../auth/session");
16
+ const membership_1 = require("../groups/membership");
16
17
  /** Both auth paths read the token from the same two places. */
17
18
  const extractToken = (req) => req.cookies?.access_token || req.headers.authorization?.replace('Bearer ', '');
18
19
  const unauthorized = (res) => {
@@ -90,7 +91,7 @@ const applyTenantGrant = (req, tenants) => {
90
91
  * Settling that in one place is what keeps the two from drifting into subtly
91
92
  * different amounts of access.
92
93
  */
93
- function admitAgent(req, res, next, requiredScopes, credential) {
94
+ async function admitAgent(req, res, next, requiredScopes, credential) {
94
95
  const missing = requiredScopes.filter((scope) => !credential.scopes.includes(scope));
95
96
  if (missing.length > 0) {
96
97
  res.status(403).json({
@@ -99,7 +100,15 @@ function admitAgent(req, res, next, requiredScopes, credential) {
99
100
  });
100
101
  return;
101
102
  }
102
- const actingAs = applyTenantGrant(req, credential.tenants);
103
+ // A grant names the groups the owner belonged to when they made it. Only the
104
+ // ones they still belong to count, so leaving a group also takes it from
105
+ // every key and connection they gave it to.
106
+ const stillMemberOf = new Set(await (0, membership_1.currentGroupsOf)({ email: credential.email }));
107
+ const tenants = {
108
+ ...credential.tenants,
109
+ allowed: credential.tenants.allowed.filter((tenant) => tenant === types_1.PERSONAL_TENANT || stillMemberOf.has(tenant))
110
+ };
111
+ const actingAs = applyTenantGrant(req, tenants);
103
112
  if (actingAs === null) {
104
113
  res.status(403).json({
105
114
  success: false,
@@ -111,11 +120,11 @@ function admitAgent(req, res, next, requiredScopes, credential) {
111
120
  // Only the granted groups, never the owner's full membership: this is what
112
121
  // stops a credential reaching a group it was not issued for through any
113
122
  // handler that consults `userGroups` instead of the `groupId` parameter.
114
- req.userGroups = (0, types_1.groupIdsOf)(credential.tenants.allowed);
123
+ req.userGroups = (0, types_1.groupIdsOf)(tenants.allowed);
115
124
  req.apiKey = {
116
125
  keyId: credential.credentialId,
117
126
  scopes: credential.scopes,
118
- tenants: credential.tenants,
127
+ tenants,
119
128
  actingAs,
120
129
  kind: credential.kind,
121
130
  label: credential.label,
@@ -158,9 +167,9 @@ const authenticateAgent = (...requiredScopes) => async function authenticateAgen
158
167
  unauthorized(res);
159
168
  return;
160
169
  }
161
- const user = claims;
162
- req.user = user;
163
- req.userGroups = user.groups ?? [];
170
+ const groups = await (0, membership_1.currentGroupsOf)(claims);
171
+ req.user = { ...claims, groups };
172
+ req.userGroups = groups;
164
173
  (0, context_1.beginAudit)(req, res, next);
165
174
  return;
166
175
  }
@@ -174,7 +183,7 @@ const authenticateAgent = (...requiredScopes) => async function authenticateAgen
174
183
  // `undefined`, scoped writes were refused as inaccessible, and a read
175
184
  // whose scope collapsed to nothing fell through to "no user context" and
176
185
  // answered from the whole collection.
177
- admitAgent(req, res, next, requiredScopes, {
186
+ await admitAgent(req, res, next, requiredScopes, {
178
187
  // The client, not the token: `jti` is a new value every hour.
179
188
  credentialId: claims.clientId ?? 'unknown-client',
180
189
  kind: 'oauth',
@@ -195,7 +204,7 @@ const authenticateAgent = (...requiredScopes) => async function authenticateAgen
195
204
  unauthorized(res);
196
205
  return;
197
206
  }
198
- admitAgent(req, res, next, requiredScopes, {
207
+ await admitAgent(req, res, next, requiredScopes, {
199
208
  credentialId: result.keyId,
200
209
  kind: 'api_key',
201
210
  label: result.label || 'An API key',
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../src/apiKeys/middleware.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../src/apiKeys/middleware.ts"],"names":[],"mappings":"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"}
@@ -7,6 +7,21 @@ export interface GroupOwner {
7
7
  /** True when group-service could not be reached and nothing was resolved. */
8
8
  stale?: boolean;
9
9
  }
10
+ /**
11
+ * Prefers the container-network address over the public hostname.
12
+ *
13
+ * `GROUP_API_URL` cannot simply be pointed inward: the frontend containers
14
+ * template that same variable into the `config.json` the *browser* downloads,
15
+ * so a Docker-internal value there breaks every group call in the UI. One
16
+ * variable genuinely has two consumers with incompatible needs, so this is the
17
+ * server-side half of it.
18
+ *
19
+ * Falling back to the public URL keeps local dev (where only `GROUP_API_URL` is
20
+ * set) and any un-migrated deployment working — it just takes the long way
21
+ * round, out through the proxy and back, carrying the internal token across the
22
+ * public edge.
23
+ */
24
+ export declare function internalGroupUrl(): string | undefined;
10
25
  /**
11
26
  * The group's owner, cached for {@link CACHE_TTL_MS}, or `null` when the group
12
27
  * does not exist or group-service could not answer.
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/groups/client.ts"],"names":[],"mappings":"AAwBA,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,4EAA4E;IAC5E,OAAO,EAAE,OAAO,CAAC;IACjB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAyGD;;;;;;;;GAQG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAqB/E;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE1D;AAED,mFAAmF;AACnF,wBAAgB,oBAAoB,IAAI,IAAI,CAG3C"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/groups/client.ts"],"names":[],"mappings":"AAwBA,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,4EAA4E;IAC5E,OAAO,EAAE,OAAO,CAAC;IACjB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAgCD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,SAAS,CAErD;AAyDD;;;;;;;;GAQG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAqB/E;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE1D;AAED,mFAAmF;AACnF,wBAAgB,oBAAoB,IAAI,IAAI,CAG3C"}
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.internalGroupUrl = internalGroupUrl;
6
7
  exports.getGroupOwner = getGroupOwner;
7
8
  exports.invalidateGroupOwner = invalidateGroupOwner;
8
9
  exports.clearGroupOwnerCache = clearGroupOwnerCache;
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/groups/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/groups/client.ts"],"names":[],"mappings":"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"}
@@ -2,4 +2,5 @@ export { getGroupOwner, invalidateGroupOwner, clearGroupOwnerCache } from './cli
2
2
  export type { GroupOwner } from './client';
3
3
  export { groupOwnerSubject } from './subject';
4
4
  export type { GroupIdResolver } from './subject';
5
+ export { getUserGroupIds, currentGroupsOf, clearMembershipCache } from './membership';
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/groups/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AACrF,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAE3C,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC9C,YAAY,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/groups/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AACrF,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAE3C,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC9C,YAAY,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAEjD,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC"}
@@ -1,10 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.groupOwnerSubject = exports.clearGroupOwnerCache = exports.invalidateGroupOwner = exports.getGroupOwner = void 0;
3
+ exports.clearMembershipCache = exports.currentGroupsOf = exports.getUserGroupIds = exports.groupOwnerSubject = exports.clearGroupOwnerCache = exports.invalidateGroupOwner = exports.getGroupOwner = void 0;
4
4
  var client_1 = require("./client");
5
5
  Object.defineProperty(exports, "getGroupOwner", { enumerable: true, get: function () { return client_1.getGroupOwner; } });
6
6
  Object.defineProperty(exports, "invalidateGroupOwner", { enumerable: true, get: function () { return client_1.invalidateGroupOwner; } });
7
7
  Object.defineProperty(exports, "clearGroupOwnerCache", { enumerable: true, get: function () { return client_1.clearGroupOwnerCache; } });
8
8
  var subject_1 = require("./subject");
9
9
  Object.defineProperty(exports, "groupOwnerSubject", { enumerable: true, get: function () { return subject_1.groupOwnerSubject; } });
10
+ var membership_1 = require("./membership");
11
+ Object.defineProperty(exports, "getUserGroupIds", { enumerable: true, get: function () { return membership_1.getUserGroupIds; } });
12
+ Object.defineProperty(exports, "currentGroupsOf", { enumerable: true, get: function () { return membership_1.currentGroupsOf; } });
13
+ Object.defineProperty(exports, "clearMembershipCache", { enumerable: true, get: function () { return membership_1.clearMembershipCache; } });
10
14
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/groups/index.ts"],"names":[],"mappings":";;;AAAA,mCAAqF;AAA5E,uGAAA,aAAa,OAAA;AAAE,8GAAA,oBAAoB,OAAA;AAAE,8GAAA,oBAAoB,OAAA;AAGlE,qCAA8C;AAArC,4GAAA,iBAAiB,OAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/groups/index.ts"],"names":[],"mappings":";;;AAAA,mCAAqF;AAA5E,uGAAA,aAAa,OAAA;AAAE,8GAAA,oBAAoB,OAAA;AAAE,8GAAA,oBAAoB,OAAA;AAGlE,qCAA8C;AAArC,4GAAA,iBAAiB,OAAA;AAG1B,2CAAsF;AAA7E,6GAAA,eAAe,OAAA;AAAE,6GAAA,eAAe,OAAA;AAAE,kHAAA,oBAAoB,OAAA"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The ids of the groups `email` belongs to right now.
3
+ *
4
+ * When group-service cannot be asked, the last answer is used even if it has
5
+ * expired, and failing that no groups at all: a person keeps their own data
6
+ * through an outage and loses group data until it ends, rather than being
7
+ * granted a group on a guess.
8
+ */
9
+ export declare function getUserGroupIds(email: string): Promise<string[]>;
10
+ /** For tests. */
11
+ export declare function clearMembershipCache(): void;
12
+ type MembershipLookup = (user: {
13
+ email: string;
14
+ groups?: string[];
15
+ }) => Promise<string[]>;
16
+ /** The groups the auth middlewares put on `req.userGroups`. */
17
+ export declare const currentGroupsOf: (user: {
18
+ email: string;
19
+ groups?: string[];
20
+ }) => Promise<string[]>;
21
+ /**
22
+ * Replaces the lookup, for tests only; pass nothing to restore the real one.
23
+ * Service suites reach it through `@tumbaland/backend-core/testing`, which makes
24
+ * a test token's own `groups` claim the membership, so they need no group-service.
25
+ */
26
+ export declare function overrideMembershipLookupForTests(lookup?: MembershipLookup): void;
27
+ export {};
28
+ //# sourceMappingURL=membership.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"membership.d.ts","sourceRoot":"","sources":["../../src/groups/membership.ts"],"names":[],"mappings":"AA+EA;;;;;;;GAOG;AACH,wBAAsB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAqBtE;AAED,iBAAiB;AACjB,wBAAgB,oBAAoB,IAAI,IAAI,CAG3C;AAED,KAAK,gBAAgB,GAAG,CAAC,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;AAK1F,+DAA+D;AAC/D,eAAO,MAAM,eAAe,GAAI,MAAM;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,KAAG,OAAO,CAAC,MAAM,EAAE,CACrE,CAAC;AAEzB;;;;GAIG;AACH,wBAAgB,gCAAgC,CAAC,MAAM,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAEhF"}
@@ -0,0 +1,122 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.currentGroupsOf = void 0;
7
+ exports.getUserGroupIds = getUserGroupIds;
8
+ exports.clearMembershipCache = clearMembershipCache;
9
+ exports.overrideMembershipLookupForTests = overrideMembershipLookupForTests;
10
+ const logger_1 = __importDefault(require("../logging/logger"));
11
+ const client_1 = require("./client");
12
+ /**
13
+ * Which groups a person belongs to, asked of group-service.
14
+ *
15
+ * This used to travel in the session token's `groups` claim, which made it only
16
+ * as fresh as the token: a new member could not see the group until their next
17
+ * renewal, and a removed one kept access until theirs, which is why membership
18
+ * changes used to sign people out. Asked here instead, with a short cache, a
19
+ * change reaches every service within CACHE_TTL_MS.
20
+ *
21
+ * Every request pays for this only once per person per service per cache
22
+ * window; concurrent callers share one request.
23
+ */
24
+ const CACHE_TTL_MS = 30_000;
25
+ /** Bounded so a service under enumeration can't grow the cache without limit. */
26
+ const MAX_CACHE_ENTRIES = 5_000;
27
+ const REQUEST_TIMEOUT_MS = 3_000;
28
+ const cache = new Map();
29
+ const inFlight = new Map();
30
+ function putInCache(email, groups) {
31
+ if (!cache.has(email) && cache.size >= MAX_CACHE_ENTRIES) {
32
+ // Map iterates in insertion order, so the first key is the oldest write.
33
+ const oldest = cache.keys().next();
34
+ if (!oldest.done)
35
+ cache.delete(oldest.value);
36
+ }
37
+ cache.set(email, { groups, expiresAt: Date.now() + CACHE_TTL_MS });
38
+ }
39
+ async function fetchUserGroupIds(email) {
40
+ const baseUrl = (0, client_1.internalGroupUrl)();
41
+ const token = process.env.INTERNAL_SERVICE_TOKEN;
42
+ if (!baseUrl || !token) {
43
+ logger_1.default.error('Group membership lookup not configured', {
44
+ hasGroupApiUrl: Boolean(baseUrl),
45
+ hasInternalToken: Boolean(token)
46
+ });
47
+ return null;
48
+ }
49
+ try {
50
+ const response = await fetch(`${baseUrl}/api/groups/mine/ids?userEmail=${encodeURIComponent(email)}`, {
51
+ method: 'GET',
52
+ headers: {
53
+ 'Content-Type': 'application/json',
54
+ 'x-internal-token': token,
55
+ 'x-service-id': process.env.SERVICE_NAME || 'unknown-service'
56
+ },
57
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
58
+ });
59
+ if (!response.ok) {
60
+ logger_1.default.error('Group membership lookup failed', { email, status: response.status });
61
+ return null;
62
+ }
63
+ const body = (await response.json());
64
+ if (!body.success || !Array.isArray(body.data)) {
65
+ logger_1.default.error('Group membership lookup returned no list', { email });
66
+ return null;
67
+ }
68
+ return body.data.filter((id) => typeof id === 'string');
69
+ }
70
+ catch (error) {
71
+ logger_1.default.error('Group membership lookup unreachable', { email, error: error?.message });
72
+ return null;
73
+ }
74
+ }
75
+ /**
76
+ * The ids of the groups `email` belongs to right now.
77
+ *
78
+ * When group-service cannot be asked, the last answer is used even if it has
79
+ * expired, and failing that no groups at all: a person keeps their own data
80
+ * through an outage and loses group data until it ends, rather than being
81
+ * granted a group on a guess.
82
+ */
83
+ async function getUserGroupIds(email) {
84
+ const key = email.toLowerCase();
85
+ const entry = cache.get(key);
86
+ if (entry && entry.expiresAt > Date.now())
87
+ return entry.groups;
88
+ let pending = inFlight.get(key);
89
+ if (!pending) {
90
+ pending = fetchUserGroupIds(key).finally(() => inFlight.delete(key));
91
+ inFlight.set(key, pending);
92
+ }
93
+ const groups = await pending;
94
+ if (groups) {
95
+ putInCache(key, groups);
96
+ return groups;
97
+ }
98
+ if (entry) {
99
+ logger_1.default.warn('Using the last known group membership while group-service is unreachable', { email: key });
100
+ return entry.groups;
101
+ }
102
+ return [];
103
+ }
104
+ /** For tests. */
105
+ function clearMembershipCache() {
106
+ cache.clear();
107
+ inFlight.clear();
108
+ }
109
+ const lookUpMembership = (user) => getUserGroupIds(user.email);
110
+ let membershipLookup = lookUpMembership;
111
+ /** The groups the auth middlewares put on `req.userGroups`. */
112
+ const currentGroupsOf = (user) => membershipLookup(user);
113
+ exports.currentGroupsOf = currentGroupsOf;
114
+ /**
115
+ * Replaces the lookup, for tests only; pass nothing to restore the real one.
116
+ * Service suites reach it through `@tumbaland/backend-core/testing`, which makes
117
+ * a test token's own `groups` claim the membership, so they need no group-service.
118
+ */
119
+ function overrideMembershipLookupForTests(lookup) {
120
+ membershipLookup = lookup ?? lookUpMembership;
121
+ }
122
+ //# sourceMappingURL=membership.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"membership.js","sourceRoot":"","sources":["../../src/groups/membership.ts"],"names":[],"mappings":";;;;;;AAuFA,0CAqBC;AAGD,oDAGC;AAgBD,4EAEC;AApID,+DAAuC;AACvC,qCAA4C;AAE5C;;;;;;;;;;;GAWG;AACH,MAAM,YAAY,GAAG,MAAM,CAAC;AAE5B,iFAAiF;AACjF,MAAM,iBAAiB,GAAG,KAAK,CAAC;AAEhC,MAAM,kBAAkB,GAAG,KAAK,CAAC;AAOjC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAsB,CAAC;AAC5C,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAoC,CAAC;AAE7D,SAAS,UAAU,CAAC,KAAa,EAAE,MAAgB;IACjD,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,IAAI,iBAAiB,EAAE,CAAC;QACzD,yEAAyE;QACzE,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC;QACnC,IAAI,CAAC,MAAM,CAAC,IAAI;YAAE,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC/C,CAAC;IACD,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,YAAY,EAAE,CAAC,CAAC;AACrE,CAAC;AAED,KAAK,UAAU,iBAAiB,CAAC,KAAa;IAC5C,MAAM,OAAO,GAAG,IAAA,yBAAgB,GAAE,CAAC;IACnC,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC;IAEjD,IAAI,CAAC,OAAO,IAAI,CAAC,KAAK,EAAE,CAAC;QACvB,gBAAM,CAAC,KAAK,CAAC,wCAAwC,EAAE;YACrD,cAAc,EAAE,OAAO,CAAC,OAAO,CAAC;YAChC,gBAAgB,EAAE,OAAO,CAAC,KAAK,CAAC;SACjC,CAAC,CAAC;QACH,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,OAAO,kCAAkC,kBAAkB,CAAC,KAAK,CAAC,EAAE,EAAE;YACpG,MAAM,EAAE,KAAK;YACb,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,kBAAkB,EAAE,KAAK;gBACzB,cAAc,EAAE,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,iBAAiB;aAC9D;YACD,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,kBAAkB,CAAC;SAChD,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,gBAAM,CAAC,KAAK,CAAC,gCAAgC,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;YACnF,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAA0C,CAAC;QAC9E,IAAI,CAAC,IAAI,CAAC,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC/C,gBAAM,CAAC,KAAK,CAAC,0CAA0C,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;YACpE,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,EAAE,EAAgB,EAAE,CAAC,OAAO,EAAE,KAAK,QAAQ,CAAC,CAAC;IACxE,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,gBAAM,CAAC,KAAK,CAAC,qCAAqC,EAAE,EAAE,KAAK,EAAE,KAAK,EAAG,KAAe,EAAE,OAAO,EAAE,CAAC,CAAC;QACjG,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACI,KAAK,UAAU,eAAe,CAAC,KAAa;IACjD,MAAM,GAAG,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC;IAChC,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC7B,IAAI,KAAK,IAAI,KAAK,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE;QAAE,OAAO,KAAK,CAAC,MAAM,CAAC;IAE/D,IAAI,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAChC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,GAAG,iBAAiB,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;QACrE,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IAC7B,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC;IAC7B,IAAI,MAAM,EAAE,CAAC;QACX,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QACxB,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,IAAI,KAAK,EAAE,CAAC;QACV,gBAAM,CAAC,IAAI,CAAC,0EAA0E,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;QACxG,OAAO,KAAK,CAAC,MAAM,CAAC;IACtB,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,iBAAiB;AACjB,SAAgB,oBAAoB;IAClC,KAAK,CAAC,KAAK,EAAE,CAAC;IACd,QAAQ,CAAC,KAAK,EAAE,CAAC;AACnB,CAAC;AAID,MAAM,gBAAgB,GAAqB,CAAC,IAAI,EAAE,EAAE,CAAC,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACjF,IAAI,gBAAgB,GAAqB,gBAAgB,CAAC;AAE1D,+DAA+D;AACxD,MAAM,eAAe,GAAG,CAAC,IAA0C,EAAqB,EAAE,CAC/F,gBAAgB,CAAC,IAAI,CAAC,CAAC;AADZ,QAAA,eAAe,mBACH;AAEzB;;;;GAIG;AACH,SAAgB,gCAAgC,CAAC,MAAyB;IACxE,gBAAgB,GAAG,MAAM,IAAI,gBAAgB,CAAC;AAChD,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"authMiddleware.d.ts","sourceRoot":"","sources":["../../src/middleware/authMiddleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAO1D;;;;;;;;;;;;GAYG;AACH,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAiCtG;AACD;;;;;;;;;;;;GAYG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBlG"}
1
+ {"version":3,"file":"authMiddleware.d.ts","sourceRoot":"","sources":["../../src/middleware/authMiddleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAQ1D;;;;;;;;;;;;GAYG;AACH,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAmCtG;AACD;;;;;;;;;;;;GAYG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBlG"}
@@ -9,6 +9,7 @@ const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
9
9
  const env_1 = require("../config/env");
10
10
  const tokens_1 = require("../oauth/tokens");
11
11
  const session_1 = require("../auth/session");
12
+ const membership_1 = require("../groups/membership");
12
13
  /**
13
14
  * JWT authentication middleware
14
15
  * Verifies JWT token and attaches user info (and its embedded groups) to
@@ -44,9 +45,11 @@ async function authenticateToken(req, res, next) {
44
45
  res.status(401).json({ success: false, message: 'Session has ended' });
45
46
  return;
46
47
  }
47
- const user = claims;
48
- req.user = user;
49
- req.userGroups = user.groups ?? [];
48
+ // Membership is asked of group-service, not read from the token, so a
49
+ // change applies within seconds rather than at the next renewal.
50
+ const groups = await (0, membership_1.currentGroupsOf)(claims);
51
+ req.user = { ...claims, groups };
52
+ req.userGroups = groups;
50
53
  }
51
54
  catch (error) {
52
55
  res.status(401).json({ success: false, message: 'Invalid or expired token' });
@@ -77,9 +80,9 @@ async function optionalAuth(req, _res, next) {
77
80
  // expired cookie, so the route still answers with its public form.
78
81
  // An ended session is treated as none, like an expired cookie.
79
82
  if (!(0, tokens_1.isAccessTokenClaims)(claims) && (await (0, session_1.isCurrentSession)(claims))) {
80
- const user = claims;
81
- req.user = user;
82
- req.userGroups = user.groups ?? [];
83
+ const groups = await (0, membership_1.currentGroupsOf)(claims);
84
+ req.user = { ...claims, groups };
85
+ req.userGroups = groups;
83
86
  }
84
87
  }
85
88
  }
@@ -1 +1 @@
1
- {"version":3,"file":"authMiddleware.js","sourceRoot":"","sources":["../../src/middleware/authMiddleware.ts"],"names":[],"mappings":";;;;;AAoBA,8CAiCC;AAcD,oCAoBC;AAtFD,gEAA+B;AAC/B,uCAA2C;AAC3C,4CAAsD;AAEtD,6CAAmD;AAEnD;;;;;;;;;;;;GAYG;AACI,KAAK,UAAU,iBAAiB,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB;IACrF,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,IAAA,gBAAU,EAAC,YAAY,CAAC,CAAC;QAC5C,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,EAAE,YAAY,IAAI,GAAG,CAAC,OAAO,CAAC,aAAa,EAAE,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;QAE7F,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,uBAAuB,EAAE,CAAC,CAAC;YAC3E,OAAO;QACT,CAAC;QAED,MAAM,MAAM,GAAG,sBAAG,CAAC,MAAM,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;QAC7C,IAAI,IAAA,4BAAmB,EAAC,MAAM,CAAC,EAAE,CAAC;YAChC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;gBACnB,OAAO,EAAE,KAAK;gBACd,OAAO,EAAE,2EAA2E;aACrF,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QAED,wEAAwE;QACxE,oCAAoC;QACpC,IAAI,CAAC,CAAC,MAAM,IAAA,0BAAgB,EAAC,MAAM,CAAC,CAAC,EAAE,CAAC;YACtC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,mBAAmB,EAAE,CAAC,CAAC;YACvE,OAAO;QACT,CAAC;QACD,MAAM,IAAI,GAAG,MAAqB,CAAC;QACnC,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;QAChB,GAAG,CAAC,UAAU,GAAG,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC;IACrC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,0BAA0B,EAAE,CAAC,CAAC;QAC9E,OAAO;IACT,CAAC;IACD,IAAI,EAAE,CAAC;AACT,CAAC;AACD;;;;;;;;;;;;GAYG;AACI,KAAK,UAAU,YAAY,CAAC,GAAY,EAAE,IAAc,EAAE,IAAkB;IACjF,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,IAAA,gBAAU,EAAC,YAAY,CAAC,CAAC;QAC5C,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,EAAE,YAAY,IAAI,GAAG,CAAC,OAAO,CAAC,aAAa,EAAE,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;QAE7F,IAAI,KAAK,EAAE,CAAC;YACV,MAAM,MAAM,GAAG,sBAAG,CAAC,MAAM,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;YAC7C,qEAAqE;YACrE,mEAAmE;YACnE,+DAA+D;YAC/D,IAAI,CAAC,IAAA,4BAAmB,EAAC,MAAM,CAAC,IAAI,CAAC,MAAM,IAAA,0BAAgB,EAAC,MAAM,CAAC,CAAC,EAAE,CAAC;gBACrE,MAAM,IAAI,GAAG,MAAqB,CAAC;gBACnC,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;gBAChB,GAAG,CAAC,UAAU,GAAG,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC;YACrC,CAAC;QACH,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,oCAAoC;IACtC,CAAC;IACD,IAAI,EAAE,CAAC;AACT,CAAC"}
1
+ {"version":3,"file":"authMiddleware.js","sourceRoot":"","sources":["../../src/middleware/authMiddleware.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=serviceTestSetup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serviceTestSetup.d.ts","sourceRoot":"","sources":["../../src/testing/serviceTestSetup.ts"],"names":[],"mappings":""}
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const session_1 = require("../auth/session");
4
+ const membership_1 = require("../groups/membership");
5
+ /**
6
+ * Jest setup for service test suites: they sign session tokens without a
7
+ * session and run with no database or group-service, so every token's session
8
+ * counts as live and its `groups` claim stands in for group membership.
9
+ * The session check itself is tested in backend-core (auth/session.test.ts).
10
+ *
11
+ * Loaded through the package's test-only entry point:
12
+ * setupFiles: ['@tumbaland/backend-core/testing']
13
+ */
14
+ (0, session_1.overrideSessionCheckForTests)(async () => true);
15
+ // No group-service either: a test token's own `groups` claim is taken as the
16
+ // person's membership. The live lookup is tested in groups/membership.test.ts.
17
+ (0, membership_1.overrideMembershipLookupForTests)(async (user) => user.groups ?? []);
18
+ //# sourceMappingURL=serviceTestSetup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serviceTestSetup.js","sourceRoot":"","sources":["../../src/testing/serviceTestSetup.ts"],"names":[],"mappings":";;AAAA,6CAA+D;AAC/D,qDAAwE;AAExE;;;;;;;;GAQG;AACH,IAAA,sCAA4B,EAAC,KAAK,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC;AAE/C,6EAA6E;AAC7E,+EAA+E;AAC/E,IAAA,6CAAgC,EAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tumbaland/backend-core",
3
- "version": "1.42.0",
3
+ "version": "1.44.0",
4
4
  "description": "Core shared functionality for Tumbaland backend services",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -12,6 +12,7 @@ import { mintAccessToken, type MintAccessTokenInput } from '../oauth/tokens';
12
12
  import { PERSONAL_TENANT } from './types';
13
13
  import { authenticateAgent, requireScope, denyApiKeys } from './middleware';
14
14
  import { overrideSessionCheckForTests } from '../auth/session';
15
+ import { overrideMembershipLookupForTests } from '../groups/membership';
15
16
 
16
17
  const mockedVerify = verifyApiKey as jest.Mock;
17
18
  const ORIGINAL_ENV = process.env;
@@ -44,11 +45,18 @@ const okVerification = (over: Record<string, unknown> = {}) => ({
44
45
  ...over
45
46
  });
46
47
 
48
+ // Unless a test says otherwise, the owner still belongs to every group used here.
49
+ let memberOf: string[];
50
+
47
51
  beforeEach(() => {
48
52
  jest.clearAllMocks();
49
53
  process.env = { ...ORIGINAL_ENV, JWT_SECRET: 'test-jwt-secret' };
54
+ memberOf = ['g1', 'g2'];
55
+ overrideMembershipLookupForTests(async () => memberOf);
50
56
  });
51
57
 
58
+ afterAll(() => overrideMembershipLookupForTests());
59
+
52
60
  afterEach(() => {
53
61
  process.env = ORIGINAL_ENV;
54
62
  });
@@ -81,6 +89,17 @@ describe('authenticateAgent — session tokens', () => {
81
89
  expect(next).not.toHaveBeenCalled();
82
90
  });
83
91
 
92
+ it("takes the person's groups from group-service, not from the token", async () => {
93
+ memberOf = ['g2'];
94
+ const token = jwt.sign({ id: 'u1', email: 'a@b.c', name: 'A', groups: ['g1'] }, 'test-jwt-secret');
95
+ const request = req({ headers: { authorization: `Bearer ${token}` } });
96
+
97
+ await authenticateAgent()(request, mockRes(), jest.fn());
98
+
99
+ expect(request.userGroups).toEqual(['g2']);
100
+ expect(request.user?.groups).toEqual(['g2']);
101
+ });
102
+
84
103
  it('accepts a valid JWT and grants the full session, with no apiKey context', async () => {
85
104
  const token = jwt.sign({ id: 'u1', email: 'a@b.c', name: 'A', groups: ['g1', 'g2'] }, 'test-jwt-secret');
86
105
  const request = req({ headers: { authorization: `Bearer ${token}` } });
@@ -359,6 +378,55 @@ describe('authenticateAgent — a multi-tenant grant', () => {
359
378
  });
360
379
  });
361
380
 
381
+ describe('authenticateAgent — groups the owner has since left', () => {
382
+ it('drops a granted group the owner no longer belongs to', async () => {
383
+ memberOf = ['g2'];
384
+ mockedVerify.mockResolvedValue(
385
+ okVerification({ tenants: { allowed: [PERSONAL_TENANT, 'g1', 'g2'], default: PERSONAL_TENANT } })
386
+ );
387
+ const request = withKey();
388
+
389
+ await authenticateAgent()(request, mockRes(), jest.fn());
390
+
391
+ expect(request.userGroups).toEqual(['g2']);
392
+ expect(request.apiKey?.tenants.allowed).toEqual([PERSONAL_TENANT, 'g2']);
393
+ });
394
+
395
+ it('refuses a request for a group the owner has left, although the key was granted it', async () => {
396
+ memberOf = [];
397
+ mockedVerify.mockResolvedValue(okVerification({ tenants: grant(PERSONAL_TENANT, 'g1') }));
398
+ const res = mockRes();
399
+ const next = jest.fn();
400
+
401
+ await authenticateAgent()(withKey(undefined, { query: { groupId: 'g1' } } as Partial<Request>), res, next);
402
+
403
+ expect(res.status).toHaveBeenCalledWith(403);
404
+ expect(next).not.toHaveBeenCalled();
405
+ });
406
+
407
+ it('refuses a key whose only group the owner has left, rather than acting somewhere else', async () => {
408
+ memberOf = [];
409
+ mockedVerify.mockResolvedValue(okVerification({ tenants: grant('g1') }));
410
+ const res = mockRes();
411
+ const next = jest.fn();
412
+
413
+ await authenticateAgent()(withKey(), res, next);
414
+
415
+ expect(res.status).toHaveBeenCalledWith(403);
416
+ expect(next).not.toHaveBeenCalled();
417
+ });
418
+
419
+ it('asks about the key owner, by email', async () => {
420
+ const lookup = jest.fn(async () => ['g1']);
421
+ overrideMembershipLookupForTests(lookup);
422
+ mockedVerify.mockResolvedValue(okVerification({ tenants: grant('g1') }));
423
+
424
+ await authenticateAgent()(withKey(), mockRes(), jest.fn());
425
+
426
+ expect(lookup).toHaveBeenCalledWith({ email: 'u1@example.com' });
427
+ });
428
+ });
429
+
362
430
  describe('requireScope', () => {
363
431
  const keyReq = (scopes: string[]) =>
364
432
  req({
@@ -17,6 +17,7 @@ import {
17
17
  type Tenant
18
18
  } from './types';
19
19
  import { isCurrentSession } from '../auth/session';
20
+ import { currentGroupsOf } from '../groups/membership';
20
21
 
21
22
  /**
22
23
  * Request-scoped facts about the key a request arrived on. Absent on ordinary
@@ -148,13 +149,13 @@ interface AgentCredential {
148
149
  * Settling that in one place is what keeps the two from drifting into subtly
149
150
  * different amounts of access.
150
151
  */
151
- function admitAgent(
152
+ async function admitAgent(
152
153
  req: Request,
153
154
  res: Response,
154
155
  next: NextFunction,
155
156
  requiredScopes: ApiKeyScope[],
156
157
  credential: AgentCredential
157
- ): void {
158
+ ): Promise<void> {
158
159
  const missing = requiredScopes.filter((scope) => !credential.scopes.includes(scope));
159
160
  if (missing.length > 0) {
160
161
  res.status(403).json({
@@ -164,7 +165,16 @@ function admitAgent(
164
165
  return;
165
166
  }
166
167
 
167
- const actingAs = applyTenantGrant(req, credential.tenants);
168
+ // A grant names the groups the owner belonged to when they made it. Only the
169
+ // ones they still belong to count, so leaving a group also takes it from
170
+ // every key and connection they gave it to.
171
+ const stillMemberOf = new Set(await currentGroupsOf({ email: credential.email }));
172
+ const tenants: ApiKeyTenant = {
173
+ ...credential.tenants,
174
+ allowed: credential.tenants.allowed.filter((tenant) => tenant === PERSONAL_TENANT || stillMemberOf.has(tenant))
175
+ };
176
+
177
+ const actingAs = applyTenantGrant(req, tenants);
168
178
  if (actingAs === null) {
169
179
  res.status(403).json({
170
180
  success: false,
@@ -177,11 +187,11 @@ function admitAgent(
177
187
  // Only the granted groups, never the owner's full membership: this is what
178
188
  // stops a credential reaching a group it was not issued for through any
179
189
  // handler that consults `userGroups` instead of the `groupId` parameter.
180
- req.userGroups = groupIdsOf(credential.tenants.allowed);
190
+ req.userGroups = groupIdsOf(tenants.allowed);
181
191
  req.apiKey = {
182
192
  keyId: credential.credentialId,
183
193
  scopes: credential.scopes,
184
- tenants: credential.tenants,
194
+ tenants,
185
195
  actingAs,
186
196
  kind: credential.kind,
187
197
  label: credential.label,
@@ -228,9 +238,9 @@ export const authenticateAgent = (...requiredScopes: ApiKeyScope[]): RequestHand
228
238
  unauthorized(res);
229
239
  return;
230
240
  }
231
- const user = claims as UserPayload;
232
- req.user = user;
233
- req.userGroups = user.groups ?? [];
241
+ const groups = await currentGroupsOf(claims as UserPayload);
242
+ req.user = { ...(claims as UserPayload), groups };
243
+ req.userGroups = groups;
234
244
  beginAudit(req, res, next);
235
245
  return;
236
246
  }
@@ -245,7 +255,7 @@ export const authenticateAgent = (...requiredScopes: ApiKeyScope[]): RequestHand
245
255
  // `undefined`, scoped writes were refused as inaccessible, and a read
246
256
  // whose scope collapsed to nothing fell through to "no user context" and
247
257
  // answered from the whole collection.
248
- admitAgent(req, res, next, requiredScopes, {
258
+ await admitAgent(req, res, next, requiredScopes, {
249
259
  // The client, not the token: `jti` is a new value every hour.
250
260
  credentialId: claims.clientId ?? 'unknown-client',
251
261
  kind: 'oauth',
@@ -268,7 +278,7 @@ export const authenticateAgent = (...requiredScopes: ApiKeyScope[]): RequestHand
268
278
  return;
269
279
  }
270
280
 
271
- admitAgent(req, res, next, requiredScopes, {
281
+ await admitAgent(req, res, next, requiredScopes, {
272
282
  credentialId: result.keyId!,
273
283
  kind: 'api_key',
274
284
  label: result.label || 'An API key',
@@ -76,7 +76,7 @@ interface GroupOwnerResponse {
76
76
  * round, out through the proxy and back, carrying the internal token across the
77
77
  * public edge.
78
78
  */
79
- function internalGroupUrl(): string | undefined {
79
+ export function internalGroupUrl(): string | undefined {
80
80
  return process.env.GROUP_INTERNAL_URL || process.env.GROUP_API_URL;
81
81
  }
82
82
 
@@ -3,3 +3,5 @@ export type { GroupOwner } from './client';
3
3
 
4
4
  export { groupOwnerSubject } from './subject';
5
5
  export type { GroupIdResolver } from './subject';
6
+
7
+ export { getUserGroupIds, currentGroupsOf, clearMembershipCache } from './membership';
@@ -0,0 +1,84 @@
1
+ import { clearMembershipCache, currentGroupsOf, getUserGroupIds } from './membership';
2
+ import logger from '../logging/logger';
3
+
4
+ jest.mock('../logging/logger', () => ({ __esModule: true, default: { error: jest.fn(), warn: jest.fn() } }));
5
+
6
+ const ORIGINAL_ENV = process.env;
7
+ const answer = (data: unknown, ok = true, status = 200) =>
8
+ ({ ok, status, json: async () => ({ success: ok, data }) }) as unknown as Response;
9
+
10
+ beforeEach(() => {
11
+ clearMembershipCache();
12
+ jest.useRealTimers();
13
+ process.env = {
14
+ ...ORIGINAL_ENV,
15
+ GROUP_INTERNAL_URL: 'http://group-service:5006',
16
+ INTERNAL_SERVICE_TOKEN: 'internal-token',
17
+ SERVICE_NAME: 'album-service'
18
+ };
19
+ global.fetch = jest.fn().mockResolvedValue(answer(['g1', 'g2']));
20
+ });
21
+
22
+ afterAll(() => {
23
+ process.env = ORIGINAL_ENV;
24
+ });
25
+
26
+ it('asks group-service over the internal channel', async () => {
27
+ await expect(getUserGroupIds('Ada@Example.com')).resolves.toEqual(['g1', 'g2']);
28
+
29
+ expect(global.fetch).toHaveBeenCalledWith(
30
+ 'http://group-service:5006/api/groups/mine/ids?userEmail=ada%40example.com',
31
+ expect.objectContaining({
32
+ headers: expect.objectContaining({ 'x-internal-token': 'internal-token', 'x-service-id': 'album-service' })
33
+ })
34
+ );
35
+ });
36
+
37
+ it('answers from the cache for 30 seconds, then asks again', async () => {
38
+ jest.useFakeTimers();
39
+
40
+ await getUserGroupIds('ada@example.com');
41
+ await getUserGroupIds('ADA@example.com');
42
+ expect(global.fetch).toHaveBeenCalledTimes(1);
43
+
44
+ jest.advanceTimersByTime(30_001);
45
+ await getUserGroupIds('ada@example.com');
46
+ expect(global.fetch).toHaveBeenCalledTimes(2);
47
+ });
48
+
49
+ it('shares one request between callers asking at the same time', async () => {
50
+ await Promise.all([getUserGroupIds('ada@example.com'), getUserGroupIds('ada@example.com')]);
51
+
52
+ expect(global.fetch).toHaveBeenCalledTimes(1);
53
+ });
54
+
55
+ it('keeps the last known membership while group-service is unreachable', async () => {
56
+ jest.useFakeTimers();
57
+ await getUserGroupIds('ada@example.com');
58
+ jest.advanceTimersByTime(30_001);
59
+ (global.fetch as jest.Mock).mockRejectedValue(new Error('connect ECONNREFUSED'));
60
+
61
+ await expect(getUserGroupIds('ada@example.com')).resolves.toEqual(['g1', 'g2']);
62
+ expect(logger.warn).toHaveBeenCalled();
63
+ });
64
+
65
+ it('grants no groups when it has never been able to ask', async () => {
66
+ (global.fetch as jest.Mock).mockResolvedValue(answer(null, false, 503));
67
+
68
+ await expect(getUserGroupIds('ada@example.com')).resolves.toEqual([]);
69
+ expect(logger.error).toHaveBeenCalled();
70
+ });
71
+
72
+ it('grants no groups when not configured, without calling anything', async () => {
73
+ delete process.env.GROUP_INTERNAL_URL;
74
+ delete process.env.GROUP_API_URL;
75
+
76
+ await expect(getUserGroupIds('ada@example.com')).resolves.toEqual([]);
77
+ expect(global.fetch).not.toHaveBeenCalled();
78
+ });
79
+
80
+ it('never takes the groups from the token', async () => {
81
+ (global.fetch as jest.Mock).mockResolvedValue(answer(['g2']));
82
+
83
+ await expect(currentGroupsOf({ email: 'ada@example.com', groups: ['g1', 'g9'] })).resolves.toEqual(['g2']);
84
+ });
@@ -0,0 +1,133 @@
1
+ import logger from '../logging/logger';
2
+ import { internalGroupUrl } from './client';
3
+
4
+ /**
5
+ * Which groups a person belongs to, asked of group-service.
6
+ *
7
+ * This used to travel in the session token's `groups` claim, which made it only
8
+ * as fresh as the token: a new member could not see the group until their next
9
+ * renewal, and a removed one kept access until theirs, which is why membership
10
+ * changes used to sign people out. Asked here instead, with a short cache, a
11
+ * change reaches every service within CACHE_TTL_MS.
12
+ *
13
+ * Every request pays for this only once per person per service per cache
14
+ * window; concurrent callers share one request.
15
+ */
16
+ const CACHE_TTL_MS = 30_000;
17
+
18
+ /** Bounded so a service under enumeration can't grow the cache without limit. */
19
+ const MAX_CACHE_ENTRIES = 5_000;
20
+
21
+ const REQUEST_TIMEOUT_MS = 3_000;
22
+
23
+ interface CacheEntry {
24
+ groups: string[];
25
+ expiresAt: number;
26
+ }
27
+
28
+ const cache = new Map<string, CacheEntry>();
29
+ const inFlight = new Map<string, Promise<string[] | null>>();
30
+
31
+ function putInCache(email: string, groups: string[]): void {
32
+ if (!cache.has(email) && cache.size >= MAX_CACHE_ENTRIES) {
33
+ // Map iterates in insertion order, so the first key is the oldest write.
34
+ const oldest = cache.keys().next();
35
+ if (!oldest.done) cache.delete(oldest.value);
36
+ }
37
+ cache.set(email, { groups, expiresAt: Date.now() + CACHE_TTL_MS });
38
+ }
39
+
40
+ async function fetchUserGroupIds(email: string): Promise<string[] | null> {
41
+ const baseUrl = internalGroupUrl();
42
+ const token = process.env.INTERNAL_SERVICE_TOKEN;
43
+
44
+ if (!baseUrl || !token) {
45
+ logger.error('Group membership lookup not configured', {
46
+ hasGroupApiUrl: Boolean(baseUrl),
47
+ hasInternalToken: Boolean(token)
48
+ });
49
+ return null;
50
+ }
51
+
52
+ try {
53
+ const response = await fetch(`${baseUrl}/api/groups/mine/ids?userEmail=${encodeURIComponent(email)}`, {
54
+ method: 'GET',
55
+ headers: {
56
+ 'Content-Type': 'application/json',
57
+ 'x-internal-token': token,
58
+ 'x-service-id': process.env.SERVICE_NAME || 'unknown-service'
59
+ },
60
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
61
+ });
62
+
63
+ if (!response.ok) {
64
+ logger.error('Group membership lookup failed', { email, status: response.status });
65
+ return null;
66
+ }
67
+
68
+ const body = (await response.json()) as { success?: boolean; data?: unknown };
69
+ if (!body.success || !Array.isArray(body.data)) {
70
+ logger.error('Group membership lookup returned no list', { email });
71
+ return null;
72
+ }
73
+ return body.data.filter((id): id is string => typeof id === 'string');
74
+ } catch (error) {
75
+ logger.error('Group membership lookup unreachable', { email, error: (error as Error)?.message });
76
+ return null;
77
+ }
78
+ }
79
+
80
+ /**
81
+ * The ids of the groups `email` belongs to right now.
82
+ *
83
+ * When group-service cannot be asked, the last answer is used even if it has
84
+ * expired, and failing that no groups at all: a person keeps their own data
85
+ * through an outage and loses group data until it ends, rather than being
86
+ * granted a group on a guess.
87
+ */
88
+ export async function getUserGroupIds(email: string): Promise<string[]> {
89
+ const key = email.toLowerCase();
90
+ const entry = cache.get(key);
91
+ if (entry && entry.expiresAt > Date.now()) return entry.groups;
92
+
93
+ let pending = inFlight.get(key);
94
+ if (!pending) {
95
+ pending = fetchUserGroupIds(key).finally(() => inFlight.delete(key));
96
+ inFlight.set(key, pending);
97
+ }
98
+
99
+ const groups = await pending;
100
+ if (groups) {
101
+ putInCache(key, groups);
102
+ return groups;
103
+ }
104
+ if (entry) {
105
+ logger.warn('Using the last known group membership while group-service is unreachable', { email: key });
106
+ return entry.groups;
107
+ }
108
+ return [];
109
+ }
110
+
111
+ /** For tests. */
112
+ export function clearMembershipCache(): void {
113
+ cache.clear();
114
+ inFlight.clear();
115
+ }
116
+
117
+ type MembershipLookup = (user: { email: string; groups?: string[] }) => Promise<string[]>;
118
+
119
+ const lookUpMembership: MembershipLookup = (user) => getUserGroupIds(user.email);
120
+ let membershipLookup: MembershipLookup = lookUpMembership;
121
+
122
+ /** The groups the auth middlewares put on `req.userGroups`. */
123
+ export const currentGroupsOf = (user: { email: string; groups?: string[] }): Promise<string[]> =>
124
+ membershipLookup(user);
125
+
126
+ /**
127
+ * Replaces the lookup, for tests only; pass nothing to restore the real one.
128
+ * Service suites reach it through `@tumbaland/backend-core/testing`, which makes
129
+ * a test token's own `groups` claim the membership, so they need no group-service.
130
+ */
131
+ export function overrideMembershipLookupForTests(lookup?: MembershipLookup): void {
132
+ membershipLookup = lookup ?? lookUpMembership;
133
+ }
@@ -5,6 +5,7 @@ import { PERSONAL_TENANT } from '../apiKeys/types';
5
5
  import { mintAccessToken } from '../oauth/tokens';
6
6
  import { UserPayload } from '../types/auth';
7
7
  import { overrideSessionCheckForTests } from '../auth/session';
8
+ import { overrideMembershipLookupForTests } from '../groups/membership';
8
9
 
9
10
  // These tokens carry no session; the session check has its own tests below and in session.test.ts.
10
11
  beforeEach(() => overrideSessionCheckForTests(async () => true));
@@ -57,14 +58,19 @@ describe('authenticateToken', () => {
57
58
  expect(req.userGroups).toEqual([]);
58
59
  });
59
60
 
60
- it('attaches userGroups from the token when present', async () => {
61
- const token = jwt.sign({ ...payload, groups: ['group-1', 'group-2'] }, 'test-secret');
61
+ it("takes the person's groups from group-service, not from the token", async () => {
62
+ const lookup = jest.fn(async () => ['live-group']);
63
+ overrideMembershipLookupForTests(lookup);
64
+ const token = jwt.sign({ ...payload, groups: ['stale-group'] }, 'test-secret');
62
65
  const req = mockReq({ cookies: { access_token: token } });
63
66
  const res = mockRes();
64
67
 
65
68
  await authenticateToken(req, res, next);
66
69
 
67
- expect(req.userGroups).toEqual(['group-1', 'group-2']);
70
+ expect(lookup).toHaveBeenCalledWith(expect.objectContaining({ email: 'user@example.com' }));
71
+ expect(req.userGroups).toEqual(['live-group']);
72
+ expect(req.user?.groups).toEqual(['live-group']);
73
+ overrideMembershipLookupForTests();
68
74
  });
69
75
 
70
76
  it('accepts a Bearer token from the Authorization header', async () => {
@@ -4,6 +4,7 @@ import { requireEnv } from '../config/env';
4
4
  import { isAccessTokenClaims } from '../oauth/tokens';
5
5
  import { UserPayload } from '../types/auth';
6
6
  import { isCurrentSession } from '../auth/session';
7
+ import { currentGroupsOf } from '../groups/membership';
7
8
 
8
9
  /**
9
10
  * JWT authentication middleware
@@ -43,9 +44,11 @@ export async function authenticateToken(req: Request, res: Response, next: NextF
43
44
  res.status(401).json({ success: false, message: 'Session has ended' });
44
45
  return;
45
46
  }
46
- const user = claims as UserPayload;
47
- req.user = user;
48
- req.userGroups = user.groups ?? [];
47
+ // Membership is asked of group-service, not read from the token, so a
48
+ // change applies within seconds rather than at the next renewal.
49
+ const groups = await currentGroupsOf(claims as UserPayload);
50
+ req.user = { ...(claims as UserPayload), groups };
51
+ req.userGroups = groups;
49
52
  } catch (error) {
50
53
  res.status(401).json({ success: false, message: 'Invalid or expired token' });
51
54
  return;
@@ -76,9 +79,9 @@ export async function optionalAuth(req: Request, _res: Response, next: NextFunct
76
79
  // expired cookie, so the route still answers with its public form.
77
80
  // An ended session is treated as none, like an expired cookie.
78
81
  if (!isAccessTokenClaims(claims) && (await isCurrentSession(claims))) {
79
- const user = claims as UserPayload;
80
- req.user = user;
81
- req.userGroups = user.groups ?? [];
82
+ const groups = await currentGroupsOf(claims as UserPayload);
83
+ req.user = { ...(claims as UserPayload), groups };
84
+ req.userGroups = groups;
82
85
  }
83
86
  }
84
87
  } catch {
@@ -0,0 +1,17 @@
1
+ import { overrideSessionCheckForTests } from '../auth/session';
2
+ import { overrideMembershipLookupForTests } from '../groups/membership';
3
+
4
+ /**
5
+ * Jest setup for service test suites: they sign session tokens without a
6
+ * session and run with no database or group-service, so every token's session
7
+ * counts as live and its `groups` claim stands in for group membership.
8
+ * The session check itself is tested in backend-core (auth/session.test.ts).
9
+ *
10
+ * Loaded through the package's test-only entry point:
11
+ * setupFiles: ['@tumbaland/backend-core/testing']
12
+ */
13
+ overrideSessionCheckForTests(async () => true);
14
+
15
+ // No group-service either: a test token's own `groups` claim is taken as the
16
+ // person's membership. The live lookup is tested in groups/membership.test.ts.
17
+ overrideMembershipLookupForTests(async (user) => user.groups ?? []);
package/testing.js CHANGED
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Test-only entry point: `@tumbaland/backend-core/testing`.
3
3
  *
4
- * Loading it makes every session token's session count as live, for service
5
- * test suites that sign tokens without a session and run with no database.
4
+ * Loading it makes every session token's session count as live, and a test
5
+ * token's `groups` claim its group membership, for service test suites that
6
+ * run with no database and no group-service.
6
7
  * Kept out of the package's main exports so production code never reaches it.
7
8
  * The session check itself is tested in backend-core (src/auth/session.test.ts).
8
9
  */
9
- require('./dist/testing/allowAllSessions');
10
+ require('./dist/testing/serviceTestSetup');
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=allowAllSessions.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"allowAllSessions.d.ts","sourceRoot":"","sources":["../../src/testing/allowAllSessions.ts"],"names":[],"mappings":""}
@@ -1,13 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const session_1 = require("../auth/session");
4
- /**
5
- * Jest setup for service test suites: they sign session tokens without a
6
- * session and run with no database, so every token's session counts as live.
7
- * The session check itself is tested in backend-core (auth/session.test.ts).
8
- *
9
- * Loaded through the package's test-only entry point:
10
- * setupFiles: ['@tumbaland/backend-core/testing']
11
- */
12
- (0, session_1.overrideSessionCheckForTests)(async () => true);
13
- //# sourceMappingURL=allowAllSessions.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"allowAllSessions.js","sourceRoot":"","sources":["../../src/testing/allowAllSessions.ts"],"names":[],"mappings":";;AAAA,6CAA+D;AAE/D;;;;;;;GAOG;AACH,IAAA,sCAA4B,EAAC,KAAK,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC"}
@@ -1,11 +0,0 @@
1
- import { overrideSessionCheckForTests } from '../auth/session';
2
-
3
- /**
4
- * Jest setup for service test suites: they sign session tokens without a
5
- * session and run with no database, so every token's session counts as live.
6
- * The session check itself is tested in backend-core (auth/session.test.ts).
7
- *
8
- * Loaded through the package's test-only entry point:
9
- * setupFiles: ['@tumbaland/backend-core/testing']
10
- */
11
- overrideSessionCheckForTests(async () => true);