@alchemy-unifi/network 0.0.1 → 0.1.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/LICENSE +21 -0
- package/README.md +92 -1
- package/dist/index.d.mts +929 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2372 -0
- package/dist/index.mjs.map +1 -0
- package/dist/rolldown-runtime-D8jQpDG7.mjs +12 -0
- package/package.json +74 -1
- package/src/AuthConfig.ts +23 -0
- package/src/AuthProvider.ts +469 -0
- package/src/Connection.ts +185 -0
- package/src/Credentials.ts +85 -0
- package/src/DnsPolicy.ts +181 -0
- package/src/FirewallPolicy.ts +456 -0
- package/src/FirewallPolicyOrdering.ts +286 -0
- package/src/FirewallZone.ts +247 -0
- package/src/HttpClient.ts +138 -0
- package/src/Lookup.ts +34 -0
- package/src/Network.ts +335 -0
- package/src/Providers.ts +59 -0
- package/src/ReadCache.ts +79 -0
- package/src/Site.ts +100 -0
- package/src/SiteManager.ts +68 -0
- package/src/State.ts +607 -0
- package/src/TrafficMatchingList.ts +250 -0
- package/src/WifiBroadcast.ts +318 -0
- package/src/index.ts +17 -0
- package/src/internal/Fields.ts +49 -0
- package/src/internal/Networks.ts +7 -0
- package/src/internal/Plan.ts +22 -0
- package/src/internal/Services.ts +19 -0
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
import * as Sdk from "@alchemy-unifi/network-sdk";
|
|
2
|
+
import { Unowned } from "alchemy/AdoptPolicy";
|
|
3
|
+
import type { Diff } from "alchemy/Diff";
|
|
4
|
+
import * as Provider from "alchemy/Provider";
|
|
5
|
+
import { Resource } from "alchemy/Resource";
|
|
6
|
+
import * as Effect from "effect/Effect";
|
|
7
|
+
import * as Stream from "effect/Stream";
|
|
8
|
+
import { withConnection } from "./Connection.ts";
|
|
9
|
+
import { type FirewallZoneReference, resolveFirewallZone } from "./FirewallZone.ts";
|
|
10
|
+
import { isCurrent, omitKeys, sortArrays } from "./internal/Fields.ts";
|
|
11
|
+
import { plannedString, UNKNOWN } from "./internal/Plan.ts";
|
|
12
|
+
import { captureServices } from "./internal/Services.ts";
|
|
13
|
+
import { describeQuery, LookupAmbiguous, LookupNotFound } from "./Lookup.ts";
|
|
14
|
+
import { type NetworkReference, resolveNetwork } from "./Network.ts";
|
|
15
|
+
import type { Providers } from "./Providers.ts";
|
|
16
|
+
import * as ReadCache from "./ReadCache.ts";
|
|
17
|
+
import { resolveSite, type Site } from "./Site.ts";
|
|
18
|
+
import { resolveTrafficMatchingList, type TrafficMatchingListReference } from "./TrafficMatchingList.ts";
|
|
19
|
+
|
|
20
|
+
/** Match the addresses or ports in a traffic-matching list (a `TrafficMatchingList` resource or reference). */
|
|
21
|
+
export interface TrafficMatchingListFilter {
|
|
22
|
+
readonly type: "TRAFFIC_MATCHING_LIST";
|
|
23
|
+
/** Match everything except the list's entries. */
|
|
24
|
+
readonly matchOpposite: boolean;
|
|
25
|
+
readonly trafficMatchingList: TrafficMatchingListReference;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** IP addresses, ranges or subnets to match: inline, or from a list. */
|
|
29
|
+
export type IpAddressFilter = Sdk.IntegrationFirewallPolicySpecificIpAddressFilterDto | TrafficMatchingListFilter;
|
|
30
|
+
|
|
31
|
+
/** Ports or port ranges to match: inline, or from a list. */
|
|
32
|
+
export type PortFilter = Sdk.IntegrationFirewallPolicyPortValueFilterDto | TrafficMatchingListFilter;
|
|
33
|
+
|
|
34
|
+
/** Networks to match (`Network` resources or references). */
|
|
35
|
+
export interface NetworkFilter {
|
|
36
|
+
/** Match every network except these. */
|
|
37
|
+
readonly matchOpposite: boolean;
|
|
38
|
+
readonly networks: ReadonlyArray<NetworkReference>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** An API traffic filter with its ID fields replaced by references. */
|
|
42
|
+
type WithReferences<F> = {
|
|
43
|
+
[K in keyof F]: K extends "ipAddressFilter"
|
|
44
|
+
? IpAddressFilter
|
|
45
|
+
: K extends "portFilter"
|
|
46
|
+
? PortFilter
|
|
47
|
+
: K extends "networkFilter"
|
|
48
|
+
? NetworkFilter
|
|
49
|
+
: F[K];
|
|
50
|
+
};
|
|
51
|
+
type Distribute<F> = F extends unknown ? WithReferences<F> : never;
|
|
52
|
+
|
|
53
|
+
/** What a policy matches at its source, besides the zone. `type` picks the kind. */
|
|
54
|
+
export type FirewallPolicySourceFilter = Distribute<Sdk.FirewallPolicySourceTrafficFilter>;
|
|
55
|
+
|
|
56
|
+
/** What a policy matches at its destination, besides the zone. `type` picks the kind. */
|
|
57
|
+
export type FirewallPolicyDestinationFilter = Distribute<Sdk.FirewallPolicyDestinationTrafficFilter>;
|
|
58
|
+
|
|
59
|
+
export interface FirewallPolicyEndpoint<Filter> {
|
|
60
|
+
/** A `FirewallZone` resource or lookup, `{ firewallZoneId }`, or a built-in constant such as `FirewallZone.INTERNAL`. */
|
|
61
|
+
readonly zone: FirewallZoneReference;
|
|
62
|
+
/** Narrows the zone's traffic. Unset matches all of it. */
|
|
63
|
+
readonly trafficFilter?: Filter;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Props are the Integration API's policy body, with zones, networks and
|
|
68
|
+
* traffic-matching lists as references, plus `site`.
|
|
69
|
+
*
|
|
70
|
+
* Props are authoritative, like every resource's: leaving out
|
|
71
|
+
* `trafficFilter`, `connectionStateFilter`, `ipsecFilter`, `schedule` or
|
|
72
|
+
* `description` means none, even if the policy has one in the console.
|
|
73
|
+
*/
|
|
74
|
+
export interface FirewallPolicyProps extends Omit<
|
|
75
|
+
Sdk.CreateFirewallPolicyRequest,
|
|
76
|
+
"siteId" | "source" | "destination"
|
|
77
|
+
> {
|
|
78
|
+
/** Site to create the policy in. @default the profile's site, else the console's only site */
|
|
79
|
+
site?: string;
|
|
80
|
+
source: FirewallPolicyEndpoint<FirewallPolicySourceFilter>;
|
|
81
|
+
destination: FirewallPolicyEndpoint<FirewallPolicyDestinationFilter>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** A policy: a `FirewallPolicy` resource, a {@link FirewallPolicy.lookup} result, or `{ firewallPolicyId }`. */
|
|
85
|
+
export interface FirewallPolicyReference {
|
|
86
|
+
readonly firewallPolicyId: string;
|
|
87
|
+
readonly siteId?: string | undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface FirewallPolicyAttributes {
|
|
91
|
+
siteId: string;
|
|
92
|
+
firewallPolicyId: string;
|
|
93
|
+
name: string;
|
|
94
|
+
sourceFirewallZoneId: string;
|
|
95
|
+
destinationFirewallZoneId: string;
|
|
96
|
+
/**
|
|
97
|
+
* The whole policy as the console reports it, so `alchemy drift` sees any
|
|
98
|
+
* change; without its `index`, which moves whenever its ordering changes.
|
|
99
|
+
*/
|
|
100
|
+
config: Omit<Sdk.FirewallPolicy, "id" | "metadata" | "index">;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface FirewallPolicy extends Resource<
|
|
104
|
+
"UnifiNetwork.FirewallPolicy",
|
|
105
|
+
FirewallPolicyProps,
|
|
106
|
+
FirewallPolicyAttributes,
|
|
107
|
+
never,
|
|
108
|
+
Providers
|
|
109
|
+
> {}
|
|
110
|
+
|
|
111
|
+
const loadPolicies = (siteId: string) => withConnection(Stream.runCollect(Sdk.getFirewallPolicies.items({ siteId })));
|
|
112
|
+
const policiesKey = ReadCache.key<ReadonlyArray<Sdk.FirewallPolicy>, Effect.Error<ReturnType<typeof loadPolicies>>>(
|
|
113
|
+
"firewallPolicies",
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* A site's firewall policies, listed once per run: all of them, including
|
|
118
|
+
* system-defined ones and ones UniFi derives from UI Objects.
|
|
119
|
+
*/
|
|
120
|
+
export const listFirewallPolicies = (siteId: string) => ReadCache.cached(policiesKey, siteId, loadPolicies(siteId));
|
|
121
|
+
|
|
122
|
+
/** Policies the user made, as opposed to system-defined ones and ones UniFi derives (from UI Objects, say), which the provider never touches. */
|
|
123
|
+
export const isUserDefined = (policy: Sdk.FirewallPolicy) => policy.metadata.origin === "USER_DEFINED";
|
|
124
|
+
|
|
125
|
+
const toAttributes = (siteId: string, policy: Sdk.FirewallPolicy): FirewallPolicyAttributes => {
|
|
126
|
+
const { id, metadata: _metadata, index: _index, ...config } = policy;
|
|
127
|
+
return {
|
|
128
|
+
siteId,
|
|
129
|
+
firewallPolicyId: id,
|
|
130
|
+
name: policy.name,
|
|
131
|
+
sourceFirewallZoneId: policy.source.zoneId,
|
|
132
|
+
destinationFirewallZoneId: policy.destination.zoneId,
|
|
133
|
+
config,
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/** Fields the console reports but rejects in request bodies, or that change without the policy being edited. */
|
|
138
|
+
const READ_ONLY = ["id", "metadata", "index"];
|
|
139
|
+
|
|
140
|
+
const resolvePortFilter = Effect.fn(function* (filter: PortFilter, site: Site) {
|
|
141
|
+
if (filter.type === "PORTS") return filter satisfies Sdk.FirewallPolicyPortFilter;
|
|
142
|
+
const trafficMatchingListId = yield* resolveTrafficMatchingList(filter.trafficMatchingList, site);
|
|
143
|
+
return {
|
|
144
|
+
type: "TRAFFIC_MATCHING_LIST",
|
|
145
|
+
matchOpposite: filter.matchOpposite,
|
|
146
|
+
trafficMatchingListId,
|
|
147
|
+
} satisfies Sdk.FirewallPolicyPortFilter;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const resolveIpAddressFilter = Effect.fn(function* (filter: IpAddressFilter, site: Site) {
|
|
151
|
+
if (filter.type === "IP_ADDRESSES") return filter satisfies Sdk.FirewallPolicyIPAddressFilter;
|
|
152
|
+
const trafficMatchingListId = yield* resolveTrafficMatchingList(filter.trafficMatchingList, site);
|
|
153
|
+
return {
|
|
154
|
+
type: "TRAFFIC_MATCHING_LIST",
|
|
155
|
+
matchOpposite: filter.matchOpposite,
|
|
156
|
+
trafficMatchingListId,
|
|
157
|
+
} satisfies Sdk.FirewallPolicyIPAddressFilter;
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const resolveNetworkFilter = (filter: NetworkFilter, site: Site) =>
|
|
161
|
+
Effect.forEach(filter.networks, (network) => resolveNetwork(network, site)).pipe(
|
|
162
|
+
Effect.map((networkIds): Sdk.FirewallPolicyNetworkFilter => ({
|
|
163
|
+
matchOpposite: filter.matchOpposite,
|
|
164
|
+
networkIds,
|
|
165
|
+
})),
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
/** `filter` with its optional `portFilter` resolved (left out when unset). */
|
|
169
|
+
const withPortFilter = Effect.fn(function* <F extends { readonly portFilter?: PortFilter }>(filter: F, site: Site) {
|
|
170
|
+
const { portFilter, ...rest } = filter;
|
|
171
|
+
if (portFilter === undefined) return rest;
|
|
172
|
+
return { ...rest, portFilter: yield* resolvePortFilter(portFilter, site) };
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const resolveSourceFilter = Effect.fn(function* (filter: FirewallPolicySourceFilter, site: Site) {
|
|
176
|
+
switch (filter.type) {
|
|
177
|
+
case "IP_ADDRESS": {
|
|
178
|
+
const { ipAddressFilter, ...rest } = yield* withPortFilter(filter, site);
|
|
179
|
+
return {
|
|
180
|
+
...rest,
|
|
181
|
+
ipAddressFilter: yield* resolveIpAddressFilter(ipAddressFilter, site),
|
|
182
|
+
} satisfies Sdk.FirewallPolicySourceTrafficFilter;
|
|
183
|
+
}
|
|
184
|
+
case "NETWORK": {
|
|
185
|
+
const { networkFilter, ...rest } = yield* withPortFilter(filter, site);
|
|
186
|
+
return {
|
|
187
|
+
...rest,
|
|
188
|
+
networkFilter: yield* resolveNetworkFilter(networkFilter, site),
|
|
189
|
+
} satisfies Sdk.FirewallPolicySourceTrafficFilter;
|
|
190
|
+
}
|
|
191
|
+
case "PORT":
|
|
192
|
+
return {
|
|
193
|
+
...filter,
|
|
194
|
+
portFilter: yield* resolvePortFilter(filter.portFilter, site),
|
|
195
|
+
} satisfies Sdk.FirewallPolicySourceTrafficFilter;
|
|
196
|
+
case "IPV6_IID":
|
|
197
|
+
return (yield* withPortFilter(filter, site)) satisfies Sdk.FirewallPolicySourceTrafficFilter;
|
|
198
|
+
case "MAC_ADDRESS":
|
|
199
|
+
return (yield* withPortFilter(filter, site)) satisfies Sdk.FirewallPolicySourceTrafficFilter;
|
|
200
|
+
case "REGION":
|
|
201
|
+
return (yield* withPortFilter(filter, site)) satisfies Sdk.FirewallPolicySourceTrafficFilter;
|
|
202
|
+
case "SITE_TO_SITE_VPN_TUNNEL":
|
|
203
|
+
return (yield* withPortFilter(filter, site)) satisfies Sdk.FirewallPolicySourceTrafficFilter;
|
|
204
|
+
case "VPN_SERVER":
|
|
205
|
+
return (yield* withPortFilter(filter, site)) satisfies Sdk.FirewallPolicySourceTrafficFilter;
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const resolveDestinationFilter = Effect.fn(function* (filter: FirewallPolicyDestinationFilter, site: Site) {
|
|
210
|
+
switch (filter.type) {
|
|
211
|
+
case "IP_ADDRESS": {
|
|
212
|
+
const { ipAddressFilter, ...rest } = yield* withPortFilter(filter, site);
|
|
213
|
+
return {
|
|
214
|
+
...rest,
|
|
215
|
+
ipAddressFilter: yield* resolveIpAddressFilter(ipAddressFilter, site),
|
|
216
|
+
} satisfies Sdk.FirewallPolicyDestinationTrafficFilter;
|
|
217
|
+
}
|
|
218
|
+
case "NETWORK": {
|
|
219
|
+
const { networkFilter, ...rest } = yield* withPortFilter(filter, site);
|
|
220
|
+
return {
|
|
221
|
+
...rest,
|
|
222
|
+
networkFilter: yield* resolveNetworkFilter(networkFilter, site),
|
|
223
|
+
} satisfies Sdk.FirewallPolicyDestinationTrafficFilter;
|
|
224
|
+
}
|
|
225
|
+
case "PORT":
|
|
226
|
+
return {
|
|
227
|
+
...filter,
|
|
228
|
+
portFilter: yield* resolvePortFilter(filter.portFilter, site),
|
|
229
|
+
} satisfies Sdk.FirewallPolicyDestinationTrafficFilter;
|
|
230
|
+
case "APPLICATION":
|
|
231
|
+
return (yield* withPortFilter(filter, site)) satisfies Sdk.FirewallPolicyDestinationTrafficFilter;
|
|
232
|
+
case "APPLICATION_CATEGORY":
|
|
233
|
+
return (yield* withPortFilter(filter, site)) satisfies Sdk.FirewallPolicyDestinationTrafficFilter;
|
|
234
|
+
case "DOMAIN":
|
|
235
|
+
return (yield* withPortFilter(filter, site)) satisfies Sdk.FirewallPolicyDestinationTrafficFilter;
|
|
236
|
+
case "IPV6_IID":
|
|
237
|
+
return (yield* withPortFilter(filter, site)) satisfies Sdk.FirewallPolicyDestinationTrafficFilter;
|
|
238
|
+
case "REGION":
|
|
239
|
+
return (yield* withPortFilter(filter, site)) satisfies Sdk.FirewallPolicyDestinationTrafficFilter;
|
|
240
|
+
case "SITE_TO_SITE_VPN_TUNNEL":
|
|
241
|
+
return (yield* withPortFilter(filter, site)) satisfies Sdk.FirewallPolicyDestinationTrafficFilter;
|
|
242
|
+
case "VPN_SERVER":
|
|
243
|
+
return (yield* withPortFilter(filter, site)) satisfies Sdk.FirewallPolicyDestinationTrafficFilter;
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
/** The request body for `props` (without `siteId`), with references resolved to IDs. */
|
|
248
|
+
const toBody = Effect.fn(function* (props: FirewallPolicyProps, site: Site) {
|
|
249
|
+
const { site: _site, source, destination, ...rest } = props;
|
|
250
|
+
const sourceFilter = source.trafficFilter;
|
|
251
|
+
const destinationFilter = destination.trafficFilter;
|
|
252
|
+
return {
|
|
253
|
+
...rest,
|
|
254
|
+
source: {
|
|
255
|
+
zoneId: yield* resolveFirewallZone(source.zone, site),
|
|
256
|
+
...(sourceFilter === undefined ? {} : { trafficFilter: yield* resolveSourceFilter(sourceFilter, site) }),
|
|
257
|
+
},
|
|
258
|
+
destination: {
|
|
259
|
+
zoneId: yield* resolveFirewallZone(destination.zone, site),
|
|
260
|
+
...(destinationFilter === undefined
|
|
261
|
+
? {}
|
|
262
|
+
: { trafficFilter: yield* resolveDestinationFilter(destinationFilter, site) }),
|
|
263
|
+
},
|
|
264
|
+
} satisfies Omit<Sdk.CreateFirewallPolicyRequest, "siteId">;
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* A policy by ID, from the run's cached list: the list returns whole
|
|
269
|
+
* policies, so a plan reading every policy costs one request, not one each
|
|
270
|
+
* (this matters through the ui.com connector's rate limit).
|
|
271
|
+
*/
|
|
272
|
+
const getPolicy = (siteId: string, firewallPolicyId: string) =>
|
|
273
|
+
listFirewallPolicies(siteId).pipe(Effect.map((policies) => policies.find((p) => p.id === firewallPolicyId)));
|
|
274
|
+
|
|
275
|
+
/** User-defined policies named `name` from `source` to `destination`. Names aren't unique, even within a zone pair. */
|
|
276
|
+
const findByName = Effect.fn(function* (
|
|
277
|
+
site: Site,
|
|
278
|
+
name: string,
|
|
279
|
+
source: FirewallZoneReference | undefined,
|
|
280
|
+
destination: FirewallZoneReference | undefined,
|
|
281
|
+
) {
|
|
282
|
+
const sourceId = source === undefined ? undefined : yield* resolveFirewallZone(source, site);
|
|
283
|
+
const destinationId = destination === undefined ? undefined : yield* resolveFirewallZone(destination, site);
|
|
284
|
+
return (yield* listFirewallPolicies(site.id)).filter(
|
|
285
|
+
(p) =>
|
|
286
|
+
isUserDefined(p) &&
|
|
287
|
+
p.name === name &&
|
|
288
|
+
(sourceId === undefined || p.source.zoneId === sourceId) &&
|
|
289
|
+
(destinationId === undefined || p.destination.zoneId === destinationId),
|
|
290
|
+
);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* A zone-based firewall policy: allow, block or reject traffic from one
|
|
295
|
+
* zone to another, optionally narrowed by traffic filters. Changing its
|
|
296
|
+
* zones updates it in place and moves it to the new zone pair's ordering
|
|
297
|
+
* (see `FirewallPolicyOrdering`).
|
|
298
|
+
*
|
|
299
|
+
* Existing user-defined policies are adopted with `--adopt` by name and zone
|
|
300
|
+
* pair (names aren't unique, so several matches adopt nothing). Policies
|
|
301
|
+
* UniFi derives, e.g. from UI Objects, are invisible: never listed, adopted
|
|
302
|
+
* or ordered.
|
|
303
|
+
*
|
|
304
|
+
* @example
|
|
305
|
+
* ```ts
|
|
306
|
+
* import * as UnifiNetwork from "@alchemy-unifi/network";
|
|
307
|
+
* import * as Alchemy from "alchemy";
|
|
308
|
+
* import * as Effect from "effect/Effect";
|
|
309
|
+
*
|
|
310
|
+
* export default Alchemy.Stack(
|
|
311
|
+
* "HomeNetwork",
|
|
312
|
+
* { providers: UnifiNetwork.providers(), state: UnifiNetwork.state() },
|
|
313
|
+
* Effect.gen(function* () {
|
|
314
|
+
* const iot = yield* UnifiNetwork.FirewallZone("iot-zone", { name: "IoT" });
|
|
315
|
+
* yield* UnifiNetwork.FirewallPolicy("block-iot-to-internal", {
|
|
316
|
+
* name: "Block IoT to Internal",
|
|
317
|
+
* enabled: true,
|
|
318
|
+
* action: { type: "BLOCK" },
|
|
319
|
+
* source: { zone: iot },
|
|
320
|
+
* destination: { zone: UnifiNetwork.FirewallZone.INTERNAL },
|
|
321
|
+
* ipProtocolScope: { ipVersion: "IPV4_AND_IPV6" },
|
|
322
|
+
* loggingEnabled: false,
|
|
323
|
+
* });
|
|
324
|
+
* }),
|
|
325
|
+
* );
|
|
326
|
+
* ```
|
|
327
|
+
*/
|
|
328
|
+
export const FirewallPolicy = Resource<FirewallPolicy>("UnifiNetwork.FirewallPolicy")({
|
|
329
|
+
/**
|
|
330
|
+
* A user-defined policy by name, optionally narrowed to a zone pair. Dies
|
|
331
|
+
* with a user-facing `LookupNotFound` or `LookupAmbiguous`: stack bodies
|
|
332
|
+
* can't fail with typed errors.
|
|
333
|
+
*/
|
|
334
|
+
lookup: Effect.fn("UnifiNetwork.FirewallPolicy.lookup")(function* (query: {
|
|
335
|
+
readonly name: string;
|
|
336
|
+
readonly source?: FirewallZoneReference;
|
|
337
|
+
readonly destination?: FirewallZoneReference;
|
|
338
|
+
readonly site?: string;
|
|
339
|
+
}) {
|
|
340
|
+
const site = yield* resolveSite(query.site);
|
|
341
|
+
const policies = yield* findByName(site, query.name, query.source, query.destination);
|
|
342
|
+
const [policy] = policies;
|
|
343
|
+
const described = describeQuery({ name: query.name });
|
|
344
|
+
if (policy === undefined) {
|
|
345
|
+
return yield* new LookupNotFound({ resource: "firewall policy", query: described, site });
|
|
346
|
+
}
|
|
347
|
+
if (policies.length > 1) {
|
|
348
|
+
return yield* new LookupAmbiguous({
|
|
349
|
+
resource: "firewall policy",
|
|
350
|
+
query: described,
|
|
351
|
+
site,
|
|
352
|
+
count: policies.length,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
return toAttributes(site.id, policy);
|
|
356
|
+
}, Effect.orDie),
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
export const FirewallPolicyProvider = () =>
|
|
360
|
+
Provider.effect(
|
|
361
|
+
FirewallPolicy,
|
|
362
|
+
Effect.gen(function* () {
|
|
363
|
+
const run = yield* captureServices;
|
|
364
|
+
|
|
365
|
+
return FirewallPolicy.Provider.of({
|
|
366
|
+
list: () =>
|
|
367
|
+
run(
|
|
368
|
+
Effect.gen(function* () {
|
|
369
|
+
const site = yield* resolveSite();
|
|
370
|
+
const policies = yield* listFirewallPolicies(site.id);
|
|
371
|
+
return policies.filter(isUserDefined).map((policy) => toAttributes(site.id, policy));
|
|
372
|
+
}),
|
|
373
|
+
),
|
|
374
|
+
|
|
375
|
+
read: ({ olds, output }) =>
|
|
376
|
+
run(
|
|
377
|
+
Effect.gen(function* () {
|
|
378
|
+
if (output !== undefined) {
|
|
379
|
+
const policy = yield* getPolicy(output.siteId, output.firewallPolicyId);
|
|
380
|
+
return policy === undefined || !isUserDefined(policy)
|
|
381
|
+
? undefined
|
|
382
|
+
: toAttributes(output.siteId, policy);
|
|
383
|
+
}
|
|
384
|
+
const site = yield* resolveSite(olds.site);
|
|
385
|
+
const matching = yield* findByName(
|
|
386
|
+
site,
|
|
387
|
+
olds.name,
|
|
388
|
+
olds.source.zone,
|
|
389
|
+
olds.destination.zone,
|
|
390
|
+
);
|
|
391
|
+
const [policy] = matching;
|
|
392
|
+
return policy === undefined || matching.length > 1
|
|
393
|
+
? undefined
|
|
394
|
+
: Unowned(toAttributes(site.id, policy));
|
|
395
|
+
}),
|
|
396
|
+
),
|
|
397
|
+
|
|
398
|
+
diff: ({ news, output }) =>
|
|
399
|
+
run(
|
|
400
|
+
Effect.gen(function* () {
|
|
401
|
+
const siteProp = plannedString(news, "site");
|
|
402
|
+
if (output === undefined || siteProp === UNKNOWN) return undefined;
|
|
403
|
+
const site = yield* resolveSite(siteProp);
|
|
404
|
+
if (site.id !== output.siteId) return { action: "replace" } satisfies Diff;
|
|
405
|
+
return undefined;
|
|
406
|
+
}),
|
|
407
|
+
),
|
|
408
|
+
|
|
409
|
+
reconcile: ({ news, output }) =>
|
|
410
|
+
run(
|
|
411
|
+
Effect.gen(function* () {
|
|
412
|
+
const site = yield* resolveSite(news.site);
|
|
413
|
+
const body = yield* toBody(news, site);
|
|
414
|
+
const observed =
|
|
415
|
+
output !== undefined && output.siteId === site.id
|
|
416
|
+
? yield* getPolicy(site.id, output.firewallPolicyId)
|
|
417
|
+
: undefined;
|
|
418
|
+
const write = ReadCache.invalidating(policiesKey, site.id);
|
|
419
|
+
if (observed === undefined) {
|
|
420
|
+
const created = yield* withConnection(
|
|
421
|
+
Sdk.createFirewallPolicy({ siteId: site.id, ...body }),
|
|
422
|
+
).pipe(write);
|
|
423
|
+
return toAttributes(site.id, created);
|
|
424
|
+
}
|
|
425
|
+
// Every list in a policy is a set (ports, addresses, networks, states), and the console reorders them.
|
|
426
|
+
if (isCurrent(sortArrays(body), sortArrays(omitKeys(observed, READ_ONLY)))) {
|
|
427
|
+
return toAttributes(site.id, observed);
|
|
428
|
+
}
|
|
429
|
+
// A full PUT of the body: optional fields it leaves out are removed.
|
|
430
|
+
const updated = yield* withConnection(
|
|
431
|
+
Sdk.updateFirewallPolicy({
|
|
432
|
+
...body,
|
|
433
|
+
siteId: site.id,
|
|
434
|
+
firewallPolicyId: observed.id,
|
|
435
|
+
}),
|
|
436
|
+
).pipe(write);
|
|
437
|
+
return toAttributes(site.id, updated);
|
|
438
|
+
}),
|
|
439
|
+
),
|
|
440
|
+
|
|
441
|
+
delete: ({ output }) =>
|
|
442
|
+
run(
|
|
443
|
+
withConnection(
|
|
444
|
+
Sdk.deleteFirewallPolicy({
|
|
445
|
+
siteId: output.siteId,
|
|
446
|
+
firewallPolicyId: output.firewallPolicyId,
|
|
447
|
+
}),
|
|
448
|
+
).pipe(
|
|
449
|
+
Effect.asVoid,
|
|
450
|
+
Effect.catchTag("NotFound", () => Effect.void),
|
|
451
|
+
ReadCache.invalidating(policiesKey, output.siteId),
|
|
452
|
+
),
|
|
453
|
+
),
|
|
454
|
+
});
|
|
455
|
+
}),
|
|
456
|
+
);
|