@cosmicdrift/kumiko-bundled-features 0.55.0 → 0.55.1
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.55.
|
|
3
|
+
"version": "0.55.1",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -175,6 +175,20 @@ const integrationFeature = defineFeature("integration", (r) => {
|
|
|
175
175
|
default: "initial",
|
|
176
176
|
write: access.roles("Admin"),
|
|
177
177
|
}),
|
|
178
|
+
// Settings-Hub system-scope proxies for the derived Stripe screen: a
|
|
179
|
+
// privileged boolean (billing-live = machine OR human-SystemAdmin) and
|
|
180
|
+
// an encrypted system secret (api-key). Both are surfaced to a human
|
|
181
|
+
// SystemAdmin by build-config-feature-schema and must be SET-able by them.
|
|
182
|
+
billingLive: createSystemConfig("boolean", {
|
|
183
|
+
default: false,
|
|
184
|
+
write: access.privileged,
|
|
185
|
+
read: access.admin,
|
|
186
|
+
}),
|
|
187
|
+
systemSecret: createSystemConfig("text", {
|
|
188
|
+
write: access.systemAdmin,
|
|
189
|
+
read: access.systemAdmin,
|
|
190
|
+
encrypted: true,
|
|
191
|
+
}),
|
|
178
192
|
},
|
|
179
193
|
});
|
|
180
194
|
});
|
|
@@ -330,6 +344,42 @@ describe("scenario 1: system-scoped service URL", () => {
|
|
|
330
344
|
});
|
|
331
345
|
});
|
|
332
346
|
|
|
347
|
+
// Settings-Hub round-trip: a human SystemAdmin saves the system-scope keys
|
|
348
|
+
// the derived configEdit screen surfaces (privileged boolean + encrypted
|
|
349
|
+
// secret), then reads them back. Regression for the checkWriteAccess bug
|
|
350
|
+
// that rejected the operator on a `privileged` key with config.errors.systemOnly.
|
|
351
|
+
describe("Settings-Hub system-scope write by a human SystemAdmin", () => {
|
|
352
|
+
const sysAccessor = () =>
|
|
353
|
+
createConfigAccessor(stack.registry, resolver, systemAdmin.tenantId, systemAdmin.id, db);
|
|
354
|
+
|
|
355
|
+
test("saves a privileged boolean (billing-live) and reads it back", async () => {
|
|
356
|
+
await stack.http.writeOk(
|
|
357
|
+
ConfigHandlers.set,
|
|
358
|
+
{ key: "integration:config:billing-live", value: true, scope: "system" },
|
|
359
|
+
systemAdmin,
|
|
360
|
+
);
|
|
361
|
+
expect(await sysAccessor()("integration:config:billing-live")).toBe(true);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("saves an encrypted system secret (api-key) and reads it back decrypted", async () => {
|
|
365
|
+
await stack.http.writeOk(
|
|
366
|
+
ConfigHandlers.set,
|
|
367
|
+
{ key: "integration:config:system-secret", value: "sk_live_roundtrip", scope: "system" },
|
|
368
|
+
systemAdmin,
|
|
369
|
+
);
|
|
370
|
+
expect(await sysAccessor()("integration:config:system-secret")).toBe("sk_live_roundtrip");
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test("a plain tenant Admin is denied the privileged key (not via system-only)", async () => {
|
|
374
|
+
const error = await stack.http.writeErr(
|
|
375
|
+
ConfigHandlers.set,
|
|
376
|
+
{ key: "integration:config:billing-live", value: false, scope: "system" },
|
|
377
|
+
tenantAdmin,
|
|
378
|
+
);
|
|
379
|
+
expectErrorIncludes(error, "access_denied");
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
333
383
|
// --- Scenario 2: Mail Server — system default + tenant override ---
|
|
334
384
|
|
|
335
385
|
describe("scenario 2: tenant-scoped mail server with system fallback", () => {
|
|
@@ -105,6 +105,40 @@ describe("prepareConfigWrite", () => {
|
|
|
105
105
|
expect(result.failure.error.i18nKey).toBe("config.errors.systemOnly");
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
+
test("privileged key (system + SystemAdmin) is writable by a human SystemAdmin", () => {
|
|
109
|
+
// The derived configEdit screen surfaces a `access.privileged`
|
|
110
|
+
// (`["system","SystemAdmin"]`) key to a human SystemAdmin (e.g. Stripe
|
|
111
|
+
// billing-live). The write must succeed — "system in the write-set"
|
|
112
|
+
// means machine-OR-operator, not machine-only.
|
|
113
|
+
const privilegedKey = createSystemConfig("boolean", { write: access.privileged });
|
|
114
|
+
const result = prepareConfigWrite({
|
|
115
|
+
registry: registryStub({ "ns:config:billing-live": privilegedKey }),
|
|
116
|
+
user: userStub(["SystemAdmin"], SYSTEM_TENANT_ID, "sysadmin-1"),
|
|
117
|
+
key: "ns:config:billing-live",
|
|
118
|
+
scope: ConfigScopes.system,
|
|
119
|
+
});
|
|
120
|
+
expect(result.ok).toBe(true);
|
|
121
|
+
if (!result.ok) throw new Error("unreachable");
|
|
122
|
+
expect(result.keyDef).toBe(privilegedKey);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("privileged key stays blocked for a non-SystemAdmin human (generic denied, not system-only)", () => {
|
|
126
|
+
// Security: the human half of `privileged` is SystemAdmin only — a plain
|
|
127
|
+
// Admin must NOT inherit it. And the error is the generic access-denied,
|
|
128
|
+
// not systemOnly (the key has a human writer, it just isn't this user).
|
|
129
|
+
const privilegedKey = createSystemConfig("boolean", { write: access.privileged });
|
|
130
|
+
const result = prepareConfigWrite({
|
|
131
|
+
registry: registryStub({ "ns:config:billing-live": privilegedKey }),
|
|
132
|
+
user: userStub(["Admin"]),
|
|
133
|
+
key: "ns:config:billing-live",
|
|
134
|
+
scope: ConfigScopes.system,
|
|
135
|
+
});
|
|
136
|
+
expect(result.ok).toBe(false);
|
|
137
|
+
if (result.ok) throw new Error("unreachable");
|
|
138
|
+
expect(result.failure.error.code).toBe("access_denied");
|
|
139
|
+
expect(result.failure.error.i18nKey).not.toBe("config.errors.systemOnly");
|
|
140
|
+
});
|
|
141
|
+
|
|
108
142
|
test("ok-path falls back to the key's declared scope when no scope is passed", () => {
|
|
109
143
|
const result = prepareConfigWrite({
|
|
110
144
|
registry: registryStub({ "ns:config:foo": TENANT_KEY_DEF }),
|
package/src/config/web/i18n.ts
CHANGED
|
@@ -15,11 +15,17 @@ export const defaultTranslations: TranslationsByLocale = {
|
|
|
15
15
|
"config.settings.system": "Plattform",
|
|
16
16
|
"config.settings.tenant": "Organisation",
|
|
17
17
|
"config.settings.user": "Persönlich",
|
|
18
|
+
"config.errors.systemOnly": "Dieser Wert kann nur vom System gesetzt werden.",
|
|
19
|
+
"config.errors.invalidScope": "Diese Ebene ist für diesen Schlüssel nicht zulässig.",
|
|
20
|
+
"config.errors.unknownKey": "Unbekannter Konfigurationsschlüssel.",
|
|
18
21
|
},
|
|
19
22
|
en: {
|
|
20
23
|
"config.settings.title": "Settings",
|
|
21
24
|
"config.settings.system": "Platform",
|
|
22
25
|
"config.settings.tenant": "Organization",
|
|
23
26
|
"config.settings.user": "Personal",
|
|
27
|
+
"config.errors.systemOnly": "This value can only be set by the system.",
|
|
28
|
+
"config.errors.invalidScope": "This scope is not allowed for this key.",
|
|
29
|
+
"config.errors.unknownKey": "Unknown configuration key.",
|
|
24
30
|
},
|
|
25
31
|
};
|
|
@@ -117,26 +117,28 @@ export function checkWriteAccess(
|
|
|
117
117
|
keyDef: ConfigKeyDefinition,
|
|
118
118
|
userRoles: readonly string[],
|
|
119
119
|
): KumikoError | null {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
120
|
+
// Direct grant first: matches SYSTEM_ROLE for the machine actor (jobs /
|
|
121
|
+
// seeds / framework-internal work) AND a human role (e.g. SystemAdmin)
|
|
122
|
+
// that the write-set lists. A privileged key (`["system", "SystemAdmin"]`)
|
|
123
|
+
// is therefore writable by BOTH — the escape hatch and the operator —
|
|
124
|
+
// which is what the derived configEdit screen (build-config-feature-schema
|
|
125
|
+
// scopedKeysAt) already assumes when it surfaces the key to a human.
|
|
126
|
+
if (hasConfigAccess(keyDef.access.write, userRoles)) return null;
|
|
127
|
+
// No direct grant. A key whose only writer is SYSTEM_ROLE is machine-only
|
|
128
|
+
// (the `access.system` preset): no human may write it, distinct error so
|
|
129
|
+
// the UI can explain "operator/job-only".
|
|
130
|
+
const humanWriters = keyDef.access.write.filter((role) => role !== SYSTEM_ROLE);
|
|
131
|
+
if (humanWriters.length === 0) {
|
|
127
132
|
return new AccessDeniedError({
|
|
128
133
|
message: "config key is system-only",
|
|
129
134
|
i18nKey: "config.errors.systemOnly",
|
|
130
135
|
details: { reason: ConfigErrors.systemOnly },
|
|
131
136
|
});
|
|
132
137
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
return null;
|
|
138
|
+
return new AccessDeniedError({
|
|
139
|
+
message: "config write access denied",
|
|
140
|
+
details: { requiredRoles: keyDef.access.write },
|
|
141
|
+
});
|
|
140
142
|
}
|
|
141
143
|
|
|
142
144
|
export function validateScope(
|