added schema expansion for testing
This commit is contained in:
parent
11434f667a
commit
5c9c3f33bf
555
docs/plans/2026-03-06-schema-expansion.md
Normal file
555
docs/plans/2026-03-06-schema-expansion.md
Normal file
@ -0,0 +1,555 @@
|
|||||||
|
# Schema Expansion: Full NetBird State Coverage
|
||||||
|
|
||||||
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to
|
||||||
|
> implement this plan task-by-task.
|
||||||
|
|
||||||
|
**Goal:** Expand the reconciler schema and export to cover all NetBird resource
|
||||||
|
types: posture checks, networks (with resources and routers), peers, users, and
|
||||||
|
resource-backed policies.
|
||||||
|
|
||||||
|
**Architecture:** Each new resource type follows the existing pattern: add NB
|
||||||
|
types → add schema → add to ActualState → add client methods → add diff logic →
|
||||||
|
add executor handlers → add export → add tests. Policies are extended to support
|
||||||
|
`destination_resource` as an alternative to `destinations`. The "All" group gets
|
||||||
|
hardcoded exclusion from deletion.
|
||||||
|
|
||||||
|
**Tech Stack:** Deno 2.x, TypeScript, Zod, injectable fetch for testing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Fix "All" group hardcoded exclusion + policy null-safety
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `src/reconcile/diff.ts:66-70` (add "All" name check)
|
||||||
|
- Modify: `src/reconcile/diff.ts:138-145` (null-safety for destinations)
|
||||||
|
- Modify: `src/reconcile/diff.test.ts` (add test for "All" exclusion with
|
||||||
|
`issued: "api"`)
|
||||||
|
|
||||||
|
The diff already filters `issued === "api"` but "All" has `issued: "api"` in
|
||||||
|
real environments. Add explicit name exclusion. Also guard against `null`
|
||||||
|
destinations in policy rules (resource-backed policies).
|
||||||
|
|
||||||
|
**Changes to `src/reconcile/diff.ts`:**
|
||||||
|
|
||||||
|
In `diffGroups`, line 67, change:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
if (!desiredNames.has(group.name) && group.issued === "api") {
|
||||||
|
```
|
||||||
|
|
||||||
|
to:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
if (!desiredNames.has(group.name) && group.issued === "api" && group.name !== "All") {
|
||||||
|
```
|
||||||
|
|
||||||
|
In `diffPolicies`, around line 143, wrap destinations extraction:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const actualDests = extractGroupNames(
|
||||||
|
existing.rules.flatMap((r) => r.destinations ?? []),
|
||||||
|
actual,
|
||||||
|
).sort();
|
||||||
|
```
|
||||||
|
|
||||||
|
Add test: `computeDiff does not delete "All" group even when issued is "api"`.
|
||||||
|
|
||||||
|
Run: `deno task test`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Add posture check and network types to `src/netbird/types.ts`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `src/netbird/types.ts`
|
||||||
|
|
||||||
|
Add these interfaces after the existing types:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/** Posture check as returned by GET /api/posture-checks */
|
||||||
|
export interface NbPostureCheck {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
checks: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Network as returned by GET /api/networks */
|
||||||
|
export interface NbNetwork {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
resources: string[];
|
||||||
|
routers: string[];
|
||||||
|
policies: string[];
|
||||||
|
routing_peers_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Network resource as returned by GET /api/networks/{id}/resources */
|
||||||
|
export interface NbNetworkResource {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
type: "host" | "subnet" | "domain";
|
||||||
|
address: string;
|
||||||
|
enabled: boolean;
|
||||||
|
groups: Array<
|
||||||
|
{ id: string; name: string; peers_count: number; resources_count: number }
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Network router as returned by GET /api/networks/{id}/routers */
|
||||||
|
export interface NbNetworkRouter {
|
||||||
|
id: string;
|
||||||
|
peer: string | null;
|
||||||
|
peer_groups: string[] | null;
|
||||||
|
metric: number;
|
||||||
|
masquerade: boolean;
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** User as returned by GET /api/users */
|
||||||
|
export interface NbUser {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
role: "owner" | "admin" | "user";
|
||||||
|
status: "active" | "invited" | "blocked";
|
||||||
|
auto_groups: string[];
|
||||||
|
is_service_user: boolean;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Also add `destinationResource` and `source_posture_checks` to `NbPolicy`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface NbPolicy {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
enabled: boolean;
|
||||||
|
rules: NbPolicyRule[];
|
||||||
|
source_posture_checks: string[]; // posture check IDs
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And add to `NbPolicyRule`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface NbPolicyRule {
|
||||||
|
// ... existing fields ...
|
||||||
|
destinationResource?: { id: string; type: string } | null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Run: `deno task check`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Add client methods for new resource types
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `src/netbird/client.ts`
|
||||||
|
|
||||||
|
Add sections for:
|
||||||
|
|
||||||
|
**Posture Checks:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
listPostureChecks(): Promise<NbPostureCheck[]>
|
||||||
|
createPostureCheck(data: Omit<NbPostureCheck, "id">): Promise<NbPostureCheck>
|
||||||
|
updatePostureCheck(id: string, data: Omit<NbPostureCheck, "id">): Promise<NbPostureCheck>
|
||||||
|
deletePostureCheck(id: string): Promise<void>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Networks:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
listNetworks(): Promise<NbNetwork[]>
|
||||||
|
createNetwork(data: { name: string; description?: string }): Promise<NbNetwork>
|
||||||
|
updateNetwork(id: string, data: { name: string; description?: string }): Promise<NbNetwork>
|
||||||
|
deleteNetwork(id: string): Promise<void>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Network Resources (nested under network):**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
listNetworkResources(networkId: string): Promise<NbNetworkResource[]>
|
||||||
|
createNetworkResource(networkId: string, data: { name: string; description?: string; address: string; enabled: boolean; groups: string[] }): Promise<NbNetworkResource>
|
||||||
|
updateNetworkResource(networkId: string, resourceId: string, data: { name: string; description?: string; address: string; enabled: boolean; groups: string[] }): Promise<NbNetworkResource>
|
||||||
|
deleteNetworkResource(networkId: string, resourceId: string): Promise<void>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Network Routers:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
listNetworkRouters(networkId: string): Promise<NbNetworkRouter[]>
|
||||||
|
createNetworkRouter(networkId: string, data: Omit<NbNetworkRouter, "id">): Promise<NbNetworkRouter>
|
||||||
|
updateNetworkRouter(networkId: string, routerId: string, data: Omit<NbNetworkRouter, "id">): Promise<NbNetworkRouter>
|
||||||
|
deleteNetworkRouter(networkId: string, routerId: string): Promise<void>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Users:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
listUsers(): Promise<NbUser[]>
|
||||||
|
createUser(data: { email: string; name?: string; role: string; auto_groups: string[]; is_service_user: boolean }): Promise<NbUser>
|
||||||
|
updateUser(id: string, data: { name?: string; role?: string; auto_groups?: string[] }): Promise<NbUser>
|
||||||
|
deleteUser(id: string): Promise<void>
|
||||||
|
```
|
||||||
|
|
||||||
|
Run: `deno task check`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Expand ActualState with new resource collections
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `src/state/actual.ts`
|
||||||
|
|
||||||
|
Add to `ActualState` interface:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
postureChecks: NbPostureCheck[];
|
||||||
|
postureChecksByName: Map<string, NbPostureCheck>;
|
||||||
|
networks: NbNetwork[];
|
||||||
|
networksByName: Map<string, NbNetwork>;
|
||||||
|
networkResources: Map<string, NbNetworkResource[]>; // networkId -> resources
|
||||||
|
networkRouters: Map<string, NbNetworkRouter[]>; // networkId -> routers
|
||||||
|
users: NbUser[];
|
||||||
|
usersByEmail: Map<string, NbUser>;
|
||||||
|
```
|
||||||
|
|
||||||
|
Expand `ClientLike` to include:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
| "listPostureChecks"
|
||||||
|
| "listNetworks"
|
||||||
|
| "listNetworkResources"
|
||||||
|
| "listNetworkRouters"
|
||||||
|
| "listUsers"
|
||||||
|
```
|
||||||
|
|
||||||
|
In `fetchActualState`: fetch posture checks, networks, users in the initial
|
||||||
|
`Promise.all`. Then for each network, fetch its resources and routers in a
|
||||||
|
second parallel batch.
|
||||||
|
|
||||||
|
Run: `deno task check`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Expand the Zod schema with new resource types
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `src/state/schema.ts`
|
||||||
|
|
||||||
|
Add schemas:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const PostureCheckSchema = z.object({
|
||||||
|
description: z.string().default(""),
|
||||||
|
checks: z.record(z.string(), z.unknown()),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const NetworkResourceSchema = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
description: z.string().default(""),
|
||||||
|
type: z.enum(["host", "subnet", "domain"]),
|
||||||
|
address: z.string(),
|
||||||
|
enabled: z.boolean().default(true),
|
||||||
|
groups: z.array(z.string()),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const NetworkRouterSchema = z.object({
|
||||||
|
peer: z.string().optional(),
|
||||||
|
peer_groups: z.array(z.string()).optional(),
|
||||||
|
metric: z.number().int().min(1).max(9999).default(9999),
|
||||||
|
masquerade: z.boolean().default(true),
|
||||||
|
enabled: z.boolean().default(true),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const NetworkSchema = z.object({
|
||||||
|
description: z.string().default(""),
|
||||||
|
resources: z.array(NetworkResourceSchema).default([]),
|
||||||
|
routers: z.array(NetworkRouterSchema).default([]),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const PeerSchema = z.object({
|
||||||
|
groups: z.array(z.string()),
|
||||||
|
login_expiration_enabled: z.boolean().default(false),
|
||||||
|
inactivity_expiration_enabled: z.boolean().default(false),
|
||||||
|
ssh_enabled: z.boolean().default(false),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UserSchema = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
role: z.enum(["owner", "admin", "user"]),
|
||||||
|
auto_groups: z.array(z.string()).default([]),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Extend `PolicySchema` to support `destination_resource`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const DestinationResourceSchema = z.object({
|
||||||
|
id: z.string(), // resource name, resolved at reconcile time
|
||||||
|
type: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const PolicySchema = z.object({
|
||||||
|
description: z.string().default(""),
|
||||||
|
enabled: z.boolean(),
|
||||||
|
sources: z.array(z.string()),
|
||||||
|
destinations: z.array(z.string()).default([]),
|
||||||
|
destination_resource: DestinationResourceSchema.optional(),
|
||||||
|
bidirectional: z.boolean(),
|
||||||
|
protocol: z.enum(["tcp", "udp", "icmp", "all"]).default("all"),
|
||||||
|
action: z.enum(["accept", "drop"]).default("accept"),
|
||||||
|
ports: z.array(z.string()).optional(),
|
||||||
|
source_posture_checks: z.array(z.string()).default([]),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Add to `DesiredStateSchema`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const DesiredStateSchema = z.object({
|
||||||
|
groups: z.record(z.string(), GroupSchema),
|
||||||
|
setup_keys: z.record(z.string(), SetupKeySchema),
|
||||||
|
policies: z.record(z.string(), PolicySchema).default({}),
|
||||||
|
posture_checks: z.record(z.string(), PostureCheckSchema).default({}),
|
||||||
|
networks: z.record(z.string(), NetworkSchema).default({}),
|
||||||
|
peers: z.record(z.string(), PeerSchema).default({}),
|
||||||
|
users: z.record(z.string(), UserSchema).default({}),
|
||||||
|
routes: z.record(z.string(), RouteSchema).default({}),
|
||||||
|
dns: z.object({
|
||||||
|
nameserver_groups: z.record(z.string(), DnsNameserverGroupSchema).default(
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
}).default({ nameserver_groups: {} }),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Update `validateCrossReferences` to also check:
|
||||||
|
|
||||||
|
- Peer groups reference existing groups
|
||||||
|
- User auto_groups reference existing groups
|
||||||
|
- Network resource groups reference existing groups
|
||||||
|
- Policy source_posture_checks reference existing posture checks
|
||||||
|
- Policy destination_resource.id references an existing network resource name
|
||||||
|
|
||||||
|
Run: `deno task check`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: Add operations for new resource types
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `src/reconcile/operations.ts`
|
||||||
|
|
||||||
|
Add to `OperationType`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
| "create_posture_check" | "update_posture_check" | "delete_posture_check"
|
||||||
|
| "create_network" | "update_network" | "delete_network"
|
||||||
|
| "create_network_resource" | "update_network_resource" | "delete_network_resource"
|
||||||
|
| "create_network_router" | "update_network_router" | "delete_network_router"
|
||||||
|
| "create_user" | "update_user" | "delete_user"
|
||||||
|
| "update_peer"
|
||||||
|
```
|
||||||
|
|
||||||
|
Update `EXECUTION_ORDER` — networks must be created before resources/routers,
|
||||||
|
posture checks before policies that reference them:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const EXECUTION_ORDER: OperationType[] = [
|
||||||
|
"create_posture_check",
|
||||||
|
"update_posture_check",
|
||||||
|
"create_group",
|
||||||
|
"update_group",
|
||||||
|
"create_setup_key",
|
||||||
|
"rename_peer",
|
||||||
|
"update_peer_groups",
|
||||||
|
"update_peer",
|
||||||
|
"create_network",
|
||||||
|
"update_network",
|
||||||
|
"create_network_resource",
|
||||||
|
"update_network_resource",
|
||||||
|
"create_network_router",
|
||||||
|
"update_network_router",
|
||||||
|
"create_user",
|
||||||
|
"update_user",
|
||||||
|
"create_policy",
|
||||||
|
"update_policy",
|
||||||
|
"create_route",
|
||||||
|
"update_route",
|
||||||
|
"create_dns",
|
||||||
|
"update_dns",
|
||||||
|
// Deletions in reverse dependency order
|
||||||
|
"delete_dns",
|
||||||
|
"delete_route",
|
||||||
|
"delete_policy",
|
||||||
|
"delete_user",
|
||||||
|
"delete_network_router",
|
||||||
|
"delete_network_resource",
|
||||||
|
"delete_network",
|
||||||
|
"delete_peer",
|
||||||
|
"delete_setup_key",
|
||||||
|
"delete_posture_check",
|
||||||
|
"delete_group",
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
Run: `deno task check`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 7: Add diff logic for new resource types
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `src/reconcile/diff.ts`
|
||||||
|
|
||||||
|
Add `diffPostureChecks`, `diffNetworks`, `diffPeers`, `diffUsers` functions and
|
||||||
|
call them from `computeDiff`.
|
||||||
|
|
||||||
|
**Posture checks:** Compare by name. Create if missing. Update if `checks`
|
||||||
|
object or description changed (deep JSON compare). Delete if not in desired.
|
||||||
|
|
||||||
|
**Networks:** Compare by name. Create network if missing. For each network, diff
|
||||||
|
resources and routers:
|
||||||
|
|
||||||
|
- Resources: match by name within the network. Create/update/delete.
|
||||||
|
- Routers: match by peer name (or peer_group). Create/update/delete.
|
||||||
|
|
||||||
|
**Peers:** Compare by name. Only update operations (never create/delete).
|
||||||
|
Compare `groups` (excluding "All"), `login_expiration_enabled`,
|
||||||
|
`inactivity_expiration_enabled`, `ssh_enabled`.
|
||||||
|
|
||||||
|
**Users:** Compare by email. Create if missing. Update if role or auto_groups
|
||||||
|
changed. Delete if not in desired (but never delete "owner" role).
|
||||||
|
|
||||||
|
**Policies update:** Handle `destination_resource` — when present, skip
|
||||||
|
group-based destination comparison. Handle `source_posture_checks`.
|
||||||
|
|
||||||
|
Run: `deno task check`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 8: Add executor handlers for new operations
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `src/reconcile/executor.ts`
|
||||||
|
|
||||||
|
Add `case` handlers in `executeSingle` for all new operation types. Network
|
||||||
|
operations need special handling: resources and routers reference the network
|
||||||
|
ID, which may be newly created. Track `createdNetworkIds` similar to
|
||||||
|
`createdGroupIds`.
|
||||||
|
|
||||||
|
Posture check operations: create/update/delete via client methods. Track
|
||||||
|
`createdPostureCheckIds`.
|
||||||
|
|
||||||
|
User operations: resolve `auto_groups` names to IDs.
|
||||||
|
|
||||||
|
Network resource operations: resolve `groups` names to IDs.
|
||||||
|
|
||||||
|
Network router operations: resolve `peer` name to peer ID, or `peer_groups`
|
||||||
|
names to group IDs.
|
||||||
|
|
||||||
|
Update `ExecutorClient` type to include all new client methods.
|
||||||
|
|
||||||
|
Run: `deno task check`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 9: Update export to cover new resource types
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `src/export.ts`
|
||||||
|
|
||||||
|
Add `exportPostureChecks`, `exportNetworks`, `exportPeers`, `exportUsers`
|
||||||
|
functions.
|
||||||
|
|
||||||
|
**Posture checks:** Keyed by name. Pass through `checks` object as-is. Include
|
||||||
|
`description`.
|
||||||
|
|
||||||
|
**Networks:** Keyed by name. For each network, fetch resources and routers from
|
||||||
|
ActualState maps. Resources: resolve group IDs to names. Routers: resolve peer
|
||||||
|
ID to peer name (via `actual.peersById`), resolve peer_group IDs to group names.
|
||||||
|
|
||||||
|
**Peers:** Keyed by peer name. Include groups (resolved to names, excluding
|
||||||
|
"All"), `login_expiration_enabled`, `inactivity_expiration_enabled`,
|
||||||
|
`ssh_enabled`.
|
||||||
|
|
||||||
|
**Users:** Keyed by email. Include name, role, auto_groups (resolved to names).
|
||||||
|
|
||||||
|
**Policies:** Handle `destinationResource` — resolve resource ID to resource
|
||||||
|
name. Include `source_posture_checks` resolved to posture check names.
|
||||||
|
|
||||||
|
Update the `exportState` return to include all new sections.
|
||||||
|
|
||||||
|
Run: `deno task check`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 10: Export the three environments to state/*.json
|
||||||
|
|
||||||
|
Run the export against all three production NetBird instances:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p state
|
||||||
|
deno task export -- --netbird-api-url https://dev.netbird.achilles-rnd.cc/api --netbird-api-token <DEV_TOKEN> > state/dev.json
|
||||||
|
deno task export -- --netbird-api-url https://achilles-rnd.cc/api --netbird-api-token <PROD_TOKEN> > state/prod.json
|
||||||
|
deno task export -- --netbird-api-url https://ext.netbird.achilles-rnd.cc/api --netbird-api-token <EXT_TOKEN> > state/ext.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify each file parses with the updated schema. Visually inspect for
|
||||||
|
completeness against dashboards.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 11: Update tests
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `src/reconcile/diff.test.ts` — tests for new diff functions
|
||||||
|
- Modify: `src/reconcile/executor.test.ts` — tests for new executor cases
|
||||||
|
- Modify: `src/export.test.ts` — tests for new export functions
|
||||||
|
- Modify: `src/state/schema.test.ts` — tests for new schema validation
|
||||||
|
- Modify: `src/state/actual.test.ts` — tests for expanded fetchActualState
|
||||||
|
- Modify: `src/integration.test.ts` — update mock data to include new resource
|
||||||
|
types
|
||||||
|
|
||||||
|
All existing tests must continue to pass. New tests should cover:
|
||||||
|
|
||||||
|
- Posture check CRUD diff/execute
|
||||||
|
- Network with resources and routers diff/execute
|
||||||
|
- Peer update diff (group changes, setting changes)
|
||||||
|
- User CRUD diff/execute
|
||||||
|
- Policy with destination_resource (export and diff)
|
||||||
|
- Policy with source_posture_checks (export and diff)
|
||||||
|
- Export of all new resource types
|
||||||
|
|
||||||
|
Run: `deno task test` — all tests must pass.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 12: Final verification
|
||||||
|
|
||||||
|
Run full quality gate:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
deno task check # type check
|
||||||
|
deno fmt --check # formatting
|
||||||
|
deno task test # all tests
|
||||||
|
```
|
||||||
|
|
||||||
|
All must pass.
|
||||||
@ -4,10 +4,15 @@ import type { ActualState } from "./state/actual.ts";
|
|||||||
import type {
|
import type {
|
||||||
NbDnsNameserverGroup,
|
NbDnsNameserverGroup,
|
||||||
NbGroup,
|
NbGroup,
|
||||||
|
NbNetwork,
|
||||||
|
NbNetworkResource,
|
||||||
|
NbNetworkRouter,
|
||||||
NbPeer,
|
NbPeer,
|
||||||
NbPolicy,
|
NbPolicy,
|
||||||
|
NbPostureCheck,
|
||||||
NbRoute,
|
NbRoute,
|
||||||
NbSetupKey,
|
NbSetupKey,
|
||||||
|
NbUser,
|
||||||
} from "./netbird/types.ts";
|
} from "./netbird/types.ts";
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@ -22,6 +27,11 @@ function buildActualState(data: {
|
|||||||
policies?: NbPolicy[];
|
policies?: NbPolicy[];
|
||||||
routes?: NbRoute[];
|
routes?: NbRoute[];
|
||||||
dns?: NbDnsNameserverGroup[];
|
dns?: NbDnsNameserverGroup[];
|
||||||
|
postureChecks?: NbPostureCheck[];
|
||||||
|
networks?: NbNetwork[];
|
||||||
|
networkResources?: Map<string, NbNetworkResource[]>;
|
||||||
|
networkRouters?: Map<string, NbNetworkRouter[]>;
|
||||||
|
users?: NbUser[];
|
||||||
}): ActualState {
|
}): ActualState {
|
||||||
const groups = data.groups ?? [];
|
const groups = data.groups ?? [];
|
||||||
const setupKeys = data.setupKeys ?? [];
|
const setupKeys = data.setupKeys ?? [];
|
||||||
@ -29,6 +39,9 @@ function buildActualState(data: {
|
|||||||
const policies = data.policies ?? [];
|
const policies = data.policies ?? [];
|
||||||
const routes = data.routes ?? [];
|
const routes = data.routes ?? [];
|
||||||
const dns = data.dns ?? [];
|
const dns = data.dns ?? [];
|
||||||
|
const postureChecks = data.postureChecks ?? [];
|
||||||
|
const networks = data.networks ?? [];
|
||||||
|
const users = data.users ?? [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
groups,
|
groups,
|
||||||
@ -45,6 +58,14 @@ function buildActualState(data: {
|
|||||||
routesByNetworkId: new Map(routes.map((r) => [r.network_id, r])),
|
routesByNetworkId: new Map(routes.map((r) => [r.network_id, r])),
|
||||||
dns,
|
dns,
|
||||||
dnsByName: new Map(dns.map((d) => [d.name, d])),
|
dnsByName: new Map(dns.map((d) => [d.name, d])),
|
||||||
|
postureChecks,
|
||||||
|
postureChecksByName: new Map(postureChecks.map((pc) => [pc.name, pc])),
|
||||||
|
networks,
|
||||||
|
networksByName: new Map(networks.map((n) => [n.name, n])),
|
||||||
|
networkResources: data.networkResources ?? new Map(),
|
||||||
|
networkRouters: data.networkRouters ?? new Map(),
|
||||||
|
users,
|
||||||
|
usersByEmail: new Map(users.map((u) => [u.email, u])),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,6 +126,7 @@ Deno.test("exportState: normal state with groups, keys, and policy", () => {
|
|||||||
name: "allow-pilot-vehicle",
|
name: "allow-pilot-vehicle",
|
||||||
description: "pilot to vehicle",
|
description: "pilot to vehicle",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
source_posture_checks: [],
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
name: "rule1",
|
name: "rule1",
|
||||||
@ -342,6 +364,7 @@ Deno.test("exportState: policies with empty rules are skipped", () => {
|
|||||||
name: "empty-policy",
|
name: "empty-policy",
|
||||||
description: "no rules",
|
description: "no rules",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
source_posture_checks: [],
|
||||||
rules: [],
|
rules: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -362,6 +385,7 @@ Deno.test("exportState: policy sources/destinations as {id,name} objects are res
|
|||||||
name: "object-refs",
|
name: "object-refs",
|
||||||
description: "",
|
description: "",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
source_posture_checks: [],
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
name: "r1",
|
name: "r1",
|
||||||
@ -399,6 +423,7 @@ Deno.test("exportState: policy without ports omits the ports field", () => {
|
|||||||
name: "no-ports",
|
name: "no-ports",
|
||||||
description: "",
|
description: "",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
source_posture_checks: [],
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
name: "r1",
|
name: "r1",
|
||||||
|
|||||||
175
src/export.ts
175
src/export.ts
@ -28,15 +28,41 @@ const DEFAULT_EXPIRES_IN = 604800;
|
|||||||
* - Routes: keyed by `network_id`. Peer groups and distribution groups
|
* - Routes: keyed by `network_id`. Peer groups and distribution groups
|
||||||
* resolved from IDs to names.
|
* resolved from IDs to names.
|
||||||
* - DNS: group IDs resolved to names.
|
* - DNS: group IDs resolved to names.
|
||||||
|
* - Posture checks: keyed by name, checks object passed through.
|
||||||
|
* - Networks: keyed by name, resources and routers resolved.
|
||||||
|
* - Peers: keyed by name, groups resolved (excluding "All").
|
||||||
|
* - Users: keyed by email, auto_groups resolved.
|
||||||
*/
|
*/
|
||||||
export function exportState(actual: ActualState): DesiredState {
|
export function exportState(actual: ActualState): DesiredState {
|
||||||
const idToName = buildIdToNameMap(actual);
|
const idToName = buildIdToNameMap(actual);
|
||||||
const setupKeyNames = new Set(actual.setupKeys.map((k) => k.name));
|
const setupKeyNames = new Set(actual.setupKeys.map((k) => k.name));
|
||||||
|
|
||||||
|
// Build resource ID → name map from all network resources
|
||||||
|
const resourceIdToName = new Map<string, string>();
|
||||||
|
for (const resources of actual.networkResources.values()) {
|
||||||
|
for (const res of resources) {
|
||||||
|
resourceIdToName.set(res.id, res.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build posture check ID → name map
|
||||||
|
const postureCheckIdToName = new Map<string, string>(
|
||||||
|
actual.postureChecks.map((pc) => [pc.id, pc.name]),
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
groups: exportGroups(actual, setupKeyNames, idToName),
|
groups: exportGroups(actual, setupKeyNames, idToName),
|
||||||
setup_keys: exportSetupKeys(actual, idToName),
|
setup_keys: exportSetupKeys(actual, idToName),
|
||||||
policies: exportPolicies(actual, idToName),
|
policies: exportPolicies(
|
||||||
|
actual,
|
||||||
|
idToName,
|
||||||
|
resourceIdToName,
|
||||||
|
postureCheckIdToName,
|
||||||
|
),
|
||||||
|
posture_checks: exportPostureChecks(actual),
|
||||||
|
networks: exportNetworks(actual, idToName),
|
||||||
|
peers: exportPeers(actual, idToName),
|
||||||
|
users: exportUsers(actual, idToName),
|
||||||
routes: exportRoutes(actual, idToName),
|
routes: exportRoutes(actual, idToName),
|
||||||
dns: {
|
dns: {
|
||||||
nameserver_groups: exportDns(actual, idToName),
|
nameserver_groups: exportDns(actual, idToName),
|
||||||
@ -89,7 +115,7 @@ function exportGroups(
|
|||||||
|
|
||||||
// Only include peers whose name matches a known setup key, since
|
// Only include peers whose name matches a known setup key, since
|
||||||
// the desired-state schema models peers as setup-key references.
|
// the desired-state schema models peers as setup-key references.
|
||||||
const peers = group.peers
|
const peers = (group.peers ?? [])
|
||||||
.map((p) => p.name)
|
.map((p) => p.name)
|
||||||
.filter((name) => setupKeyNames.has(name));
|
.filter((name) => setupKeyNames.has(name));
|
||||||
|
|
||||||
@ -143,6 +169,8 @@ function isEnrolled(usedTimes: number, usageLimit: number): boolean {
|
|||||||
function exportPolicies(
|
function exportPolicies(
|
||||||
actual: ActualState,
|
actual: ActualState,
|
||||||
idToName: Map<string, string>,
|
idToName: Map<string, string>,
|
||||||
|
resourceIdToName: Map<string, string>,
|
||||||
|
postureCheckIdToName: Map<string, string>,
|
||||||
): DesiredState["policies"] {
|
): DesiredState["policies"] {
|
||||||
const result: DesiredState["policies"] = {};
|
const result: DesiredState["policies"] = {};
|
||||||
|
|
||||||
@ -151,11 +179,7 @@ function exportPolicies(
|
|||||||
|
|
||||||
const rule = policy.rules[0];
|
const rule = policy.rules[0];
|
||||||
const sources = resolveIds(
|
const sources = resolveIds(
|
||||||
rule.sources.map(extractGroupId),
|
(rule.sources ?? []).map(extractGroupId),
|
||||||
idToName,
|
|
||||||
);
|
|
||||||
const destinations = resolveIds(
|
|
||||||
rule.destinations.map(extractGroupId),
|
|
||||||
idToName,
|
idToName,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -163,12 +187,32 @@ function exportPolicies(
|
|||||||
description: policy.description,
|
description: policy.description,
|
||||||
enabled: policy.enabled,
|
enabled: policy.enabled,
|
||||||
sources,
|
sources,
|
||||||
destinations,
|
destinations: [],
|
||||||
bidirectional: rule.bidirectional,
|
bidirectional: rule.bidirectional,
|
||||||
protocol: rule.protocol,
|
protocol: rule.protocol,
|
||||||
action: rule.action,
|
action: rule.action,
|
||||||
|
source_posture_checks: resolveIds(
|
||||||
|
policy.source_posture_checks ?? [],
|
||||||
|
postureCheckIdToName,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle destination_resource vs group-based destinations
|
||||||
|
if (rule.destinationResource) {
|
||||||
|
const resourceName = resourceIdToName.get(
|
||||||
|
rule.destinationResource.id,
|
||||||
|
);
|
||||||
|
entry.destination_resource = {
|
||||||
|
id: resourceName ?? rule.destinationResource.id,
|
||||||
|
type: rule.destinationResource.type,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
entry.destinations = resolveIds(
|
||||||
|
(rule.destinations ?? []).map(extractGroupId),
|
||||||
|
idToName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (rule.ports && rule.ports.length > 0) {
|
if (rule.ports && rule.ports.length > 0) {
|
||||||
entry.ports = rule.ports;
|
entry.ports = rule.ports;
|
||||||
}
|
}
|
||||||
@ -179,6 +223,121 @@ function exportPolicies(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Posture Checks
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function exportPostureChecks(
|
||||||
|
actual: ActualState,
|
||||||
|
): DesiredState["posture_checks"] {
|
||||||
|
const result: DesiredState["posture_checks"] = {};
|
||||||
|
|
||||||
|
for (const pc of actual.postureChecks) {
|
||||||
|
result[pc.name] = {
|
||||||
|
description: pc.description,
|
||||||
|
checks: pc.checks,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Networks
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function exportNetworks(
|
||||||
|
actual: ActualState,
|
||||||
|
idToName: Map<string, string>,
|
||||||
|
): DesiredState["networks"] {
|
||||||
|
const result: DesiredState["networks"] = {};
|
||||||
|
|
||||||
|
for (const network of actual.networks) {
|
||||||
|
const resources = actual.networkResources.get(network.id) ?? [];
|
||||||
|
const routers = actual.networkRouters.get(network.id) ?? [];
|
||||||
|
|
||||||
|
result[network.name] = {
|
||||||
|
description: network.description,
|
||||||
|
resources: resources.map((res) => ({
|
||||||
|
name: res.name,
|
||||||
|
description: res.description,
|
||||||
|
type: res.type,
|
||||||
|
address: res.address,
|
||||||
|
enabled: res.enabled,
|
||||||
|
groups: res.groups.map((g) => {
|
||||||
|
// Resource groups are objects with id/name — use the idToName map
|
||||||
|
// for consistency, falling back to the embedded name.
|
||||||
|
return idToName.get(g.id) ?? g.name;
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
routers: routers.map((router) => {
|
||||||
|
const entry: DesiredState["networks"][string]["routers"][number] = {
|
||||||
|
metric: router.metric,
|
||||||
|
masquerade: router.masquerade,
|
||||||
|
enabled: router.enabled,
|
||||||
|
};
|
||||||
|
if (router.peer) {
|
||||||
|
const peer = actual.peersById.get(router.peer);
|
||||||
|
entry.peer = peer ? peer.name : router.peer;
|
||||||
|
}
|
||||||
|
if (router.peer_groups && router.peer_groups.length > 0) {
|
||||||
|
entry.peer_groups = resolveIds(router.peer_groups, idToName);
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Peers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function exportPeers(
|
||||||
|
actual: ActualState,
|
||||||
|
idToName: Map<string, string>,
|
||||||
|
): DesiredState["peers"] {
|
||||||
|
const result: DesiredState["peers"] = {};
|
||||||
|
|
||||||
|
for (const peer of actual.peers) {
|
||||||
|
const groups = peer.groups
|
||||||
|
.filter((g) => g.name !== "All")
|
||||||
|
.map((g) => idToName.get(g.id) ?? g.name);
|
||||||
|
|
||||||
|
result[peer.name] = {
|
||||||
|
groups,
|
||||||
|
login_expiration_enabled: peer.login_expiration_enabled,
|
||||||
|
inactivity_expiration_enabled: peer.inactivity_expiration_enabled,
|
||||||
|
ssh_enabled: peer.ssh_enabled,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Users
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function exportUsers(
|
||||||
|
actual: ActualState,
|
||||||
|
idToName: Map<string, string>,
|
||||||
|
): DesiredState["users"] {
|
||||||
|
const result: DesiredState["users"] = {};
|
||||||
|
|
||||||
|
for (const user of actual.users) {
|
||||||
|
result[user.email] = {
|
||||||
|
name: user.name,
|
||||||
|
role: user.role,
|
||||||
|
auto_groups: resolveIds(user.auto_groups, idToName),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Routes
|
// Routes
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@ -369,6 +369,11 @@ function createExportMockFetch(calls: ApiCall[]) {
|
|||||||
if (method === "GET" && path === "/dns/nameservers") {
|
if (method === "GET" && path === "/dns/nameservers") {
|
||||||
return Response.json([]);
|
return Response.json([]);
|
||||||
}
|
}
|
||||||
|
if (method === "GET" && path === "/posture-checks") {
|
||||||
|
return Response.json([]);
|
||||||
|
}
|
||||||
|
if (method === "GET" && path === "/networks") return Response.json([]);
|
||||||
|
if (method === "GET" && path === "/users") return Response.json([]);
|
||||||
if (method === "GET" && path === "/events/audit") {
|
if (method === "GET" && path === "/events/audit") {
|
||||||
return Response.json([]);
|
return Response.json([]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,10 +2,15 @@ import type {
|
|||||||
NbDnsNameserverGroup,
|
NbDnsNameserverGroup,
|
||||||
NbEvent,
|
NbEvent,
|
||||||
NbGroup,
|
NbGroup,
|
||||||
|
NbNetwork,
|
||||||
|
NbNetworkResource,
|
||||||
|
NbNetworkRouter,
|
||||||
NbPeer,
|
NbPeer,
|
||||||
NbPolicy,
|
NbPolicy,
|
||||||
|
NbPostureCheck,
|
||||||
NbRoute,
|
NbRoute,
|
||||||
NbSetupKey,
|
NbSetupKey,
|
||||||
|
NbUser,
|
||||||
} from "./types.ts";
|
} from "./types.ts";
|
||||||
|
|
||||||
/** Narrowed fetch signature used for dependency injection. */
|
/** Narrowed fetch signature used for dependency injection. */
|
||||||
@ -142,6 +147,7 @@ export class NetbirdClient {
|
|||||||
name?: string;
|
name?: string;
|
||||||
ssh_enabled?: boolean;
|
ssh_enabled?: boolean;
|
||||||
login_expiration_enabled?: boolean;
|
login_expiration_enabled?: boolean;
|
||||||
|
inactivity_expiration_enabled?: boolean;
|
||||||
},
|
},
|
||||||
): Promise<NbPeer> {
|
): Promise<NbPeer> {
|
||||||
return this.request("PUT", `/peers/${id}`, data);
|
return this.request("PUT", `/peers/${id}`, data);
|
||||||
@ -223,4 +229,169 @@ export class NetbirdClient {
|
|||||||
listEvents(): Promise<NbEvent[]> {
|
listEvents(): Promise<NbEvent[]> {
|
||||||
return this.request("GET", "/events/audit");
|
return this.request("GET", "/events/audit");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Posture Checks
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
listPostureChecks(): Promise<NbPostureCheck[]> {
|
||||||
|
return this.request("GET", "/posture-checks");
|
||||||
|
}
|
||||||
|
|
||||||
|
createPostureCheck(
|
||||||
|
data: Omit<NbPostureCheck, "id">,
|
||||||
|
): Promise<NbPostureCheck> {
|
||||||
|
return this.request("POST", "/posture-checks", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePostureCheck(
|
||||||
|
id: string,
|
||||||
|
data: Omit<NbPostureCheck, "id">,
|
||||||
|
): Promise<NbPostureCheck> {
|
||||||
|
return this.request("PUT", `/posture-checks/${id}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
deletePostureCheck(id: string): Promise<void> {
|
||||||
|
return this.request("DELETE", `/posture-checks/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Networks
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
listNetworks(): Promise<NbNetwork[]> {
|
||||||
|
return this.request("GET", "/networks");
|
||||||
|
}
|
||||||
|
|
||||||
|
createNetwork(
|
||||||
|
data: { name: string; description?: string },
|
||||||
|
): Promise<NbNetwork> {
|
||||||
|
return this.request("POST", "/networks", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateNetwork(
|
||||||
|
id: string,
|
||||||
|
data: { name: string; description?: string },
|
||||||
|
): Promise<NbNetwork> {
|
||||||
|
return this.request("PUT", `/networks/${id}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteNetwork(id: string): Promise<void> {
|
||||||
|
return this.request("DELETE", `/networks/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Network Resources
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
listNetworkResources(networkId: string): Promise<NbNetworkResource[]> {
|
||||||
|
return this.request("GET", `/networks/${networkId}/resources`);
|
||||||
|
}
|
||||||
|
|
||||||
|
createNetworkResource(
|
||||||
|
networkId: string,
|
||||||
|
data: {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
address: string;
|
||||||
|
enabled: boolean;
|
||||||
|
groups: string[];
|
||||||
|
},
|
||||||
|
): Promise<NbNetworkResource> {
|
||||||
|
return this.request("POST", `/networks/${networkId}/resources`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateNetworkResource(
|
||||||
|
networkId: string,
|
||||||
|
resourceId: string,
|
||||||
|
data: {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
address: string;
|
||||||
|
enabled: boolean;
|
||||||
|
groups: string[];
|
||||||
|
},
|
||||||
|
): Promise<NbNetworkResource> {
|
||||||
|
return this.request(
|
||||||
|
"PUT",
|
||||||
|
`/networks/${networkId}/resources/${resourceId}`,
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteNetworkResource(
|
||||||
|
networkId: string,
|
||||||
|
resourceId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
return this.request(
|
||||||
|
"DELETE",
|
||||||
|
`/networks/${networkId}/resources/${resourceId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Network Routers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
listNetworkRouters(networkId: string): Promise<NbNetworkRouter[]> {
|
||||||
|
return this.request("GET", `/networks/${networkId}/routers`);
|
||||||
|
}
|
||||||
|
|
||||||
|
createNetworkRouter(
|
||||||
|
networkId: string,
|
||||||
|
data: Omit<NbNetworkRouter, "id">,
|
||||||
|
): Promise<NbNetworkRouter> {
|
||||||
|
return this.request("POST", `/networks/${networkId}/routers`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateNetworkRouter(
|
||||||
|
networkId: string,
|
||||||
|
routerId: string,
|
||||||
|
data: Omit<NbNetworkRouter, "id">,
|
||||||
|
): Promise<NbNetworkRouter> {
|
||||||
|
return this.request(
|
||||||
|
"PUT",
|
||||||
|
`/networks/${networkId}/routers/${routerId}`,
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteNetworkRouter(
|
||||||
|
networkId: string,
|
||||||
|
routerId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
return this.request(
|
||||||
|
"DELETE",
|
||||||
|
`/networks/${networkId}/routers/${routerId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Users
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
listUsers(): Promise<NbUser[]> {
|
||||||
|
return this.request("GET", "/users");
|
||||||
|
}
|
||||||
|
|
||||||
|
createUser(data: {
|
||||||
|
email: string;
|
||||||
|
name?: string;
|
||||||
|
role: string;
|
||||||
|
auto_groups: string[];
|
||||||
|
is_service_user: boolean;
|
||||||
|
}): Promise<NbUser> {
|
||||||
|
return this.request("POST", "/users", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUser(
|
||||||
|
id: string,
|
||||||
|
data: { name?: string; role?: string; auto_groups?: string[] },
|
||||||
|
): Promise<NbUser> {
|
||||||
|
return this.request("PUT", `/users/${id}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteUser(id: string): Promise<void> {
|
||||||
|
return this.request("DELETE", `/users/${id}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ export interface NbGroup {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
peers_count: number;
|
peers_count: number;
|
||||||
peers: Array<{ id: string; name: string }>;
|
peers: Array<{ id: string; name: string }> | null;
|
||||||
issued: "api" | "jwt" | "integration";
|
issued: "api" | "jwt" | "integration";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,6 +46,7 @@ export interface NbPolicy {
|
|||||||
description: string;
|
description: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
rules: NbPolicyRule[];
|
rules: NbPolicyRule[];
|
||||||
|
source_posture_checks: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NbPolicyRule {
|
export interface NbPolicyRule {
|
||||||
@ -57,8 +58,9 @@ export interface NbPolicyRule {
|
|||||||
bidirectional: boolean;
|
bidirectional: boolean;
|
||||||
protocol: "tcp" | "udp" | "icmp" | "all";
|
protocol: "tcp" | "udp" | "icmp" | "all";
|
||||||
ports?: string[];
|
ports?: string[];
|
||||||
sources: Array<string | { id: string; name: string }>;
|
sources: Array<string | { id: string; name: string }> | null;
|
||||||
destinations: Array<string | { id: string; name: string }>;
|
destinations: Array<string | { id: string; name: string }> | null;
|
||||||
|
destinationResource?: { id: string; type: string } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Route as returned by GET /api/routes */
|
/** Route as returned by GET /api/routes */
|
||||||
@ -94,6 +96,62 @@ export interface NbDnsNameserverGroup {
|
|||||||
search_domains_enabled: boolean;
|
search_domains_enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Posture check as returned by GET /api/posture-checks */
|
||||||
|
export interface NbPostureCheck {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
checks: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Network as returned by GET /api/networks */
|
||||||
|
export interface NbNetwork {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
resources: string[];
|
||||||
|
routers: string[];
|
||||||
|
policies: string[];
|
||||||
|
routing_peers_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Network resource as returned by GET /api/networks/{id}/resources */
|
||||||
|
export interface NbNetworkResource {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
type: "host" | "subnet" | "domain";
|
||||||
|
address: string;
|
||||||
|
enabled: boolean;
|
||||||
|
groups: Array<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
peers_count: number;
|
||||||
|
resources_count: number;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Network router as returned by GET /api/networks/{id}/routers */
|
||||||
|
export interface NbNetworkRouter {
|
||||||
|
id: string;
|
||||||
|
peer: string | null;
|
||||||
|
peer_groups: string[] | null;
|
||||||
|
metric: number;
|
||||||
|
masquerade: boolean;
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** User as returned by GET /api/users */
|
||||||
|
export interface NbUser {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
role: "owner" | "admin" | "user";
|
||||||
|
status: "active" | "invited" | "blocked";
|
||||||
|
auto_groups: string[];
|
||||||
|
is_service_user: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/** Audit event as returned by GET /api/events/audit */
|
/** Audit event as returned by GET /api/events/audit */
|
||||||
export interface NbEvent {
|
export interface NbEvent {
|
||||||
id: number;
|
id: number;
|
||||||
|
|||||||
@ -20,10 +20,36 @@ function emptyActual(): ActualState {
|
|||||||
routesByNetworkId: new Map(),
|
routesByNetworkId: new Map(),
|
||||||
dns: [],
|
dns: [],
|
||||||
dnsByName: new Map(),
|
dnsByName: new Map(),
|
||||||
|
postureChecks: [],
|
||||||
|
postureChecksByName: new Map(),
|
||||||
|
networks: [],
|
||||||
|
networksByName: new Map(),
|
||||||
|
networkResources: new Map(),
|
||||||
|
networkRouters: new Map(),
|
||||||
|
users: [],
|
||||||
|
usersByEmail: new Map(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const DESIRED: DesiredState = {
|
/** Builds a minimal DesiredState with defaults for all required sections. */
|
||||||
|
function desiredState(
|
||||||
|
overrides: Partial<DesiredState> = {},
|
||||||
|
): DesiredState {
|
||||||
|
return {
|
||||||
|
groups: {},
|
||||||
|
setup_keys: {},
|
||||||
|
policies: {},
|
||||||
|
routes: {},
|
||||||
|
dns: { nameserver_groups: {} },
|
||||||
|
posture_checks: {},
|
||||||
|
networks: {},
|
||||||
|
peers: {},
|
||||||
|
users: {},
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const DESIRED: DesiredState = desiredState({
|
||||||
groups: { pilots: { peers: ["Pilot-hawk-72"] } },
|
groups: { pilots: { peers: ["Pilot-hawk-72"] } },
|
||||||
setup_keys: {
|
setup_keys: {
|
||||||
"Pilot-hawk-72": {
|
"Pilot-hawk-72": {
|
||||||
@ -34,10 +60,7 @@ const DESIRED: DesiredState = {
|
|||||||
enrolled: false,
|
enrolled: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
policies: {},
|
});
|
||||||
routes: {},
|
|
||||||
dns: { nameserver_groups: {} },
|
|
||||||
};
|
|
||||||
|
|
||||||
Deno.test("computeDiff against empty actual produces create ops", () => {
|
Deno.test("computeDiff against empty actual produces create ops", () => {
|
||||||
const ops = computeDiff(DESIRED, emptyActual());
|
const ops = computeDiff(DESIRED, emptyActual());
|
||||||
@ -80,13 +103,7 @@ Deno.test("computeDiff with matching state produces no ops", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Deno.test("computeDiff does not delete system groups", () => {
|
Deno.test("computeDiff does not delete system groups", () => {
|
||||||
const desired: DesiredState = {
|
const desired = desiredState();
|
||||||
groups: {},
|
|
||||||
setup_keys: {},
|
|
||||||
policies: {},
|
|
||||||
routes: {},
|
|
||||||
dns: { nameserver_groups: {} },
|
|
||||||
};
|
|
||||||
const actual = emptyActual();
|
const actual = emptyActual();
|
||||||
|
|
||||||
const jwtGroup: NbGroup = {
|
const jwtGroup: NbGroup = {
|
||||||
@ -104,13 +121,7 @@ Deno.test("computeDiff does not delete system groups", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Deno.test("computeDiff deletes api-issued groups not in desired", () => {
|
Deno.test("computeDiff deletes api-issued groups not in desired", () => {
|
||||||
const desired: DesiredState = {
|
const desired = desiredState();
|
||||||
groups: {},
|
|
||||||
setup_keys: {},
|
|
||||||
policies: {},
|
|
||||||
routes: {},
|
|
||||||
dns: { nameserver_groups: {} },
|
|
||||||
};
|
|
||||||
const actual = emptyActual();
|
const actual = emptyActual();
|
||||||
|
|
||||||
const staleGroup: NbGroup = {
|
const staleGroup: NbGroup = {
|
||||||
@ -143,13 +154,9 @@ Deno.test("computeDiff detects group peer membership change", () => {
|
|||||||
actual.groups = [group];
|
actual.groups = [group];
|
||||||
|
|
||||||
// Desired has a peer in the group, actual has none
|
// Desired has a peer in the group, actual has none
|
||||||
const desired: DesiredState = {
|
const desired = desiredState({
|
||||||
groups: { pilots: { peers: ["Pilot-hawk-72"] } },
|
groups: { pilots: { peers: ["Pilot-hawk-72"] } },
|
||||||
setup_keys: {},
|
});
|
||||||
policies: {},
|
|
||||||
routes: {},
|
|
||||||
dns: { nameserver_groups: {} },
|
|
||||||
};
|
|
||||||
const ops = computeDiff(desired, actual);
|
const ops = computeDiff(desired, actual);
|
||||||
const updateOps = ops.filter((o) => o.type === "update_group");
|
const updateOps = ops.filter((o) => o.type === "update_group");
|
||||||
assertEquals(updateOps.length, 1);
|
assertEquals(updateOps.length, 1);
|
||||||
@ -157,8 +164,7 @@ Deno.test("computeDiff detects group peer membership change", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Deno.test("computeDiff skips enrolled setup keys", () => {
|
Deno.test("computeDiff skips enrolled setup keys", () => {
|
||||||
const desired: DesiredState = {
|
const desired = desiredState({
|
||||||
groups: {},
|
|
||||||
setup_keys: {
|
setup_keys: {
|
||||||
"Already-enrolled": {
|
"Already-enrolled": {
|
||||||
type: "one-off",
|
type: "one-off",
|
||||||
@ -168,19 +174,14 @@ Deno.test("computeDiff skips enrolled setup keys", () => {
|
|||||||
enrolled: true,
|
enrolled: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
policies: {},
|
});
|
||||||
routes: {},
|
|
||||||
dns: { nameserver_groups: {} },
|
|
||||||
};
|
|
||||||
const ops = computeDiff(desired, emptyActual());
|
const ops = computeDiff(desired, emptyActual());
|
||||||
const createKeyOps = ops.filter((o) => o.type === "create_setup_key");
|
const createKeyOps = ops.filter((o) => o.type === "create_setup_key");
|
||||||
assertEquals(createKeyOps.length, 0);
|
assertEquals(createKeyOps.length, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
Deno.test("computeDiff creates policy when not in actual", () => {
|
Deno.test("computeDiff creates policy when not in actual", () => {
|
||||||
const desired: DesiredState = {
|
const desired = desiredState({
|
||||||
groups: {},
|
|
||||||
setup_keys: {},
|
|
||||||
policies: {
|
policies: {
|
||||||
"allow-pilots": {
|
"allow-pilots": {
|
||||||
description: "Allow pilot traffic",
|
description: "Allow pilot traffic",
|
||||||
@ -190,11 +191,10 @@ Deno.test("computeDiff creates policy when not in actual", () => {
|
|||||||
bidirectional: true,
|
bidirectional: true,
|
||||||
protocol: "all",
|
protocol: "all",
|
||||||
action: "accept",
|
action: "accept",
|
||||||
|
source_posture_checks: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
routes: {},
|
});
|
||||||
dns: { nameserver_groups: {} },
|
|
||||||
};
|
|
||||||
const ops = computeDiff(desired, emptyActual());
|
const ops = computeDiff(desired, emptyActual());
|
||||||
const policyOps = ops.filter((o) => o.type === "create_policy");
|
const policyOps = ops.filter((o) => o.type === "create_policy");
|
||||||
assertEquals(policyOps.length, 1);
|
assertEquals(policyOps.length, 1);
|
||||||
@ -220,6 +220,7 @@ Deno.test("computeDiff detects policy enabled change", () => {
|
|||||||
name: "allow-pilots",
|
name: "allow-pilots",
|
||||||
description: "Allow pilot traffic",
|
description: "Allow pilot traffic",
|
||||||
enabled: true, // currently enabled
|
enabled: true, // currently enabled
|
||||||
|
source_posture_checks: [],
|
||||||
rules: [{
|
rules: [{
|
||||||
name: "allow-pilots",
|
name: "allow-pilots",
|
||||||
description: "",
|
description: "",
|
||||||
@ -233,9 +234,8 @@ Deno.test("computeDiff detects policy enabled change", () => {
|
|||||||
});
|
});
|
||||||
actual.policies = [actual.policiesByName.get("allow-pilots")!];
|
actual.policies = [actual.policiesByName.get("allow-pilots")!];
|
||||||
|
|
||||||
const desired: DesiredState = {
|
const desired = desiredState({
|
||||||
groups: { pilots: { peers: [] } },
|
groups: { pilots: { peers: [] } },
|
||||||
setup_keys: {},
|
|
||||||
policies: {
|
policies: {
|
||||||
"allow-pilots": {
|
"allow-pilots": {
|
||||||
description: "Allow pilot traffic",
|
description: "Allow pilot traffic",
|
||||||
@ -245,11 +245,10 @@ Deno.test("computeDiff detects policy enabled change", () => {
|
|||||||
bidirectional: true,
|
bidirectional: true,
|
||||||
protocol: "all",
|
protocol: "all",
|
||||||
action: "accept",
|
action: "accept",
|
||||||
|
source_posture_checks: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
routes: {},
|
});
|
||||||
dns: { nameserver_groups: {} },
|
|
||||||
};
|
|
||||||
const ops = computeDiff(desired, actual);
|
const ops = computeDiff(desired, actual);
|
||||||
const updateOps = ops.filter((o) => o.type === "update_policy");
|
const updateOps = ops.filter((o) => o.type === "update_policy");
|
||||||
assertEquals(updateOps.length, 1);
|
assertEquals(updateOps.length, 1);
|
||||||
@ -257,10 +256,7 @@ Deno.test("computeDiff detects policy enabled change", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Deno.test("computeDiff creates route when not in actual", () => {
|
Deno.test("computeDiff creates route when not in actual", () => {
|
||||||
const desired: DesiredState = {
|
const desired = desiredState({
|
||||||
groups: {},
|
|
||||||
setup_keys: {},
|
|
||||||
policies: {},
|
|
||||||
routes: {
|
routes: {
|
||||||
"vpn-exit": {
|
"vpn-exit": {
|
||||||
description: "VPN exit route",
|
description: "VPN exit route",
|
||||||
@ -273,8 +269,7 @@ Deno.test("computeDiff creates route when not in actual", () => {
|
|||||||
keep_route: true,
|
keep_route: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
dns: { nameserver_groups: {} },
|
});
|
||||||
};
|
|
||||||
const ops = computeDiff(desired, emptyActual());
|
const ops = computeDiff(desired, emptyActual());
|
||||||
const routeOps = ops.filter((o) => o.type === "create_route");
|
const routeOps = ops.filter((o) => o.type === "create_route");
|
||||||
assertEquals(routeOps.length, 1);
|
assertEquals(routeOps.length, 1);
|
||||||
@ -282,11 +277,7 @@ Deno.test("computeDiff creates route when not in actual", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Deno.test("computeDiff creates dns when not in actual", () => {
|
Deno.test("computeDiff creates dns when not in actual", () => {
|
||||||
const desired: DesiredState = {
|
const desired = desiredState({
|
||||||
groups: {},
|
|
||||||
setup_keys: {},
|
|
||||||
policies: {},
|
|
||||||
routes: {},
|
|
||||||
dns: {
|
dns: {
|
||||||
nameserver_groups: {
|
nameserver_groups: {
|
||||||
"cloudflare": {
|
"cloudflare": {
|
||||||
@ -300,7 +291,7 @@ Deno.test("computeDiff creates dns when not in actual", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
const ops = computeDiff(desired, emptyActual());
|
const ops = computeDiff(desired, emptyActual());
|
||||||
const dnsOps = ops.filter((o) => o.type === "create_dns");
|
const dnsOps = ops.filter((o) => o.type === "create_dns");
|
||||||
assertEquals(dnsOps.length, 1);
|
assertEquals(dnsOps.length, 1);
|
||||||
@ -309,7 +300,7 @@ Deno.test("computeDiff creates dns when not in actual", () => {
|
|||||||
|
|
||||||
Deno.test("computeDiff operations are sorted by EXECUTION_ORDER", () => {
|
Deno.test("computeDiff operations are sorted by EXECUTION_ORDER", () => {
|
||||||
// Desired state that produces creates for multiple resource types
|
// Desired state that produces creates for multiple resource types
|
||||||
const desired: DesiredState = {
|
const desired = desiredState({
|
||||||
groups: { pilots: { peers: [] } },
|
groups: { pilots: { peers: [] } },
|
||||||
setup_keys: {
|
setup_keys: {
|
||||||
"new-key": {
|
"new-key": {
|
||||||
@ -329,11 +320,10 @@ Deno.test("computeDiff operations are sorted by EXECUTION_ORDER", () => {
|
|||||||
bidirectional: true,
|
bidirectional: true,
|
||||||
protocol: "all",
|
protocol: "all",
|
||||||
action: "accept",
|
action: "accept",
|
||||||
|
source_posture_checks: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
routes: {},
|
});
|
||||||
dns: { nameserver_groups: {} },
|
|
||||||
};
|
|
||||||
const ops = computeDiff(desired, emptyActual());
|
const ops = computeDiff(desired, emptyActual());
|
||||||
|
|
||||||
// create_group must come before create_setup_key, which must come before
|
// create_group must come before create_setup_key, which must come before
|
||||||
|
|||||||
@ -15,8 +15,12 @@ export function computeDiff(
|
|||||||
): Operation[] {
|
): Operation[] {
|
||||||
const ops: Operation[] = [];
|
const ops: Operation[] = [];
|
||||||
|
|
||||||
|
diffPostureChecks(desired, actual, ops);
|
||||||
diffGroups(desired, actual, ops);
|
diffGroups(desired, actual, ops);
|
||||||
diffSetupKeys(desired, actual, ops);
|
diffSetupKeys(desired, actual, ops);
|
||||||
|
diffNetworks(desired, actual, ops);
|
||||||
|
diffPeers(desired, actual, ops);
|
||||||
|
diffUsers(desired, actual, ops);
|
||||||
diffPolicies(desired, actual, ops);
|
diffPolicies(desired, actual, ops);
|
||||||
diffRoutes(desired, actual, ops);
|
diffRoutes(desired, actual, ops);
|
||||||
diffDns(desired, actual, ops);
|
diffDns(desired, actual, ops);
|
||||||
@ -24,6 +28,53 @@ export function computeDiff(
|
|||||||
return sortByExecutionOrder(ops);
|
return sortByExecutionOrder(ops);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Posture Checks
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function diffPostureChecks(
|
||||||
|
desired: DesiredState,
|
||||||
|
actual: ActualState,
|
||||||
|
ops: Operation[],
|
||||||
|
): void {
|
||||||
|
const desiredNames = new Set(Object.keys(desired.posture_checks));
|
||||||
|
|
||||||
|
for (const [name, config] of Object.entries(desired.posture_checks)) {
|
||||||
|
const existing = actual.postureChecksByName.get(name);
|
||||||
|
if (!existing) {
|
||||||
|
ops.push({
|
||||||
|
type: "create_posture_check",
|
||||||
|
name,
|
||||||
|
details: {
|
||||||
|
description: config.description,
|
||||||
|
checks: config.checks,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
existing.description !== config.description ||
|
||||||
|
JSON.stringify(existing.checks) !== JSON.stringify(config.checks)
|
||||||
|
) {
|
||||||
|
ops.push({
|
||||||
|
type: "update_posture_check",
|
||||||
|
name,
|
||||||
|
details: {
|
||||||
|
description: config.description,
|
||||||
|
checks: config.checks,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const pc of actual.postureChecks) {
|
||||||
|
if (!desiredNames.has(pc.name)) {
|
||||||
|
ops.push({ type: "delete_posture_check", name: pc.name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Groups
|
// Groups
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@ -47,7 +98,7 @@ function diffGroups(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Compare peer membership by name (sorted for stable comparison)
|
// Compare peer membership by name (sorted for stable comparison)
|
||||||
const actualPeerNames = existing.peers.map((p) => p.name).sort();
|
const actualPeerNames = (existing.peers ?? []).map((p) => p.name).sort();
|
||||||
const desiredPeerNames = [...config.peers].sort();
|
const desiredPeerNames = [...config.peers].sort();
|
||||||
if (!arraysEqual(actualPeerNames, desiredPeerNames)) {
|
if (!arraysEqual(actualPeerNames, desiredPeerNames)) {
|
||||||
ops.push({
|
ops.push({
|
||||||
@ -64,7 +115,10 @@ function diffGroups(
|
|||||||
// Delete groups that exist in actual but not in desired.
|
// Delete groups that exist in actual but not in desired.
|
||||||
// Only delete API-issued groups — system and JWT groups are managed externally.
|
// Only delete API-issued groups — system and JWT groups are managed externally.
|
||||||
for (const group of actual.groups) {
|
for (const group of actual.groups) {
|
||||||
if (!desiredNames.has(group.name) && group.issued === "api") {
|
if (
|
||||||
|
!desiredNames.has(group.name) && group.issued === "api" &&
|
||||||
|
group.name !== "All"
|
||||||
|
) {
|
||||||
ops.push({ type: "delete_group", name: group.name });
|
ops.push({ type: "delete_group", name: group.name });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -106,6 +160,389 @@ function diffSetupKeys(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Networks (including resources and routers)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function diffNetworks(
|
||||||
|
desired: DesiredState,
|
||||||
|
actual: ActualState,
|
||||||
|
ops: Operation[],
|
||||||
|
): void {
|
||||||
|
const desiredNames = new Set(Object.keys(desired.networks));
|
||||||
|
|
||||||
|
for (const [name, config] of Object.entries(desired.networks)) {
|
||||||
|
const existing = actual.networksByName.get(name);
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
ops.push({
|
||||||
|
type: "create_network",
|
||||||
|
name,
|
||||||
|
details: { description: config.description },
|
||||||
|
});
|
||||||
|
|
||||||
|
// All resources and routers under a new network are creates
|
||||||
|
for (const res of config.resources) {
|
||||||
|
ops.push({
|
||||||
|
type: "create_network_resource",
|
||||||
|
name: res.name,
|
||||||
|
details: {
|
||||||
|
network_name: name,
|
||||||
|
description: res.description,
|
||||||
|
type: res.type,
|
||||||
|
address: res.address,
|
||||||
|
enabled: res.enabled,
|
||||||
|
groups: res.groups,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const router of config.routers) {
|
||||||
|
ops.push({
|
||||||
|
type: "create_network_router",
|
||||||
|
name: routerKey(router),
|
||||||
|
details: {
|
||||||
|
network_name: name,
|
||||||
|
peer: router.peer,
|
||||||
|
peer_groups: router.peer_groups,
|
||||||
|
metric: router.metric,
|
||||||
|
masquerade: router.masquerade,
|
||||||
|
enabled: router.enabled,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Network exists — check for description change
|
||||||
|
if (existing.description !== config.description) {
|
||||||
|
ops.push({
|
||||||
|
type: "update_network",
|
||||||
|
name,
|
||||||
|
details: { description: config.description },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Diff resources within this network
|
||||||
|
const actualResources = actual.networkResources.get(existing.id) ?? [];
|
||||||
|
diffNetworkResources(name, config.resources, actualResources, actual, ops);
|
||||||
|
|
||||||
|
// Diff routers within this network
|
||||||
|
const actualRouters = actual.networkRouters.get(existing.id) ?? [];
|
||||||
|
diffNetworkRouters(name, config.routers, actualRouters, actual, ops);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete networks not in desired (this also implicitly removes their resources/routers)
|
||||||
|
for (const network of actual.networks) {
|
||||||
|
if (!desiredNames.has(network.name)) {
|
||||||
|
// Delete routers and resources first (execution order handles this,
|
||||||
|
// but we still emit the ops)
|
||||||
|
const routers = actual.networkRouters.get(network.id) ?? [];
|
||||||
|
for (const router of routers) {
|
||||||
|
ops.push({
|
||||||
|
type: "delete_network_router",
|
||||||
|
name: actualRouterKey(router, actual),
|
||||||
|
details: { network_name: network.name, router_id: router.id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const resources = actual.networkResources.get(network.id) ?? [];
|
||||||
|
for (const res of resources) {
|
||||||
|
ops.push({
|
||||||
|
type: "delete_network_resource",
|
||||||
|
name: res.name,
|
||||||
|
details: { network_name: network.name, resource_id: res.id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ops.push({ type: "delete_network", name: network.name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function diffNetworkResources(
|
||||||
|
networkName: string,
|
||||||
|
desiredResources: DesiredState["networks"][string]["resources"],
|
||||||
|
actualResources: ActualState["networkResources"] extends Map<
|
||||||
|
string,
|
||||||
|
infer V
|
||||||
|
> ? V
|
||||||
|
: never,
|
||||||
|
actual: ActualState,
|
||||||
|
ops: Operation[],
|
||||||
|
): void {
|
||||||
|
const actualByName = new Map(actualResources.map((r) => [r.name, r]));
|
||||||
|
const desiredNames = new Set(desiredResources.map((r) => r.name));
|
||||||
|
|
||||||
|
for (const res of desiredResources) {
|
||||||
|
const existing = actualByName.get(res.name);
|
||||||
|
if (!existing) {
|
||||||
|
ops.push({
|
||||||
|
type: "create_network_resource",
|
||||||
|
name: res.name,
|
||||||
|
details: {
|
||||||
|
network_name: networkName,
|
||||||
|
description: res.description,
|
||||||
|
type: res.type,
|
||||||
|
address: res.address,
|
||||||
|
enabled: res.enabled,
|
||||||
|
groups: res.groups,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare fields: resolve actual group names for comparison
|
||||||
|
const actualGroupNames = existing.groups.map((g) => g.name).sort();
|
||||||
|
const desiredGroupNames = [...res.groups].sort();
|
||||||
|
|
||||||
|
if (
|
||||||
|
existing.description !== res.description ||
|
||||||
|
existing.type !== res.type ||
|
||||||
|
existing.address !== res.address ||
|
||||||
|
existing.enabled !== res.enabled ||
|
||||||
|
!arraysEqual(actualGroupNames, desiredGroupNames)
|
||||||
|
) {
|
||||||
|
ops.push({
|
||||||
|
type: "update_network_resource",
|
||||||
|
name: res.name,
|
||||||
|
details: {
|
||||||
|
network_name: networkName,
|
||||||
|
resource_id: existing.id,
|
||||||
|
description: res.description,
|
||||||
|
type: res.type,
|
||||||
|
address: res.address,
|
||||||
|
enabled: res.enabled,
|
||||||
|
groups: res.groups,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete resources not in desired
|
||||||
|
for (const res of actualResources) {
|
||||||
|
if (!desiredNames.has(res.name)) {
|
||||||
|
ops.push({
|
||||||
|
type: "delete_network_resource",
|
||||||
|
name: res.name,
|
||||||
|
details: { network_name: networkName, resource_id: res.id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function diffNetworkRouters(
|
||||||
|
networkName: string,
|
||||||
|
desiredRouters: DesiredState["networks"][string]["routers"],
|
||||||
|
actualRouters: ActualState["networkRouters"] extends Map<string, infer V> ? V
|
||||||
|
: never,
|
||||||
|
actual: ActualState,
|
||||||
|
ops: Operation[],
|
||||||
|
): void {
|
||||||
|
// Match routers by their key (peer name or serialized peer_groups)
|
||||||
|
const actualByKey = new Map(
|
||||||
|
actualRouters.map((r) => [actualRouterKey(r, actual), r]),
|
||||||
|
);
|
||||||
|
const desiredKeys = new Set(desiredRouters.map((r) => routerKey(r)));
|
||||||
|
|
||||||
|
for (const router of desiredRouters) {
|
||||||
|
const key = routerKey(router);
|
||||||
|
const existing = actualByKey.get(key);
|
||||||
|
if (!existing) {
|
||||||
|
ops.push({
|
||||||
|
type: "create_network_router",
|
||||||
|
name: key,
|
||||||
|
details: {
|
||||||
|
network_name: networkName,
|
||||||
|
peer: router.peer,
|
||||||
|
peer_groups: router.peer_groups,
|
||||||
|
metric: router.metric,
|
||||||
|
masquerade: router.masquerade,
|
||||||
|
enabled: router.enabled,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare mutable fields
|
||||||
|
if (
|
||||||
|
existing.metric !== router.metric ||
|
||||||
|
existing.masquerade !== router.masquerade ||
|
||||||
|
existing.enabled !== router.enabled
|
||||||
|
) {
|
||||||
|
ops.push({
|
||||||
|
type: "update_network_router",
|
||||||
|
name: key,
|
||||||
|
details: {
|
||||||
|
network_name: networkName,
|
||||||
|
router_id: existing.id,
|
||||||
|
peer: router.peer,
|
||||||
|
peer_groups: router.peer_groups,
|
||||||
|
metric: router.metric,
|
||||||
|
masquerade: router.masquerade,
|
||||||
|
enabled: router.enabled,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete routers not in desired
|
||||||
|
for (const router of actualRouters) {
|
||||||
|
const key = actualRouterKey(router, actual);
|
||||||
|
if (!desiredKeys.has(key)) {
|
||||||
|
ops.push({
|
||||||
|
type: "delete_network_router",
|
||||||
|
name: key,
|
||||||
|
details: { network_name: networkName, router_id: router.id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a stable key for a desired router config.
|
||||||
|
* Uses the peer name if set, otherwise serializes peer_groups sorted.
|
||||||
|
*/
|
||||||
|
function routerKey(
|
||||||
|
router: { peer?: string; peer_groups?: string[] },
|
||||||
|
): string {
|
||||||
|
if (router.peer) return `peer:${router.peer}`;
|
||||||
|
return `groups:${[...(router.peer_groups ?? [])].sort().join(",")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a stable key for an actual router, resolving peer ID to name.
|
||||||
|
*/
|
||||||
|
function actualRouterKey(
|
||||||
|
router: { peer: string | null; peer_groups: string[] | null },
|
||||||
|
actual: ActualState,
|
||||||
|
): string {
|
||||||
|
if (router.peer) {
|
||||||
|
const peer = actual.peersById.get(router.peer);
|
||||||
|
return `peer:${peer ? peer.name : router.peer}`;
|
||||||
|
}
|
||||||
|
// peer_groups on actual routers are group IDs — resolve to names
|
||||||
|
const groupNames = (router.peer_groups ?? [])
|
||||||
|
.map((id) => {
|
||||||
|
const g = actual.groupsById.get(id);
|
||||||
|
return g ? g.name : id;
|
||||||
|
})
|
||||||
|
.sort();
|
||||||
|
return `groups:${groupNames.join(",")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Peers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function diffPeers(
|
||||||
|
desired: DesiredState,
|
||||||
|
actual: ActualState,
|
||||||
|
ops: Operation[],
|
||||||
|
): void {
|
||||||
|
for (const [name, config] of Object.entries(desired.peers)) {
|
||||||
|
const existing = actual.peersByName.get(name);
|
||||||
|
if (!existing) continue; // Never create or delete peers
|
||||||
|
|
||||||
|
let changed = false;
|
||||||
|
|
||||||
|
// Compare groups (excluding "All"), resolve actual peer group names
|
||||||
|
const actualGroupNames = existing.groups
|
||||||
|
.map((g) => g.name)
|
||||||
|
.filter((n) => n !== "All")
|
||||||
|
.sort();
|
||||||
|
const desiredGroupNames = [...config.groups].sort();
|
||||||
|
if (!arraysEqual(actualGroupNames, desiredGroupNames)) {
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
existing.login_expiration_enabled !== config.login_expiration_enabled ||
|
||||||
|
existing.inactivity_expiration_enabled !==
|
||||||
|
config.inactivity_expiration_enabled ||
|
||||||
|
existing.ssh_enabled !== config.ssh_enabled
|
||||||
|
) {
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
ops.push({
|
||||||
|
type: "update_peer",
|
||||||
|
name,
|
||||||
|
details: {
|
||||||
|
groups: config.groups,
|
||||||
|
login_expiration_enabled: config.login_expiration_enabled,
|
||||||
|
inactivity_expiration_enabled: config.inactivity_expiration_enabled,
|
||||||
|
ssh_enabled: config.ssh_enabled,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Users
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function diffUsers(
|
||||||
|
desired: DesiredState,
|
||||||
|
actual: ActualState,
|
||||||
|
ops: Operation[],
|
||||||
|
): void {
|
||||||
|
const desiredEmails = new Set(Object.keys(desired.users));
|
||||||
|
|
||||||
|
for (const [email, config] of Object.entries(desired.users)) {
|
||||||
|
const existing = actual.usersByEmail.get(email);
|
||||||
|
if (!existing) {
|
||||||
|
ops.push({
|
||||||
|
type: "create_user",
|
||||||
|
name: email,
|
||||||
|
details: {
|
||||||
|
email,
|
||||||
|
name: config.name,
|
||||||
|
role: config.role,
|
||||||
|
auto_groups: config.auto_groups,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare role and auto_groups
|
||||||
|
const actualAutoGroupNames = resolveIds(
|
||||||
|
existing.auto_groups,
|
||||||
|
actual,
|
||||||
|
).sort();
|
||||||
|
const desiredAutoGroupNames = [...config.auto_groups].sort();
|
||||||
|
|
||||||
|
if (
|
||||||
|
existing.role !== config.role ||
|
||||||
|
!arraysEqual(actualAutoGroupNames, desiredAutoGroupNames)
|
||||||
|
) {
|
||||||
|
ops.push({
|
||||||
|
type: "update_user",
|
||||||
|
name: email,
|
||||||
|
details: {
|
||||||
|
name: config.name,
|
||||||
|
role: config.role,
|
||||||
|
auto_groups: config.auto_groups,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete users not in desired, but NEVER delete owners
|
||||||
|
for (const user of actual.users) {
|
||||||
|
if (!desiredEmails.has(user.email) && user.role !== "owner") {
|
||||||
|
ops.push({ type: "delete_user", name: user.email });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Resolves group IDs to group names using actual state. */
|
||||||
|
function resolveIds(ids: string[], actual: ActualState): string[] {
|
||||||
|
return ids.map((id) => {
|
||||||
|
const group = actual.groupsById.get(id);
|
||||||
|
return group ? group.name : id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Policies
|
// Policies
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@ -127,29 +564,56 @@ function diffPolicies(
|
|||||||
enabled: config.enabled,
|
enabled: config.enabled,
|
||||||
sources: config.sources,
|
sources: config.sources,
|
||||||
destinations: config.destinations,
|
destinations: config.destinations,
|
||||||
|
destination_resource: config.destination_resource,
|
||||||
|
source_posture_checks: config.source_posture_checks,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract group names from actual rules for comparison.
|
// Extract group names from actual rules for comparison.
|
||||||
// A policy may have multiple rules; aggregate sources/destinations
|
|
||||||
// across all rules for a flat comparison against the desired config.
|
|
||||||
const actualSources = extractGroupNames(
|
const actualSources = extractGroupNames(
|
||||||
existing.rules.flatMap((r) => r.sources),
|
existing.rules.flatMap((r) => r.sources ?? []),
|
||||||
actual,
|
|
||||||
).sort();
|
|
||||||
const actualDests = extractGroupNames(
|
|
||||||
existing.rules.flatMap((r) => r.destinations),
|
|
||||||
actual,
|
actual,
|
||||||
).sort();
|
).sort();
|
||||||
const desiredSources = [...config.sources].sort();
|
const desiredSources = [...config.sources].sort();
|
||||||
const desiredDests = [...config.destinations].sort();
|
|
||||||
|
let destsChanged = false;
|
||||||
|
if (config.destination_resource) {
|
||||||
|
// When desired has destination_resource, compare against actual rule's destinationResource
|
||||||
|
const actualDestRes = existing.rules[0]?.destinationResource;
|
||||||
|
if (
|
||||||
|
!actualDestRes ||
|
||||||
|
actualDestRes.id !== config.destination_resource.id ||
|
||||||
|
actualDestRes.type !== config.destination_resource.type
|
||||||
|
) {
|
||||||
|
destsChanged = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Standard group-based destination comparison
|
||||||
|
const actualDests = extractGroupNames(
|
||||||
|
existing.rules.flatMap((r) => r.destinations ?? []),
|
||||||
|
actual,
|
||||||
|
).sort();
|
||||||
|
const desiredDests = [...config.destinations].sort();
|
||||||
|
destsChanged = !arraysEqual(actualDests, desiredDests);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare source_posture_checks
|
||||||
|
const actualPostureChecks = [
|
||||||
|
...(existing.source_posture_checks ?? []),
|
||||||
|
].sort();
|
||||||
|
const desiredPostureChecks = [...config.source_posture_checks].sort();
|
||||||
|
const postureChecksChanged = !arraysEqual(
|
||||||
|
actualPostureChecks,
|
||||||
|
desiredPostureChecks,
|
||||||
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
existing.enabled !== config.enabled ||
|
existing.enabled !== config.enabled ||
|
||||||
!arraysEqual(actualSources, desiredSources) ||
|
!arraysEqual(actualSources, desiredSources) ||
|
||||||
!arraysEqual(actualDests, desiredDests)
|
destsChanged ||
|
||||||
|
postureChecksChanged
|
||||||
) {
|
) {
|
||||||
ops.push({
|
ops.push({
|
||||||
type: "update_policy",
|
type: "update_policy",
|
||||||
@ -158,6 +622,8 @@ function diffPolicies(
|
|||||||
enabled: config.enabled,
|
enabled: config.enabled,
|
||||||
sources: config.sources,
|
sources: config.sources,
|
||||||
destinations: config.destinations,
|
destinations: config.destinations,
|
||||||
|
destination_resource: config.destination_resource,
|
||||||
|
source_posture_checks: config.source_posture_checks,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -176,7 +642,7 @@ function diffPolicies(
|
|||||||
* back to the ID if the group is unknown (defensive).
|
* back to the ID if the group is unknown (defensive).
|
||||||
*/
|
*/
|
||||||
function extractGroupNames(
|
function extractGroupNames(
|
||||||
refs: NbPolicyRule["sources"],
|
refs: NonNullable<NbPolicyRule["sources"]>,
|
||||||
actual: ActualState,
|
actual: ActualState,
|
||||||
): string[] {
|
): string[] {
|
||||||
return refs.map((ref) => {
|
return refs.map((ref) => {
|
||||||
|
|||||||
@ -19,6 +19,14 @@ function emptyActual(): ActualState {
|
|||||||
routesByNetworkId: new Map(),
|
routesByNetworkId: new Map(),
|
||||||
dns: [],
|
dns: [],
|
||||||
dnsByName: new Map(),
|
dnsByName: new Map(),
|
||||||
|
postureChecks: [],
|
||||||
|
postureChecksByName: new Map(),
|
||||||
|
networks: [],
|
||||||
|
networksByName: new Map(),
|
||||||
|
networkResources: new Map(),
|
||||||
|
networkRouters: new Map(),
|
||||||
|
users: [],
|
||||||
|
usersByEmail: new Map(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -31,6 +31,21 @@ type ExecutorClient = Pick<
|
|||||||
| "createDnsNameserverGroup"
|
| "createDnsNameserverGroup"
|
||||||
| "updateDnsNameserverGroup"
|
| "updateDnsNameserverGroup"
|
||||||
| "deleteDnsNameserverGroup"
|
| "deleteDnsNameserverGroup"
|
||||||
|
| "createPostureCheck"
|
||||||
|
| "updatePostureCheck"
|
||||||
|
| "deletePostureCheck"
|
||||||
|
| "createNetwork"
|
||||||
|
| "updateNetwork"
|
||||||
|
| "deleteNetwork"
|
||||||
|
| "createNetworkResource"
|
||||||
|
| "updateNetworkResource"
|
||||||
|
| "deleteNetworkResource"
|
||||||
|
| "createNetworkRouter"
|
||||||
|
| "updateNetworkRouter"
|
||||||
|
| "deleteNetworkRouter"
|
||||||
|
| "createUser"
|
||||||
|
| "updateUser"
|
||||||
|
| "deleteUser"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -48,6 +63,8 @@ export async function executeOperations(
|
|||||||
): Promise<ExecutionResult> {
|
): Promise<ExecutionResult> {
|
||||||
const results: OperationResult[] = [];
|
const results: OperationResult[] = [];
|
||||||
const createdGroupIds = new Map<string, string>();
|
const createdGroupIds = new Map<string, string>();
|
||||||
|
const createdPostureCheckIds = new Map<string, string>();
|
||||||
|
const createdNetworkIds = new Map<string, string>();
|
||||||
const createdKeys = new Map<string, string>();
|
const createdKeys = new Map<string, string>();
|
||||||
|
|
||||||
function resolveGroupId(name: string): string {
|
function resolveGroupId(name: string): string {
|
||||||
@ -70,14 +87,41 @@ export async function executeOperations(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolvePeerId(name: string): string {
|
||||||
|
const peer = actual.peersByName.get(name);
|
||||||
|
if (peer) return peer.id;
|
||||||
|
throw new Error(`peer "${name}" not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePostureCheckId(name: string): string {
|
||||||
|
const created = createdPostureCheckIds.get(name);
|
||||||
|
if (created) return created;
|
||||||
|
const existing = actual.postureChecksByName.get(name);
|
||||||
|
if (existing) return existing.id;
|
||||||
|
throw new Error(`posture check "${name}" not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveNetworkId(name: string): string {
|
||||||
|
const created = createdNetworkIds.get(name);
|
||||||
|
if (created) return created;
|
||||||
|
const existing = actual.networksByName.get(name);
|
||||||
|
if (existing) return existing.id;
|
||||||
|
throw new Error(`network "${name}" not found`);
|
||||||
|
}
|
||||||
|
|
||||||
for (const op of ops) {
|
for (const op of ops) {
|
||||||
try {
|
try {
|
||||||
await executeSingle(op, client, actual, {
|
await executeSingle(op, client, actual, {
|
||||||
createdGroupIds,
|
createdGroupIds,
|
||||||
|
createdPostureCheckIds,
|
||||||
|
createdNetworkIds,
|
||||||
createdKeys,
|
createdKeys,
|
||||||
resolveGroupId,
|
resolveGroupId,
|
||||||
resolveGroupIds,
|
resolveGroupIds,
|
||||||
resolvePeerIds,
|
resolvePeerIds,
|
||||||
|
resolvePeerId,
|
||||||
|
resolvePostureCheckId,
|
||||||
|
resolveNetworkId,
|
||||||
});
|
});
|
||||||
results.push({ ...op, status: "success" });
|
results.push({ ...op, status: "success" });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -99,10 +143,15 @@ export async function executeOperations(
|
|||||||
|
|
||||||
interface ExecutorContext {
|
interface ExecutorContext {
|
||||||
createdGroupIds: Map<string, string>;
|
createdGroupIds: Map<string, string>;
|
||||||
|
createdPostureCheckIds: Map<string, string>;
|
||||||
|
createdNetworkIds: Map<string, string>;
|
||||||
createdKeys: Map<string, string>;
|
createdKeys: Map<string, string>;
|
||||||
resolveGroupId: (name: string) => string;
|
resolveGroupId: (name: string) => string;
|
||||||
resolveGroupIds: (names: string[]) => string[];
|
resolveGroupIds: (names: string[]) => string[];
|
||||||
resolvePeerIds: (names: string[]) => string[];
|
resolvePeerIds: (names: string[]) => string[];
|
||||||
|
resolvePeerId: (name: string) => string;
|
||||||
|
resolvePostureCheckId: (name: string) => string;
|
||||||
|
resolveNetworkId: (name: string) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function executeSingle(
|
async function executeSingle(
|
||||||
@ -114,6 +163,37 @@ async function executeSingle(
|
|||||||
const d = op.details ?? {};
|
const d = op.details ?? {};
|
||||||
|
|
||||||
switch (op.type) {
|
switch (op.type) {
|
||||||
|
// ----- Posture Checks -----
|
||||||
|
case "create_posture_check": {
|
||||||
|
const pc = await client.createPostureCheck({
|
||||||
|
name: op.name,
|
||||||
|
description: (d.description as string) ?? "",
|
||||||
|
checks: (d.checks as Record<string, unknown>) ?? {},
|
||||||
|
});
|
||||||
|
ctx.createdPostureCheckIds.set(op.name, pc.id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "update_posture_check": {
|
||||||
|
const existing = actual.postureChecksByName.get(op.name);
|
||||||
|
if (!existing) {
|
||||||
|
throw new Error(`posture check "${op.name}" not found for update`);
|
||||||
|
}
|
||||||
|
await client.updatePostureCheck(existing.id, {
|
||||||
|
name: op.name,
|
||||||
|
description: (d.description as string) ?? existing.description,
|
||||||
|
checks: (d.checks as Record<string, unknown>) ?? existing.checks,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "delete_posture_check": {
|
||||||
|
const existing = actual.postureChecksByName.get(op.name);
|
||||||
|
if (!existing) {
|
||||||
|
throw new Error(`posture check "${op.name}" not found for delete`);
|
||||||
|
}
|
||||||
|
await client.deletePostureCheck(existing.id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// ----- Groups -----
|
// ----- Groups -----
|
||||||
case "create_group": {
|
case "create_group": {
|
||||||
const peerNames = d.peers as string[] | undefined;
|
const peerNames = d.peers as string[] | undefined;
|
||||||
@ -127,7 +207,9 @@ async function executeSingle(
|
|||||||
}
|
}
|
||||||
case "update_group": {
|
case "update_group": {
|
||||||
const existing = actual.groupsByName.get(op.name);
|
const existing = actual.groupsByName.get(op.name);
|
||||||
if (!existing) throw new Error(`group "${op.name}" not found for update`);
|
if (!existing) {
|
||||||
|
throw new Error(`group "${op.name}" not found for update`);
|
||||||
|
}
|
||||||
const desiredPeers = d.desired_peers as string[] | undefined;
|
const desiredPeers = d.desired_peers as string[] | undefined;
|
||||||
const peerIds = desiredPeers?.length
|
const peerIds = desiredPeers?.length
|
||||||
? ctx.resolvePeerIds(desiredPeers)
|
? ctx.resolvePeerIds(desiredPeers)
|
||||||
@ -180,7 +262,6 @@ async function executeSingle(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "update_peer_groups": {
|
case "update_peer_groups": {
|
||||||
// This op type updates peer-level properties; details.id is the peer ID
|
|
||||||
const peerId = d.id as string;
|
const peerId = d.id as string;
|
||||||
if (!peerId) throw new Error(`update_peer_groups missing details.id`);
|
if (!peerId) throw new Error(`update_peer_groups missing details.id`);
|
||||||
await client.updatePeer(peerId, {
|
await client.updatePeer(peerId, {
|
||||||
@ -192,6 +273,19 @@ async function executeSingle(
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "update_peer": {
|
||||||
|
const peerId = ctx.resolvePeerId(op.name);
|
||||||
|
await client.updatePeer(peerId, {
|
||||||
|
login_expiration_enabled: d.login_expiration_enabled as
|
||||||
|
| boolean
|
||||||
|
| undefined,
|
||||||
|
inactivity_expiration_enabled: d.inactivity_expiration_enabled as
|
||||||
|
| boolean
|
||||||
|
| undefined,
|
||||||
|
ssh_enabled: d.ssh_enabled as boolean | undefined,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "delete_peer": {
|
case "delete_peer": {
|
||||||
const peer = actual.peersByName.get(op.name);
|
const peer = actual.peersByName.get(op.name);
|
||||||
if (!peer) throw new Error(`peer "${op.name}" not found for delete`);
|
if (!peer) throw new Error(`peer "${op.name}" not found for delete`);
|
||||||
@ -199,26 +293,213 @@ async function executeSingle(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----- Networks -----
|
||||||
|
case "create_network": {
|
||||||
|
const network = await client.createNetwork({
|
||||||
|
name: op.name,
|
||||||
|
description: (d.description as string) ?? "",
|
||||||
|
});
|
||||||
|
ctx.createdNetworkIds.set(op.name, network.id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "update_network": {
|
||||||
|
const existing = actual.networksByName.get(op.name);
|
||||||
|
if (!existing) {
|
||||||
|
throw new Error(`network "${op.name}" not found for update`);
|
||||||
|
}
|
||||||
|
await client.updateNetwork(existing.id, {
|
||||||
|
name: op.name,
|
||||||
|
description: (d.description as string) ?? existing.description,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "delete_network": {
|
||||||
|
const existing = actual.networksByName.get(op.name);
|
||||||
|
if (!existing) {
|
||||||
|
throw new Error(`network "${op.name}" not found for delete`);
|
||||||
|
}
|
||||||
|
await client.deleteNetwork(existing.id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Network Resources -----
|
||||||
|
case "create_network_resource": {
|
||||||
|
const networkName = d.network_name as string;
|
||||||
|
if (!networkName) {
|
||||||
|
throw new Error("create_network_resource missing network_name");
|
||||||
|
}
|
||||||
|
const networkId = ctx.resolveNetworkId(networkName);
|
||||||
|
const groupIds = ctx.resolveGroupIds(d.groups as string[] ?? []);
|
||||||
|
await client.createNetworkResource(networkId, {
|
||||||
|
name: op.name,
|
||||||
|
description: (d.description as string) ?? "",
|
||||||
|
address: d.address as string,
|
||||||
|
enabled: (d.enabled as boolean) ?? true,
|
||||||
|
groups: groupIds,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "update_network_resource": {
|
||||||
|
const networkName = d.network_name as string;
|
||||||
|
if (!networkName) {
|
||||||
|
throw new Error("update_network_resource missing network_name");
|
||||||
|
}
|
||||||
|
const networkId = ctx.resolveNetworkId(networkName);
|
||||||
|
const resourceId = d.resource_id as string;
|
||||||
|
if (!resourceId) {
|
||||||
|
throw new Error("update_network_resource missing resource_id");
|
||||||
|
}
|
||||||
|
const groupIds = ctx.resolveGroupIds(d.groups as string[] ?? []);
|
||||||
|
await client.updateNetworkResource(networkId, resourceId, {
|
||||||
|
name: op.name,
|
||||||
|
description: (d.description as string) ?? "",
|
||||||
|
address: d.address as string,
|
||||||
|
enabled: (d.enabled as boolean) ?? true,
|
||||||
|
groups: groupIds,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "delete_network_resource": {
|
||||||
|
const networkName = d.network_name as string;
|
||||||
|
if (!networkName) {
|
||||||
|
throw new Error("delete_network_resource missing network_name");
|
||||||
|
}
|
||||||
|
const networkId = ctx.resolveNetworkId(networkName);
|
||||||
|
const resourceId = d.resource_id as string;
|
||||||
|
if (!resourceId) {
|
||||||
|
throw new Error("delete_network_resource missing resource_id");
|
||||||
|
}
|
||||||
|
await client.deleteNetworkResource(networkId, resourceId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Network Routers -----
|
||||||
|
case "create_network_router": {
|
||||||
|
const networkName = d.network_name as string;
|
||||||
|
if (!networkName) {
|
||||||
|
throw new Error("create_network_router missing network_name");
|
||||||
|
}
|
||||||
|
const networkId = ctx.resolveNetworkId(networkName);
|
||||||
|
const peer = d.peer ? ctx.resolvePeerId(d.peer as string) : null;
|
||||||
|
const peerGroups = d.peer_groups
|
||||||
|
? ctx.resolveGroupIds(d.peer_groups as string[])
|
||||||
|
: null;
|
||||||
|
await client.createNetworkRouter(networkId, {
|
||||||
|
peer,
|
||||||
|
peer_groups: peerGroups,
|
||||||
|
metric: (d.metric as number) ?? 9999,
|
||||||
|
masquerade: (d.masquerade as boolean) ?? true,
|
||||||
|
enabled: (d.enabled as boolean) ?? true,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "update_network_router": {
|
||||||
|
const networkName = d.network_name as string;
|
||||||
|
if (!networkName) {
|
||||||
|
throw new Error("update_network_router missing network_name");
|
||||||
|
}
|
||||||
|
const networkId = ctx.resolveNetworkId(networkName);
|
||||||
|
const routerId = d.router_id as string;
|
||||||
|
if (!routerId) {
|
||||||
|
throw new Error("update_network_router missing router_id");
|
||||||
|
}
|
||||||
|
const peer = d.peer ? ctx.resolvePeerId(d.peer as string) : null;
|
||||||
|
const peerGroups = d.peer_groups
|
||||||
|
? ctx.resolveGroupIds(d.peer_groups as string[])
|
||||||
|
: null;
|
||||||
|
await client.updateNetworkRouter(networkId, routerId, {
|
||||||
|
peer,
|
||||||
|
peer_groups: peerGroups,
|
||||||
|
metric: (d.metric as number) ?? 9999,
|
||||||
|
masquerade: (d.masquerade as boolean) ?? true,
|
||||||
|
enabled: (d.enabled as boolean) ?? true,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "delete_network_router": {
|
||||||
|
const networkName = d.network_name as string;
|
||||||
|
if (!networkName) {
|
||||||
|
throw new Error("delete_network_router missing network_name");
|
||||||
|
}
|
||||||
|
const networkId = ctx.resolveNetworkId(networkName);
|
||||||
|
const routerId = d.router_id as string;
|
||||||
|
if (!routerId) {
|
||||||
|
throw new Error("delete_network_router missing router_id");
|
||||||
|
}
|
||||||
|
await client.deleteNetworkRouter(networkId, routerId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Users -----
|
||||||
|
case "create_user": {
|
||||||
|
const autoGroupIds = ctx.resolveGroupIds(
|
||||||
|
d.auto_groups as string[] ?? [],
|
||||||
|
);
|
||||||
|
await client.createUser({
|
||||||
|
email: d.email as string,
|
||||||
|
name: d.name as string | undefined,
|
||||||
|
role: d.role as string,
|
||||||
|
auto_groups: autoGroupIds,
|
||||||
|
is_service_user: false,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "update_user": {
|
||||||
|
const existing = actual.usersByEmail.get(op.name);
|
||||||
|
if (!existing) {
|
||||||
|
throw new Error(`user "${op.name}" not found for update`);
|
||||||
|
}
|
||||||
|
const autoGroupIds = ctx.resolveGroupIds(
|
||||||
|
d.auto_groups as string[] ?? [],
|
||||||
|
);
|
||||||
|
await client.updateUser(existing.id, {
|
||||||
|
name: d.name as string | undefined,
|
||||||
|
role: d.role as string | undefined,
|
||||||
|
auto_groups: autoGroupIds,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "delete_user": {
|
||||||
|
const existing = actual.usersByEmail.get(op.name);
|
||||||
|
if (!existing) {
|
||||||
|
throw new Error(`user "${op.name}" not found for delete`);
|
||||||
|
}
|
||||||
|
await client.deleteUser(existing.id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// ----- Policies -----
|
// ----- Policies -----
|
||||||
case "create_policy": {
|
case "create_policy": {
|
||||||
const sourceIds = ctx.resolveGroupIds(d.sources as string[] ?? []);
|
const sourceIds = ctx.resolveGroupIds(d.sources as string[] ?? []);
|
||||||
const destIds = ctx.resolveGroupIds(d.destinations as string[] ?? []);
|
const destResource = d.destination_resource as
|
||||||
|
| { id: string; type: string }
|
||||||
|
| undefined;
|
||||||
|
const destIds = destResource
|
||||||
|
? []
|
||||||
|
: ctx.resolveGroupIds(d.destinations as string[] ?? []);
|
||||||
|
const postureCheckIds = (d.source_posture_checks as string[] ?? [])
|
||||||
|
.map((name) => ctx.resolvePostureCheckId(name));
|
||||||
|
const rule: Record<string, unknown> = {
|
||||||
|
name: op.name,
|
||||||
|
description: (d.description as string) ?? "",
|
||||||
|
enabled: (d.enabled as boolean) ?? true,
|
||||||
|
action: (d.action as string) ?? "accept",
|
||||||
|
bidirectional: (d.bidirectional as boolean) ?? true,
|
||||||
|
protocol: (d.protocol as string) ?? "all",
|
||||||
|
ports: d.ports as string[] | undefined,
|
||||||
|
sources: sourceIds,
|
||||||
|
destinations: destIds,
|
||||||
|
};
|
||||||
|
if (destResource) {
|
||||||
|
rule.destinationResource = destResource;
|
||||||
|
}
|
||||||
await client.createPolicy({
|
await client.createPolicy({
|
||||||
name: op.name,
|
name: op.name,
|
||||||
description: (d.description as string) ?? "",
|
description: (d.description as string) ?? "",
|
||||||
enabled: (d.enabled as boolean) ?? true,
|
enabled: (d.enabled as boolean) ?? true,
|
||||||
|
source_posture_checks: postureCheckIds,
|
||||||
rules: [
|
rules: [
|
||||||
{
|
rule as unknown as import("../netbird/types.ts").NbPolicyRule,
|
||||||
name: op.name,
|
|
||||||
description: (d.description as string) ?? "",
|
|
||||||
enabled: (d.enabled as boolean) ?? true,
|
|
||||||
action: (d.action as "accept" | "drop") ?? "accept",
|
|
||||||
bidirectional: (d.bidirectional as boolean) ?? true,
|
|
||||||
protocol: (d.protocol as "tcp" | "udp" | "icmp" | "all") ?? "all",
|
|
||||||
ports: d.ports as string[] | undefined,
|
|
||||||
sources: sourceIds,
|
|
||||||
destinations: destIds,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
@ -229,23 +510,35 @@ async function executeSingle(
|
|||||||
throw new Error(`policy "${op.name}" not found for update`);
|
throw new Error(`policy "${op.name}" not found for update`);
|
||||||
}
|
}
|
||||||
const sourceIds = ctx.resolveGroupIds(d.sources as string[] ?? []);
|
const sourceIds = ctx.resolveGroupIds(d.sources as string[] ?? []);
|
||||||
const destIds = ctx.resolveGroupIds(d.destinations as string[] ?? []);
|
const destResource = d.destination_resource as
|
||||||
|
| { id: string; type: string }
|
||||||
|
| undefined;
|
||||||
|
const destIds = destResource
|
||||||
|
? []
|
||||||
|
: ctx.resolveGroupIds(d.destinations as string[] ?? []);
|
||||||
|
const postureCheckIds = (d.source_posture_checks as string[] ?? [])
|
||||||
|
.map((name) => ctx.resolvePostureCheckId(name));
|
||||||
|
const rule: Record<string, unknown> = {
|
||||||
|
name: op.name,
|
||||||
|
description: (d.description as string) ?? existing.description,
|
||||||
|
enabled: (d.enabled as boolean) ?? existing.enabled,
|
||||||
|
action: (d.action as string) ?? "accept",
|
||||||
|
bidirectional: (d.bidirectional as boolean) ?? true,
|
||||||
|
protocol: (d.protocol as string) ?? "all",
|
||||||
|
ports: d.ports as string[] | undefined,
|
||||||
|
sources: sourceIds,
|
||||||
|
destinations: destIds,
|
||||||
|
};
|
||||||
|
if (destResource) {
|
||||||
|
rule.destinationResource = destResource;
|
||||||
|
}
|
||||||
await client.updatePolicy(existing.id, {
|
await client.updatePolicy(existing.id, {
|
||||||
name: op.name,
|
name: op.name,
|
||||||
description: (d.description as string) ?? existing.description,
|
description: (d.description as string) ?? existing.description,
|
||||||
enabled: (d.enabled as boolean) ?? existing.enabled,
|
enabled: (d.enabled as boolean) ?? existing.enabled,
|
||||||
|
source_posture_checks: postureCheckIds,
|
||||||
rules: [
|
rules: [
|
||||||
{
|
rule as unknown as import("../netbird/types.ts").NbPolicyRule,
|
||||||
name: op.name,
|
|
||||||
description: (d.description as string) ?? existing.description,
|
|
||||||
enabled: (d.enabled as boolean) ?? existing.enabled,
|
|
||||||
action: (d.action as "accept" | "drop") ?? "accept",
|
|
||||||
bidirectional: (d.bidirectional as boolean) ?? true,
|
|
||||||
protocol: (d.protocol as "tcp" | "udp" | "icmp" | "all") ?? "all",
|
|
||||||
ports: d.ports as string[] | undefined,
|
|
||||||
sources: sourceIds,
|
|
||||||
destinations: destIds,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|||||||
@ -6,6 +6,7 @@ export type OperationType =
|
|||||||
| "delete_setup_key"
|
| "delete_setup_key"
|
||||||
| "rename_peer"
|
| "rename_peer"
|
||||||
| "update_peer_groups"
|
| "update_peer_groups"
|
||||||
|
| "update_peer"
|
||||||
| "delete_peer"
|
| "delete_peer"
|
||||||
| "create_policy"
|
| "create_policy"
|
||||||
| "update_policy"
|
| "update_policy"
|
||||||
@ -15,7 +16,22 @@ export type OperationType =
|
|||||||
| "delete_route"
|
| "delete_route"
|
||||||
| "create_dns"
|
| "create_dns"
|
||||||
| "update_dns"
|
| "update_dns"
|
||||||
| "delete_dns";
|
| "delete_dns"
|
||||||
|
| "create_posture_check"
|
||||||
|
| "update_posture_check"
|
||||||
|
| "delete_posture_check"
|
||||||
|
| "create_network"
|
||||||
|
| "update_network"
|
||||||
|
| "delete_network"
|
||||||
|
| "create_network_resource"
|
||||||
|
| "update_network_resource"
|
||||||
|
| "delete_network_resource"
|
||||||
|
| "create_network_router"
|
||||||
|
| "update_network_router"
|
||||||
|
| "delete_network_router"
|
||||||
|
| "create_user"
|
||||||
|
| "update_user"
|
||||||
|
| "delete_user";
|
||||||
|
|
||||||
export interface Operation {
|
export interface Operation {
|
||||||
type: OperationType;
|
type: OperationType;
|
||||||
@ -30,11 +46,23 @@ export interface OperationResult extends Operation {
|
|||||||
|
|
||||||
/** Order in which operation types must be executed */
|
/** Order in which operation types must be executed */
|
||||||
export const EXECUTION_ORDER: OperationType[] = [
|
export const EXECUTION_ORDER: OperationType[] = [
|
||||||
|
// Creates: dependencies first
|
||||||
|
"create_posture_check",
|
||||||
|
"update_posture_check",
|
||||||
"create_group",
|
"create_group",
|
||||||
"update_group",
|
"update_group",
|
||||||
"create_setup_key",
|
"create_setup_key",
|
||||||
"rename_peer",
|
"rename_peer",
|
||||||
"update_peer_groups",
|
"update_peer_groups",
|
||||||
|
"update_peer",
|
||||||
|
"create_network",
|
||||||
|
"update_network",
|
||||||
|
"create_network_resource",
|
||||||
|
"update_network_resource",
|
||||||
|
"create_network_router",
|
||||||
|
"update_network_router",
|
||||||
|
"create_user",
|
||||||
|
"update_user",
|
||||||
"create_policy",
|
"create_policy",
|
||||||
"update_policy",
|
"update_policy",
|
||||||
"create_route",
|
"create_route",
|
||||||
@ -45,7 +73,12 @@ export const EXECUTION_ORDER: OperationType[] = [
|
|||||||
"delete_dns",
|
"delete_dns",
|
||||||
"delete_route",
|
"delete_route",
|
||||||
"delete_policy",
|
"delete_policy",
|
||||||
|
"delete_user",
|
||||||
|
"delete_network_router",
|
||||||
|
"delete_network_resource",
|
||||||
|
"delete_network",
|
||||||
"delete_peer",
|
"delete_peer",
|
||||||
"delete_setup_key",
|
"delete_setup_key",
|
||||||
|
"delete_posture_check",
|
||||||
"delete_group",
|
"delete_group",
|
||||||
];
|
];
|
||||||
|
|||||||
@ -3,10 +3,15 @@ import { fetchActualState } from "./actual.ts";
|
|||||||
import type {
|
import type {
|
||||||
NbDnsNameserverGroup,
|
NbDnsNameserverGroup,
|
||||||
NbGroup,
|
NbGroup,
|
||||||
|
NbNetwork,
|
||||||
|
NbNetworkResource,
|
||||||
|
NbNetworkRouter,
|
||||||
NbPeer,
|
NbPeer,
|
||||||
NbPolicy,
|
NbPolicy,
|
||||||
|
NbPostureCheck,
|
||||||
NbRoute,
|
NbRoute,
|
||||||
NbSetupKey,
|
NbSetupKey,
|
||||||
|
NbUser,
|
||||||
} from "../netbird/types.ts";
|
} from "../netbird/types.ts";
|
||||||
|
|
||||||
/** Minimal mock NetBird client that returns predetermined data */
|
/** Minimal mock NetBird client that returns predetermined data */
|
||||||
@ -17,6 +22,11 @@ function mockClient(data: {
|
|||||||
policies?: NbPolicy[];
|
policies?: NbPolicy[];
|
||||||
routes?: NbRoute[];
|
routes?: NbRoute[];
|
||||||
dns?: NbDnsNameserverGroup[];
|
dns?: NbDnsNameserverGroup[];
|
||||||
|
postureChecks?: NbPostureCheck[];
|
||||||
|
networks?: NbNetwork[];
|
||||||
|
networkResources?: Map<string, NbNetworkResource[]>;
|
||||||
|
networkRouters?: Map<string, NbNetworkRouter[]>;
|
||||||
|
users?: NbUser[];
|
||||||
}) {
|
}) {
|
||||||
return {
|
return {
|
||||||
listGroups: () => Promise.resolve(data.groups ?? []),
|
listGroups: () => Promise.resolve(data.groups ?? []),
|
||||||
@ -25,6 +35,13 @@ function mockClient(data: {
|
|||||||
listPolicies: () => Promise.resolve(data.policies ?? []),
|
listPolicies: () => Promise.resolve(data.policies ?? []),
|
||||||
listRoutes: () => Promise.resolve(data.routes ?? []),
|
listRoutes: () => Promise.resolve(data.routes ?? []),
|
||||||
listDnsNameserverGroups: () => Promise.resolve(data.dns ?? []),
|
listDnsNameserverGroups: () => Promise.resolve(data.dns ?? []),
|
||||||
|
listPostureChecks: () => Promise.resolve(data.postureChecks ?? []),
|
||||||
|
listNetworks: () => Promise.resolve(data.networks ?? []),
|
||||||
|
listNetworkResources: (networkId: string) =>
|
||||||
|
Promise.resolve(data.networkResources?.get(networkId) ?? []),
|
||||||
|
listNetworkRouters: (networkId: string) =>
|
||||||
|
Promise.resolve(data.networkRouters?.get(networkId) ?? []),
|
||||||
|
listUsers: () => Promise.resolve(data.users ?? []),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,6 +119,7 @@ Deno.test("fetchActualState indexes all resource types", async () => {
|
|||||||
name: "allow-ops",
|
name: "allow-ops",
|
||||||
description: "ops traffic",
|
description: "ops traffic",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
source_posture_checks: [],
|
||||||
rules: [],
|
rules: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@ -2,10 +2,15 @@ import type { NetbirdClient } from "../netbird/client.ts";
|
|||||||
import type {
|
import type {
|
||||||
NbDnsNameserverGroup,
|
NbDnsNameserverGroup,
|
||||||
NbGroup,
|
NbGroup,
|
||||||
|
NbNetwork,
|
||||||
|
NbNetworkResource,
|
||||||
|
NbNetworkRouter,
|
||||||
NbPeer,
|
NbPeer,
|
||||||
NbPolicy,
|
NbPolicy,
|
||||||
|
NbPostureCheck,
|
||||||
NbRoute,
|
NbRoute,
|
||||||
NbSetupKey,
|
NbSetupKey,
|
||||||
|
NbUser,
|
||||||
} from "../netbird/types.ts";
|
} from "../netbird/types.ts";
|
||||||
|
|
||||||
/** Indexed view of all current NetBird state */
|
/** Indexed view of all current NetBird state */
|
||||||
@ -24,6 +29,14 @@ export interface ActualState {
|
|||||||
routesByNetworkId: Map<string, NbRoute>;
|
routesByNetworkId: Map<string, NbRoute>;
|
||||||
dns: NbDnsNameserverGroup[];
|
dns: NbDnsNameserverGroup[];
|
||||||
dnsByName: Map<string, NbDnsNameserverGroup>;
|
dnsByName: Map<string, NbDnsNameserverGroup>;
|
||||||
|
postureChecks: NbPostureCheck[];
|
||||||
|
postureChecksByName: Map<string, NbPostureCheck>;
|
||||||
|
networks: NbNetwork[];
|
||||||
|
networksByName: Map<string, NbNetwork>;
|
||||||
|
networkResources: Map<string, NbNetworkResource[]>; // keyed by network ID
|
||||||
|
networkRouters: Map<string, NbNetworkRouter[]>; // keyed by network ID
|
||||||
|
users: NbUser[];
|
||||||
|
usersByEmail: Map<string, NbUser>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -40,6 +53,11 @@ type ClientLike = Pick<
|
|||||||
| "listPolicies"
|
| "listPolicies"
|
||||||
| "listRoutes"
|
| "listRoutes"
|
||||||
| "listDnsNameserverGroups"
|
| "listDnsNameserverGroups"
|
||||||
|
| "listPostureChecks"
|
||||||
|
| "listNetworks"
|
||||||
|
| "listNetworkResources"
|
||||||
|
| "listNetworkRouters"
|
||||||
|
| "listUsers"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -50,15 +68,51 @@ type ClientLike = Pick<
|
|||||||
export async function fetchActualState(
|
export async function fetchActualState(
|
||||||
client: ClientLike,
|
client: ClientLike,
|
||||||
): Promise<ActualState> {
|
): Promise<ActualState> {
|
||||||
const [groups, setupKeys, peers, policies, routes, dns] = await Promise.all([
|
const [
|
||||||
|
groups,
|
||||||
|
setupKeys,
|
||||||
|
peers,
|
||||||
|
policies,
|
||||||
|
routes,
|
||||||
|
dns,
|
||||||
|
postureChecks,
|
||||||
|
networks,
|
||||||
|
users,
|
||||||
|
] = await Promise.all([
|
||||||
client.listGroups(),
|
client.listGroups(),
|
||||||
client.listSetupKeys(),
|
client.listSetupKeys(),
|
||||||
client.listPeers(),
|
client.listPeers(),
|
||||||
client.listPolicies(),
|
client.listPolicies(),
|
||||||
client.listRoutes(),
|
client.listRoutes(),
|
||||||
client.listDnsNameserverGroups(),
|
client.listDnsNameserverGroups(),
|
||||||
|
client.listPostureChecks(),
|
||||||
|
client.listNetworks(),
|
||||||
|
client.listUsers(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Fetch sub-resources for each network in parallel
|
||||||
|
const [resourcesByNetwork, routersByNetwork] = await Promise.all([
|
||||||
|
Promise.all(
|
||||||
|
networks.map(async (n) => ({
|
||||||
|
id: n.id,
|
||||||
|
resources: await client.listNetworkResources(n.id),
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
Promise.all(
|
||||||
|
networks.map(async (n) => ({
|
||||||
|
id: n.id,
|
||||||
|
routers: await client.listNetworkRouters(n.id),
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const networkResources = new Map<string, NbNetworkResource[]>(
|
||||||
|
resourcesByNetwork.map((r) => [r.id, r.resources]),
|
||||||
|
);
|
||||||
|
const networkRouters = new Map<string, NbNetworkRouter[]>(
|
||||||
|
routersByNetwork.map((r) => [r.id, r.routers]),
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
groups,
|
groups,
|
||||||
groupsByName: new Map(groups.map((g) => [g.name, g])),
|
groupsByName: new Map(groups.map((g) => [g.name, g])),
|
||||||
@ -74,5 +128,13 @@ export async function fetchActualState(
|
|||||||
routesByNetworkId: new Map(routes.map((r) => [r.network_id, r])),
|
routesByNetworkId: new Map(routes.map((r) => [r.network_id, r])),
|
||||||
dns,
|
dns,
|
||||||
dnsByName: new Map(dns.map((d) => [d.name, d])),
|
dnsByName: new Map(dns.map((d) => [d.name, d])),
|
||||||
|
postureChecks,
|
||||||
|
postureChecksByName: new Map(postureChecks.map((pc) => [pc.name, pc])),
|
||||||
|
networks,
|
||||||
|
networksByName: new Map(networks.map((n) => [n.name, n])),
|
||||||
|
networkResources,
|
||||||
|
networkRouters,
|
||||||
|
users,
|
||||||
|
usersByEmail: new Map(users.map((u) => [u.email, u])),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,15 +14,22 @@ export const GroupSchema = z.object({
|
|||||||
peers: z.array(z.string()),
|
peers: z.array(z.string()),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const DestinationResourceSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
type: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
export const PolicySchema = z.object({
|
export const PolicySchema = z.object({
|
||||||
description: z.string().default(""),
|
description: z.string().default(""),
|
||||||
enabled: z.boolean(),
|
enabled: z.boolean(),
|
||||||
sources: z.array(z.string()),
|
sources: z.array(z.string()),
|
||||||
destinations: z.array(z.string()),
|
destinations: z.array(z.string()).default([]),
|
||||||
bidirectional: z.boolean(),
|
bidirectional: z.boolean(),
|
||||||
protocol: z.enum(["tcp", "udp", "icmp", "all"]).default("all"),
|
protocol: z.enum(["tcp", "udp", "icmp", "all"]).default("all"),
|
||||||
action: z.enum(["accept", "drop"]).default("accept"),
|
action: z.enum(["accept", "drop"]).default("accept"),
|
||||||
ports: z.array(z.string()).optional(),
|
ports: z.array(z.string()).optional(),
|
||||||
|
destination_resource: DestinationResourceSchema.optional(),
|
||||||
|
source_posture_checks: z.array(z.string()).default([]),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const RouteSchema = z.object({
|
export const RouteSchema = z.object({
|
||||||
@ -53,6 +60,47 @@ export const DnsNameserverGroupSchema = z.object({
|
|||||||
search_domains_enabled: z.boolean().default(false),
|
search_domains_enabled: z.boolean().default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const PostureCheckSchema = z.object({
|
||||||
|
description: z.string().default(""),
|
||||||
|
checks: z.record(z.string(), z.unknown()),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const NetworkResourceSchema = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
description: z.string().default(""),
|
||||||
|
type: z.enum(["host", "subnet", "domain"]),
|
||||||
|
address: z.string(),
|
||||||
|
enabled: z.boolean().default(true),
|
||||||
|
groups: z.array(z.string()),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const NetworkRouterSchema = z.object({
|
||||||
|
peer: z.string().optional(),
|
||||||
|
peer_groups: z.array(z.string()).optional(),
|
||||||
|
metric: z.number().int().min(1).max(9999).default(9999),
|
||||||
|
masquerade: z.boolean().default(true),
|
||||||
|
enabled: z.boolean().default(true),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const NetworkSchema = z.object({
|
||||||
|
description: z.string().default(""),
|
||||||
|
resources: z.array(NetworkResourceSchema).default([]),
|
||||||
|
routers: z.array(NetworkRouterSchema).default([]),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const PeerSchema = z.object({
|
||||||
|
groups: z.array(z.string()),
|
||||||
|
login_expiration_enabled: z.boolean().default(false),
|
||||||
|
inactivity_expiration_enabled: z.boolean().default(false),
|
||||||
|
ssh_enabled: z.boolean().default(false),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UserSchema = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
role: z.enum(["owner", "admin", "user"]),
|
||||||
|
auto_groups: z.array(z.string()).default([]),
|
||||||
|
});
|
||||||
|
|
||||||
// --- Top-level schema ---
|
// --- Top-level schema ---
|
||||||
|
|
||||||
export const DesiredStateSchema = z.object({
|
export const DesiredStateSchema = z.object({
|
||||||
@ -64,6 +112,10 @@ export const DesiredStateSchema = z.object({
|
|||||||
nameserver_groups: z.record(z.string(), DnsNameserverGroupSchema)
|
nameserver_groups: z.record(z.string(), DnsNameserverGroupSchema)
|
||||||
.default({}),
|
.default({}),
|
||||||
}).default({ nameserver_groups: {} }),
|
}).default({ nameserver_groups: {} }),
|
||||||
|
posture_checks: z.record(z.string(), PostureCheckSchema).default({}),
|
||||||
|
networks: z.record(z.string(), NetworkSchema).default({}),
|
||||||
|
peers: z.record(z.string(), PeerSchema).default({}),
|
||||||
|
users: z.record(z.string(), UserSchema).default({}),
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Inferred types ---
|
// --- Inferred types ---
|
||||||
@ -74,6 +126,15 @@ export type GroupConfig = z.infer<typeof GroupSchema>;
|
|||||||
export type PolicyConfig = z.infer<typeof PolicySchema>;
|
export type PolicyConfig = z.infer<typeof PolicySchema>;
|
||||||
export type RouteConfig = z.infer<typeof RouteSchema>;
|
export type RouteConfig = z.infer<typeof RouteSchema>;
|
||||||
export type DnsNameserverGroupConfig = z.infer<typeof DnsNameserverGroupSchema>;
|
export type DnsNameserverGroupConfig = z.infer<typeof DnsNameserverGroupSchema>;
|
||||||
|
export type PostureCheckConfig = z.infer<typeof PostureCheckSchema>;
|
||||||
|
export type NetworkConfig = z.infer<typeof NetworkSchema>;
|
||||||
|
export type NetworkResourceConfig = z.infer<typeof NetworkResourceSchema>;
|
||||||
|
export type NetworkRouterConfig = z.infer<typeof NetworkRouterSchema>;
|
||||||
|
export type PeerConfig = z.infer<typeof PeerSchema>;
|
||||||
|
export type UserConfig = z.infer<typeof UserSchema>;
|
||||||
|
export type DestinationResourceConfig = z.infer<
|
||||||
|
typeof DestinationResourceSchema
|
||||||
|
>;
|
||||||
|
|
||||||
// --- Cross-reference validation ---
|
// --- Cross-reference validation ---
|
||||||
|
|
||||||
@ -89,11 +150,16 @@ export type DnsNameserverGroupConfig = z.infer<typeof DnsNameserverGroupSchema>;
|
|||||||
* 4. Every peer_group and distribution_group in a route references an
|
* 4. Every peer_group and distribution_group in a route references an
|
||||||
* existing group.
|
* existing group.
|
||||||
* 5. Every group in a DNS nameserver group references an existing group.
|
* 5. Every group in a DNS nameserver group references an existing group.
|
||||||
|
* 6. Every group in a peer config references an existing group.
|
||||||
|
* 7. Every auto_group on a user references an existing group.
|
||||||
|
* 8. Every group on a network resource references an existing group.
|
||||||
|
* 9. Every source_posture_check in a policy references an existing posture check.
|
||||||
*/
|
*/
|
||||||
export function validateCrossReferences(state: DesiredState): string[] {
|
export function validateCrossReferences(state: DesiredState): string[] {
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
const groupNames = new Set(Object.keys(state.groups));
|
const groupNames = new Set(Object.keys(state.groups));
|
||||||
const setupKeyNames = new Set(Object.keys(state.setup_keys));
|
const setupKeyNames = new Set(Object.keys(state.setup_keys));
|
||||||
|
const postureCheckNames = new Set(Object.keys(state.posture_checks));
|
||||||
|
|
||||||
// 1. Peers in groups must reference existing setup keys
|
// 1. Peers in groups must reference existing setup keys
|
||||||
for (const [groupName, group] of Object.entries(state.groups)) {
|
for (const [groupName, group] of Object.entries(state.groups)) {
|
||||||
@ -168,5 +234,51 @@ export function validateCrossReferences(state: DesiredState): string[] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 6. Peer groups must reference existing groups
|
||||||
|
for (const [peerName, peer] of Object.entries(state.peers)) {
|
||||||
|
for (const g of peer.groups) {
|
||||||
|
if (!groupNames.has(g)) {
|
||||||
|
errors.push(
|
||||||
|
`peer "${peerName}": group "${g}" does not match any group`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. User auto_groups must reference existing groups
|
||||||
|
for (const [userName, user] of Object.entries(state.users)) {
|
||||||
|
for (const ag of user.auto_groups) {
|
||||||
|
if (!groupNames.has(ag)) {
|
||||||
|
errors.push(
|
||||||
|
`user "${userName}": auto_group "${ag}" does not match any group`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. Network resource groups must reference existing groups
|
||||||
|
for (const [networkName, network] of Object.entries(state.networks)) {
|
||||||
|
for (const resource of network.resources) {
|
||||||
|
for (const g of resource.groups) {
|
||||||
|
if (!groupNames.has(g)) {
|
||||||
|
errors.push(
|
||||||
|
`network "${networkName}": resource "${resource.name}" group "${g}" does not match any group`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. Policy source_posture_checks must reference existing posture checks
|
||||||
|
for (const [policyName, policy] of Object.entries(state.policies)) {
|
||||||
|
for (const pc of policy.source_posture_checks) {
|
||||||
|
if (!postureCheckNames.has(pc)) {
|
||||||
|
errors.push(
|
||||||
|
`policy "${policyName}": source_posture_check "${pc}" does not match any posture check`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|||||||
414
state/dev.json
Normal file
414
state/dev.json
Normal file
@ -0,0 +1,414 @@
|
|||||||
|
{
|
||||||
|
"groups": {
|
||||||
|
"dev-team": {
|
||||||
|
"peers": []
|
||||||
|
},
|
||||||
|
"dev-services": {
|
||||||
|
"peers": []
|
||||||
|
},
|
||||||
|
"fusion": {
|
||||||
|
"peers": []
|
||||||
|
},
|
||||||
|
"test-gs": {
|
||||||
|
"peers": []
|
||||||
|
},
|
||||||
|
"restricted": {
|
||||||
|
"peers": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"setup_keys": {
|
||||||
|
"public-site": {
|
||||||
|
"type": "reusable",
|
||||||
|
"expires_in": 604800,
|
||||||
|
"usage_limit": 0,
|
||||||
|
"auto_groups": [
|
||||||
|
"dev-services"
|
||||||
|
],
|
||||||
|
"enrolled": false
|
||||||
|
},
|
||||||
|
"docs vps": {
|
||||||
|
"type": "reusable",
|
||||||
|
"expires_in": 604800,
|
||||||
|
"usage_limit": 0,
|
||||||
|
"auto_groups": [
|
||||||
|
"dev-services"
|
||||||
|
],
|
||||||
|
"enrolled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {
|
||||||
|
"Dev to test gs": {
|
||||||
|
"description": "",
|
||||||
|
"enabled": true,
|
||||||
|
"sources": [
|
||||||
|
"dev-team"
|
||||||
|
],
|
||||||
|
"destinations": [
|
||||||
|
"All"
|
||||||
|
],
|
||||||
|
"bidirectional": false,
|
||||||
|
"protocol": "all",
|
||||||
|
"action": "accept",
|
||||||
|
"source_posture_checks": []
|
||||||
|
},
|
||||||
|
"Dev Access to Gitea": {
|
||||||
|
"description": "",
|
||||||
|
"enabled": true,
|
||||||
|
"sources": [
|
||||||
|
"dev-team"
|
||||||
|
],
|
||||||
|
"destinations": [
|
||||||
|
"dev-services"
|
||||||
|
],
|
||||||
|
"bidirectional": false,
|
||||||
|
"protocol": "all",
|
||||||
|
"action": "accept",
|
||||||
|
"source_posture_checks": []
|
||||||
|
},
|
||||||
|
"Fusion Access All": {
|
||||||
|
"description": "",
|
||||||
|
"enabled": true,
|
||||||
|
"sources": [
|
||||||
|
"fusion"
|
||||||
|
],
|
||||||
|
"destinations": [
|
||||||
|
"dev-team",
|
||||||
|
"test-gs"
|
||||||
|
],
|
||||||
|
"bidirectional": true,
|
||||||
|
"protocol": "all",
|
||||||
|
"action": "accept",
|
||||||
|
"source_posture_checks": []
|
||||||
|
},
|
||||||
|
"only fusion": {
|
||||||
|
"description": "",
|
||||||
|
"enabled": false,
|
||||||
|
"sources": [
|
||||||
|
"restricted"
|
||||||
|
],
|
||||||
|
"destinations": [
|
||||||
|
"fusion"
|
||||||
|
],
|
||||||
|
"bidirectional": true,
|
||||||
|
"protocol": "all",
|
||||||
|
"action": "accept",
|
||||||
|
"source_posture_checks": []
|
||||||
|
},
|
||||||
|
"Ground Stations to Debian Repository": {
|
||||||
|
"description": "",
|
||||||
|
"enabled": true,
|
||||||
|
"sources": [
|
||||||
|
"test-gs"
|
||||||
|
],
|
||||||
|
"destinations": [
|
||||||
|
"dev-services"
|
||||||
|
],
|
||||||
|
"bidirectional": true,
|
||||||
|
"protocol": "all",
|
||||||
|
"action": "accept",
|
||||||
|
"source_posture_checks": []
|
||||||
|
},
|
||||||
|
"dev services can communicate": {
|
||||||
|
"description": "",
|
||||||
|
"enabled": true,
|
||||||
|
"sources": [
|
||||||
|
"dev-services"
|
||||||
|
],
|
||||||
|
"destinations": [
|
||||||
|
"dev-services"
|
||||||
|
],
|
||||||
|
"bidirectional": true,
|
||||||
|
"protocol": "all",
|
||||||
|
"action": "accept",
|
||||||
|
"source_posture_checks": []
|
||||||
|
},
|
||||||
|
"Everyone can access docs": {
|
||||||
|
"description": "",
|
||||||
|
"enabled": true,
|
||||||
|
"sources": [
|
||||||
|
"All"
|
||||||
|
],
|
||||||
|
"destinations": [],
|
||||||
|
"bidirectional": false,
|
||||||
|
"protocol": "all",
|
||||||
|
"action": "accept",
|
||||||
|
"source_posture_checks": [],
|
||||||
|
"destination_resource": {
|
||||||
|
"id": "docs.blastpilot.achilles-rnd.cc",
|
||||||
|
"type": "domain"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"posture_checks": {
|
||||||
|
"10.112.*.* subnet access": {
|
||||||
|
"description": "",
|
||||||
|
"checks": {
|
||||||
|
"peer_network_range_check": {
|
||||||
|
"action": "allow",
|
||||||
|
"ranges": [
|
||||||
|
"10.112.0.0/16"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"networks": {
|
||||||
|
"Internal Services": {
|
||||||
|
"description": "",
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"name": "docs.blastpilot.achilles-rnd.cc",
|
||||||
|
"description": "docs.blastpilot.achilles-rnd.cc",
|
||||||
|
"type": "domain",
|
||||||
|
"address": "docs.blastpilot.achilles-rnd.cc",
|
||||||
|
"enabled": true,
|
||||||
|
"groups": [
|
||||||
|
"All"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"routers": [
|
||||||
|
{
|
||||||
|
"metric": 9999,
|
||||||
|
"masquerade": true,
|
||||||
|
"enabled": true,
|
||||||
|
"peer": "blast-fusion"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"peers": {
|
||||||
|
"acarus": {
|
||||||
|
"groups": [
|
||||||
|
"dev-team"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": true,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"blast-fusion": {
|
||||||
|
"groups": [
|
||||||
|
"fusion"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"blastgs-fpv3": {
|
||||||
|
"groups": [
|
||||||
|
"test-gs"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"documentation-site": {
|
||||||
|
"groups": [
|
||||||
|
"dev-services"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"gitea-server": {
|
||||||
|
"groups": [
|
||||||
|
"dev-services"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"grc-1-3bat": {
|
||||||
|
"groups": [
|
||||||
|
"test-gs"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"grc-422-vlad.blast.local": {
|
||||||
|
"groups": [
|
||||||
|
"test-gs"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"ihor-rnd": {
|
||||||
|
"groups": [
|
||||||
|
"dev-team"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"ivan-rnd": {
|
||||||
|
"groups": [
|
||||||
|
"dev-team"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"multik-acer1": {
|
||||||
|
"groups": [
|
||||||
|
"dev-team"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"multik-ptt-test-gs": {
|
||||||
|
"groups": [
|
||||||
|
"dev-team",
|
||||||
|
"fusion",
|
||||||
|
"test-gs"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"oleksandr": {
|
||||||
|
"groups": [
|
||||||
|
"dev-team"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": true,
|
||||||
|
"inactivity_expiration_enabled": true,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"prox": {
|
||||||
|
"groups": [
|
||||||
|
"dev-team"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"prox-orangepi": {
|
||||||
|
"groups": [
|
||||||
|
"dev-team"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"prox-pc": {
|
||||||
|
"groups": [
|
||||||
|
"dev-team"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"prox-ubuntu-vm": {
|
||||||
|
"groups": [
|
||||||
|
"dev-team"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"public-website-vps": {
|
||||||
|
"groups": [
|
||||||
|
"dev-services"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"remote-autel-1-rnd": {
|
||||||
|
"groups": [
|
||||||
|
"dev-team"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"rpitest2": {
|
||||||
|
"groups": [
|
||||||
|
"test-gs"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"seed-asus1": {
|
||||||
|
"groups": [
|
||||||
|
"dev-team",
|
||||||
|
"fusion"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"seed-linux": {
|
||||||
|
"groups": [
|
||||||
|
"dev-team"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": true,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"seed-macbook1": {
|
||||||
|
"groups": [
|
||||||
|
"dev-team"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"testovyy-nrk-1-rnd-new-arch": {
|
||||||
|
"groups": [
|
||||||
|
"test-gs"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"ubuntu": {
|
||||||
|
"groups": [],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"admin@achilles.local": {
|
||||||
|
"name": "admin",
|
||||||
|
"role": "owner",
|
||||||
|
"auto_groups": []
|
||||||
|
},
|
||||||
|
"seed@achilles.local": {
|
||||||
|
"name": "seed",
|
||||||
|
"role": "admin",
|
||||||
|
"auto_groups": [
|
||||||
|
"dev-team"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"keltir@achilles.local": {
|
||||||
|
"name": "keltir",
|
||||||
|
"role": "admin",
|
||||||
|
"auto_groups": [
|
||||||
|
"dev-team"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"eugene@achilles.local": {
|
||||||
|
"name": "eugene",
|
||||||
|
"role": "admin",
|
||||||
|
"auto_groups": [
|
||||||
|
"dev-team"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"sava@achilles.local": {
|
||||||
|
"name": "sava",
|
||||||
|
"role": "admin",
|
||||||
|
"auto_groups": [
|
||||||
|
"dev-team"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"routes": {},
|
||||||
|
"dns": {
|
||||||
|
"nameserver_groups": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
1022
state/ext.json
Normal file
1022
state/ext.json
Normal file
File diff suppressed because it is too large
Load Diff
627
state/prod.json
Normal file
627
state/prod.json
Normal file
@ -0,0 +1,627 @@
|
|||||||
|
{
|
||||||
|
"groups": {
|
||||||
|
"battalion-1-pilots": {
|
||||||
|
"peers": []
|
||||||
|
},
|
||||||
|
"battalion-2-pilots": {
|
||||||
|
"peers": []
|
||||||
|
},
|
||||||
|
"battalion-3-pilots": {
|
||||||
|
"peers": []
|
||||||
|
},
|
||||||
|
"battalion-1-ground-stations": {
|
||||||
|
"peers": []
|
||||||
|
},
|
||||||
|
"battalion-2-ground-stations": {
|
||||||
|
"peers": []
|
||||||
|
},
|
||||||
|
"battalion-3-ground-stations": {
|
||||||
|
"peers": []
|
||||||
|
},
|
||||||
|
"dev-team": {
|
||||||
|
"peers": []
|
||||||
|
},
|
||||||
|
"fusion": {
|
||||||
|
"peers": []
|
||||||
|
},
|
||||||
|
"exp-company-ground-stations": {
|
||||||
|
"peers": []
|
||||||
|
},
|
||||||
|
"exp-company-pilots": {
|
||||||
|
"peers": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"setup_keys": {
|
||||||
|
"1bat-multik": {
|
||||||
|
"type": "reusable",
|
||||||
|
"expires_in": 604800,
|
||||||
|
"usage_limit": 10,
|
||||||
|
"auto_groups": [
|
||||||
|
"battalion-1-ground-stations",
|
||||||
|
"battalion-1-pilots"
|
||||||
|
],
|
||||||
|
"enrolled": false
|
||||||
|
},
|
||||||
|
"boots-laptops": {
|
||||||
|
"type": "reusable",
|
||||||
|
"expires_in": 604800,
|
||||||
|
"usage_limit": 5,
|
||||||
|
"auto_groups": [
|
||||||
|
"battalion-1-ground-stations",
|
||||||
|
"battalion-1-pilots"
|
||||||
|
],
|
||||||
|
"enrolled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {
|
||||||
|
"1st Battalion - Internal Access": {
|
||||||
|
"description": "Allow 1st Battalion pilots to access their ground stations",
|
||||||
|
"enabled": true,
|
||||||
|
"sources": [
|
||||||
|
"battalion-1-pilots",
|
||||||
|
"fusion"
|
||||||
|
],
|
||||||
|
"destinations": [
|
||||||
|
"battalion-1-ground-stations",
|
||||||
|
"fusion"
|
||||||
|
],
|
||||||
|
"bidirectional": true,
|
||||||
|
"protocol": "all",
|
||||||
|
"action": "accept",
|
||||||
|
"source_posture_checks": []
|
||||||
|
},
|
||||||
|
"2nd Battalion - Internal Access": {
|
||||||
|
"description": "Allow 2nd Battalion pilots to access their ground stations",
|
||||||
|
"enabled": true,
|
||||||
|
"sources": [
|
||||||
|
"battalion-2-pilots",
|
||||||
|
"fusion"
|
||||||
|
],
|
||||||
|
"destinations": [
|
||||||
|
"battalion-2-ground-stations",
|
||||||
|
"fusion"
|
||||||
|
],
|
||||||
|
"bidirectional": true,
|
||||||
|
"protocol": "all",
|
||||||
|
"action": "accept",
|
||||||
|
"source_posture_checks": []
|
||||||
|
},
|
||||||
|
"3rd Battalion - Internal Access": {
|
||||||
|
"description": "Allow 3rd Battalion pilots to access their ground stations",
|
||||||
|
"enabled": true,
|
||||||
|
"sources": [
|
||||||
|
"battalion-3-pilots",
|
||||||
|
"fusion"
|
||||||
|
],
|
||||||
|
"destinations": [
|
||||||
|
"battalion-3-ground-stations",
|
||||||
|
"fusion"
|
||||||
|
],
|
||||||
|
"bidirectional": true,
|
||||||
|
"protocol": "all",
|
||||||
|
"action": "accept",
|
||||||
|
"source_posture_checks": []
|
||||||
|
},
|
||||||
|
"Dev Team - Full Access": {
|
||||||
|
"description": "Dev team can access all peers for troubleshooting",
|
||||||
|
"enabled": true,
|
||||||
|
"sources": [
|
||||||
|
"dev-team"
|
||||||
|
],
|
||||||
|
"destinations": [
|
||||||
|
"All"
|
||||||
|
],
|
||||||
|
"bidirectional": true,
|
||||||
|
"protocol": "all",
|
||||||
|
"action": "accept",
|
||||||
|
"source_posture_checks": [
|
||||||
|
"Restrict admins to Ukraine"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Fusion Access All Pilots and Ground Stations": {
|
||||||
|
"description": "",
|
||||||
|
"enabled": true,
|
||||||
|
"sources": [
|
||||||
|
"fusion"
|
||||||
|
],
|
||||||
|
"destinations": [
|
||||||
|
"dev-team",
|
||||||
|
"exp-company-ground-stations",
|
||||||
|
"exp-company-pilots",
|
||||||
|
"battalion-1-ground-stations",
|
||||||
|
"battalion-2-ground-stations",
|
||||||
|
"battalion-2-pilots",
|
||||||
|
"battalion-3-ground-stations",
|
||||||
|
"battalion-3-pilots",
|
||||||
|
"battalion-1-pilots"
|
||||||
|
],
|
||||||
|
"bidirectional": true,
|
||||||
|
"protocol": "all",
|
||||||
|
"action": "accept",
|
||||||
|
"source_posture_checks": []
|
||||||
|
},
|
||||||
|
"exp-company-pilots2gs": {
|
||||||
|
"description": "",
|
||||||
|
"enabled": true,
|
||||||
|
"sources": [
|
||||||
|
"exp-company-pilots",
|
||||||
|
"fusion"
|
||||||
|
],
|
||||||
|
"destinations": [
|
||||||
|
"exp-company-ground-stations",
|
||||||
|
"fusion"
|
||||||
|
],
|
||||||
|
"bidirectional": true,
|
||||||
|
"protocol": "all",
|
||||||
|
"action": "accept",
|
||||||
|
"source_posture_checks": []
|
||||||
|
},
|
||||||
|
"Everyone can access docs": {
|
||||||
|
"description": "Internal Services ",
|
||||||
|
"enabled": false,
|
||||||
|
"sources": [
|
||||||
|
"All"
|
||||||
|
],
|
||||||
|
"destinations": [],
|
||||||
|
"bidirectional": false,
|
||||||
|
"protocol": "all",
|
||||||
|
"action": "accept",
|
||||||
|
"source_posture_checks": [],
|
||||||
|
"destination_resource": {
|
||||||
|
"id": "docs.blastpilot.achilles-rnd.cc",
|
||||||
|
"type": "domain"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"posture_checks": {
|
||||||
|
"Restrict admins to Ukraine": {
|
||||||
|
"description": "",
|
||||||
|
"checks": {
|
||||||
|
"geo_location_check": {
|
||||||
|
"action": "allow",
|
||||||
|
"locations": [
|
||||||
|
{
|
||||||
|
"country_code": "UA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"country_code": "PL"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"networks": {
|
||||||
|
"Internal Services": {
|
||||||
|
"description": "",
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"name": "docs.blastpilot.achilles-rnd.cc",
|
||||||
|
"description": "docs.blastpilot.achilles-rnd.cc",
|
||||||
|
"type": "domain",
|
||||||
|
"address": "docs.blastpilot.achilles-rnd.cc",
|
||||||
|
"enabled": true,
|
||||||
|
"groups": [
|
||||||
|
"All"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"routers": [
|
||||||
|
{
|
||||||
|
"metric": 9999,
|
||||||
|
"masquerade": true,
|
||||||
|
"enabled": true,
|
||||||
|
"peer": "blast-fusion"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"peers": {
|
||||||
|
"3bat-goggles-laptop": {
|
||||||
|
"groups": [
|
||||||
|
"battalion-3-pilots",
|
||||||
|
"battalion-3-ground-stations"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"3bat-lin-win-laptop": {
|
||||||
|
"groups": [
|
||||||
|
"battalion-3-pilots",
|
||||||
|
"battalion-3-ground-stations"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"3bat-linux-laptop": {
|
||||||
|
"groups": [
|
||||||
|
"battalion-3-pilots",
|
||||||
|
"battalion-3-ground-stations"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"acarus": {
|
||||||
|
"groups": [
|
||||||
|
"dev-team"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": true,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"banya-slackware-laptop": {
|
||||||
|
"groups": [
|
||||||
|
"battalion-3-pilots",
|
||||||
|
"battalion-3-ground-stations"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"banya1-laptop": {
|
||||||
|
"groups": [
|
||||||
|
"battalion-3-pilots",
|
||||||
|
"battalion-3-ground-stations"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"banya2-laptop": {
|
||||||
|
"groups": [
|
||||||
|
"battalion-3-pilots",
|
||||||
|
"battalion-3-ground-stations"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"banya3-laptop": {
|
||||||
|
"groups": [
|
||||||
|
"battalion-3-pilots",
|
||||||
|
"battalion-3-ground-stations"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"banya4-laptop": {
|
||||||
|
"groups": [
|
||||||
|
"battalion-3-pilots",
|
||||||
|
"battalion-3-ground-stations"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"bilozir1-laptop": {
|
||||||
|
"groups": [
|
||||||
|
"battalion-2-pilots",
|
||||||
|
"battalion-2-ground-stations"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"blast-fusion": {
|
||||||
|
"groups": [
|
||||||
|
"fusion"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"blastgs-agent-dji-goggles1": {
|
||||||
|
"groups": [
|
||||||
|
"battalion-3-pilots",
|
||||||
|
"battalion-3-ground-stations"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"boots1-laptop": {
|
||||||
|
"groups": [
|
||||||
|
"battalion-1-pilots",
|
||||||
|
"battalion-1-ground-stations"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"boots2-laptop": {
|
||||||
|
"groups": [
|
||||||
|
"battalion-1-pilots",
|
||||||
|
"battalion-1-ground-stations"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"exp-lenovo-laptop": {
|
||||||
|
"groups": [
|
||||||
|
"exp-company-ground-stations",
|
||||||
|
"exp-company-pilots"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"ihor-rnd-laptop": {
|
||||||
|
"groups": [
|
||||||
|
"dev-team"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"ivan-rnd-laptop": {
|
||||||
|
"groups": [
|
||||||
|
"dev-team"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"kaban-1-laptop": {
|
||||||
|
"groups": [
|
||||||
|
"battalion-1-pilots",
|
||||||
|
"battalion-1-ground-stations"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"kaban-2-laptop-1bat": {
|
||||||
|
"groups": [
|
||||||
|
"battalion-1-pilots",
|
||||||
|
"battalion-1-ground-stations"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"kaban-3-laptop-1bat": {
|
||||||
|
"groups": [
|
||||||
|
"battalion-1-pilots",
|
||||||
|
"battalion-1-ground-stations"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"mango-rexp1-laptop": {
|
||||||
|
"groups": [
|
||||||
|
"exp-company-ground-stations",
|
||||||
|
"exp-company-pilots"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"mavic-rnd-laptop": {
|
||||||
|
"groups": [
|
||||||
|
"dev-team"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"multik-rnd-laptop": {
|
||||||
|
"groups": [
|
||||||
|
"dev-team"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"oleksandr-rnd-laptop": {
|
||||||
|
"groups": [
|
||||||
|
"dev-team"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"prox-rnd-laptop": {
|
||||||
|
"groups": [
|
||||||
|
"dev-team"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"remote-autel-1-1bat-1rrbpak": {
|
||||||
|
"groups": [
|
||||||
|
"battalion-1-pilots",
|
||||||
|
"battalion-1-ground-stations"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"remote-autel-1-3bat-5rrbpak": {
|
||||||
|
"groups": [
|
||||||
|
"battalion-3-pilots",
|
||||||
|
"battalion-3-ground-stations"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"remote-autel-1-rexp": {
|
||||||
|
"groups": [
|
||||||
|
"exp-company-ground-stations",
|
||||||
|
"exp-company-pilots"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"remote-autel-2-1bat-1rrbpak": {
|
||||||
|
"groups": [
|
||||||
|
"battalion-1-pilots",
|
||||||
|
"battalion-1-ground-stations"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"remote-autel-2-3bat-5rrbpak": {
|
||||||
|
"groups": [
|
||||||
|
"battalion-3-pilots",
|
||||||
|
"battalion-3-ground-stations"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"remote-autel-2-rexp": {
|
||||||
|
"groups": [
|
||||||
|
"battalion-2-pilots",
|
||||||
|
"battalion-2-ground-stations"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"remote-autel-3-1bat": {
|
||||||
|
"groups": [
|
||||||
|
"battalion-1-pilots",
|
||||||
|
"battalion-1-ground-stations"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"remote-autel-3-2bat-3rrbpak": {
|
||||||
|
"groups": [
|
||||||
|
"battalion-2-pilots",
|
||||||
|
"battalion-2-ground-stations"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"remote-autel-3-3bat-5rrbpak": {
|
||||||
|
"groups": [
|
||||||
|
"battalion-3-pilots",
|
||||||
|
"battalion-3-ground-stations"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"remote-autel-4-1bat": {
|
||||||
|
"groups": [
|
||||||
|
"battalion-1-pilots",
|
||||||
|
"battalion-1-ground-stations"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"remote-autel-4-2bat-3rrbpak": {
|
||||||
|
"groups": [
|
||||||
|
"battalion-2-pilots",
|
||||||
|
"battalion-2-ground-stations"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"remote-matrice-1-3bat-5rrbpak": {
|
||||||
|
"groups": [
|
||||||
|
"battalion-3-pilots",
|
||||||
|
"battalion-3-ground-stations"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"rexp-lenovo-laptop": {
|
||||||
|
"groups": [
|
||||||
|
"exp-company-ground-stations",
|
||||||
|
"exp-company-pilots"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"seed-1-rnd-laptop": {
|
||||||
|
"groups": [
|
||||||
|
"dev-team"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"seed-asus1": {
|
||||||
|
"groups": [
|
||||||
|
"dev-team"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"seed-macbook1": {
|
||||||
|
"groups": [
|
||||||
|
"dev-team"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
},
|
||||||
|
"ugv-1-1bat": {
|
||||||
|
"groups": [
|
||||||
|
"battalion-1-pilots",
|
||||||
|
"battalion-1-ground-stations"
|
||||||
|
],
|
||||||
|
"login_expiration_enabled": false,
|
||||||
|
"inactivity_expiration_enabled": false,
|
||||||
|
"ssh_enabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"seed@achilles.local": {
|
||||||
|
"name": "seed",
|
||||||
|
"role": "admin",
|
||||||
|
"auto_groups": [
|
||||||
|
"dev-team"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"keltir@achilles.local": {
|
||||||
|
"name": "Artem",
|
||||||
|
"role": "admin",
|
||||||
|
"auto_groups": [
|
||||||
|
"dev-team"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"vlad.stus@gmail.com": {
|
||||||
|
"name": "admin",
|
||||||
|
"role": "owner",
|
||||||
|
"auto_groups": [
|
||||||
|
"dev-team"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"": {
|
||||||
|
"name": "Automation Service",
|
||||||
|
"role": "admin",
|
||||||
|
"auto_groups": []
|
||||||
|
},
|
||||||
|
"eugene@achilles.local": {
|
||||||
|
"name": "eugene",
|
||||||
|
"role": "admin",
|
||||||
|
"auto_groups": [
|
||||||
|
"dev-team"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"routes": {},
|
||||||
|
"dns": {
|
||||||
|
"nameserver_groups": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user