@geolonia/geonicdb-cli 0.6.4 → 0.7.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 CHANGED
@@ -213,12 +213,16 @@ Displays the current authenticated user, token expiry, and active profile.
213
213
  | `--entity-types <types>` | Allowed entity types (comma-separated) |
214
214
  | `--rate-limit <n>` | Rate limit (requests per minute) |
215
215
  | `--dpop-required` | Require DPoP token binding (RFC 9449) |
216
+ | `--permissions <perms>` | Permissions for auto-generated XACML policy (comma-separated) |
216
217
  | `--save` | Save the API key to profile config |
217
218
 
218
219
  ```bash
219
220
  # Create an API key and save to config
220
221
  geonic me api-keys create --name my-app --scopes read:entities --save
221
222
 
223
+ # Create with permissions (auto-generates XACML policy)
224
+ geonic me api-keys create --name my-app --permissions read,write --save
225
+
222
226
  # Create from JSON
223
227
  geonic me api-keys create '{"name":"my-app","allowedScopes":["read:entities"]}'
224
228
  ```
@@ -392,6 +396,16 @@ Temporal entityOperations query supports: `--aggr-methods`, `--aggr-period`.
392
396
  | `admin policies activate <id>` | Activate a policy |
393
397
  | `admin policies deactivate <id>` | Deactivate a policy |
394
398
 
399
+ **XACML Authorization Model**: All authorization is unified under XACML policies. Default role policies (priority 0):
400
+
401
+ | Role | Default Behavior |
402
+ |---|---|
403
+ | `user` | GET only (read-only) |
404
+ | `api_key` | All Deny |
405
+ | `anonymous` | All Deny |
406
+
407
+ Custom policies with higher priority (e.g. 100) override defaults. Target resource attributes include: `path`, `entityType`, `entityId`, `entityOwner`, `tenantService`, `servicePath`. The `servicePath` attribute supports glob patterns (e.g. `/opendata/**`) and regex matching.
408
+
395
409
  #### admin oauth-clients
396
410
 
397
411
  | Subcommand | Description |
@@ -412,7 +426,9 @@ Temporal entityOperations query supports: `--aggr-methods`, `--aggr-period`.
412
426
  | `admin api-keys update <keyId> [json]` | Update an API key |
413
427
  | `admin api-keys delete <keyId>` | Delete an API key |
414
428
 
415
- `admin api-keys list` supports `--tenant-id` to filter by tenant. `admin api-keys create` supports flag options: `--name`, `--scopes`, `--origins`, `--entity-types`, `--rate-limit`, `--dpop-required`, `--tenant-id`, `--save`. `admin api-keys update` supports `--name`, `--scopes`, `--origins`, `--entity-types`, `--rate-limit`, `--dpop-required` / `--no-dpop-required`.
429
+ `admin api-keys list` supports `--tenant-id` to filter by tenant. `admin api-keys create` supports flag options: `--name`, `--scopes`, `--origins`, `--entity-types`, `--rate-limit`, `--dpop-required`, `--permissions`, `--tenant-id`, `--save`. `admin api-keys update` supports `--name`, `--scopes`, `--origins`, `--entity-types`, `--rate-limit`, `--dpop-required` / `--no-dpop-required`, `--permissions`.
430
+
431
+ **Permissions**: The `--permissions` flag accepts a comma-separated list of `read`, `write`, `create`, `update`, `delete`. `write` is an alias for `create` + `update` + `delete`. When specified, XACML policies are auto-generated for the API key (respects `allowedEntityTypes`).
416
432
 
417
433
  **Note**: `allowedOrigins` must contain at least 1 item when specified. Use `*` to allow all origins. `allowedEntityTypes` is enforced at runtime — API key holders can only access entities of the specified types. `admin api-keys list` / `admin api-keys get` output includes a `dpopRequired` field (boolean).
418
434
 
@@ -560,13 +576,13 @@ geonic entities list --api-key gdb_your_api_key_here
560
576
  GDB_API_KEY=gdb_your_api_key_here geonic entities list
561
577
  ```
562
578
 
563
- When both a Bearer token and an API key are configured, both headers are sent (the server determines precedence).
579
+ When both a Bearer token and an API key are configured, headers are sent exclusively — the API key takes precedence when present.
564
580
 
565
581
  ### Valid Scopes
566
582
 
567
583
  `read:entities`, `write:entities`, `read:subscriptions`, `write:subscriptions`, `read:registrations`, `write:registrations`, `read:rules`, `write:rules`, `read:custom-data-models`, `write:custom-data-models`, `admin:users`, `admin:tenants`, `admin:policies`, `admin:oauth-clients`, `admin:api-keys`, `admin:metrics`
568
584
 
569
- `write:X` implies `read:X`. `admin:X` implies both `read:X` and `write:X`.
585
+ `admin:X` implies both `read:X` and `write:X`. `write:X` does **not** imply `read:X` specify both explicitly if needed.
570
586
 
571
587
  Special scopes: `permanent` (no token expiry), `jwt` (JWT format token).
572
588
 
package/dist/index.js CHANGED
@@ -945,13 +945,23 @@ var SCOPES_HELP_NOTES = [
945
945
  " admin:users, admin:tenants, admin:policies, admin:oauth-clients,",
946
946
  " admin:api-keys, admin:metrics",
947
947
  "",
948
- "write:X implies read:X. admin:X implies both read:X and write:X."
948
+ "admin:X implies both read:X and write:X.",
949
+ "write:X does NOT imply read:X \u2014 specify both if needed."
949
950
  ];
950
951
  var API_KEY_SCOPES_HELP_NOTES = [
951
952
  "Valid scopes:",
952
953
  " read:entities, write:entities, read:subscriptions, write:subscriptions,",
953
954
  " read:registrations, write:registrations"
954
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
+ }
955
965
  function resolveOptions(cmd) {
956
966
  const opts = cmd.optsWithGlobals();
957
967
  const config = loadConfig(opts.profile);
@@ -1426,7 +1436,7 @@ function addMeApiKeysSubcommand(me) {
1426
1436
  command: "geonic me api-keys list"
1427
1437
  }
1428
1438
  ]);
1429
- const create = apiKeys.command("create [json]").description("Create a new API key").option("--name <name>", "Key name").option("--scopes <scopes>", "Allowed scopes (comma-separated)").option("--origins <origins>", "Allowed origins (comma-separated)").option("--entity-types <types>", "Allowed entity types (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(
1439
+ const create = apiKeys.command("create [json]").description("Create a new API key").option("--name <name>", "Key name").option("--scopes <scopes>", "Allowed scopes (comma-separated)").option("--origins <origins>", "Allowed origins (comma-separated)").option("--entity-types <types>", "Allowed entity types (comma-separated)").option("--rate-limit <n>", "Rate limit per minute").option("--dpop-required", "Require DPoP token binding").option("--permissions <perms>", "Comma-separated permissions (read, write, create, update, delete)").option("--save", "Save the API key to config for automatic use").action(
1430
1440
  withErrorHandler(async (json, _opts, cmd) => {
1431
1441
  const opts = cmd.opts();
1432
1442
  if (opts.origins !== void 0) {
@@ -1439,13 +1449,14 @@ function addMeApiKeysSubcommand(me) {
1439
1449
  let body;
1440
1450
  if (json) {
1441
1451
  body = await parseJsonInput(json);
1442
- } else if (opts.name || opts.scopes || opts.origins || opts.entityTypes || opts.rateLimit || opts.dpopRequired !== void 0) {
1452
+ } else if (opts.name || opts.scopes || opts.origins || opts.entityTypes || opts.rateLimit || opts.dpopRequired !== void 0 || opts.permissions) {
1443
1453
  const payload = {};
1444
1454
  if (opts.name) payload.name = opts.name;
1445
1455
  if (opts.scopes) payload.allowedScopes = opts.scopes.split(",").map((s) => s.trim()).filter(Boolean);
1446
1456
  if (opts.origins) payload.allowedOrigins = opts.origins.split(",").map((s) => s.trim()).filter(Boolean);
1447
1457
  if (opts.entityTypes) payload.allowedEntityTypes = opts.entityTypes.split(",").map((s) => s.trim()).filter(Boolean);
1448
1458
  if (opts.dpopRequired !== void 0) payload.dpopRequired = opts.dpopRequired;
1459
+ if (opts.permissions) payload.permissions = parsePermissions(opts.permissions);
1449
1460
  if (opts.rateLimit) {
1450
1461
  const raw = opts.rateLimit.trim();
1451
1462
  if (!/^\d+$/.test(raw)) {
@@ -1494,12 +1505,22 @@ function addMeApiKeysSubcommand(me) {
1494
1505
  console.error("API key created.");
1495
1506
  })
1496
1507
  );
1497
- addNotes(create, API_KEY_SCOPES_HELP_NOTES);
1508
+ addNotes(create, [
1509
+ ...API_KEY_SCOPES_HELP_NOTES,
1510
+ "",
1511
+ "Valid permissions: read, write, create, update, delete",
1512
+ " write = create + update + delete",
1513
+ " Permissions auto-generate XACML policies (allowedEntityTypes respected)."
1514
+ ]);
1498
1515
  addExamples(create, [
1499
1516
  {
1500
1517
  description: "Create an API key with flags",
1501
1518
  command: "geonic me api-keys create --name my-app --scopes read:entities --origins 'https://example.com'"
1502
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"
1523
+ },
1503
1524
  {
1504
1525
  description: "Create and save API key to config",
1505
1526
  command: "geonic me api-keys create --name my-app --save"
@@ -3201,7 +3222,7 @@ function registerUsersCommand(parent) {
3201
3222
  }
3202
3223
  ]);
3203
3224
  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": "super_admin"\n }'
3225
+ '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
3226
  ).action(
3206
3227
  withErrorHandler(async (json, _opts, cmd) => {
3207
3228
  const body = await parseJsonInput(json);
@@ -3216,8 +3237,12 @@ function registerUsersCommand(parent) {
3216
3237
  );
3217
3238
  addExamples(create, [
3218
3239
  {
3219
- description: "Create with inline JSON",
3220
- command: `geonic admin users create '{"email":"user@example.com","password":"SecurePassword123!","role":"super_admin"}'`
3240
+ description: "Create a tenant admin",
3241
+ command: `geonic admin users create '{"email":"admin@example.com","password":"SecurePass12345!","role":"tenant_admin","tenantId":"<tenant-id>"}'`
3242
+ },
3243
+ {
3244
+ description: "Create a user for a tenant",
3245
+ command: `geonic admin users create '{"email":"user@example.com","password":"SecurePass12345!","role":"user","tenantId":"<tenant-id>"}'`
3221
3246
  },
3222
3247
  {
3223
3248
  description: "Create from a JSON file",
@@ -3361,7 +3386,7 @@ function registerPoliciesCommand(parent) {
3361
3386
  }
3362
3387
  ]);
3363
3388
  const create = policies.command("create [json]").description(
3364
- 'Create a new policy\n\nJSON payload example:\n {\n "description": "Allow all entities",\n "rules": [{"ruleId": "allow-all", "effect": "Permit"}]\n }'
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: higher number = higher priority (default: 0).\n Custom policies (e.g. 100) override default role policies (0).\n\nDefault role policies (priority 0):\n user \u2192 GET only, api_key \u2192 all Deny, anonymous \u2192 all Deny'
3365
3390
  ).action(
3366
3391
  withErrorHandler(async (json, _opts, cmd) => {
3367
3392
  const body = await parseJsonInput(json);
@@ -3379,6 +3404,18 @@ function registerPoliciesCommand(parent) {
3379
3404
  description: "Create with inline JSON",
3380
3405
  command: `geonic admin policies create '{"description":"Allow all entities","rules":[{"ruleId":"allow-all","effect":"Permit"}]}'`
3381
3406
  },
3407
+ {
3408
+ description: "Create with target (entity type + method)",
3409
+ 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"}]}'`
3410
+ },
3411
+ {
3412
+ description: "Create anonymous access policy",
3413
+ 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"}]}'`
3414
+ },
3415
+ {
3416
+ description: "Create servicePath-based policy (glob match)",
3417
+ 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"}]}'`
3418
+ },
3382
3419
  {
3383
3420
  description: "Create from a JSON file",
3384
3421
  command: "geonic admin policies create @policy.json"
@@ -3677,6 +3714,7 @@ function buildBodyFromFlags(opts) {
3677
3714
  payload.rateLimit = { perMinute };
3678
3715
  }
3679
3716
  if (opts.dpopRequired !== void 0) payload.dpopRequired = opts.dpopRequired;
3717
+ if (opts.permissions) payload.permissions = parsePermissions(opts.permissions);
3680
3718
  if (opts.tenantId) payload.tenantId = opts.tenantId;
3681
3719
  return payload;
3682
3720
  }
@@ -3722,14 +3760,14 @@ function registerApiKeysCommand(parent) {
3722
3760
  command: "geonic admin api-keys get <key-id>"
3723
3761
  }
3724
3762
  ]);
3725
- const create = apiKeys.command("create [json]").description("Create a new API key").option("--name <name>", "Key name").option("--scopes <scopes>", "Comma-separated scopes").option("--origins <origins>", "Comma-separated origins").option("--entity-types <types>", "Comma-separated entity types").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(
3763
+ const create = apiKeys.command("create [json]").description("Create a new API key").option("--name <name>", "Key name").option("--scopes <scopes>", "Comma-separated scopes").option("--origins <origins>", "Comma-separated origins").option("--entity-types <types>", "Comma-separated entity types").option("--rate-limit <n>", "Rate limit per minute").option("--dpop-required", "Require DPoP token binding").option("--permissions <perms>", "Comma-separated permissions (read, write, create, update, delete)").option("--tenant-id <id>", "Tenant ID").option("--save", "Save the API key to profile config").action(
3726
3764
  withErrorHandler(async (json, _opts, cmd) => {
3727
3765
  const opts = cmd.opts();
3728
3766
  validateOrigins(void 0, opts);
3729
3767
  let body;
3730
3768
  if (json) {
3731
3769
  body = await parseJsonInput(json);
3732
- } else if (opts.name || opts.scopes || opts.origins || opts.entityTypes || opts.rateLimit || opts.dpopRequired !== void 0 || opts.tenantId) {
3770
+ } else if (opts.name || opts.scopes || opts.origins || opts.entityTypes || opts.rateLimit || opts.dpopRequired !== void 0 || opts.permissions || opts.tenantId) {
3733
3771
  body = buildBodyFromFlags(opts);
3734
3772
  } else {
3735
3773
  body = await parseJsonInput();
@@ -3761,12 +3799,22 @@ function registerApiKeysCommand(parent) {
3761
3799
  console.error("API key created.");
3762
3800
  })
3763
3801
  );
3764
- addNotes(create, API_KEY_SCOPES_HELP_NOTES);
3802
+ addNotes(create, [
3803
+ ...API_KEY_SCOPES_HELP_NOTES,
3804
+ "",
3805
+ "Valid permissions: read, write, create, update, delete",
3806
+ " write = create + update + delete",
3807
+ " Permissions auto-generate XACML policies (allowedEntityTypes respected)."
3808
+ ]);
3765
3809
  addExamples(create, [
3766
3810
  {
3767
3811
  description: "Create an API key with flags",
3768
3812
  command: "geonic admin api-keys create --name my-key --scopes read:entities,write:entities --origins '*'"
3769
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 '*'"
3817
+ },
3770
3818
  {
3771
3819
  description: "Create an API key with DPoP required",
3772
3820
  command: "geonic admin api-keys create --name my-key --dpop-required"
@@ -3776,7 +3824,7 @@ function registerApiKeysCommand(parent) {
3776
3824
  command: "geonic admin api-keys create @key.json --save"
3777
3825
  }
3778
3826
  ]);
3779
- const update = apiKeys.command("update <keyId> [json]").description("Update an API key").option("--name <name>", "Key name").option("--scopes <scopes>", "Comma-separated scopes").option("--origins <origins>", "Comma-separated origins").option("--entity-types <types>", "Comma-separated entity types").option("--rate-limit <n>", "Rate limit per minute").option("--dpop-required", "Require DPoP token binding").option("--no-dpop-required", "Disable DPoP token binding").action(
3827
+ const update = apiKeys.command("update <keyId> [json]").description("Update an API key").option("--name <name>", "Key name").option("--scopes <scopes>", "Comma-separated scopes").option("--origins <origins>", "Comma-separated origins").option("--entity-types <types>", "Comma-separated entity types").option("--rate-limit <n>", "Rate limit per minute").option("--dpop-required", "Require DPoP token binding").option("--no-dpop-required", "Disable DPoP token binding").option("--permissions <perms>", "Comma-separated permissions (read, write, create, update, delete)").action(
3780
3828
  withErrorHandler(
3781
3829
  async (keyId, json, _opts, cmd) => {
3782
3830
  const opts = cmd.opts();
@@ -3784,7 +3832,7 @@ function registerApiKeysCommand(parent) {
3784
3832
  let body;
3785
3833
  if (json) {
3786
3834
  body = await parseJsonInput(json);
3787
- } else if (opts.name || opts.scopes || opts.origins || opts.entityTypes || opts.rateLimit || opts.dpopRequired !== void 0) {
3835
+ } else if (opts.name || opts.scopes || opts.origins || opts.entityTypes || opts.rateLimit || opts.dpopRequired !== void 0 || opts.permissions) {
3788
3836
  body = buildBodyFromFlags(opts);
3789
3837
  } else {
3790
3838
  body = await parseJsonInput();
@@ -3802,12 +3850,22 @@ function registerApiKeysCommand(parent) {
3802
3850
  }
3803
3851
  )
3804
3852
  );
3805
- addNotes(update, API_KEY_SCOPES_HELP_NOTES);
3853
+ addNotes(update, [
3854
+ ...API_KEY_SCOPES_HELP_NOTES,
3855
+ "",
3856
+ "Valid permissions: read, write, create, update, delete",
3857
+ " write = create + update + delete",
3858
+ " Permissions auto-generate XACML policies (allowedEntityTypes respected)."
3859
+ ]);
3806
3860
  addExamples(update, [
3807
3861
  {
3808
3862
  description: "Update an API key name",
3809
3863
  command: "geonic admin api-keys update <key-id> --name new-name"
3810
3864
  },
3865
+ {
3866
+ description: "Update permissions",
3867
+ command: "geonic admin api-keys update <key-id> --permissions read,write"
3868
+ },
3811
3869
  {
3812
3870
  description: "Enable DPoP requirement",
3813
3871
  command: "geonic admin api-keys update <key-id> --dpop-required"