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
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@

## [Unreleased]

## [0.8.0] - 2026-03-23

### 2026-03-23
- **Feat**: `me policies` コマンド群を新設 — `list`, `get`, `create`, `update`, `delete` で `/me/policies` セルフサービスポリシー管理に対応 (GeonicDB #764) (#83, #86)
Comment on lines +10 to +13

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

リリース日が未来日付で固定されています。

このPR時点(2026-03-22)で 0.8.02026-03-23 と確定すると、未リリース状態との整合が崩れやすいです。リリース当日確定にするか、Unreleased 側で保持する運用が安全です。

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

In `@CHANGELOG.md` around lines 10 - 13, The changelog entry for "## [0.8.0] -
2026-03-23" is using a future fixed release date; either move the entire "0.8.0"
section under the "Unreleased" heading or change the date to the actual release
date only when publishing (do not hardcode 2026-03-23 now). Locate the heading
"## [0.8.0] - 2026-03-23" and update it to "## [0.8.0] - Unreleased" (or remove
the date entirely) until you are ready to tag the release, or set the date to
2026-03-22 only at release time. Ensure the rest of the list (the `me policies`
bullet) stays under that adjusted heading.

- **Feat**: `me api-keys update` コマンドを追加 — `PATCH /me/api-keys/{keyId}` 対応 (`--name`, `--policy-id`, `--origins`, `--rate-limit`, `--dpop-required`/`--no-dpop-required`, `--active`/`--inactive`) (#85)
- **Feat**: `me oauth-clients update` コマンドを追加 — `PATCH /me/oauth-clients/{clientId}` 対応 (`--name`, `--description`, `--policy-id`, `--active`/`--inactive`) (#85)
- **Feat**: `me oauth-clients regenerate-secret` コマンドを追加 — `POST /me/oauth-clients/{clientId}/regenerate-secret` 対応 (#85)
- **Docs**: XACML デフォルトポリシー再設計に合わせて README を更新 — `user` デフォルト動作、priority 体系表、`me policies` セクション、認可モデル説明を整理 (GeonicDB #762) (#84)

### 2026-03-21
- **Breaking**: API キー (`admin api-keys`, `me api-keys`) から `--scopes`, `--permissions`, `--entity-types` オプションを削除し、`--policy <policyId>` オプションを追加 — 認可は policyId 紐づけ方式に移行 (GeonicDB #757) (#82)
- **Breaking**: OAuth クライアント (`admin oauth-clients`, `me oauth-clients`) から `--scopes` オプションと `allowedScopes` フィールドを削除し、`--policy <policyId>` オプションを追加 (GeonicDB #757) (#82)
Expand Down Expand Up @@ -149,7 +158,8 @@
### 2026-02-26
- **Docs**: README にインストール手順・使い方・コマンドリファレンスを追加 (#1)

[Unreleased]: https://github.com/geolonia/geonicdb-cli/compare/v0.7.0...HEAD
[Unreleased]: https://github.com/geolonia/geonicdb-cli/compare/v0.8.0...HEAD
[0.8.0]: https://github.com/geolonia/geonicdb-cli/compare/v0.7.0...v0.8.0
[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.6.3]: https://github.com/geolonia/geonicdb-cli/compare/v0.6.2...v0.6.3
Expand Down
82 changes: 74 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,24 +193,37 @@ Displays the current authenticated user, token expiry, and active profile.
|---|---|
| `me oauth-clients list` | List your OAuth clients |
| `me oauth-clients create [json]` | Create a new OAuth client |
| `me oauth-clients update <clientId> [json]` | Update an OAuth client |
| `me oauth-clients regenerate-secret <clientId>` | Regenerate client secret |
| `me oauth-clients delete <id>` | Delete an OAuth client |

`me oauth-clients create` supports flag options: `--name`, `--policy`, `--save`. Use `--save` to store client credentials in config for automatic re-authentication.

`me oauth-clients update` supports: `--name`, `--description`, `--policy-id` (use `null` to unbind), `--active`, `--inactive`.

```bash
# Create with flags
geonic me oauth-clients create --name my-ci-bot --policy <policy-id>

# Create from JSON (note: field is "name", not "clientName")
geonic me oauth-clients create '{"name":"my-bot","policyId":"<policy-id>"}'

# Attach a personal policy
geonic me oauth-clients update <client-id> --policy-id my-readonly

# Unbind policy
geonic me oauth-clients update <client-id> --policy-id null
```

**Note**: `--policy-id` on update accepts only policies created by yourself (`/me/policies`). Policies created via `admin policies` cannot be bound here.

#### me api-keys

| Subcommand | Description |
|---|---|
| `me api-keys list` | List your API keys |
| `me api-keys create [json]` | Create a new API key |
| `me api-keys update <keyId> [json]` | Update an API key |
| `me api-keys delete <keyId>` | Delete an API key |

`me api-keys create` supports flag options:
Expand All @@ -224,16 +237,55 @@ geonic me oauth-clients create '{"name":"my-bot","policyId":"<policy-id>"}'
| `--dpop-required` | Require DPoP token binding (RFC 9449) |
| `--save` | Save the API key to profile config |

`me api-keys update` supports: `--name`, `--policy-id` (use `null` to unbind), `--origins`, `--rate-limit`, `--dpop-required` / `--no-dpop-required`, `--active`, `--inactive`.

```bash
# Create an API key with a policy and save to config
geonic me api-keys create --name my-app --policy <policy-id> --save

# Create from JSON
geonic me api-keys create '{"name":"my-app","policyId":"<policy-id>"}'

# Attach a personal policy
geonic me api-keys update <key-id> --policy-id my-readonly

# Unbind policy
geonic me api-keys update <key-id> --policy-id null
```

`me api-keys list` output includes a `dpopRequired` field (boolean).

**Note**: `--policy-id` on update accepts only policies created by yourself (`/me/policies`). Policies created via `admin policies` cannot be bound here.

#### me policies

| Subcommand | Description |
|---|---|
| `me policies list` | List your personal policies |
| `me policies get <policyId>` | Get a personal policy by ID |
| `me policies create [json]` | Create a personal policy |
| `me policies update <policyId> [json]` | Update a personal policy |
| `me policies delete <policyId>` | Delete a personal policy |

Personal policies (`scope: personal`) are created by `user` role accounts for self-service access control. They can be bound to your own API keys and OAuth clients.

**Constraints (enforced server-side)**:
- `priority` is fixed at 100 (user role minimum — cannot escalate)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- `scope` is always `personal` — not applied tenant-wide
- `target` is required
- Data API paths only (`/v2/**`, `/ngsi-ld/**` etc.) — admin/me paths are not allowed

```bash
# Create a GET-only policy
geonic me policies create @readonly-policy.json

# Bind to an API key
geonic me api-keys update <key-id> --policy-id my-readonly

# Bind to an OAuth client
geonic me oauth-clients update <client-id> --policy-id my-readonly
```

### entities — Manage context entities

| Subcommand | Description |
Expand Down Expand Up @@ -401,15 +453,24 @@ 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):
**XACML Authorization Model**: All authorization is unified under XACML policies. Default role policies:

| Role | Default Behavior |
|---|---|
| `user` | GET only (read-only) |
| `api_key` | All Deny |
| `anonymous` | All Deny |
| Role | Default Behavior | Default priority |
|---|---|---|
| `user` | `/v2/**` and `/ngsi-ld/**` — all methods (CRUD) Permit. Other data APIs (`/catalog`, `/rules`, etc.) — GET only. | 100 |
| `api_key` | All Deny | 100 |
| `anonymous` | All Deny | 100 |

**Priority**: Smaller `priority` value = higher precedence (e.g. `priority: 10` overrides the user default at `priority: 100`).

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.
| priority range | Who creates | Notes |
|---|---|---|
| -1 | System | deny-fence (e.g. super_admin data API block) — cannot be overridden |
| 0 | System | super_admin default — tenant_admin and below cannot override |
| 10–99 | `tenant_admin` | Custom tenant-wide policies |
| 100 | System / `user` (self-service via `/me/policies`) | `user` / `api_key` / `anonymous` defaults and personal policies — server fixes personal policy priority at 100 |

Custom `tenant_admin` policies (priority 10–99) override the user 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

Expand Down Expand Up @@ -585,10 +646,15 @@ When both a Bearer token and an API key are configured, headers are sent exclusi

### Authorization Model

All authorization for API keys and OAuth clients is controlled via XACML policies. Use `--policy <policyId>` when creating API keys or OAuth clients to attach an existing policy. Manage policies with `geonic admin policies` commands.
All authorization for API keys and OAuth clients is controlled via XACML policies. Use `--policy <policyId>` when creating API keys or OAuth clients to attach an existing policy.

- **Tenant admins**: manage tenant-wide policies with `geonic admin policies` commands.
- **Users**: manage personal policies with `geonic me policies` commands and bind them to your own API keys / OAuth clients with `--policy-id`.

See the [admin policies](#admin-policies) section for details on the XACML authorization model, default role policies, and target resource attributes.

**Note**: `--policy-id` on `me api-keys update` / `me oauth-clients update` accepts only policies where `createdBy` matches your own user ID (i.e. policies created via `me policies create`). Policies created via `admin policies` cannot be bound to personal resources.

## Development

Requires Node.js >= 20.
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions src/commands/admin/policies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,11 @@ export function registerPoliciesCommand(parent: Command): void {
" actions — attributeId: method (GET, POST, PATCH, DELETE)\n\n" +
"Each element: {attributeId, matchValue, matchFunction?}\n" +
" 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",
"Priority: 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\n" +
"Default role policies (priority 100):\n" +
" user → /v2/** and /ngsi-ld/** all methods Permit; other data APIs GET only\n" +
" api_key → all Deny, anonymous → all Deny",
)
.action(
withErrorHandler(async (json: unknown, _opts: unknown, cmd: Command) => {
Expand Down
4 changes: 4 additions & 0 deletions src/commands/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { clientCredentialsGrant } from "../oauth.js";
import { addExamples } from "./help.js";
import { addMeOAuthClientsSubcommand } from "./me-oauth-clients.js";
import { addMeApiKeysSubcommand } from "./me-api-keys.js";
import { addMePoliciesSubcommand } from "./me-policies.js";

function createLoginCommand(): Command {
return new Command("login")
Expand Down Expand Up @@ -460,6 +461,9 @@ export function registerAuthCommands(program: Command): void {
// Add me api-keys subcommands
addMeApiKeysSubcommand(me);

// Add me policies subcommands
addMePoliciesSubcommand(me);

// Backward-compatible hidden aliases
program.addCommand(createLoginCommand(), { hidden: true });
program.addCommand(createLogoutCommand(), { hidden: true });
Expand Down
97 changes: 97 additions & 0 deletions src/commands/me-api-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,103 @@ export function addMeApiKeysSubcommand(me: Command): void {
},
]);

// api-keys update
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(
withErrorHandler(async (keyId: unknown, json: unknown, _opts: unknown, cmd: Command) => {
const opts = cmd.opts() as {
name?: string;
policyId?: string;
origins?: string;
rateLimit?: string;
dpopRequired?: boolean;
active?: boolean;
inactive?: boolean;
};

// Validate --origins flag early
if (opts.origins !== undefined) {
const parsed = opts.origins.split(",").map((s: string) => s.trim()).filter(Boolean);
if (parsed.length === 0) {
printError("allowedOrigins must contain at least 1 item. Use '*' to allow all origins.");
process.exit(1);
}
}

let body: unknown;
if (json) {
body = await parseJsonInput(json as string | undefined);
} else if (opts.name || opts.policyId !== undefined || opts.origins !== undefined || opts.rateLimit || opts.dpopRequired !== undefined || opts.active || opts.inactive) {
const payload: Record<string, unknown> = {};
if (opts.name) payload.name = opts.name;
if (opts.policyId !== undefined) payload.policyId = opts.policyId === "null" ? null : opts.policyId;
if (opts.origins !== undefined) payload.allowedOrigins = opts.origins.split(",").map((s: string) => s.trim()).filter(Boolean);
if (opts.dpopRequired !== undefined) payload.dpopRequired = opts.dpopRequired;
if (opts.rateLimit) {
const raw = opts.rateLimit.trim();
if (!/^\d+$/.test(raw)) {
printError("--rate-limit must be a positive integer.");
process.exit(1);
}
const perMinute = Number(raw);
if (perMinute <= 0) {
printError("--rate-limit must be a positive integer.");
process.exit(1);
}
payload.rateLimit = { perMinute };
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (opts.active) payload.isActive = true;
if (opts.inactive) payload.isActive = false;
body = payload;
} else {
body = await parseJsonInput();
}

const client = createClient(cmd);
const format = getFormat(cmd);
const response = await client.rawRequest(
"PATCH",
`/me/api-keys/${encodeURIComponent(String(keyId))}`,
{ body },
);
outputResponse(response, format);
console.error("API key updated.");
}),
);

addExamples(update, [
{
description: "Rename an API key",
command: "geonic me api-keys update <key-id> --name new-name",
},
{
description: "Attach a policy",
command: "geonic me api-keys update <key-id> --policy-id <policy-id>",
},
{
description: "Unbind policy",
command: "geonic me api-keys update <key-id> --policy-id null",
},
{
description: "Deactivate an API key",
command: "geonic me api-keys update <key-id> --inactive",
},
{
description: "Update from JSON",
command: 'geonic me api-keys update <key-id> \'{"name":"new-name","rateLimit":{"perMinute":60}}\'',
},
]);

// api-keys delete
const del = apiKeys
.command("delete <keyId>")
Expand Down
Loading
Loading