@udibo/oauth2 0.2.0 → 0.3.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.
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Contract test suite for a Udibo tenant's authorization answers: what
|
|
3
|
-
* introspection reports about a signed-in person,
|
|
4
|
-
* and `POST /api/check/batch`
|
|
3
|
+
* introspection reports about a signed-in person, what `GET /api/memberships`
|
|
4
|
+
* lists for them, and what `POST /api/check` and `POST /api/check/batch`
|
|
5
|
+
* answer.
|
|
5
6
|
*
|
|
6
7
|
* It runs against `createFakeTenant` from `@udibo/oauth2/testing` and against the
|
|
7
8
|
* real identity service in Udibo's own repository, so the fake an app tests
|
|
@@ -50,7 +51,10 @@ export interface TenantContractFixture {
|
|
|
50
51
|
addUser(permissions: string[]): Promise<string>;
|
|
51
52
|
/** Adds an organization and returns its id. */
|
|
52
53
|
addOrganization(): Promise<string>;
|
|
53
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* Makes a person a `member` of an organization, holding `permissions`
|
|
56
|
+
* inside it.
|
|
57
|
+
*/
|
|
54
58
|
addMember(organizationId: string, userId: string, permissions: string[]): Promise<void>;
|
|
55
59
|
/** Ends a membership. */
|
|
56
60
|
removeMember(organizationId: string, userId: string): Promise<void>;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Contract test suite for a Udibo tenant's authorization answers: what
|
|
3
|
-
* introspection reports about a signed-in person,
|
|
4
|
-
* and `POST /api/check/batch`
|
|
3
|
+
* introspection reports about a signed-in person, what `GET /api/memberships`
|
|
4
|
+
* lists for them, and what `POST /api/check` and `POST /api/check/batch`
|
|
5
|
+
* answer.
|
|
5
6
|
*
|
|
6
7
|
* It runs against `createFakeTenant` from `@udibo/oauth2/testing` and against the
|
|
7
8
|
* real identity service in Udibo's own repository, so the fake an app tests
|
|
@@ -152,6 +153,26 @@ export function runTenantContractTests(options) {
|
|
|
152
153
|
assertEquals(sorted(claims.permissions), []);
|
|
153
154
|
});
|
|
154
155
|
});
|
|
156
|
+
describe("GET /api/memberships", () => {
|
|
157
|
+
it("lists the organizations the caller belongs to, and no others", async () => {
|
|
158
|
+
const { access_token } = await tenant.signIn(people.member);
|
|
159
|
+
const response = await send(new URL("/api/memberships", tenant.issuer), { headers: { authorization: `Bearer ${access_token}` } });
|
|
160
|
+
assertEquals(response.status, 200);
|
|
161
|
+
const body = await response.json();
|
|
162
|
+
assertEquals(body.memberships.map((membership) => membership.org_id).sort(), [organizations.home, organizations.other].sort());
|
|
163
|
+
for (const membership of body.memberships) {
|
|
164
|
+
assertEquals(membership.roles, ["member"]);
|
|
165
|
+
assert(membership.org_slug, "every membership names its slug");
|
|
166
|
+
assert(membership.name, "every membership names its organization");
|
|
167
|
+
}
|
|
168
|
+
assertEquals(body.cursor, null);
|
|
169
|
+
});
|
|
170
|
+
it("refuses a caller with no bearer token", async () => {
|
|
171
|
+
const response = await send(new URL("/api/memberships", tenant.issuer));
|
|
172
|
+
await response.body?.cancel();
|
|
173
|
+
assertEquals(response.status, 401);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
155
176
|
describe("POST /api/check", () => {
|
|
156
177
|
it("answers the credential's own organization when no resource is named", async () => {
|
|
157
178
|
const { access_token } = await tenant.signIn(people.member, organizations.home);
|
package/esm/testing/tenant.d.ts
CHANGED
|
@@ -32,10 +32,15 @@ export interface FakeTenantOrganization {
|
|
|
32
32
|
id: string;
|
|
33
33
|
/** The handle `org_slug` carries. Never an authorization key. */
|
|
34
34
|
slug: string;
|
|
35
|
+
/** The display name `/api/memberships` reports. Defaults to the slug. */
|
|
36
|
+
name?: string;
|
|
35
37
|
}
|
|
36
38
|
/** A person's place in one organization. */
|
|
37
39
|
export interface FakeTenantMembership {
|
|
38
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* The membership's roles, as `org_roles` and `/api/memberships` report
|
|
42
|
+
* them — `member`, `admin` or `owner` on a real tenant.
|
|
43
|
+
*/
|
|
39
44
|
roles?: string[];
|
|
40
45
|
/** Permissions held inside this organization only. */
|
|
41
46
|
permissions?: string[];
|
|
@@ -99,9 +104,10 @@ export interface FakeTenantOptions {
|
|
|
99
104
|
/**
|
|
100
105
|
* A stand-in for a Udibo tenant, for testing an app that signs in against one
|
|
101
106
|
* without running the identity service. It answers the tenant's protocol
|
|
102
|
-
* surface — discovery, authorize, token, introspection, UserInfo, JWKS —
|
|
103
|
-
*
|
|
104
|
-
* `POST /api/check/batch`, with the same shapes a real
|
|
107
|
+
* surface — discovery, authorize, token, introspection, UserInfo, JWKS — the
|
|
108
|
+
* caller's own `GET /api/memberships`, and the two authorization questions,
|
|
109
|
+
* `POST /api/check` and `POST /api/check/batch`, with the same shapes a real
|
|
110
|
+
* tenant uses.
|
|
105
111
|
*
|
|
106
112
|
* Nothing here is interactive: {@linkcode signInAs} decides who the next
|
|
107
113
|
* authorization request authenticates, and there is no consent step. Serve
|
package/esm/testing/tenant.js
CHANGED
|
@@ -277,6 +277,22 @@ export async function createFakeTenant(options) {
|
|
|
277
277
|
}
|
|
278
278
|
return organization.id;
|
|
279
279
|
};
|
|
280
|
+
const memberships = async (request) => {
|
|
281
|
+
const caller = await authenticated(request);
|
|
282
|
+
if (caller instanceof Response)
|
|
283
|
+
return caller;
|
|
284
|
+
return Response.json({
|
|
285
|
+
memberships: [...organizations.values()]
|
|
286
|
+
.filter((organization) => organization.members.has(caller.user.id))
|
|
287
|
+
.map((organization) => ({
|
|
288
|
+
org_id: organization.id,
|
|
289
|
+
org_slug: organization.slug,
|
|
290
|
+
name: organization.name ?? organization.slug,
|
|
291
|
+
roles: organization.members.get(caller.user.id).roles ?? [],
|
|
292
|
+
})),
|
|
293
|
+
cursor: null,
|
|
294
|
+
});
|
|
295
|
+
};
|
|
280
296
|
const authorize = async (request) => {
|
|
281
297
|
const chosen = signedIn;
|
|
282
298
|
const requested = new URL(request.url).searchParams.get("organization");
|
|
@@ -327,6 +343,7 @@ export async function createFakeTenant(options) {
|
|
|
327
343
|
[`GET ${PATHS.userinfo}`]: (r) => server.handleUserInfoRequest(r),
|
|
328
344
|
[`POST ${PATHS.userinfo}`]: (r) => server.handleUserInfoRequest(r),
|
|
329
345
|
[`GET ${PATHS.jwks}`]: (r) => server.handleJwksRequest(r),
|
|
346
|
+
"GET /api/memberships": memberships,
|
|
330
347
|
"POST /api/check": check,
|
|
331
348
|
"POST /api/check/batch": checkBatch,
|
|
332
349
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@udibo/oauth2",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A flexible OAuth2 toolkit: framework-agnostic authorization & resource servers, a typed client, Hono adapters (including a backend-for-frontend), and React bindings.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|