Add API key management functionality with PostgreSQL support

- Introduced `main.go` implementing API key creation, listing, updating, and revocation.
- Added `go.mod` and `go.sum` for dependency management.
- Created `migrations/001_create_api_keys.sql` for setting up the API keys table in PostgreSQL.
- Updated `openapi.yaml` to include new endpoints for managing API keys.
- Added documentation for the admin UI and API key management in `docs/admin_ui.md` and `docs/design.md`.
- Enhanced `README.md` with environment variable requirements and startup instructions.
This commit is contained in:
president
2026-02-05 23:30:04 +09:00
parent de44ef33b9
commit 8300e517e4
8 changed files with 1367 additions and 21 deletions

View File

@@ -1,3 +1,15 @@
# pgvecterAPI
mcp サーバーのブリッチ
mcp サーバーのブリッチ
## 開発メモ
### 必要な環境変数
- `ADMIN_API_KEY`: 管理者APIの認証キー
- `DATABASE_URL`: PostgreSQL接続文字列(未指定の場合はメモリ保存)
- `PORT`: HTTPポート(省略時 `8080`)
### 起動
```bash
ADMIN_API_KEY=your-admin-key DATABASE_URL=postgres://... go run .
```

57
docs/admin_ui.md Normal file
View File

@@ -0,0 +1,57 @@
# pgvecter API 管理者UI 最小仕様
作成日: 2026-02-04
目的: APIキーの発行・失効・権限管理を最小コストで行う管理者向けUI
## 1. 前提
- 管理者UIはAPIサーバーに同梱する簡易HTML
- 認証は管理者APIキー
- 利用者は社内の管理者のみ
- 画面は最小限(一覧、発行、失効、編集)
## 2. 認証方式
- ヘッダー: `X-ADMIN-API-KEY`
- 有効なキーのみUIアクセス可
- ログにキーの生値は出さない(マスク)
## 3. 画面構成(最小)
1. APIキー一覧
- 表示項目: `id`, `label`, `permissions.users`, `status`, `created_at`, `last_used_at`
- 操作: 失効、編集
2. APIキー発行
- 入力: `label``permissions.users`(配列)
- 生成後はキーを一度だけ表示
3. APIキー編集
- 編集可能: `label`, `permissions.users`
- キーの再表示は禁止
## 4. API(管理用) 仕様(案)
- `GET /admin/api-keys`
- 説明: APIキー一覧を取得
- `POST /admin/api-keys`
- body: `{ label: string, permissions_users: string[] }`
- 説明: 新規キー発行。レスポンスで `api_key` を一度だけ返す
- `PATCH /admin/api-keys/:id`
- body: `{ label?: string, permissions_users?: string[] }`
- 説明: メタ情報の更新
- `POST /admin/api-keys/:id/revoke`
- 説明: 失効
## 5. データモデル(案)
- `api_keys`
- `id` (uuid)
- `label` (text)
- `api_key_hash` (text) 生値は保存しない
- `permissions_users` (text[])
- `status` (active|revoked)
- `created_at` (timestamptz)
- `last_used_at` (timestamptz)
## 6. ルール
- `permissions.users` はサーバ側で強制注入され、クライアント入力は無視
- 失効キーは即時無効
- 発行したキーは一度だけ表示
- `last_used_at` はAPI利用時に更新
## 7. 監査ログ(最小)
- `who`(管理者), `action`(issue/revoke/update), `target_id`, `time`, `ip`

View File

@@ -10,10 +10,11 @@
## 2. 前提・制約
- pgvecter への接続方式: 未確定
- 認証方式: 未確定
- 認証方式: APIキー (社内のみ、運用は手動・将来は簡易ツール化)
- 対象言語/フレームワーク: 未確定
- デプロイ先: 未確定
- 自社PostgreSQL設置: `labo.sunamura-llc.com`
- 想定組織: 単一組織(将来的に社員利用を想定)
## 3. 想定ユースケース
- 文書/テキストの埋め込み登録
@@ -46,11 +47,16 @@
- `POST /kb/search`
- body: `{ query: string, top_k?: number, filter?: object }`
- embedding 生成 → `ORDER BY embedding <=> $1` (cosine 等)
- 検索対象: `kb_doc_chunks` を正とする(効率優先)
- `GET /health`
- 死活監視
## 4.1.2 `/kb/search` filter 仕様(案)
- 方針: `metadata`(jsonb) の一致/包含フィルタのみサポート
- パス表記: `metadata.foo.bar` のみ許可(プレフィックス必須)
- 型ルール:
- `eq`/`in` は文字列/数値/真偽値のプリミティブを対象
- `contains` は文字列か配列のみ
- 演算子:
- `eq`: 完全一致
- `in`: いずれかに一致
@@ -60,22 +66,23 @@
- `and`: すべて満たす
- `or`: いずれか満たす
- 例:
- `{"and":[{"eq":{"metadata.source":"crm"}},{"in":{"metadata.permissions.roles":["sales","ops"]}}]}`
- `{"exists":{"metadata.permissions.users":true}}`
- `{"and":[{"eq":{"metadata.source":"crm"}},{"exists":{"metadata.permissions.users":true}}]}`
- 注意: `permissions.users` はサーバ側で注入し、クライアントからの直接指定は原則無効
## 4.1.3 `/kb/search` filter 実装ルール(案)
- 対象列は `metadata` のみ。SQL では `metadata` の JSONB 演算子を使用する。
- パラメータは必ずバインドする(文字列連結禁止)。
- 実装は「フィルタJSON → SQL条件 + params」の変換器で行う。
- キー表記は `metadata.foo.bar` のドット区切り。
- JSONB 抽出`metadata #>> '{foo,bar}'` (text) を基本とする
- 型に応じて JSONB 抽出方法を切り替える(文字列/配列の混在を防止)
- 権限フィルタはクライアント入力と分離し、サーバ側で必ず注入する。
### 演算子ごとのSQL変換
- `eq`: `metadata #>> '{path}' = $n`
- `in`: `metadata #>> '{path}' = ANY($n)` (配列)
- `contains`:
- 文字列: `metadata #>> '{path}' ILIKE '%' || $n || '%'`
- 配列: `metadata @> $n::jsonb` (部分一致)
- 配列: `metadata #> '{path}' @> $n::jsonb` (部分一致)
- `exists`: `metadata ? 'key'` (最終キー)
### 論理結合
@@ -85,8 +92,8 @@
### 例(変換イメージ)
- 入力: `{"eq":{"metadata.source":"crm"}}`
- SQL: `metadata #>> '{source}' = $1`
- 入力: `{"in":{"metadata.permissions.roles":["sales","ops"]}}`
- SQL: `metadata #>> '{permissions,roles}' = ANY($1)`
- 入力: `{"exists":{"metadata.permissions.users":true}}`
- SQL: `metadata ? 'permissions' AND (metadata #> '{permissions}') ? 'users'`
### db.query(読み取り専用)
- 許可するSQL: `SELECT` のみ
@@ -127,6 +134,8 @@
- `content` (text)
- `metadata` (jsonb)
- `embedding` (vector(N))
- `embedding_model` (text)
- `embedding_dim` (int)
- `updated_at` (timestamp)
- インデックス
- `HNSW` 推奨: `CREATE INDEX ... USING hnsw (embedding vector_cosine_ops);`
@@ -147,8 +156,10 @@
- 権限: `metadata` に格納
- 業務基幹: 同期まで行う
- 埋め込み次元: 1536
- 権限キー: `permissions.roles`, `permissions.users`, `permissions.orgs`
- 埋め込みモデル変更: 将来差し替えあり(モデル名と次元を記録)
- 権限キー: `permissions.users` のみ(当面)
- 同期方式: 定期 pull + 差分
- チャンク仕様: 600 tokens、overlap 80 tokens、段落優先
## 5.4 DDL候補(ドラフト)
```sql
@@ -159,6 +170,8 @@ CREATE TABLE kb_docs (
title text,
content text,
metadata jsonb NOT NULL DEFAULT '{}',
embedding_model text NOT NULL,
embedding_dim int NOT NULL,
updated_at timestamptz NOT NULL DEFAULT now()
);
@@ -170,6 +183,8 @@ CREATE TABLE kb_doc_chunks (
content text NOT NULL,
metadata jsonb NOT NULL DEFAULT '{}',
embedding vector(1536) NOT NULL,
embedding_model text NOT NULL,
embedding_dim int NOT NULL,
updated_at timestamptz NOT NULL DEFAULT now()
);
@@ -198,16 +213,16 @@ CREATE INDEX kb_doc_chunks_embedding_hnsw
```
### 参考: metadata に入れる権限キー案
- `permissions.roles`: ["sales", "ops"]
- `permissions.users`: ["user_123", "user_456"]
- `permissions.orgs`: ["org_a"]
## 6. セキュリティ/運用
- 認証: TBD
- 認可: TBD
- レート制限: TBD
- 監視/ログ: TBD
- 認証: APIキー (社内のみ、キー単位で権限・用途を分離、運用は手動)
- 認可: 単一組織だが社員利用を想定し、権限は `metadata.permissions.users` をサーバ側で強制注入
- レート制限: APIキー単位で制限
- 監視/ログ: リクエストID、実行時間、件数を記録
- 監査ログ: どのツールがどの引数で呼ばれたか記録
- ログの扱い: PIIのマスキング、保持期間を短期化
- 管理者UI: APIサーバーに同梱する簡易HTML(管理者APIキーで保護)
## 6.1 SQL 安全ガード(提案)
- `SELECT` のみ許可(`WITH ... SELECT` を許可する場合は明示的に許可)
@@ -218,6 +233,7 @@ CREATE INDEX kb_doc_chunks_embedding_hnsw
- ホワイトリスト: 許可されたスキーマ/テーブルのみ
- 危険関数禁止: `pg_read_file`, `pg_ls_dir`, `dblink`, `lo_export`, `COPY TO`
- `EXPLAIN/ANALYZE` 禁止
- SQLパーサによる構文検証(正規表現のみは禁止)
- 監査ログ: `who`, `sql`, `params`, `duration`, `row_count`
## 7. 追加確認事項
@@ -226,3 +242,4 @@ CREATE INDEX kb_doc_chunks_embedding_hnsw
- SLA/性能要件
- バックアップ/リストア要件
- 業務基盤システムの接続方式
- 管理者UIの最小機能(発行、一覧、失効、`permissions.users` 編集)

12
go.mod Normal file
View File

@@ -0,0 +1,12 @@
module pgvecterapi
go 1.24.0
require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.8.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/text v0.29.0 // indirect
)

19
go.sum Normal file
View File

@@ -0,0 +1,19 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

1027
main.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
-- API key store
CREATE TABLE IF NOT EXISTS api_keys (
id text PRIMARY KEY,
label text NOT NULL,
api_key_hash text NOT NULL UNIQUE,
permissions_users text[] NOT NULL,
status text NOT NULL,
created_at timestamptz NOT NULL,
last_used_at timestamptz
);
CREATE INDEX IF NOT EXISTS api_keys_status_idx ON api_keys(status);

View File

@@ -4,7 +4,7 @@ info:
version: 0.1.0
description: Minimal tool API for db query + knowledge base upsert/search.
security:
- BearerAuth: []
- ApiKeyAuth: []
servers:
- url: https://labo.sunamura-llc.com
description: Production
@@ -92,12 +92,127 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/admin/api-keys:
get:
summary: List API keys (admin)
operationId: adminApiKeysList
security:
- AdminApiKeyAuth: []
responses:
"200":
description: API key list
content:
application/json:
schema:
$ref: "#/components/schemas/AdminApiKeyListResponse"
"401":
description: Unauthorized
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
post:
summary: Issue API key (admin)
operationId: adminApiKeysCreate
security:
- AdminApiKeyAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/AdminApiKeyCreateRequest"
responses:
"200":
description: Issued API key (shown once)
content:
application/json:
schema:
$ref: "#/components/schemas/AdminApiKeyCreateResponse"
"400":
description: Invalid request
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"401":
description: Unauthorized
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/admin/api-keys/{id}:
patch:
summary: Update API key metadata (admin)
operationId: adminApiKeysUpdate
security:
- AdminApiKeyAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/AdminApiKeyUpdateRequest"
responses:
"200":
description: Updated API key
content:
application/json:
schema:
$ref: "#/components/schemas/AdminApiKey"
"400":
description: Invalid request
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"401":
description: Unauthorized
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/admin/api-keys/{id}/revoke:
post:
summary: Revoke API key (admin)
operationId: adminApiKeysRevoke
security:
- AdminApiKeyAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
"200":
description: Revoked API key
content:
application/json:
schema:
$ref: "#/components/schemas/AdminApiKey"
"401":
description: Unauthorized
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
ApiKeyAuth:
type: apiKey
in: header
name: X-API-KEY
AdminApiKeyAuth:
type: apiKey
in: header
name: X-ADMIN-API-KEY
schemas:
HealthResponse:
type: object
@@ -169,6 +284,11 @@ components:
properties:
query:
type: string
query_embedding:
type: array
items:
type: number
description: Optional precomputed embedding vector. If omitted, server-side embedding is required.
top_k:
type: integer
default: 5
@@ -179,7 +299,7 @@ components:
additionalProperties: true
description: |
JSON filter over metadata. Supported operators: eq, in, contains, exists, and/or.
Example: {"and":[{"eq":{"metadata.source":"crm"}},{"in":{"metadata.permissions.roles":["sales","ops"]}}]}
Example: {"and":[{"eq":{"metadata.source":"crm"}},{"exists":{"metadata.permissions.users":true}}]}
required:
- query
KbSearchResponse:
@@ -208,6 +328,76 @@ components:
- id
- content
- score
AdminApiKey:
type: object
properties:
id:
type: string
label:
type: string
permissions_users:
type: array
items:
type: string
status:
type: string
enum:
- active
- revoked
created_at:
type: string
format: date-time
last_used_at:
type: string
format: date-time
nullable: true
required:
- id
- label
- permissions_users
- status
- created_at
AdminApiKeyListResponse:
type: object
properties:
items:
type: array
items:
$ref: "#/components/schemas/AdminApiKey"
required:
- items
AdminApiKeyCreateRequest:
type: object
properties:
label:
type: string
permissions_users:
type: array
items:
type: string
required:
- label
- permissions_users
AdminApiKeyCreateResponse:
type: object
properties:
api_key:
type: string
description: Returned only once on creation
key:
$ref: "#/components/schemas/AdminApiKey"
required:
- api_key
- key
AdminApiKeyUpdateRequest:
type: object
properties:
label:
type: string
permissions_users:
type: array
items:
type: string
ErrorResponse:
type: object
properties: