@coffre/core 0.5.1 → 0.6.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/dist/{access-CwsFLfTz.d.ts → access-BpcBLDW9.d.ts} +16 -8
- package/dist/{access-pnJ-u4x2.js → access-DvwZzqRR.js} +17 -12
- package/dist/access.d.ts +2 -2
- package/dist/access.js +2 -2
- package/dist/identity.d.ts +146 -3
- package/dist/identity.js +386 -10
- package/dist/schemas.d.ts +3 -3
- package/dist/schemas.js +1 -1
- package/dist/vault.d.ts +56 -23
- package/dist/workloads.d.ts +23 -6
- package/dist/workloads.js +42 -10
- package/package.json +2 -2
- package/src/access-conversion.ts +1 -1
- package/src/access.ts +18 -12
- package/src/identity/github-app.ts +347 -0
- package/src/identity/index.ts +1 -0
- package/src/identity/signin/config.ts +66 -13
- package/src/identity/signin/github.ts +17 -5
- package/src/identity/workloads.ts +45 -8
- package/src/vault.ts +54 -18
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* Each returns a `SigninProvider`, and a provider that speaks neither can be
|
|
18
18
|
* one of the deployment's own, in the same list.
|
|
19
19
|
*/
|
|
20
|
+
import { GitHubApp, pkcs8Of, type GitHubAppOptions } from '../github-app.ts';
|
|
20
21
|
import { GitHubSigninProvider } from './github.ts';
|
|
21
22
|
import { OidcSigninProvider } from './oidc.ts';
|
|
22
23
|
import type { SigninBrand, SigninProvider } from './types.ts';
|
|
@@ -95,6 +96,15 @@ export type WorkloadsOptions = {
|
|
|
95
96
|
* loopback too, so no instance anyone else can reach ever trusts one.
|
|
96
97
|
*/
|
|
97
98
|
allowLoopbackIssuersForDevelopment?: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* The deployment's own GitHub App, `githubApp(…)`, installed on its
|
|
101
|
+
* organizations: where an owner trusts a workflow, the pages list the
|
|
102
|
+
* repositories it is installed on, private ones too, and a repository
|
|
103
|
+
* typed by name resolves through it (docs/github-app.md). The same value
|
|
104
|
+
* may sign people in, `github({ app })`. Left out, repositories are typed,
|
|
105
|
+
* and only public ones' IDs are looked up.
|
|
106
|
+
*/
|
|
107
|
+
github?: GitHubApp;
|
|
98
108
|
};
|
|
99
109
|
|
|
100
110
|
/** Cloudflare's rate-limiting binding, as coffre calls it; `processLimits()` has the same shape. */
|
|
@@ -102,7 +112,7 @@ export type RateLimiter = { limit(options: { key: string }): Promise<{ success:
|
|
|
102
112
|
|
|
103
113
|
export type WorkloadLimits = { perSource: RateLimiter; total: RateLimiter };
|
|
104
114
|
|
|
105
|
-
export type WorkloadsConfig = { allowLoopback: boolean; limits: WorkloadLimits };
|
|
115
|
+
export type WorkloadsConfig = { allowLoopback: boolean; limits: WorkloadLimits; github: GitHubApp | null };
|
|
106
116
|
|
|
107
117
|
/**
|
|
108
118
|
* MCP clients, Claude among them, acting as the people who connect them,
|
|
@@ -241,28 +251,63 @@ export function microsoft(
|
|
|
241
251
|
});
|
|
242
252
|
}
|
|
243
253
|
|
|
244
|
-
/**
|
|
254
|
+
/**
|
|
255
|
+
* GitHub accounts; with `organization`, only that organization's members.
|
|
256
|
+
* Through an OAuth app, or a GitHub App's OAuth client, by its credentials;
|
|
257
|
+
* or the deployment's GitHub App, `github({ app })`, the same value its
|
|
258
|
+
* `workloads.github` lists repositories with:
|
|
259
|
+
*
|
|
260
|
+
* const app = githubApp({ appId, privateKey, clientId, clientSecret });
|
|
261
|
+
* signin({ providers: [github({ app, organization: 'acme' })], workloads: { limits, github: app } })
|
|
262
|
+
*/
|
|
245
263
|
export function github(
|
|
246
|
-
options:
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
webUrl?: string;
|
|
251
|
-
apiUrl?: string;
|
|
252
|
-
},
|
|
264
|
+
options: { id?: string; label?: string; organization?: string } & (
|
|
265
|
+
| (Credentials & { webUrl?: string; apiUrl?: string })
|
|
266
|
+
| { app: GitHubApp }
|
|
267
|
+
),
|
|
253
268
|
): GitHubSigninProvider {
|
|
254
269
|
const id = checkId(options.id ?? 'github');
|
|
270
|
+
const how = ((): Credentials & { webUrl?: string; apiUrl?: string } => {
|
|
271
|
+
if (!('app' in options)) return options;
|
|
272
|
+
const client = options.app instanceof GitHubApp ? options.app.signinClient() : null;
|
|
273
|
+
if (client === null) {
|
|
274
|
+
throw new Error(`sign-in provider ${id} signs in with the GitHub App's OAuth client: githubApp({ appId, privateKey, clientId, clientSecret })`);
|
|
275
|
+
}
|
|
276
|
+
return { ...client, webUrl: options.app.webUrl, apiUrl: options.app.apiUrl };
|
|
277
|
+
})();
|
|
255
278
|
return new GitHubSigninProvider({
|
|
256
279
|
id,
|
|
257
280
|
label: options.label ?? 'GitHub',
|
|
258
281
|
brand: 'github',
|
|
259
|
-
...credentials(id,
|
|
260
|
-
webUrl: baseUrl(
|
|
261
|
-
apiUrl: baseUrl(
|
|
282
|
+
...credentials(id, how),
|
|
283
|
+
webUrl: baseUrl(how.webUrl ?? 'https://github.com', `sign-in provider ${id} web URL`),
|
|
284
|
+
apiUrl: baseUrl(how.apiUrl ?? 'https://api.github.com', `sign-in provider ${id} API URL`),
|
|
262
285
|
organization: options.organization?.trim() || null,
|
|
263
286
|
});
|
|
264
287
|
}
|
|
265
288
|
|
|
289
|
+
/**
|
|
290
|
+
* The deployment's GitHub App, checked where it is written: its numeric ID
|
|
291
|
+
* and private key, and its OAuth client when it signs people in too. One
|
|
292
|
+
* value, passed to each side that uses it: `github({ app })` and
|
|
293
|
+
* `workloads.github` (`../github-app.ts`).
|
|
294
|
+
*/
|
|
295
|
+
export function githubApp(options: GitHubAppOptions): GitHubApp {
|
|
296
|
+
const appId = String(options.appId ?? '').trim();
|
|
297
|
+
if (!/^[1-9][0-9]{0,19}$/.test(appId)) throw new Error('githubApp(…).appId must be the app\'s numeric ID (its settings page, "App ID"), not its client ID');
|
|
298
|
+
const [clientId, clientSecret] = [options.clientId?.trim() || null, options.clientSecret || null];
|
|
299
|
+
if ((clientId === null) !== (clientSecret === null)) {
|
|
300
|
+
throw new Error('githubApp(…) takes its OAuth client whole, clientId and clientSecret, or neither');
|
|
301
|
+
}
|
|
302
|
+
return new GitHubApp({
|
|
303
|
+
appId,
|
|
304
|
+
pkcs8: pkcs8Of(options.privateKey),
|
|
305
|
+
webUrl: baseUrl(options.webUrl ?? 'https://github.com', 'githubApp(…).webUrl'),
|
|
306
|
+
apiUrl: baseUrl(options.apiUrl ?? 'https://api.github.com', 'githubApp(…).apiUrl'),
|
|
307
|
+
...(clientId === null || clientSecret === null ? {} : { client: { clientId, clientSecret } }),
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
266
311
|
function isLoopback(url: URL): boolean {
|
|
267
312
|
return url.hostname === '127.0.0.1' || url.hostname === 'localhost' || url.hostname === '[::1]';
|
|
268
313
|
}
|
|
@@ -370,7 +415,15 @@ function workloadsConfig(options: WorkloadsOptions, publicUrl: string): Workload
|
|
|
370
415
|
`workloads.allowLoopbackIssuersForDevelopment is for an instance on loopback, and ${publicUrl} is not one: turn it off`,
|
|
371
416
|
);
|
|
372
417
|
}
|
|
373
|
-
|
|
418
|
+
const app = options.github;
|
|
419
|
+
if (app !== undefined && !(app instanceof GitHubApp)) {
|
|
420
|
+
throw new Error('workloads.github is the deployment\'s GitHub App: githubApp({ appId, privateKey })');
|
|
421
|
+
}
|
|
422
|
+
// Its repositories' IDs are trusted under github.com's issuer, the one the lookup and the pages bind to.
|
|
423
|
+
if (app !== undefined && app.apiUrl !== 'https://api.github.com' && !isLoopback(new URL(app.apiUrl))) {
|
|
424
|
+
throw new Error('workloads.github is for github.com: GitHub Enterprise Server is not supported yet; sign in with its app, and leave workloads.github out');
|
|
425
|
+
}
|
|
426
|
+
return { allowLoopback: loopback, limits: options.limits, github: app ?? null };
|
|
374
427
|
}
|
|
375
428
|
|
|
376
429
|
/** A deployment's own provider is checked like coffre's: it is only typed, not trusted. */
|
|
@@ -86,12 +86,16 @@ export class GitHubSigninProvider implements SigninProvider {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
const organization = this.config.organization;
|
|
89
|
-
|
|
89
|
+
const standing = organization === null ? 'member' : await this.#membership(token, organization);
|
|
90
|
+
if (standing === 'unseen') {
|
|
90
91
|
throw new SigninError(
|
|
91
92
|
'not_in_organization',
|
|
92
|
-
|
|
93
|
+
`${label} would not show ${organization}'s members to coffre: a GitHub App needs the Members organization permission, read, and to be installed on ${organization}; an OAuth app, the organization's approval`,
|
|
93
94
|
);
|
|
94
95
|
}
|
|
96
|
+
if (standing === 'outside') {
|
|
97
|
+
throw new SigninError('not_in_organization', `only members of the ${organization} organization may sign in with ${label}`);
|
|
98
|
+
}
|
|
95
99
|
|
|
96
100
|
const listed = await this.#api<GitHubEmail[]>(token, '/user/emails');
|
|
97
101
|
const emails = (Array.isArray(listed) ? listed : [])
|
|
@@ -136,17 +140,25 @@ export class GitHubSigninProvider implements SigninProvider {
|
|
|
136
140
|
return body.access_token;
|
|
137
141
|
}
|
|
138
142
|
|
|
139
|
-
|
|
143
|
+
/**
|
|
144
|
+
* Whether the person is an active member: `outside` when GitHub says they
|
|
145
|
+
* are not (404), `unseen` when it will not say (403), which a GitHub App's
|
|
146
|
+
* user token gets without the Members permission or an installation on
|
|
147
|
+
* the organization, and an OAuth app's where the organization restricts
|
|
148
|
+
* it. Either way, no sign-in.
|
|
149
|
+
*/
|
|
150
|
+
async #membership(token: string, organization: string): Promise<'member' | 'outside' | 'unseen'> {
|
|
140
151
|
const response = await this.#send(
|
|
141
152
|
`${this.config.apiUrl}/user/memberships/orgs/${encodeURIComponent(organization)}`,
|
|
142
153
|
{ headers: apiHeaders(token) },
|
|
143
154
|
);
|
|
144
|
-
if (response.status === 404
|
|
155
|
+
if (response.status === 404) return 'outside';
|
|
156
|
+
if (response.status === 403) return 'unseen';
|
|
145
157
|
const body = (await readJson(response)) as { state?: unknown } | null;
|
|
146
158
|
if (!response.ok || body === null) {
|
|
147
159
|
throw new SigninError('invalid_response', `${this.config.label} answered the membership check with ${response.status}`);
|
|
148
160
|
}
|
|
149
|
-
return body.state === 'active';
|
|
161
|
+
return body.state === 'active' ? 'member' : 'outside';
|
|
150
162
|
}
|
|
151
163
|
|
|
152
164
|
async #api<T>(token: string, path: string): Promise<T | null> {
|
|
@@ -15,6 +15,12 @@
|
|
|
15
15
|
* workflow_ref: 'acme/api/.github/workflows/ci.yml@refs/heads/main',
|
|
16
16
|
* ref: 'refs/heads/main',
|
|
17
17
|
* event_name: ['pull_request', 'push', 'workflow_dispatch'] } }
|
|
18
|
+
*
|
|
19
|
+
* A GitHub binding may also name `environment`, the GitHub environment its
|
|
20
|
+
* job runs in, so that the environment's protection rules (its required
|
|
21
|
+
* reviewers, the branches it deploys) gate every run that signs in. GitHub
|
|
22
|
+
* changes `sub` for such a job, `repo:acme/api:environment:production`, but
|
|
23
|
+
* no GitHub binding matches `sub`: `ref` and `workflow_ref` stay as they were.
|
|
18
24
|
*/
|
|
19
25
|
|
|
20
26
|
export const GITHUB_ISSUER = 'https://token.actions.githubusercontent.com';
|
|
@@ -106,6 +112,13 @@ const DIGITS = /^[1-9][0-9]{0,19}$/;
|
|
|
106
112
|
const SHA = /^[0-9a-f]{40}$/;
|
|
107
113
|
const REF = /^refs\/(heads|tags)\/\S{1,200}$/;
|
|
108
114
|
const WORKFLOW = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/\.github\/workflows\/[^@\s]+\.ya?ml@\S+$/;
|
|
115
|
+
/**
|
|
116
|
+
* A GitHub environment's name, as GitHub takes one: at most 255 characters,
|
|
117
|
+
* none a control character, and no space at either end, which GitHub would
|
|
118
|
+
* not keep. GitHub documents no narrower set: `production`, `Staging EU`
|
|
119
|
+
* and `prod/eu:1` are all names.
|
|
120
|
+
*/
|
|
121
|
+
const ENVIRONMENT = /^(?! )[^\p{Cc}]{1,255}(?<! )$/u;
|
|
109
122
|
|
|
110
123
|
/** A claim a binding names: one value, or, for what started the run, any of several. */
|
|
111
124
|
export type ClaimValue = string | string[];
|
|
@@ -209,6 +222,9 @@ function checkGitHub(profile: WorkloadProfile, claims: BindingClaims): void {
|
|
|
209
222
|
const value = claims[id];
|
|
210
223
|
if (value !== undefined && !DIGITS.test(value as string)) throw new BindingInvalid(`${id} is GitHub's numeric ID, not a name`);
|
|
211
224
|
}
|
|
225
|
+
if (claims.environment !== undefined && !ENVIRONMENT.test(claims.environment as string)) {
|
|
226
|
+
throw new BindingInvalid('environment is a GitHub environment\'s name: at most 255 characters, no control character, no space at either end');
|
|
227
|
+
}
|
|
212
228
|
const events = claimValues(claims.event_name);
|
|
213
229
|
const ref = claims.ref as string;
|
|
214
230
|
if (!REF.test(ref)) throw new BindingInvalid('ref is a full ref: refs/heads/<branch> or refs/tags/<tag>');
|
|
@@ -245,17 +261,24 @@ function checkGitLab(claims: BindingClaims): void {
|
|
|
245
261
|
/**
|
|
246
262
|
* The claims of a verified token that differ from what a binding expects,
|
|
247
263
|
* by name: none when it matches. A claim matches when the token carries its
|
|
248
|
-
* value, or one of its values.
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
* `
|
|
264
|
+
* value, or one of its values. On GitHub's profiles, two readings:
|
|
265
|
+
*
|
|
266
|
+
* - A `pull_request` run, at `refs/pull/<n>/merge`, counts as a run at the
|
|
267
|
+
* branch it merges into: its `ref`, and the ref its `workflow_ref` ends
|
|
268
|
+
* in, are read as `refs/heads/<base_ref>`.
|
|
269
|
+
* - `environment` matches whatever its case: GitHub's environment names are
|
|
270
|
+
* case-insensitive, so `Production` and `production` are one environment,
|
|
271
|
+
* under the same protection rules.
|
|
252
272
|
*/
|
|
253
273
|
export function differingClaims(profile: WorkloadProfile, expected: BindingClaims, token: Record<string, unknown>): string[] {
|
|
254
|
-
const
|
|
274
|
+
const github = profile.startsWith('github');
|
|
275
|
+
const claims = github ? atBaseBranch(token) : token;
|
|
255
276
|
return Object.entries(expected)
|
|
256
277
|
.filter(([name, value]) => {
|
|
257
278
|
const actual = claims[name];
|
|
258
|
-
|
|
279
|
+
if (typeof actual !== 'string') return true;
|
|
280
|
+
if (github && name === 'environment') return actual.toLowerCase() !== (value as string).toLowerCase();
|
|
281
|
+
return !claimValues(value).includes(actual);
|
|
259
282
|
})
|
|
260
283
|
.map(([name]) => name);
|
|
261
284
|
}
|
|
@@ -351,13 +374,18 @@ function started(input: Record<string, unknown>, one: string, many: string): Cla
|
|
|
351
374
|
return list as string[];
|
|
352
375
|
}
|
|
353
376
|
|
|
354
|
-
/**
|
|
377
|
+
/**
|
|
378
|
+
* A workflow of the repository, run at `ref` by any of `events`; a pull
|
|
379
|
+
* request, by the branch it merges into. An `environment` narrows it to the
|
|
380
|
+
* jobs that run in that GitHub environment.
|
|
381
|
+
*/
|
|
355
382
|
export function githubWorkflow(input: {
|
|
356
383
|
repository: string;
|
|
357
384
|
repositoryId: string;
|
|
358
385
|
ownerId: string;
|
|
359
386
|
workflow: string;
|
|
360
387
|
ref: string;
|
|
388
|
+
environment?: string;
|
|
361
389
|
} & Starts<'event', 'events'>): Omit<BindingPolicy, 'issuer'> {
|
|
362
390
|
return {
|
|
363
391
|
profile: 'github',
|
|
@@ -367,14 +395,21 @@ export function githubWorkflow(input: {
|
|
|
367
395
|
workflow_ref: workflowRef(input.repository, input.workflow, input.ref),
|
|
368
396
|
ref: input.ref,
|
|
369
397
|
event_name: started(input, 'event', 'events'),
|
|
398
|
+
...inEnvironment(input.environment),
|
|
370
399
|
},
|
|
371
400
|
};
|
|
372
401
|
}
|
|
373
402
|
|
|
403
|
+
/** The `environment` claim, when one is named. */
|
|
404
|
+
function inEnvironment(environment: string | undefined): BindingClaims {
|
|
405
|
+
return environment === undefined ? {} : { environment };
|
|
406
|
+
}
|
|
407
|
+
|
|
374
408
|
/**
|
|
375
409
|
* A reusable workflow at one commit, called at `ref`: from one repository,
|
|
376
410
|
* or, with `repositoryId` null, from any repository of the organization. A
|
|
377
|
-
* `caller` workflow narrows it to one calling workflow
|
|
411
|
+
* `caller` workflow narrows it to one calling workflow, and an
|
|
412
|
+
* `environment` to the jobs that run in that GitHub environment.
|
|
378
413
|
*/
|
|
379
414
|
export function githubReusable(input: {
|
|
380
415
|
repositoryId: string | null;
|
|
@@ -383,6 +418,7 @@ export function githubReusable(input: {
|
|
|
383
418
|
called: string;
|
|
384
419
|
sha: string;
|
|
385
420
|
caller?: { repository: string; workflow: string };
|
|
421
|
+
environment?: string;
|
|
386
422
|
} & Starts<'event', 'events'>): Omit<BindingPolicy, 'issuer'> {
|
|
387
423
|
const claims: BindingClaims = {
|
|
388
424
|
repository_owner_id: input.ownerId,
|
|
@@ -391,6 +427,7 @@ export function githubReusable(input: {
|
|
|
391
427
|
event_name: started(input, 'event', 'events'),
|
|
392
428
|
job_workflow_ref: input.called,
|
|
393
429
|
job_workflow_sha: input.sha,
|
|
430
|
+
...inEnvironment(input.environment),
|
|
394
431
|
};
|
|
395
432
|
if (input.caller !== undefined) claims.workflow_ref = workflowRef(input.caller.repository, input.caller.workflow, input.ref);
|
|
396
433
|
return { profile: input.repositoryId === null ? 'github-reusable-organization' : 'github-reusable', claims };
|
package/src/vault.ts
CHANGED
|
@@ -56,20 +56,30 @@ export interface Vault {
|
|
|
56
56
|
endReferences(input: EndReferencesInput): Promise<Outcome<{ seqs: number[] }>>;
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
|
-
* What one principal holds right now: the app asks once per request
|
|
60
|
-
*
|
|
61
|
-
* themselves, and need not ask.
|
|
59
|
+
* What one principal holds right now: the app asks once per request,
|
|
60
|
+
* and at sign-in for each verified email. A decision, its rows checked:
|
|
61
|
+
* lists of who holds what read the rows themselves, and need not ask.
|
|
62
|
+
* A person the vault's `owners` names who is no Owner of all of it
|
|
63
|
+
* becomes one here, logged (`reason: 'owners'`).
|
|
62
64
|
*/
|
|
63
65
|
access(principal: string): Promise<Access>;
|
|
64
|
-
/**
|
|
65
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Set roles at several places for one principal: all of it, or none.
|
|
68
|
+
* With `admit`, a service account that is not a member is admitted
|
|
69
|
+
* first, in the same decision, as `admit` would: `admitted` says so.
|
|
70
|
+
*/
|
|
71
|
+
setAccess(input: SetAccessInput): Promise<Outcome<{ changes: AccessChange[]; admitted?: Admitted }>>;
|
|
66
72
|
/**
|
|
67
73
|
* Add a member, bring back a removed one, or change a person's instance
|
|
68
74
|
* role and its scope. `generation` is theirs now: sessions and tokens of
|
|
69
75
|
* an earlier one are dead.
|
|
70
76
|
*/
|
|
71
|
-
admit(input: AdmitInput): Promise<Outcome<
|
|
72
|
-
/**
|
|
77
|
+
admit(input: AdmitInput): Promise<Outcome<Admitted>>;
|
|
78
|
+
/**
|
|
79
|
+
* Remove a member: revoke every grant and refuse them until admitted
|
|
80
|
+
* again, from `generation` on. An Owner, only an Owner of the whole
|
|
81
|
+
* instance removes, and never the last.
|
|
82
|
+
*/
|
|
73
83
|
remove(input: RemoveInput): Promise<Outcome<{ revoked: Grant[]; generation: number }>>;
|
|
74
84
|
/**
|
|
75
85
|
* The instance's settings, as the vault's newest `settings.change` entry
|
|
@@ -96,10 +106,22 @@ export interface Vault {
|
|
|
96
106
|
/**
|
|
97
107
|
* What the vault's configuration says, which the app shows: the keys its
|
|
98
108
|
* checkpoints verify under, by the key id each checkpoint names (the one
|
|
99
|
-
* it signs with now, and those of the KEKs it replaced)
|
|
100
|
-
* admins, as principals.
|
|
109
|
+
* it signs with now, and those of the KEKs it replaced).
|
|
101
110
|
*/
|
|
102
|
-
about(): Promise<{
|
|
111
|
+
about(): Promise<{
|
|
112
|
+
checkpointKeys: Record<string, CheckpointKey>;
|
|
113
|
+
/**
|
|
114
|
+
* The emails of those the deployment names Owners of the whole instance
|
|
115
|
+
* (`owners`), lowercased: the vault makes them Owners, and refuses to
|
|
116
|
+
* demote or remove them. Absent from a vault of 0.5, which names none.
|
|
117
|
+
*/
|
|
118
|
+
owners?: string[];
|
|
119
|
+
/**
|
|
120
|
+
* @deprecated Always empty: an app of 0.5, deployed after this vault,
|
|
121
|
+
* lists and checks root admins here until it is. Gone in 0.7.
|
|
122
|
+
*/
|
|
123
|
+
rootAdmins: [];
|
|
124
|
+
}>;
|
|
103
125
|
/**
|
|
104
126
|
* What an escrowed vault key is checked against: the vault key it wraps
|
|
105
127
|
* under now, and each one's check, as the vault wrote it. None of it is
|
|
@@ -150,8 +172,12 @@ export type RefusalCode =
|
|
|
150
172
|
| 'ended'
|
|
151
173
|
/** Not allowed to manage access, members or the log. */
|
|
152
174
|
| 'not_allowed'
|
|
153
|
-
/**
|
|
154
|
-
|
|
175
|
+
/**
|
|
176
|
+
* The change would leave the instance without an Owner of all of it: the
|
|
177
|
+
* last one is not demoted, narrowed or removed, by anyone, until another
|
|
178
|
+
* is made.
|
|
179
|
+
*/
|
|
180
|
+
| 'last_owner'
|
|
155
181
|
/** Well-formed, but not something the rules allow: a project role on an environment, a service account with an instance role. */
|
|
156
182
|
| 'invalid'
|
|
157
183
|
/** The vault's own log no longer carries the head the last checkpoint signed, or does not rehash since. */
|
|
@@ -282,22 +308,19 @@ export type Access = {
|
|
|
282
308
|
/**
|
|
283
309
|
* `unknown` for someone never admitted, `tampered` for a member whose row
|
|
284
310
|
* fails the vault's integrity check: refused everything, as `removed` is.
|
|
285
|
-
* Root admins are always active.
|
|
286
311
|
*/
|
|
287
312
|
status: 'active' | 'removed' | 'unknown' | 'tampered';
|
|
288
313
|
/** Advanced by removal, even if the app cannot commit its credential revocations. */
|
|
289
314
|
generation: number;
|
|
290
|
-
isRootAdmin: boolean;
|
|
291
315
|
/**
|
|
292
316
|
* Their instance role and its scope, while active: `member` everywhere
|
|
293
|
-
* for service accounts and anyone not active
|
|
294
|
-
* root admins. Scopes name projects by id.
|
|
317
|
+
* for service accounts and anyone not active. Scopes name projects by id.
|
|
295
318
|
*/
|
|
296
319
|
role: InstanceRole;
|
|
297
320
|
scope: Scope;
|
|
298
321
|
/** Live grants only, and none unless active. */
|
|
299
322
|
grants: Grant[];
|
|
300
|
-
/** When the status last changed, and who changed it; null for
|
|
323
|
+
/** When the status last changed, and who changed it; null for strangers. */
|
|
301
324
|
since: string | null;
|
|
302
325
|
by: string | null;
|
|
303
326
|
};
|
|
@@ -317,6 +340,12 @@ export type SetAccessInput = Correlation & Through & {
|
|
|
317
340
|
actor: string;
|
|
318
341
|
principal: string;
|
|
319
342
|
changes: GrantChange[];
|
|
343
|
+
/**
|
|
344
|
+
* Admit the principal, a service account, unless it is a member: a
|
|
345
|
+
* grant refused admits nobody. A vault older than this refuses one that
|
|
346
|
+
* is not a member, `not_a_member`, and admits nobody either.
|
|
347
|
+
*/
|
|
348
|
+
admit?: boolean;
|
|
320
349
|
};
|
|
321
350
|
|
|
322
351
|
export type AdmitInput = Correlation & Through & {
|
|
@@ -325,12 +354,19 @@ export type AdmitInput = Correlation & Through & {
|
|
|
325
354
|
/**
|
|
326
355
|
* A person's instance role, and where it applies (everywhere, left out).
|
|
327
356
|
* Left out, an existing member keeps theirs and a new one is a `member`.
|
|
328
|
-
* Changing it takes someone who runs the instance, about someone else
|
|
357
|
+
* Changing it takes someone who runs the instance, about someone else;
|
|
358
|
+
* making someone an Owner, or changing an Owner's, an Owner of the whole
|
|
359
|
+
* instance, who may change their own too. It never leaves the instance
|
|
360
|
+
* without an Owner of all of it (`last_owner`), nor demotes one the
|
|
361
|
+
* deployment names (`owners`; `not_allowed`).
|
|
329
362
|
*/
|
|
330
363
|
role?: InstanceRole;
|
|
331
364
|
scope?: Scope;
|
|
332
365
|
};
|
|
333
366
|
|
|
367
|
+
/** A member as `admit` left them; `created` when they were not one before. */
|
|
368
|
+
export type Admitted = { created: boolean; role: InstanceRole; scope: Scope; generation: number };
|
|
369
|
+
|
|
334
370
|
export type RemoveInput = Correlation & Through & {
|
|
335
371
|
actor: string;
|
|
336
372
|
principal: string;
|