cauthy-auth 0.28.1 → 0.31.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.
- package/README.md +61 -3
- package/dist/{cauthy-CTVg4kvo.d.mts → cauthy-2jtP2LVQ.d.mts} +19 -1
- package/dist/{cauthy-BAwAJChs.d.ts → cauthy-CKfvvv_i.d.ts} +19 -1
- package/dist/cli.js +163 -59
- package/dist/client.d.mts +91 -10
- package/dist/client.d.ts +91 -10
- package/dist/client.js +39 -3
- package/dist/client.js.map +1 -1
- package/dist/client.mjs +39 -3
- package/dist/client.mjs.map +1 -1
- package/dist/express.d.mts +2 -2
- package/dist/express.d.ts +2 -2
- package/dist/fastify.d.mts +2 -2
- package/dist/fastify.d.ts +2 -2
- package/dist/hono.d.mts +2 -2
- package/dist/hono.d.ts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +387 -148
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +385 -148
- package/dist/index.mjs.map +1 -1
- package/dist/next.d.mts +2 -2
- package/dist/next.d.ts +2 -2
- package/dist/react.d.mts +275 -23
- package/dist/react.d.ts +275 -23
- package/dist/react.js +312 -78
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +311 -78
- package/dist/react.mjs.map +1 -1
- package/dist/styles.css +8 -0
- package/dist/svelte.d.mts +2 -2
- package/dist/svelte.d.ts +2 -2
- package/dist/svelte.js +53 -4
- package/dist/svelte.js.map +1 -1
- package/dist/svelte.mjs +53 -4
- package/dist/svelte.mjs.map +1 -1
- package/dist/{system-CVBW_AV8.d.mts → system-BrrJsrFW.d.mts} +321 -109
- package/dist/{system-Bn8uckvQ.d.ts → system-BvvoNs2y.d.ts} +321 -109
- package/dist/vue.d.mts +2 -2
- package/dist/vue.d.ts +2 -2
- package/dist/vue.js +53 -4
- package/dist/vue.js.map +1 -1
- package/dist/vue.mjs +53 -4
- package/dist/vue.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,16 +47,21 @@ const { signIn } = useAuth()
|
|
|
47
47
|
const r = await signIn.email({ email, password }) // r.ok, r.status, r.error.field
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
What Cauthy mails opens your pages too: a new password, a confirmed address, a sign-in link.
|
|
50
|
+
What Cauthy mails opens your pages too: a new password, a confirmed address, a sign-in link, an invitation.
|
|
51
51
|
|
|
52
52
|
```ts
|
|
53
|
-
export const auth = createCauthy({ pages: { signIn: '/sign-in', resetPassword: '/new-password', verifyEmail: '/confirm' } })
|
|
53
|
+
export const auth = createCauthy({ pages: { signIn: '/sign-in', resetPassword: '/new-password', verifyEmail: '/confirm', acceptInvite: '/accept-invite' } })
|
|
54
54
|
```
|
|
55
55
|
|
|
56
56
|
```tsx
|
|
57
|
-
import { ResetPassword, VerifyEmail } from 'cauthy-auth/react'
|
|
57
|
+
import { AcceptInvite, ResetPassword, VerifyEmail } from 'cauthy-auth/react'
|
|
58
|
+
<AcceptInvite links={{ signIn: '/sign-in' }} returnTo="/" /> // "You've been invited to Acme", a password, signed in
|
|
59
|
+
|
|
60
|
+
const { invitation, acceptInvitation } = useAuth() // or with your own form
|
|
58
61
|
```
|
|
59
62
|
|
|
63
|
+
The forms follow what the workspace turned on in Cauthy's dashboard: each provider's button carries its logo (`classNames.providerIcon` styles it), and a workspace that turned off the password or passkeys does not see them offered. `methods()` says so: `passwordSignIn`, `passkey`, `providers[].iconUrl`.
|
|
64
|
+
|
|
60
65
|
And the person's account, in your application: name, password, second step, sessions.
|
|
61
66
|
|
|
62
67
|
```tsx
|
|
@@ -64,6 +69,33 @@ import { Account } from 'cauthy-auth/react'
|
|
|
64
69
|
<Account /> // or: const { account } = useAuth()
|
|
65
70
|
```
|
|
66
71
|
|
|
72
|
+
And the person's organizations, run by their own members: the people of one, and the organization itself. Cauthy decides each change by the person's role in the system's catalog, with their own session:
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
import { OrganizationMembers } from 'cauthy-auth/react'
|
|
76
|
+
<OrganizationMembers organizationId={org.id} /> // shows only what the person's role allows
|
|
77
|
+
|
|
78
|
+
const { organizations } = useAuth()
|
|
79
|
+
const [org] = await organizations.list() // org.can: { readMembers, manageMembers, removeMembers, update, archive, delete }
|
|
80
|
+
await organizations.add(org.id, { email: 'ana@example.com', role: 'reader' })
|
|
81
|
+
await organizations.setRole(org.id, userId, 'admin')
|
|
82
|
+
await organizations.remove(org.id, userId) // anybody may remove themselves: leave
|
|
83
|
+
await organizations.rename(org.id, 'Acme Books') // r.organization
|
|
84
|
+
await organizations.archive(org.id) // only the workspace restores it
|
|
85
|
+
await organizations.delete(org.id) // for good
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
| Reserved power | Lets a member |
|
|
89
|
+
| --- | --- |
|
|
90
|
+
| `cauthy:members:read` | see the people (`can.readMembers`) |
|
|
91
|
+
| `cauthy:members:manage` | add and invite people, change their role (`can.manageMembers`) |
|
|
92
|
+
| `cauthy:members:remove` | remove another member (`can.removeMembers`) |
|
|
93
|
+
| `cauthy:organization:update` | rename the organization (`can.update`) |
|
|
94
|
+
| `cauthy:organization:archive` | archive it (`can.archive`) |
|
|
95
|
+
| `cauthy:organization:delete` | delete it for good (`can.delete`) |
|
|
96
|
+
|
|
97
|
+
Every catalog has a reserved role, `owner`, with all of them. Only an owner gives the owner role, or changes or removes another owner, and an organization always keeps one: its last owner cannot be removed, demoted or leave (`last_owner`). On the server they read like any other permission: `can(organization, 'cauthy:organization', 'delete')`.
|
|
98
|
+
|
|
67
99
|
Roles and permissions with their names, from the catalog Cauthy has:
|
|
68
100
|
|
|
69
101
|
```bash
|
|
@@ -75,6 +107,30 @@ import { can, hasRole } from './policy'
|
|
|
75
107
|
can(organization, 'ledger:write') // only what the catalog names compiles
|
|
76
108
|
```
|
|
77
109
|
|
|
110
|
+
A system may also declare its resources: their actions, and the attributes a condition reads. A permission may then carry a condition, in CEL (`resource.attrs.amount <= 1000000`, `resource.owner_id == subject.id`). Such a permission is not in `permissions`: it comes in `organization.conditional`, `can` never counts it, and Cauthy decides it per question:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
const r = await auth.check(request, { organization, resource: 'invoices', action: 'approve', attributes: { amount: 500 } })
|
|
114
|
+
r.allowed, r.reason, r.rule // rule: which role or exception decided
|
|
115
|
+
|
|
116
|
+
await organizations.check({ organization, resource: 'invoices', action: 'approve', attributes: { amount: 500 } }) // in the browser
|
|
117
|
+
await auth.system.check({ organizationId, userId, resource: 'invoices', action: 'read' }) // about any member
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`--pull` writes the declared resources too, and a check is typed by them:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
export const policy = definePolicy({
|
|
124
|
+
catalog: [{ resource: 'invoices', actions: ['read', 'approve'], attributes: { amount: 'number', branch: 'string' } }],
|
|
125
|
+
roles: [{ role: 'accountant', permissions: [{ resource: 'invoices', action: 'approve', condition: 'resource.attrs.amount <= 1000000' }] }],
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
await policy.check(auth, request, { organization, resource: 'invoices', action: 'approve', attributes: { amount: 500 } })
|
|
129
|
+
// 'payroll', action 'delete' or amount: '500' do not compile
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`npx cauthy-auth policy --file policy.json --push` loads a first catalog: a list of roles, or `{ "catalog": [...], "roles": [...] }`, whose resources go first so the roles are checked against them. Pushing takes a personal access token with `systems:policy`.
|
|
133
|
+
|
|
78
134
|
Each framework has its own few lines:
|
|
79
135
|
|
|
80
136
|
| | |
|
|
@@ -115,6 +171,8 @@ const org = await auth.system.organizations.create({ name: 'Acme' })
|
|
|
115
171
|
await auth.system.members.add(org.id, { role: 'owner', email: 'ana@acme.com' })
|
|
116
172
|
```
|
|
117
173
|
|
|
174
|
+
Its invitations open your `pages.acceptInvite` when the application knows its public origin (`baseURL`, `CAUTHY_BASE_URL`); otherwise Cauthy's own page.
|
|
175
|
+
|
|
118
176
|
A backend that is handed a Cauthy access token checks it with Cauthy's published keys: no secret, no call per request.
|
|
119
177
|
|
|
120
178
|
```ts
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { J as JsonValue, S as Session,
|
|
1
|
+
import { J as JsonValue, S as Session, t as SessionStatus, s as SessionOrganization, f as CheckQuestion, k as OrganizationRef, g as CheckResult, d as CauthySystem, q as PublicSession, c as CauthyOptions } from './system-BrrJsrFW.mjs';
|
|
2
2
|
import { JWTVerifyGetKey } from 'jose';
|
|
3
3
|
|
|
4
4
|
/** Something that happened in Cauthy, as a subscriber is told. Moments are UTC. */
|
|
@@ -80,6 +80,24 @@ interface Cauthy {
|
|
|
80
80
|
}>;
|
|
81
81
|
/** The organizations the person belongs to (of `system`, when set), with their role in each. */
|
|
82
82
|
organizations(session: Session): Promise<SessionOrganization[]>;
|
|
83
|
+
/**
|
|
84
|
+
* Asks Cauthy whether the signed-in person may do an action on a resource
|
|
85
|
+
* in an organization, with what its conditions read (`attributes`, whose
|
|
86
|
+
* the record is). It is how a permission with a condition is decided —
|
|
87
|
+
* `can` never counts those — and it decides any other the same way.
|
|
88
|
+
*
|
|
89
|
+
* const r = await auth.check(request, { organization, resource: 'invoices', action: 'approve', attributes: { amount: 500 } })
|
|
90
|
+
* if (!r.allowed) return forbidden(r.reason)
|
|
91
|
+
*
|
|
92
|
+
* Nobody signed in, an organization the person is not in, or a disabled
|
|
93
|
+
* account answer `allowed: false`. A request whose session must be renewed
|
|
94
|
+
* first reads as nobody signed in: give it the `Session` from
|
|
95
|
+
* `getSession(request, responseHeaders)` instead. Throws `CauthyApiError`
|
|
96
|
+
* when Cauthy refuses the question itself (a malformed one) or cannot be reached.
|
|
97
|
+
*/
|
|
98
|
+
check(who: Request | Session, question: CheckQuestion & {
|
|
99
|
+
organization: OrganizationRef;
|
|
100
|
+
}): Promise<CheckResult>;
|
|
83
101
|
/**
|
|
84
102
|
* Verifies a webhook delivery from Cauthy and answers the event it carries;
|
|
85
103
|
* throws `CauthyWebhookError` for anything else. No secret is involved: the
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { J as JsonValue, S as Session,
|
|
1
|
+
import { J as JsonValue, S as Session, t as SessionStatus, s as SessionOrganization, f as CheckQuestion, k as OrganizationRef, g as CheckResult, d as CauthySystem, q as PublicSession, c as CauthyOptions } from './system-BvvoNs2y.js';
|
|
2
2
|
import { JWTVerifyGetKey } from 'jose';
|
|
3
3
|
|
|
4
4
|
/** Something that happened in Cauthy, as a subscriber is told. Moments are UTC. */
|
|
@@ -80,6 +80,24 @@ interface Cauthy {
|
|
|
80
80
|
}>;
|
|
81
81
|
/** The organizations the person belongs to (of `system`, when set), with their role in each. */
|
|
82
82
|
organizations(session: Session): Promise<SessionOrganization[]>;
|
|
83
|
+
/**
|
|
84
|
+
* Asks Cauthy whether the signed-in person may do an action on a resource
|
|
85
|
+
* in an organization, with what its conditions read (`attributes`, whose
|
|
86
|
+
* the record is). It is how a permission with a condition is decided —
|
|
87
|
+
* `can` never counts those — and it decides any other the same way.
|
|
88
|
+
*
|
|
89
|
+
* const r = await auth.check(request, { organization, resource: 'invoices', action: 'approve', attributes: { amount: 500 } })
|
|
90
|
+
* if (!r.allowed) return forbidden(r.reason)
|
|
91
|
+
*
|
|
92
|
+
* Nobody signed in, an organization the person is not in, or a disabled
|
|
93
|
+
* account answer `allowed: false`. A request whose session must be renewed
|
|
94
|
+
* first reads as nobody signed in: give it the `Session` from
|
|
95
|
+
* `getSession(request, responseHeaders)` instead. Throws `CauthyApiError`
|
|
96
|
+
* when Cauthy refuses the question itself (a malformed one) or cannot be reached.
|
|
97
|
+
*/
|
|
98
|
+
check(who: Request | Session, question: CheckQuestion & {
|
|
99
|
+
organization: OrganizationRef;
|
|
100
|
+
}): Promise<CheckResult>;
|
|
83
101
|
/**
|
|
84
102
|
* Verifies a webhook delivery from Cauthy and answers the event it carries;
|
|
85
103
|
* throws `CauthyWebhookError` for anything else. No secret is involved: the
|
package/dist/cli.js
CHANGED
|
@@ -368,73 +368,147 @@ ${declaration}() {
|
|
|
368
368
|
|
|
369
369
|
// src/permissions.ts
|
|
370
370
|
var IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
371
|
-
function policySource(system,
|
|
371
|
+
function policySource(system, read) {
|
|
372
|
+
const roles = Array.isArray(read) ? read : read.roles;
|
|
373
|
+
const catalog = Array.isArray(read) ? [] : read.catalog ?? [];
|
|
372
374
|
const text = (v) => JSON.stringify(v);
|
|
373
375
|
const key = (k) => IDENT.test(k) ? k : text(k);
|
|
374
|
-
const one = (r) => {
|
|
376
|
+
const one = (r, pad) => {
|
|
375
377
|
const parts = [`role: ${text(r.role)}`];
|
|
376
378
|
if (r.name) parts.push(`name: ${text(r.name)}`);
|
|
377
379
|
if (r.description) parts.push(`description: ${text(r.description)}`);
|
|
378
380
|
if (r.inherits?.length) parts.push(`inherits: [${r.inherits.map(text).join(", ")}]`);
|
|
379
381
|
if (r.permissions?.length) {
|
|
380
|
-
const list = r.permissions.map((p) =>
|
|
382
|
+
const list = r.permissions.map((p) => `${pad} { ${[
|
|
383
|
+
...p.kind && p.kind !== "*" ? [`${key("kind")}: ${text(p.kind)}`] : [],
|
|
384
|
+
`resource: ${text(p.resource)}`,
|
|
385
|
+
`action: ${text(p.action)}`,
|
|
386
|
+
...p.condition ? [`condition: ${text(p.condition)}`] : []
|
|
387
|
+
].join(", ")} },`);
|
|
381
388
|
parts.push(`permissions: [
|
|
382
389
|
${list.join("\n")}
|
|
383
|
-
]`);
|
|
390
|
+
${pad} ]`);
|
|
384
391
|
}
|
|
385
|
-
return
|
|
386
|
-
${parts.join(
|
|
387
|
-
|
|
392
|
+
return `${pad} {
|
|
393
|
+
${pad} ${parts.join(`,
|
|
394
|
+
${pad} `)},
|
|
395
|
+
${pad} },`;
|
|
388
396
|
};
|
|
389
|
-
|
|
397
|
+
const resource = (c) => {
|
|
398
|
+
const parts = [`resource: ${text(c.resource)}`];
|
|
399
|
+
if (c.description) parts.push(`description: ${text(c.description)}`);
|
|
400
|
+
parts.push(`actions: [${c.actions.map(text).join(", ")}]`);
|
|
401
|
+
const attrs = Object.entries(c.attributes ?? {});
|
|
402
|
+
if (attrs.length) parts.push(`attributes: { ${attrs.map(([k, v]) => `${key(k)}: ${text(v)}`).join(", ")} }`);
|
|
403
|
+
return ` { ${parts.join(", ")} },`;
|
|
404
|
+
};
|
|
405
|
+
const head = `// The catalog of roles of ${text(system)}, as Cauthy has it.
|
|
390
406
|
// Written by \`cauthy-auth policy --pull\`: run it again after the catalog
|
|
391
407
|
// changes, and do not edit this file by hand.
|
|
392
|
-
|
|
408
|
+
`;
|
|
409
|
+
if (catalog.length === 0) {
|
|
410
|
+
return `${head}
|
|
393
411
|
import { definePolicy, type PermissionOf, type RoleOf } from 'cauthy-auth'
|
|
394
412
|
|
|
395
413
|
export const policy = definePolicy([
|
|
396
|
-
${roles.map(one).join("\n")}
|
|
414
|
+
${roles.map((r) => one(r, "")).join("\n")}
|
|
397
415
|
])
|
|
398
416
|
|
|
399
417
|
export const { can, hasRole } = policy
|
|
400
418
|
export type Role = RoleOf<typeof policy>
|
|
401
419
|
export type Permission = PermissionOf<typeof policy>
|
|
402
420
|
`;
|
|
403
|
-
}
|
|
404
|
-
async function pullPolicy(o = {}) {
|
|
405
|
-
const env2 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
|
|
406
|
-
const issuer = withoutTrailingSlashes(o.issuer ?? env2("CAUTHY_ISSUER") ?? "");
|
|
407
|
-
const system = o.system ?? env2("CAUTHY_SYSTEM") ?? "";
|
|
408
|
-
const tenant = o.tenant ?? env2("CAUTHY_TENANT") ?? "";
|
|
409
|
-
const token = o.token ?? env2("CAUTHY_TOKEN") ?? "";
|
|
410
|
-
for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_SYSTEM: system, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
|
|
411
|
-
if (v === "") throw new Error(`cauthy-auth: ${name} is required to read a policy`);
|
|
412
421
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
422
|
+
return `${head}
|
|
423
|
+
import { definePolicy, type CheckOf, type PermissionOf, type RoleOf } from 'cauthy-auth'
|
|
424
|
+
|
|
425
|
+
export const policy = definePolicy({
|
|
426
|
+
catalog: [
|
|
427
|
+
${catalog.map(resource).join("\n")}
|
|
428
|
+
],
|
|
429
|
+
roles: [
|
|
430
|
+
${roles.map((r) => one(r, " ")).join("\n")}
|
|
431
|
+
],
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
export const { can, hasRole } = policy
|
|
435
|
+
export type Role = RoleOf<typeof policy>
|
|
436
|
+
export type Permission = PermissionOf<typeof policy>
|
|
437
|
+
/** What a check may ask: \`policy.check(auth, request, { organization, ...question })\`. */
|
|
438
|
+
export type Check = CheckOf<typeof policy>
|
|
439
|
+
`;
|
|
419
440
|
}
|
|
420
|
-
|
|
441
|
+
function reach(o, doing) {
|
|
421
442
|
const env2 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
443
|
+
const r = {
|
|
444
|
+
issuer: withoutTrailingSlashes(o.issuer ?? env2("CAUTHY_ISSUER") ?? ""),
|
|
445
|
+
system: o.system ?? env2("CAUTHY_SYSTEM") ?? "",
|
|
446
|
+
tenant: o.tenant ?? env2("CAUTHY_TENANT") ?? "",
|
|
447
|
+
token: o.token ?? env2("CAUTHY_TOKEN") ?? ""
|
|
448
|
+
};
|
|
449
|
+
for (const [name, v] of Object.entries({ CAUTHY_ISSUER: r.issuer, CAUTHY_SYSTEM: r.system, CAUTHY_TENANT: r.tenant, CAUTHY_TOKEN: r.token })) {
|
|
450
|
+
if (v === "") throw new Error(`cauthy-auth: ${name} is required to ${doing}`);
|
|
428
451
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
452
|
+
return { ...r, fetch: o.fetch ?? fetch };
|
|
453
|
+
}
|
|
454
|
+
async function systemCall(r, method, what, body) {
|
|
455
|
+
const res = await r.fetch(`${r.issuer}/v1/systems/${encodeURIComponent(r.system)}/${what}`, {
|
|
456
|
+
...method === "PUT" ? { method } : {},
|
|
457
|
+
headers: {
|
|
458
|
+
authorization: `Bearer ${r.token}`,
|
|
459
|
+
"x-tenant-key": r.tenant,
|
|
460
|
+
...body === void 0 ? { accept: "application/json" } : { "content-type": "application/json" }
|
|
461
|
+
},
|
|
462
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) }
|
|
433
463
|
});
|
|
464
|
+
const answer = await res.json().catch(() => null);
|
|
434
465
|
if (!res.ok) {
|
|
435
|
-
const
|
|
436
|
-
throw new Error(`cauthy-auth: Cauthy
|
|
466
|
+
const verb = method === "GET" ? `refused to give the ${what}` : `refused the ${what}`;
|
|
467
|
+
throw new Error(`cauthy-auth: Cauthy ${verb} (${res.status}): ${answer?.errors?.[0]?.message ?? res.statusText}`);
|
|
437
468
|
}
|
|
469
|
+
return answer?.data;
|
|
470
|
+
}
|
|
471
|
+
async function pullPolicy(o = {}) {
|
|
472
|
+
const data = await systemCall(reach(o, "read a policy"), "GET", "policy");
|
|
473
|
+
return data?.roles ?? [];
|
|
474
|
+
}
|
|
475
|
+
async function pullCatalog(o = {}) {
|
|
476
|
+
const data = await systemCall(reach(o, "read a catalog"), "GET", "catalog");
|
|
477
|
+
return data?.resources ?? [];
|
|
478
|
+
}
|
|
479
|
+
async function pushPolicy(o) {
|
|
480
|
+
const r = reach(o, "push a policy");
|
|
481
|
+
if (o.catalog !== void 0) await systemCall(r, "PUT", "catalog", { resources: o.catalog });
|
|
482
|
+
await systemCall(r, "PUT", "policy", { roles: o.roles });
|
|
483
|
+
}
|
|
484
|
+
function readPolicyFile(parsed) {
|
|
485
|
+
if (Array.isArray(parsed)) return { catalog: [], roles: parsed };
|
|
486
|
+
const o = parsed ?? {};
|
|
487
|
+
if (!Array.isArray(o.roles)) throw new Error('no "roles" list');
|
|
488
|
+
if (o.catalog !== void 0 && !Array.isArray(o.catalog)) throw new Error('"catalog" is not a list');
|
|
489
|
+
return { catalog: o.catalog ?? [], roles: o.roles };
|
|
490
|
+
}
|
|
491
|
+
function checkWire(q) {
|
|
492
|
+
return {
|
|
493
|
+
resource: q.resource,
|
|
494
|
+
action: q.action,
|
|
495
|
+
...q.resourceId !== void 0 ? { resource_id: q.resourceId } : {},
|
|
496
|
+
...q.ownerId !== void 0 ? { owner_id: q.ownerId } : {},
|
|
497
|
+
...q.attributes !== void 0 ? { attributes: q.attributes } : {}
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
function checkResultOf(d) {
|
|
501
|
+
const r = d?.rule;
|
|
502
|
+
const rule = r && typeof r === "object" ? {
|
|
503
|
+
source: r.source === "exception" ? "exception" : "role",
|
|
504
|
+
...r.role ? { role: String(r.role) } : {},
|
|
505
|
+
kind: String(r.kind ?? "*"),
|
|
506
|
+
resource: String(r.resource ?? ""),
|
|
507
|
+
action: String(r.action ?? ""),
|
|
508
|
+
effect: r.effect === "allow" ? "allow" : "deny",
|
|
509
|
+
...r.condition ? { condition: String(r.condition) } : {}
|
|
510
|
+
} : void 0;
|
|
511
|
+
return { allowed: d?.allowed === true, reason: String(d?.reason ?? ""), ...rule ? { rule } : {} };
|
|
438
512
|
}
|
|
439
513
|
|
|
440
514
|
// src/system.ts
|
|
@@ -914,6 +988,9 @@ function createSystem(given = {}) {
|
|
|
914
988
|
const clientSecret = given.clientSecret ?? env("CAUTHY_CLIENT_SECRET") ?? "";
|
|
915
989
|
const envKey = env("CAUTHY_SYSTEM_KEY");
|
|
916
990
|
const key = given.key ?? (envKey ? { privateKey: envKey, keyId: env("CAUTHY_SYSTEM_KEY_ID") ?? "" } : void 0);
|
|
991
|
+
const ownSystem = given.system ?? env("CAUTHY_SYSTEM") ?? "";
|
|
992
|
+
const acceptInviteUrl = given.acceptInviteUrl ?? env("CAUTHY_ACCEPT_INVITE_URL") ?? "";
|
|
993
|
+
const inviteHeaders = /^https?:\/\//.test(acceptInviteUrl) ? { "x-cauthy-client-id": clientId, "x-cauthy-link-to": acceptInviteUrl } : {};
|
|
917
994
|
const doFetch = (...args) => (given.fetch ?? fetch)(...args);
|
|
918
995
|
const tokenEndpoint = `${issuer}/v1/oauth/token`;
|
|
919
996
|
function configured() {
|
|
@@ -956,10 +1033,10 @@ function createSystem(given = {}) {
|
|
|
956
1033
|
asking = void 0;
|
|
957
1034
|
});
|
|
958
1035
|
}
|
|
959
|
-
async function call(method, path, body) {
|
|
1036
|
+
async function call(method, path, body, extra = {}) {
|
|
960
1037
|
const send = async () => doFetch(`${issuer}${path}`, {
|
|
961
1038
|
method,
|
|
962
|
-
headers: { authorization: `Bearer ${await token()}`, ...body === void 0 ? {} : { "content-type": "application/json" } },
|
|
1039
|
+
headers: { authorization: `Bearer ${await token()}`, ...extra, ...body === void 0 ? {} : { "content-type": "application/json" } },
|
|
963
1040
|
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
964
1041
|
});
|
|
965
1042
|
let res = await send();
|
|
@@ -970,7 +1047,7 @@ function createSystem(given = {}) {
|
|
|
970
1047
|
if (!res.ok) await fail2(res, `${method} ${path}`);
|
|
971
1048
|
return res.status === 204 ? void 0 : res.json();
|
|
972
1049
|
}
|
|
973
|
-
const data = async (method, path, body) => (await call(method, path, body))?.data;
|
|
1050
|
+
const data = async (method, path, body, extra) => (await call(method, path, body, extra))?.data;
|
|
974
1051
|
const id = encodeURIComponent;
|
|
975
1052
|
return {
|
|
976
1053
|
token,
|
|
@@ -1007,7 +1084,8 @@ function createSystem(given = {}) {
|
|
|
1007
1084
|
return toMember(await data(
|
|
1008
1085
|
"POST",
|
|
1009
1086
|
`/v1/organizations/${id(orgId)}/members`,
|
|
1010
|
-
{ role: m.role, user_id: m.userId, email: m.email, name: m.name, locale: m.locale, client_id: clientId }
|
|
1087
|
+
{ role: m.role, user_id: m.userId, email: m.email, name: m.name, locale: m.locale, client_id: clientId },
|
|
1088
|
+
inviteHeaders
|
|
1011
1089
|
));
|
|
1012
1090
|
},
|
|
1013
1091
|
async setRole(orgId, userId, role) {
|
|
@@ -1018,7 +1096,7 @@ function createSystem(given = {}) {
|
|
|
1018
1096
|
}
|
|
1019
1097
|
},
|
|
1020
1098
|
async invite(person) {
|
|
1021
|
-
const u = await data("POST", "/v1/users", { ...person, client_id: clientId });
|
|
1099
|
+
const u = await data("POST", "/v1/users", { ...person, client_id: clientId }, inviteHeaders);
|
|
1022
1100
|
return { id: u.id, email: u.email };
|
|
1023
1101
|
},
|
|
1024
1102
|
theme: {
|
|
@@ -1031,6 +1109,19 @@ function createSystem(given = {}) {
|
|
|
1031
1109
|
return (await data("GET", `/v1/systems/${id(system)}/policy`)).roles ?? [];
|
|
1032
1110
|
}
|
|
1033
1111
|
},
|
|
1112
|
+
catalog: {
|
|
1113
|
+
async get(system) {
|
|
1114
|
+
return (await data("GET", `/v1/systems/${id(system)}/catalog`)).resources ?? [];
|
|
1115
|
+
},
|
|
1116
|
+
async set(system, resources) {
|
|
1117
|
+
return (await data("PUT", `/v1/systems/${id(system)}/catalog`, { resources })).resources ?? [];
|
|
1118
|
+
}
|
|
1119
|
+
},
|
|
1120
|
+
async check(q) {
|
|
1121
|
+
const system = q.system ?? ownSystem;
|
|
1122
|
+
if (system === "") throw new CauthyConfigError("name the system to check in (system, or CAUTHY_SYSTEM)");
|
|
1123
|
+
return checkResultOf(await data("POST", `/v1/systems/${id(system)}/check`, { organization_id: q.organizationId, user_id: q.userId, ...checkWire(q) }));
|
|
1124
|
+
},
|
|
1034
1125
|
async publicJwks() {
|
|
1035
1126
|
if (!key) return { keys: [] };
|
|
1036
1127
|
const { kty, n, e } = asJwk(key) ?? await (0, import_jose.exportJWK)(await signingKey());
|
|
@@ -1194,10 +1285,12 @@ var POLICY_HELP = `cauthy-auth policy --pull [--out <file.ts>] [--system <name>]
|
|
|
1194
1285
|
cauthy-auth policy --file <roles.json> [--system <name>] [--push]
|
|
1195
1286
|
|
|
1196
1287
|
--pull writes the catalog Cauthy has as a TypeScript module, so the
|
|
1197
|
-
application's roles and permissions are typed from it
|
|
1288
|
+
application's roles and permissions are typed from it, and, when the system
|
|
1289
|
+
declares its resources, what a check may ask (resources, actions, attributes):
|
|
1198
1290
|
|
|
1199
|
-
import { can, hasRole } from './policy'
|
|
1291
|
+
import { can, hasRole, policy } from './policy'
|
|
1200
1292
|
can(organization, 'ledger:write')
|
|
1293
|
+
await policy.check(auth, request, { organization, resource: 'invoices', action: 'approve', attributes: { amount: 500 } })
|
|
1201
1294
|
|
|
1202
1295
|
--pull Read the catalog from Cauthy.
|
|
1203
1296
|
--out <file> Write the module there. Without it, it is printed.
|
|
@@ -1208,13 +1301,19 @@ CAUTHY_TOKEN (a personal access token with organizations:manage).
|
|
|
1208
1301
|
|
|
1209
1302
|
--file does the opposite, once:
|
|
1210
1303
|
|
|
1211
|
-
Loads a system's catalog of roles into Cauthy from a file:
|
|
1304
|
+
Loads a system's catalog of roles into Cauthy from a file: a list of roles,
|
|
1305
|
+
or the resources the system declares with them.
|
|
1212
1306
|
|
|
1213
|
-
{ "
|
|
1214
|
-
|
|
1215
|
-
|
|
1307
|
+
{ "catalog": [ { "resource": "invoices", "actions": ["read", "approve"],
|
|
1308
|
+
"attributes": { "amount": "number", "branch": "string" } } ],
|
|
1309
|
+
"roles": [ { "role": "accountant", "name": "Accountant", "inherits": ["reader"],
|
|
1310
|
+
"permissions": [ { "resource": "invoices", "action": "read" },
|
|
1311
|
+
{ "resource": "invoices", "action": "approve",
|
|
1312
|
+
"condition": "resource.attrs.amount <= 1000000" } ] } ] }
|
|
1216
1313
|
|
|
1217
|
-
--file <file> The catalog, as JSON.
|
|
1314
|
+
--file <file> The catalog, as JSON. Without "catalog", the resources
|
|
1315
|
+
Cauthy has stay as they are; with it, they are sent first
|
|
1316
|
+
and the roles are checked against them.
|
|
1218
1317
|
--system <name> The system it is of. Default: CAUTHY_SYSTEM.
|
|
1219
1318
|
--push Send it to Cauthy, REPLACING the catalog it has. Without it
|
|
1220
1319
|
the catalog is only checked and shown.
|
|
@@ -1223,7 +1322,7 @@ Use it to load a first catalog. Afterwards the catalog is Cauthy's, edited in
|
|
|
1223
1322
|
its dashboard: pushing again overwrites what was edited there.
|
|
1224
1323
|
|
|
1225
1324
|
To push: CAUTHY_ISSUER, CAUTHY_TENANT (the workspace's slug) and CAUTHY_TOKEN
|
|
1226
|
-
(a personal access token with
|
|
1325
|
+
(a personal access token with systems:policy).
|
|
1227
1326
|
`;
|
|
1228
1327
|
async function policy(argv) {
|
|
1229
1328
|
if (argv.includes("--help") || argv.includes("-h")) {
|
|
@@ -1249,12 +1348,14 @@ ${POLICY_HELP}`);
|
|
|
1249
1348
|
}
|
|
1250
1349
|
if (pull) {
|
|
1251
1350
|
const of = system ?? process.env.CAUTHY_SYSTEM ?? fail("name the system with --system or CAUTHY_SYSTEM");
|
|
1252
|
-
const
|
|
1351
|
+
const own = process.env.CAUTHY_TOKEN ? void 0 : createSystem();
|
|
1352
|
+
const [read, catalog2] = await Promise.all(own ? [own.policy.get(of), own.catalog.get(of).catch(() => [])] : [pullPolicy({ system: of }), pullCatalog({ system: of }).catch(() => [])]);
|
|
1253
1353
|
if (read.length === 0) fail(`Cauthy has no catalog of roles for "${of}"`);
|
|
1254
|
-
const source = policySource(of, read);
|
|
1354
|
+
const source = policySource(of, { catalog: catalog2, roles: read });
|
|
1255
1355
|
if (out2) {
|
|
1256
1356
|
(0, import_node_fs.writeFileSync)(out2, source);
|
|
1257
|
-
|
|
1357
|
+
const resources = catalog2.length ? ` and ${catalog2.length} resources (${catalog2.map((c) => c.resource).join(", ")})` : "";
|
|
1358
|
+
process.stdout.write(`${read.length} roles${resources} written to ${out2}: ${read.map((r) => r.role).join(", ")}
|
|
1258
1359
|
`);
|
|
1259
1360
|
} else {
|
|
1260
1361
|
process.stdout.write(source);
|
|
@@ -1262,20 +1363,23 @@ ${POLICY_HELP}`);
|
|
|
1262
1363
|
return;
|
|
1263
1364
|
}
|
|
1264
1365
|
if (!file) fail("give the catalog with --file\n\n" + POLICY_HELP);
|
|
1265
|
-
let
|
|
1366
|
+
let given;
|
|
1367
|
+
let withCatalog = false;
|
|
1266
1368
|
try {
|
|
1267
1369
|
const parsed = JSON.parse((0, import_node_fs.readFileSync)(file, "utf8"));
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
roles = list;
|
|
1370
|
+
given = readPolicyFile(parsed);
|
|
1371
|
+
withCatalog = !Array.isArray(parsed) && parsed.catalog !== void 0;
|
|
1271
1372
|
} catch (err) {
|
|
1272
1373
|
return fail(`cannot read ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1273
1374
|
}
|
|
1375
|
+
const { roles, catalog } = given;
|
|
1274
1376
|
const permissions = roles.reduce((n, r) => n + (r.permissions?.length ?? 0), 0);
|
|
1377
|
+
if (withCatalog) process.stdout.write(`${catalog.length} resources: ${catalog.map((c) => c.resource).join(", ")}
|
|
1378
|
+
`);
|
|
1275
1379
|
process.stdout.write(`${roles.length} roles, ${permissions} permissions: ${roles.map((r) => r.role).join(", ")}
|
|
1276
1380
|
`);
|
|
1277
1381
|
if (push) {
|
|
1278
|
-
await pushPolicy({ system, roles });
|
|
1382
|
+
await pushPolicy({ system, roles, ...withCatalog ? { catalog } : {} });
|
|
1279
1383
|
process.stdout.write("Sent to Cauthy: this is now the catalog of the system.\n");
|
|
1280
1384
|
} else {
|
|
1281
1385
|
process.stdout.write("Not sent. Add --push to give it to Cauthy.\n");
|