Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,19 @@

## [Unreleased]

## [0.7.0] - 2026-03-20

### 2026-03-20
- **Feat**: `admin api-keys create/update` および `me api-keys create` に `--permissions` オプションを追加 — `read`, `write`, `create`, `update`, `delete` を指定して XACML ポリシーを自動生成 (GeonicDB #753) (#79)
- **Breaking**: `admin tenants update` から `--anonymous-access` / `--no-anonymous-access` フラグを削除 — テナントフィーチャーフラグが廃止され、匿名アクセスは XACML ポリシーで制御 (GeonicDB #752) (#79)
- **Breaking**: スコープヘルプから `write:X implies read:X` の記述を削除 — `write:X` は `read:X` を暗黙的に含まなくなった (GeonicDB #723) (#79)
- **Docs**: `admin policies create` のヘルプに `servicePath` リソース属性、priority の説明(大きい数値=高優先度)、デフォルトロールポリシーの説明を追加 (GeonicDB #747, #751, #752) (#79)
- **Docs**: README を更新 — `--permissions` オプション、XACML 認可モデル、スコープ包含関係、API キーヘッダ排他仕様を反映 (#79)

### 2026-03-19
- **Feat**: `admin tenants update` に `--anonymous-access` / `--no-anonymous-access` オプションを追加 — テナントの匿名アクセス設定を CLI から管理可能に (#76)
- **Docs**: `admin policies create` のヘルプに `target` フィールド(subjects, resources, actions, environments)のスキーマ例を追加 (#75)
- **Feat**: `admin tenants update` に `--anonymous-access` / `--no-anonymous-access` オプションを追加 — テナントの匿名アクセス設定を CLI から管理可能に (#78)
- **Docs**: `admin policies create` のヘルプに `target` フィールド(subjects, resources, actions, environments)のスキーマ例を追加 (#78)
- **Docs**: `admin users create` のヘルプを API 仕様に合わせて改善 (#77)

### 2026-03-17
- **Fix**: API キーコマンド (`me api-keys`, `admin api-keys`) のヘルプで表示されるスコープ一覧を、API キーで実際に使用可能な6スコープに修正 (#74)
Expand Down Expand Up @@ -133,7 +143,9 @@
### 2026-02-26
- **Docs**: README にインストール手順・使い方・コマンドリファレンスを追加 (#1)

[Unreleased]: https://github.com/geolonia/geonicdb-cli/compare/v0.6.3...HEAD
[Unreleased]: https://github.com/geolonia/geonicdb-cli/compare/v0.7.0...HEAD
[0.7.0]: https://github.com/geolonia/geonicdb-cli/compare/v0.6.4...v0.7.0
[0.6.4]: https://github.com/geolonia/geonicdb-cli/compare/v0.6.3...v0.6.4
Comment on lines +146 to +148

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: v0.6.4 タグの存在を確認

# v0.6.4 タグが存在するか確認
if git tag | grep -q "^v0.6.4$"; then
  echo "✓ v0.6.4 タグが存在します"
  git log --oneline v0.6.3..v0.6.4
else
  echo "✗ v0.6.4 タグは存在しません"
fi

# 最新のタグを表示
echo ""
echo "最新のバージョンタグ:"
git tag --sort=-version:refname | head -n 5

Repository: geolonia/geonicdb-cli

Length of output: 96


🏁 Script executed:

# Check the current state of CHANGELOG.md lines around 146-148
wc -l CHANGELOG.md

Repository: geolonia/geonicdb-cli

Length of output: 81


🏁 Script executed:

# Read the end of CHANGELOG.md to see the link definitions
tail -20 CHANGELOG.md

Repository: geolonia/geonicdb-cli

Length of output: 1222


🏁 Script executed:

# Search for [0.6.4] section in the entire CHANGELOG.md
grep -n "\[0\.6\.4\]" CHANGELOG.md

Repository: geolonia/geonicdb-cli

Length of output: 142


** версия 0.6.4 のリンク定義が無効です。v0.6.4 タグが存在しないため削除してください。**

Git リポジトリに v0.6.4 タグが存在しません。以下の対応が必要です:

  • 147 行目:[0.7.0] の比較リンクを v0.6.4...v0.7.0 から v0.6.3...v0.7.0 に修正
  • 148 行目:[0.6.4] のリンク定義を削除
修正内容(CHANGELOG.md 147-148 行目)
[Unreleased]: https://github.com/geolonia/geonicdb-cli/compare/v0.7.0...HEAD
-[0.7.0]: https://github.com/geolonia/geonicdb-cli/compare/v0.6.4...v0.7.0
-[0.6.4]: https://github.com/geolonia/geonicdb-cli/compare/v0.6.3...v0.6.4
+[0.7.0]: https://github.com/geolonia/geonicdb-cli/compare/v0.6.3...v0.7.0
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
[Unreleased]: https://github.com/geolonia/geonicdb-cli/compare/v0.7.0...HEAD
[0.7.0]: https://github.com/geolonia/geonicdb-cli/compare/v0.6.4...v0.7.0
[0.6.4]: https://github.com/geolonia/geonicdb-cli/compare/v0.6.3...v0.6.4
[Unreleased]: https://github.com/geolonia/geonicdb-cli/compare/v0.7.0...HEAD
[0.7.0]: https://github.com/geolonia/geonicdb-cli/compare/v0.6.3...v0.7.0
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 148-148: Link and image reference definitions should be needed
Unused link or image reference definition: "0.6.4"

(MD053, link-image-reference-definitions)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CHANGELOG.md` around lines 146 - 148, Update the changelog links: change the
compare range referenced by the link label [0.7.0] from v0.6.4...v0.7.0 to
v0.6.3...v0.7.0 (adjust the URL/compare range accordingly) and remove the
unused/invalid link label [0.6.4] entirely so there is no reference to a
non-existent v0.6.4 tag.

[0.6.3]: https://github.com/geolonia/geonicdb-cli/compare/v0.6.2...v0.6.3
[0.6.2]: https://github.com/geolonia/geonicdb-cli/compare/v0.6.1...v0.6.2
[0.6.1]: https://github.com/geolonia/geonicdb-cli/compare/v0.6.0...v0.6.1
Expand Down
22 changes: 19 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,12 +213,16 @@ Displays the current authenticated user, token expiry, and active profile.
| `--entity-types <types>` | Allowed entity types (comma-separated) |
| `--rate-limit <n>` | Rate limit (requests per minute) |
| `--dpop-required` | Require DPoP token binding (RFC 9449) |
| `--permissions <perms>` | Permissions for auto-generated XACML policy (comma-separated) |
| `--save` | Save the API key to profile config |

```bash
# Create an API key and save to config
geonic me api-keys create --name my-app --scopes read:entities --save

# Create with permissions (auto-generates XACML policy)
geonic me api-keys create --name my-app --permissions read,write --save

# Create from JSON
geonic me api-keys create '{"name":"my-app","allowedScopes":["read:entities"]}'
```
Expand Down Expand Up @@ -392,6 +396,16 @@ Temporal entityOperations query supports: `--aggr-methods`, `--aggr-period`.
| `admin policies activate <id>` | Activate a policy |
| `admin policies deactivate <id>` | Deactivate a policy |

**XACML Authorization Model**: All authorization is unified under XACML policies. Default role policies (priority 0):

| Role | Default Behavior |
|---|---|
| `user` | GET only (read-only) |
| `api_key` | All Deny |
| `anonymous` | All Deny |

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.

#### admin oauth-clients

| Subcommand | Description |
Expand All @@ -412,7 +426,9 @@ Temporal entityOperations query supports: `--aggr-methods`, `--aggr-period`.
| `admin api-keys update <keyId> [json]` | Update an API key |
| `admin api-keys delete <keyId>` | Delete an API key |

`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`.
`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`.

**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`).

**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).

Expand Down Expand Up @@ -560,13 +576,13 @@ geonic entities list --api-key gdb_your_api_key_here
GDB_API_KEY=gdb_your_api_key_here geonic entities list
```

When both a Bearer token and an API key are configured, both headers are sent (the server determines precedence).
When both a Bearer token and an API key are configured, headers are sent exclusively — the API key takes precedence when present.

### Valid Scopes

`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`

`write:X` implies `read:X`. `admin:X` implies both `read:X` and `write:X`.
`admin:X` implies both `read:X` and `write:X`. `write:X` does **not** imply `read:X` — specify both explicitly if needed.

Special scopes: `permanent` (no token expiry), `jwt` (JWT format token).

Expand Down
35 changes: 30 additions & 5 deletions src/commands/admin/api-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { loadConfig, saveConfig } from "../../config.js";
import { parseJsonInput } from "../../input.js";
import { printError, printWarning } from "../../output.js";
import { addExamples, addNotes } from "../help.js";
import { API_KEY_SCOPES_HELP_NOTES } from "../../helpers.js";
import { API_KEY_SCOPES_HELP_NOTES, parsePermissions } from "../../helpers.js";

function validateOrigins(body: unknown, opts: Record<string, unknown>): void {
// Validate origins if provided via flags
Expand Down Expand Up @@ -45,6 +45,7 @@ function buildBodyFromFlags(opts: Record<string, unknown>): Record<string, unkno
payload.rateLimit = { perMinute };
}
if (opts.dpopRequired !== undefined) payload.dpopRequired = opts.dpopRequired;
if (opts.permissions) payload.permissions = parsePermissions(opts.permissions as string);
if (opts.tenantId) payload.tenantId = opts.tenantId;
return payload;
}
Expand Down Expand Up @@ -117,6 +118,7 @@ export function registerApiKeysCommand(parent: Command): void {
.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(
Expand All @@ -128,6 +130,7 @@ export function registerApiKeysCommand(parent: Command): void {
entityTypes?: string;
rateLimit?: string;
dpopRequired?: boolean;
permissions?: string;
tenantId?: string;
save?: boolean;
};
Expand All @@ -137,7 +140,7 @@ export function registerApiKeysCommand(parent: Command): void {
let body: unknown;
if (json) {
body = await parseJsonInput(json as string | undefined);
} else if (opts.name || opts.scopes || opts.origins || opts.entityTypes || opts.rateLimit || opts.dpopRequired !== undefined || opts.tenantId) {
} else if (opts.name || opts.scopes || opts.origins || opts.entityTypes || opts.rateLimit || opts.dpopRequired !== undefined || opts.permissions || opts.tenantId) {
body = buildBodyFromFlags(opts);
} else {
body = await parseJsonInput();
Expand Down Expand Up @@ -174,13 +177,23 @@ export function registerApiKeysCommand(parent: Command): void {
}),
);

addNotes(create, API_KEY_SCOPES_HELP_NOTES);
addNotes(create, [
...API_KEY_SCOPES_HELP_NOTES,
"",
"Valid permissions: read, write, create, update, delete",
" write = create + update + delete",
" Permissions auto-generate XACML policies (allowedEntityTypes respected).",
]);

addExamples(create, [
{
description: "Create an API key with flags",
command: "geonic admin api-keys create --name my-key --scopes read:entities,write:entities --origins '*'",
},
{
description: "Create with permissions (auto-generates XACML policy)",
command: "geonic admin api-keys create --name my-key --permissions read,write --origins '*'",
},
{
description: "Create an API key with DPoP required",
command: "geonic admin api-keys create --name my-key --dpop-required",
Expand All @@ -202,6 +215,7 @@ export function registerApiKeysCommand(parent: Command): void {
.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)")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
.action(
withErrorHandler(
async (keyId: unknown, json: unknown, _opts: unknown, cmd: Command) => {
Expand All @@ -212,14 +226,15 @@ export function registerApiKeysCommand(parent: Command): void {
entityTypes?: string;
rateLimit?: string;
dpopRequired?: boolean;
permissions?: string;
};

validateOrigins(undefined, opts);

let body: unknown;
if (json) {
body = await parseJsonInput(json as string | undefined);
} else if (opts.name || opts.scopes || opts.origins || opts.entityTypes || opts.rateLimit || opts.dpopRequired !== undefined) {
} else if (opts.name || opts.scopes || opts.origins || opts.entityTypes || opts.rateLimit || opts.dpopRequired !== undefined || opts.permissions) {
body = buildBodyFromFlags(opts);
} else {
body = await parseJsonInput();
Expand All @@ -240,13 +255,23 @@ export function registerApiKeysCommand(parent: Command): void {
),
);

addNotes(update, API_KEY_SCOPES_HELP_NOTES);
addNotes(update, [
...API_KEY_SCOPES_HELP_NOTES,
"",
"Valid permissions: read, write, create, update, delete",
" write = create + update + delete",
" Permissions auto-generate XACML policies (allowedEntityTypes respected).",
]);

addExamples(update, [
{
description: "Update an API key name",
command: "geonic admin api-keys update <key-id> --name new-name",
},
{
description: "Update permissions",
command: "geonic admin api-keys update <key-id> --permissions read,write",
},
{
description: "Enable DPoP requirement",
command: "geonic admin api-keys update <key-id> --dpop-required",
Expand Down
12 changes: 10 additions & 2 deletions src/commands/admin/policies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,14 @@ export function registerPoliciesCommand(parent: Command): void {
" }\n\n" +
"Target fields:\n" +
" subjects — attributeId: role, userId, email, tenantId\n" +
" resources — attributeId: path, entityType, entityId, entityOwner, tenantService\n" +
" resources — attributeId: path, entityType, entityId, entityOwner, tenantService, servicePath\n" +
" actions — attributeId: method (GET, POST, PATCH, DELETE)\n\n" +
"Each element: {attributeId, matchValue, matchFunction?}\n" +
" matchFunction: \"string-equal\" (default) | \"string-regexp\" | \"glob\"",
" matchFunction: \"string-equal\" (default) | \"string-regexp\" | \"glob\"\n\n" +
"Priority: higher number = higher priority (default: 0).\n" +
" Custom policies (e.g. 100) override default role policies (0).\n\n" +
"Default role policies (priority 0):\n" +
" user → GET only, api_key → all Deny, anonymous → all Deny",
)
.action(
withErrorHandler(async (json: unknown, _opts: unknown, cmd: Command) => {
Expand Down Expand Up @@ -105,6 +109,10 @@ export function registerPoliciesCommand(parent: Command): void {
description: "Create anonymous access policy",
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"}]}'`,
},
{
description: "Create servicePath-based policy (glob match)",
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"}]}'`,
},
{
description: "Create from a JSON file",
command: "geonic admin policies create @policy.json",
Expand Down
26 changes: 2 additions & 24 deletions src/commands/admin/tenants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,24 +107,10 @@ export function registerTenantsCommand(parent: Command): void {
"JSON payload: only specified fields are updated.\n" +
' e.g. {"name": "new-name", "description": "Updated description"}',
)
.option("--anonymous-access", "Enable anonymous access for the tenant")
.option("--no-anonymous-access", "Disable anonymous access for the tenant")
.action(
withErrorHandler(
async (id: unknown, json: unknown, opts: unknown, cmd: Command) => {
const options = opts as { anonymousAccess?: boolean };
let body: Record<string, unknown>;
if (json) {
body = (await parseJsonInput(json as string)) as Record<string, unknown>;
} else if (options.anonymousAccess !== undefined) {
body = {
settings: {
features: { anonymousAccessEnabled: options.anonymousAccess },
},
};
} else {
body = (await parseJsonInput(undefined)) as Record<string, unknown>;
}
async (id: unknown, json: unknown, _opts: unknown, cmd: Command) => {
const body = (await parseJsonInput(json as string | undefined)) as Record<string, unknown>;
const client = createClient(cmd);
const format = getFormat(cmd);
const response = await client.rawRequest(
Expand All @@ -147,14 +133,6 @@ export function registerTenantsCommand(parent: Command): void {
description: "Rename a tenant",
command: `geonic admin tenants update <tenant-id> '{"name":"new-name"}'`,
},
{
description: "Enable anonymous access",
command: "geonic admin tenants update <tenant-id> --anonymous-access",
},
{
description: "Disable anonymous access",
command: "geonic admin tenants update <tenant-id> --no-anonymous-access",
},
{
description: "Update from a JSON file",
command: "geonic admin tenants update <tenant-id> @patch.json",
Expand Down
19 changes: 16 additions & 3 deletions src/commands/me-api-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { loadConfig, saveConfig } from "../config.js";
import { parseJsonInput } from "../input.js";
import { printError, printWarning } from "../output.js";
import { addExamples, addNotes } from "./help.js";
import { API_KEY_SCOPES_HELP_NOTES } from "../helpers.js";
import { API_KEY_SCOPES_HELP_NOTES, parsePermissions } from "../helpers.js";

export function addMeApiKeysSubcommand(me: Command): void {
const apiKeys = me
Expand Down Expand Up @@ -41,6 +41,7 @@ export function addMeApiKeysSubcommand(me: Command): void {
.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(
withErrorHandler(async (json: unknown, _opts: unknown, cmd: Command) => {
Expand All @@ -51,6 +52,7 @@ export function addMeApiKeysSubcommand(me: Command): void {
entityTypes?: string;
rateLimit?: string;
dpopRequired?: boolean;
permissions?: string;
save?: boolean;
};

Expand All @@ -66,13 +68,14 @@ export function addMeApiKeysSubcommand(me: Command): void {
let body: unknown;
if (json) {
body = await parseJsonInput(json as string | undefined);
} else if (opts.name || opts.scopes || opts.origins || opts.entityTypes || opts.rateLimit || opts.dpopRequired !== undefined) {
} else if (opts.name || opts.scopes || opts.origins || opts.entityTypes || opts.rateLimit || opts.dpopRequired !== undefined || opts.permissions) {
const payload: Record<string, unknown> = {};
if (opts.name) payload.name = opts.name;
if (opts.scopes) payload.allowedScopes = opts.scopes.split(",").map((s: string) => s.trim()).filter(Boolean);
if (opts.origins) payload.allowedOrigins = opts.origins.split(",").map((s: string) => s.trim()).filter(Boolean);
if (opts.entityTypes) payload.allowedEntityTypes = opts.entityTypes.split(",").map((s: string) => s.trim()).filter(Boolean);
if (opts.dpopRequired !== undefined) payload.dpopRequired = opts.dpopRequired;
if (opts.permissions) payload.permissions = parsePermissions(opts.permissions);
if (opts.rateLimit) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const raw = opts.rateLimit.trim();
if (!/^\d+$/.test(raw)) {
Expand Down Expand Up @@ -128,13 +131,23 @@ export function addMeApiKeysSubcommand(me: Command): void {
}),
);

addNotes(create, API_KEY_SCOPES_HELP_NOTES);
addNotes(create, [
...API_KEY_SCOPES_HELP_NOTES,
"",
"Valid permissions: read, write, create, update, delete",
" write = create + update + delete",
" Permissions auto-generate XACML policies (allowedEntityTypes respected).",
]);

addExamples(create, [
{
description: "Create an API key with flags",
command: "geonic me api-keys create --name my-app --scopes read:entities --origins 'https://example.com'",
},
{
description: "Create with permissions (auto-generates XACML policy)",
command: "geonic me api-keys create --name my-app --permissions read,write --save",
},
{
description: "Create and save API key to config",
command: "geonic me api-keys create --name my-app --save",
Expand Down
Loading
Loading