@geolonia/geonicdb-cli 0.6.4 → 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 +96 -15
- package/dist/index.js +359 -50
- 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,21 +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
|
-
"write:X implies read:X. admin:X implies both read:X and write:X."
|
|
949
|
-
];
|
|
950
|
-
var API_KEY_SCOPES_HELP_NOTES = [
|
|
951
|
-
"Valid scopes:",
|
|
952
|
-
" read:entities, write:entities, read:subscriptions, write:subscriptions,",
|
|
953
|
-
" read:registrations, write:registrations"
|
|
954
|
-
];
|
|
955
949
|
function resolveOptions(cmd) {
|
|
956
950
|
const opts = cmd.optsWithGlobals();
|
|
957
951
|
const config = loadConfig(opts.profile);
|
|
@@ -1017,6 +1011,8 @@ function withErrorHandler(fn) {
|
|
|
1017
1011
|
}
|
|
1018
1012
|
if (err instanceof GdbClientError && err.status === 401) {
|
|
1019
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).");
|
|
1020
1016
|
} else if (err instanceof GdbClientError && err.status === 403) {
|
|
1021
1017
|
const detail = (err.ngsiError?.detail ?? err.ngsiError?.description ?? "").toLowerCase();
|
|
1022
1018
|
if (detail.includes("entity type") || detail.includes("allowedentitytypes")) {
|
|
@@ -1325,16 +1321,16 @@ function addMeOAuthClientsSubcommand(me) {
|
|
|
1325
1321
|
command: "geonic me oauth-clients list"
|
|
1326
1322
|
}
|
|
1327
1323
|
]);
|
|
1328
|
-
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(
|
|
1329
1325
|
withErrorHandler(async (json, _opts, cmd) => {
|
|
1330
1326
|
const opts = cmd.opts();
|
|
1331
1327
|
let body;
|
|
1332
1328
|
if (json) {
|
|
1333
1329
|
body = await parseJsonInput(json);
|
|
1334
|
-
} else if (opts.name || opts.
|
|
1330
|
+
} else if (opts.name || opts.policy) {
|
|
1335
1331
|
const payload = {};
|
|
1336
|
-
if (opts.name) payload.
|
|
1337
|
-
if (opts.
|
|
1332
|
+
if (opts.name) payload.name = opts.name;
|
|
1333
|
+
if (opts.policy) payload.policyId = opts.policy;
|
|
1338
1334
|
body = payload;
|
|
1339
1335
|
} else {
|
|
1340
1336
|
body = await parseJsonInput();
|
|
@@ -1357,8 +1353,7 @@ function addMeOAuthClientsSubcommand(me) {
|
|
|
1357
1353
|
const tokenResult = await clientCredentialsGrant({
|
|
1358
1354
|
baseUrl,
|
|
1359
1355
|
clientId,
|
|
1360
|
-
clientSecret
|
|
1361
|
-
scope: data.allowedScopes?.join(" ")
|
|
1356
|
+
clientSecret
|
|
1362
1357
|
});
|
|
1363
1358
|
const config = loadConfig(globalOpts.profile);
|
|
1364
1359
|
config.clientId = clientId;
|
|
@@ -1376,11 +1371,18 @@ function addMeOAuthClientsSubcommand(me) {
|
|
|
1376
1371
|
printSuccess("OAuth client created.");
|
|
1377
1372
|
})
|
|
1378
1373
|
);
|
|
1379
|
-
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
|
+
]);
|
|
1380
1378
|
addExamples(create, [
|
|
1381
1379
|
{
|
|
1382
1380
|
description: "Create an OAuth client with flags",
|
|
1383
|
-
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>"
|
|
1384
1386
|
},
|
|
1385
1387
|
{
|
|
1386
1388
|
description: "Create and save credentials for auto-reauth",
|
|
@@ -1388,7 +1390,53 @@ function addMeOAuthClientsSubcommand(me) {
|
|
|
1388
1390
|
},
|
|
1389
1391
|
{
|
|
1390
1392
|
description: "Create an OAuth client from JSON",
|
|
1391
|
-
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"
|
|
1392
1440
|
}
|
|
1393
1441
|
]);
|
|
1394
1442
|
const del = oauthClients.command("delete <id>").description("Delete an OAuth client").action(
|
|
@@ -1407,6 +1455,25 @@ function addMeOAuthClientsSubcommand(me) {
|
|
|
1407
1455
|
command: "geonic me oauth-clients delete <client-id>"
|
|
1408
1456
|
}
|
|
1409
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
|
+
]);
|
|
1410
1477
|
}
|
|
1411
1478
|
|
|
1412
1479
|
// src/commands/me-api-keys.ts
|
|
@@ -1426,7 +1493,7 @@ function addMeApiKeysSubcommand(me) {
|
|
|
1426
1493
|
command: "geonic me api-keys list"
|
|
1427
1494
|
}
|
|
1428
1495
|
]);
|
|
1429
|
-
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(
|
|
1430
1497
|
withErrorHandler(async (json, _opts, cmd) => {
|
|
1431
1498
|
const opts = cmd.opts();
|
|
1432
1499
|
if (opts.origins !== void 0) {
|
|
@@ -1439,12 +1506,11 @@ function addMeApiKeysSubcommand(me) {
|
|
|
1439
1506
|
let body;
|
|
1440
1507
|
if (json) {
|
|
1441
1508
|
body = await parseJsonInput(json);
|
|
1442
|
-
} else if (opts.name || opts.
|
|
1509
|
+
} else if (opts.name || opts.policy || opts.origins || opts.rateLimit || opts.dpopRequired !== void 0) {
|
|
1443
1510
|
const payload = {};
|
|
1444
1511
|
if (opts.name) payload.name = opts.name;
|
|
1445
|
-
if (opts.
|
|
1512
|
+
if (opts.policy) payload.policyId = opts.policy;
|
|
1446
1513
|
if (opts.origins) payload.allowedOrigins = opts.origins.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1447
|
-
if (opts.entityTypes) payload.allowedEntityTypes = opts.entityTypes.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1448
1514
|
if (opts.dpopRequired !== void 0) payload.dpopRequired = opts.dpopRequired;
|
|
1449
1515
|
if (opts.rateLimit) {
|
|
1450
1516
|
const raw = opts.rateLimit.trim();
|
|
@@ -1494,11 +1560,14 @@ function addMeApiKeysSubcommand(me) {
|
|
|
1494
1560
|
console.error("API key created.");
|
|
1495
1561
|
})
|
|
1496
1562
|
);
|
|
1497
|
-
addNotes(create,
|
|
1563
|
+
addNotes(create, [
|
|
1564
|
+
"Use --policy to attach an existing XACML policy to the API key.",
|
|
1565
|
+
"Manage policies with `geonic admin policies` commands."
|
|
1566
|
+
]);
|
|
1498
1567
|
addExamples(create, [
|
|
1499
1568
|
{
|
|
1500
|
-
description: "Create an API key with
|
|
1501
|
-
command: "geonic me api-keys create --name my-app --
|
|
1569
|
+
description: "Create an API key with a policy",
|
|
1570
|
+
command: "geonic me api-keys create --name my-app --policy <policy-id>"
|
|
1502
1571
|
},
|
|
1503
1572
|
{
|
|
1504
1573
|
description: "Create and save API key to config",
|
|
@@ -1506,7 +1575,7 @@ function addMeApiKeysSubcommand(me) {
|
|
|
1506
1575
|
},
|
|
1507
1576
|
{
|
|
1508
1577
|
description: "Create an API key from JSON",
|
|
1509
|
-
command: `geonic me api-keys create '{"name":"my-app","
|
|
1578
|
+
command: `geonic me api-keys create '{"name":"my-app","policyId":"<policy-id>"}'`
|
|
1510
1579
|
},
|
|
1511
1580
|
{
|
|
1512
1581
|
description: "Create an API key with rate limiting",
|
|
@@ -1517,6 +1586,77 @@ function addMeApiKeysSubcommand(me) {
|
|
|
1517
1586
|
command: "geonic me api-keys create --name my-app --dpop-required"
|
|
1518
1587
|
}
|
|
1519
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
|
+
]);
|
|
1520
1660
|
const del = apiKeys.command("delete <keyId>").description("Delete an API key").action(
|
|
1521
1661
|
withErrorHandler(async (keyId, _opts, cmd) => {
|
|
1522
1662
|
const client = createClient(cmd);
|
|
@@ -1535,6 +1675,149 @@ function addMeApiKeysSubcommand(me) {
|
|
|
1535
1675
|
]);
|
|
1536
1676
|
}
|
|
1537
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
|
+
|
|
1538
1821
|
// src/commands/auth.ts
|
|
1539
1822
|
function createLoginCommand() {
|
|
1540
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(
|
|
@@ -1866,6 +2149,7 @@ function registerAuthCommands(program2) {
|
|
|
1866
2149
|
]);
|
|
1867
2150
|
addMeOAuthClientsSubcommand(me);
|
|
1868
2151
|
addMeApiKeysSubcommand(me);
|
|
2152
|
+
addMePoliciesSubcommand(me);
|
|
1869
2153
|
program2.addCommand(createLoginCommand(), { hidden: true });
|
|
1870
2154
|
program2.addCommand(createLogoutCommand(), { hidden: true });
|
|
1871
2155
|
const hiddenWhoami = new Command("whoami").description("Display current authenticated user").action(createMeAction());
|
|
@@ -3201,7 +3485,7 @@ function registerUsersCommand(parent) {
|
|
|
3201
3485
|
}
|
|
3202
3486
|
]);
|
|
3203
3487
|
const create = users.command("create [json]").description(
|
|
3204
|
-
'Create a new user\n\nJSON payload example:\n {\n "email": "user@example.com",\n "password": "SecurePassword123!",\n "role": "
|
|
3488
|
+
'Create a new user\n\nJSON payload example:\n {\n "email": "user@example.com",\n "password": "SecurePassword123!",\n "role": "tenant_admin",\n "tenantId": "<tenant-id>"\n }\n\nRoles: super_admin, tenant_admin, user\ntenantId is required for tenant_admin and user roles.'
|
|
3205
3489
|
).action(
|
|
3206
3490
|
withErrorHandler(async (json, _opts, cmd) => {
|
|
3207
3491
|
const body = await parseJsonInput(json);
|
|
@@ -3216,8 +3500,12 @@ function registerUsersCommand(parent) {
|
|
|
3216
3500
|
);
|
|
3217
3501
|
addExamples(create, [
|
|
3218
3502
|
{
|
|
3219
|
-
description: "Create
|
|
3220
|
-
command: `geonic admin users create '{"email":"
|
|
3503
|
+
description: "Create a tenant admin",
|
|
3504
|
+
command: `geonic admin users create '{"email":"admin@example.com","password":"SecurePass12345!","role":"tenant_admin","tenantId":"<tenant-id>"}'`
|
|
3505
|
+
},
|
|
3506
|
+
{
|
|
3507
|
+
description: "Create a user for a tenant",
|
|
3508
|
+
command: `geonic admin users create '{"email":"user@example.com","password":"SecurePass12345!","role":"user","tenantId":"<tenant-id>"}'`
|
|
3221
3509
|
},
|
|
3222
3510
|
{
|
|
3223
3511
|
description: "Create from a JSON file",
|
|
@@ -3361,7 +3649,7 @@ function registerPoliciesCommand(parent) {
|
|
|
3361
3649
|
}
|
|
3362
3650
|
]);
|
|
3363
3651
|
const create = policies.command("create [json]").description(
|
|
3364
|
-
'Create a new policy\n\nJSON payload
|
|
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'
|
|
3365
3653
|
).action(
|
|
3366
3654
|
withErrorHandler(async (json, _opts, cmd) => {
|
|
3367
3655
|
const body = await parseJsonInput(json);
|
|
@@ -3379,6 +3667,18 @@ function registerPoliciesCommand(parent) {
|
|
|
3379
3667
|
description: "Create with inline JSON",
|
|
3380
3668
|
command: `geonic admin policies create '{"description":"Allow all entities","rules":[{"ruleId":"allow-all","effect":"Permit"}]}'`
|
|
3381
3669
|
},
|
|
3670
|
+
{
|
|
3671
|
+
description: "Create with target (entity type + method)",
|
|
3672
|
+
command: `geonic admin policies create '{"description":"Allow GET Landmark","target":{"resources":[{"attributeId":"entityType","matchValue":"Landmark"}],"actions":[{"attributeId":"method","matchValue":"GET"}]},"rules":[{"ruleId":"permit-get","effect":"Permit"}]}'`
|
|
3673
|
+
},
|
|
3674
|
+
{
|
|
3675
|
+
description: "Create anonymous access policy",
|
|
3676
|
+
command: `geonic admin policies create '{"policyId":"public-read","target":{"subjects":[{"attributeId":"role","matchValue":"anonymous"}],"resources":[{"attributeId":"entityType","matchValue":"WeatherObserved"}],"actions":[{"attributeId":"method","matchValue":"GET"}]},"rules":[{"effect":"Permit"}]}'`
|
|
3677
|
+
},
|
|
3678
|
+
{
|
|
3679
|
+
description: "Create servicePath-based policy (glob match)",
|
|
3680
|
+
command: `geonic admin policies create '{"description":"Allow read on /opendata/**","priority":100,"target":{"resources":[{"attributeId":"servicePath","matchValue":"/opendata/**","matchFunction":"glob"}],"actions":[{"attributeId":"method","matchValue":"GET"}]},"rules":[{"effect":"Permit"}]}'`
|
|
3681
|
+
},
|
|
3382
3682
|
{
|
|
3383
3683
|
description: "Create from a JSON file",
|
|
3384
3684
|
command: "geonic admin policies create @policy.json"
|
|
@@ -3505,7 +3805,7 @@ function registerOAuthClientsCommand(parent) {
|
|
|
3505
3805
|
}
|
|
3506
3806
|
]);
|
|
3507
3807
|
const create = oauthClients.command("create [json]").description(
|
|
3508
|
-
'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 }'
|
|
3509
3809
|
).action(
|
|
3510
3810
|
withErrorHandler(async (json, _opts, cmd) => {
|
|
3511
3811
|
const body = await parseJsonInput(json);
|
|
@@ -3521,7 +3821,7 @@ function registerOAuthClientsCommand(parent) {
|
|
|
3521
3821
|
addExamples(create, [
|
|
3522
3822
|
{
|
|
3523
3823
|
description: "Create with inline JSON",
|
|
3524
|
-
command: `geonic admin oauth-clients create '{"
|
|
3824
|
+
command: `geonic admin oauth-clients create '{"name":"my-app","policyId":"<policy-id>"}'`
|
|
3525
3825
|
},
|
|
3526
3826
|
{
|
|
3527
3827
|
description: "Create from a JSON file",
|
|
@@ -3660,9 +3960,8 @@ function validateOrigins(body, opts) {
|
|
|
3660
3960
|
function buildBodyFromFlags(opts) {
|
|
3661
3961
|
const payload = {};
|
|
3662
3962
|
if (opts.name) payload.name = opts.name;
|
|
3663
|
-
if (opts.
|
|
3963
|
+
if (opts.policy) payload.policyId = opts.policy;
|
|
3664
3964
|
if (opts.origins) payload.allowedOrigins = opts.origins.split(",").map((s) => s.trim()).filter(Boolean);
|
|
3665
|
-
if (opts.entityTypes) payload.allowedEntityTypes = opts.entityTypes.split(",").map((s) => s.trim()).filter(Boolean);
|
|
3666
3965
|
if (opts.rateLimit) {
|
|
3667
3966
|
const raw = String(opts.rateLimit).trim();
|
|
3668
3967
|
if (!/^\d+$/.test(raw)) {
|
|
@@ -3722,14 +4021,14 @@ function registerApiKeysCommand(parent) {
|
|
|
3722
4021
|
command: "geonic admin api-keys get <key-id>"
|
|
3723
4022
|
}
|
|
3724
4023
|
]);
|
|
3725
|
-
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(
|
|
3726
4025
|
withErrorHandler(async (json, _opts, cmd) => {
|
|
3727
4026
|
const opts = cmd.opts();
|
|
3728
4027
|
validateOrigins(void 0, opts);
|
|
3729
4028
|
let body;
|
|
3730
4029
|
if (json) {
|
|
3731
4030
|
body = await parseJsonInput(json);
|
|
3732
|
-
} else if (opts.name || opts.
|
|
4031
|
+
} else if (opts.name || opts.policy || opts.origins || opts.rateLimit || opts.dpopRequired !== void 0 || opts.tenantId) {
|
|
3733
4032
|
body = buildBodyFromFlags(opts);
|
|
3734
4033
|
} else {
|
|
3735
4034
|
body = await parseJsonInput();
|
|
@@ -3761,11 +4060,14 @@ function registerApiKeysCommand(parent) {
|
|
|
3761
4060
|
console.error("API key created.");
|
|
3762
4061
|
})
|
|
3763
4062
|
);
|
|
3764
|
-
addNotes(create,
|
|
4063
|
+
addNotes(create, [
|
|
4064
|
+
"Use --policy to attach an existing XACML policy to the API key.",
|
|
4065
|
+
"Manage policies with `geonic admin policies` commands."
|
|
4066
|
+
]);
|
|
3765
4067
|
addExamples(create, [
|
|
3766
4068
|
{
|
|
3767
|
-
description: "Create an API key with
|
|
3768
|
-
command: "geonic admin api-keys create --name my-key --
|
|
4069
|
+
description: "Create an API key with a policy",
|
|
4070
|
+
command: "geonic admin api-keys create --name my-key --policy <policy-id> --origins '*'"
|
|
3769
4071
|
},
|
|
3770
4072
|
{
|
|
3771
4073
|
description: "Create an API key with DPoP required",
|
|
@@ -3776,7 +4078,7 @@ function registerApiKeysCommand(parent) {
|
|
|
3776
4078
|
command: "geonic admin api-keys create @key.json --save"
|
|
3777
4079
|
}
|
|
3778
4080
|
]);
|
|
3779
|
-
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(
|
|
3780
4082
|
withErrorHandler(
|
|
3781
4083
|
async (keyId, json, _opts, cmd) => {
|
|
3782
4084
|
const opts = cmd.opts();
|
|
@@ -3784,7 +4086,7 @@ function registerApiKeysCommand(parent) {
|
|
|
3784
4086
|
let body;
|
|
3785
4087
|
if (json) {
|
|
3786
4088
|
body = await parseJsonInput(json);
|
|
3787
|
-
} else if (opts.name || opts.
|
|
4089
|
+
} else if (opts.name || opts.policy || opts.origins || opts.rateLimit || opts.dpopRequired !== void 0) {
|
|
3788
4090
|
body = buildBodyFromFlags(opts);
|
|
3789
4091
|
} else {
|
|
3790
4092
|
body = await parseJsonInput();
|
|
@@ -3802,12 +4104,19 @@ function registerApiKeysCommand(parent) {
|
|
|
3802
4104
|
}
|
|
3803
4105
|
)
|
|
3804
4106
|
);
|
|
3805
|
-
addNotes(update,
|
|
4107
|
+
addNotes(update, [
|
|
4108
|
+
"Use --policy to attach an existing XACML policy to the API key.",
|
|
4109
|
+
"Manage policies with `geonic admin policies` commands."
|
|
4110
|
+
]);
|
|
3806
4111
|
addExamples(update, [
|
|
3807
4112
|
{
|
|
3808
4113
|
description: "Update an API key name",
|
|
3809
4114
|
command: "geonic admin api-keys update <key-id> --name new-name"
|
|
3810
4115
|
},
|
|
4116
|
+
{
|
|
4117
|
+
description: "Attach a policy",
|
|
4118
|
+
command: "geonic admin api-keys update <key-id> --policy <policy-id>"
|
|
4119
|
+
},
|
|
3811
4120
|
{
|
|
3812
4121
|
description: "Enable DPoP requirement",
|
|
3813
4122
|
command: "geonic admin api-keys update <key-id> --dpop-required"
|