@microagi/alchemy-gcp 0.10.0 → 0.11.1
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/lib/Storage/StorageBucketIamMember.d.ts +55 -3
- package/lib/Storage/StorageBucketIamMember.d.ts.map +1 -1
- package/lib/Storage/StorageBucketIamMember.js +76 -15
- package/lib/Storage/StorageBucketIamMember.js.map +1 -1
- package/lib/Storage/index.d.ts +1 -1
- package/lib/Storage/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/Storage/StorageBucketIamMember.ts +145 -19
- package/src/Storage/index.ts +1 -0
|
@@ -12,9 +12,20 @@ import type * as GCP from "../Providers.ts";
|
|
|
12
12
|
* cannot do — **removes it again on delete**, so grants revoke when the
|
|
13
13
|
* declaring resource leaves the stack.
|
|
14
14
|
*
|
|
15
|
-
* Identity is the `(bucket, role, member)`
|
|
16
|
-
* immutable — changing any replaces (the old
|
|
17
|
-
* one granted).
|
|
15
|
+
* Identity is the `(bucket, role, member, condition.expression)` tuple;
|
|
16
|
+
* all are immutable — changing any replaces (the old grant is revoked,
|
|
17
|
+
* the new one granted).
|
|
18
|
+
*
|
|
19
|
+
* With a `condition`, the member lands in the binding whose condition
|
|
20
|
+
* **expression** matches (CEL, compared byte-for-byte) — the binding is
|
|
21
|
+
* created with the given title/description if absent. Title and
|
|
22
|
+
* description are create-time metadata only: an existing binding with
|
|
23
|
+
* the same expression but a different title is adopted as-is, never
|
|
24
|
+
* retitled (that lets hand-applied conditional grants converge into the
|
|
25
|
+
* stack without a duplicate binding). Conditional bindings require the
|
|
26
|
+
* bucket to have uniform bucket-level access (GCS rejects conditions
|
|
27
|
+
* otherwise) and policy version 3 — the read-modify-write here always
|
|
28
|
+
* requests/writes version 3.
|
|
18
29
|
*
|
|
19
30
|
* The read-modify-write on the shared bucket policy is member-scoped:
|
|
20
31
|
* exactly one member is added/removed, every foreign role and member is
|
|
@@ -36,7 +47,38 @@ import type * as GCP from "../Providers.ts";
|
|
|
36
47
|
* "principal://iam.googleapis.com/projects/864268824536/locations/global/workloadIdentityPools/micro-research-cluster-a.svc.id.goog/subject/ns/collab-srl/sa/default",
|
|
37
48
|
* });
|
|
38
49
|
* ```
|
|
50
|
+
*
|
|
51
|
+
* @section Prefix-scoped read on a customer bucket
|
|
52
|
+
* @example Conditional grant limited to one object prefix (+ bucket-level list)
|
|
53
|
+
* ```typescript
|
|
54
|
+
* yield* GCP.StorageBucketIamMember("QaDatasetRead", {
|
|
55
|
+
* bucket: "golden-zone-prod-770927",
|
|
56
|
+
* role: "roles/storage.objectViewer",
|
|
57
|
+
* member: "principal://iam.googleapis.com/…/subject/ns/collab-jeffrey-coworking/sa/default",
|
|
58
|
+
* condition: {
|
|
59
|
+
* title: "collab-jeffrey-coworking",
|
|
60
|
+
* expression:
|
|
61
|
+
* 'resource.name.startsWith("projects/_/buckets/golden-zone-prod-770927/objects/qa/") || resource.name == "projects/_/buckets/golden-zone-prod-770927"',
|
|
62
|
+
* },
|
|
63
|
+
* });
|
|
64
|
+
* ```
|
|
39
65
|
*/
|
|
66
|
+
export type StorageBucketIamMemberCondition = {
|
|
67
|
+
/**
|
|
68
|
+
* Binding title — create-time metadata only. NOT identity: a live
|
|
69
|
+
* binding with the same expression but another title is adopted, not
|
|
70
|
+
* retitled/replaced.
|
|
71
|
+
*/
|
|
72
|
+
title: string;
|
|
73
|
+
/** Optional human note on the binding. Create-time metadata only. */
|
|
74
|
+
description?: string;
|
|
75
|
+
/**
|
|
76
|
+
* CEL condition expression. Part of the resource identity — compared
|
|
77
|
+
* byte-for-byte against live bindings' expressions, so keep the
|
|
78
|
+
* rendering deterministic. Immutable — replace.
|
|
79
|
+
*/
|
|
80
|
+
expression: string;
|
|
81
|
+
};
|
|
40
82
|
export type StorageBucketIamMemberProps = {
|
|
41
83
|
/**
|
|
42
84
|
* Name of the (pre-existing) GCS bucket to grant on. The bucket is
|
|
@@ -51,6 +93,14 @@ export type StorageBucketIamMemberProps = {
|
|
|
51
93
|
* `group:…`, …). Immutable — replace.
|
|
52
94
|
*/
|
|
53
95
|
member: string;
|
|
96
|
+
/**
|
|
97
|
+
* Optional IAM Condition scoping the grant (e.g. to an object
|
|
98
|
+
* prefix). Absent → the member joins the role's UNCONDITIONAL
|
|
99
|
+
* binding. The condition **expression** is identity (immutable —
|
|
100
|
+
* replace); title/description are create-time metadata. Requires
|
|
101
|
+
* uniform bucket-level access on the bucket.
|
|
102
|
+
*/
|
|
103
|
+
condition?: StorageBucketIamMemberCondition;
|
|
54
104
|
};
|
|
55
105
|
export type StorageBucketIamMemberAttributes = {
|
|
56
106
|
/** Bucket name, threaded through from props. */
|
|
@@ -59,6 +109,8 @@ export type StorageBucketIamMemberAttributes = {
|
|
|
59
109
|
role: string;
|
|
60
110
|
/** IAM member, threaded through from props. */
|
|
61
111
|
member: string;
|
|
112
|
+
/** IAM Condition, threaded through from props (absent = unconditional). */
|
|
113
|
+
condition?: StorageBucketIamMemberCondition;
|
|
62
114
|
};
|
|
63
115
|
export type StorageBucketIamMember = Resource<"GCP.StorageBucketIamMember", StorageBucketIamMemberProps, StorageBucketIamMemberAttributes, never, GCP.Providers>;
|
|
64
116
|
export declare const StorageBucketIamMember: import("alchemy").ResourceClass<StorageBucketIamMember>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StorageBucketIamMember.d.ts","sourceRoot":"","sources":["../../src/Storage/StorageBucketIamMember.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAI7C,OAAO,KAAK,KAAK,GAAG,MAAM,iBAAiB,CAAC;AAE5C
|
|
1
|
+
{"version":3,"file":"StorageBucketIamMember.d.ts","sourceRoot":"","sources":["../../src/Storage/StorageBucketIamMember.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAI7C,OAAO,KAAK,KAAK,GAAG,MAAM,iBAAiB,CAAC;AAE5C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AACH,MAAM,MAAM,+BAA+B,GAAG;IAC5C;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,+BAA+B,CAAC;CAC7C,CAAC;AAEF,MAAM,MAAM,gCAAgC,GAAG;IAC7C,gDAAgD;IAChD,MAAM,EAAE,MAAM,CAAC;IACf,6CAA6C;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,+CAA+C;IAC/C,MAAM,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,SAAS,CAAC,EAAE,+BAA+B,CAAC;CAC7C,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAC3C,4BAA4B,EAC5B,2BAA2B,EAC3B,gCAAgC,EAChC,KAAK,EACL,GAAG,CAAC,SAAS,CACd,CAAC;AAEF,eAAO,MAAM,sBAAsB,yDAElC,CAAC;AAEF,eAAO,MAAM,8BAA8B,yLAyOxC,CAAC"}
|
|
@@ -19,12 +19,20 @@ export const StorageBucketIamMemberProvider = () => Provider.effect(StorageBucke
|
|
|
19
19
|
while: (e) => e?._tag !== "ConfigError",
|
|
20
20
|
schedule: Schedule.exponential(Duration.seconds(2)).pipe(Schedule.both(Schedule.recurs(8))),
|
|
21
21
|
}));
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
|
|
22
|
+
// Binding selector: with no condition, ONLY the unconditional
|
|
23
|
+
// binding for `role` (conditional bindings are someone else's);
|
|
24
|
+
// with a condition, the binding whose expression matches
|
|
25
|
+
// byte-for-byte — title/description deliberately ignored so
|
|
26
|
+
// hand-applied grants with the same expression are adopted.
|
|
27
|
+
const bindingMatches = (b, role, condition) => b.role === role &&
|
|
28
|
+
(condition
|
|
29
|
+
? b.condition?.expression === condition.expression
|
|
30
|
+
: !b.condition);
|
|
31
|
+
const hasMember = (policy, role, member, condition) => (policy.bindings ?? []).some((b) => bindingMatches(b, role, condition) &&
|
|
32
|
+
(b.members ?? []).includes(member));
|
|
25
33
|
return {
|
|
26
|
-
// Identity IS the
|
|
27
|
-
stables: ["bucket", "role", "member"],
|
|
34
|
+
// Identity IS the tuple; all fields are static values.
|
|
35
|
+
stables: ["bucket", "role", "member", "condition"],
|
|
28
36
|
diff: Effect.fn(function* ({ news, olds = {}, output }) {
|
|
29
37
|
if (!isResolved(news))
|
|
30
38
|
return undefined;
|
|
@@ -44,6 +52,19 @@ export const StorageBucketIamMemberProvider = () => Provider.effect(StorageBucke
|
|
|
44
52
|
(priorMember && priorMember !== news.member)) {
|
|
45
53
|
return { action: "replace" };
|
|
46
54
|
}
|
|
55
|
+
// Condition expression is identity too — but only when a prior
|
|
56
|
+
// identity exists at all (a partially-persisted record with no
|
|
57
|
+
// bucket names no grant to revoke). Absent condition compares
|
|
58
|
+
// as "" so unconditional↔conditional flips replace, while a
|
|
59
|
+
// 0.10.x record (no condition attribute) vs. unconditional
|
|
60
|
+
// props stays a no-op. `||` like the triple above: an empty
|
|
61
|
+
// persisted expression is NOT a prior identity and must not
|
|
62
|
+
// block the olds fallback.
|
|
63
|
+
const priorCond = output?.condition?.expression || olds.condition?.expression || "";
|
|
64
|
+
const newsCond = news.condition?.expression ?? "";
|
|
65
|
+
if (priorBucket && priorCond !== newsCond) {
|
|
66
|
+
return { action: "replace" };
|
|
67
|
+
}
|
|
47
68
|
return undefined;
|
|
48
69
|
}),
|
|
49
70
|
reconcile: Effect.fn(function* ({ news }) {
|
|
@@ -54,31 +75,64 @@ export const StorageBucketIamMemberProvider = () => Provider.effect(StorageBucke
|
|
|
54
75
|
}).pipe(Effect.catchTag("NotFound", () => Effect.fail(new ConfigError({
|
|
55
76
|
message: `Bucket ${news.bucket} does not exist — StorageBucketIamMember grants on pre-existing buckets only.`,
|
|
56
77
|
}))));
|
|
57
|
-
if (hasMember(current, news.role, news.member))
|
|
78
|
+
if (hasMember(current, news.role, news.member, news.condition)) {
|
|
58
79
|
return;
|
|
80
|
+
}
|
|
59
81
|
const bindings = (current.bindings ?? []).map((b) => ({
|
|
60
82
|
...b,
|
|
61
83
|
members: [...(b.members ?? [])],
|
|
62
84
|
}));
|
|
63
|
-
const existing = bindings.find((b) => b
|
|
85
|
+
const existing = bindings.find((b) => bindingMatches(b, news.role, news.condition));
|
|
64
86
|
if (existing) {
|
|
65
87
|
existing.members = [...(existing.members ?? []), news.member];
|
|
66
88
|
}
|
|
67
89
|
else {
|
|
68
|
-
bindings.push({
|
|
90
|
+
bindings.push({
|
|
91
|
+
role: news.role,
|
|
92
|
+
members: [news.member],
|
|
93
|
+
...(news.condition
|
|
94
|
+
? {
|
|
95
|
+
condition: {
|
|
96
|
+
title: news.condition.title,
|
|
97
|
+
...(news.condition.description
|
|
98
|
+
? { description: news.condition.description }
|
|
99
|
+
: {}),
|
|
100
|
+
expression: news.condition.expression,
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
: {}),
|
|
104
|
+
});
|
|
69
105
|
}
|
|
70
106
|
yield* setIamPolicyBuckets({
|
|
71
107
|
bucket: news.bucket,
|
|
72
108
|
body: { ...current, bindings, version: 3 },
|
|
73
109
|
});
|
|
74
110
|
}));
|
|
75
|
-
return {
|
|
111
|
+
return {
|
|
112
|
+
bucket: news.bucket,
|
|
113
|
+
role: news.role,
|
|
114
|
+
member: news.member,
|
|
115
|
+
...(news.condition ? { condition: news.condition } : {}),
|
|
116
|
+
};
|
|
76
117
|
}),
|
|
77
|
-
delete: Effect.fn(function* ({ output }) {
|
|
118
|
+
delete: Effect.fn(function* ({ output, olds }) {
|
|
78
119
|
// Partially-persisted attributes (empty fields) name no real
|
|
79
120
|
// grant — nothing to revoke, and getIamPolicy("") would 4xx.
|
|
80
121
|
if (!output.bucket || !output.role || !output.member)
|
|
81
122
|
return;
|
|
123
|
+
// Prior condition with an olds fallback: if the persisted
|
|
124
|
+
// attributes lost the condition while the live binding is
|
|
125
|
+
// conditional, matching without it would target the
|
|
126
|
+
// UNCONDITIONAL binding — either revoking a member some other
|
|
127
|
+
// resource owns there, or no-op'ing and leaving the scoped
|
|
128
|
+
// grant behind. Selected on expression TRUTHINESS (same rule
|
|
129
|
+
// as diff's `||` chain): a stub condition with an empty
|
|
130
|
+
// expression is not a prior identity and must not block olds.
|
|
131
|
+
const condition = output.condition?.expression
|
|
132
|
+
? output.condition
|
|
133
|
+
: olds?.condition?.expression
|
|
134
|
+
? olds.condition
|
|
135
|
+
: undefined;
|
|
82
136
|
yield* etagRetry(Effect.gen(function* () {
|
|
83
137
|
const current = yield* getIamPolicyBuckets({
|
|
84
138
|
bucket: output.bucket,
|
|
@@ -87,14 +141,15 @@ export const StorageBucketIamMemberProvider = () => Provider.effect(StorageBucke
|
|
|
87
141
|
// Bucket gone (or unreadable because gone) — the grant
|
|
88
142
|
// went with it; idempotent success.
|
|
89
143
|
Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
|
|
90
|
-
if (!current ||
|
|
144
|
+
if (!current ||
|
|
145
|
+
!hasMember(current, output.role, output.member, condition)) {
|
|
91
146
|
return;
|
|
92
147
|
}
|
|
93
|
-
// Remove exactly our member from the
|
|
148
|
+
// Remove exactly our member from the matched binding;
|
|
94
149
|
// drop the binding entirely if that empties it (GCS rejects
|
|
95
150
|
// bindings with zero members).
|
|
96
151
|
const bindings = (current.bindings ?? [])
|
|
97
|
-
.map((b) => b
|
|
152
|
+
.map((b) => bindingMatches(b, output.role, condition)
|
|
98
153
|
? {
|
|
99
154
|
...b,
|
|
100
155
|
members: (b.members ?? []).filter((m) => m !== output.member),
|
|
@@ -111,6 +166,7 @@ export const StorageBucketIamMemberProvider = () => Provider.effect(StorageBucke
|
|
|
111
166
|
const bucket = output?.bucket ?? olds?.bucket;
|
|
112
167
|
const role = output?.role ?? olds?.role;
|
|
113
168
|
const member = output?.member ?? olds?.member;
|
|
169
|
+
const condition = output?.condition ?? olds?.condition;
|
|
114
170
|
if (!bucket || !role || !member)
|
|
115
171
|
return undefined;
|
|
116
172
|
const current = yield* getIamPolicyBuckets({
|
|
@@ -121,9 +177,14 @@ export const StorageBucketIamMemberProvider = () => Provider.effect(StorageBucke
|
|
|
121
177
|
return undefined;
|
|
122
178
|
// Absent member → undefined: the engine treats that as drift
|
|
123
179
|
// and the next plan re-grants.
|
|
124
|
-
if (!hasMember(current, role, member))
|
|
180
|
+
if (!hasMember(current, role, member, condition))
|
|
125
181
|
return undefined;
|
|
126
|
-
return {
|
|
182
|
+
return {
|
|
183
|
+
bucket,
|
|
184
|
+
role,
|
|
185
|
+
member,
|
|
186
|
+
...(condition ? { condition } : {}),
|
|
187
|
+
};
|
|
127
188
|
}),
|
|
128
189
|
};
|
|
129
190
|
}));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StorageBucketIamMember.js","sourceRoot":"","sources":["../../src/Storage/StorageBucketIamMember.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,KAAK,OAAO,MAAM,iCAAiC,CAAC;AAC3D,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAC;AAC5C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"StorageBucketIamMember.js","sourceRoot":"","sources":["../../src/Storage/StorageBucketIamMember.ts"],"names":[],"mappings":"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"}
|
package/lib/Storage/index.d.ts
CHANGED
|
@@ -2,5 +2,5 @@ export { StorageBucket, StorageBucketProvider, } from "./Bucket.ts";
|
|
|
2
2
|
export type { StorageBucketProps, StorageBucketIamBinding, StorageBucketBindingContract, } from "./Bucket.ts";
|
|
3
3
|
export * from "./BucketIamMember.ts";
|
|
4
4
|
export { StorageBucketIamMember, StorageBucketIamMemberProvider, } from "./StorageBucketIamMember.ts";
|
|
5
|
-
export type { StorageBucketIamMemberProps, StorageBucketIamMemberAttributes, } from "./StorageBucketIamMember.ts";
|
|
5
|
+
export type { StorageBucketIamMemberProps, StorageBucketIamMemberAttributes, StorageBucketIamMemberCondition, } from "./StorageBucketIamMember.ts";
|
|
6
6
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/Storage/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,qBAAqB,GACtB,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,kBAAkB,EAClB,uBAAuB,EACvB,4BAA4B,GAC7B,MAAM,aAAa,CAAC;AACrB,cAAc,sBAAsB,CAAC;AACrC,OAAO,EACL,sBAAsB,EACtB,8BAA8B,GAC/B,MAAM,6BAA6B,CAAC;AACrC,YAAY,EACV,2BAA2B,EAC3B,gCAAgC,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/Storage/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,qBAAqB,GACtB,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,kBAAkB,EAClB,uBAAuB,EACvB,4BAA4B,GAC7B,MAAM,aAAa,CAAC;AACrB,cAAc,sBAAsB,CAAC;AACrC,OAAO,EACL,sBAAsB,EACtB,8BAA8B,GAC/B,MAAM,6BAA6B,CAAC;AACrC,YAAY,EACV,2BAA2B,EAC3B,gCAAgC,EAChC,+BAA+B,GAChC,MAAM,6BAA6B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@microagi/alchemy-gcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "GCP provider for Alchemy v2 (Effect-based IaC). Resources for Project, ServiceAccount, Cluster, NodePool, Compute, ManagedLustre, ServiceUsage, ServiceNetworking, Cloud Run (Service + Job + IAM), Cloud SQL (Instance + Database + User with IAM database authn), Artifact Registry (Docker repositories, standard + remote pull-through caches), Kubernetes (typed Secret + generic SSA Manifest + Helm release), with ADC-backed credentials and target-side IAM bindings.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"alchemy",
|
|
@@ -19,9 +19,20 @@ import type * as GCP from "../Providers.ts";
|
|
|
19
19
|
* cannot do — **removes it again on delete**, so grants revoke when the
|
|
20
20
|
* declaring resource leaves the stack.
|
|
21
21
|
*
|
|
22
|
-
* Identity is the `(bucket, role, member)`
|
|
23
|
-
* immutable — changing any replaces (the old
|
|
24
|
-
* one granted).
|
|
22
|
+
* Identity is the `(bucket, role, member, condition.expression)` tuple;
|
|
23
|
+
* all are immutable — changing any replaces (the old grant is revoked,
|
|
24
|
+
* the new one granted).
|
|
25
|
+
*
|
|
26
|
+
* With a `condition`, the member lands in the binding whose condition
|
|
27
|
+
* **expression** matches (CEL, compared byte-for-byte) — the binding is
|
|
28
|
+
* created with the given title/description if absent. Title and
|
|
29
|
+
* description are create-time metadata only: an existing binding with
|
|
30
|
+
* the same expression but a different title is adopted as-is, never
|
|
31
|
+
* retitled (that lets hand-applied conditional grants converge into the
|
|
32
|
+
* stack without a duplicate binding). Conditional bindings require the
|
|
33
|
+
* bucket to have uniform bucket-level access (GCS rejects conditions
|
|
34
|
+
* otherwise) and policy version 3 — the read-modify-write here always
|
|
35
|
+
* requests/writes version 3.
|
|
25
36
|
*
|
|
26
37
|
* The read-modify-write on the shared bucket policy is member-scoped:
|
|
27
38
|
* exactly one member is added/removed, every foreign role and member is
|
|
@@ -43,7 +54,39 @@ import type * as GCP from "../Providers.ts";
|
|
|
43
54
|
* "principal://iam.googleapis.com/projects/864268824536/locations/global/workloadIdentityPools/micro-research-cluster-a.svc.id.goog/subject/ns/collab-srl/sa/default",
|
|
44
55
|
* });
|
|
45
56
|
* ```
|
|
57
|
+
*
|
|
58
|
+
* @section Prefix-scoped read on a customer bucket
|
|
59
|
+
* @example Conditional grant limited to one object prefix (+ bucket-level list)
|
|
60
|
+
* ```typescript
|
|
61
|
+
* yield* GCP.StorageBucketIamMember("QaDatasetRead", {
|
|
62
|
+
* bucket: "golden-zone-prod-770927",
|
|
63
|
+
* role: "roles/storage.objectViewer",
|
|
64
|
+
* member: "principal://iam.googleapis.com/…/subject/ns/collab-jeffrey-coworking/sa/default",
|
|
65
|
+
* condition: {
|
|
66
|
+
* title: "collab-jeffrey-coworking",
|
|
67
|
+
* expression:
|
|
68
|
+
* 'resource.name.startsWith("projects/_/buckets/golden-zone-prod-770927/objects/qa/") || resource.name == "projects/_/buckets/golden-zone-prod-770927"',
|
|
69
|
+
* },
|
|
70
|
+
* });
|
|
71
|
+
* ```
|
|
46
72
|
*/
|
|
73
|
+
export type StorageBucketIamMemberCondition = {
|
|
74
|
+
/**
|
|
75
|
+
* Binding title — create-time metadata only. NOT identity: a live
|
|
76
|
+
* binding with the same expression but another title is adopted, not
|
|
77
|
+
* retitled/replaced.
|
|
78
|
+
*/
|
|
79
|
+
title: string;
|
|
80
|
+
/** Optional human note on the binding. Create-time metadata only. */
|
|
81
|
+
description?: string;
|
|
82
|
+
/**
|
|
83
|
+
* CEL condition expression. Part of the resource identity — compared
|
|
84
|
+
* byte-for-byte against live bindings' expressions, so keep the
|
|
85
|
+
* rendering deterministic. Immutable — replace.
|
|
86
|
+
*/
|
|
87
|
+
expression: string;
|
|
88
|
+
};
|
|
89
|
+
|
|
47
90
|
export type StorageBucketIamMemberProps = {
|
|
48
91
|
/**
|
|
49
92
|
* Name of the (pre-existing) GCS bucket to grant on. The bucket is
|
|
@@ -58,6 +101,14 @@ export type StorageBucketIamMemberProps = {
|
|
|
58
101
|
* `group:…`, …). Immutable — replace.
|
|
59
102
|
*/
|
|
60
103
|
member: string;
|
|
104
|
+
/**
|
|
105
|
+
* Optional IAM Condition scoping the grant (e.g. to an object
|
|
106
|
+
* prefix). Absent → the member joins the role's UNCONDITIONAL
|
|
107
|
+
* binding. The condition **expression** is identity (immutable —
|
|
108
|
+
* replace); title/description are create-time metadata. Requires
|
|
109
|
+
* uniform bucket-level access on the bucket.
|
|
110
|
+
*/
|
|
111
|
+
condition?: StorageBucketIamMemberCondition;
|
|
61
112
|
};
|
|
62
113
|
|
|
63
114
|
export type StorageBucketIamMemberAttributes = {
|
|
@@ -67,6 +118,8 @@ export type StorageBucketIamMemberAttributes = {
|
|
|
67
118
|
role: string;
|
|
68
119
|
/** IAM member, threaded through from props. */
|
|
69
120
|
member: string;
|
|
121
|
+
/** IAM Condition, threaded through from props (absent = unconditional). */
|
|
122
|
+
condition?: StorageBucketIamMemberCondition;
|
|
70
123
|
};
|
|
71
124
|
|
|
72
125
|
export type StorageBucketIamMember = Resource<
|
|
@@ -103,20 +156,36 @@ export const StorageBucketIamMemberProvider = () =>
|
|
|
103
156
|
}),
|
|
104
157
|
);
|
|
105
158
|
|
|
106
|
-
//
|
|
107
|
-
//
|
|
159
|
+
// Binding selector: with no condition, ONLY the unconditional
|
|
160
|
+
// binding for `role` (conditional bindings are someone else's);
|
|
161
|
+
// with a condition, the binding whose expression matches
|
|
162
|
+
// byte-for-byte — title/description deliberately ignored so
|
|
163
|
+
// hand-applied grants with the same expression are adopted.
|
|
164
|
+
const bindingMatches = (
|
|
165
|
+
b: { role?: string; condition?: { expression?: string } },
|
|
166
|
+
role: string,
|
|
167
|
+
condition?: StorageBucketIamMemberCondition,
|
|
168
|
+
) =>
|
|
169
|
+
b.role === role &&
|
|
170
|
+
(condition
|
|
171
|
+
? b.condition?.expression === condition.expression
|
|
172
|
+
: !b.condition);
|
|
173
|
+
|
|
108
174
|
const hasMember = (
|
|
109
175
|
policy: storage.Policy,
|
|
110
176
|
role: string,
|
|
111
177
|
member: string,
|
|
178
|
+
condition?: StorageBucketIamMemberCondition,
|
|
112
179
|
) =>
|
|
113
180
|
(policy.bindings ?? []).some(
|
|
114
|
-
(b) =>
|
|
181
|
+
(b) =>
|
|
182
|
+
bindingMatches(b, role, condition) &&
|
|
183
|
+
(b.members ?? []).includes(member),
|
|
115
184
|
);
|
|
116
185
|
|
|
117
186
|
return {
|
|
118
|
-
// Identity IS the
|
|
119
|
-
stables: ["bucket", "role", "member"],
|
|
187
|
+
// Identity IS the tuple; all fields are static values.
|
|
188
|
+
stables: ["bucket", "role", "member", "condition"],
|
|
120
189
|
diff: Effect.fn(function* ({ news, olds = {}, output }) {
|
|
121
190
|
if (!isResolved(news)) return undefined;
|
|
122
191
|
// Prior identity from persisted attributes first, then olds —
|
|
@@ -137,6 +206,20 @@ export const StorageBucketIamMemberProvider = () =>
|
|
|
137
206
|
) {
|
|
138
207
|
return { action: "replace" } as const;
|
|
139
208
|
}
|
|
209
|
+
// Condition expression is identity too — but only when a prior
|
|
210
|
+
// identity exists at all (a partially-persisted record with no
|
|
211
|
+
// bucket names no grant to revoke). Absent condition compares
|
|
212
|
+
// as "" so unconditional↔conditional flips replace, while a
|
|
213
|
+
// 0.10.x record (no condition attribute) vs. unconditional
|
|
214
|
+
// props stays a no-op. `||` like the triple above: an empty
|
|
215
|
+
// persisted expression is NOT a prior identity and must not
|
|
216
|
+
// block the olds fallback.
|
|
217
|
+
const priorCond =
|
|
218
|
+
output?.condition?.expression || olds.condition?.expression || "";
|
|
219
|
+
const newsCond = news.condition?.expression ?? "";
|
|
220
|
+
if (priorBucket && priorCond !== newsCond) {
|
|
221
|
+
return { action: "replace" } as const;
|
|
222
|
+
}
|
|
140
223
|
return undefined;
|
|
141
224
|
}),
|
|
142
225
|
reconcile: Effect.fn(function* ({ news }) {
|
|
@@ -154,19 +237,35 @@ export const StorageBucketIamMemberProvider = () =>
|
|
|
154
237
|
),
|
|
155
238
|
),
|
|
156
239
|
);
|
|
157
|
-
if (hasMember(current, news.role, news.member))
|
|
240
|
+
if (hasMember(current, news.role, news.member, news.condition)) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
158
243
|
|
|
159
244
|
const bindings = (current.bindings ?? []).map((b) => ({
|
|
160
245
|
...b,
|
|
161
246
|
members: [...(b.members ?? [])],
|
|
162
247
|
}));
|
|
163
|
-
const existing = bindings.find(
|
|
164
|
-
(b
|
|
248
|
+
const existing = bindings.find((b) =>
|
|
249
|
+
bindingMatches(b, news.role, news.condition),
|
|
165
250
|
);
|
|
166
251
|
if (existing) {
|
|
167
252
|
existing.members = [...(existing.members ?? []), news.member];
|
|
168
253
|
} else {
|
|
169
|
-
bindings.push({
|
|
254
|
+
bindings.push({
|
|
255
|
+
role: news.role,
|
|
256
|
+
members: [news.member],
|
|
257
|
+
...(news.condition
|
|
258
|
+
? {
|
|
259
|
+
condition: {
|
|
260
|
+
title: news.condition.title,
|
|
261
|
+
...(news.condition.description
|
|
262
|
+
? { description: news.condition.description }
|
|
263
|
+
: {}),
|
|
264
|
+
expression: news.condition.expression,
|
|
265
|
+
},
|
|
266
|
+
}
|
|
267
|
+
: {}),
|
|
268
|
+
});
|
|
170
269
|
}
|
|
171
270
|
yield* setIamPolicyBuckets({
|
|
172
271
|
bucket: news.bucket,
|
|
@@ -174,12 +273,30 @@ export const StorageBucketIamMemberProvider = () =>
|
|
|
174
273
|
});
|
|
175
274
|
}),
|
|
176
275
|
);
|
|
177
|
-
return {
|
|
276
|
+
return {
|
|
277
|
+
bucket: news.bucket,
|
|
278
|
+
role: news.role,
|
|
279
|
+
member: news.member,
|
|
280
|
+
...(news.condition ? { condition: news.condition } : {}),
|
|
281
|
+
};
|
|
178
282
|
}),
|
|
179
|
-
delete: Effect.fn(function* ({ output }) {
|
|
283
|
+
delete: Effect.fn(function* ({ output, olds }) {
|
|
180
284
|
// Partially-persisted attributes (empty fields) name no real
|
|
181
285
|
// grant — nothing to revoke, and getIamPolicy("") would 4xx.
|
|
182
286
|
if (!output.bucket || !output.role || !output.member) return;
|
|
287
|
+
// Prior condition with an olds fallback: if the persisted
|
|
288
|
+
// attributes lost the condition while the live binding is
|
|
289
|
+
// conditional, matching without it would target the
|
|
290
|
+
// UNCONDITIONAL binding — either revoking a member some other
|
|
291
|
+
// resource owns there, or no-op'ing and leaving the scoped
|
|
292
|
+
// grant behind. Selected on expression TRUTHINESS (same rule
|
|
293
|
+
// as diff's `||` chain): a stub condition with an empty
|
|
294
|
+
// expression is not a prior identity and must not block olds.
|
|
295
|
+
const condition = output.condition?.expression
|
|
296
|
+
? output.condition
|
|
297
|
+
: olds?.condition?.expression
|
|
298
|
+
? olds.condition
|
|
299
|
+
: undefined;
|
|
183
300
|
yield* etagRetry(
|
|
184
301
|
Effect.gen(function* () {
|
|
185
302
|
const current = yield* getIamPolicyBuckets({
|
|
@@ -192,15 +309,18 @@ export const StorageBucketIamMemberProvider = () =>
|
|
|
192
309
|
Effect.succeed(undefined as storage.Policy | undefined),
|
|
193
310
|
),
|
|
194
311
|
);
|
|
195
|
-
if (
|
|
312
|
+
if (
|
|
313
|
+
!current ||
|
|
314
|
+
!hasMember(current, output.role, output.member, condition)
|
|
315
|
+
) {
|
|
196
316
|
return;
|
|
197
317
|
}
|
|
198
|
-
// Remove exactly our member from the
|
|
318
|
+
// Remove exactly our member from the matched binding;
|
|
199
319
|
// drop the binding entirely if that empties it (GCS rejects
|
|
200
320
|
// bindings with zero members).
|
|
201
321
|
const bindings = (current.bindings ?? [])
|
|
202
322
|
.map((b) =>
|
|
203
|
-
b
|
|
323
|
+
bindingMatches(b, output.role, condition)
|
|
204
324
|
? {
|
|
205
325
|
...b,
|
|
206
326
|
members: (b.members ?? []).filter(
|
|
@@ -221,6 +341,7 @@ export const StorageBucketIamMemberProvider = () =>
|
|
|
221
341
|
const bucket = output?.bucket ?? olds?.bucket;
|
|
222
342
|
const role = output?.role ?? olds?.role;
|
|
223
343
|
const member = output?.member ?? olds?.member;
|
|
344
|
+
const condition = output?.condition ?? olds?.condition;
|
|
224
345
|
if (!bucket || !role || !member) return undefined;
|
|
225
346
|
const current = yield* getIamPolicyBuckets({
|
|
226
347
|
bucket,
|
|
@@ -236,8 +357,13 @@ export const StorageBucketIamMemberProvider = () =>
|
|
|
236
357
|
if (!current) return undefined;
|
|
237
358
|
// Absent member → undefined: the engine treats that as drift
|
|
238
359
|
// and the next plan re-grants.
|
|
239
|
-
if (!hasMember(current, role, member)) return undefined;
|
|
240
|
-
return {
|
|
360
|
+
if (!hasMember(current, role, member, condition)) return undefined;
|
|
361
|
+
return {
|
|
362
|
+
bucket,
|
|
363
|
+
role,
|
|
364
|
+
member,
|
|
365
|
+
...(condition ? { condition } : {}),
|
|
366
|
+
};
|
|
241
367
|
}),
|
|
242
368
|
};
|
|
243
369
|
}),
|