@microagi/alchemy-gcp 0.9.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/CHANGELOG.md +28 -0
- package/lib/Providers.d.ts.map +1 -1
- package/lib/Providers.js +3 -1
- package/lib/Providers.js.map +1 -1
- package/lib/Storage/StorageBucketIamMember.d.ts +118 -0
- package/lib/Storage/StorageBucketIamMember.d.ts.map +1 -0
- package/lib/Storage/StorageBucketIamMember.js +176 -0
- package/lib/Storage/StorageBucketIamMember.js.map +1 -0
- package/lib/Storage/index.d.ts +2 -0
- package/lib/Storage/index.d.ts.map +1 -1
- package/lib/Storage/index.js +1 -0
- package/lib/Storage/index.js.map +1 -1
- package/package.json +1 -1
- package/src/Providers.ts +6 -0
- package/src/Storage/StorageBucketIamMember.ts +355 -0
- package/src/Storage/index.ts +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,34 @@ All notable changes to `@microagi/alchemy-gcp`. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this package
|
|
5
5
|
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## 0.10.0 — 2026-07-02
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`GCP.StorageBucketIamMember`** — standalone resource managing a
|
|
12
|
+
single `(bucket, role, member)` IAM entry on an **existing** GCS
|
|
13
|
+
bucket by raw name (a bucket the stack does not create, adopt, or
|
|
14
|
+
delete). Unlike the `storageBucketIamMember` binding helper — which
|
|
15
|
+
requires a stack-owned `StorageBucket` target and is additive-only —
|
|
16
|
+
this resource **revokes the member on delete** (dropping the binding
|
|
17
|
+
if emptied), so grants disappear when the declaring resource leaves
|
|
18
|
+
the stack. Member-scoped read-modify-write preserves all foreign
|
|
19
|
+
roles/members verbatim; the get→set cycle retries with backoff for
|
|
20
|
+
etag races; a missing bucket fails fast as `ConfigError`. `diff`
|
|
21
|
+
replaces on any change to the triple (persisted `output` checked
|
|
22
|
+
first, `||`-fallback so empty persisted fields are never treated as
|
|
23
|
+
a prior identity); `read` reports member absence as drift so the
|
|
24
|
+
engine re-grants. First consumer: research-infra's collab-reconciler
|
|
25
|
+
per-group bucket read grants.
|
|
26
|
+
|
|
27
|
+
## 0.9.0 — 2026-06-30
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- **`GCP.HelmRelease`** — Alchemy resource for managing Helm chart
|
|
32
|
+
releases on GKE clusters (#8). _(Entry backfilled — 0.9.0 shipped
|
|
33
|
+
without a changelog entry.)_
|
|
34
|
+
|
|
7
35
|
## 0.8.0 — 2026-06-17
|
|
8
36
|
|
|
9
37
|
### Added
|
package/lib/Providers.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Providers.d.ts","sourceRoot":"","sources":["../src/Providers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;;
|
|
1
|
+
{"version":3,"file":"Providers.d.ts","sourceRoot":"","sources":["../src/Providers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;;AAmEtC,qBAAa,SAAU,SAAQ,cAA+C;CAAG;AAEjF,MAAM,MAAM,oBAAoB,GAAG,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC,CAAC;AAEhF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,SAAS,gaAuEnB,CAAC"}
|
package/lib/Providers.js
CHANGED
|
@@ -24,6 +24,7 @@ import { Job, JobProvider } from "./Run/Job.js";
|
|
|
24
24
|
import { Service, ServiceProvider } from "./Run/Service.js";
|
|
25
25
|
import { PsaConnection, PsaConnectionProvider, } from "./ServiceNetworking/PsaConnection.js";
|
|
26
26
|
import { StorageBucket, StorageBucketProvider, } from "./Storage/Bucket.js";
|
|
27
|
+
import { StorageBucketIamMember, StorageBucketIamMemberProvider, } from "./Storage/StorageBucketIamMember.js";
|
|
27
28
|
import { ApiEnable, ApiEnableProvider } from "./ServiceUsage/ApiEnable.js";
|
|
28
29
|
import { SqlDatabase, SqlDatabaseProvider } from "./Sqladmin/Database.js";
|
|
29
30
|
import { SqlInstance, SqlInstanceProvider } from "./Sqladmin/Instance.js";
|
|
@@ -61,6 +62,7 @@ export const providers = () => Layer.effect(Providers, Provider.collection([
|
|
|
61
62
|
PsaConnection,
|
|
62
63
|
ManagedLustreInstance,
|
|
63
64
|
StorageBucket,
|
|
65
|
+
StorageBucketIamMember,
|
|
64
66
|
Service,
|
|
65
67
|
Job,
|
|
66
68
|
SqlInstance,
|
|
@@ -72,7 +74,7 @@ export const providers = () => Layer.effect(Providers, Provider.collection([
|
|
|
72
74
|
KubernetesSecret,
|
|
73
75
|
KubernetesManifest,
|
|
74
76
|
HelmRelease,
|
|
75
|
-
])).pipe(Layer.provide(Layer.mergeAll(ProjectProvider(), ClusterProvider(), NodePoolProvider(), ApiEnableProvider(), NetworkProvider(), SubnetworkProvider(), FirewallProvider(), AddressProvider(), ForwardingRuleProvider(), GlobalAddressProvider(), SharedVpcHostProvider(), SharedVpcServiceProjectProvider(), PsaConnectionProvider(), ManagedLustreInstanceProvider(), StorageBucketProvider(), ServiceProvider(), JobProvider(), SqlInstanceProvider(), SqlDatabaseProvider(), SqlUserProvider(), ArtifactRegistryRepositoryProvider(), ServiceAccountProvider(), ServiceAccountKeyProvider(), KubernetesSecretProvider(), KubernetesManifestProvider(), HelmReleaseProvider())), Layer.provideMerge(fromAuthProvider()), Layer.provideMerge(GCPAuth),
|
|
77
|
+
])).pipe(Layer.provide(Layer.mergeAll(ProjectProvider(), ClusterProvider(), NodePoolProvider(), ApiEnableProvider(), NetworkProvider(), SubnetworkProvider(), FirewallProvider(), AddressProvider(), ForwardingRuleProvider(), GlobalAddressProvider(), SharedVpcHostProvider(), SharedVpcServiceProjectProvider(), PsaConnectionProvider(), ManagedLustreInstanceProvider(), StorageBucketProvider(), StorageBucketIamMemberProvider(), ServiceProvider(), JobProvider(), SqlInstanceProvider(), SqlDatabaseProvider(), SqlUserProvider(), ArtifactRegistryRepositoryProvider(), ServiceAccountProvider(), ServiceAccountKeyProvider(), KubernetesSecretProvider(), KubernetesManifestProvider(), HelmReleaseProvider())), Layer.provideMerge(fromAuthProvider()), Layer.provideMerge(GCPAuth),
|
|
76
78
|
// Auth/config failures (missing ADC, malformed profile) become
|
|
77
79
|
// unrecoverable defects rather than typed errors — mirrors
|
|
78
80
|
// `AWS/Providers.ts` and `Cloudflare/Providers.ts`. Resource lifecycle
|
package/lib/Providers.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Providers.js","sourceRoot":"","sources":["../src/Providers.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Providers.js","sourceRoot":"","sources":["../src/Providers.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Resource } from "alchemy";
|
|
2
|
+
import * as Provider from "alchemy/Provider";
|
|
3
|
+
import type * as GCP from "../Providers.ts";
|
|
4
|
+
/**
|
|
5
|
+
* A single `(role, member)` IAM binding entry on an **existing** GCS
|
|
6
|
+
* bucket that this stack does not manage. Contrast with
|
|
7
|
+
* {@link storageBucketIamMember}, the target-side binding helper: that
|
|
8
|
+
* one requires a stack-owned {@link StorageBucket} and is additive-only
|
|
9
|
+
* (bindings written into the bucket's binding bag are merged on
|
|
10
|
+
* reconcile but never pruned). This standalone resource takes the raw
|
|
11
|
+
* bucket name, adds the member on reconcile, and — the part the helper
|
|
12
|
+
* cannot do — **removes it again on delete**, so grants revoke when the
|
|
13
|
+
* declaring resource leaves the stack.
|
|
14
|
+
*
|
|
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.
|
|
29
|
+
*
|
|
30
|
+
* The read-modify-write on the shared bucket policy is member-scoped:
|
|
31
|
+
* exactly one member is added/removed, every foreign role and member is
|
|
32
|
+
* preserved verbatim, and the whole cycle retries with backoff so
|
|
33
|
+
* concurrent writers on the same bucket compose safely (GCS rejects a
|
|
34
|
+
* stale-etag set with `Conflict`/`PreconditionFailed`).
|
|
35
|
+
*
|
|
36
|
+
* There is nothing GCP-side to carry alchemy ownership labels on an IAM
|
|
37
|
+
* binding, so `read` has no `Unowned` adoption gate — presence of the
|
|
38
|
+
* exact member under the exact role IS the resource.
|
|
39
|
+
*
|
|
40
|
+
* @section Granting on an unmanaged bucket
|
|
41
|
+
* @example Read access for a collaborator group's namespace identity
|
|
42
|
+
* ```typescript
|
|
43
|
+
* yield* GCP.StorageBucketIamMember("SrlPackagingRead", {
|
|
44
|
+
* bucket: "srl_200hr_packaging",
|
|
45
|
+
* role: "roles/storage.objectViewer",
|
|
46
|
+
* member:
|
|
47
|
+
* "principal://iam.googleapis.com/projects/864268824536/locations/global/workloadIdentityPools/micro-research-cluster-a.svc.id.goog/subject/ns/collab-srl/sa/default",
|
|
48
|
+
* });
|
|
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
|
+
* ```
|
|
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
|
+
};
|
|
82
|
+
export type StorageBucketIamMemberProps = {
|
|
83
|
+
/**
|
|
84
|
+
* Name of the (pre-existing) GCS bucket to grant on. The bucket is
|
|
85
|
+
* NOT created, adopted, or deleted by this resource. Immutable —
|
|
86
|
+
* replace.
|
|
87
|
+
*/
|
|
88
|
+
bucket: string;
|
|
89
|
+
/** IAM role, e.g. `roles/storage.objectViewer`. Immutable — replace. */
|
|
90
|
+
role: string;
|
|
91
|
+
/**
|
|
92
|
+
* IAM member string (`serviceAccount:…`, `principal://…`,
|
|
93
|
+
* `group:…`, …). Immutable — replace.
|
|
94
|
+
*/
|
|
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;
|
|
104
|
+
};
|
|
105
|
+
export type StorageBucketIamMemberAttributes = {
|
|
106
|
+
/** Bucket name, threaded through from props. */
|
|
107
|
+
bucket: string;
|
|
108
|
+
/** IAM role, threaded through from props. */
|
|
109
|
+
role: string;
|
|
110
|
+
/** IAM member, threaded through from props. */
|
|
111
|
+
member: string;
|
|
112
|
+
/** IAM Condition, threaded through from props (absent = unconditional). */
|
|
113
|
+
condition?: StorageBucketIamMemberCondition;
|
|
114
|
+
};
|
|
115
|
+
export type StorageBucketIamMember = Resource<"GCP.StorageBucketIamMember", StorageBucketIamMemberProps, StorageBucketIamMemberAttributes, never, GCP.Providers>;
|
|
116
|
+
export declare const StorageBucketIamMember: import("alchemy").ResourceClass<StorageBucketIamMember>;
|
|
117
|
+
export declare const StorageBucketIamMemberProvider: () => import("effect/Layer").Layer<Provider.Provider<StorageBucketIamMember>, never, import("@distilled.cloud/gcp").Credentials | import("effect/unstable/http/HttpClient").HttpClient>;
|
|
118
|
+
//# sourceMappingURL=StorageBucketIamMember.d.ts.map
|
|
@@ -0,0 +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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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"}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { ConfigError } from "@distilled.cloud/gcp";
|
|
2
|
+
import * as storage from "@distilled.cloud/gcp/storage-v1";
|
|
3
|
+
import { Resource } from "alchemy";
|
|
4
|
+
import { isResolved } from "alchemy/Diff";
|
|
5
|
+
import * as Provider from "alchemy/Provider";
|
|
6
|
+
import * as Duration from "effect/Duration";
|
|
7
|
+
import * as Effect from "effect/Effect";
|
|
8
|
+
import * as Schedule from "effect/Schedule";
|
|
9
|
+
export const StorageBucketIamMember = Resource("GCP.StorageBucketIamMember");
|
|
10
|
+
export const StorageBucketIamMemberProvider = () => Provider.effect(StorageBucketIamMember, Effect.gen(function* () {
|
|
11
|
+
const getIamPolicyBuckets = yield* storage.getIamPolicyBuckets;
|
|
12
|
+
const setIamPolicyBuckets = yield* storage.setIamPolicyBuckets;
|
|
13
|
+
// Same backoff as StorageBucket's syncIam: the get→set cycle races
|
|
14
|
+
// other writers on the bucket policy (including sibling instances
|
|
15
|
+
// of this resource); a stale etag fails the set, the retry re-reads.
|
|
16
|
+
// ConfigError (bucket doesn't exist) is a terminal misconfiguration
|
|
17
|
+
// — fail fast instead of burning the ~8-minute backoff on it.
|
|
18
|
+
const etagRetry = (eff) => eff.pipe(Effect.retry({
|
|
19
|
+
while: (e) => e?._tag !== "ConfigError",
|
|
20
|
+
schedule: Schedule.exponential(Duration.seconds(2)).pipe(Schedule.both(Schedule.recurs(8))),
|
|
21
|
+
}));
|
|
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));
|
|
33
|
+
return {
|
|
34
|
+
// Identity IS the tuple; all fields are static values.
|
|
35
|
+
stables: ["bucket", "role", "member", "condition"],
|
|
36
|
+
diff: Effect.fn(function* ({ news, olds = {}, output }) {
|
|
37
|
+
if (!isResolved(news))
|
|
38
|
+
return undefined;
|
|
39
|
+
// Prior identity from persisted attributes first, then olds —
|
|
40
|
+
// mirrors Bucket.ts (`output?.name || olds.name`). `||`, not
|
|
41
|
+
// `??`: an empty string in a partially-persisted attribute is
|
|
42
|
+
// NOT a prior identity — treating it as one would force a
|
|
43
|
+
// spurious replace whose delete targets the wrong (empty)
|
|
44
|
+
// identity. If only `output` survives (e.g. props were lost),
|
|
45
|
+
// a changed triple must still replace, or the old member is
|
|
46
|
+
// never revoked.
|
|
47
|
+
const priorBucket = output?.bucket || olds.bucket;
|
|
48
|
+
const priorRole = output?.role || olds.role;
|
|
49
|
+
const priorMember = output?.member || olds.member;
|
|
50
|
+
if ((priorBucket && priorBucket !== news.bucket) ||
|
|
51
|
+
(priorRole && priorRole !== news.role) ||
|
|
52
|
+
(priorMember && priorMember !== news.member)) {
|
|
53
|
+
return { action: "replace" };
|
|
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
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}),
|
|
68
|
+
reconcile: Effect.fn(function* ({ news }) {
|
|
69
|
+
yield* etagRetry(Effect.gen(function* () {
|
|
70
|
+
const current = yield* getIamPolicyBuckets({
|
|
71
|
+
bucket: news.bucket,
|
|
72
|
+
optionsRequestedPolicyVersion: 3,
|
|
73
|
+
}).pipe(Effect.catchTag("NotFound", () => Effect.fail(new ConfigError({
|
|
74
|
+
message: `Bucket ${news.bucket} does not exist — StorageBucketIamMember grants on pre-existing buckets only.`,
|
|
75
|
+
}))));
|
|
76
|
+
if (hasMember(current, news.role, news.member, news.condition)) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const bindings = (current.bindings ?? []).map((b) => ({
|
|
80
|
+
...b,
|
|
81
|
+
members: [...(b.members ?? [])],
|
|
82
|
+
}));
|
|
83
|
+
const existing = bindings.find((b) => bindingMatches(b, news.role, news.condition));
|
|
84
|
+
if (existing) {
|
|
85
|
+
existing.members = [...(existing.members ?? []), news.member];
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
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
|
+
});
|
|
103
|
+
}
|
|
104
|
+
yield* setIamPolicyBuckets({
|
|
105
|
+
bucket: news.bucket,
|
|
106
|
+
body: { ...current, bindings, version: 3 },
|
|
107
|
+
});
|
|
108
|
+
}));
|
|
109
|
+
return {
|
|
110
|
+
bucket: news.bucket,
|
|
111
|
+
role: news.role,
|
|
112
|
+
member: news.member,
|
|
113
|
+
...(news.condition ? { condition: news.condition } : {}),
|
|
114
|
+
};
|
|
115
|
+
}),
|
|
116
|
+
delete: Effect.fn(function* ({ output }) {
|
|
117
|
+
// Partially-persisted attributes (empty fields) name no real
|
|
118
|
+
// grant — nothing to revoke, and getIamPolicy("") would 4xx.
|
|
119
|
+
if (!output.bucket || !output.role || !output.member)
|
|
120
|
+
return;
|
|
121
|
+
yield* etagRetry(Effect.gen(function* () {
|
|
122
|
+
const current = yield* getIamPolicyBuckets({
|
|
123
|
+
bucket: output.bucket,
|
|
124
|
+
optionsRequestedPolicyVersion: 3,
|
|
125
|
+
}).pipe(
|
|
126
|
+
// Bucket gone (or unreadable because gone) — the grant
|
|
127
|
+
// went with it; idempotent success.
|
|
128
|
+
Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
|
|
129
|
+
if (!current ||
|
|
130
|
+
!hasMember(current, output.role, output.member, output.condition)) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
// Remove exactly our member from the matched binding;
|
|
134
|
+
// drop the binding entirely if that empties it (GCS rejects
|
|
135
|
+
// bindings with zero members).
|
|
136
|
+
const bindings = (current.bindings ?? [])
|
|
137
|
+
.map((b) => bindingMatches(b, output.role, output.condition)
|
|
138
|
+
? {
|
|
139
|
+
...b,
|
|
140
|
+
members: (b.members ?? []).filter((m) => m !== output.member),
|
|
141
|
+
}
|
|
142
|
+
: { ...b, members: [...(b.members ?? [])] })
|
|
143
|
+
.filter((b) => (b.members?.length ?? 0) > 0);
|
|
144
|
+
yield* setIamPolicyBuckets({
|
|
145
|
+
bucket: output.bucket,
|
|
146
|
+
body: { ...current, bindings, version: 3 },
|
|
147
|
+
}).pipe(Effect.catchTag("NotFound", () => Effect.void));
|
|
148
|
+
}));
|
|
149
|
+
}),
|
|
150
|
+
read: Effect.fn(function* ({ output, olds }) {
|
|
151
|
+
const bucket = output?.bucket ?? olds?.bucket;
|
|
152
|
+
const role = output?.role ?? olds?.role;
|
|
153
|
+
const member = output?.member ?? olds?.member;
|
|
154
|
+
const condition = output?.condition ?? olds?.condition;
|
|
155
|
+
if (!bucket || !role || !member)
|
|
156
|
+
return undefined;
|
|
157
|
+
const current = yield* getIamPolicyBuckets({
|
|
158
|
+
bucket,
|
|
159
|
+
optionsRequestedPolicyVersion: 3,
|
|
160
|
+
}).pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)), Effect.catchTag("Forbidden", () => Effect.succeed(undefined)));
|
|
161
|
+
if (!current)
|
|
162
|
+
return undefined;
|
|
163
|
+
// Absent member → undefined: the engine treats that as drift
|
|
164
|
+
// and the next plan re-grants.
|
|
165
|
+
if (!hasMember(current, role, member, condition))
|
|
166
|
+
return undefined;
|
|
167
|
+
return {
|
|
168
|
+
bucket,
|
|
169
|
+
role,
|
|
170
|
+
member,
|
|
171
|
+
...(condition ? { condition } : {}),
|
|
172
|
+
};
|
|
173
|
+
}),
|
|
174
|
+
};
|
|
175
|
+
}));
|
|
176
|
+
//# sourceMappingURL=StorageBucketIamMember.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { StorageBucket, StorageBucketProvider, } from "./Bucket.ts";
|
|
2
2
|
export type { StorageBucketProps, StorageBucketIamBinding, StorageBucketBindingContract, } from "./Bucket.ts";
|
|
3
3
|
export * from "./BucketIamMember.ts";
|
|
4
|
+
export { StorageBucketIamMember, StorageBucketIamMemberProvider, } from "./StorageBucketIamMember.ts";
|
|
5
|
+
export type { StorageBucketIamMemberProps, StorageBucketIamMemberAttributes, StorageBucketIamMemberCondition, } from "./StorageBucketIamMember.ts";
|
|
4
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"}
|
|
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/lib/Storage/index.js
CHANGED
package/lib/Storage/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/Storage/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,qBAAqB,GACtB,MAAM,aAAa,CAAC;AAMrB,cAAc,sBAAsB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/Storage/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,qBAAqB,GACtB,MAAM,aAAa,CAAC;AAMrB,cAAc,sBAAsB,CAAC;AACrC,OAAO,EACL,sBAAsB,EACtB,8BAA8B,GAC/B,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",
|
package/src/Providers.ts
CHANGED
|
@@ -57,6 +57,10 @@ import {
|
|
|
57
57
|
StorageBucket,
|
|
58
58
|
StorageBucketProvider,
|
|
59
59
|
} from "./Storage/Bucket.ts";
|
|
60
|
+
import {
|
|
61
|
+
StorageBucketIamMember,
|
|
62
|
+
StorageBucketIamMemberProvider,
|
|
63
|
+
} from "./Storage/StorageBucketIamMember.ts";
|
|
60
64
|
import { ApiEnable, ApiEnableProvider } from "./ServiceUsage/ApiEnable.ts";
|
|
61
65
|
import { SqlDatabase, SqlDatabaseProvider } from "./Sqladmin/Database.ts";
|
|
62
66
|
import { SqlInstance, SqlInstanceProvider } from "./Sqladmin/Instance.ts";
|
|
@@ -100,6 +104,7 @@ export const providers = () =>
|
|
|
100
104
|
PsaConnection,
|
|
101
105
|
ManagedLustreInstance,
|
|
102
106
|
StorageBucket,
|
|
107
|
+
StorageBucketIamMember,
|
|
103
108
|
Service,
|
|
104
109
|
Job,
|
|
105
110
|
SqlInstance,
|
|
@@ -130,6 +135,7 @@ export const providers = () =>
|
|
|
130
135
|
PsaConnectionProvider(),
|
|
131
136
|
ManagedLustreInstanceProvider(),
|
|
132
137
|
StorageBucketProvider(),
|
|
138
|
+
StorageBucketIamMemberProvider(),
|
|
133
139
|
ServiceProvider(),
|
|
134
140
|
JobProvider(),
|
|
135
141
|
SqlInstanceProvider(),
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import { ConfigError } from "@distilled.cloud/gcp";
|
|
2
|
+
import * as storage from "@distilled.cloud/gcp/storage-v1";
|
|
3
|
+
import { Resource } from "alchemy";
|
|
4
|
+
import { isResolved } from "alchemy/Diff";
|
|
5
|
+
import * as Provider from "alchemy/Provider";
|
|
6
|
+
import * as Duration from "effect/Duration";
|
|
7
|
+
import * as Effect from "effect/Effect";
|
|
8
|
+
import * as Schedule from "effect/Schedule";
|
|
9
|
+
import type * as GCP from "../Providers.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A single `(role, member)` IAM binding entry on an **existing** GCS
|
|
13
|
+
* bucket that this stack does not manage. Contrast with
|
|
14
|
+
* {@link storageBucketIamMember}, the target-side binding helper: that
|
|
15
|
+
* one requires a stack-owned {@link StorageBucket} and is additive-only
|
|
16
|
+
* (bindings written into the bucket's binding bag are merged on
|
|
17
|
+
* reconcile but never pruned). This standalone resource takes the raw
|
|
18
|
+
* bucket name, adds the member on reconcile, and — the part the helper
|
|
19
|
+
* cannot do — **removes it again on delete**, so grants revoke when the
|
|
20
|
+
* declaring resource leaves the stack.
|
|
21
|
+
*
|
|
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.
|
|
36
|
+
*
|
|
37
|
+
* The read-modify-write on the shared bucket policy is member-scoped:
|
|
38
|
+
* exactly one member is added/removed, every foreign role and member is
|
|
39
|
+
* preserved verbatim, and the whole cycle retries with backoff so
|
|
40
|
+
* concurrent writers on the same bucket compose safely (GCS rejects a
|
|
41
|
+
* stale-etag set with `Conflict`/`PreconditionFailed`).
|
|
42
|
+
*
|
|
43
|
+
* There is nothing GCP-side to carry alchemy ownership labels on an IAM
|
|
44
|
+
* binding, so `read` has no `Unowned` adoption gate — presence of the
|
|
45
|
+
* exact member under the exact role IS the resource.
|
|
46
|
+
*
|
|
47
|
+
* @section Granting on an unmanaged bucket
|
|
48
|
+
* @example Read access for a collaborator group's namespace identity
|
|
49
|
+
* ```typescript
|
|
50
|
+
* yield* GCP.StorageBucketIamMember("SrlPackagingRead", {
|
|
51
|
+
* bucket: "srl_200hr_packaging",
|
|
52
|
+
* role: "roles/storage.objectViewer",
|
|
53
|
+
* member:
|
|
54
|
+
* "principal://iam.googleapis.com/projects/864268824536/locations/global/workloadIdentityPools/micro-research-cluster-a.svc.id.goog/subject/ns/collab-srl/sa/default",
|
|
55
|
+
* });
|
|
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
|
+
* ```
|
|
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
|
+
|
|
90
|
+
export type StorageBucketIamMemberProps = {
|
|
91
|
+
/**
|
|
92
|
+
* Name of the (pre-existing) GCS bucket to grant on. The bucket is
|
|
93
|
+
* NOT created, adopted, or deleted by this resource. Immutable —
|
|
94
|
+
* replace.
|
|
95
|
+
*/
|
|
96
|
+
bucket: string;
|
|
97
|
+
/** IAM role, e.g. `roles/storage.objectViewer`. Immutable — replace. */
|
|
98
|
+
role: string;
|
|
99
|
+
/**
|
|
100
|
+
* IAM member string (`serviceAccount:…`, `principal://…`,
|
|
101
|
+
* `group:…`, …). Immutable — replace.
|
|
102
|
+
*/
|
|
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;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export type StorageBucketIamMemberAttributes = {
|
|
115
|
+
/** Bucket name, threaded through from props. */
|
|
116
|
+
bucket: string;
|
|
117
|
+
/** IAM role, threaded through from props. */
|
|
118
|
+
role: string;
|
|
119
|
+
/** IAM member, threaded through from props. */
|
|
120
|
+
member: string;
|
|
121
|
+
/** IAM Condition, threaded through from props (absent = unconditional). */
|
|
122
|
+
condition?: StorageBucketIamMemberCondition;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export type StorageBucketIamMember = Resource<
|
|
126
|
+
"GCP.StorageBucketIamMember",
|
|
127
|
+
StorageBucketIamMemberProps,
|
|
128
|
+
StorageBucketIamMemberAttributes,
|
|
129
|
+
never,
|
|
130
|
+
GCP.Providers
|
|
131
|
+
>;
|
|
132
|
+
|
|
133
|
+
export const StorageBucketIamMember = Resource<StorageBucketIamMember>(
|
|
134
|
+
"GCP.StorageBucketIamMember",
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
export const StorageBucketIamMemberProvider = () =>
|
|
138
|
+
Provider.effect(
|
|
139
|
+
StorageBucketIamMember,
|
|
140
|
+
Effect.gen(function* () {
|
|
141
|
+
const getIamPolicyBuckets = yield* storage.getIamPolicyBuckets;
|
|
142
|
+
const setIamPolicyBuckets = yield* storage.setIamPolicyBuckets;
|
|
143
|
+
|
|
144
|
+
// Same backoff as StorageBucket's syncIam: the get→set cycle races
|
|
145
|
+
// other writers on the bucket policy (including sibling instances
|
|
146
|
+
// of this resource); a stale etag fails the set, the retry re-reads.
|
|
147
|
+
// ConfigError (bucket doesn't exist) is a terminal misconfiguration
|
|
148
|
+
// — fail fast instead of burning the ~8-minute backoff on it.
|
|
149
|
+
const etagRetry = <A, E, R>(eff: Effect.Effect<A, E, R>) =>
|
|
150
|
+
eff.pipe(
|
|
151
|
+
Effect.retry({
|
|
152
|
+
while: (e) => (e as { _tag?: string })?._tag !== "ConfigError",
|
|
153
|
+
schedule: Schedule.exponential(Duration.seconds(2)).pipe(
|
|
154
|
+
Schedule.both(Schedule.recurs(8)),
|
|
155
|
+
),
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
|
|
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
|
+
|
|
174
|
+
const hasMember = (
|
|
175
|
+
policy: storage.Policy,
|
|
176
|
+
role: string,
|
|
177
|
+
member: string,
|
|
178
|
+
condition?: StorageBucketIamMemberCondition,
|
|
179
|
+
) =>
|
|
180
|
+
(policy.bindings ?? []).some(
|
|
181
|
+
(b) =>
|
|
182
|
+
bindingMatches(b, role, condition) &&
|
|
183
|
+
(b.members ?? []).includes(member),
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
// Identity IS the tuple; all fields are static values.
|
|
188
|
+
stables: ["bucket", "role", "member", "condition"],
|
|
189
|
+
diff: Effect.fn(function* ({ news, olds = {}, output }) {
|
|
190
|
+
if (!isResolved(news)) return undefined;
|
|
191
|
+
// Prior identity from persisted attributes first, then olds —
|
|
192
|
+
// mirrors Bucket.ts (`output?.name || olds.name`). `||`, not
|
|
193
|
+
// `??`: an empty string in a partially-persisted attribute is
|
|
194
|
+
// NOT a prior identity — treating it as one would force a
|
|
195
|
+
// spurious replace whose delete targets the wrong (empty)
|
|
196
|
+
// identity. If only `output` survives (e.g. props were lost),
|
|
197
|
+
// a changed triple must still replace, or the old member is
|
|
198
|
+
// never revoked.
|
|
199
|
+
const priorBucket = output?.bucket || olds.bucket;
|
|
200
|
+
const priorRole = output?.role || olds.role;
|
|
201
|
+
const priorMember = output?.member || olds.member;
|
|
202
|
+
if (
|
|
203
|
+
(priorBucket && priorBucket !== news.bucket) ||
|
|
204
|
+
(priorRole && priorRole !== news.role) ||
|
|
205
|
+
(priorMember && priorMember !== news.member)
|
|
206
|
+
) {
|
|
207
|
+
return { action: "replace" } as const;
|
|
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
|
+
}
|
|
221
|
+
return undefined;
|
|
222
|
+
}),
|
|
223
|
+
reconcile: Effect.fn(function* ({ news }) {
|
|
224
|
+
yield* etagRetry(
|
|
225
|
+
Effect.gen(function* () {
|
|
226
|
+
const current = yield* getIamPolicyBuckets({
|
|
227
|
+
bucket: news.bucket,
|
|
228
|
+
optionsRequestedPolicyVersion: 3,
|
|
229
|
+
}).pipe(
|
|
230
|
+
Effect.catchTag("NotFound", () =>
|
|
231
|
+
Effect.fail(
|
|
232
|
+
new ConfigError({
|
|
233
|
+
message: `Bucket ${news.bucket} does not exist — StorageBucketIamMember grants on pre-existing buckets only.`,
|
|
234
|
+
}),
|
|
235
|
+
),
|
|
236
|
+
),
|
|
237
|
+
);
|
|
238
|
+
if (hasMember(current, news.role, news.member, news.condition)) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const bindings = (current.bindings ?? []).map((b) => ({
|
|
243
|
+
...b,
|
|
244
|
+
members: [...(b.members ?? [])],
|
|
245
|
+
}));
|
|
246
|
+
const existing = bindings.find((b) =>
|
|
247
|
+
bindingMatches(b, news.role, news.condition),
|
|
248
|
+
);
|
|
249
|
+
if (existing) {
|
|
250
|
+
existing.members = [...(existing.members ?? []), news.member];
|
|
251
|
+
} else {
|
|
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
|
+
});
|
|
267
|
+
}
|
|
268
|
+
yield* setIamPolicyBuckets({
|
|
269
|
+
bucket: news.bucket,
|
|
270
|
+
body: { ...current, bindings, version: 3 },
|
|
271
|
+
});
|
|
272
|
+
}),
|
|
273
|
+
);
|
|
274
|
+
return {
|
|
275
|
+
bucket: news.bucket,
|
|
276
|
+
role: news.role,
|
|
277
|
+
member: news.member,
|
|
278
|
+
...(news.condition ? { condition: news.condition } : {}),
|
|
279
|
+
};
|
|
280
|
+
}),
|
|
281
|
+
delete: Effect.fn(function* ({ output }) {
|
|
282
|
+
// Partially-persisted attributes (empty fields) name no real
|
|
283
|
+
// grant — nothing to revoke, and getIamPolicy("") would 4xx.
|
|
284
|
+
if (!output.bucket || !output.role || !output.member) return;
|
|
285
|
+
yield* etagRetry(
|
|
286
|
+
Effect.gen(function* () {
|
|
287
|
+
const current = yield* getIamPolicyBuckets({
|
|
288
|
+
bucket: output.bucket,
|
|
289
|
+
optionsRequestedPolicyVersion: 3,
|
|
290
|
+
}).pipe(
|
|
291
|
+
// Bucket gone (or unreadable because gone) — the grant
|
|
292
|
+
// went with it; idempotent success.
|
|
293
|
+
Effect.catchTag("NotFound", () =>
|
|
294
|
+
Effect.succeed(undefined as storage.Policy | undefined),
|
|
295
|
+
),
|
|
296
|
+
);
|
|
297
|
+
if (
|
|
298
|
+
!current ||
|
|
299
|
+
!hasMember(current, output.role, output.member, output.condition)
|
|
300
|
+
) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
// Remove exactly our member from the matched binding;
|
|
304
|
+
// drop the binding entirely if that empties it (GCS rejects
|
|
305
|
+
// bindings with zero members).
|
|
306
|
+
const bindings = (current.bindings ?? [])
|
|
307
|
+
.map((b) =>
|
|
308
|
+
bindingMatches(b, output.role, output.condition)
|
|
309
|
+
? {
|
|
310
|
+
...b,
|
|
311
|
+
members: (b.members ?? []).filter(
|
|
312
|
+
(m) => m !== output.member,
|
|
313
|
+
),
|
|
314
|
+
}
|
|
315
|
+
: { ...b, members: [...(b.members ?? [])] },
|
|
316
|
+
)
|
|
317
|
+
.filter((b) => (b.members?.length ?? 0) > 0);
|
|
318
|
+
yield* setIamPolicyBuckets({
|
|
319
|
+
bucket: output.bucket,
|
|
320
|
+
body: { ...current, bindings, version: 3 },
|
|
321
|
+
}).pipe(Effect.catchTag("NotFound", () => Effect.void));
|
|
322
|
+
}),
|
|
323
|
+
);
|
|
324
|
+
}),
|
|
325
|
+
read: Effect.fn(function* ({ output, olds }) {
|
|
326
|
+
const bucket = output?.bucket ?? olds?.bucket;
|
|
327
|
+
const role = output?.role ?? olds?.role;
|
|
328
|
+
const member = output?.member ?? olds?.member;
|
|
329
|
+
const condition = output?.condition ?? olds?.condition;
|
|
330
|
+
if (!bucket || !role || !member) return undefined;
|
|
331
|
+
const current = yield* getIamPolicyBuckets({
|
|
332
|
+
bucket,
|
|
333
|
+
optionsRequestedPolicyVersion: 3,
|
|
334
|
+
}).pipe(
|
|
335
|
+
Effect.catchTag("NotFound", () =>
|
|
336
|
+
Effect.succeed(undefined as storage.Policy | undefined),
|
|
337
|
+
),
|
|
338
|
+
Effect.catchTag("Forbidden", () =>
|
|
339
|
+
Effect.succeed(undefined as storage.Policy | undefined),
|
|
340
|
+
),
|
|
341
|
+
);
|
|
342
|
+
if (!current) return undefined;
|
|
343
|
+
// Absent member → undefined: the engine treats that as drift
|
|
344
|
+
// and the next plan re-grants.
|
|
345
|
+
if (!hasMember(current, role, member, condition)) return undefined;
|
|
346
|
+
return {
|
|
347
|
+
bucket,
|
|
348
|
+
role,
|
|
349
|
+
member,
|
|
350
|
+
...(condition ? { condition } : {}),
|
|
351
|
+
};
|
|
352
|
+
}),
|
|
353
|
+
};
|
|
354
|
+
}),
|
|
355
|
+
);
|
package/src/Storage/index.ts
CHANGED
|
@@ -8,3 +8,12 @@ export type {
|
|
|
8
8
|
StorageBucketBindingContract,
|
|
9
9
|
} from "./Bucket.ts";
|
|
10
10
|
export * from "./BucketIamMember.ts";
|
|
11
|
+
export {
|
|
12
|
+
StorageBucketIamMember,
|
|
13
|
+
StorageBucketIamMemberProvider,
|
|
14
|
+
} from "./StorageBucketIamMember.ts";
|
|
15
|
+
export type {
|
|
16
|
+
StorageBucketIamMemberProps,
|
|
17
|
+
StorageBucketIamMemberAttributes,
|
|
18
|
+
StorageBucketIamMemberCondition,
|
|
19
|
+
} from "./StorageBucketIamMember.ts";
|