Skip to content

Commit ccb339e

Browse files
authored
feat: テナント匿名アクセス設定 & policies create ヘルプ改善 (#78)
* feat: admin tenants update に --anonymous-access オプションを追加、policies create のヘルプに target スキーマ例を追加 - admin tenants update に --anonymous-access / --no-anonymous-access フラグを追加し、 テナントの anonymousAccessEnabled を CLI から直接切り替え可能にした (#76) - admin policies create のヘルプに target フィールド (subjects, resources, actions, environments) の構造と実用的な JSON 例を追加 (#75) - 匿名アクセスポリシーの作成例を policies create の examples に追加 - テナント update の --anonymous-access フラグに対するテスト4件を追加 Closes #75, Closes #76 * fix: サーバー側スキーマに合わせて実装を修正 - anonymousAccessEnabled を settings.features 配下のネスト構造に修正 (トップレベルではなく settings.features.anonymousAccessEnabled) - policy target から environments フィールドを削除(サーバー未サポート) - action の attributeId を "action" から "method" (HTTP method) に修正 - target の matchFunction オプション (string-equal, string-regexp, glob) を記載 - subjects/resources/actions の有効な attributeId 一覧をヘルプに追加
1 parent 745ca86 commit ccb339e

4 files changed

Lines changed: 95 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77

88
## [Unreleased]
99

10+
### 2026-03-19
11+
- **Feat**: `admin tenants update``--anonymous-access` / `--no-anonymous-access` オプションを追加 — テナントの匿名アクセス設定を CLI から管理可能に (#76)
12+
- **Docs**: `admin policies create` のヘルプに `target` フィールド(subjects, resources, actions, environments)のスキーマ例を追加 (#75)
13+
1014
### 2026-03-17
1115
- **Fix**: API キーコマンド (`me api-keys`, `admin api-keys`) のヘルプで表示されるスコープ一覧を、API キーで実際に使用可能な6スコープに修正 (#74)
1216

src/commands/admin/policies.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,27 @@ export function registerPoliciesCommand(parent: Command): void {
5757
.command("create [json]")
5858
.description(
5959
"Create a new policy\n\n" +
60-
"JSON payload example:\n" +
60+
"JSON payload examples:\n\n" +
61+
" Allow all entities:\n" +
6162
" {\n" +
6263
' "description": "Allow all entities",\n' +
6364
' "rules": [{"ruleId": "allow-all", "effect": "Permit"}]\n' +
64-
" }",
65+
" }\n\n" +
66+
" Allow GET access to a specific entity type:\n" +
67+
" {\n" +
68+
' "description": "Allow GET access to Landmark entities",\n' +
69+
' "target": {\n' +
70+
' "resources": [{"attributeId": "entityType", "matchValue": "Landmark"}],\n' +
71+
' "actions": [{"attributeId": "method", "matchValue": "GET"}]\n' +
72+
" },\n" +
73+
' "rules": [{"ruleId": "permit-get", "effect": "Permit"}]\n' +
74+
" }\n\n" +
75+
"Target fields:\n" +
76+
" subjects — attributeId: role, userId, email, tenantId\n" +
77+
" resources — attributeId: path, entityType, entityId, entityOwner, tenantService\n" +
78+
" actions — attributeId: method (GET, POST, PATCH, DELETE)\n\n" +
79+
"Each element: {attributeId, matchValue, matchFunction?}\n" +
80+
" matchFunction: \"string-equal\" (default) | \"string-regexp\" | \"glob\"",
6581
)
6682
.action(
6783
withErrorHandler(async (json: unknown, _opts: unknown, cmd: Command) => {
@@ -81,6 +97,14 @@ export function registerPoliciesCommand(parent: Command): void {
8197
description: "Create with inline JSON",
8298
command: `geonic admin policies create '{"description":"Allow all entities","rules":[{"ruleId":"allow-all","effect":"Permit"}]}'`,
8399
},
100+
{
101+
description: "Create with target (entity type + method)",
102+
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"}]}'`,
103+
},
104+
{
105+
description: "Create anonymous access policy",
106+
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"}]}'`,
107+
},
84108
{
85109
description: "Create from a JSON file",
86110
command: "geonic admin policies create @policy.json",

src/commands/admin/tenants.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,24 @@ export function registerTenantsCommand(parent: Command): void {
107107
"JSON payload: only specified fields are updated.\n" +
108108
' e.g. {"name": "new-name", "description": "Updated description"}',
109109
)
110+
.option("--anonymous-access", "Enable anonymous access for the tenant")
111+
.option("--no-anonymous-access", "Disable anonymous access for the tenant")
110112
.action(
111113
withErrorHandler(
112-
async (id: unknown, json: unknown, _opts: unknown, cmd: Command) => {
113-
const body = await parseJsonInput(json as string | undefined);
114+
async (id: unknown, json: unknown, opts: unknown, cmd: Command) => {
115+
const options = opts as { anonymousAccess?: boolean };
116+
let body: Record<string, unknown>;
117+
if (json) {
118+
body = (await parseJsonInput(json as string)) as Record<string, unknown>;
119+
} else if (options.anonymousAccess !== undefined) {
120+
body = {
121+
settings: {
122+
features: { anonymousAccessEnabled: options.anonymousAccess },
123+
},
124+
};
125+
} else {
126+
body = (await parseJsonInput(undefined)) as Record<string, unknown>;
127+
}
114128
const client = createClient(cmd);
115129
const format = getFormat(cmd);
116130
const response = await client.rawRequest(
@@ -133,6 +147,14 @@ export function registerTenantsCommand(parent: Command): void {
133147
description: "Rename a tenant",
134148
command: `geonic admin tenants update <tenant-id> '{"name":"new-name"}'`,
135149
},
150+
{
151+
description: "Enable anonymous access",
152+
command: "geonic admin tenants update <tenant-id> --anonymous-access",
153+
},
154+
{
155+
description: "Disable anonymous access",
156+
command: "geonic admin tenants update <tenant-id> --no-anonymous-access",
157+
},
136158
{
137159
description: "Update from a JSON file",
138160
command: "geonic admin tenants update <tenant-id> @patch.json",

tests/admin-tenants.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,47 @@ describe("admin tenants commands", () => {
103103
expect(outputResponse).toHaveBeenCalled();
104104
expect(printSuccess).toHaveBeenCalledWith("Tenant updated.");
105105
});
106+
107+
it("enables anonymous access with --anonymous-access", async () => {
108+
client.rawRequest.mockResolvedValue(mockResponse({ id: "t1" }));
109+
const program = makeProgram();
110+
await runCommand(program, ["admin", "tenants", "update", "t1", "--anonymous-access"]);
111+
expect(client.rawRequest).toHaveBeenCalledWith("PATCH", "/admin/tenants/t1", {
112+
body: { settings: { features: { anonymousAccessEnabled: true } } },
113+
});
114+
expect(parseJsonInput).not.toHaveBeenCalled();
115+
expect(printSuccess).toHaveBeenCalledWith("Tenant updated.");
116+
});
117+
118+
it("disables anonymous access with --no-anonymous-access", async () => {
119+
client.rawRequest.mockResolvedValue(mockResponse({ id: "t1" }));
120+
const program = makeProgram();
121+
await runCommand(program, ["admin", "tenants", "update", "t1", "--no-anonymous-access"]);
122+
expect(client.rawRequest).toHaveBeenCalledWith("PATCH", "/admin/tenants/t1", {
123+
body: { settings: { features: { anonymousAccessEnabled: false } } },
124+
});
125+
expect(parseJsonInput).not.toHaveBeenCalled();
126+
expect(printSuccess).toHaveBeenCalledWith("Tenant updated.");
127+
});
128+
129+
it("prefers json argument over --anonymous-access flag", async () => {
130+
const body = { name: "updated" };
131+
vi.mocked(parseJsonInput).mockResolvedValue(body);
132+
client.rawRequest.mockResolvedValue(mockResponse({ id: "t1" }));
133+
const program = makeProgram();
134+
await runCommand(program, ["admin", "tenants", "update", "t1", '{"name":"updated"}', "--anonymous-access"]);
135+
expect(client.rawRequest).toHaveBeenCalledWith("PATCH", "/admin/tenants/t1", { body });
136+
});
137+
138+
it("falls back to parseJsonInput when no json and no flag", async () => {
139+
const body = { description: "interactive" };
140+
vi.mocked(parseJsonInput).mockResolvedValue(body);
141+
client.rawRequest.mockResolvedValue(mockResponse({ id: "t1" }));
142+
const program = makeProgram();
143+
await runCommand(program, ["admin", "tenants", "update", "t1"]);
144+
expect(parseJsonInput).toHaveBeenCalledWith(undefined);
145+
expect(client.rawRequest).toHaveBeenCalledWith("PATCH", "/admin/tenants/t1", { body });
146+
});
106147
});
107148

108149
describe("tenants delete", () => {

0 commit comments

Comments
 (0)