unoverse 0.1.108 → 0.1.110
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/lib/urls.mjs +21 -11
- package/operator/infra/aws/main.tf +31 -6
- package/operator/infra/aws/pretoken/index.mjs +19 -1
- package/operator/lib/ground.sh +0 -1
- package/package.json +1 -1
package/lib/urls.mjs
CHANGED
|
@@ -60,20 +60,30 @@ function findUniverseRoot() {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
/**
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
/**
|
|
64
|
+
* EVERY ground with state, not the first one found.
|
|
65
|
+
*
|
|
66
|
+
* This looped ["infra/digitalocean", "infra/aws"] and RETURNED on the first match, so an
|
|
67
|
+
* operator running both clouds was shown DigitalOcean and told nothing about AWS — the same
|
|
68
|
+
* first-match bug that let `unoverse destroy` aim at the wrong cloud. "Where is my universe"
|
|
69
|
+
* has two answers when there are two universes.
|
|
70
|
+
*
|
|
71
|
+
* Returns [{ ground, outputs }], in the order the grounds are listed.
|
|
72
|
+
*/
|
|
73
|
+
function allGroundOutputs(root) {
|
|
74
|
+
const found = [];
|
|
75
|
+
for (const ground of ["digitalocean", "aws"]) {
|
|
76
|
+
const dir = join(root, "infra", ground);
|
|
67
77
|
if (!existsSync(join(dir, "terraform.tfstate")) && !existsSync(join(dir, ".terraform"))) continue;
|
|
68
78
|
const r = spawnSync("terraform", ["output", "-json"], { cwd: dir, encoding: "utf8" });
|
|
69
|
-
if (r.status
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
79
|
+
if (r.status !== 0) continue;
|
|
80
|
+
try {
|
|
81
|
+
const out = JSON.parse(r.stdout);
|
|
82
|
+
const values = Object.fromEntries(Object.entries(out).map(([k, v]) => [k, v.value]));
|
|
83
|
+
if (Object.keys(values).length) found.push({ ground, outputs: values });
|
|
84
|
+
} catch { /* a ground that cannot be read is simply not shown */ }
|
|
75
85
|
}
|
|
76
|
-
return
|
|
86
|
+
return found;
|
|
77
87
|
}
|
|
78
88
|
|
|
79
89
|
/**
|
|
@@ -388,6 +388,10 @@ resource "time_sleep" "iam_propagation" {
|
|
|
388
388
|
|
|
389
389
|
resource "aws_lambda_function" "pretoken" {
|
|
390
390
|
depends_on = [time_sleep.iam_propagation]
|
|
391
|
+
# The role → permission map, so the Lambda can emit both claims from one group list.
|
|
392
|
+
environment {
|
|
393
|
+
variables = { ROLE_PERMISSIONS = jsonencode(local.all_roles) }
|
|
394
|
+
}
|
|
391
395
|
function_name = "${var.name}-pretoken"
|
|
392
396
|
role = aws_iam_role.pretoken.arn
|
|
393
397
|
runtime = "nodejs20.x"
|
|
@@ -459,18 +463,39 @@ resource "aws_cognito_user_pool" "pool" {
|
|
|
459
463
|
# comes from var.roles — the deployment's own `noun:verb` role list, matched by
|
|
460
464
|
# node manifests' `requires.role` (DECLARATIVE_NODES.md §9.13).
|
|
461
465
|
locals {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
466
|
+
# TWO LEVELS, BECAUSE THE PLATFORM READS TWO CLAIMS.
|
|
467
|
+
#
|
|
468
|
+
# Auth0 (the DigitalOcean ground) models this properly and is the standing contract:
|
|
469
|
+
#
|
|
470
|
+
# ROLE admin → permission admin:access
|
|
471
|
+
# ROLE developer → permissions marketplace:publish, workflow:author, workflow:promote
|
|
472
|
+
#
|
|
473
|
+
# Canvas gates on `roles` containing "admin" (apps/canvas/src/App.jsx). The publish and
|
|
474
|
+
# builder gates read `permissions` for marketplace:publish and workflow:author
|
|
475
|
+
# (auth/publishGate.ts, auth/auth.ts). Roles are who somebody is; permissions are what
|
|
476
|
+
# they may do, and the platform never confuses them.
|
|
477
|
+
#
|
|
478
|
+
# Cognito has ONE level — groups — so the mapping lives here and the Lambda applies it.
|
|
479
|
+
# This ground previously made GROUPS the permissions and emitted them as both claims, so
|
|
480
|
+
# a pool had workflow:author and no admin: every gate passed except the one that decides
|
|
481
|
+
# whether you may open Canvas at all.
|
|
482
|
+
role_permissions = {
|
|
483
|
+
admin = ["admin:access", "workflow:author", "marketplace:publish", "workflow:promote"]
|
|
484
|
+
developer = ["workflow:author", "marketplace:publish", "workflow:promote"]
|
|
485
|
+
}
|
|
486
|
+
# Extra roles a deployment invents carry themselves as their own permission, which is what
|
|
487
|
+
# a node's `requires.role` matches.
|
|
488
|
+
all_roles = merge(
|
|
489
|
+
local.role_permissions,
|
|
490
|
+
{ for r in var.roles : r => [r] },
|
|
491
|
+
)
|
|
467
492
|
}
|
|
468
493
|
|
|
469
494
|
resource "aws_cognito_user_group" "roles" {
|
|
470
495
|
for_each = local.all_roles
|
|
471
496
|
name = each.key
|
|
472
497
|
user_pool_id = aws_cognito_user_pool.pool.id
|
|
473
|
-
description = each.value
|
|
498
|
+
description = "Grants: ${join(", ", each.value)}"
|
|
474
499
|
}
|
|
475
500
|
|
|
476
501
|
# The initial ADMIN — the first human in the universe. Without this, a fresh pool
|
|
@@ -13,13 +13,31 @@
|
|
|
13
13
|
* `permissions`: the platform's builder/publish gates read permissions, node
|
|
14
14
|
* requires.role matches either.
|
|
15
15
|
*/
|
|
16
|
+
// The role → permission map, from the ground (ROLE_PERMISSIONS, set by terraform). Its
|
|
17
|
+
// absence is not an error: without it a group grants itself, which is the old behaviour.
|
|
18
|
+
const ROLE_PERMISSIONS = (() => {
|
|
19
|
+
try { return JSON.parse(process.env.ROLE_PERMISSIONS ?? "{}"); } catch { return {}; }
|
|
20
|
+
})();
|
|
21
|
+
|
|
16
22
|
export const handler = async (event) => {
|
|
17
23
|
const groups = event.request.groupConfiguration?.groupsToOverride ?? [];
|
|
18
24
|
const email = event.request.userAttributes?.email;
|
|
19
25
|
|
|
26
|
+
// ROLES ARE WHO YOU ARE; PERMISSIONS ARE WHAT YOU MAY DO. Cognito has one level — groups
|
|
27
|
+
// — and the platform reads two claims: Canvas gates on `roles` containing "admin", while
|
|
28
|
+
// the publish and builder gates read `permissions` for marketplace:publish and
|
|
29
|
+
// workflow:author. Emitting groups as BOTH made every group a role and a permission at
|
|
30
|
+
// once, so a pool built from permission-shaped groups had no "admin" and Canvas refused
|
|
31
|
+
// an administrator who held every permission it grants.
|
|
32
|
+
//
|
|
33
|
+
// Groups are the roles. Permissions are what those roles map to, unioned.
|
|
34
|
+
const permissions = [...new Set(
|
|
35
|
+
groups.flatMap((g) => ROLE_PERMISSIONS[g] ?? [g]),
|
|
36
|
+
)];
|
|
37
|
+
|
|
20
38
|
const claims = {
|
|
21
39
|
roles: groups,
|
|
22
|
-
permissions
|
|
40
|
+
permissions,
|
|
23
41
|
...(email ? { email } : {}),
|
|
24
42
|
};
|
|
25
43
|
|
package/operator/lib/ground.sh
CHANGED
|
@@ -322,7 +322,6 @@ roles = [
|
|
|
322
322
|
"workflow:author", # build and test workflows (builder gate — ENFORCED)
|
|
323
323
|
"marketplace:publish", # publish assets to this universe (publish gate — ENFORCED)
|
|
324
324
|
"workflow:promote", # promote a draft to active / go-live (declared only)
|
|
325
|
-
"admin:access", # admin to Unoverse Canvas (declared only)
|
|
326
325
|
]
|
|
327
326
|
|
|
328
327
|
# The rendered output (terraform output -raw env_production > ../../.env.production)
|
package/package.json
CHANGED