@mannyc1/ts-release 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +60 -0
- package/CHANGELOG.md +82 -0
- package/README.md +129 -6
- package/SPEC.md +51 -0
- package/apps/release-ts/README.md +7 -0
- package/apps/ts-release-agents/package.json +1 -1
- package/apps/ts-release-agents/src/check.ts +7 -2
- package/apps/ts-release-agents/test/contract.test.ts +2 -2
- package/dist/bin/ts-release.js +154 -47
- package/dist/drivers/archive.d.ts +2 -1
- package/dist/drivers/archive.d.ts.map +1 -1
- package/dist/drivers/archive.js +12 -8
- package/dist/drivers/archive.js.map +1 -1
- package/dist/drivers/process.d.ts.map +1 -1
- package/dist/drivers/process.js +4 -4
- package/dist/drivers/process.js.map +1 -1
- package/dist/drivers/python-wheel.d.ts +21 -0
- package/dist/drivers/python-wheel.d.ts.map +1 -0
- package/dist/drivers/python-wheel.js +109 -0
- package/dist/drivers/python-wheel.js.map +1 -0
- package/dist/drivers/workspace.d.ts +3 -0
- package/dist/drivers/workspace.d.ts.map +1 -1
- package/dist/drivers/workspace.js +5 -1
- package/dist/drivers/workspace.js.map +1 -1
- package/dist/operation-journal/authority.d.ts +3 -0
- package/dist/operation-journal/authority.d.ts.map +1 -0
- package/dist/operation-journal/authority.js +147 -0
- package/dist/operation-journal/authority.js.map +1 -0
- package/dist/operation-journal/aws/deadline.d.ts +12 -0
- package/dist/operation-journal/aws/deadline.d.ts.map +1 -0
- package/dist/operation-journal/aws/deadline.js +38 -0
- package/dist/operation-journal/aws/deadline.js.map +1 -0
- package/dist/operation-journal/aws/oidc.d.ts +27 -0
- package/dist/operation-journal/aws/oidc.d.ts.map +1 -0
- package/dist/operation-journal/aws/oidc.js +361 -0
- package/dist/operation-journal/aws/oidc.js.map +1 -0
- package/dist/operation-journal/aws/policy.d.ts +42 -0
- package/dist/operation-journal/aws/policy.d.ts.map +1 -0
- package/dist/operation-journal/aws/policy.js +404 -0
- package/dist/operation-journal/aws/policy.js.map +1 -0
- package/dist/operation-journal/aws/s3-boundary.d.ts +17 -0
- package/dist/operation-journal/aws/s3-boundary.d.ts.map +1 -0
- package/dist/operation-journal/aws/s3-boundary.js +417 -0
- package/dist/operation-journal/aws/s3-boundary.js.map +1 -0
- package/dist/operation-journal/aws.d.ts +11 -0
- package/dist/operation-journal/aws.d.ts.map +1 -0
- package/dist/operation-journal/aws.js +50 -0
- package/dist/operation-journal/aws.js.map +1 -0
- package/dist/operation-journal/canonical.d.ts +52 -0
- package/dist/operation-journal/canonical.d.ts.map +1 -0
- package/dist/operation-journal/canonical.js +282 -0
- package/dist/operation-journal/canonical.js.map +1 -0
- package/dist/operation-journal/model.d.ts +251 -0
- package/dist/operation-journal/model.d.ts.map +1 -0
- package/dist/operation-journal/model.js +56 -0
- package/dist/operation-journal/model.js.map +1 -0
- package/dist/operation-journal/reducer.d.ts +28 -0
- package/dist/operation-journal/reducer.d.ts.map +1 -0
- package/dist/operation-journal/reducer.js +73 -0
- package/dist/operation-journal/reducer.js.map +1 -0
- package/dist/operation-journal/s3.d.ts +7 -0
- package/dist/operation-journal/s3.d.ts.map +1 -0
- package/dist/operation-journal/s3.js +667 -0
- package/dist/operation-journal/s3.js.map +1 -0
- package/dist/operation-journal.d.ts +14 -0
- package/dist/operation-journal.d.ts.map +1 -0
- package/dist/operation-journal.js +12 -0
- package/dist/operation-journal.js.map +1 -0
- package/dist/platform/credentials.d.ts.map +1 -1
- package/dist/platform/credentials.js +65 -3
- package/dist/platform/credentials.js.map +1 -1
- package/dist/publication/github.d.ts.map +1 -1
- package/dist/publication/github.js +66 -15
- package/dist/publication/github.js.map +1 -1
- package/dist/release/capabilities.d.ts.map +1 -1
- package/dist/release/capabilities.js +2 -2
- package/dist/release/capabilities.js.map +1 -1
- package/dist/release/prepare.d.ts.map +1 -1
- package/dist/release/prepare.js +4 -5
- package/dist/release/prepare.js.map +1 -1
- package/dist/release/prepared-store.d.ts.map +1 -1
- package/dist/release/prepared-store.js +11 -9
- package/dist/release/prepared-store.js.map +1 -1
- package/package.json +19 -4
- package/src/drivers/archive.ts +17 -8
- package/src/drivers/process.ts +3 -4
- package/src/drivers/python-wheel.ts +145 -0
- package/src/drivers/workspace.ts +7 -1
- package/src/operation-journal/authority.ts +162 -0
- package/src/operation-journal/aws/deadline.ts +41 -0
- package/src/operation-journal/aws/oidc.ts +439 -0
- package/src/operation-journal/aws/policy.ts +491 -0
- package/src/operation-journal/aws/s3-boundary.ts +562 -0
- package/src/operation-journal/aws.ts +60 -0
- package/src/operation-journal/canonical.ts +373 -0
- package/src/operation-journal/model.ts +301 -0
- package/src/operation-journal/reducer.ts +112 -0
- package/src/operation-journal/s3.ts +884 -0
- package/src/operation-journal.ts +51 -0
- package/src/platform/credentials.ts +71 -3
- package/src/publication/github.ts +83 -15
- package/src/release/capabilities.ts +3 -2
- package/src/release/prepare.ts +3 -5
- package/src/release/prepared-store.ts +8 -7
- package/templates/README.md +2 -2
- package/templates/github-actions/release.yml +24 -7
- package/templates/github-actions/reviewed-release.yml +49 -14
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import { encodeCanonicalJson, parseStrictJson, type Json } from "../../model/canonical.js"
|
|
2
|
+
import { objectChecksum } from "../canonical.js"
|
|
3
|
+
import {
|
|
4
|
+
S3JournalBoundaryError,
|
|
5
|
+
type S3JournalAuthority,
|
|
6
|
+
type S3JournalOidcTrustConditions
|
|
7
|
+
} from "../model.js"
|
|
8
|
+
|
|
9
|
+
const encoder = new TextEncoder()
|
|
10
|
+
|
|
11
|
+
type JsonObject = { readonly [key: string]: Json }
|
|
12
|
+
|
|
13
|
+
interface NormalizedPrincipal {
|
|
14
|
+
readonly AWS?: ReadonlyArray<string>
|
|
15
|
+
readonly Federated?: ReadonlyArray<string>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface NormalizedStatement {
|
|
19
|
+
readonly Sid: string
|
|
20
|
+
readonly Effect: "Allow" | "Deny"
|
|
21
|
+
readonly Principal?: "*" | NormalizedPrincipal
|
|
22
|
+
readonly Action: ReadonlyArray<string>
|
|
23
|
+
readonly Resource?: ReadonlyArray<string>
|
|
24
|
+
readonly NotResource?: ReadonlyArray<string>
|
|
25
|
+
readonly Condition?: Readonly<Record<string, Readonly<Record<string, ReadonlyArray<string>>>>>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface NormalizedPolicy {
|
|
29
|
+
readonly Version: "2012-10-17"
|
|
30
|
+
readonly Statement: ReadonlyArray<NormalizedStatement>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface AwsJournalPolicyCoordinates {
|
|
34
|
+
readonly accountId: string
|
|
35
|
+
readonly bucketArn: string
|
|
36
|
+
readonly roleArn: string
|
|
37
|
+
readonly rolePolicyName: string
|
|
38
|
+
readonly oidcTrust: S3JournalOidcTrustConditions
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface AwsJournalObservedPolicyDigests {
|
|
42
|
+
readonly bucketPolicyDigest: string
|
|
43
|
+
readonly rolePolicyDigest: string
|
|
44
|
+
readonly oidcTrustPolicyDigest: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const boundaryFailure = (reason: string): S3JournalBoundaryError =>
|
|
48
|
+
S3JournalBoundaryError.make({
|
|
49
|
+
operation: "observe-authority",
|
|
50
|
+
commitment: "not-applicable",
|
|
51
|
+
reason
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const fail = (reason: string): never => {
|
|
55
|
+
throw boundaryFailure(reason)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const isObject = (value: Json): value is JsonObject =>
|
|
59
|
+
typeof value === "object" && value !== null && !Array.isArray(value)
|
|
60
|
+
|
|
61
|
+
const exactObject = (
|
|
62
|
+
value: Json,
|
|
63
|
+
fields: ReadonlyArray<string>,
|
|
64
|
+
label: string
|
|
65
|
+
): JsonObject => {
|
|
66
|
+
if (!isObject(value)) fail(`${label} must be an object.`)
|
|
67
|
+
const object = value as JsonObject
|
|
68
|
+
const actual = Object.keys(object).sort()
|
|
69
|
+
const expected = [...fields].sort()
|
|
70
|
+
if (actual.length !== expected.length || actual.some((field, index) => field !== expected[index])) {
|
|
71
|
+
fail(`${label} fields are not the exact admitted set.`)
|
|
72
|
+
}
|
|
73
|
+
return object
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const stringValue = (value: Json | undefined, label: string): string => {
|
|
77
|
+
if (typeof value !== "string" || value.length === 0 || value !== value.normalize("NFC")) {
|
|
78
|
+
fail(`${label} must be one non-empty NFC string.`)
|
|
79
|
+
}
|
|
80
|
+
return value as string
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const stringSet = (value: Json | undefined, label: string): ReadonlyArray<string> => {
|
|
84
|
+
const values = Array.isArray(value)
|
|
85
|
+
? value.map((item, index) => stringValue(item, `${label}[${index}]`))
|
|
86
|
+
: [stringValue(value, label)]
|
|
87
|
+
const sorted = [...values].sort()
|
|
88
|
+
if (sorted.length === 0 || new Set(sorted).size !== sorted.length) {
|
|
89
|
+
fail(`${label} must be one duplicate-free string set.`)
|
|
90
|
+
}
|
|
91
|
+
return sorted
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const normalizePrincipal = (value: Json | undefined, label: string): "*" | NormalizedPrincipal => {
|
|
95
|
+
if (value === "*") return "*"
|
|
96
|
+
if (!isObject(value!)) fail(`${label} must be '*' or an exact principal object.`)
|
|
97
|
+
const principal = value as JsonObject
|
|
98
|
+
const fields = Object.keys(principal).sort()
|
|
99
|
+
if (fields.length !== 1 || (fields[0] !== "AWS" && fields[0] !== "Federated")) {
|
|
100
|
+
fail(`${label} must contain exactly one AWS or Federated principal kind.`)
|
|
101
|
+
}
|
|
102
|
+
const field = fields[0] as "AWS" | "Federated"
|
|
103
|
+
return { [field]: stringSet(principal[field], `${label}.${field}`) }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const normalizeCondition = (
|
|
107
|
+
value: Json | undefined,
|
|
108
|
+
label: string
|
|
109
|
+
): Readonly<Record<string, Readonly<Record<string, ReadonlyArray<string>>>>> => {
|
|
110
|
+
if (!isObject(value!)) fail(`${label} must be an object.`)
|
|
111
|
+
const condition = value as JsonObject
|
|
112
|
+
const normalized: Record<string, Record<string, ReadonlyArray<string>>> = {}
|
|
113
|
+
const operators = Object.keys(condition).sort()
|
|
114
|
+
if (operators.length === 0) fail(`${label} must not be empty.`)
|
|
115
|
+
for (const operator of operators) {
|
|
116
|
+
if (!/^[A-Za-z][A-Za-z0-9:]*$/u.test(operator)) fail(`${label} has an invalid operator.`)
|
|
117
|
+
const entries = condition[operator]
|
|
118
|
+
if (!isObject(entries!)) fail(`${label}.${operator} must be an object.`)
|
|
119
|
+
const normalizedEntries: Record<string, ReadonlyArray<string>> = {}
|
|
120
|
+
const keys = Object.keys(entries as JsonObject).sort()
|
|
121
|
+
if (keys.length === 0) fail(`${label}.${operator} must not be empty.`)
|
|
122
|
+
for (const key of keys) {
|
|
123
|
+
normalizedEntries[key] = stringSet((entries as JsonObject)[key], `${label}.${operator}.${key}`)
|
|
124
|
+
}
|
|
125
|
+
normalized[operator] = normalizedEntries
|
|
126
|
+
}
|
|
127
|
+
return normalized
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const normalizeStatement = (value: Json, index: number): NormalizedStatement => {
|
|
131
|
+
if (!isObject(value)) fail(`Policy statement ${index} must be an object.`)
|
|
132
|
+
const object = value as JsonObject
|
|
133
|
+
const admitted = new Set(["Sid", "Effect", "Principal", "Action", "Resource", "NotResource", "Condition"])
|
|
134
|
+
for (const field of Object.keys(object)) {
|
|
135
|
+
if (!admitted.has(field)) fail(`Policy statement ${index} contains unadmitted field ${field}.`)
|
|
136
|
+
}
|
|
137
|
+
const sid = stringValue(object.Sid, `Policy statement ${index}.Sid`)
|
|
138
|
+
if (!/^[A-Za-z0-9]{1,128}$/u.test(sid)) fail(`Policy statement ${index}.Sid is not canonical.`)
|
|
139
|
+
const effect = stringValue(object.Effect, `Policy statement ${index}.Effect`)
|
|
140
|
+
if (effect !== "Allow" && effect !== "Deny") fail(`Policy statement ${index}.Effect is not admitted.`)
|
|
141
|
+
if (object.Resource !== undefined && object.NotResource !== undefined) {
|
|
142
|
+
fail(`Policy statement ${index} cannot contain both Resource and NotResource.`)
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
Sid: sid,
|
|
146
|
+
Effect: effect as "Allow" | "Deny",
|
|
147
|
+
...(object.Principal === undefined ? {} : { Principal: normalizePrincipal(object.Principal, `Policy statement ${index}.Principal`) }),
|
|
148
|
+
Action: stringSet(object.Action, `Policy statement ${index}.Action`),
|
|
149
|
+
...(object.Resource === undefined ? {} : { Resource: stringSet(object.Resource, `Policy statement ${index}.Resource`) }),
|
|
150
|
+
...(object.NotResource === undefined ? {} : { NotResource: stringSet(object.NotResource, `Policy statement ${index}.NotResource`) }),
|
|
151
|
+
...(object.Condition === undefined ? {} : { Condition: normalizeCondition(object.Condition, `Policy statement ${index}.Condition`) })
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const parsePolicyDocument = (document: string, label: string): NormalizedPolicy => {
|
|
156
|
+
let parsed: Json
|
|
157
|
+
try {
|
|
158
|
+
const source = document.trimStart().startsWith("{") ? document : decodeURIComponent(document)
|
|
159
|
+
parsed = parseStrictJson(source)
|
|
160
|
+
} catch {
|
|
161
|
+
return fail(`${label} is not one valid JSON or RFC3986-encoded JSON policy.`)
|
|
162
|
+
}
|
|
163
|
+
const policy = exactObject(parsed, ["Statement", "Version"], label)
|
|
164
|
+
if (policy.Version !== "2012-10-17") fail(`${label} has an unsupported version.`)
|
|
165
|
+
const rawStatements = Array.isArray(policy.Statement) ? policy.Statement : [policy.Statement!]
|
|
166
|
+
if (rawStatements.length === 0) fail(`${label} has no statements.`)
|
|
167
|
+
const statements = rawStatements.map(normalizeStatement).sort((left, right) => left.Sid.localeCompare(right.Sid))
|
|
168
|
+
if (new Set(statements.map((statement) => statement.Sid)).size !== statements.length) {
|
|
169
|
+
fail(`${label} repeats a statement Sid.`)
|
|
170
|
+
}
|
|
171
|
+
return { Version: "2012-10-17", Statement: statements }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const canonicalPolicy = (policy: NormalizedPolicy): string => encodeCanonicalJson(policy)
|
|
175
|
+
const policyDigest = (policy: NormalizedPolicy): string => objectChecksum(encoder.encode(canonicalPolicy(policy)))
|
|
176
|
+
|
|
177
|
+
const condition = (
|
|
178
|
+
operator: string,
|
|
179
|
+
entries: Readonly<Record<string, string | ReadonlyArray<string>>>
|
|
180
|
+
): Readonly<Record<string, Readonly<Record<string, ReadonlyArray<string>>>>> => ({
|
|
181
|
+
[operator]: Object.fromEntries(Object.entries(entries).map(([key, value]) => [
|
|
182
|
+
key,
|
|
183
|
+
[...(Array.isArray(value) ? value : [value])].sort()
|
|
184
|
+
]))
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
const statement = (value: NormalizedStatement): NormalizedStatement => value
|
|
188
|
+
|
|
189
|
+
const requireImmutableCalledWorkflow = (coordinates: AwsJournalPolicyCoordinates): void => {
|
|
190
|
+
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/\.github\/workflows\/[A-Za-z0-9._-]+\.ya?ml@[a-f0-9]{40}$/u
|
|
191
|
+
.test(coordinates.oidcTrust.jobWorkflowRef)) {
|
|
192
|
+
fail("Journal OIDC trust must pin the called workflow to one immutable source SHA.")
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export const makeAwsJournalTrustPolicy = (
|
|
197
|
+
coordinates: AwsJournalPolicyCoordinates
|
|
198
|
+
): NormalizedPolicy => {
|
|
199
|
+
requireImmutableCalledWorkflow(coordinates)
|
|
200
|
+
return {
|
|
201
|
+
Version: "2012-10-17",
|
|
202
|
+
Statement: [statement({
|
|
203
|
+
Sid: "AllowGitHubJournalOidc",
|
|
204
|
+
Effect: "Allow",
|
|
205
|
+
Principal: {
|
|
206
|
+
Federated: [`arn:aws:iam::${coordinates.accountId}:oidc-provider/token.actions.githubusercontent.com`]
|
|
207
|
+
},
|
|
208
|
+
Action: ["sts:AssumeRoleWithWebIdentity"],
|
|
209
|
+
Condition: condition("StringEquals", {
|
|
210
|
+
"token.actions.githubusercontent.com:aud": coordinates.oidcTrust.audience,
|
|
211
|
+
"token.actions.githubusercontent.com:sub": coordinates.oidcTrust.subject,
|
|
212
|
+
"token.actions.githubusercontent.com:repository": coordinates.oidcTrust.repository,
|
|
213
|
+
"token.actions.githubusercontent.com:repository_id": coordinates.oidcTrust.repositoryId,
|
|
214
|
+
"token.actions.githubusercontent.com:repository_owner_id": coordinates.oidcTrust.repositoryOwnerId,
|
|
215
|
+
"token.actions.githubusercontent.com:workflow": coordinates.oidcTrust.workflow,
|
|
216
|
+
"token.actions.githubusercontent.com:ref": coordinates.oidcTrust.ref,
|
|
217
|
+
"token.actions.githubusercontent.com:environment": coordinates.oidcTrust.environment,
|
|
218
|
+
"token.actions.githubusercontent.com:job_workflow_ref": coordinates.oidcTrust.jobWorkflowRef
|
|
219
|
+
})
|
|
220
|
+
})]
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export const makeAwsJournalRolePolicy = (
|
|
225
|
+
coordinates: AwsJournalPolicyCoordinates
|
|
226
|
+
): NormalizedPolicy => ({
|
|
227
|
+
Version: "2012-10-17",
|
|
228
|
+
Statement: [
|
|
229
|
+
statement({
|
|
230
|
+
Sid: "DenyJournalBucketGovernanceMutation",
|
|
231
|
+
Effect: "Deny",
|
|
232
|
+
Action: [
|
|
233
|
+
"s3:DeleteBucket*",
|
|
234
|
+
"s3:DeletePublicAccessBlock",
|
|
235
|
+
"s3:PutBucket*",
|
|
236
|
+
"s3:PutLifecycleConfiguration",
|
|
237
|
+
"s3:PutPublicAccessBlock"
|
|
238
|
+
].sort(),
|
|
239
|
+
Resource: [coordinates.bucketArn]
|
|
240
|
+
}),
|
|
241
|
+
statement({
|
|
242
|
+
Sid: "DenyJournalCopy",
|
|
243
|
+
Effect: "Deny",
|
|
244
|
+
Action: ["s3:PutObject"],
|
|
245
|
+
Resource: [`${coordinates.bucketArn}/operation-journal/v1/*`],
|
|
246
|
+
Condition: condition("Null", { "s3:x-amz-copy-source": "false" })
|
|
247
|
+
}),
|
|
248
|
+
statement({
|
|
249
|
+
Sid: "DenyJournalDestructiveObjectMutation",
|
|
250
|
+
Effect: "Deny",
|
|
251
|
+
Action: [
|
|
252
|
+
"s3:AbortMultipartUpload",
|
|
253
|
+
"s3:BypassGovernanceRetention",
|
|
254
|
+
"s3:DeleteObject",
|
|
255
|
+
"s3:DeleteObjectVersion",
|
|
256
|
+
"s3:PutObjectAcl",
|
|
257
|
+
"s3:PutObjectLegalHold",
|
|
258
|
+
"s3:PutObjectRetention",
|
|
259
|
+
"s3:PutObjectTagging"
|
|
260
|
+
].sort(),
|
|
261
|
+
Resource: [`${coordinates.bucketArn}/*`]
|
|
262
|
+
}),
|
|
263
|
+
statement({
|
|
264
|
+
Sid: "DenyJournalIamMutation",
|
|
265
|
+
Effect: "Deny",
|
|
266
|
+
Action: [
|
|
267
|
+
"iam:Add*",
|
|
268
|
+
"iam:Attach*",
|
|
269
|
+
"iam:Change*",
|
|
270
|
+
"iam:Create*",
|
|
271
|
+
"iam:Deactivate*",
|
|
272
|
+
"iam:Delete*",
|
|
273
|
+
"iam:Detach*",
|
|
274
|
+
"iam:Enable*",
|
|
275
|
+
"iam:PassRole",
|
|
276
|
+
"iam:Put*",
|
|
277
|
+
"iam:Remove*",
|
|
278
|
+
"iam:Reset*",
|
|
279
|
+
"iam:Resync*",
|
|
280
|
+
"iam:Set*",
|
|
281
|
+
"iam:Tag*",
|
|
282
|
+
"iam:Untag*",
|
|
283
|
+
"iam:Update*",
|
|
284
|
+
"iam:Upload*"
|
|
285
|
+
].sort(),
|
|
286
|
+
Resource: ["*"]
|
|
287
|
+
}),
|
|
288
|
+
statement({
|
|
289
|
+
Sid: "DenyJournalMultipart",
|
|
290
|
+
Effect: "Deny",
|
|
291
|
+
Action: ["s3:PutObject"],
|
|
292
|
+
Resource: [`${coordinates.bucketArn}/operation-journal/v1/*`],
|
|
293
|
+
Condition: condition("Bool", { "s3:ObjectCreationOperation": "false" })
|
|
294
|
+
}),
|
|
295
|
+
statement({
|
|
296
|
+
Sid: "DenyJournalWritesOutsideNamespace",
|
|
297
|
+
Effect: "Deny",
|
|
298
|
+
Action: ["s3:PutObject"],
|
|
299
|
+
NotResource: [`${coordinates.bucketArn}/operation-journal/v1/*`]
|
|
300
|
+
}),
|
|
301
|
+
statement({
|
|
302
|
+
Sid: "ListJournalNamespace",
|
|
303
|
+
Effect: "Allow",
|
|
304
|
+
Action: ["s3:ListBucketVersions"],
|
|
305
|
+
Resource: [coordinates.bucketArn],
|
|
306
|
+
Condition: condition("StringLike", { "s3:prefix": "operation-journal/v1/*" })
|
|
307
|
+
}),
|
|
308
|
+
statement({
|
|
309
|
+
Sid: "ReadJournalBucketGovernance",
|
|
310
|
+
Effect: "Allow",
|
|
311
|
+
Action: [
|
|
312
|
+
"s3:GetBucketObjectLockConfiguration",
|
|
313
|
+
"s3:GetBucketOwnershipControls",
|
|
314
|
+
"s3:GetBucketPolicy",
|
|
315
|
+
"s3:GetBucketPolicyStatus",
|
|
316
|
+
"s3:GetBucketPublicAccessBlock",
|
|
317
|
+
"s3:GetBucketVersioning"
|
|
318
|
+
].sort(),
|
|
319
|
+
Resource: [coordinates.bucketArn]
|
|
320
|
+
}),
|
|
321
|
+
statement({
|
|
322
|
+
Sid: "ReadJournalRole",
|
|
323
|
+
Effect: "Allow",
|
|
324
|
+
Action: [
|
|
325
|
+
"iam:GetRole",
|
|
326
|
+
"iam:GetRolePolicy",
|
|
327
|
+
"iam:ListAttachedRolePolicies",
|
|
328
|
+
"iam:ListRolePolicies"
|
|
329
|
+
].sort(),
|
|
330
|
+
Resource: [coordinates.roleArn]
|
|
331
|
+
}),
|
|
332
|
+
statement({
|
|
333
|
+
Sid: "ReadWriteJournalObjects",
|
|
334
|
+
Effect: "Allow",
|
|
335
|
+
Action: [
|
|
336
|
+
"s3:GetObjectRetention",
|
|
337
|
+
"s3:GetObjectVersion",
|
|
338
|
+
"s3:GetObjectVersionAttributes",
|
|
339
|
+
"s3:PutObject"
|
|
340
|
+
].sort(),
|
|
341
|
+
Resource: [`${coordinates.bucketArn}/operation-journal/v1/*`]
|
|
342
|
+
})
|
|
343
|
+
].sort((left, right) => left.Sid.localeCompare(right.Sid))
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
export const makeAwsJournalBucketPolicy = (
|
|
347
|
+
coordinates: AwsJournalPolicyCoordinates
|
|
348
|
+
): NormalizedPolicy => ({
|
|
349
|
+
Version: "2012-10-17",
|
|
350
|
+
Statement: [
|
|
351
|
+
statement({
|
|
352
|
+
Sid: "DenyJournalCopy",
|
|
353
|
+
Effect: "Deny",
|
|
354
|
+
Principal: "*",
|
|
355
|
+
Action: ["s3:PutObject"],
|
|
356
|
+
Resource: [`${coordinates.bucketArn}/operation-journal/v1/*`],
|
|
357
|
+
Condition: condition("Null", { "s3:x-amz-copy-source": "false" })
|
|
358
|
+
}),
|
|
359
|
+
statement({
|
|
360
|
+
Sid: "DenyJournalDeletes",
|
|
361
|
+
Effect: "Deny",
|
|
362
|
+
Principal: "*",
|
|
363
|
+
Action: ["s3:DeleteObject", "s3:DeleteObjectVersion"],
|
|
364
|
+
Resource: [`${coordinates.bucketArn}/operation-journal/v1/*`]
|
|
365
|
+
}),
|
|
366
|
+
statement({
|
|
367
|
+
Sid: "DenyJournalDualConditionWrite",
|
|
368
|
+
Effect: "Deny",
|
|
369
|
+
Principal: "*",
|
|
370
|
+
Action: ["s3:PutObject"],
|
|
371
|
+
Resource: [`${coordinates.bucketArn}/operation-journal/v1/*`],
|
|
372
|
+
Condition: {
|
|
373
|
+
Bool: { "s3:ObjectCreationOperation": ["true"] },
|
|
374
|
+
Null: { "s3:if-match": ["false"], "s3:if-none-match": ["false"] }
|
|
375
|
+
}
|
|
376
|
+
}),
|
|
377
|
+
statement({
|
|
378
|
+
Sid: "DenyJournalMetadataMutation",
|
|
379
|
+
Effect: "Deny",
|
|
380
|
+
Principal: "*",
|
|
381
|
+
Action: [
|
|
382
|
+
"s3:PutObjectAcl",
|
|
383
|
+
"s3:PutObjectLegalHold",
|
|
384
|
+
"s3:PutObjectRetention",
|
|
385
|
+
"s3:PutObjectTagging"
|
|
386
|
+
].sort(),
|
|
387
|
+
Resource: [`${coordinates.bucketArn}/operation-journal/v1/*`]
|
|
388
|
+
}),
|
|
389
|
+
statement({
|
|
390
|
+
Sid: "DenyJournalMultipart",
|
|
391
|
+
Effect: "Deny",
|
|
392
|
+
Principal: "*",
|
|
393
|
+
Action: ["s3:PutObject"],
|
|
394
|
+
Resource: [`${coordinates.bucketArn}/operation-journal/v1/*`],
|
|
395
|
+
Condition: condition("Bool", { "s3:ObjectCreationOperation": "false" })
|
|
396
|
+
}),
|
|
397
|
+
statement({
|
|
398
|
+
Sid: "DenyJournalRoleWriteOutsideNamespace",
|
|
399
|
+
Effect: "Deny",
|
|
400
|
+
Principal: "*",
|
|
401
|
+
Action: ["s3:PutObject"],
|
|
402
|
+
NotResource: [`${coordinates.bucketArn}/operation-journal/v1/*`],
|
|
403
|
+
Condition: condition("ArnEquals", { "aws:PrincipalArn": coordinates.roleArn })
|
|
404
|
+
}),
|
|
405
|
+
statement({
|
|
406
|
+
Sid: "DenyJournalUnconditionalWrite",
|
|
407
|
+
Effect: "Deny",
|
|
408
|
+
Principal: "*",
|
|
409
|
+
Action: ["s3:PutObject"],
|
|
410
|
+
Resource: [`${coordinates.bucketArn}/operation-journal/v1/*`],
|
|
411
|
+
Condition: {
|
|
412
|
+
Bool: { "s3:ObjectCreationOperation": ["true"] },
|
|
413
|
+
Null: { "s3:if-match": ["true"], "s3:if-none-match": ["true"] }
|
|
414
|
+
}
|
|
415
|
+
}),
|
|
416
|
+
statement({
|
|
417
|
+
Sid: "DenyNonJournalPrincipalList",
|
|
418
|
+
Effect: "Deny",
|
|
419
|
+
Principal: "*",
|
|
420
|
+
Action: ["s3:ListBucketVersions"],
|
|
421
|
+
Resource: [coordinates.bucketArn],
|
|
422
|
+
Condition: condition("ArnNotEquals", { "aws:PrincipalArn": coordinates.roleArn })
|
|
423
|
+
}),
|
|
424
|
+
statement({
|
|
425
|
+
Sid: "DenyNonJournalPrincipalObjectAccess",
|
|
426
|
+
Effect: "Deny",
|
|
427
|
+
Principal: "*",
|
|
428
|
+
Action: [
|
|
429
|
+
"s3:GetObject",
|
|
430
|
+
"s3:GetObjectAttributes",
|
|
431
|
+
"s3:GetObjectRetention",
|
|
432
|
+
"s3:GetObjectVersion",
|
|
433
|
+
"s3:GetObjectVersionAttributes",
|
|
434
|
+
"s3:PutObject"
|
|
435
|
+
].sort(),
|
|
436
|
+
Resource: [`${coordinates.bucketArn}/operation-journal/v1/*`],
|
|
437
|
+
Condition: condition("ArnNotEquals", { "aws:PrincipalArn": coordinates.roleArn })
|
|
438
|
+
})
|
|
439
|
+
].sort((left, right) => left.Sid.localeCompare(right.Sid))
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
const requireExactPolicy = (
|
|
443
|
+
observedDocument: string,
|
|
444
|
+
expected: NormalizedPolicy,
|
|
445
|
+
label: string
|
|
446
|
+
): NormalizedPolicy => {
|
|
447
|
+
const observed = parsePolicyDocument(observedDocument, label)
|
|
448
|
+
if (canonicalPolicy(observed) !== canonicalPolicy(expected)) {
|
|
449
|
+
fail(`${label} does not equal the one admitted operational-journal policy.`)
|
|
450
|
+
}
|
|
451
|
+
return observed
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export const validateAwsJournalPolicies = (input: {
|
|
455
|
+
readonly coordinates: AwsJournalPolicyCoordinates
|
|
456
|
+
readonly trustPolicyDocument: string
|
|
457
|
+
readonly rolePolicyDocument: string
|
|
458
|
+
readonly bucketPolicyDocument: string
|
|
459
|
+
}): AwsJournalObservedPolicyDigests => {
|
|
460
|
+
const trust = requireExactPolicy(
|
|
461
|
+
input.trustPolicyDocument,
|
|
462
|
+
makeAwsJournalTrustPolicy(input.coordinates),
|
|
463
|
+
"Journal role trust policy"
|
|
464
|
+
)
|
|
465
|
+
const role = requireExactPolicy(
|
|
466
|
+
input.rolePolicyDocument,
|
|
467
|
+
makeAwsJournalRolePolicy(input.coordinates),
|
|
468
|
+
"Journal role permission policy"
|
|
469
|
+
)
|
|
470
|
+
const bucket = requireExactPolicy(
|
|
471
|
+
input.bucketPolicyDocument,
|
|
472
|
+
makeAwsJournalBucketPolicy(input.coordinates),
|
|
473
|
+
"Journal bucket policy"
|
|
474
|
+
)
|
|
475
|
+
return {
|
|
476
|
+
bucketPolicyDigest: policyDigest(bucket),
|
|
477
|
+
rolePolicyDigest: policyDigest(role),
|
|
478
|
+
oidcTrustPolicyDigest: policyDigest(trust)
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export const assertAwsJournalPolicyDigests = (
|
|
483
|
+
authority: S3JournalAuthority,
|
|
484
|
+
observed: AwsJournalObservedPolicyDigests
|
|
485
|
+
): void => {
|
|
486
|
+
if (authority.bucketPolicyDigest !== observed.bucketPolicyDigest ||
|
|
487
|
+
authority.rolePolicyDigest !== observed.rolePolicyDigest ||
|
|
488
|
+
authority.oidcTrustPolicyDigest !== observed.oidcTrustPolicyDigest) {
|
|
489
|
+
fail("Observed journal policy digests drifted from the exact activation contract.")
|
|
490
|
+
}
|
|
491
|
+
}
|