@microagi/alchemy-gcp 0.10.0 → 0.11.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/lib/Storage/StorageBucketIamMember.d.ts +55 -3
- package/lib/Storage/StorageBucketIamMember.d.ts.map +1 -1
- package/lib/Storage/StorageBucketIamMember.js +60 -14
- 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 +129 -18
- 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,yLA0NxC,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,17 @@ 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.
|
|
61
|
+
const priorCond = output?.condition?.expression ?? olds.condition?.expression ?? "";
|
|
62
|
+
const newsCond = news.condition?.expression ?? "";
|
|
63
|
+
if (priorBucket && priorCond !== newsCond) {
|
|
64
|
+
return { action: "replace" };
|
|
65
|
+
}
|
|
47
66
|
return undefined;
|
|
48
67
|
}),
|
|
49
68
|
reconcile: Effect.fn(function* ({ news }) {
|
|
@@ -54,25 +73,45 @@ export const StorageBucketIamMemberProvider = () => Provider.effect(StorageBucke
|
|
|
54
73
|
}).pipe(Effect.catchTag("NotFound", () => Effect.fail(new ConfigError({
|
|
55
74
|
message: `Bucket ${news.bucket} does not exist — StorageBucketIamMember grants on pre-existing buckets only.`,
|
|
56
75
|
}))));
|
|
57
|
-
if (hasMember(current, news.role, news.member))
|
|
76
|
+
if (hasMember(current, news.role, news.member, news.condition)) {
|
|
58
77
|
return;
|
|
78
|
+
}
|
|
59
79
|
const bindings = (current.bindings ?? []).map((b) => ({
|
|
60
80
|
...b,
|
|
61
81
|
members: [...(b.members ?? [])],
|
|
62
82
|
}));
|
|
63
|
-
const existing = bindings.find((b) => b
|
|
83
|
+
const existing = bindings.find((b) => bindingMatches(b, news.role, news.condition));
|
|
64
84
|
if (existing) {
|
|
65
85
|
existing.members = [...(existing.members ?? []), news.member];
|
|
66
86
|
}
|
|
67
87
|
else {
|
|
68
|
-
bindings.push({
|
|
88
|
+
bindings.push({
|
|
89
|
+
role: news.role,
|
|
90
|
+
members: [news.member],
|
|
91
|
+
...(news.condition
|
|
92
|
+
? {
|
|
93
|
+
condition: {
|
|
94
|
+
title: news.condition.title,
|
|
95
|
+
...(news.condition.description
|
|
96
|
+
? { description: news.condition.description }
|
|
97
|
+
: {}),
|
|
98
|
+
expression: news.condition.expression,
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
: {}),
|
|
102
|
+
});
|
|
69
103
|
}
|
|
70
104
|
yield* setIamPolicyBuckets({
|
|
71
105
|
bucket: news.bucket,
|
|
72
106
|
body: { ...current, bindings, version: 3 },
|
|
73
107
|
});
|
|
74
108
|
}));
|
|
75
|
-
return {
|
|
109
|
+
return {
|
|
110
|
+
bucket: news.bucket,
|
|
111
|
+
role: news.role,
|
|
112
|
+
member: news.member,
|
|
113
|
+
...(news.condition ? { condition: news.condition } : {}),
|
|
114
|
+
};
|
|
76
115
|
}),
|
|
77
116
|
delete: Effect.fn(function* ({ output }) {
|
|
78
117
|
// Partially-persisted attributes (empty fields) name no real
|
|
@@ -87,14 +126,15 @@ export const StorageBucketIamMemberProvider = () => Provider.effect(StorageBucke
|
|
|
87
126
|
// Bucket gone (or unreadable because gone) — the grant
|
|
88
127
|
// went with it; idempotent success.
|
|
89
128
|
Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
|
|
90
|
-
if (!current ||
|
|
129
|
+
if (!current ||
|
|
130
|
+
!hasMember(current, output.role, output.member, output.condition)) {
|
|
91
131
|
return;
|
|
92
132
|
}
|
|
93
|
-
// Remove exactly our member from the
|
|
133
|
+
// Remove exactly our member from the matched binding;
|
|
94
134
|
// drop the binding entirely if that empties it (GCS rejects
|
|
95
135
|
// bindings with zero members).
|
|
96
136
|
const bindings = (current.bindings ?? [])
|
|
97
|
-
.map((b) => b
|
|
137
|
+
.map((b) => bindingMatches(b, output.role, output.condition)
|
|
98
138
|
? {
|
|
99
139
|
...b,
|
|
100
140
|
members: (b.members ?? []).filter((m) => m !== output.member),
|
|
@@ -111,6 +151,7 @@ export const StorageBucketIamMemberProvider = () => Provider.effect(StorageBucke
|
|
|
111
151
|
const bucket = output?.bucket ?? olds?.bucket;
|
|
112
152
|
const role = output?.role ?? olds?.role;
|
|
113
153
|
const member = output?.member ?? olds?.member;
|
|
154
|
+
const condition = output?.condition ?? olds?.condition;
|
|
114
155
|
if (!bucket || !role || !member)
|
|
115
156
|
return undefined;
|
|
116
157
|
const current = yield* getIamPolicyBuckets({
|
|
@@ -121,9 +162,14 @@ export const StorageBucketIamMemberProvider = () => Provider.effect(StorageBucke
|
|
|
121
162
|
return undefined;
|
|
122
163
|
// Absent member → undefined: the engine treats that as drift
|
|
123
164
|
// and the next plan re-grants.
|
|
124
|
-
if (!hasMember(current, role, member))
|
|
165
|
+
if (!hasMember(current, role, member, condition))
|
|
125
166
|
return undefined;
|
|
126
|
-
return {
|
|
167
|
+
return {
|
|
168
|
+
bucket,
|
|
169
|
+
role,
|
|
170
|
+
member,
|
|
171
|
+
...(condition ? { condition } : {}),
|
|
172
|
+
};
|
|
127
173
|
}),
|
|
128
174
|
};
|
|
129
175
|
}));
|
|
@@ -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.0",
|
|
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,18 @@ 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.
|
|
215
|
+
const priorCond =
|
|
216
|
+
output?.condition?.expression ?? olds.condition?.expression ?? "";
|
|
217
|
+
const newsCond = news.condition?.expression ?? "";
|
|
218
|
+
if (priorBucket && priorCond !== newsCond) {
|
|
219
|
+
return { action: "replace" } as const;
|
|
220
|
+
}
|
|
140
221
|
return undefined;
|
|
141
222
|
}),
|
|
142
223
|
reconcile: Effect.fn(function* ({ news }) {
|
|
@@ -154,19 +235,35 @@ export const StorageBucketIamMemberProvider = () =>
|
|
|
154
235
|
),
|
|
155
236
|
),
|
|
156
237
|
);
|
|
157
|
-
if (hasMember(current, news.role, news.member))
|
|
238
|
+
if (hasMember(current, news.role, news.member, news.condition)) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
158
241
|
|
|
159
242
|
const bindings = (current.bindings ?? []).map((b) => ({
|
|
160
243
|
...b,
|
|
161
244
|
members: [...(b.members ?? [])],
|
|
162
245
|
}));
|
|
163
|
-
const existing = bindings.find(
|
|
164
|
-
(b
|
|
246
|
+
const existing = bindings.find((b) =>
|
|
247
|
+
bindingMatches(b, news.role, news.condition),
|
|
165
248
|
);
|
|
166
249
|
if (existing) {
|
|
167
250
|
existing.members = [...(existing.members ?? []), news.member];
|
|
168
251
|
} else {
|
|
169
|
-
bindings.push({
|
|
252
|
+
bindings.push({
|
|
253
|
+
role: news.role,
|
|
254
|
+
members: [news.member],
|
|
255
|
+
...(news.condition
|
|
256
|
+
? {
|
|
257
|
+
condition: {
|
|
258
|
+
title: news.condition.title,
|
|
259
|
+
...(news.condition.description
|
|
260
|
+
? { description: news.condition.description }
|
|
261
|
+
: {}),
|
|
262
|
+
expression: news.condition.expression,
|
|
263
|
+
},
|
|
264
|
+
}
|
|
265
|
+
: {}),
|
|
266
|
+
});
|
|
170
267
|
}
|
|
171
268
|
yield* setIamPolicyBuckets({
|
|
172
269
|
bucket: news.bucket,
|
|
@@ -174,7 +271,12 @@ export const StorageBucketIamMemberProvider = () =>
|
|
|
174
271
|
});
|
|
175
272
|
}),
|
|
176
273
|
);
|
|
177
|
-
return {
|
|
274
|
+
return {
|
|
275
|
+
bucket: news.bucket,
|
|
276
|
+
role: news.role,
|
|
277
|
+
member: news.member,
|
|
278
|
+
...(news.condition ? { condition: news.condition } : {}),
|
|
279
|
+
};
|
|
178
280
|
}),
|
|
179
281
|
delete: Effect.fn(function* ({ output }) {
|
|
180
282
|
// Partially-persisted attributes (empty fields) name no real
|
|
@@ -192,15 +294,18 @@ export const StorageBucketIamMemberProvider = () =>
|
|
|
192
294
|
Effect.succeed(undefined as storage.Policy | undefined),
|
|
193
295
|
),
|
|
194
296
|
);
|
|
195
|
-
if (
|
|
297
|
+
if (
|
|
298
|
+
!current ||
|
|
299
|
+
!hasMember(current, output.role, output.member, output.condition)
|
|
300
|
+
) {
|
|
196
301
|
return;
|
|
197
302
|
}
|
|
198
|
-
// Remove exactly our member from the
|
|
303
|
+
// Remove exactly our member from the matched binding;
|
|
199
304
|
// drop the binding entirely if that empties it (GCS rejects
|
|
200
305
|
// bindings with zero members).
|
|
201
306
|
const bindings = (current.bindings ?? [])
|
|
202
307
|
.map((b) =>
|
|
203
|
-
b
|
|
308
|
+
bindingMatches(b, output.role, output.condition)
|
|
204
309
|
? {
|
|
205
310
|
...b,
|
|
206
311
|
members: (b.members ?? []).filter(
|
|
@@ -221,6 +326,7 @@ export const StorageBucketIamMemberProvider = () =>
|
|
|
221
326
|
const bucket = output?.bucket ?? olds?.bucket;
|
|
222
327
|
const role = output?.role ?? olds?.role;
|
|
223
328
|
const member = output?.member ?? olds?.member;
|
|
329
|
+
const condition = output?.condition ?? olds?.condition;
|
|
224
330
|
if (!bucket || !role || !member) return undefined;
|
|
225
331
|
const current = yield* getIamPolicyBuckets({
|
|
226
332
|
bucket,
|
|
@@ -236,8 +342,13 @@ export const StorageBucketIamMemberProvider = () =>
|
|
|
236
342
|
if (!current) return undefined;
|
|
237
343
|
// Absent member → undefined: the engine treats that as drift
|
|
238
344
|
// and the next plan re-grants.
|
|
239
|
-
if (!hasMember(current, role, member)) return undefined;
|
|
240
|
-
return {
|
|
345
|
+
if (!hasMember(current, role, member, condition)) return undefined;
|
|
346
|
+
return {
|
|
347
|
+
bucket,
|
|
348
|
+
role,
|
|
349
|
+
member,
|
|
350
|
+
...(condition ? { condition } : {}),
|
|
351
|
+
};
|
|
241
352
|
}),
|
|
242
353
|
};
|
|
243
354
|
}),
|