@geolonia/geonicdb-cli 0.6.3 → 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,8 +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
  ];
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
+ }
950
965
  function resolveOptions(cmd) {
951
966
  const opts = cmd.optsWithGlobals();
952
967
  const config = loadConfig(opts.profile);
@@ -1421,7 +1436,7 @@ function addMeApiKeysSubcommand(me) {
1421
1436
  command: "geonic me api-keys list"
1422
1437
  }
1423
1438
  ]);
1424
- 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(
1425
1440
  withErrorHandler(async (json, _opts, cmd) => {
1426
1441
  const opts = cmd.opts();
1427
1442
  if (opts.origins !== void 0) {
@@ -1434,13 +1449,14 @@ function addMeApiKeysSubcommand(me) {
1434
1449
  let body;
1435
1450
  if (json) {
1436
1451
  body = await parseJsonInput(json);
1437
- } 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) {
1438
1453
  const payload = {};
1439
1454
  if (opts.name) payload.name = opts.name;
1440
1455
  if (opts.scopes) payload.allowedScopes = opts.scopes.split(",").map((s) => s.trim()).filter(Boolean);
1441
1456
  if (opts.origins) payload.allowedOrigins = opts.origins.split(",").map((s) => s.trim()).filter(Boolean);
1442
1457
  if (opts.entityTypes) payload.allowedEntityTypes = opts.entityTypes.split(",").map((s) => s.trim()).filter(Boolean);
1443
1458
  if (opts.dpopRequired !== void 0) payload.dpopRequired = opts.dpopRequired;
1459
+ if (opts.permissions) payload.permissions = parsePermissions(opts.permissions);
1444
1460
  if (opts.rateLimit) {
1445
1461
  const raw = opts.rateLimit.trim();
1446
1462
  if (!/^\d+$/.test(raw)) {
@@ -1489,12 +1505,22 @@ function addMeApiKeysSubcommand(me) {
1489
1505
  console.error("API key created.");
1490
1506
  })
1491
1507
  );
1492
- addNotes(create, 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
+ ]);
1493
1515
  addExamples(create, [
1494
1516
  {
1495
1517
  description: "Create an API key with flags",
1496
1518
  command: "geonic me api-keys create --name my-app --scopes read:entities --origins 'https://example.com'"
1497
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
+ },
1498
1524
  {
1499
1525
  description: "Create and save API key to config",
1500
1526
  command: "geonic me api-keys create --name my-app --save"
@@ -3196,7 +3222,7 @@ function registerUsersCommand(parent) {
3196
3222
  }
3197
3223
  ]);
3198
3224
  const create = users.command("create [json]").description(
3199
- '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.'
3200
3226
  ).action(
3201
3227
  withErrorHandler(async (json, _opts, cmd) => {
3202
3228
  const body = await parseJsonInput(json);
@@ -3211,8 +3237,12 @@ function registerUsersCommand(parent) {
3211
3237
  );
3212
3238
  addExamples(create, [
3213
3239
  {
3214
- description: "Create with inline JSON",
3215
- 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>"}'`
3216
3246
  },
3217
3247
  {
3218
3248
  description: "Create from a JSON file",
@@ -3356,7 +3386,7 @@ function registerPoliciesCommand(parent) {
3356
3386
  }
3357
3387
  ]);
3358
3388
  const create = policies.command("create [json]").description(
3359
- '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'
3360
3390
  ).action(
3361
3391
  withErrorHandler(async (json, _opts, cmd) => {
3362
3392
  const body = await parseJsonInput(json);
@@ -3374,6 +3404,18 @@ function registerPoliciesCommand(parent) {
3374
3404
  description: "Create with inline JSON",
3375
3405
  command: `geonic admin policies create '{"description":"Allow all entities","rules":[{"ruleId":"allow-all","effect":"Permit"}]}'`
3376
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
+ },
3377
3419
  {
3378
3420
  description: "Create from a JSON file",
3379
3421
  command: "geonic admin policies create @policy.json"
@@ -3672,6 +3714,7 @@ function buildBodyFromFlags(opts) {
3672
3714
  payload.rateLimit = { perMinute };
3673
3715
  }
3674
3716
  if (opts.dpopRequired !== void 0) payload.dpopRequired = opts.dpopRequired;
3717
+ if (opts.permissions) payload.permissions = parsePermissions(opts.permissions);
3675
3718
  if (opts.tenantId) payload.tenantId = opts.tenantId;
3676
3719
  return payload;
3677
3720
  }
@@ -3717,14 +3760,14 @@ function registerApiKeysCommand(parent) {
3717
3760
  command: "geonic admin api-keys get <key-id>"
3718
3761
  }
3719
3762
  ]);
3720
- 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(
3721
3764
  withErrorHandler(async (json, _opts, cmd) => {
3722
3765
  const opts = cmd.opts();
3723
3766
  validateOrigins(void 0, opts);
3724
3767
  let body;
3725
3768
  if (json) {
3726
3769
  body = await parseJsonInput(json);
3727
- } 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) {
3728
3771
  body = buildBodyFromFlags(opts);
3729
3772
  } else {
3730
3773
  body = await parseJsonInput();
@@ -3756,12 +3799,22 @@ function registerApiKeysCommand(parent) {
3756
3799
  console.error("API key created.");
3757
3800
  })
3758
3801
  );
3759
- addNotes(create, 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
+ ]);
3760
3809
  addExamples(create, [
3761
3810
  {
3762
3811
  description: "Create an API key with flags",
3763
3812
  command: "geonic admin api-keys create --name my-key --scopes read:entities,write:entities --origins '*'"
3764
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
+ },
3765
3818
  {
3766
3819
  description: "Create an API key with DPoP required",
3767
3820
  command: "geonic admin api-keys create --name my-key --dpop-required"
@@ -3771,7 +3824,7 @@ function registerApiKeysCommand(parent) {
3771
3824
  command: "geonic admin api-keys create @key.json --save"
3772
3825
  }
3773
3826
  ]);
3774
- 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(
3775
3828
  withErrorHandler(
3776
3829
  async (keyId, json, _opts, cmd) => {
3777
3830
  const opts = cmd.opts();
@@ -3779,7 +3832,7 @@ function registerApiKeysCommand(parent) {
3779
3832
  let body;
3780
3833
  if (json) {
3781
3834
  body = await parseJsonInput(json);
3782
- } 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) {
3783
3836
  body = buildBodyFromFlags(opts);
3784
3837
  } else {
3785
3838
  body = await parseJsonInput();
@@ -3797,12 +3850,22 @@ function registerApiKeysCommand(parent) {
3797
3850
  }
3798
3851
  )
3799
3852
  );
3800
- addNotes(update, 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
+ ]);
3801
3860
  addExamples(update, [
3802
3861
  {
3803
3862
  description: "Update an API key name",
3804
3863
  command: "geonic admin api-keys update <key-id> --name new-name"
3805
3864
  },
3865
+ {
3866
+ description: "Update permissions",
3867
+ command: "geonic admin api-keys update <key-id> --permissions read,write"
3868
+ },
3806
3869
  {
3807
3870
  description: "Enable DPoP requirement",
3808
3871
  command: "geonic admin api-keys update <key-id> --dpop-required"