qatools-cli 1.1.3 → 1.1.4
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 +1 -1
- package/qatools.js +118 -0
package/package.json
CHANGED
package/qatools.js
CHANGED
|
@@ -903,6 +903,14 @@ program
|
|
|
903
903
|
next: `qatools requirement-update -i <id> --status approved (or refine AC first)`,
|
|
904
904
|
});
|
|
905
905
|
}
|
|
906
|
+
if (data.agenda.accessModelMissing) {
|
|
907
|
+
sections.push({
|
|
908
|
+
icon: "⚪",
|
|
909
|
+
title: "No access model defined for this project",
|
|
910
|
+
items: [" • Who can sign in? Which roles exist? What's protected? How is data isolated?"],
|
|
911
|
+
next: `qatools access show (shows a minimal example) → qatools access set -f model.json`,
|
|
912
|
+
});
|
|
913
|
+
}
|
|
906
914
|
|
|
907
915
|
if (sections.length === 0) {
|
|
908
916
|
console.log(" ✓ Nothing on the agenda. The project is in a clean state.\n");
|
|
@@ -1370,6 +1378,116 @@ program
|
|
|
1370
1378
|
process.exit(0);
|
|
1371
1379
|
});
|
|
1372
1380
|
|
|
1381
|
+
// access subcommands grouped under 'access'
|
|
1382
|
+
const accessCmd = program
|
|
1383
|
+
.command("access")
|
|
1384
|
+
.description("Manage the project's Access Model (who-can-do-what, structured)");
|
|
1385
|
+
|
|
1386
|
+
accessCmd
|
|
1387
|
+
.command("show")
|
|
1388
|
+
.description("Show the access model for the project")
|
|
1389
|
+
.option("--project <id>", "Target project ID (overrides default)")
|
|
1390
|
+
.option("-o, --output <path>", "Save to JSON file")
|
|
1391
|
+
.option("--json", "Output JSON to stdout")
|
|
1392
|
+
.option("-e, --email <email>", "Login email")
|
|
1393
|
+
.option("-p, --password <password>", "Login password")
|
|
1394
|
+
.action(async (options) => {
|
|
1395
|
+
await authenticate(options);
|
|
1396
|
+
const creds = getCredentials();
|
|
1397
|
+
const projectId = options.project || creds?.projectId;
|
|
1398
|
+
if (!projectId) { console.error("No project configured. Run: qatools setup, or pass --project <id>"); process.exit(1); }
|
|
1399
|
+
const getModel = httpsCallable(functions, "getAccessModel");
|
|
1400
|
+
let result;
|
|
1401
|
+
try { result = await getModel({ projectId }); }
|
|
1402
|
+
catch (e) { console.error("Failed:", e.message); process.exit(1); }
|
|
1403
|
+
const model = result.data.accessModel;
|
|
1404
|
+
if (!model) {
|
|
1405
|
+
console.log(`No access model defined for this project.`);
|
|
1406
|
+
console.log(`Define one by writing a JSON file and running: qatools access set -f model.json`);
|
|
1407
|
+
console.log(``);
|
|
1408
|
+
console.log(`Minimal shape:`);
|
|
1409
|
+
console.log(JSON.stringify({
|
|
1410
|
+
authMethods: [{ key: "google", description: "Google OAuth", primary: true }],
|
|
1411
|
+
roles: [{ key: "owner", description: "Project owner with full control" }],
|
|
1412
|
+
resources: [{ key: "project", description: "Project metadata", ownedBy: "project", permissions: [{ role: "owner", actions: ["read","create","update","delete"] }] }],
|
|
1413
|
+
isolationRules: [{ scope: "project", description: "Data scoped by projectId field", enforcedBy: ["firestore.rules", "checkProjectAccess()"] }],
|
|
1414
|
+
}, null, 2));
|
|
1415
|
+
process.exit(0);
|
|
1416
|
+
}
|
|
1417
|
+
if (options.output) {
|
|
1418
|
+
fs.writeFileSync(path.resolve(options.output), JSON.stringify(model, null, 2));
|
|
1419
|
+
console.log(`Saved to ${options.output}`);
|
|
1420
|
+
process.exit(0);
|
|
1421
|
+
}
|
|
1422
|
+
if (options.json) {
|
|
1423
|
+
console.log(JSON.stringify(model, null, 2));
|
|
1424
|
+
process.exit(0);
|
|
1425
|
+
}
|
|
1426
|
+
// Pretty text output
|
|
1427
|
+
console.log(``);
|
|
1428
|
+
console.log(`Access Model — project ${projectId}`);
|
|
1429
|
+
console.log(``);
|
|
1430
|
+
if (model.authMethods?.length) {
|
|
1431
|
+
console.log(`AUTH METHODS`);
|
|
1432
|
+
for (const a of model.authMethods) console.log(` ${a.primary ? "*" : " "} ${a.key.padEnd(15)} ${a.description}`);
|
|
1433
|
+
console.log(``);
|
|
1434
|
+
}
|
|
1435
|
+
if (model.roles?.length) {
|
|
1436
|
+
console.log(`ROLES`);
|
|
1437
|
+
for (const r of model.roles) {
|
|
1438
|
+
const inh = r.inherits?.length ? ` (inherits: ${r.inherits.join(", ")})` : "";
|
|
1439
|
+
console.log(` ${r.key.padEnd(15)} ${r.description}${inh}`);
|
|
1440
|
+
}
|
|
1441
|
+
console.log(``);
|
|
1442
|
+
}
|
|
1443
|
+
if (model.resources?.length) {
|
|
1444
|
+
console.log(`RESOURCES`);
|
|
1445
|
+
for (const r of model.resources) {
|
|
1446
|
+
const coll = r.collection ? ` [${r.collection}]` : "";
|
|
1447
|
+
console.log(` ${r.key}${coll} — owned by ${r.ownedBy}`);
|
|
1448
|
+
console.log(` ${r.description}`);
|
|
1449
|
+
for (const p of (r.permissions || [])) {
|
|
1450
|
+
const cond = p.condition ? ` (${p.condition})` : "";
|
|
1451
|
+
console.log(` ${p.role.padEnd(15)} ${p.actions.join(",")}${cond}`);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
console.log(``);
|
|
1455
|
+
}
|
|
1456
|
+
if (model.isolationRules?.length) {
|
|
1457
|
+
console.log(`ISOLATION RULES`);
|
|
1458
|
+
for (const i of model.isolationRules) {
|
|
1459
|
+
console.log(` [${i.scope}] ${i.description}`);
|
|
1460
|
+
if (i.enforcedBy?.length) console.log(` enforced by: ${i.enforcedBy.join(", ")}`);
|
|
1461
|
+
}
|
|
1462
|
+
console.log(``);
|
|
1463
|
+
}
|
|
1464
|
+
process.exit(0);
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
accessCmd
|
|
1468
|
+
.command("set")
|
|
1469
|
+
.description("Upload (replace) the entire access model from a JSON file")
|
|
1470
|
+
.requiredOption("-f, --file <path>", "Path to access model JSON")
|
|
1471
|
+
.option("--project <id>", "Target project ID (overrides default)")
|
|
1472
|
+
.option("-e, --email <email>", "Login email")
|
|
1473
|
+
.option("-p, --password <password>", "Login password")
|
|
1474
|
+
.action(async (options) => {
|
|
1475
|
+
await authenticate(options);
|
|
1476
|
+
const creds = getCredentials();
|
|
1477
|
+
const projectId = options.project || creds?.projectId;
|
|
1478
|
+
if (!projectId) { console.error("No project configured. Run: qatools setup, or pass --project <id>"); process.exit(1); }
|
|
1479
|
+
const model = JSON.parse(fs.readFileSync(path.resolve(options.file), "utf8"));
|
|
1480
|
+
const saveModel = httpsCallable(functions, "saveAccessModel");
|
|
1481
|
+
try {
|
|
1482
|
+
const result = await saveModel({ projectId, model });
|
|
1483
|
+
console.log(`Access model saved (${result.data.id}) — project ${projectId}`);
|
|
1484
|
+
} catch (e) {
|
|
1485
|
+
console.error("Failed:", e.message);
|
|
1486
|
+
process.exit(1);
|
|
1487
|
+
}
|
|
1488
|
+
process.exit(0);
|
|
1489
|
+
});
|
|
1490
|
+
|
|
1373
1491
|
program
|
|
1374
1492
|
.command("epics")
|
|
1375
1493
|
.description("List epics for the project")
|