@tatara/cli 0.5.0 → 0.6.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/dist/bin.mjs +200 -24
- package/package.json +1 -1
- package/templates/features/auth-keycloak/files+authz/src/core/auth/tests/keycloak-authz.test.ts +2 -2
- package/templates/features/authz/files/src/core/authz/commands/grant-role.ts +5 -5
- package/templates/features/authz/files/src/core/authz/commands/sync-permissions.ts +2 -2
- package/templates/features/authz/files/src/core/authz/db/seed.ts +2 -2
- package/templates/features/authz/files/src/core/authz/index.ts +9 -0
- package/templates/features/authz/files/src/core/authz/routes/authorization.routes.ts +9 -280
- package/templates/features/authz/files/src/core/authz/system-roles.ts +0 -2
- package/templates/features/authz/files/src/core/authz/tests/authz.test.ts +1 -2
- package/templates/features/authz/files/tests/helpers/authz.ts +6 -8
- package/templates/features/authz/files+drizzle/src/core/authz/db/store.ts +3 -3
- package/templates/features/authz/files+prisma/src/core/authz/db/store.ts +3 -3
- package/templates/generators/permission/files/src/modules/__moduleKebab__/permissions/__nameKebab__.permissions.ts +1 -1
- package/templates/generators/resource/files+authz/src/modules/__moduleKebab__/permissions/__nameKebab__.permissions.ts +1 -1
- package/templates/generators/route/files+authz/src/modules/__moduleKebab__/permissions/__nameKebab__.permissions.ts +1 -1
- package/templates/versions.json +2 -1
- package/templates/features/authz/files/src/core/authz/cache.ts +0 -31
- package/templates/features/authz/files/src/core/authz/catalog.ts +0 -85
- package/templates/features/authz/files/src/core/authz/constants.ts +0 -32
- package/templates/features/authz/files/src/core/authz/middleware.ts +0 -21
- package/templates/features/authz/files/src/core/authz/permissions.ts +0 -31
- package/templates/features/authz/files/src/core/authz/resolver.ts +0 -98
- package/templates/features/authz/files/src/core/authz/services/audit.ts +0 -24
- package/templates/features/authz/files/src/core/authz/services/grants.ts +0 -29
- package/templates/features/authz/files/src/core/authz/services/members.ts +0 -84
- package/templates/features/authz/files/src/core/authz/services/roles.ts +0 -73
- package/templates/features/authz/files/src/core/authz/services/sync.ts +0 -34
- package/templates/features/authz/files/src/core/authz/types.ts +0 -87
- package/templates/features/authz/files/src/core/authz/validators.ts +0 -99
package/dist/bin.mjs
CHANGED
|
@@ -273,13 +273,12 @@ async function sync(project = loadProject()) {
|
|
|
273
273
|
const permissions = await find(root, ["src/modules/*/permissions/*.permissions.ts"]);
|
|
274
274
|
const imports = permissions.map((f) => `import ${identifier(f, "permissions")} from "${specifier(f)}";`);
|
|
275
275
|
const names = permissions.map((f) => identifier(f, "permissions"));
|
|
276
|
-
await writeIfChanged(join(out, "permissions.ts"), `${HEADER}import { mergeCatalogs, type PermissionKeysOf } from "
|
|
277
|
-
import corePermissions from "#core/authz/permissions";
|
|
276
|
+
await writeIfChanged(join(out, "permissions.ts"), `${HEADER}import { authzPermissions, mergeCatalogs, type PermissionKeysOf } from "@tatara/authz";
|
|
278
277
|
${imports.join("\n")}${imports.length ? "\n" : ""}
|
|
279
|
-
export const catalog = mergeCatalogs(
|
|
278
|
+
export const catalog = mergeCatalogs(authzPermissions${names.map((n) => `, ${n}`).join("")});
|
|
280
279
|
|
|
281
280
|
export type PermissionKey =
|
|
282
|
-
| PermissionKeysOf<typeof
|
|
281
|
+
| PermissionKeysOf<typeof authzPermissions>${names.map((n) => `\n\t| PermissionKeysOf<typeof ${n}>`).join("")};
|
|
283
282
|
`, written);
|
|
284
283
|
}
|
|
285
284
|
if (project.config.database !== "none") {
|
|
@@ -682,6 +681,7 @@ const authz = {
|
|
|
682
681
|
id: "authz",
|
|
683
682
|
kind: "addon",
|
|
684
683
|
label: "Permission system",
|
|
684
|
+
dependencies: ["@tatara/authz"],
|
|
685
685
|
slots: [
|
|
686
686
|
{
|
|
687
687
|
file: "tests/helpers/auth.ts",
|
|
@@ -711,9 +711,9 @@ const authz = {
|
|
|
711
711
|
|
|
712
712
|
- Permissions are \`resource:action\` (optionally scoped \`own | team | org\`, where a wider scope
|
|
713
713
|
satisfies a narrower one). Each module declares its resources and the grants built-in roles get
|
|
714
|
-
in \`permissions/<name>.permissions.ts\` (\`definePermissions\`); \`tatara sync\` merges them into a
|
|
714
|
+
in \`permissions/<name>.permissions.ts\` (\`definePermissions\` from \`@tatara/authz\`); \`tatara sync\` merges them into a
|
|
715
715
|
typed catalog.
|
|
716
|
-
- Guard routes with \`middleware: [requirePermission("project:read")]\` from \`#core/authz/
|
|
716
|
+
- Guard routes with \`middleware: [requirePermission("project:read")]\` from \`#core/authz/index\`
|
|
717
717
|
and declare \`security: [{ bearerAuth: ["project:read"] }]\`. For scoped actions, call
|
|
718
718
|
\`resolveScope(principal, "doc:read")\` in the service and filter rows by the returned scope.
|
|
719
719
|
- Effective permissions = role grants (assigned roles + built-in roles whose key matches an
|
|
@@ -1454,7 +1454,7 @@ const authOrgs = {
|
|
|
1454
1454
|
file: "src/core/auth/auth.ts",
|
|
1455
1455
|
slot: "imports",
|
|
1456
1456
|
when: ["authz"],
|
|
1457
|
-
content: "import { removeMemberAccess } from \"#core/authz/
|
|
1457
|
+
content: "import { removeMemberAccess } from \"#core/authz/index\";"
|
|
1458
1458
|
},
|
|
1459
1459
|
{
|
|
1460
1460
|
file: "tests/helpers/auth.ts",
|
|
@@ -2452,7 +2452,7 @@ function guardSlots(project, n) {
|
|
|
2452
2452
|
order: fills.length
|
|
2453
2453
|
});
|
|
2454
2454
|
if (project.has("authz")) {
|
|
2455
|
-
add("imports", "import { requirePermission } from \"#core/authz/
|
|
2455
|
+
add("imports", "import { requirePermission } from \"#core/authz/index\";");
|
|
2456
2456
|
for (const [route, action] of Object.entries(ACTIONS)) add(`guard-${route}`, `middleware: [requirePermission("${n.kebab}:${action}")] as const,\nsecurity: [{ bearerAuth: ["${n.kebab}:${action}"] }],`);
|
|
2457
2457
|
add("auth-errors", "...errorResponses(401, 403),");
|
|
2458
2458
|
} else if (project.has("auth")) {
|
|
@@ -3186,6 +3186,36 @@ function createBaseline(project, deps) {
|
|
|
3186
3186
|
} };
|
|
3187
3187
|
}
|
|
3188
3188
|
//#endregion
|
|
3189
|
+
//#region src/upgrade/codemods/imports.ts
|
|
3190
|
+
const escapeRegExp$1 = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3191
|
+
/**
|
|
3192
|
+
* The names `text` imports (or re-exports) from `specifier`, or null when it doesn't use it.
|
|
3193
|
+
* A namespace, default, side-effect or dynamic import gives ["*"]: it could reach any name.
|
|
3194
|
+
*/
|
|
3195
|
+
function importsOf(text, specifier) {
|
|
3196
|
+
const spec = `["']${escapeRegExp$1(specifier)}["']`;
|
|
3197
|
+
const names = [];
|
|
3198
|
+
let found = false;
|
|
3199
|
+
const fromClause = new RegExp(`(?:import|export)\\s+(?:type\\s+)?([^;]*?)\\s+from\\s+${spec}`, "g");
|
|
3200
|
+
for (const match of text.matchAll(fromClause)) {
|
|
3201
|
+
found = true;
|
|
3202
|
+
const braces = (match[1] ?? "").match(/^\{([^}]*)\}$/);
|
|
3203
|
+
if (!braces) {
|
|
3204
|
+
names.push("*");
|
|
3205
|
+
continue;
|
|
3206
|
+
}
|
|
3207
|
+
for (const part of (braces[1] ?? "").split(",")) {
|
|
3208
|
+
const name = part.trim().replace(/^type\s+/, "").split(/\s+as\s+/)[0]?.trim();
|
|
3209
|
+
if (name) names.push(name);
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
if (new RegExp(`import\\s*\\(\\s*${spec}\\s*\\)|import\\s+${spec}`).test(text)) {
|
|
3213
|
+
found = true;
|
|
3214
|
+
names.push("*");
|
|
3215
|
+
}
|
|
3216
|
+
return found ? names : null;
|
|
3217
|
+
}
|
|
3218
|
+
//#endregion
|
|
3189
3219
|
//#region src/upgrade/codemods/auth-package.ts
|
|
3190
3220
|
const AUTH = "@tatara/auth";
|
|
3191
3221
|
const KEYCLOAK = "@tatara/keycloak";
|
|
@@ -3262,27 +3292,21 @@ function groups(keycloak) {
|
|
|
3262
3292
|
});
|
|
3263
3293
|
return all;
|
|
3264
3294
|
}
|
|
3265
|
-
async function read$
|
|
3295
|
+
async function read$3(ctx, path) {
|
|
3266
3296
|
return ctx.read(path).catch(() => void 0);
|
|
3267
3297
|
}
|
|
3268
|
-
const escapeRegExp$1 = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3269
3298
|
/** True when `text` imports `entry` (or one of its gone names). */
|
|
3270
3299
|
function usesExport(text, entry) {
|
|
3271
|
-
const
|
|
3300
|
+
const names = importsOf(text, entry.specifier);
|
|
3301
|
+
if (!names) return false;
|
|
3272
3302
|
const gone = entry.names;
|
|
3273
|
-
|
|
3274
|
-
if (!gone) return true;
|
|
3275
|
-
const braces = (match[1] ?? "").match(/\{([^}]*)\}/);
|
|
3276
|
-
if (!braces) return true;
|
|
3277
|
-
if ((braces[1] ?? "").split(",").map((part) => part.trim().replace(/^type\s+/, "").split(/\s+as\s+/)[0]?.trim()).some((name) => name && gone.includes(name))) return true;
|
|
3278
|
-
}
|
|
3279
|
-
return false;
|
|
3303
|
+
return !gone || names.some((name) => name === "*" || gone.includes(name));
|
|
3280
3304
|
}
|
|
3281
3305
|
/** Why the group can't move, or [] when it can. */
|
|
3282
3306
|
async function blockersOf(ctx, group, sources) {
|
|
3283
3307
|
const blockers = [];
|
|
3284
3308
|
for (const path of [...group.regenerate, ...group.remove]) {
|
|
3285
|
-
const text = await read$
|
|
3309
|
+
const text = await read$3(ctx, path);
|
|
3286
3310
|
if (text === void 0) continue;
|
|
3287
3311
|
if (!await ctx.baseline.isUnchanged(path, text)) blockers.push(`${path} is customised`);
|
|
3288
3312
|
}
|
|
@@ -3316,7 +3340,7 @@ const authPackage = {
|
|
|
3316
3340
|
}
|
|
3317
3341
|
for (const group of ready) {
|
|
3318
3342
|
for (const path of group.regenerate) {
|
|
3319
|
-
if (await read$
|
|
3343
|
+
if (await read$3(ctx, path) === void 0) continue;
|
|
3320
3344
|
const next = await ctx.renderCurrent(path);
|
|
3321
3345
|
if (next !== void 0) await ctx.write(path, next);
|
|
3322
3346
|
}
|
|
@@ -3327,6 +3351,151 @@ const authPackage = {
|
|
|
3327
3351
|
}
|
|
3328
3352
|
};
|
|
3329
3353
|
//#endregion
|
|
3354
|
+
//#region src/upgrade/codemods/authz-package.ts
|
|
3355
|
+
const AUTHZ = "@tatara/authz";
|
|
3356
|
+
const INDEX = "#core/authz/index";
|
|
3357
|
+
const DIR = "src/core/authz";
|
|
3358
|
+
/** Copies the package replaces. */
|
|
3359
|
+
const REMOVED = [
|
|
3360
|
+
"cache.ts",
|
|
3361
|
+
"catalog.ts",
|
|
3362
|
+
"constants.ts",
|
|
3363
|
+
"middleware.ts",
|
|
3364
|
+
"permissions.ts",
|
|
3365
|
+
"resolver.ts",
|
|
3366
|
+
"types.ts",
|
|
3367
|
+
"validators.ts",
|
|
3368
|
+
"services/audit.ts",
|
|
3369
|
+
"services/grants.ts",
|
|
3370
|
+
"services/members.ts",
|
|
3371
|
+
"services/roles.ts",
|
|
3372
|
+
"services/sync.ts"
|
|
3373
|
+
].map((f) => `${DIR}/${f}`);
|
|
3374
|
+
/** App files that stay (or are new), and become what this CLI generates. system-roles.ts is the app's config and stays as it is. */
|
|
3375
|
+
const REGENERATED$1 = [
|
|
3376
|
+
`${DIR}/index.ts`,
|
|
3377
|
+
`${DIR}/db/seed.ts`,
|
|
3378
|
+
`${DIR}/commands/grant-role.ts`,
|
|
3379
|
+
`${DIR}/commands/sync-permissions.ts`,
|
|
3380
|
+
`${DIR}/routes/authorization.routes.ts`,
|
|
3381
|
+
`${DIR}/tests/authz.test.ts`,
|
|
3382
|
+
"src/core/auth/tests/keycloak-authz.test.ts",
|
|
3383
|
+
"tests/helpers/authz.ts"
|
|
3384
|
+
];
|
|
3385
|
+
/** Where other files' imports go, and the names that exist there. undefined: every name. */
|
|
3386
|
+
const MOVES = [
|
|
3387
|
+
{
|
|
3388
|
+
from: "#core/authz/constants",
|
|
3389
|
+
to: AUTHZ
|
|
3390
|
+
},
|
|
3391
|
+
{
|
|
3392
|
+
from: "#core/authz/types",
|
|
3393
|
+
to: AUTHZ
|
|
3394
|
+
},
|
|
3395
|
+
{
|
|
3396
|
+
from: "#core/authz/catalog",
|
|
3397
|
+
to: AUTHZ
|
|
3398
|
+
},
|
|
3399
|
+
{
|
|
3400
|
+
from: "#core/authz/validators",
|
|
3401
|
+
to: AUTHZ
|
|
3402
|
+
},
|
|
3403
|
+
{
|
|
3404
|
+
from: "#core/authz/middleware",
|
|
3405
|
+
to: INDEX,
|
|
3406
|
+
names: ["requirePermission"]
|
|
3407
|
+
},
|
|
3408
|
+
{
|
|
3409
|
+
from: "#core/authz/resolver",
|
|
3410
|
+
to: INDEX,
|
|
3411
|
+
names: ["checkPermission", "resolveScope"]
|
|
3412
|
+
},
|
|
3413
|
+
{
|
|
3414
|
+
from: "#core/authz/services/members",
|
|
3415
|
+
to: INDEX,
|
|
3416
|
+
names: ["removeMemberAccess"]
|
|
3417
|
+
},
|
|
3418
|
+
{
|
|
3419
|
+
from: "#core/authz/cache",
|
|
3420
|
+
to: INDEX,
|
|
3421
|
+
names: []
|
|
3422
|
+
},
|
|
3423
|
+
{
|
|
3424
|
+
from: "#core/authz/permissions",
|
|
3425
|
+
to: INDEX,
|
|
3426
|
+
names: []
|
|
3427
|
+
},
|
|
3428
|
+
...[
|
|
3429
|
+
"audit",
|
|
3430
|
+
"grants",
|
|
3431
|
+
"roles",
|
|
3432
|
+
"sync"
|
|
3433
|
+
].map((s) => ({
|
|
3434
|
+
from: `#core/authz/services/${s}`,
|
|
3435
|
+
to: INDEX,
|
|
3436
|
+
names: []
|
|
3437
|
+
}))
|
|
3438
|
+
];
|
|
3439
|
+
/** Names the 0.5 constants file had that the package dropped. */
|
|
3440
|
+
const DROPPED_CONSTANTS = ["CACHE_TTL_MS"];
|
|
3441
|
+
async function read$2(ctx, path) {
|
|
3442
|
+
return ctx.read(path).catch(() => void 0);
|
|
3443
|
+
}
|
|
3444
|
+
const authzPackage = {
|
|
3445
|
+
version: "0.6.0",
|
|
3446
|
+
id: "authz-package",
|
|
3447
|
+
description: "Use @tatara/authz (catalog, resolver, guards, services, routes)",
|
|
3448
|
+
async run(ctx) {
|
|
3449
|
+
if (!ctx.project.config.addons.includes("authz")) return;
|
|
3450
|
+
await ctx.addDependency(AUTHZ, versionOf(AUTHZ));
|
|
3451
|
+
const blockers = [];
|
|
3452
|
+
const regenerate = [];
|
|
3453
|
+
for (const path of REGENERATED$1) {
|
|
3454
|
+
const text = await read$2(ctx, path);
|
|
3455
|
+
if (text === void 0) {
|
|
3456
|
+
if (path === `${DIR}/index.ts`) regenerate.push(path);
|
|
3457
|
+
continue;
|
|
3458
|
+
}
|
|
3459
|
+
if (await ctx.baseline.isUnchanged(path, text)) regenerate.push(path);
|
|
3460
|
+
else blockers.push(`${path} is customised`);
|
|
3461
|
+
}
|
|
3462
|
+
const removed = [];
|
|
3463
|
+
for (const path of REMOVED) {
|
|
3464
|
+
const text = await read$2(ctx, path);
|
|
3465
|
+
if (text === void 0) continue;
|
|
3466
|
+
removed.push(path);
|
|
3467
|
+
if (!await ctx.baseline.isUnchanged(path, text)) blockers.push(`${path} is customised`);
|
|
3468
|
+
}
|
|
3469
|
+
const own = /* @__PURE__ */ new Set([...REGENERATED$1, ...REMOVED]);
|
|
3470
|
+
const sources = await glob(["src/**/*.ts", "tests/**/*.ts"], {
|
|
3471
|
+
cwd: ctx.project.root,
|
|
3472
|
+
ignore: ["src/generated/**"]
|
|
3473
|
+
});
|
|
3474
|
+
for (const path of sources) {
|
|
3475
|
+
if (own.has(path)) continue;
|
|
3476
|
+
const text = await ctx.read(path);
|
|
3477
|
+
for (const move of MOVES) {
|
|
3478
|
+
const names = importsOf(text, move.from);
|
|
3479
|
+
if (!names) continue;
|
|
3480
|
+
const allowed = move.names;
|
|
3481
|
+
const missing = names.filter((n) => n === "*" || (allowed ? !allowed.includes(n) : move.from.endsWith("/constants") && DROPPED_CONSTANTS.includes(n)));
|
|
3482
|
+
if (missing.length > 0) blockers.push(`${path} imports ${missing.join(", ")} from ${move.from}`);
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3485
|
+
if (blockers.length > 0) {
|
|
3486
|
+
ctx.report.notes.push(`Nothing in ${DIR} was changed: ${blockers.join("; ")}. Compare with a newly generated app and move onto @tatara/authz (createAuthz in ${DIR}/index.ts) by hand.`);
|
|
3487
|
+
ctx.report.blocked.push("authz-package");
|
|
3488
|
+
return;
|
|
3489
|
+
}
|
|
3490
|
+
for (const path of regenerate) {
|
|
3491
|
+
const next = await ctx.renderCurrent(path);
|
|
3492
|
+
if (next !== void 0) await ctx.write(path, next);
|
|
3493
|
+
}
|
|
3494
|
+
for (const path of removed) await ctx.removeIfUnchanged(path);
|
|
3495
|
+
for (const move of MOVES) await ctx.rewriteImports(move.from, move.to);
|
|
3496
|
+
}
|
|
3497
|
+
};
|
|
3498
|
+
//#endregion
|
|
3330
3499
|
//#region src/upgrade/codemods/core-package.ts
|
|
3331
3500
|
const CORE = "@tatara/core";
|
|
3332
3501
|
/** Files @tatara/core replaces, with the import specifier apps used for each. */
|
|
@@ -3586,6 +3755,7 @@ const CODEMODS = [
|
|
|
3586
3755
|
for (const path of await outsideImporters(ctx)) blockers.push(`${path} imports one of the moved files`);
|
|
3587
3756
|
if (blockers.length > 0) {
|
|
3588
3757
|
ctx.report.notes.push(`Nothing in src/core/http was changed: ${blockers.join("; ")}. Move these onto @tatara/http (createApp, mountRoutes, serveOpenApi, handleErrors, startServer and its error classes) by hand, then delete the old files.`);
|
|
3758
|
+
ctx.report.blocked.push("http-package");
|
|
3589
3759
|
return;
|
|
3590
3760
|
}
|
|
3591
3761
|
for (const path of replace) {
|
|
@@ -3601,7 +3771,8 @@ const CODEMODS = [
|
|
|
3601
3771
|
}
|
|
3602
3772
|
},
|
|
3603
3773
|
dbTesting,
|
|
3604
|
-
authPackage
|
|
3774
|
+
authPackage,
|
|
3775
|
+
authzPackage
|
|
3605
3776
|
];
|
|
3606
3777
|
//#endregion
|
|
3607
3778
|
//#region src/upgrade/context.ts
|
|
@@ -3613,7 +3784,8 @@ function createContext(project, baseline, opts = {}) {
|
|
|
3613
3784
|
regenerated: [],
|
|
3614
3785
|
removed: [],
|
|
3615
3786
|
kept: [],
|
|
3616
|
-
notes: []
|
|
3787
|
+
notes: [],
|
|
3788
|
+
blocked: []
|
|
3617
3789
|
};
|
|
3618
3790
|
const abs = (path) => join(project.root, path);
|
|
3619
3791
|
let rendered;
|
|
@@ -3725,13 +3897,16 @@ async function upgrade(project, opts = {}) {
|
|
|
3725
3897
|
const from = project.config.version;
|
|
3726
3898
|
const ctx = createContext(project, opts.baseline ?? createBaseline(project), { dryRun: opts.dryRun });
|
|
3727
3899
|
const due = [...opts.codemods ?? CODEMODS].filter((m) => compareVersions(m.version, from) > 0 && compareVersions(m.version, target) <= 0).sort((a, b) => compareVersions(a.version, b.version));
|
|
3900
|
+
let reached = target;
|
|
3728
3901
|
for (const mod of due) {
|
|
3902
|
+
const blockedBefore = ctx.report.blocked.length;
|
|
3729
3903
|
try {
|
|
3730
3904
|
await mod.run(ctx);
|
|
3731
3905
|
} catch (error) {
|
|
3732
3906
|
throw new Error(`codemod ${mod.id} (${mod.version}) failed: ${error.message}`, { cause: error });
|
|
3733
3907
|
}
|
|
3734
3908
|
ctx.report.ran.push(mod.id);
|
|
3909
|
+
if (ctx.report.blocked.length > blockedBefore && reached === target) reached = due.slice(0, due.indexOf(mod)).at(-1)?.version ?? from;
|
|
3735
3910
|
}
|
|
3736
3911
|
if (opts.dryRun) return ctx.report;
|
|
3737
3912
|
const format = opts.formatFile ?? biomeFormat;
|
|
@@ -3741,9 +3916,9 @@ async function upgrade(project, opts = {}) {
|
|
|
3741
3916
|
const config = JSON.parse(await readFile(path, "utf8"));
|
|
3742
3917
|
const next = {
|
|
3743
3918
|
...config,
|
|
3744
|
-
version:
|
|
3919
|
+
version: reached,
|
|
3745
3920
|
generated: {
|
|
3746
|
-
cli:
|
|
3921
|
+
cli: reached,
|
|
3747
3922
|
files: await nextRecord(project, ctx.report)
|
|
3748
3923
|
}
|
|
3749
3924
|
};
|
|
@@ -3763,6 +3938,7 @@ function printUpgrade(report, from, to) {
|
|
|
3763
3938
|
for (const path of report.removed) console.log(pc.dim(` removed ${path} (now provided by a package)`));
|
|
3764
3939
|
for (const { path } of report.kept) console.log(pc.yellow(` kept ${path}: changed in this app; move the change into options or eject`));
|
|
3765
3940
|
for (const note of report.notes) console.log(` ${note}`);
|
|
3941
|
+
if (report.blocked.length > 0) console.log(pc.yellow(` ${report.blocked.join(", ")} did not run: make the changes above, then run tatara upgrade again`));
|
|
3766
3942
|
}
|
|
3767
3943
|
//#endregion
|
|
3768
3944
|
//#region src/bin.ts
|
package/package.json
CHANGED
package/templates/features/auth-keycloak/files+authz/src/core/auth/tests/keycloak-authz.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import { authz } from "#core/authz/index";
|
|
3
3
|
import { api } from "#tests/helpers/api";
|
|
4
4
|
import { keycloakToken } from "#tests/helpers/auth";
|
|
5
5
|
import { resetDatabase } from "#tests/helpers/db";
|
|
@@ -12,7 +12,7 @@ beforeEach(async () => {
|
|
|
12
12
|
|
|
13
13
|
describe("Keycloak access tokens with authz", () => {
|
|
14
14
|
it("lets token roles match built-in authz roles", async () => {
|
|
15
|
-
await syncSystemRoles();
|
|
15
|
+
await authz.syncSystemRoles();
|
|
16
16
|
const admin = await keycloakToken({ roles: ["admin"] });
|
|
17
17
|
const user = await keycloakToken({ roles: ["user"] });
|
|
18
18
|
expect((await api.get("/api/authorization/roles", { headers: bearer(admin) })).status).toBe(
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { DEFAULT_ORG_ID, SYSTEM_ORG_ID } from "
|
|
3
|
-
import {
|
|
4
|
-
import { assignRole } from "#core/authz/services/members";
|
|
2
|
+
import { DEFAULT_ORG_ID, SYSTEM_ORG_ID } from "@tatara/authz";
|
|
3
|
+
import { authz } from "#core/authz/index";
|
|
5
4
|
import { defineCommand } from "@tatara/core";
|
|
6
5
|
|
|
7
6
|
export default defineCommand({
|
|
@@ -13,9 +12,10 @@ export default defineCommand({
|
|
|
13
12
|
org: z.string().default(DEFAULT_ORG_ID),
|
|
14
13
|
}),
|
|
15
14
|
async run({ user, role, org }, { logger }) {
|
|
16
|
-
const found = (await store.findRoleByKey(org, role)) ??
|
|
15
|
+
const found = (await authz.store.findRoleByKey(org, role)) ??
|
|
16
|
+
(await authz.store.findRoleByKey(SYSTEM_ORG_ID, role));
|
|
17
17
|
if (!found) throw new Error(`No role "${role}" (run authz:sync-permissions first for built-in roles)`);
|
|
18
|
-
const added = await assignRole(null, org, user, found.id);
|
|
18
|
+
const added = await authz.assignRole(null, org, user, found.id);
|
|
19
19
|
logger.info({ user, role, org, added }, added ? "role assigned" : "role already assigned");
|
|
20
20
|
},
|
|
21
21
|
});
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { defineCommand } from "@tatara/core";
|
|
2
|
-
import {
|
|
2
|
+
import { authz } from "#core/authz/index";
|
|
3
3
|
|
|
4
4
|
export default defineCommand({
|
|
5
5
|
name: "authz:sync-permissions",
|
|
6
6
|
description: "Create/update built-in roles so their grants match the code (run on every deploy)",
|
|
7
7
|
async run(_args, { logger }) {
|
|
8
|
-
const result = await syncSystemRoles();
|
|
8
|
+
const result = await authz.syncSystemRoles();
|
|
9
9
|
logger.info(result, "built-in roles in sync");
|
|
10
10
|
},
|
|
11
11
|
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { authz } from "#core/authz/index";
|
|
2
2
|
import { defineSeed } from "@tatara/db";
|
|
3
3
|
|
|
4
4
|
export default defineSeed({
|
|
5
5
|
name: "authz:system-roles",
|
|
6
6
|
order: -10,
|
|
7
7
|
async run({ logger }) {
|
|
8
|
-
logger.info(await syncSystemRoles(), "built-in roles in sync");
|
|
8
|
+
logger.info(await authz.syncSystemRoles(), "built-in roles in sync");
|
|
9
9
|
},
|
|
10
10
|
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createAuthz } from "@tatara/authz";
|
|
2
|
+
import { store } from "#core/authz/db/store";
|
|
3
|
+
import { SYSTEM_ROLES } from "#core/authz/system-roles";
|
|
4
|
+
import { catalog, type PermissionKey } from "#generated/permissions";
|
|
5
|
+
|
|
6
|
+
/** The app's authorization: its store, the permission catalog from `tatara sync`, built-in roles. */
|
|
7
|
+
export const authz = createAuthz<PermissionKey>({ store, catalog, systemRoles: SYSTEM_ROLES });
|
|
8
|
+
|
|
9
|
+
export const { checkPermission, removeMemberAccess, requirePermission, resolveScope } = authz;
|