@coffre/core 0.4.6 → 0.5.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 +420 -0
- package/dist/access-pnJ-u4x2.js +522 -0
- package/dist/access.d.ts +2 -136
- package/dist/access.js +2 -135
- package/dist/identity.d.ts +2 -2
- package/dist/identity.js +2 -2
- package/dist/schemas.d.ts +23 -1
- package/dist/schemas.js +14 -2
- package/dist/vault.d.ts +66 -28
- package/dist/vault.js +2 -2
- package/dist/workloads.d.ts +60 -20
- package/dist/workloads.js +141 -26
- package/package.json +2 -2
- package/src/access-conversion.ts +202 -0
- package/src/access-fault.ts +6 -4
- package/src/access.ts +346 -49
- package/src/identity/workloads.ts +154 -42
- package/src/schemas.ts +12 -1
- package/src/vault.ts +61 -27
package/dist/access.js
CHANGED
|
@@ -1,135 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* Who may do what: seven permissions and the six roles that bundle them.
|
|
4
|
-
*
|
|
5
|
-
* Deliberately constants and not tables or a policy language. A grant names
|
|
6
|
-
* one role for one member on one place, and a check is a lookup in this file,
|
|
7
|
-
* so authorisation stays something you can read end to end.
|
|
8
|
-
*/
|
|
9
|
-
const PERMISSIONS = [
|
|
10
|
-
"secret.read",
|
|
11
|
-
"secret.write",
|
|
12
|
-
"secret.archive",
|
|
13
|
-
"audit.read",
|
|
14
|
-
"environment.manage",
|
|
15
|
-
"grant.manage",
|
|
16
|
-
"project.manage"
|
|
17
|
-
];
|
|
18
|
-
/**
|
|
19
|
-
* Permissions that only make sense on a project. Granting
|
|
20
|
-
* `environment.manage` on one environment would be incoherent: what it
|
|
21
|
-
* authorises is creating that environment's siblings.
|
|
22
|
-
*/
|
|
23
|
-
const PROJECT_ONLY_PERMISSIONS = [
|
|
24
|
-
"environment.manage",
|
|
25
|
-
"grant.manage",
|
|
26
|
-
"project.manage"
|
|
27
|
-
];
|
|
28
|
-
const ROLES = {
|
|
29
|
-
viewer: {
|
|
30
|
-
name: "Viewer",
|
|
31
|
-
description: "Read secret values.",
|
|
32
|
-
permissions: ["secret.read"]
|
|
33
|
-
},
|
|
34
|
-
developer: {
|
|
35
|
-
name: "Developer",
|
|
36
|
-
description: "Read and write secrets.",
|
|
37
|
-
permissions: ["secret.read", "secret.write"]
|
|
38
|
-
},
|
|
39
|
-
maintainer: {
|
|
40
|
-
name: "Maintainer",
|
|
41
|
-
description: "Read, write and retire secrets, and manage environments.",
|
|
42
|
-
permissions: [
|
|
43
|
-
"secret.read",
|
|
44
|
-
"secret.write",
|
|
45
|
-
"secret.archive",
|
|
46
|
-
"environment.manage"
|
|
47
|
-
]
|
|
48
|
-
},
|
|
49
|
-
"access-manager": {
|
|
50
|
-
name: "Access manager",
|
|
51
|
-
description: "Manage who has access. Cannot read secret values.",
|
|
52
|
-
permissions: ["grant.manage"]
|
|
53
|
-
},
|
|
54
|
-
auditor: {
|
|
55
|
-
name: "Auditor",
|
|
56
|
-
description: "Read the audit log. Cannot read secret values.",
|
|
57
|
-
permissions: ["audit.read"]
|
|
58
|
-
},
|
|
59
|
-
owner: {
|
|
60
|
-
name: "Owner",
|
|
61
|
-
description: "Everything, including reading secret values.",
|
|
62
|
-
permissions: [...PERMISSIONS]
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
const ROLE_NAMES = Object.keys(ROLES);
|
|
66
|
-
function isRole(value) {
|
|
67
|
-
return Object.hasOwn(ROLES, value);
|
|
68
|
-
}
|
|
69
|
-
function roleGrants(role, permission) {
|
|
70
|
-
return ROLES[role].permissions.includes(permission);
|
|
71
|
-
}
|
|
72
|
-
/** Roles that hold a project-only permission can only be granted on a project. */
|
|
73
|
-
function assignableToEnvironment(role) {
|
|
74
|
-
return !ROLES[role].permissions.some((permission) => PROJECT_ONLY_PERMISSIONS.includes(permission));
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* What a grant's fields say it is on, or null when they say nothing
|
|
78
|
-
* coherent: an environment without its project, as a grant whose
|
|
79
|
-
* environment row is gone reads, or a slug beside an id. A grant is on
|
|
80
|
-
* every project only when it names no place at all; anything else that
|
|
81
|
-
* lacks a field covers nothing.
|
|
82
|
-
*/
|
|
83
|
-
function grantKind(grant) {
|
|
84
|
-
if (grant.environmentId !== null) return grant.projectId !== null && grant.environmentSlug === null ? "environment" : null;
|
|
85
|
-
if (grant.projectId !== null) return grant.environmentSlug === null ? "project" : null;
|
|
86
|
-
return "every-project";
|
|
87
|
-
}
|
|
88
|
-
/** Every project, as a path names it. */
|
|
89
|
-
const EVERY_PROJECT = "*";
|
|
90
|
-
/** A grant on every project as a path: `*`, or `*` and the environment slug it covers in each. */
|
|
91
|
-
function everyProjectPath(environmentSlug) {
|
|
92
|
-
return environmentSlug === null ? "*" : `*/${environmentSlug}`;
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Whether a grant at `grant` reaches `place`:
|
|
96
|
-
*
|
|
97
|
-
* market market and each of its environments
|
|
98
|
-
* market/dev market/dev
|
|
99
|
-
* * every project and every environment
|
|
100
|
-
* *, on dev each environment whose slug is dev, in every project
|
|
101
|
-
*
|
|
102
|
-
* An environment's grant never reaches up to its project, whether it names
|
|
103
|
-
* the environment or its slug.
|
|
104
|
-
*/
|
|
105
|
-
function covers(grant, place) {
|
|
106
|
-
switch (grantKind(grant)) {
|
|
107
|
-
case "environment": return grant.projectId === place.projectId && grant.environmentId === (place.environmentId ?? null);
|
|
108
|
-
case "project": return grant.projectId === place.projectId;
|
|
109
|
-
case "every-project": return grant.environmentSlug === null || place.environmentId != null && place.environmentSlug !== null && place.environmentSlug === grant.environmentSlug;
|
|
110
|
-
case null: return false;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Whether `holder` may do `permission` at `place`: a grant that covers it
|
|
115
|
-
* gives the role's permissions there, and grants add up. Owners also manage
|
|
116
|
-
* every project. `developer` on `market/prod` does not let anyone rename
|
|
117
|
-
* `market`.
|
|
118
|
-
*/
|
|
119
|
-
function allows(holder, permission, place) {
|
|
120
|
-
if (holder.isRootAdmin) return true;
|
|
121
|
-
if (place.environmentId == null && holder.isOwner && PROJECT_ONLY_PERMISSIONS.includes(permission)) return true;
|
|
122
|
-
return holder.grants.some((grant) => covers(grant, place) && roleGrants(grant.role, permission));
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Whether the actor may manage grants at this place: on a project or its
|
|
126
|
-
* environments, with `grant.manage` on the project; on every project, as an
|
|
127
|
-
* instance owner or a root admin only.
|
|
128
|
-
*/
|
|
129
|
-
function mayManageAccess(actor, place) {
|
|
130
|
-
const kind = grantKind(place);
|
|
131
|
-
if (kind === "every-project") return actor.isOwner || actor.isRootAdmin;
|
|
132
|
-
return kind !== null && place.projectId !== null && allows(actor, "grant.manage", { projectId: place.projectId });
|
|
133
|
-
}
|
|
134
|
-
//#endregion
|
|
135
|
-
export { EVERY_PROJECT, PERMISSIONS, PROJECT_ONLY_PERMISSIONS, ROLES, ROLE_NAMES, allows, assignableToEnvironment, covers, everyProjectPath, grantKind, isRole, mayManageAccess, roleGrants };
|
|
1
|
+
import { A as roleGrants, C as isScope, D as mayManageAccess, E as managesService, F as convertEveryProjectGrants, M as scopeInWords, N as setsUpServices, O as normalScope, P as unscoped, S as isRole, T as makesProjects, _ as grantKind, a as NOTHING, b as isCredentialScope, c as ROLES, d as admits, f as allows, g as givesService, h as credentialScopeInWords, i as MAX_SCOPE_PLACES, j as runsInstance, k as readsWholeLog, l as ROLE_NAMES, m as covers, n as INSTANCE_ROLES, o as PERMISSIONS, p as assignableToEnvironment, r as INSTANCE_ROLE_NAMES, s as PROJECT_ONLY_PERMISSIONS, t as EVERYWHERE, u as administers, v as inScope, w as issuesCredential, x as isInstanceRole, y as instanceRoleGrants } from "./access-pnJ-u4x2.js";
|
|
2
|
+
export { EVERYWHERE, INSTANCE_ROLES, INSTANCE_ROLE_NAMES, MAX_SCOPE_PLACES, NOTHING, PERMISSIONS, PROJECT_ONLY_PERMISSIONS, ROLES, ROLE_NAMES, administers, admits, allows, assignableToEnvironment, convertEveryProjectGrants, covers, credentialScopeInWords, givesService, grantKind, inScope, instanceRoleGrants, isCredentialScope, isInstanceRole, isRole, isScope, issuesCredential, makesProjects, managesService, mayManageAccess, normalScope, readsWholeLog, roleGrants, runsInstance, scopeInWords, setsUpServices, unscoped };
|
package/dist/identity.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BindingClaims, BindingInvalid, BindingPolicy, GITHUB_EVENTS, GITHUB_ISSUER, GITLAB_ISSUER, GITLAB_PIPELINE_SOURCES, MAX_BINDINGS, MAX_CLAIMS, MAX_CLAIM_BYTES, REQUIRED_CLAIMS, WORKLOAD_PROFILES, WorkloadProfile, canonicalClaims, checkBinding, checkFetchUrl, defaultIssuer, fullRef, githubReusable, githubWorkflow, gitlabProject, workflowRef } from "./workloads.js";
|
|
1
|
+
import { BindingClaims, BindingInvalid, BindingPolicy, ClaimValue, EVENT_EXPOSURE, GITHUB_EVENTS, GITHUB_ISSUER, GITLAB_ISSUER, GITLAB_PIPELINE_SOURCES, MAX_BINDINGS, MAX_CLAIMS, MAX_CLAIM_BYTES, REQUIRED_CLAIMS, Starts, WORKLOAD_PROFILES, WorkloadProfile, canonicalClaims, checkBinding, checkFetchUrl, claimValues, defaultIssuer, differingClaims, fullRef, githubEventsAt, githubReusable, githubWorkflow, gitlabProject, workflowRef } from "./workloads.js";
|
|
2
2
|
import { JSONWebKeySet, JWTPayload } from "jose";
|
|
3
3
|
//#region src/identity/signin/types.d.ts
|
|
4
4
|
/**
|
|
@@ -587,4 +587,4 @@ export declare function verifyWorkloadToken(token: string, keys: JSONWebKeySet,
|
|
|
587
587
|
*/
|
|
588
588
|
export declare function issuedRefusal(iat: number, now: number): WorkloadTokenRefused | null;
|
|
589
589
|
//#endregion
|
|
590
|
-
export { BindingClaims, BindingInvalid, BindingPolicy, GITHUB_EVENTS, GITHUB_ISSUER, GITLAB_ISSUER, GITLAB_PIPELINE_SOURCES, type JSONWebKeySet, MAX_BINDINGS, MAX_CLAIMS, MAX_CLAIM_BYTES, REQUIRED_CLAIMS, WORKLOAD_PROFILES, WorkloadProfile, canonicalClaims, checkBinding, checkFetchUrl, defaultIssuer, fullRef, githubReusable, githubWorkflow, gitlabProject, workflowRef };
|
|
590
|
+
export { BindingClaims, BindingInvalid, BindingPolicy, ClaimValue, EVENT_EXPOSURE, GITHUB_EVENTS, GITHUB_ISSUER, GITLAB_ISSUER, GITLAB_PIPELINE_SOURCES, type JSONWebKeySet, MAX_BINDINGS, MAX_CLAIMS, MAX_CLAIM_BYTES, REQUIRED_CLAIMS, Starts, WORKLOAD_PROFILES, WorkloadProfile, canonicalClaims, checkBinding, checkFetchUrl, claimValues, defaultIssuer, differingClaims, fullRef, githubEventsAt, githubReusable, githubWorkflow, gitlabProject, workflowRef };
|
package/dist/identity.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BindingInvalid, GITHUB_EVENTS, GITHUB_ISSUER, GITLAB_ISSUER, GITLAB_PIPELINE_SOURCES, MAX_BINDINGS, MAX_CLAIMS, MAX_CLAIM_BYTES, REQUIRED_CLAIMS, WORKLOAD_PROFILES, canonicalClaims, checkBinding, checkFetchUrl, defaultIssuer, fullRef, githubReusable, githubWorkflow, gitlabProject, workflowRef } from "./workloads.js";
|
|
1
|
+
import { BindingInvalid, EVENT_EXPOSURE, GITHUB_EVENTS, GITHUB_ISSUER, GITLAB_ISSUER, GITLAB_PIPELINE_SOURCES, MAX_BINDINGS, MAX_CLAIMS, MAX_CLAIM_BYTES, REQUIRED_CLAIMS, WORKLOAD_PROFILES, canonicalClaims, checkBinding, checkFetchUrl, claimValues, defaultIssuer, differingClaims, fullRef, githubEventsAt, githubReusable, githubWorkflow, gitlabProject, workflowRef } from "./workloads.js";
|
|
2
2
|
import { createCipheriv, createDecipheriv, createHash, hkdfSync, randomBytes } from "node:crypto";
|
|
3
3
|
import * as oauth from "oauth4webapi";
|
|
4
4
|
import { createLocalJWKSet, createRemoteJWKSet, errors, jwtVerify } from "jose";
|
|
@@ -830,4 +830,4 @@ function refusalOf(error) {
|
|
|
830
830
|
return new WorkloadTokenRefused("signature", "the token does not verify");
|
|
831
831
|
}
|
|
832
832
|
//#endregion
|
|
833
|
-
export { ACCESS_JWT_HEADER, AccessIdentityVerifier, BindingInvalid, CLOCK_TOLERANCE_SECONDS, GITHUB_EVENTS, GITHUB_ISSUER, GITLAB_ISSUER, GITLAB_PIPELINE_SOURCES, GitHubSigninProvider, MAX_BINDINGS, MAX_CLAIMS, MAX_CLAIM_BYTES, MAX_TOKEN_AGE_SECONDS, MAX_TOKEN_BYTES, OidcSigninProvider, REQUIRED_CLAIMS, SigninError, WORKLOAD_ALGORITHMS, WORKLOAD_PROFILES, WorkloadTokenRefused, canonicalClaims, checkBinding, checkFetchUrl, cloudflareAccess, decodeWorkloadToken, defaultIssuer, defineSignin, deriveKey, fullRef, generateToken, github, githubReusable, githubWorkflow, gitlabProject, google, hashToken, isCoffreToken, issuedRefusal, microsoft, oidc, publicOrigin, seal, signin, toPrincipal, tokenHint, tokenKind, unseal, verifyWorkloadToken, workflowRef };
|
|
833
|
+
export { ACCESS_JWT_HEADER, AccessIdentityVerifier, BindingInvalid, CLOCK_TOLERANCE_SECONDS, EVENT_EXPOSURE, GITHUB_EVENTS, GITHUB_ISSUER, GITLAB_ISSUER, GITLAB_PIPELINE_SOURCES, GitHubSigninProvider, MAX_BINDINGS, MAX_CLAIMS, MAX_CLAIM_BYTES, MAX_TOKEN_AGE_SECONDS, MAX_TOKEN_BYTES, OidcSigninProvider, REQUIRED_CLAIMS, SigninError, WORKLOAD_ALGORITHMS, WORKLOAD_PROFILES, WorkloadTokenRefused, canonicalClaims, checkBinding, checkFetchUrl, claimValues, cloudflareAccess, decodeWorkloadToken, defaultIssuer, defineSignin, deriveKey, differingClaims, fullRef, generateToken, github, githubEventsAt, githubReusable, githubWorkflow, gitlabProject, google, hashToken, isCoffreToken, issuedRefusal, microsoft, oidc, publicOrigin, seal, signin, toPrincipal, tokenHint, tokenKind, unseal, verifyWorkloadToken, workflowRef };
|
package/dist/schemas.d.ts
CHANGED
|
@@ -29,9 +29,31 @@ export declare const principalType: z.ZodEnum<{
|
|
|
29
29
|
}>;
|
|
30
30
|
export declare const principalId: z.ZodString;
|
|
31
31
|
export declare const instanceRole: z.ZodEnum<{
|
|
32
|
+
member: "member";
|
|
33
|
+
auditor: "auditor";
|
|
34
|
+
developer: "developer";
|
|
35
|
+
admin: "admin";
|
|
32
36
|
owner: "owner";
|
|
33
|
-
user: "user";
|
|
34
37
|
}>;
|
|
38
|
+
/** Which projects or environments a scope takes in, by slug: `all`, `{ only: [...] }` or `{ except: [...] }`. */
|
|
39
|
+
export declare const scopeFilter: z.ZodUnion<readonly [z.ZodLiteral<"all">, z.ZodObject<{
|
|
40
|
+
only: z.ZodArray<z.ZodString>;
|
|
41
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
42
|
+
except: z.ZodArray<z.ZodString>;
|
|
43
|
+
}, z.core.$strict>]>;
|
|
44
|
+
/** Where an instance role applies, as the API takes it: either filter left out is `all`. */
|
|
45
|
+
export declare const scopeInput: z.ZodObject<{
|
|
46
|
+
projects: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"all">, z.ZodObject<{
|
|
47
|
+
only: z.ZodArray<z.ZodString>;
|
|
48
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
49
|
+
except: z.ZodArray<z.ZodString>;
|
|
50
|
+
}, z.core.$strict>]>>;
|
|
51
|
+
environments: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"all">, z.ZodObject<{
|
|
52
|
+
only: z.ZodArray<z.ZodString>;
|
|
53
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
54
|
+
except: z.ZodArray<z.ZodString>;
|
|
55
|
+
}, z.core.$strict>]>>;
|
|
56
|
+
}, z.core.$strict>;
|
|
35
57
|
export declare const grantId: z.ZodString;
|
|
36
58
|
export declare const emailAddress: z.ZodString;
|
|
37
59
|
//#endregion
|
package/dist/schemas.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { r as INSTANCE_ROLE_NAMES } from "./access-pnJ-u4x2.js";
|
|
1
2
|
import { PROJECT_PAGES } from "./pages.js";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
//#region src/schemas.ts
|
|
@@ -33,8 +34,19 @@ function shownMember(member) {
|
|
|
33
34
|
}
|
|
34
35
|
const principalType = z.enum(["user", "service"]);
|
|
35
36
|
const principalId = z.string().trim().min(1).max(320);
|
|
36
|
-
const instanceRole = z.enum(
|
|
37
|
+
const instanceRole = z.enum(INSTANCE_ROLE_NAMES);
|
|
38
|
+
/** Which projects or environments a scope takes in, by slug: `all`, `{ only: [...] }` or `{ except: [...] }`. */
|
|
39
|
+
const scopeFilter = z.union([
|
|
40
|
+
z.literal("all"),
|
|
41
|
+
z.object({ only: z.array(slug).max(200) }).strict(),
|
|
42
|
+
z.object({ except: z.array(slug).min(1).max(200) }).strict()
|
|
43
|
+
]);
|
|
44
|
+
/** Where an instance role applies, as the API takes it: either filter left out is `all`. */
|
|
45
|
+
const scopeInput = z.object({
|
|
46
|
+
projects: scopeFilter.optional(),
|
|
47
|
+
environments: scopeFilter.optional()
|
|
48
|
+
}).strict();
|
|
37
49
|
const grantId = z.string().uuid();
|
|
38
50
|
const emailAddress = z.string().email().max(320);
|
|
39
51
|
//#endregion
|
|
40
|
-
export { TOMBSTONE, displayName, emailAddress, environmentSlug, folderName, grantId, instanceRole, isTombstone, principalId, principalType, secretKey, shownMember, slug, tombstoneOf };
|
|
52
|
+
export { TOMBSTONE, displayName, emailAddress, environmentSlug, folderName, grantId, instanceRole, isTombstone, principalId, principalType, scopeFilter, scopeInput, secretKey, shownMember, slug, tombstoneOf };
|
package/dist/vault.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GrantPlace,
|
|
1
|
+
import { _ as Role, b as ServiceSetting, c as InstanceRole, i as GrantPlace, t as CredentialScope, v as Scope } from "./access-CwsFLfTz.js";
|
|
2
2
|
//#region src/access-fault.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* Why the vault's members and grants do not follow from its log, as facts
|
|
@@ -48,14 +48,16 @@ type AccessFault =
|
|
|
48
48
|
principal: string;
|
|
49
49
|
why: 'mac' | 'stale';
|
|
50
50
|
};
|
|
51
|
+
/** A grant a fault names; `environmentSlug` only on a grant on every project of 0.4, which names no project. */
|
|
51
52
|
type FaultGrant = GrantPlace & {
|
|
53
|
+
environmentSlug?: string | null;
|
|
52
54
|
principal: string;
|
|
53
55
|
role: string;
|
|
54
56
|
};
|
|
55
57
|
/** How to name a principal (`user:ada@…`) and a place; ids by default. */
|
|
56
58
|
type FaultNames = {
|
|
57
59
|
principal?: (principal: string) => string;
|
|
58
|
-
/** Asked only of a project's or an environment's place: a grant on every project is named by its path. */
|
|
60
|
+
/** Asked only of a project's or an environment's place: a grant on every project of 0.4 is named by its path. */
|
|
59
61
|
place?: (projectId: string, environmentId: string | null) => string;
|
|
60
62
|
};
|
|
61
63
|
/** The fault in a sentence, with whatever names the caller can give. */
|
|
@@ -146,13 +148,14 @@ export interface Vault {
|
|
|
146
148
|
changes: AccessChange[];
|
|
147
149
|
}>>;
|
|
148
150
|
/**
|
|
149
|
-
* Add a member, bring back a removed one, or change
|
|
150
|
-
*
|
|
151
|
-
* one are dead.
|
|
151
|
+
* Add a member, bring back a removed one, or change a person's instance
|
|
152
|
+
* role and its scope. `generation` is theirs now: sessions and tokens of
|
|
153
|
+
* an earlier one are dead.
|
|
152
154
|
*/
|
|
153
155
|
admit(input: AdmitInput): Promise<Outcome<{
|
|
154
156
|
created: boolean;
|
|
155
|
-
|
|
157
|
+
role: InstanceRole;
|
|
158
|
+
scope: Scope;
|
|
156
159
|
generation: number;
|
|
157
160
|
}>>;
|
|
158
161
|
/** Remove a member: revoke every grant and refuse them until admitted again, from `generation` on. */
|
|
@@ -160,6 +163,16 @@ export interface Vault {
|
|
|
160
163
|
revoked: Grant[];
|
|
161
164
|
generation: number;
|
|
162
165
|
}>>;
|
|
166
|
+
/**
|
|
167
|
+
* The instance's settings, as the vault's newest `settings.change` entry
|
|
168
|
+
* says, by its MAC: an entry is where they live, so each change is logged
|
|
169
|
+
* by being made. Everywhere, until someone narrows it.
|
|
170
|
+
*/
|
|
171
|
+
settings(): Promise<Settings>;
|
|
172
|
+
/** Change the instance's settings: those who run it, never anyone else. */
|
|
173
|
+
setSettings(input: SetSettingsInput): Promise<Outcome<{
|
|
174
|
+
settings: Settings;
|
|
175
|
+
}>>;
|
|
163
176
|
/**
|
|
164
177
|
* Sign the log up to its last entry, in an `audit.checkpoint` entry of
|
|
165
178
|
* the vault's, if the chain recomputes, the vault's entries by their MACs,
|
|
@@ -198,7 +211,7 @@ export interface Vault {
|
|
|
198
211
|
* from it. The links and hashes of the chain are the caller's to
|
|
199
212
|
* recompute through `upTo`, as the app does first; with none, the vault
|
|
200
213
|
* recomputes them all itself. A verdict and nothing else, so anyone may
|
|
201
|
-
* ask; the app asks for
|
|
214
|
+
* ask; the app asks for those who read the whole log (`readsWholeLog`).
|
|
202
215
|
*/
|
|
203
216
|
verifyLog(input: VerifyLogInput): Promise<LogVerification>;
|
|
204
217
|
}
|
|
@@ -207,7 +220,7 @@ export interface Vault {
|
|
|
207
220
|
* `access_seq` names, and what a finding about them is newer than while it
|
|
208
221
|
* stands.
|
|
209
222
|
*/
|
|
210
|
-
export declare const ACCESS_ACTIONS: readonly ["member.add", "member.restore", "member.owner", "member.remove", "access.grant", "access.revoke"];
|
|
223
|
+
export declare const ACCESS_ACTIONS: readonly ["member.add", "member.restore", "member.owner", "member.role", "member.remove", "access.grant", "access.revoke"];
|
|
211
224
|
/** A refusal, already logged by the time the app sees it. */
|
|
212
225
|
export type Refusal = {
|
|
213
226
|
code: RefusalCode;
|
|
@@ -237,7 +250,7 @@ export type RefusalCode =
|
|
|
237
250
|
'not_allowed' |
|
|
238
251
|
/** Root admins come from the vault's configuration and cannot be changed. */
|
|
239
252
|
'root_admin' |
|
|
240
|
-
/** Well-formed, but not something the rules allow: a project role on an environment, an
|
|
253
|
+
/** Well-formed, but not something the rules allow: a project role on an environment, a service account with an instance role. */
|
|
241
254
|
'invalid' |
|
|
242
255
|
/** The vault's own log no longer carries the head the last checkpoint signed, or does not rehash since. */
|
|
243
256
|
'log_broken' |
|
|
@@ -311,7 +324,16 @@ type Correlation = {
|
|
|
311
324
|
*/
|
|
312
325
|
credentialId?: string | null;
|
|
313
326
|
};
|
|
314
|
-
|
|
327
|
+
/**
|
|
328
|
+
* The scope of the credential the request came in on, a service token's or
|
|
329
|
+
* a trust binding's: unlike the correlation, the vault decides on it, and
|
|
330
|
+
* its principal holds nothing past it (`allows`). Null, or left out, for a
|
|
331
|
+
* session's, which carries all its member holds.
|
|
332
|
+
*/
|
|
333
|
+
export type Through = {
|
|
334
|
+
credentialScope?: CredentialScope | null;
|
|
335
|
+
};
|
|
336
|
+
export type UnwrapInput = Correlation & Through & {
|
|
315
337
|
principal: string;
|
|
316
338
|
purpose: Purpose;
|
|
317
339
|
/**
|
|
@@ -336,7 +358,7 @@ export type HolderRef = {
|
|
|
336
358
|
secretId: string;
|
|
337
359
|
path: string;
|
|
338
360
|
};
|
|
339
|
-
export type ReferenceInput = Correlation & {
|
|
361
|
+
export type ReferenceInput = Correlation & Through & {
|
|
340
362
|
principal: string;
|
|
341
363
|
/** `id` is the reference's, a fresh UUID; the vault reads the source by its id. */
|
|
342
364
|
items: {
|
|
@@ -347,13 +369,13 @@ export type ReferenceInput = Correlation & {
|
|
|
347
369
|
};
|
|
348
370
|
}[];
|
|
349
371
|
};
|
|
350
|
-
export type EndReferencesInput = Correlation & {
|
|
372
|
+
export type EndReferencesInput = Correlation & Through & {
|
|
351
373
|
principal: string;
|
|
352
374
|
/** `abandoned`: sealed for a write that stored nothing, so that no row written later revives it. */
|
|
353
375
|
reason: 'replaced' | 'broken' | 'abandoned';
|
|
354
376
|
items: Via[];
|
|
355
377
|
};
|
|
356
|
-
export type WrapInput = Correlation & {
|
|
378
|
+
export type WrapInput = Correlation & Through & {
|
|
357
379
|
principal: string;
|
|
358
380
|
/** `key` is a fresh 32-byte data key, base64. */
|
|
359
381
|
items: {
|
|
@@ -361,7 +383,7 @@ export type WrapInput = Correlation & {
|
|
|
361
383
|
key: string;
|
|
362
384
|
}[];
|
|
363
385
|
};
|
|
364
|
-
export type RewrapInput = Correlation & {
|
|
386
|
+
export type RewrapInput = Correlation & Through & {
|
|
365
387
|
principal: string;
|
|
366
388
|
/** `secret` is the proposed new version; `secretVersionId` is the stored source. */
|
|
367
389
|
items: {
|
|
@@ -370,9 +392,8 @@ export type RewrapInput = Correlation & {
|
|
|
370
392
|
}[];
|
|
371
393
|
};
|
|
372
394
|
/**
|
|
373
|
-
* A grant: on a project, one of its environments (`projectId` is the
|
|
374
|
-
* environment's project
|
|
375
|
-
* project (`GrantPlace` in @coffre/core/access).
|
|
395
|
+
* A grant: on a project, or one of its environments (`projectId` is the
|
|
396
|
+
* environment's project; `GrantPlace` in @coffre/core/access).
|
|
376
397
|
*/
|
|
377
398
|
export type Grant = GrantPlace & {
|
|
378
399
|
role: Role;
|
|
@@ -392,8 +413,13 @@ export type Access = {
|
|
|
392
413
|
/** Advanced by removal, even if the app cannot commit its credential revocations. */
|
|
393
414
|
generation: number;
|
|
394
415
|
isRootAdmin: boolean;
|
|
395
|
-
/**
|
|
396
|
-
|
|
416
|
+
/**
|
|
417
|
+
* Their instance role and its scope, while active: `member` everywhere
|
|
418
|
+
* for service accounts and anyone not active, `owner` everywhere for
|
|
419
|
+
* root admins. Scopes name projects by id.
|
|
420
|
+
*/
|
|
421
|
+
role: InstanceRole;
|
|
422
|
+
scope: Scope;
|
|
397
423
|
/** Live grants only, and none unless active. */
|
|
398
424
|
grants: Grant[];
|
|
399
425
|
/** When the status last changed, and who changed it; null for root admins and strangers. */
|
|
@@ -402,30 +428,42 @@ export type Access = {
|
|
|
402
428
|
};
|
|
403
429
|
/**
|
|
404
430
|
* One place: a role to hold there, with an optional end, or null for none.
|
|
405
|
-
* An environment's place names its project too
|
|
406
|
-
* is null.
|
|
431
|
+
* An environment's place names its project too.
|
|
407
432
|
*/
|
|
408
|
-
export type GrantChange =
|
|
409
|
-
environmentSlug?: string | null;
|
|
433
|
+
export type GrantChange = GrantPlace & {
|
|
410
434
|
role: Role | null;
|
|
411
435
|
expiresAt: string | null;
|
|
412
436
|
};
|
|
413
437
|
export type AccessChange = 'created' | 'updated' | 'revoked' | 'unchanged';
|
|
414
|
-
export type SetAccessInput = Correlation & {
|
|
438
|
+
export type SetAccessInput = Correlation & Through & {
|
|
415
439
|
actor: string;
|
|
416
440
|
principal: string;
|
|
417
441
|
changes: GrantChange[];
|
|
418
442
|
};
|
|
419
|
-
export type AdmitInput = Correlation & {
|
|
443
|
+
export type AdmitInput = Correlation & Through & {
|
|
420
444
|
actor: string;
|
|
421
445
|
principal: string;
|
|
422
|
-
/**
|
|
423
|
-
|
|
446
|
+
/**
|
|
447
|
+
* A person's instance role, and where it applies (everywhere, left out).
|
|
448
|
+
* Left out, an existing member keeps theirs and a new one is a `member`.
|
|
449
|
+
* Changing it takes someone who runs the instance, about someone else.
|
|
450
|
+
*/
|
|
451
|
+
role?: InstanceRole;
|
|
452
|
+
scope?: Scope;
|
|
424
453
|
};
|
|
425
|
-
export type RemoveInput = Correlation & {
|
|
454
|
+
export type RemoveInput = Correlation & Through & {
|
|
426
455
|
actor: string;
|
|
427
456
|
principal: string;
|
|
428
457
|
};
|
|
458
|
+
/** What the instance's settings hold. */
|
|
459
|
+
export type Settings = {
|
|
460
|
+
/** Where people set up service accounts themselves (`ServiceSetting` in @coffre/core/access), projects by id. */
|
|
461
|
+
serviceAccounts: ServiceSetting;
|
|
462
|
+
};
|
|
463
|
+
export type SetSettingsInput = Correlation & Through & {
|
|
464
|
+
actor: string;
|
|
465
|
+
settings: Settings;
|
|
466
|
+
};
|
|
429
467
|
/**
|
|
430
468
|
* A prefix of the log the vault signed: every entry up to `seq`, the last
|
|
431
469
|
* of which has `hash`, which pins every byte before it. Kept as the vault's
|
package/dist/vault.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { everyProjectPath, grantKind } from "./access.js";
|
|
2
1
|
//#region src/checkpoint.ts
|
|
3
2
|
/** What a checkpoint's signature covers: the prefix of the log up to entry `seq`, and when. */
|
|
4
3
|
function checkpointMessage(checkpoint) {
|
|
@@ -26,7 +25,7 @@ function describeAccessFault(fault, names = {}) {
|
|
|
26
25
|
const where = names.place ?? ((projectId, environmentId) => environmentId === null ? projectId : `${projectId}/${environmentId}`);
|
|
27
26
|
const grant = (held) => {
|
|
28
27
|
const { principal, projectId, environmentId, environmentSlug, role } = held;
|
|
29
|
-
const place =
|
|
28
|
+
const place = projectId === null && environmentId === null ? environmentSlug == null ? "*" : `*/${environmentSlug}` : where(projectId ?? "no project", environmentId);
|
|
30
29
|
return `${who(principal)} as ${role} on ${place}`;
|
|
31
30
|
};
|
|
32
31
|
switch (fault.kind) {
|
|
@@ -50,6 +49,7 @@ const ACCESS_ACTIONS = [
|
|
|
50
49
|
"member.add",
|
|
51
50
|
"member.restore",
|
|
52
51
|
"member.owner",
|
|
52
|
+
"member.role",
|
|
53
53
|
"member.remove",
|
|
54
54
|
"access.grant",
|
|
55
55
|
"access.revoke"
|
package/dist/workloads.d.ts
CHANGED
|
@@ -4,15 +4,18 @@
|
|
|
4
4
|
* their platform signs for them (docs/design/oidc.md).
|
|
5
5
|
*
|
|
6
6
|
* A binding names a profile, an issuer, and claims a token must carry, each
|
|
7
|
-
* a top-level string matched exactly. The
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* a top-level string matched exactly. The event that started the run may
|
|
8
|
+
* be any of several: a list. The profile sets the minimum: the server
|
|
9
|
+
* refuses a binding that lacks a claim its profile requires, from the UI,
|
|
10
|
+
* the CLI or the API alike. Say a service is bound to `ci.yml` of
|
|
11
|
+
* `acme/api`, pushed or dispatched on `main`, or run for a pull request
|
|
12
|
+
* into `main`:
|
|
11
13
|
*
|
|
12
14
|
* { profile: 'github', issuer: 'https://token.actions.githubusercontent.com',
|
|
13
15
|
* claims: { repository_owner_id: '9919', repository_id: '41532',
|
|
14
|
-
* workflow_ref: 'acme/api/.github/workflows/
|
|
15
|
-
* ref: 'refs/heads/main',
|
|
16
|
+
* workflow_ref: 'acme/api/.github/workflows/ci.yml@refs/heads/main',
|
|
17
|
+
* ref: 'refs/heads/main',
|
|
18
|
+
* event_name: ['pull_request', 'push', 'workflow_dispatch'] } }
|
|
16
19
|
*/
|
|
17
20
|
export declare const GITHUB_ISSUER = "https://token.actions.githubusercontent.com";
|
|
18
21
|
export declare const GITLAB_ISSUER = "https://gitlab.com";
|
|
@@ -30,12 +33,29 @@ export declare const WORKLOAD_PROFILES: readonly WorkloadProfile[];
|
|
|
30
33
|
/** The claims each profile requires; a binding may add more, to narrow it. */
|
|
31
34
|
export declare const REQUIRED_CLAIMS: Record<WorkloadProfile, readonly string[]>;
|
|
32
35
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
36
|
+
* The events a GitHub binding may trust. The first four are started by
|
|
37
|
+
* someone with write access, at the ref the binding names. The last two
|
|
38
|
+
* can run code nobody reviewed, so the owner opts in to each:
|
|
39
|
+
*
|
|
40
|
+
* - `pull_request` runs the pull request's own code and workflow file, at
|
|
41
|
+
* `refs/pull/<n>/merge`; a binding matches it by the branch it merges
|
|
42
|
+
* into. Forks get no ID token, unless the repository sends write tokens
|
|
43
|
+
* to their pull requests (a setting of private repositories), so this
|
|
44
|
+
* trusts whoever can push a branch to the repository.
|
|
45
|
+
* - `workflow_run` runs the default branch's workflow, but a fork's pull
|
|
46
|
+
* request can start it: the run is as safe as what it does with the
|
|
47
|
+
* code and artifacts of the run that started it.
|
|
48
|
+
*
|
|
49
|
+
* `pull_request_target` and the rest are refused, even when their ref
|
|
50
|
+
* happens to match: they exist to act on anyone's pull request, with the
|
|
51
|
+
* base repository's tokens.
|
|
52
|
+
*/
|
|
53
|
+
export declare const GITHUB_EVENTS: readonly ["push", "workflow_dispatch", "schedule", "release", "pull_request", "workflow_run"];
|
|
54
|
+
/**
|
|
55
|
+
* What trusting each of the last two events exposes, as the UI, the CLI and
|
|
56
|
+
* the approval page say it when one is chosen.
|
|
37
57
|
*/
|
|
38
|
-
export declare const
|
|
58
|
+
export declare const EVENT_EXPOSURE: Readonly<Record<string, string>>;
|
|
39
59
|
/** Pipelines that run the project's own code, at its own ref: never a merge request's. */
|
|
40
60
|
export declare const GITLAB_PIPELINE_SOURCES: readonly ["push", "web", "schedule"];
|
|
41
61
|
/** At most this many live bindings per service, and claims per binding. */
|
|
@@ -43,7 +63,9 @@ export declare const MAX_BINDINGS = 16;
|
|
|
43
63
|
export declare const MAX_CLAIMS = 16;
|
|
44
64
|
/** The longest claim value, in bytes. */
|
|
45
65
|
export declare const MAX_CLAIM_BYTES = 256;
|
|
46
|
-
|
|
66
|
+
/** A claim a binding names: one value, or, for what started the run, any of several. */
|
|
67
|
+
export type ClaimValue = string | string[];
|
|
68
|
+
export type BindingClaims = Record<string, ClaimValue>;
|
|
47
69
|
/** What makes a binding, checked: its profile, its issuer as a URL, and its claims, sorted. */
|
|
48
70
|
export type BindingPolicy = {
|
|
49
71
|
profile: WorkloadProfile;
|
|
@@ -67,6 +89,17 @@ export declare function checkBinding(input: {
|
|
|
67
89
|
}, options?: {
|
|
68
90
|
allowLoopback?: boolean;
|
|
69
91
|
}): BindingPolicy;
|
|
92
|
+
/** A claim's values, one or several. */
|
|
93
|
+
export declare function claimValues(value: ClaimValue | undefined): string[];
|
|
94
|
+
/**
|
|
95
|
+
* The claims of a verified token that differ from what a binding expects,
|
|
96
|
+
* by name: none when it matches. A claim matches when the token carries its
|
|
97
|
+
* value, or one of its values. A GitHub `pull_request` run, at
|
|
98
|
+
* `refs/pull/<n>/merge`, counts as a run at the branch it merges into: its
|
|
99
|
+
* `ref`, and the ref its `workflow_ref` ends in, are read as
|
|
100
|
+
* `refs/heads/<base_ref>`.
|
|
101
|
+
*/
|
|
102
|
+
export declare function differingClaims(profile: WorkloadProfile, expected: BindingClaims, token: Record<string, unknown>): string[];
|
|
70
103
|
/** The claims as the MAC and the database hold them: JSON, sorted by name. */
|
|
71
104
|
export declare function canonicalClaims(claims: BindingClaims): string;
|
|
72
105
|
/**
|
|
@@ -84,17 +117,26 @@ export declare function checkFetchUrl(value: string, what: string, options: {
|
|
|
84
117
|
}): URL;
|
|
85
118
|
/** `refs/heads/main`, or `refs/tags/v1`, as GitHub names a ref. */
|
|
86
119
|
export declare function fullRef(kind: 'branch' | 'tag', name: string): string;
|
|
120
|
+
/** The GitHub events a binding at a branch, or at a tag, may name. */
|
|
121
|
+
export declare function githubEventsAt(kind: 'branch' | 'tag'): string[];
|
|
87
122
|
/** A workflow file at a ref, as GitHub's tokens name it: `acme/api/.github/workflows/deploy.yml@refs/heads/main`. */
|
|
88
123
|
export declare function workflowRef(repository: string, file: string, ref: string): string;
|
|
89
|
-
/**
|
|
124
|
+
/**
|
|
125
|
+
* What started the run, as the presets take it: `event` (`source`, for
|
|
126
|
+
* GitLab), the one value 0.4.6's presets took, or `events` (`sources`), any
|
|
127
|
+
* of several. Naming both, or neither, is a type error and, for a caller the
|
|
128
|
+
* types do not reach, a `BindingInvalid` at run time: a preset never guesses
|
|
129
|
+
* which one was meant.
|
|
130
|
+
*/
|
|
131
|
+
export type Starts<One extends string, Many extends string> = ({ [K in One]: string; } & { [K in Many]?: never; }) | ({ [K in Many]: string[]; } & { [K in One]?: never; });
|
|
132
|
+
/** A workflow of the repository, run at `ref` by any of `events`; a pull request, by the branch it merges into. */
|
|
90
133
|
export declare function githubWorkflow(input: {
|
|
91
134
|
repository: string;
|
|
92
135
|
repositoryId: string;
|
|
93
136
|
ownerId: string;
|
|
94
137
|
workflow: string;
|
|
95
138
|
ref: string;
|
|
96
|
-
|
|
97
|
-
}): Omit<BindingPolicy, 'issuer'>;
|
|
139
|
+
} & Starts<'event', 'events'>): Omit<BindingPolicy, 'issuer'>;
|
|
98
140
|
/**
|
|
99
141
|
* A reusable workflow at one commit, called at `ref`: from one repository,
|
|
100
142
|
* or, with `repositoryId` null, from any repository of the organization. A
|
|
@@ -104,20 +146,18 @@ export declare function githubReusable(input: {
|
|
|
104
146
|
repositoryId: string | null;
|
|
105
147
|
ownerId: string;
|
|
106
148
|
ref: string;
|
|
107
|
-
event: string;
|
|
108
149
|
called: string;
|
|
109
150
|
sha: string;
|
|
110
151
|
caller?: {
|
|
111
152
|
repository: string;
|
|
112
153
|
workflow: string;
|
|
113
154
|
};
|
|
114
|
-
}): Omit<BindingPolicy, 'issuer'>;
|
|
115
|
-
/** A GitLab project's pipelines from `
|
|
155
|
+
} & Starts<'event', 'events'>): Omit<BindingPolicy, 'issuer'>;
|
|
156
|
+
/** A GitLab project's pipelines from any of `sources`, at a branch or tag. */
|
|
116
157
|
export declare function gitlabProject(input: {
|
|
117
158
|
namespaceId: string;
|
|
118
159
|
projectId: string;
|
|
119
160
|
refType: 'branch' | 'tag';
|
|
120
161
|
ref: string;
|
|
121
|
-
|
|
122
|
-
}): Omit<BindingPolicy, 'issuer'>;
|
|
162
|
+
} & Starts<'source', 'sources'>): Omit<BindingPolicy, 'issuer'>;
|
|
123
163
|
//#endregion
|