@geolonia/geonicdb-cli 0.7.0 → 0.8.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/README.md +91 -26
- package/dist/index.js +332 -81
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -785,6 +785,15 @@ var GdbClient = class _GdbClient {
|
|
|
785
785
|
canRefresh() {
|
|
786
786
|
return (!!this.refreshToken || !!this.clientId && !!this.clientSecret) && !this.apiKey;
|
|
787
787
|
}
|
|
788
|
+
/** Check whether an error indicates an authentication/token problem that may be resolved by refreshing. */
|
|
789
|
+
static isTokenError(err) {
|
|
790
|
+
if (err.status === 401) return true;
|
|
791
|
+
if (err.status === 403) {
|
|
792
|
+
const msg = (err.message ?? "").toLowerCase();
|
|
793
|
+
return msg.includes("not assigned to any tenant") || msg.includes("invalid token");
|
|
794
|
+
}
|
|
795
|
+
return false;
|
|
796
|
+
}
|
|
788
797
|
async performTokenRefresh() {
|
|
789
798
|
if (this.refreshPromise) return this.refreshPromise;
|
|
790
799
|
this.refreshPromise = this.doRefresh();
|
|
@@ -888,7 +897,7 @@ var GdbClient = class _GdbClient {
|
|
|
888
897
|
try {
|
|
889
898
|
return await this.executeRequest(method, path, options);
|
|
890
899
|
} catch (err) {
|
|
891
|
-
if (err instanceof GdbClientError && err
|
|
900
|
+
if (err instanceof GdbClientError && _GdbClient.isTokenError(err) && this.canRefresh()) {
|
|
892
901
|
const refreshed = await this.performTokenRefresh();
|
|
893
902
|
if (refreshed) {
|
|
894
903
|
return await this.executeRequest(method, path, options);
|
|
@@ -917,7 +926,7 @@ var GdbClient = class _GdbClient {
|
|
|
917
926
|
try {
|
|
918
927
|
return await this.executeRawRequest(method, path, options);
|
|
919
928
|
} catch (err) {
|
|
920
|
-
if (err instanceof GdbClientError && err
|
|
929
|
+
if (err instanceof GdbClientError && _GdbClient.isTokenError(err) && this.canRefresh()) {
|
|
921
930
|
const refreshed = await this.performTokenRefresh();
|
|
922
931
|
if (refreshed) {
|
|
923
932
|
return await this.executeRawRequest(method, path, options);
|
|
@@ -937,31 +946,6 @@ var GdbClientError = class extends Error {
|
|
|
937
946
|
};
|
|
938
947
|
|
|
939
948
|
// src/helpers.ts
|
|
940
|
-
var SCOPES_HELP_NOTES = [
|
|
941
|
-
"Valid scopes:",
|
|
942
|
-
" read:entities, write:entities, read:subscriptions, write:subscriptions,",
|
|
943
|
-
" read:registrations, write:registrations, read:rules, write:rules,",
|
|
944
|
-
" read:custom-data-models, write:custom-data-models,",
|
|
945
|
-
" admin:users, admin:tenants, admin:policies, admin:oauth-clients,",
|
|
946
|
-
" admin:api-keys, admin:metrics",
|
|
947
|
-
"",
|
|
948
|
-
"admin:X implies both read:X and write:X.",
|
|
949
|
-
"write:X does NOT imply read:X \u2014 specify both if needed."
|
|
950
|
-
];
|
|
951
|
-
var API_KEY_SCOPES_HELP_NOTES = [
|
|
952
|
-
"Valid scopes:",
|
|
953
|
-
" read:entities, write:entities, read:subscriptions, write:subscriptions,",
|
|
954
|
-
" read:registrations, write:registrations"
|
|
955
|
-
];
|
|
956
|
-
var VALID_PERMISSIONS = /* @__PURE__ */ new Set(["read", "write", "create", "update", "delete"]);
|
|
957
|
-
function parsePermissions(raw) {
|
|
958
|
-
const permissions = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
959
|
-
if (permissions.length === 0 || permissions.some((p) => !VALID_PERMISSIONS.has(p))) {
|
|
960
|
-
printError("--permissions must be a comma-separated list of: read, write, create, update, delete");
|
|
961
|
-
process.exit(1);
|
|
962
|
-
}
|
|
963
|
-
return permissions;
|
|
964
|
-
}
|
|
965
949
|
function resolveOptions(cmd) {
|
|
966
950
|
const opts = cmd.optsWithGlobals();
|
|
967
951
|
const config = loadConfig(opts.profile);
|
|
@@ -1027,6 +1011,8 @@ function withErrorHandler(fn) {
|
|
|
1027
1011
|
}
|
|
1028
1012
|
if (err instanceof GdbClientError && err.status === 401) {
|
|
1029
1013
|
printError("Authentication failed. Please re-authenticate (e.g., `geonic auth login` or check your API key).");
|
|
1014
|
+
} else if (err instanceof GdbClientError && err.status === 403 && /not assigned to any tenant|invalid token/i.test(err.message)) {
|
|
1015
|
+
printError("Authentication failed. Please re-authenticate (e.g., `geonic auth login` or check your API key).");
|
|
1030
1016
|
} else if (err instanceof GdbClientError && err.status === 403) {
|
|
1031
1017
|
const detail = (err.ngsiError?.detail ?? err.ngsiError?.description ?? "").toLowerCase();
|
|
1032
1018
|
if (detail.includes("entity type") || detail.includes("allowedentitytypes")) {
|
|
@@ -1335,16 +1321,16 @@ function addMeOAuthClientsSubcommand(me) {
|
|
|
1335
1321
|
command: "geonic me oauth-clients list"
|
|
1336
1322
|
}
|
|
1337
1323
|
]);
|
|
1338
|
-
const create = oauthClients.command("create [json]").description("Create a new OAuth client").option("--name <name>", "Client name").option("--
|
|
1324
|
+
const create = oauthClients.command("create [json]").description("Create a new OAuth client").option("--name <name>", "Client name").option("--policy <policyId>", "Policy ID to attach").option("--save", "Save credentials to config for automatic re-authentication").action(
|
|
1339
1325
|
withErrorHandler(async (json, _opts, cmd) => {
|
|
1340
1326
|
const opts = cmd.opts();
|
|
1341
1327
|
let body;
|
|
1342
1328
|
if (json) {
|
|
1343
1329
|
body = await parseJsonInput(json);
|
|
1344
|
-
} else if (opts.name || opts.
|
|
1330
|
+
} else if (opts.name || opts.policy) {
|
|
1345
1331
|
const payload = {};
|
|
1346
|
-
if (opts.name) payload.
|
|
1347
|
-
if (opts.
|
|
1332
|
+
if (opts.name) payload.name = opts.name;
|
|
1333
|
+
if (opts.policy) payload.policyId = opts.policy;
|
|
1348
1334
|
body = payload;
|
|
1349
1335
|
} else {
|
|
1350
1336
|
body = await parseJsonInput();
|
|
@@ -1367,8 +1353,7 @@ function addMeOAuthClientsSubcommand(me) {
|
|
|
1367
1353
|
const tokenResult = await clientCredentialsGrant({
|
|
1368
1354
|
baseUrl,
|
|
1369
1355
|
clientId,
|
|
1370
|
-
clientSecret
|
|
1371
|
-
scope: data.allowedScopes?.join(" ")
|
|
1356
|
+
clientSecret
|
|
1372
1357
|
});
|
|
1373
1358
|
const config = loadConfig(globalOpts.profile);
|
|
1374
1359
|
config.clientId = clientId;
|
|
@@ -1386,11 +1371,18 @@ function addMeOAuthClientsSubcommand(me) {
|
|
|
1386
1371
|
printSuccess("OAuth client created.");
|
|
1387
1372
|
})
|
|
1388
1373
|
);
|
|
1389
|
-
addNotes(create,
|
|
1374
|
+
addNotes(create, [
|
|
1375
|
+
"Use --policy to attach an existing XACML policy to the OAuth client.",
|
|
1376
|
+
"Manage policies with `geonic admin policies` commands."
|
|
1377
|
+
]);
|
|
1390
1378
|
addExamples(create, [
|
|
1391
1379
|
{
|
|
1392
1380
|
description: "Create an OAuth client with flags",
|
|
1393
|
-
command: "geonic me oauth-clients create --name my-ci-bot
|
|
1381
|
+
command: "geonic me oauth-clients create --name my-ci-bot"
|
|
1382
|
+
},
|
|
1383
|
+
{
|
|
1384
|
+
description: "Create with a policy attached",
|
|
1385
|
+
command: "geonic me oauth-clients create --name my-ci-bot --policy <policy-id>"
|
|
1394
1386
|
},
|
|
1395
1387
|
{
|
|
1396
1388
|
description: "Create and save credentials for auto-reauth",
|
|
@@ -1398,7 +1390,53 @@ function addMeOAuthClientsSubcommand(me) {
|
|
|
1398
1390
|
},
|
|
1399
1391
|
{
|
|
1400
1392
|
description: "Create an OAuth client from JSON",
|
|
1401
|
-
command: `geonic me oauth-clients create '{"
|
|
1393
|
+
command: `geonic me oauth-clients create '{"name":"my-bot","policyId":"<policy-id>"}'`
|
|
1394
|
+
}
|
|
1395
|
+
]);
|
|
1396
|
+
const update = oauthClients.command("update <clientId> [json]").description("Update an OAuth client").option("--name <name>", "Client name").option("--description <desc>", "Client description").option("--policy-id <policyId>", "Policy ID to attach (use 'null' to unbind)").option("--active", "Activate the OAuth client").option("--inactive", "Deactivate the OAuth client").action(
|
|
1397
|
+
withErrorHandler(async (clientId, json, _opts, cmd) => {
|
|
1398
|
+
const opts = cmd.opts();
|
|
1399
|
+
let body;
|
|
1400
|
+
if (json) {
|
|
1401
|
+
body = await parseJsonInput(json);
|
|
1402
|
+
} else if (opts.name || opts.description || opts.policyId !== void 0 || opts.active || opts.inactive) {
|
|
1403
|
+
const payload = {};
|
|
1404
|
+
if (opts.name) payload.name = opts.name;
|
|
1405
|
+
if (opts.description) payload.description = opts.description;
|
|
1406
|
+
if (opts.policyId !== void 0) payload.policyId = opts.policyId === "null" ? null : opts.policyId;
|
|
1407
|
+
if (opts.active) payload.isActive = true;
|
|
1408
|
+
if (opts.inactive) payload.isActive = false;
|
|
1409
|
+
body = payload;
|
|
1410
|
+
} else {
|
|
1411
|
+
body = await parseJsonInput();
|
|
1412
|
+
}
|
|
1413
|
+
const client = createClient(cmd);
|
|
1414
|
+
const format = getFormat(cmd);
|
|
1415
|
+
const response = await client.rawRequest(
|
|
1416
|
+
"PATCH",
|
|
1417
|
+
`/me/oauth-clients/${encodeURIComponent(String(clientId))}`,
|
|
1418
|
+
{ body }
|
|
1419
|
+
);
|
|
1420
|
+
outputResponse(response, format);
|
|
1421
|
+
printSuccess("OAuth client updated.");
|
|
1422
|
+
})
|
|
1423
|
+
);
|
|
1424
|
+
addExamples(update, [
|
|
1425
|
+
{
|
|
1426
|
+
description: "Rename an OAuth client",
|
|
1427
|
+
command: "geonic me oauth-clients update <client-id> --name new-name"
|
|
1428
|
+
},
|
|
1429
|
+
{
|
|
1430
|
+
description: "Attach a policy",
|
|
1431
|
+
command: "geonic me oauth-clients update <client-id> --policy-id <policy-id>"
|
|
1432
|
+
},
|
|
1433
|
+
{
|
|
1434
|
+
description: "Unbind policy",
|
|
1435
|
+
command: "geonic me oauth-clients update <client-id> --policy-id null"
|
|
1436
|
+
},
|
|
1437
|
+
{
|
|
1438
|
+
description: "Deactivate an OAuth client",
|
|
1439
|
+
command: "geonic me oauth-clients update <client-id> --inactive"
|
|
1402
1440
|
}
|
|
1403
1441
|
]);
|
|
1404
1442
|
const del = oauthClients.command("delete <id>").description("Delete an OAuth client").action(
|
|
@@ -1417,6 +1455,25 @@ function addMeOAuthClientsSubcommand(me) {
|
|
|
1417
1455
|
command: "geonic me oauth-clients delete <client-id>"
|
|
1418
1456
|
}
|
|
1419
1457
|
]);
|
|
1458
|
+
const regenerateSecret = oauthClients.command("regenerate-secret <clientId>").description("Regenerate the client secret of an OAuth client").action(
|
|
1459
|
+
withErrorHandler(async (clientId, _opts, cmd) => {
|
|
1460
|
+
const client = createClient(cmd);
|
|
1461
|
+
const format = getFormat(cmd);
|
|
1462
|
+
const response = await client.rawRequest(
|
|
1463
|
+
"POST",
|
|
1464
|
+
`/me/oauth-clients/${encodeURIComponent(String(clientId))}/regenerate-secret`
|
|
1465
|
+
);
|
|
1466
|
+
printWarning("Save the new clientSecret now \u2014 it will not be shown again.");
|
|
1467
|
+
outputResponse(response, format);
|
|
1468
|
+
printSuccess("OAuth client secret regenerated.");
|
|
1469
|
+
})
|
|
1470
|
+
);
|
|
1471
|
+
addExamples(regenerateSecret, [
|
|
1472
|
+
{
|
|
1473
|
+
description: "Regenerate client secret",
|
|
1474
|
+
command: "geonic me oauth-clients regenerate-secret <client-id>"
|
|
1475
|
+
}
|
|
1476
|
+
]);
|
|
1420
1477
|
}
|
|
1421
1478
|
|
|
1422
1479
|
// src/commands/me-api-keys.ts
|
|
@@ -1436,7 +1493,7 @@ function addMeApiKeysSubcommand(me) {
|
|
|
1436
1493
|
command: "geonic me api-keys list"
|
|
1437
1494
|
}
|
|
1438
1495
|
]);
|
|
1439
|
-
const create = apiKeys.command("create [json]").description("Create a new API key").option("--name <name>", "Key name").option("--
|
|
1496
|
+
const create = apiKeys.command("create [json]").description("Create a new API key").option("--name <name>", "Key name").option("--policy <policyId>", "Policy ID to attach").option("--origins <origins>", "Allowed origins (comma-separated)").option("--rate-limit <n>", "Rate limit per minute").option("--dpop-required", "Require DPoP token binding").option("--save", "Save the API key to config for automatic use").action(
|
|
1440
1497
|
withErrorHandler(async (json, _opts, cmd) => {
|
|
1441
1498
|
const opts = cmd.opts();
|
|
1442
1499
|
if (opts.origins !== void 0) {
|
|
@@ -1449,14 +1506,12 @@ function addMeApiKeysSubcommand(me) {
|
|
|
1449
1506
|
let body;
|
|
1450
1507
|
if (json) {
|
|
1451
1508
|
body = await parseJsonInput(json);
|
|
1452
|
-
} else if (opts.name || opts.
|
|
1509
|
+
} else if (opts.name || opts.policy || opts.origins || opts.rateLimit || opts.dpopRequired !== void 0) {
|
|
1453
1510
|
const payload = {};
|
|
1454
1511
|
if (opts.name) payload.name = opts.name;
|
|
1455
|
-
if (opts.
|
|
1512
|
+
if (opts.policy) payload.policyId = opts.policy;
|
|
1456
1513
|
if (opts.origins) payload.allowedOrigins = opts.origins.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1457
|
-
if (opts.entityTypes) payload.allowedEntityTypes = opts.entityTypes.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1458
1514
|
if (opts.dpopRequired !== void 0) payload.dpopRequired = opts.dpopRequired;
|
|
1459
|
-
if (opts.permissions) payload.permissions = parsePermissions(opts.permissions);
|
|
1460
1515
|
if (opts.rateLimit) {
|
|
1461
1516
|
const raw = opts.rateLimit.trim();
|
|
1462
1517
|
if (!/^\d+$/.test(raw)) {
|
|
@@ -1506,20 +1561,13 @@ function addMeApiKeysSubcommand(me) {
|
|
|
1506
1561
|
})
|
|
1507
1562
|
);
|
|
1508
1563
|
addNotes(create, [
|
|
1509
|
-
|
|
1510
|
-
""
|
|
1511
|
-
"Valid permissions: read, write, create, update, delete",
|
|
1512
|
-
" write = create + update + delete",
|
|
1513
|
-
" Permissions auto-generate XACML policies (allowedEntityTypes respected)."
|
|
1564
|
+
"Use --policy to attach an existing XACML policy to the API key.",
|
|
1565
|
+
"Manage policies with `geonic admin policies` commands."
|
|
1514
1566
|
]);
|
|
1515
1567
|
addExamples(create, [
|
|
1516
1568
|
{
|
|
1517
|
-
description: "Create an API key with
|
|
1518
|
-
command: "geonic me api-keys create --name my-app --
|
|
1519
|
-
},
|
|
1520
|
-
{
|
|
1521
|
-
description: "Create with permissions (auto-generates XACML policy)",
|
|
1522
|
-
command: "geonic me api-keys create --name my-app --permissions read,write --save"
|
|
1569
|
+
description: "Create an API key with a policy",
|
|
1570
|
+
command: "geonic me api-keys create --name my-app --policy <policy-id>"
|
|
1523
1571
|
},
|
|
1524
1572
|
{
|
|
1525
1573
|
description: "Create and save API key to config",
|
|
@@ -1527,7 +1575,7 @@ function addMeApiKeysSubcommand(me) {
|
|
|
1527
1575
|
},
|
|
1528
1576
|
{
|
|
1529
1577
|
description: "Create an API key from JSON",
|
|
1530
|
-
command: `geonic me api-keys create '{"name":"my-app","
|
|
1578
|
+
command: `geonic me api-keys create '{"name":"my-app","policyId":"<policy-id>"}'`
|
|
1531
1579
|
},
|
|
1532
1580
|
{
|
|
1533
1581
|
description: "Create an API key with rate limiting",
|
|
@@ -1538,6 +1586,77 @@ function addMeApiKeysSubcommand(me) {
|
|
|
1538
1586
|
command: "geonic me api-keys create --name my-app --dpop-required"
|
|
1539
1587
|
}
|
|
1540
1588
|
]);
|
|
1589
|
+
const update = apiKeys.command("update <keyId> [json]").description("Update an API key").option("--name <name>", "Key name").option("--policy-id <policyId>", "Policy ID to attach (use 'null' to unbind)").option("--origins <origins>", "Allowed origins (comma-separated)").option("--rate-limit <n>", "Rate limit (requests per minute)").option("--dpop-required", "Require DPoP token binding").option("--no-dpop-required", "Disable DPoP requirement").option("--active", "Activate the API key").option("--inactive", "Deactivate the API key").action(
|
|
1590
|
+
withErrorHandler(async (keyId, json, _opts, cmd) => {
|
|
1591
|
+
const opts = cmd.opts();
|
|
1592
|
+
if (opts.origins !== void 0) {
|
|
1593
|
+
const parsed = opts.origins.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1594
|
+
if (parsed.length === 0) {
|
|
1595
|
+
printError("allowedOrigins must contain at least 1 item. Use '*' to allow all origins.");
|
|
1596
|
+
process.exit(1);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
let body;
|
|
1600
|
+
if (json) {
|
|
1601
|
+
body = await parseJsonInput(json);
|
|
1602
|
+
} else if (opts.name || opts.policyId !== void 0 || opts.origins !== void 0 || opts.rateLimit || opts.dpopRequired !== void 0 || opts.active || opts.inactive) {
|
|
1603
|
+
const payload = {};
|
|
1604
|
+
if (opts.name) payload.name = opts.name;
|
|
1605
|
+
if (opts.policyId !== void 0) payload.policyId = opts.policyId === "null" ? null : opts.policyId;
|
|
1606
|
+
if (opts.origins !== void 0) payload.allowedOrigins = opts.origins.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1607
|
+
if (opts.dpopRequired !== void 0) payload.dpopRequired = opts.dpopRequired;
|
|
1608
|
+
if (opts.rateLimit) {
|
|
1609
|
+
const raw = opts.rateLimit.trim();
|
|
1610
|
+
if (!/^\d+$/.test(raw)) {
|
|
1611
|
+
printError("--rate-limit must be a positive integer.");
|
|
1612
|
+
process.exit(1);
|
|
1613
|
+
}
|
|
1614
|
+
const perMinute = Number(raw);
|
|
1615
|
+
if (perMinute <= 0) {
|
|
1616
|
+
printError("--rate-limit must be a positive integer.");
|
|
1617
|
+
process.exit(1);
|
|
1618
|
+
}
|
|
1619
|
+
payload.rateLimit = { perMinute };
|
|
1620
|
+
}
|
|
1621
|
+
if (opts.active) payload.isActive = true;
|
|
1622
|
+
if (opts.inactive) payload.isActive = false;
|
|
1623
|
+
body = payload;
|
|
1624
|
+
} else {
|
|
1625
|
+
body = await parseJsonInput();
|
|
1626
|
+
}
|
|
1627
|
+
const client = createClient(cmd);
|
|
1628
|
+
const format = getFormat(cmd);
|
|
1629
|
+
const response = await client.rawRequest(
|
|
1630
|
+
"PATCH",
|
|
1631
|
+
`/me/api-keys/${encodeURIComponent(String(keyId))}`,
|
|
1632
|
+
{ body }
|
|
1633
|
+
);
|
|
1634
|
+
outputResponse(response, format);
|
|
1635
|
+
console.error("API key updated.");
|
|
1636
|
+
})
|
|
1637
|
+
);
|
|
1638
|
+
addExamples(update, [
|
|
1639
|
+
{
|
|
1640
|
+
description: "Rename an API key",
|
|
1641
|
+
command: "geonic me api-keys update <key-id> --name new-name"
|
|
1642
|
+
},
|
|
1643
|
+
{
|
|
1644
|
+
description: "Attach a policy",
|
|
1645
|
+
command: "geonic me api-keys update <key-id> --policy-id <policy-id>"
|
|
1646
|
+
},
|
|
1647
|
+
{
|
|
1648
|
+
description: "Unbind policy",
|
|
1649
|
+
command: "geonic me api-keys update <key-id> --policy-id null"
|
|
1650
|
+
},
|
|
1651
|
+
{
|
|
1652
|
+
description: "Deactivate an API key",
|
|
1653
|
+
command: "geonic me api-keys update <key-id> --inactive"
|
|
1654
|
+
},
|
|
1655
|
+
{
|
|
1656
|
+
description: "Update from JSON",
|
|
1657
|
+
command: `geonic me api-keys update <key-id> '{"name":"new-name","rateLimit":{"perMinute":60}}'`
|
|
1658
|
+
}
|
|
1659
|
+
]);
|
|
1541
1660
|
const del = apiKeys.command("delete <keyId>").description("Delete an API key").action(
|
|
1542
1661
|
withErrorHandler(async (keyId, _opts, cmd) => {
|
|
1543
1662
|
const client = createClient(cmd);
|
|
@@ -1556,6 +1675,149 @@ function addMeApiKeysSubcommand(me) {
|
|
|
1556
1675
|
]);
|
|
1557
1676
|
}
|
|
1558
1677
|
|
|
1678
|
+
// src/commands/me-policies.ts
|
|
1679
|
+
function addMePoliciesSubcommand(me) {
|
|
1680
|
+
const policies = me.command("policies").description("Manage your personal XACML policies");
|
|
1681
|
+
const list = policies.command("list").description("List your personal policies").action(
|
|
1682
|
+
withErrorHandler(async (_opts, cmd) => {
|
|
1683
|
+
const client = createClient(cmd);
|
|
1684
|
+
const format = getFormat(cmd);
|
|
1685
|
+
const response = await client.rawRequest("GET", "/me/policies");
|
|
1686
|
+
outputResponse(response, format);
|
|
1687
|
+
})
|
|
1688
|
+
);
|
|
1689
|
+
addExamples(list, [
|
|
1690
|
+
{
|
|
1691
|
+
description: "List your personal policies",
|
|
1692
|
+
command: "geonic me policies list"
|
|
1693
|
+
}
|
|
1694
|
+
]);
|
|
1695
|
+
const get = policies.command("get <policyId>").description("Get a personal policy by ID").action(
|
|
1696
|
+
withErrorHandler(async (policyId, _opts, cmd) => {
|
|
1697
|
+
const client = createClient(cmd);
|
|
1698
|
+
const format = getFormat(cmd);
|
|
1699
|
+
const response = await client.rawRequest(
|
|
1700
|
+
"GET",
|
|
1701
|
+
`/me/policies/${encodeURIComponent(String(policyId))}`
|
|
1702
|
+
);
|
|
1703
|
+
outputResponse(response, format);
|
|
1704
|
+
})
|
|
1705
|
+
);
|
|
1706
|
+
addExamples(get, [
|
|
1707
|
+
{
|
|
1708
|
+
description: "Get a personal policy by ID",
|
|
1709
|
+
command: "geonic me policies get <policy-id>"
|
|
1710
|
+
}
|
|
1711
|
+
]);
|
|
1712
|
+
const create = policies.command("create [json]").description(
|
|
1713
|
+
`Create a personal XACML policy
|
|
1714
|
+
|
|
1715
|
+
Constraints (enforced server-side):
|
|
1716
|
+
- priority is fixed at 100 (user role minimum)
|
|
1717
|
+
- scope is 'personal' \u2014 not applied tenant-wide
|
|
1718
|
+
- target is required
|
|
1719
|
+
- data API paths only (/v2/**, /ngsi-ld/** etc.)
|
|
1720
|
+
|
|
1721
|
+
Example \u2014 GET-only policy for /v2/**:
|
|
1722
|
+
{
|
|
1723
|
+
"policyId": "my-readonly",
|
|
1724
|
+
"target": {
|
|
1725
|
+
"resources": [{"attributeId": "path", "matchValue": "/v2/**", "matchFunction": "glob"}]
|
|
1726
|
+
},
|
|
1727
|
+
"rules": [
|
|
1728
|
+
{"ruleId": "allow-get", "effect": "Permit", "target": {"actions": [{"attributeId": "method", "matchValue": "GET"}]}},
|
|
1729
|
+
{"ruleId": "deny-others", "effect": "Deny"}
|
|
1730
|
+
]
|
|
1731
|
+
}`
|
|
1732
|
+
).option("--policy-id <id>", "Policy ID (auto-generated UUID if omitted)").option("--description <text>", "Policy description").action(
|
|
1733
|
+
withErrorHandler(async (json, _opts, cmd) => {
|
|
1734
|
+
const opts = cmd.opts();
|
|
1735
|
+
let body;
|
|
1736
|
+
if (json) {
|
|
1737
|
+
body = await parseJsonInput(json);
|
|
1738
|
+
} else if (opts.policyId || opts.description) {
|
|
1739
|
+
const payload = {};
|
|
1740
|
+
if (opts.policyId) payload.policyId = opts.policyId;
|
|
1741
|
+
if (opts.description) payload.description = opts.description;
|
|
1742
|
+
body = payload;
|
|
1743
|
+
} else {
|
|
1744
|
+
body = await parseJsonInput();
|
|
1745
|
+
}
|
|
1746
|
+
const client = createClient(cmd);
|
|
1747
|
+
const format = getFormat(cmd);
|
|
1748
|
+
const response = await client.rawRequest("POST", "/me/policies", { body });
|
|
1749
|
+
outputResponse(response, format);
|
|
1750
|
+
printSuccess("Policy created.");
|
|
1751
|
+
})
|
|
1752
|
+
);
|
|
1753
|
+
addNotes(create, [
|
|
1754
|
+
"priority is always set to 100 by the server regardless of the value you specify.",
|
|
1755
|
+
"Bind the policy to an API key or OAuth client with `geonic me api-keys update --policy-id` or `geonic me oauth-clients update --policy-id`."
|
|
1756
|
+
]);
|
|
1757
|
+
addExamples(create, [
|
|
1758
|
+
{
|
|
1759
|
+
description: "Create a GET-only policy from inline JSON",
|
|
1760
|
+
command: `geonic me policies create '{"policyId":"my-readonly","target":{"resources":[{"attributeId":"path","matchValue":"/v2/**","matchFunction":"glob"}]},"rules":[{"ruleId":"allow-get","effect":"Permit","target":{"actions":[{"attributeId":"method","matchValue":"GET"}]}},{"ruleId":"deny-others","effect":"Deny"}]}'`
|
|
1761
|
+
},
|
|
1762
|
+
{
|
|
1763
|
+
description: "Create from a JSON file",
|
|
1764
|
+
command: "geonic me policies create @policy.json"
|
|
1765
|
+
},
|
|
1766
|
+
{
|
|
1767
|
+
description: "Create from stdin",
|
|
1768
|
+
command: "cat policy.json | geonic me policies create"
|
|
1769
|
+
}
|
|
1770
|
+
]);
|
|
1771
|
+
const update = policies.command("update <policyId> [json]").description("Update a personal policy (partial update)").option("--description <text>", "Policy description").action(
|
|
1772
|
+
withErrorHandler(async (policyId, json, _opts, cmd) => {
|
|
1773
|
+
const opts = cmd.opts();
|
|
1774
|
+
let body;
|
|
1775
|
+
if (json) {
|
|
1776
|
+
body = await parseJsonInput(json);
|
|
1777
|
+
} else if (opts.description) {
|
|
1778
|
+
body = { description: opts.description };
|
|
1779
|
+
} else {
|
|
1780
|
+
body = await parseJsonInput();
|
|
1781
|
+
}
|
|
1782
|
+
const client = createClient(cmd);
|
|
1783
|
+
const format = getFormat(cmd);
|
|
1784
|
+
const response = await client.rawRequest(
|
|
1785
|
+
"PATCH",
|
|
1786
|
+
`/me/policies/${encodeURIComponent(String(policyId))}`,
|
|
1787
|
+
{ body }
|
|
1788
|
+
);
|
|
1789
|
+
outputResponse(response, format);
|
|
1790
|
+
printSuccess("Policy updated.");
|
|
1791
|
+
})
|
|
1792
|
+
);
|
|
1793
|
+
addExamples(update, [
|
|
1794
|
+
{
|
|
1795
|
+
description: "Update policy rules",
|
|
1796
|
+
command: `geonic me policies update <policy-id> '{"rules":[{"ruleId":"allow-get","effect":"Permit"}]}'`
|
|
1797
|
+
},
|
|
1798
|
+
{
|
|
1799
|
+
description: "Update from a JSON file",
|
|
1800
|
+
command: "geonic me policies update <policy-id> @patch.json"
|
|
1801
|
+
}
|
|
1802
|
+
]);
|
|
1803
|
+
const del = policies.command("delete <policyId>").description("Delete a personal policy").action(
|
|
1804
|
+
withErrorHandler(async (policyId, _opts, cmd) => {
|
|
1805
|
+
const client = createClient(cmd);
|
|
1806
|
+
await client.rawRequest(
|
|
1807
|
+
"DELETE",
|
|
1808
|
+
`/me/policies/${encodeURIComponent(String(policyId))}`
|
|
1809
|
+
);
|
|
1810
|
+
printSuccess("Policy deleted.");
|
|
1811
|
+
})
|
|
1812
|
+
);
|
|
1813
|
+
addExamples(del, [
|
|
1814
|
+
{
|
|
1815
|
+
description: "Delete a personal policy",
|
|
1816
|
+
command: "geonic me policies delete <policy-id>"
|
|
1817
|
+
}
|
|
1818
|
+
]);
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1559
1821
|
// src/commands/auth.ts
|
|
1560
1822
|
function createLoginCommand() {
|
|
1561
1823
|
return new Command("login").description("Authenticate and save token").option("--client-credentials", "Use OAuth 2.0 Client Credentials flow").option("--client-id <id>", "OAuth client ID").option("--client-secret <secret>", "OAuth client secret").option("--scope <scopes>", "OAuth scopes (space-separated)").option("--tenant-id <id>", "Tenant ID for scoped authentication").action(
|
|
@@ -1887,6 +2149,7 @@ function registerAuthCommands(program2) {
|
|
|
1887
2149
|
]);
|
|
1888
2150
|
addMeOAuthClientsSubcommand(me);
|
|
1889
2151
|
addMeApiKeysSubcommand(me);
|
|
2152
|
+
addMePoliciesSubcommand(me);
|
|
1890
2153
|
program2.addCommand(createLoginCommand(), { hidden: true });
|
|
1891
2154
|
program2.addCommand(createLogoutCommand(), { hidden: true });
|
|
1892
2155
|
const hiddenWhoami = new Command("whoami").description("Display current authenticated user").action(createMeAction());
|
|
@@ -3386,7 +3649,7 @@ function registerPoliciesCommand(parent) {
|
|
|
3386
3649
|
}
|
|
3387
3650
|
]);
|
|
3388
3651
|
const create = policies.command("create [json]").description(
|
|
3389
|
-
'Create a new policy\n\nJSON payload examples:\n\n Allow all entities:\n {\n "description": "Allow all entities",\n "rules": [{"ruleId": "allow-all", "effect": "Permit"}]\n }\n\n Allow GET access to a specific entity type:\n {\n "description": "Allow GET access to Landmark entities",\n "target": {\n "resources": [{"attributeId": "entityType", "matchValue": "Landmark"}],\n "actions": [{"attributeId": "method", "matchValue": "GET"}]\n },\n "rules": [{"ruleId": "permit-get", "effect": "Permit"}]\n }\n\nTarget fields:\n subjects \u2014 attributeId: role, userId, email, tenantId\n resources \u2014 attributeId: path, entityType, entityId, entityOwner, tenantService, servicePath\n actions \u2014 attributeId: method (GET, POST, PATCH, DELETE)\n\nEach element: {attributeId, matchValue, matchFunction?}\n matchFunction: "string-equal" (default) | "string-regexp" | "glob"\n\nPriority:
|
|
3652
|
+
'Create a new policy\n\nJSON payload examples:\n\n Allow all entities:\n {\n "description": "Allow all entities",\n "rules": [{"ruleId": "allow-all", "effect": "Permit"}]\n }\n\n Allow GET access to a specific entity type:\n {\n "description": "Allow GET access to Landmark entities",\n "target": {\n "resources": [{"attributeId": "entityType", "matchValue": "Landmark"}],\n "actions": [{"attributeId": "method", "matchValue": "GET"}]\n },\n "rules": [{"ruleId": "permit-get", "effect": "Permit"}]\n }\n\nTarget fields:\n subjects \u2014 attributeId: role, userId, email, tenantId\n resources \u2014 attributeId: path, entityType, entityId, entityOwner, tenantService, servicePath\n actions \u2014 attributeId: method (GET, POST, PATCH, DELETE)\n\nEach element: {attributeId, matchValue, matchFunction?}\n matchFunction: "string-equal" (default) | "string-regexp" | "glob"\n\nPriority: smaller value = higher precedence (e.g. priority 10 overrides user default at 100).\n tenant_admin: minimum priority 10. user self-service (/me/policies): fixed at 100.\n\nDefault role policies (priority 100):\n user \u2192 /v2/** and /ngsi-ld/** all methods Permit; other data APIs GET only\n api_key \u2192 all Deny, anonymous \u2192 all Deny'
|
|
3390
3653
|
).action(
|
|
3391
3654
|
withErrorHandler(async (json, _opts, cmd) => {
|
|
3392
3655
|
const body = await parseJsonInput(json);
|
|
@@ -3542,7 +3805,7 @@ function registerOAuthClientsCommand(parent) {
|
|
|
3542
3805
|
}
|
|
3543
3806
|
]);
|
|
3544
3807
|
const create = oauthClients.command("create [json]").description(
|
|
3545
|
-
'Create a new OAuth client\n\nJSON payload example:\n {\n "
|
|
3808
|
+
'Create a new OAuth client\n\nJSON payload example:\n {\n "name": "my-app",\n "policyId": "<policy-id>"\n }'
|
|
3546
3809
|
).action(
|
|
3547
3810
|
withErrorHandler(async (json, _opts, cmd) => {
|
|
3548
3811
|
const body = await parseJsonInput(json);
|
|
@@ -3558,7 +3821,7 @@ function registerOAuthClientsCommand(parent) {
|
|
|
3558
3821
|
addExamples(create, [
|
|
3559
3822
|
{
|
|
3560
3823
|
description: "Create with inline JSON",
|
|
3561
|
-
command: `geonic admin oauth-clients create '{"
|
|
3824
|
+
command: `geonic admin oauth-clients create '{"name":"my-app","policyId":"<policy-id>"}'`
|
|
3562
3825
|
},
|
|
3563
3826
|
{
|
|
3564
3827
|
description: "Create from a JSON file",
|
|
@@ -3697,9 +3960,8 @@ function validateOrigins(body, opts) {
|
|
|
3697
3960
|
function buildBodyFromFlags(opts) {
|
|
3698
3961
|
const payload = {};
|
|
3699
3962
|
if (opts.name) payload.name = opts.name;
|
|
3700
|
-
if (opts.
|
|
3963
|
+
if (opts.policy) payload.policyId = opts.policy;
|
|
3701
3964
|
if (opts.origins) payload.allowedOrigins = opts.origins.split(",").map((s) => s.trim()).filter(Boolean);
|
|
3702
|
-
if (opts.entityTypes) payload.allowedEntityTypes = opts.entityTypes.split(",").map((s) => s.trim()).filter(Boolean);
|
|
3703
3965
|
if (opts.rateLimit) {
|
|
3704
3966
|
const raw = String(opts.rateLimit).trim();
|
|
3705
3967
|
if (!/^\d+$/.test(raw)) {
|
|
@@ -3714,7 +3976,6 @@ function buildBodyFromFlags(opts) {
|
|
|
3714
3976
|
payload.rateLimit = { perMinute };
|
|
3715
3977
|
}
|
|
3716
3978
|
if (opts.dpopRequired !== void 0) payload.dpopRequired = opts.dpopRequired;
|
|
3717
|
-
if (opts.permissions) payload.permissions = parsePermissions(opts.permissions);
|
|
3718
3979
|
if (opts.tenantId) payload.tenantId = opts.tenantId;
|
|
3719
3980
|
return payload;
|
|
3720
3981
|
}
|
|
@@ -3760,14 +4021,14 @@ function registerApiKeysCommand(parent) {
|
|
|
3760
4021
|
command: "geonic admin api-keys get <key-id>"
|
|
3761
4022
|
}
|
|
3762
4023
|
]);
|
|
3763
|
-
const create = apiKeys.command("create [json]").description("Create a new API key").option("--name <name>", "Key name").option("--
|
|
4024
|
+
const create = apiKeys.command("create [json]").description("Create a new API key").option("--name <name>", "Key name").option("--policy <policyId>", "Policy ID to attach").option("--origins <origins>", "Comma-separated origins").option("--rate-limit <n>", "Rate limit per minute").option("--dpop-required", "Require DPoP token binding").option("--tenant-id <id>", "Tenant ID").option("--save", "Save the API key to profile config").action(
|
|
3764
4025
|
withErrorHandler(async (json, _opts, cmd) => {
|
|
3765
4026
|
const opts = cmd.opts();
|
|
3766
4027
|
validateOrigins(void 0, opts);
|
|
3767
4028
|
let body;
|
|
3768
4029
|
if (json) {
|
|
3769
4030
|
body = await parseJsonInput(json);
|
|
3770
|
-
} else if (opts.name || opts.
|
|
4031
|
+
} else if (opts.name || opts.policy || opts.origins || opts.rateLimit || opts.dpopRequired !== void 0 || opts.tenantId) {
|
|
3771
4032
|
body = buildBodyFromFlags(opts);
|
|
3772
4033
|
} else {
|
|
3773
4034
|
body = await parseJsonInput();
|
|
@@ -3800,20 +4061,13 @@ function registerApiKeysCommand(parent) {
|
|
|
3800
4061
|
})
|
|
3801
4062
|
);
|
|
3802
4063
|
addNotes(create, [
|
|
3803
|
-
|
|
3804
|
-
""
|
|
3805
|
-
"Valid permissions: read, write, create, update, delete",
|
|
3806
|
-
" write = create + update + delete",
|
|
3807
|
-
" Permissions auto-generate XACML policies (allowedEntityTypes respected)."
|
|
4064
|
+
"Use --policy to attach an existing XACML policy to the API key.",
|
|
4065
|
+
"Manage policies with `geonic admin policies` commands."
|
|
3808
4066
|
]);
|
|
3809
4067
|
addExamples(create, [
|
|
3810
4068
|
{
|
|
3811
|
-
description: "Create an API key with
|
|
3812
|
-
command: "geonic admin api-keys create --name my-key --
|
|
3813
|
-
},
|
|
3814
|
-
{
|
|
3815
|
-
description: "Create with permissions (auto-generates XACML policy)",
|
|
3816
|
-
command: "geonic admin api-keys create --name my-key --permissions read,write --origins '*'"
|
|
4069
|
+
description: "Create an API key with a policy",
|
|
4070
|
+
command: "geonic admin api-keys create --name my-key --policy <policy-id> --origins '*'"
|
|
3817
4071
|
},
|
|
3818
4072
|
{
|
|
3819
4073
|
description: "Create an API key with DPoP required",
|
|
@@ -3824,7 +4078,7 @@ function registerApiKeysCommand(parent) {
|
|
|
3824
4078
|
command: "geonic admin api-keys create @key.json --save"
|
|
3825
4079
|
}
|
|
3826
4080
|
]);
|
|
3827
|
-
const update = apiKeys.command("update <keyId> [json]").description("Update an API key").option("--name <name>", "Key name").option("--
|
|
4081
|
+
const update = apiKeys.command("update <keyId> [json]").description("Update an API key").option("--name <name>", "Key name").option("--policy <policyId>", "Policy ID to attach").option("--origins <origins>", "Comma-separated origins").option("--rate-limit <n>", "Rate limit per minute").option("--dpop-required", "Require DPoP token binding").option("--no-dpop-required", "Disable DPoP token binding").action(
|
|
3828
4082
|
withErrorHandler(
|
|
3829
4083
|
async (keyId, json, _opts, cmd) => {
|
|
3830
4084
|
const opts = cmd.opts();
|
|
@@ -3832,7 +4086,7 @@ function registerApiKeysCommand(parent) {
|
|
|
3832
4086
|
let body;
|
|
3833
4087
|
if (json) {
|
|
3834
4088
|
body = await parseJsonInput(json);
|
|
3835
|
-
} else if (opts.name || opts.
|
|
4089
|
+
} else if (opts.name || opts.policy || opts.origins || opts.rateLimit || opts.dpopRequired !== void 0) {
|
|
3836
4090
|
body = buildBodyFromFlags(opts);
|
|
3837
4091
|
} else {
|
|
3838
4092
|
body = await parseJsonInput();
|
|
@@ -3851,11 +4105,8 @@ function registerApiKeysCommand(parent) {
|
|
|
3851
4105
|
)
|
|
3852
4106
|
);
|
|
3853
4107
|
addNotes(update, [
|
|
3854
|
-
|
|
3855
|
-
""
|
|
3856
|
-
"Valid permissions: read, write, create, update, delete",
|
|
3857
|
-
" write = create + update + delete",
|
|
3858
|
-
" Permissions auto-generate XACML policies (allowedEntityTypes respected)."
|
|
4108
|
+
"Use --policy to attach an existing XACML policy to the API key.",
|
|
4109
|
+
"Manage policies with `geonic admin policies` commands."
|
|
3859
4110
|
]);
|
|
3860
4111
|
addExamples(update, [
|
|
3861
4112
|
{
|
|
@@ -3863,8 +4114,8 @@ function registerApiKeysCommand(parent) {
|
|
|
3863
4114
|
command: "geonic admin api-keys update <key-id> --name new-name"
|
|
3864
4115
|
},
|
|
3865
4116
|
{
|
|
3866
|
-
description: "
|
|
3867
|
-
command: "geonic admin api-keys update <key-id> --
|
|
4117
|
+
description: "Attach a policy",
|
|
4118
|
+
command: "geonic admin api-keys update <key-id> --policy <policy-id>"
|
|
3868
4119
|
},
|
|
3869
4120
|
{
|
|
3870
4121
|
description: "Enable DPoP requirement",
|