@ankhorage/infra 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +17 -0
- package/dist/adapters/minikube/auth/index.d.ts +7 -0
- package/dist/adapters/minikube/auth/index.d.ts.map +1 -0
- package/dist/adapters/minikube/auth/index.js +14 -0
- package/dist/adapters/minikube/auth/index.js.map +1 -0
- package/dist/adapters/minikube/auth/supabase/index.d.ts +7 -0
- package/dist/adapters/minikube/auth/supabase/index.d.ts.map +1 -0
- package/dist/adapters/minikube/auth/supabase/index.js +213 -0
- package/dist/adapters/minikube/auth/supabase/index.js.map +1 -0
- package/dist/adapters/minikube/authz/cerbos/index.d.ts +9 -0
- package/dist/adapters/minikube/authz/cerbos/index.d.ts.map +1 -0
- package/dist/adapters/minikube/authz/cerbos/index.js +360 -0
- package/dist/adapters/minikube/authz/cerbos/index.js.map +1 -0
- package/dist/adapters/minikube/authz/index.d.ts +9 -0
- package/dist/adapters/minikube/authz/index.d.ts.map +1 -0
- package/dist/adapters/minikube/authz/index.js +23 -0
- package/dist/adapters/minikube/authz/index.js.map +1 -0
- package/dist/adapters/minikube/base/index.d.ts +9 -0
- package/dist/adapters/minikube/base/index.d.ts.map +1 -0
- package/dist/adapters/minikube/base/index.js +1371 -0
- package/dist/adapters/minikube/base/index.js.map +1 -0
- package/dist/adapters/minikube/contracts.d.ts +9 -0
- package/dist/adapters/minikube/contracts.d.ts.map +1 -0
- package/dist/adapters/minikube/contracts.js +9 -0
- package/dist/adapters/minikube/contracts.js.map +1 -0
- package/dist/adapters/minikube/index.d.ts +3 -0
- package/dist/adapters/minikube/index.d.ts.map +1 -0
- package/dist/adapters/minikube/index.js +114 -0
- package/dist/adapters/minikube/index.js.map +1 -0
- package/dist/adapters/minikube/storage/index.d.ts +7 -0
- package/dist/adapters/minikube/storage/index.d.ts.map +1 -0
- package/dist/adapters/minikube/storage/index.js +77 -0
- package/dist/adapters/minikube/storage/index.js.map +1 -0
- package/dist/adapters/minikube/storage/supabase/index.d.ts +7 -0
- package/dist/adapters/minikube/storage/supabase/index.d.ts.map +1 -0
- package/dist/adapters/minikube/storage/supabase/index.js +81 -0
- package/dist/adapters/minikube/storage/supabase/index.js.map +1 -0
- package/dist/datasets.d.ts +11 -0
- package/dist/datasets.d.ts.map +1 -0
- package/dist/datasets.js +397 -0
- package/dist/datasets.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +70 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +86 -0
- package/src/adapters/minikube/README.md +73 -0
- package/src/adapters/minikube/auth/index.ts +23 -0
- package/src/adapters/minikube/auth/supabase/index.ts +269 -0
- package/src/adapters/minikube/authz/cerbos/index.ts +458 -0
- package/src/adapters/minikube/authz/index.ts +32 -0
- package/src/adapters/minikube/base/index.test.ts +103 -0
- package/src/adapters/minikube/base/index.ts +1478 -0
- package/src/adapters/minikube/contracts.ts +17 -0
- package/src/adapters/minikube/index.ts +149 -0
- package/src/adapters/minikube/storage/index.ts +91 -0
- package/src/adapters/minikube/storage/supabase/index.ts +112 -0
- package/src/datasets.test.ts +178 -0
- package/src/datasets.ts +562 -0
- package/src/index.test.ts +679 -0
- package/src/index.ts +43 -0
- package/src/minikubeImageLifecycle.test.ts +34 -0
- package/src/types.ts +82 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
import type { AppManifest, NavigatorSpec, RouteDefinition } from '@ankhorage/contracts';
|
|
2
|
+
|
|
3
|
+
import type { InfraManifestInput } from '../../../../types';
|
|
4
|
+
import type { MinikubeAdapterArtifacts } from '../../contracts';
|
|
5
|
+
|
|
6
|
+
export function generateCerbosAuthzArtifacts(args: {
|
|
7
|
+
manifest: InfraManifestInput;
|
|
8
|
+
namespace: string;
|
|
9
|
+
appManifest?: Pick<AppManifest, 'metadata' | 'navigator' | 'screens' | 'settings'>;
|
|
10
|
+
}): MinikubeAdapterArtifacts {
|
|
11
|
+
const { manifest, namespace, appManifest } = args;
|
|
12
|
+
|
|
13
|
+
const root = 'infra/minikube/k8s/authz/cerbos';
|
|
14
|
+
const resourceRoot = 'authz/cerbos';
|
|
15
|
+
const intent = buildCerbosPolicyIntent({ manifest, appManifest });
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
files: [
|
|
19
|
+
{
|
|
20
|
+
path: `${root}/cerbos.configmap.yaml`,
|
|
21
|
+
content: getCerbosConfigMap(namespace),
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
path: `${root}/cerbos.policy.configmap.yaml`,
|
|
25
|
+
content: getCerbosPolicyConfigMap({ namespace, intent }),
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
path: `${root}/cerbos.deployment.yaml`,
|
|
29
|
+
content: getCerbosDeployment(namespace),
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
path: `${root}/cerbos.service.yaml`,
|
|
33
|
+
content: getCerbosService(namespace),
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
resources: [
|
|
37
|
+
`${resourceRoot}/cerbos.configmap.yaml`,
|
|
38
|
+
`${resourceRoot}/cerbos.policy.configmap.yaml`,
|
|
39
|
+
`${resourceRoot}/cerbos.deployment.yaml`,
|
|
40
|
+
`${resourceRoot}/cerbos.service.yaml`,
|
|
41
|
+
],
|
|
42
|
+
envEntries: ['CERBOS_URL=http://cerbos:3592'],
|
|
43
|
+
warnings: [],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getCerbosConfigMap(namespace: string) {
|
|
48
|
+
return `apiVersion: v1
|
|
49
|
+
kind: ConfigMap
|
|
50
|
+
metadata:
|
|
51
|
+
name: cerbos-config
|
|
52
|
+
namespace: ${namespace}
|
|
53
|
+
data:
|
|
54
|
+
config.yaml: |
|
|
55
|
+
server:
|
|
56
|
+
httpListenAddr: ":3592"
|
|
57
|
+
storage:
|
|
58
|
+
driver: "disk"
|
|
59
|
+
disk:
|
|
60
|
+
directory: /policies
|
|
61
|
+
`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getCerbosPolicyConfigMap(args: { namespace: string; intent: CerbosPolicyIntent }) {
|
|
65
|
+
const { namespace, intent } = args;
|
|
66
|
+
const entries = getCerbosPolicyEntries(intent);
|
|
67
|
+
const renderedEntries = entries
|
|
68
|
+
.map((entry) => {
|
|
69
|
+
const indented = indentLines(entry.content, 4);
|
|
70
|
+
return ` ${entry.key}: |
|
|
71
|
+
${indented}`;
|
|
72
|
+
})
|
|
73
|
+
.join('\n');
|
|
74
|
+
|
|
75
|
+
return `apiVersion: v1
|
|
76
|
+
kind: ConfigMap
|
|
77
|
+
metadata:
|
|
78
|
+
name: cerbos-policy
|
|
79
|
+
namespace: ${namespace}
|
|
80
|
+
data:
|
|
81
|
+
${renderedEntries}
|
|
82
|
+
`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getCerbosDeployment(namespace: string) {
|
|
86
|
+
return `apiVersion: apps/v1
|
|
87
|
+
kind: Deployment
|
|
88
|
+
metadata:
|
|
89
|
+
name: cerbos
|
|
90
|
+
namespace: ${namespace}
|
|
91
|
+
labels:
|
|
92
|
+
app.kubernetes.io/name: cerbos
|
|
93
|
+
spec:
|
|
94
|
+
replicas: 1
|
|
95
|
+
selector:
|
|
96
|
+
matchLabels:
|
|
97
|
+
app.kubernetes.io/name: cerbos
|
|
98
|
+
template:
|
|
99
|
+
metadata:
|
|
100
|
+
labels:
|
|
101
|
+
app.kubernetes.io/name: cerbos
|
|
102
|
+
spec:
|
|
103
|
+
containers:
|
|
104
|
+
- name: cerbos
|
|
105
|
+
image: ghcr.io/cerbos/cerbos:0.40.0
|
|
106
|
+
args:
|
|
107
|
+
- server
|
|
108
|
+
- --config=/config/config.yaml
|
|
109
|
+
ports:
|
|
110
|
+
- containerPort: 3592
|
|
111
|
+
volumeMounts:
|
|
112
|
+
- name: cerbos-config
|
|
113
|
+
mountPath: /config
|
|
114
|
+
- name: cerbos-policy
|
|
115
|
+
mountPath: /policies
|
|
116
|
+
volumes:
|
|
117
|
+
- name: cerbos-config
|
|
118
|
+
configMap:
|
|
119
|
+
name: cerbos-config
|
|
120
|
+
- name: cerbos-policy
|
|
121
|
+
configMap:
|
|
122
|
+
name: cerbos-policy
|
|
123
|
+
`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function getCerbosService(namespace: string) {
|
|
127
|
+
return `apiVersion: v1
|
|
128
|
+
kind: Service
|
|
129
|
+
metadata:
|
|
130
|
+
name: cerbos
|
|
131
|
+
namespace: ${namespace}
|
|
132
|
+
spec:
|
|
133
|
+
selector:
|
|
134
|
+
app.kubernetes.io/name: cerbos
|
|
135
|
+
ports:
|
|
136
|
+
- name: http
|
|
137
|
+
port: 3592
|
|
138
|
+
targetPort: 3592
|
|
139
|
+
`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const PUBLIC_GUARD_HINTS = new Set(['public', 'guest', 'anonymous', 'unauthenticated']);
|
|
143
|
+
const AUTH_GUARD_HINTS = new Set([
|
|
144
|
+
'auth',
|
|
145
|
+
'authenticated',
|
|
146
|
+
'private',
|
|
147
|
+
'protected',
|
|
148
|
+
'requiresauth',
|
|
149
|
+
'requires-auth',
|
|
150
|
+
]);
|
|
151
|
+
|
|
152
|
+
type AuthScope = NonNullable<InfraManifestInput['auth']>['scope'] | 'none';
|
|
153
|
+
|
|
154
|
+
interface CerbosRouteIntent {
|
|
155
|
+
name: string;
|
|
156
|
+
guards: string[];
|
|
157
|
+
screenId?: string;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
interface CerbosPolicyIntent {
|
|
161
|
+
authScope: AuthScope;
|
|
162
|
+
publicRoutes: string[];
|
|
163
|
+
protectedRoutes: string[];
|
|
164
|
+
publicScreens: string[];
|
|
165
|
+
protectedScreens: string[];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
interface CerbosPolicyEntry {
|
|
169
|
+
key: string;
|
|
170
|
+
content: string;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function getCerbosPolicyEntries(intent: CerbosPolicyIntent): CerbosPolicyEntry[] {
|
|
174
|
+
const entries: CerbosPolicyEntry[] = [
|
|
175
|
+
{
|
|
176
|
+
key: 'app.resource_policy.yaml',
|
|
177
|
+
content: getAppPolicy(intent),
|
|
178
|
+
},
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
if (intent.publicRoutes.length > 0 || intent.protectedRoutes.length > 0) {
|
|
182
|
+
entries.push({
|
|
183
|
+
key: 'route.resource_policy.yaml',
|
|
184
|
+
content: getRoutePolicy(intent),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (intent.publicScreens.length > 0 || intent.protectedScreens.length > 0) {
|
|
189
|
+
entries.push({
|
|
190
|
+
key: 'screen.resource_policy.yaml',
|
|
191
|
+
content: getScreenPolicy(intent),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return entries;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function getAppPolicy(intent: CerbosPolicyIntent): string {
|
|
199
|
+
const roles = intent.authScope === 'global' ? ['authenticated'] : ['anonymous', 'authenticated'];
|
|
200
|
+
|
|
201
|
+
return `apiVersion: api.cerbos.dev/v1
|
|
202
|
+
resourcePolicy:
|
|
203
|
+
version: "default"
|
|
204
|
+
resource: "app"
|
|
205
|
+
rules:
|
|
206
|
+
${indentLines(renderRule({ actions: ['*'], roles }), 4)}
|
|
207
|
+
`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function getRoutePolicy(intent: CerbosPolicyIntent): string {
|
|
211
|
+
const rules: string[] = [];
|
|
212
|
+
|
|
213
|
+
if (intent.publicRoutes.length > 0) {
|
|
214
|
+
rules.push(
|
|
215
|
+
renderRule({
|
|
216
|
+
actions: ['view', 'navigate'],
|
|
217
|
+
roles: ['anonymous', 'authenticated'],
|
|
218
|
+
conditionExpr: getInConditionExpr('route', intent.publicRoutes),
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (intent.protectedRoutes.length > 0) {
|
|
224
|
+
rules.push(
|
|
225
|
+
renderRule({
|
|
226
|
+
actions: ['view', 'navigate'],
|
|
227
|
+
roles: ['authenticated'],
|
|
228
|
+
conditionExpr: getInConditionExpr('route', intent.protectedRoutes),
|
|
229
|
+
}),
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return `apiVersion: api.cerbos.dev/v1
|
|
234
|
+
resourcePolicy:
|
|
235
|
+
version: "default"
|
|
236
|
+
resource: "route"
|
|
237
|
+
rules:
|
|
238
|
+
${indentLines(rules.join('\n'), 4)}
|
|
239
|
+
`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function getScreenPolicy(intent: CerbosPolicyIntent): string {
|
|
243
|
+
const rules: string[] = [];
|
|
244
|
+
|
|
245
|
+
if (intent.publicScreens.length > 0) {
|
|
246
|
+
rules.push(
|
|
247
|
+
renderRule({
|
|
248
|
+
actions: ['view', 'render'],
|
|
249
|
+
roles: ['anonymous', 'authenticated'],
|
|
250
|
+
conditionExpr: getInConditionExpr('screen_id', intent.publicScreens),
|
|
251
|
+
}),
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (intent.protectedScreens.length > 0) {
|
|
256
|
+
rules.push(
|
|
257
|
+
renderRule({
|
|
258
|
+
actions: ['view', 'render'],
|
|
259
|
+
roles: ['authenticated'],
|
|
260
|
+
conditionExpr: getInConditionExpr('screen_id', intent.protectedScreens),
|
|
261
|
+
}),
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return `apiVersion: api.cerbos.dev/v1
|
|
266
|
+
resourcePolicy:
|
|
267
|
+
version: "default"
|
|
268
|
+
resource: "screen"
|
|
269
|
+
rules:
|
|
270
|
+
${indentLines(rules.join('\n'), 4)}
|
|
271
|
+
`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function renderRule(args: { actions: string[]; roles: string[]; conditionExpr?: string }): string {
|
|
275
|
+
const { actions, roles, conditionExpr } = args;
|
|
276
|
+
|
|
277
|
+
const lines = [
|
|
278
|
+
`- actions: ${JSON.stringify(actions)}`,
|
|
279
|
+
' effect: EFFECT_ALLOW',
|
|
280
|
+
` roles: ${JSON.stringify(roles)}`,
|
|
281
|
+
];
|
|
282
|
+
|
|
283
|
+
if (conditionExpr) {
|
|
284
|
+
lines.push(' condition:');
|
|
285
|
+
lines.push(' match:');
|
|
286
|
+
lines.push(` expr: ${JSON.stringify(conditionExpr)}`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return lines.join('\n');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function getInConditionExpr(attribute: string, values: string[]): string {
|
|
293
|
+
return `request.resource.attr.${attribute} in ${JSON.stringify(values)}`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function indentLines(content: string, spaces: number): string {
|
|
297
|
+
const prefix = ' '.repeat(spaces);
|
|
298
|
+
return content
|
|
299
|
+
.trimEnd()
|
|
300
|
+
.split('\n')
|
|
301
|
+
.map((line) => `${prefix}${line}`)
|
|
302
|
+
.join('\n');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function buildCerbosPolicyIntent(args: {
|
|
306
|
+
manifest: InfraManifestInput;
|
|
307
|
+
appManifest?: Pick<AppManifest, 'metadata' | 'navigator' | 'screens' | 'settings'>;
|
|
308
|
+
}): CerbosPolicyIntent {
|
|
309
|
+
const { manifest, appManifest } = args;
|
|
310
|
+
const authScope = manifest.auth?.scope ?? 'none';
|
|
311
|
+
|
|
312
|
+
const routes = appManifest?.navigator ? flattenNavigatorRoutes(appManifest.navigator) : [];
|
|
313
|
+
const signInRoute = appManifest?.settings.authFlow.signInRoute;
|
|
314
|
+
const signUpRoute = appManifest?.settings.authFlow.signUpRoute;
|
|
315
|
+
const unauthorizedRoute = appManifest?.settings.authFlow.unauthorizedRoute;
|
|
316
|
+
|
|
317
|
+
const publicRouteSet = new Set<string>();
|
|
318
|
+
const protectedRouteSet = new Set<string>();
|
|
319
|
+
|
|
320
|
+
for (const route of routes) {
|
|
321
|
+
const isPublic = isPublicRoute({
|
|
322
|
+
route,
|
|
323
|
+
authScope,
|
|
324
|
+
signInRoute,
|
|
325
|
+
signUpRoute,
|
|
326
|
+
unauthorizedRoute,
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
if (isPublic) {
|
|
330
|
+
publicRouteSet.add(route.name);
|
|
331
|
+
} else {
|
|
332
|
+
protectedRouteSet.add(route.name);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const screenVisibility = new Map<string, 'public' | 'protected'>();
|
|
337
|
+
for (const route of routes) {
|
|
338
|
+
if (!route.screenId) continue;
|
|
339
|
+
const isPublic = publicRouteSet.has(route.name);
|
|
340
|
+
|
|
341
|
+
const current = screenVisibility.get(route.screenId);
|
|
342
|
+
if (current === 'protected') continue;
|
|
343
|
+
screenVisibility.set(route.screenId, isPublic ? 'public' : 'protected');
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (appManifest?.screens) {
|
|
347
|
+
const defaultVisibility: 'public' | 'protected' =
|
|
348
|
+
authScope === 'global' ? 'protected' : 'public';
|
|
349
|
+
|
|
350
|
+
for (const screenId of Object.keys(appManifest.screens)) {
|
|
351
|
+
if (!screenVisibility.has(screenId)) {
|
|
352
|
+
screenVisibility.set(screenId, defaultVisibility);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const publicScreens = [...screenVisibility.entries()]
|
|
358
|
+
.filter(([, visibility]) => visibility === 'public')
|
|
359
|
+
.map(([screenId]) => screenId)
|
|
360
|
+
.sort();
|
|
361
|
+
const protectedScreens = [...screenVisibility.entries()]
|
|
362
|
+
.filter(([, visibility]) => visibility === 'protected')
|
|
363
|
+
.map(([screenId]) => screenId)
|
|
364
|
+
.sort();
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
authScope,
|
|
368
|
+
publicRoutes: [...publicRouteSet].sort(),
|
|
369
|
+
protectedRoutes: [...protectedRouteSet].sort(),
|
|
370
|
+
publicScreens,
|
|
371
|
+
protectedScreens,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function flattenNavigatorRoutes(navigator: NavigatorSpec): CerbosRouteIntent[] {
|
|
376
|
+
const deduped = new Map<string, CerbosRouteIntent>();
|
|
377
|
+
|
|
378
|
+
const walk = (current: NavigatorSpec) => {
|
|
379
|
+
for (const route of current.routes) {
|
|
380
|
+
const normalizedGuards = normalizeGuards(route.guards);
|
|
381
|
+
const existing = deduped.get(route.name);
|
|
382
|
+
deduped.set(route.name, {
|
|
383
|
+
name: route.name,
|
|
384
|
+
guards: unique([...(existing?.guards ?? []), ...normalizedGuards]),
|
|
385
|
+
screenId: route.screenId ?? existing?.screenId,
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
if (route.navigator) {
|
|
389
|
+
walk(route.navigator);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
walk(navigator);
|
|
395
|
+
return [...deduped.values()];
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function normalizeGuards(guards?: RouteDefinition['guards']): string[] {
|
|
399
|
+
return (guards ?? []).map((guard) => guard.trim().toLowerCase()).filter(Boolean);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function isPublicRoute(args: {
|
|
403
|
+
route: CerbosRouteIntent;
|
|
404
|
+
authScope: AuthScope;
|
|
405
|
+
signInRoute?: string;
|
|
406
|
+
signUpRoute?: string;
|
|
407
|
+
unauthorizedRoute?: string;
|
|
408
|
+
}): boolean {
|
|
409
|
+
const { route, authScope, signInRoute, signUpRoute, unauthorizedRoute } = args;
|
|
410
|
+
const guardSet = new Set(route.guards);
|
|
411
|
+
const signInRouteName = authFlowPathToRouteName(signInRoute);
|
|
412
|
+
const signUpRouteName = authFlowPathToRouteName(signUpRoute);
|
|
413
|
+
const unauthorizedRouteName = authFlowPathToRouteName(unauthorizedRoute);
|
|
414
|
+
|
|
415
|
+
if (
|
|
416
|
+
route.name === signInRouteName ||
|
|
417
|
+
route.name === signUpRouteName ||
|
|
418
|
+
route.name === unauthorizedRouteName
|
|
419
|
+
) {
|
|
420
|
+
return true;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (hasAny(guardSet, PUBLIC_GUARD_HINTS)) {
|
|
424
|
+
return true;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (hasAny(guardSet, AUTH_GUARD_HINTS)) {
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
switch (authScope) {
|
|
432
|
+
case 'none':
|
|
433
|
+
return true;
|
|
434
|
+
case 'global':
|
|
435
|
+
default:
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function hasAny(values: Set<string>, candidates: Set<string>): boolean {
|
|
441
|
+
for (const candidate of candidates) {
|
|
442
|
+
if (values.has(candidate)) return true;
|
|
443
|
+
}
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function unique(values: string[]): string[] {
|
|
448
|
+
return [...new Set(values)];
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function authFlowPathToRouteName(routePath: string | undefined): string | undefined {
|
|
452
|
+
if (!routePath) {
|
|
453
|
+
return undefined;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const normalized = routePath.trim().replace(/^\/+/, '').replace(/\/+$/, '');
|
|
457
|
+
return normalized === '' ? 'index' : normalized;
|
|
458
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { AppManifest } from '@ankhorage/contracts';
|
|
2
|
+
|
|
3
|
+
import type { InfraManifestInput } from '../../../types';
|
|
4
|
+
import { emptyMinikubeArtifacts, type MinikubeAdapterArtifacts } from '../contracts';
|
|
5
|
+
import { generateCerbosAuthzArtifacts } from './cerbos';
|
|
6
|
+
|
|
7
|
+
export function generateAuthorizationArtifacts(args: {
|
|
8
|
+
manifest: InfraManifestInput;
|
|
9
|
+
namespace: string;
|
|
10
|
+
appManifest?: Pick<AppManifest, 'metadata' | 'navigator' | 'screens' | 'settings'>;
|
|
11
|
+
}): MinikubeAdapterArtifacts {
|
|
12
|
+
const { manifest, namespace, appManifest } = args;
|
|
13
|
+
const engine = manifest.auth?.authorization.engine;
|
|
14
|
+
|
|
15
|
+
if (!engine) {
|
|
16
|
+
return emptyMinikubeArtifacts();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
switch (engine) {
|
|
20
|
+
case 'cerbos':
|
|
21
|
+
return generateCerbosAuthzArtifacts({ manifest, namespace, appManifest });
|
|
22
|
+
case 'native':
|
|
23
|
+
return {
|
|
24
|
+
...emptyMinikubeArtifacts(),
|
|
25
|
+
warnings: [
|
|
26
|
+
'Authorization engine "native" selected. No dedicated authz infrastructure artifacts were generated.',
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
default:
|
|
30
|
+
throw new Error(`Unsupported authorization engine for minikube adapter: ${String(engine)}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import type { InfraManifestInput } from '../../../types';
|
|
4
|
+
import { generateMinikubeBaseArtifacts } from './index';
|
|
5
|
+
|
|
6
|
+
describe('generateMinikubeBaseArtifacts Supabase local ports', () => {
|
|
7
|
+
test('keeps legacy my-app defaults while allocating a different range for another app', () => {
|
|
8
|
+
const myAppEnv = getGeneratedFileContent('my-app', 'infra/minikube/.env.example');
|
|
9
|
+
const chessEnv = getGeneratedFileContent('chess', 'infra/minikube/.env.example');
|
|
10
|
+
|
|
11
|
+
expect(myAppEnv).toContain('SUPABASE_LOCAL_PORT_BASE=55320');
|
|
12
|
+
expect(myAppEnv).toContain('SUPABASE_LOCAL_SHADOW_PORT=55320');
|
|
13
|
+
expect(myAppEnv).toContain('SUPABASE_LOCAL_API_PORT=55321');
|
|
14
|
+
expect(myAppEnv).toContain('SUPABASE_LOCAL_DB_PORT=55322');
|
|
15
|
+
expect(myAppEnv).toContain('SUPABASE_LOCAL_STUDIO_PORT=55323');
|
|
16
|
+
expect(myAppEnv).toContain('SUPABASE_LOCAL_INBUCKET_PORT=55324');
|
|
17
|
+
expect(myAppEnv).toContain('SUPABASE_LOCAL_ANALYTICS_PORT=55325');
|
|
18
|
+
|
|
19
|
+
expect(chessEnv).toContain('SUPABASE_LOCAL_PORT_BASE=62200');
|
|
20
|
+
expect(chessEnv).toContain('SUPABASE_LOCAL_SHADOW_PORT=62200');
|
|
21
|
+
expect(chessEnv).toContain('SUPABASE_LOCAL_API_PORT=62201');
|
|
22
|
+
expect(chessEnv).toContain('SUPABASE_LOCAL_DB_PORT=62202');
|
|
23
|
+
expect(chessEnv).toContain('SUPABASE_LOCAL_STUDIO_PORT=62203');
|
|
24
|
+
expect(chessEnv).toContain('SUPABASE_LOCAL_INBUCKET_PORT=62204');
|
|
25
|
+
expect(chessEnv).toContain('SUPABASE_LOCAL_ANALYTICS_PORT=62205');
|
|
26
|
+
expect(chessEnv).not.toContain('SUPABASE_LOCAL_DB_PORT=55322');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('generates bootstrap logic that derives ports from the app port base and preflights collisions', () => {
|
|
30
|
+
const script = getGeneratedFileContent('chess', 'infra/minikube/scripts/supabase-local-env.sh');
|
|
31
|
+
|
|
32
|
+
expect(script).toContain('SUPABASE_LOCAL_PORT_BASE="${SUPABASE_LOCAL_PORT_BASE:-62200}"');
|
|
33
|
+
expect(script).toContain(
|
|
34
|
+
'SUPABASE_LOCAL_DB_PORT="${SUPABASE_LOCAL_DB_PORT:-$((SUPABASE_LOCAL_PORT_BASE + 2))}"',
|
|
35
|
+
);
|
|
36
|
+
expect(script).toContain('assert_supabase_local_ports_available');
|
|
37
|
+
expect(script).toContain('Supabase local port preflight failed for project');
|
|
38
|
+
expect(script).toContain(
|
|
39
|
+
'Override SUPABASE_LOCAL_PORT_BASE or the specific SUPABASE_LOCAL_*_PORT values',
|
|
40
|
+
);
|
|
41
|
+
expect(script).toContain(
|
|
42
|
+
'upsert_env "${ENV_FILE}" "SUPABASE_LOCAL_PORT_BASE" "${SUPABASE_LOCAL_PORT_BASE}"',
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('generates bootstrap logic that retries unhealthy local Supabase startup once', () => {
|
|
47
|
+
const script = getGeneratedFileContent('chess', 'infra/minikube/scripts/supabase-local-env.sh');
|
|
48
|
+
|
|
49
|
+
expect(script).toContain('start_supabase_local_stack() {');
|
|
50
|
+
expect(script).toContain(
|
|
51
|
+
'Supabase local start failed. Stopping stale local stack and retrying once...',
|
|
52
|
+
);
|
|
53
|
+
expect(script).toContain('supabase stop --no-backup >/dev/null 2>&1 || true');
|
|
54
|
+
expect(script).toContain('Supabase local start failed after retry.');
|
|
55
|
+
expect(script).toContain('start_supabase_local_stack');
|
|
56
|
+
expect(countOccurrences(script, 'supabase start >/dev/null')).toBe(2);
|
|
57
|
+
expect(countOccurrences(script, 'supabase stop --no-backup')).toBe(1);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
function getGeneratedFileContent(namespace: string, filePath: string): string {
|
|
62
|
+
const artifact = generateMinikubeBaseArtifacts({
|
|
63
|
+
manifest: createInfraManifest(),
|
|
64
|
+
namespace,
|
|
65
|
+
extraResources: [],
|
|
66
|
+
extraEnvEntries: [],
|
|
67
|
+
}).find((file) => file.path === filePath);
|
|
68
|
+
|
|
69
|
+
if (!artifact) {
|
|
70
|
+
throw new Error(`Expected generated artifact at ${filePath}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return artifact.content;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function countOccurrences(source: string, needle: string): number {
|
|
77
|
+
return source.split(needle).length - 1;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function createInfraManifest(): InfraManifestInput {
|
|
81
|
+
return {
|
|
82
|
+
deployment: {
|
|
83
|
+
target: 'minikube',
|
|
84
|
+
monitoring: false,
|
|
85
|
+
},
|
|
86
|
+
auth: {
|
|
87
|
+
scope: 'global',
|
|
88
|
+
provider: 'supabase',
|
|
89
|
+
authorization: {
|
|
90
|
+
kind: 'RBAC',
|
|
91
|
+
engine: 'native',
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
database: {
|
|
95
|
+
provider: 'supabase',
|
|
96
|
+
tier: 'dev',
|
|
97
|
+
},
|
|
98
|
+
networking: {
|
|
99
|
+
cdn: false,
|
|
100
|
+
},
|
|
101
|
+
plugins: [],
|
|
102
|
+
};
|
|
103
|
+
}
|