1import * as compute from "@distilled.cloud/gcp/compute_v1";
2import * as resourcemanager from "@distilled.cloud/gcp/cloudresourcemanager_v3";
3import {
4 encodeDescription,
5 ignoredCodes,
6 hasOwnershipMarker,
7 lastSegment,
8 parseDescription,
9 sameJson,
10 sorted,
11 toPhysicalName,
12} from "./internal.ts";
13import { waitOrganizationOperation } from "./operations.ts";
14import type {
15 SecurityPolicyAdaptiveProtectionConfig,
16 SecurityPolicyAdvancedOptionsConfig,
17 SecurityPolicyDdosProtectionConfig,
18 SecurityPolicyRecaptchaOptionsConfig,
19 SecurityPolicyRule,
20 SecurityPolicyRuleMatcher,
21 SecurityPolicyType,
22 SecurityPolicyUserDefinedField,
23} from "./SecurityPolicy.ts";
24import * as Data from "effect/Data";
25import * as Effect from "effect/Effect";
26import * as Schedule from "effect/Schedule";
27import * as Stream from "effect/Stream";
28import { Unowned } from "../../AdoptPolicy.ts";
29import { isResolved } from "../../Diff.ts";
30import * as Provider from "../../Provider.ts";
31import { Resource } from "../../Resource.ts";
32import { GcpEnvironment } from "../Environment.ts";
33import { createInternalLabels, hasAlchemyLabels } from "../Labels.ts";
34import type { Providers } from "../Providers.ts";
35
36const DEFAULT_TYPE = "CLOUD_ARMOR";
37const DEFAULT_RULE_PRIORITY = 2147483647;
38
39export type OrganizationSecurityPolicyType = SecurityPolicyType;
40export type OrganizationSecurityPolicyRule = SecurityPolicyRule;
41export type OrganizationSecurityPolicyRuleMatcher = SecurityPolicyRuleMatcher;
42
43export type OrganizationSecurityPolicyProps = {
44 /**
45 * User-provided RFC1035 name (`shortName`). Unique within the parent
46 * organization. If omitted, a unique name is generated from the stack,
47 * stage, and logical id. Immutable — changing it replaces the policy.
48 * After create, GCP also assigns a numeric `name`.
49 */
50 shortName?: string;
51 /**
52 * Parent organization or folder. Format `organizations/{organization}`
53 * or `folders/{folder}`. If omitted, Alchemy uses the project parent
54 * from Cloud Resource Manager. Immutable — changing it replaces the
55 * policy.
56 */
57 parent?: string;
58 /**
59 * Optional description. Organization security policies have no
60 * `setLabels` API, so Alchemy ownership is stored in a `[alchemy …]`
61 * prefix for `list` / nuke.
62 */
63 description?: string;
64 /**
65 * Intended use of the policy. Set only at create time — changing it
66 * replaces the policy.
67 * @default "CLOUD_ARMOR"
68 */
69 type?: OrganizationSecurityPolicyType;
70 /**
71 * Ordered Cloud Armor rules. There must always be a default rule at
72 * priority `2147483647` matching `*`. If omitted on create, GCP adds
73 * an `allow` default. When this field is set, Alchemy syncs rules with
74 * `addRule` / `patchRule` / `removeRule`. When omitted on later
75 * updates, observed rules are left in place.
76 */
77 rules?: OrganizationSecurityPolicyRule[];
78 /**
79 * JSON parsing, log level, and related WAF options. Only applied when
80 * set.
81 */
82 advancedOptionsConfig?: SecurityPolicyAdvancedOptionsConfig;
83 /**
84 * Cloud Armor Adaptive Protection. Only applied when set.
85 */
86 adaptiveProtectionConfig?: SecurityPolicyAdaptiveProtectionConfig;
87 /**
88 * reCAPTCHA site key used by `redirect` rules of type
89 * `GOOGLE_RECAPTCHA`. Only applied when set.
90 */
91 recaptchaOptionsConfig?: SecurityPolicyRecaptchaOptionsConfig;
92 /**
93 * Network DDoS protection (`STANDARD` or `ADVANCED`). Only applied
94 * when set.
95 */
96 ddosProtectionConfig?: SecurityPolicyDdosProtectionConfig;
97 /**
98 * Packet field extractors for `CLOUD_ARMOR_NETWORK` policies. Only
99 * applied when set.
100 */
101 userDefinedFields?: SecurityPolicyUserDefinedField[];
102};
103
104export type OrganizationSecurityPolicy = Resource<
105 "GCP.Compute.OrganizationSecurityPolicy",
106 OrganizationSecurityPolicyProps,
107 {
108 /** User-provided RFC1035 name. */
109 shortName: string;
110 /** Server-assigned numeric policy id (`name` in the API). */
111 securityPolicyId: string;
112 /** Parent `organizations/{organization}` or `folders/{folder}`. */
113 parent: string;
114 /** Project id of the deploying stack. */
115 project: string;
116 /** Policy type. */
117 type: string;
118 /** User description with the Alchemy ownership prefix stripped. */
119 description: string | undefined;
120 /** Rules currently attached (including the default). */
121 rules: OrganizationSecurityPolicyRule[];
122 /** Advanced WAF options, if configured. */
123 advancedOptionsConfig: SecurityPolicyAdvancedOptionsConfig | undefined;
124 /** Adaptive Protection config, if configured. */
125 adaptiveProtectionConfig:
126 | SecurityPolicyAdaptiveProtectionConfig
127 | undefined;
128 /** reCAPTCHA options, if configured. */
129 recaptchaOptionsConfig: SecurityPolicyRecaptchaOptionsConfig | undefined;
130 /** DDoS protection config, if configured. */
131 ddosProtectionConfig: SecurityPolicyDdosProtectionConfig | undefined;
132 /** User-defined packet fields, if configured. */
133 userDefinedFields: SecurityPolicyUserDefinedField[];
134 /** Optimistic-locking fingerprint. */
135 fingerprint: string | undefined;
136 /** Server-defined URL. */
137 selfLink: string | undefined;
138 /** Server-assigned numeric id (same as `securityPolicyId` when present). */
139 id: string | undefined;
140 /** RFC3339 creation timestamp. */
141 creationTimestamp: string | undefined;
142 /** Resource kind. */
143 kind: string | undefined;
144 },
145 never,
146 Providers
147>;
148
149/**
150 * An organization-scoped Cloud Armor security policy.
151 *
152 * Organization security policies live under
153 * `locations/global/securityPolicies` and are identified by a
154 * server-assigned numeric id. The user-facing name is `shortName`. Parent
155 * and type are immutable. Description and WAF options update in place via
156 * `organizationSecurityPolicies.patch`. Rules are synced with `addRule` /
157 * `patchRule` / `removeRule`.
158 *
159 * ### Creating an Organization Security Policy
160 * **Example:** Generated name with a deny rule
161 * ```typescript
162 * const policy = yield* GCP.Compute.OrganizationSecurityPolicy("OrgArmor", {
163 * description: "deny a scanner",
164 * rules: [
165 * {
166 * action: "deny(403)",
167 * priority: 1000,
168 * match: {
169 * versionedExpr: "SRC_IPS_V1",
170 * config: { srcIpRanges: ["9.9.9.0/24"] },
171 * },
172 * },
173 * ],
174 * });
175 * ```
176 *
177 * **Example:** Named policy under an organization
178 * ```typescript
179 * const policy = yield* GCP.Compute.OrganizationSecurityPolicy("OrgArmor", {
180 * shortName: "app-org-armor",
181 * parent: "organizations/123456789",
182 * description: "org WAF",
183 * });
184 * ```
185 *
186 * @resource
187 * @category Compute
188 */
189export const OrganizationSecurityPolicy = Resource<OrganizationSecurityPolicy>(
190 "GCP.Compute.OrganizationSecurityPolicy",
191);
192
193export class OrganizationSecurityPolicyNotResolved extends Data.TaggedError(
194 "GCP.Compute.OrganizationSecurityPolicyNotResolved",
195)<{
196 securityPolicyId: string;
197 shortName: string;
198}> {}
199
200export class OrganizationSecurityPolicyParentRequired extends Data.TaggedError(
201 "GCP.Compute.OrganizationSecurityPolicyParentRequired",
202)<{
203 project: string;
204}> {}
205
206const typeOf = (value: string | undefined) =>
207 (value ?? DEFAULT_TYPE).toUpperCase();
208
209const normalizeParent = (value: string): string => {
210 const trimmed = value.trim().replace(/\/+$/, "");
211 if (trimmed.startsWith("organizations/") || trimmed.startsWith("folders/")) {
212 return trimmed;
213 }
214 if (trimmed.startsWith("organization/")) {
215 return `organizations/${trimmed.slice("organization/".length)}`;
216 }
217 if (trimmed.startsWith("folder/")) {
218 return `folders/${trimmed.slice("folder/".length)}`;
219 }
220 if (/^\d+$/.test(trimmed)) return `organizations/${trimmed}`;
221 return trimmed;
222};
223
224const projectParent = (project: string) =>
225 resourcemanager.getProjects({ name: `projects/${project}` }).pipe(
226 Effect.map((resource) =>
227 resource.parent !== undefined && resource.parent.length > 0
228 ? normalizeParent(resource.parent)
229 : undefined,
230 ),
231 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
232 );
233
234const resolveParent = (
235 news: { parent?: string },
236 output: { parent?: string } | undefined,
237) =>
238 Effect.gen(function* () {
239 const explicit = news.parent ?? output?.parent;
240 if (explicit !== undefined && explicit.length > 0) {
241 return normalizeParent(explicit);
242 }
243 const env = yield* GcpEnvironment.current;
244 const fromProject = yield* projectParent(env.project);
245 if (fromProject !== undefined) return fromProject;
246 return yield* new OrganizationSecurityPolicyParentRequired({
247 project: env.project,
248 });
249 });
250
251const listParents = (project: string) =>
252 Effect.gen(function* () {
253 const parents = new Set<string>();
254 const fromProject = yield* projectParent(project);
255 if (fromProject !== undefined) parents.add(fromProject);
256 return [...parents];
257 });
258
259const canonMatch = (match: SecurityPolicyRuleMatcher | undefined) => {
260 if (match === undefined) return undefined;
261 return {
262 versionedExpr: match.versionedExpr,
263 config: match.config
264 ? { srcIpRanges: sorted(match.config.srcIpRanges) }
265 : undefined,
266 expr: match.expr,
267 exprOptions: match.exprOptions,
268 };
269};
270
271const canonRule = (rule: SecurityPolicyRule) => ({
272 priority: rule.priority,
273 action: rule.action,
274 description: rule.description ?? "",
275 preview: rule.preview === true,
276 match: canonMatch(rule.match),
277 redirectOptions: rule.redirectOptions ?? null,
278 rateLimitOptions: rule.rateLimitOptions ?? null,
279 headerAction: rule.headerAction ?? null,
280 preconfiguredWafConfig: rule.preconfiguredWafConfig ?? null,
281 networkMatch: rule.networkMatch ?? null,
282});
283
284const ruleEquals = (left: SecurityPolicyRule, right: SecurityPolicyRule) =>
285 sameJson(canonRule(left), canonRule(right));
286
287const toRuleBody = (rule: SecurityPolicyRule): SecurityPolicyRule => ({
288 priority: rule.priority,
289 action: rule.action,
290 description: rule.description,
291 preview: rule.preview === true ? true : undefined,
292 match: rule.match,
293 redirectOptions: rule.redirectOptions,
294 rateLimitOptions: rule.rateLimitOptions,
295 headerAction: rule.headerAction,
296 preconfiguredWafConfig: rule.preconfiguredWafConfig,
297 networkMatch: rule.networkMatch,
298});
299
300const defaultAllowRule = (): SecurityPolicyRule => ({
301 action: "allow",
302 priority: DEFAULT_RULE_PRIORITY,
303 match: {
304 versionedExpr: "SRC_IPS_V1",
305 config: { srcIpRanges: ["*"] },
306 },
307});
308
309const withDefaultRule = (
310 rules: readonly SecurityPolicyRule[] | undefined,
311): SecurityPolicyRule[] | undefined => {
312 if (rules === undefined) return undefined;
313 const bodies = rules.map(toRuleBody);
314 if (bodies.some((rule) => rule.priority === DEFAULT_RULE_PRIORITY)) {
315 return bodies;
316 }
317 return [...bodies, defaultAllowRule()];
318};
319
320const desiredRules = (
321 news: OrganizationSecurityPolicyProps,
322 observed: readonly SecurityPolicyRule[],
323): SecurityPolicyRule[] | undefined => {
324 if (news.rules === undefined) return undefined;
325 const byPriority = new Map<number, SecurityPolicyRule>();
326 for (const rule of news.rules) {
327 if (rule.priority === undefined) continue;
328 byPriority.set(rule.priority, rule);
329 }
330 if (!byPriority.has(DEFAULT_RULE_PRIORITY)) {
331 const observedDefault = observed.find(
332 (rule) => rule.priority === DEFAULT_RULE_PRIORITY,
333 );
334 byPriority.set(
335 DEFAULT_RULE_PRIORITY,
336 observedDefault ?? defaultAllowRule(),
337 );
338 }
339 return [...byPriority.values()].sort(
340 (left, right) => (left.priority ?? 0) - (right.priority ?? 0),
341 );
342};
343
344const subsetEqual = (observed: unknown, desired: unknown): boolean => {
345 if (desired === undefined) return true;
346 if (typeof desired !== typeof observed) return false;
347 if (desired === null || observed === null) return desired === observed;
348 if (Array.isArray(desired)) {
349 if (!Array.isArray(observed) || desired.length !== observed.length) {
350 return false;
351 }
352 return desired.every((item, index) => subsetEqual(observed[index], item));
353 }
354 if (typeof desired === "object") {
355 if (typeof observed !== "object") return false;
356 const current = observed as Record<string, unknown>;
357 return Object.entries(desired as Record<string, unknown>).every(
358 ([key, value]) => value === undefined || subsetEqual(current[key], value),
359 );
360 }
361 return observed === desired;
362};
363
364const policyIdOf = (policy: compute.SecurityPolicy) =>
365 policy.name ?? policy.id ?? lastSegment(policy.selfLink);
366
367const toAttrs = (
368 policy: compute.SecurityPolicy,
369 project: string,
370): OrganizationSecurityPolicy["Attributes"] => {
371 const parsed = parseDescription(policy.description);
372 return {
373 shortName: policy.shortName ?? policy.name ?? "",
374 securityPolicyId: policyIdOf(policy),
375 parent: policy.parent ?? "",
376 project,
377 type: typeOf(policy.type),
378 description: parsed.description,
379 rules: policy.rules ?? [],
380 advancedOptionsConfig: policy.advancedOptionsConfig,
381 adaptiveProtectionConfig: policy.adaptiveProtectionConfig,
382 recaptchaOptionsConfig: policy.recaptchaOptionsConfig,
383 ddosProtectionConfig: policy.ddosProtectionConfig,
384 userDefinedFields: policy.userDefinedFields ?? [],
385 fingerprint: policy.fingerprint,
386 selfLink: policy.selfLink,
387 id: policy.id,
388 creationTimestamp: policy.creationTimestamp,
389 kind: policy.kind,
390 };
391};
392
393const getById = (securityPolicy: string) =>
394 compute
395 .getOrganizationSecurityPolicies({ securityPolicy })
396 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
397
398const findByShortName = (parentId: string, shortName: string) =>
399 compute.listOrganizationSecurityPolicies
400 .items({
401 parentId,
402 maxResults: 500,
403 returnPartialSuccess: true,
404 })
405 .pipe(
406 Stream.filter((policy) => policy.shortName === shortName),
407 Stream.runCollect,
408 Effect.map((chunk) => Array.from(chunk)[0]),
409 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
410 );
411
412const observe = (
413 securityPolicyId: string | undefined,
414 parentId: string,
415 shortName: string,
416) =>
417 Effect.gen(function* () {
418 if (securityPolicyId !== undefined && securityPolicyId.length > 0) {
419 const existing = yield* getById(securityPolicyId);
420 if (existing !== undefined) return existing;
421 }
422 return yield* findByShortName(parentId, shortName);
423 });
424
425const runOp = <E extends { readonly _tag: string }, R>(
426 securityPolicyId: string,
427 parentId: string,
428 start: Effect.Effect<compute.Operation, E, R>,
429 options?: { ignoreAlreadyExists?: boolean; ignoreNotFound?: boolean },
430) =>
431 start.pipe(
432 Effect.flatMap((operation) =>
433 waitOrganizationOperation(operation, parentId, {
434 ignore: ignoredCodes(options),
435 }),
436 ),
437 Effect.retry({
438 while: (error) => error._tag === "Conflict",
439 times: 5,
440 schedule: Schedule.spaced("1 second"),
441 }),
442 );
443
444const idFromOperation = (operation: compute.Operation) => {
445 if (operation.targetId !== undefined && operation.targetId.length > 0) {
446 return operation.targetId;
447 }
448 const fromLink = lastSegment(operation.targetLink);
449 return fromLink.length > 0 ? fromLink : "";
450};
451
452const awaitResource = (
453 securityPolicyId: string,
454 parentId: string,
455 shortName: string,
456) =>
457 observe(securityPolicyId, parentId, shortName).pipe(
458 Effect.flatMap((policy) =>
459 policy !== undefined
460 ? Effect.succeed(policy)
461 : Effect.fail(
462 new OrganizationSecurityPolicyNotResolved({
463 securityPolicyId,
464 shortName,
465 }),
466 ),
467 ),
468 Effect.retry({
469 while: (error) =>
470 error._tag === "GCP.Compute.OrganizationSecurityPolicyNotResolved",
471 times: 8,
472 schedule: Schedule.spaced("1 second"),
473 }),
474 );
475
476const syncRules = (
477 securityPolicyId: string,
478 parentId: string,
479 observed: readonly SecurityPolicyRule[],
480 desired: readonly SecurityPolicyRule[],
481) =>
482 Effect.gen(function* () {
483 const observedByPriority = new Map<number, SecurityPolicyRule>();
484 for (const rule of observed) {
485 if (rule.priority !== undefined) {
486 observedByPriority.set(rule.priority, rule);
487 }
488 }
489 const desiredByPriority = new Map<number, SecurityPolicyRule>();
490 for (const rule of desired) {
491 if (rule.priority !== undefined) {
492 desiredByPriority.set(rule.priority, rule);
493 }
494 }
495
496 for (const [priority, rule] of desiredByPriority) {
497 const current = observedByPriority.get(priority);
498 if (current === undefined) {
499 yield* runOp(
500 securityPolicyId,
501 parentId,
502 compute.addRuleOrganizationSecurityPolicies({
503 securityPolicy: securityPolicyId,
504 body: toRuleBody(rule),
505 }),
506 { ignoreAlreadyExists: true },
507 );
508 continue;
509 }
510 if (!ruleEquals(current, rule)) {
511 yield* runOp(
512 securityPolicyId,
513 parentId,
514 compute.patchRuleOrganizationSecurityPolicies({
515 securityPolicy: securityPolicyId,
516 priority,
517 body: toRuleBody(rule),
518 }),
519 );
520 }
521 }
522
523 for (const priority of observedByPriority.keys()) {
524 if (desiredByPriority.has(priority)) continue;
525 if (priority === DEFAULT_RULE_PRIORITY) continue;
526 yield* runOp(
527 securityPolicyId,
528 parentId,
529 compute.removeRuleOrganizationSecurityPolicies({
530 securityPolicy: securityPolicyId,
531 priority,
532 }),
533 { ignoreNotFound: true },
534 ).pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
535 }
536 });
537
538export const OrganizationSecurityPolicyProvider = () =>
539 Provider.succeed(OrganizationSecurityPolicy, {
540 stables: [
541 "shortName",
542 "securityPolicyId",
543 "parent",
544 "project",
545 "type",
546 "id",
547 "selfLink",
548 "creationTimestamp",
549 ],
550
551 diff: Effect.fn(function* ({ news, olds, output }) {
552 if (!isResolved(news)) return undefined;
553 const previousName = olds?.shortName ?? output?.shortName;
554 const nextName = news.shortName ?? previousName;
555 const nameChanged =
556 previousName !== undefined &&
557 nextName !== undefined &&
558 previousName !== nextName;
559 const previousParent = olds?.parent ?? output?.parent;
560 const nextParent =
561 news.parent !== undefined
562 ? normalizeParent(news.parent)
563 : previousParent;
564 const parentChanged =
565 previousParent !== undefined &&
566 nextParent !== undefined &&
567 normalizeParent(previousParent) !== normalizeParent(nextParent);
568 const previousType = typeOf(olds?.type ?? output?.type);
569 const nextType = typeOf(news.type ?? output?.type);
570 if (nameChanged) {
571 return { action: "replace" as const, deleteFirst: false };
572 }
573 if (parentChanged || previousType !== nextType) {
574 return { action: "replace" as const, deleteFirst: true };
575 }
576 return undefined;
577 }),
578
579 read: Effect.fn(function* ({ id, olds, output }) {
580 const env = yield* GcpEnvironment.current;
581 const shortName = yield* toPhysicalName(
582 id,
583 olds?.shortName,
584 output?.shortName,
585 "policy",
586 );
587 const parent = yield* resolveParent(olds ?? {}, output).pipe(
588 Effect.catchTag(
589 "GCP.Compute.OrganizationSecurityPolicyParentRequired",
590 () => Effect.succeed(output?.parent ?? olds?.parent ?? ""),
591 ),
592 );
593 const existing = yield* observe(
594 output?.securityPolicyId,
595 parent,
596 shortName,
597 );
598 if (existing === undefined) return undefined;
599 const attrs = toAttrs(existing, env.project);
600 const { labels } = parseDescription(existing.description);
601 return (yield* hasAlchemyLabels(id, labels)) ? attrs : Unowned(attrs);
602 }),
603
604 list: () =>
605 Effect.gen(function* () {
606 const env = yield* GcpEnvironment.current;
607 const parents = yield* listParents(env.project);
608 if (parents.length === 0) return [];
609 const listed: OrganizationSecurityPolicy["Attributes"][] = [];
610 for (const parentId of parents) {
611 const chunk = yield* compute.listOrganizationSecurityPolicies
612 .items({
613 parentId,
614 maxResults: 500,
615 returnPartialSuccess: true,
616 })
617 .pipe(
618 Stream.filter((policy) => hasOwnershipMarker(policy.description)),
619 Stream.map((policy) => toAttrs(policy, env.project)),
620 Stream.runCollect,
621 Effect.map((items) => Array.from(items)),
622 Effect.catchTag("NotFound", () =>
623 Effect.succeed(
624 [] as OrganizationSecurityPolicy["Attributes"][],
625 ),
626 ),
627 );
628 listed.push(...chunk);
629 }
630 return listed;
631 }),
632
633 reconcile: Effect.fn(function* ({ id, news, output }) {
634 const env = yield* GcpEnvironment.current;
635 const shortName = yield* toPhysicalName(
636 id,
637 news.shortName,
638 output?.shortName,
639 "policy",
640 );
641 const parentId = yield* resolveParent(news, output);
642 const ownership = yield* createInternalLabels(id);
643 const desiredDescription = encodeDescription(ownership, news.description);
644
645 let current = yield* observe(
646 output?.securityPolicyId,
647 parentId,
648 shortName,
649 );
650
651 if (current === undefined) {
652 const inserted = yield* runOp(
653 shortName,
654 parentId,
655 compute.insertOrganizationSecurityPolicies({
656 parentId,
657 body: {
658 shortName,
659 description: desiredDescription,
660 type: typeOf(news.type),
661 rules: withDefaultRule(news.rules),
662 advancedOptionsConfig: news.advancedOptionsConfig,
663 adaptiveProtectionConfig: news.adaptiveProtectionConfig,
664 recaptchaOptionsConfig: news.recaptchaOptionsConfig,
665 ddosProtectionConfig: news.ddosProtectionConfig,
666 userDefinedFields: news.userDefinedFields,
667 },
668 }),
669 { ignoreAlreadyExists: true },
670 ).pipe(Effect.catchTag("Conflict", () => Effect.succeed(undefined)));
671 const createdId =
672 inserted !== undefined ? idFromOperation(inserted) : "";
673 current = yield* awaitResource(
674 createdId.length > 0 ? createdId : (output?.securityPolicyId ?? ""),
675 parentId,
676 shortName,
677 );
678 }
679
680 if (current === undefined) {
681 return yield* new OrganizationSecurityPolicyNotResolved({
682 securityPolicyId: output?.securityPolicyId ?? "",
683 shortName,
684 });
685 }
686
687 const securityPolicyId = policyIdOf(current);
688 const patch: compute.SecurityPolicy = {
689 fingerprint: current.fingerprint,
690 };
691 let needsPatch = false;
692 if ((current.description ?? "") !== desiredDescription) {
693 patch.description = desiredDescription;
694 needsPatch = true;
695 }
696 if (
697 news.advancedOptionsConfig !== undefined &&
698 !subsetEqual(current.advancedOptionsConfig, news.advancedOptionsConfig)
699 ) {
700 patch.advancedOptionsConfig = news.advancedOptionsConfig;
701 needsPatch = true;
702 }
703 if (
704 news.adaptiveProtectionConfig !== undefined &&
705 !subsetEqual(
706 current.adaptiveProtectionConfig,
707 news.adaptiveProtectionConfig,
708 )
709 ) {
710 patch.adaptiveProtectionConfig = news.adaptiveProtectionConfig;
711 needsPatch = true;
712 }
713 if (
714 news.recaptchaOptionsConfig !== undefined &&
715 !subsetEqual(
716 current.recaptchaOptionsConfig,
717 news.recaptchaOptionsConfig,
718 )
719 ) {
720 patch.recaptchaOptionsConfig = news.recaptchaOptionsConfig;
721 needsPatch = true;
722 }
723 if (
724 news.ddosProtectionConfig !== undefined &&
725 !subsetEqual(current.ddosProtectionConfig, news.ddosProtectionConfig)
726 ) {
727 patch.ddosProtectionConfig = news.ddosProtectionConfig;
728 needsPatch = true;
729 }
730 if (
731 news.userDefinedFields !== undefined &&
732 !sameJson(current.userDefinedFields ?? [], news.userDefinedFields)
733 ) {
734 patch.userDefinedFields = news.userDefinedFields;
735 needsPatch = true;
736 }
737 if (needsPatch) {
738 yield* runOp(
739 securityPolicyId,
740 parentId,
741 compute.patchOrganizationSecurityPolicies({
742 securityPolicy: securityPolicyId,
743 body: patch,
744 }),
745 );
746 current = (yield* getById(securityPolicyId)) ?? current;
747 }
748
749 const nextRules = desiredRules(news, current.rules ?? []);
750 if (nextRules !== undefined) {
751 yield* syncRules(
752 securityPolicyId,
753 parentId,
754 current.rules ?? [],
755 nextRules,
756 );
757 current = (yield* getById(securityPolicyId)) ?? current;
758 }
759
760 if (current === undefined) {
761 return yield* new OrganizationSecurityPolicyNotResolved({
762 securityPolicyId,
763 shortName,
764 });
765 }
766
767 return toAttrs(current, env.project);
768 }),
769
770 delete: Effect.fn(function* ({ output }) {
771 const securityPolicyId = output.securityPolicyId;
772 if (securityPolicyId.length === 0) return;
773 const parentId =
774 output.parent.length > 0 ? normalizeParent(output.parent) : undefined;
775 yield* compute
776 .deleteOrganizationSecurityPolicies({
777 securityPolicy: securityPolicyId,
778 })
779 .pipe(
780 Effect.flatMap((operation) =>
781 waitOrganizationOperation(operation, parentId, {
782 ignore: ["RESOURCE_NOT_FOUND"],
783 }),
784 ),
785 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
786 Effect.retry({
787 while: (error) => error._tag === "Conflict",
788 times: 8,
789 schedule: Schedule.spaced("2 seconds"),
790 }),
791 );
792 }),
793 });