Conversation
Pugma
left a comment
There was a problem hiding this comment.
確認しました! 実装ありがとうございます
また、レビュー遅くなってすみません 🙇
テストでもう少しエラーに関するケースも追加したほうがいいのかなという印象はありましたが、概ね実装は問題なさそうです 👀
一点だけ、軽く確認してもらえればいいかなという感じです
| type ClientWithSecret struct { | ||
| Client | ||
| ClientSecret string | ||
| } |
There was a problem hiding this comment.
直前の Client 構造体はアプリで管理する情報 (domain) として用意されていて、現に他のところでも使われていると思います
ただ ClientWithSecret は、現状は Client 登録時の response としてしか使われておらず、また domain として ClientSecret 要素がそのまま string で平文として保管されていそうな印象もある気がします
個人的には
- この構造体を router 層に移動する
- ClientSecret を domain として持つときは
CryptedClientSecretとか、暗号化がされていることを明示できる命名にする
ほうが、読んだときに意図がわかりやすい気がします!
There was a problem hiding this comment.
Pull request overview
OAuth2/OIDC の Client 管理(CRUD + シークレット再生成)を admin API として追加し、OpenAPI 定義・ルーティング・永続化・テストまで一通り実装する PR です。
Changes:
/api/v1/admin/clients配下に Client の CRUD とシークレット再生成 API を追加(OpenAPI/生成コード/ハンドラ)- Client のドメイン・リポジトリ・ユースケースを追加し、MariaDB(sqlc) 永続化を実装
- ユースケース単体テストと DB を使った統合テストを追加
Reviewed changes
Copilot reviewed 18 out of 19 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/usecase/client.go | Client CRUD/secret 再生成のユースケースを追加 |
| internal/usecase/client_test.go | ユースケースの単体テスト(インメモリ mock repo)を追加 |
| internal/router/v1/handler.go | v1 ハンドラの DI コンテナを追加 |
| internal/router/v1/client.go | Client CRUD/secret API の Echo ハンドラ実装を追加 |
| internal/router/v1/client_test.go | MariaDB を使った admin client API の統合テストを追加 |
| internal/router/v1/gen/server.go | OpenAPI から生成された server ラッパ/ルーティング/strict ハンドラを追加 |
| internal/router/v1/gen/models.go | OpenAPI から生成された Client モデル群を追加 |
| internal/domain/client.go | Client/ClientType のドメインモデルを追加 |
| internal/repository/client.go | clients テーブル向けリポジトリ実装を追加 |
| internal/repository/oidc/querier.go | sqlc Querier に DeleteAllClients を追加 |
| internal/repository/oidc/oidc.sql.go | DeleteAllClients の生成コードを追加 |
| internal/repository/oidc/db.go | DeleteAllClients の prepared statement を追加 |
| internal/testutil/root.go | go.mod を辿ってプロジェクトルートを探すテストユーティリティを追加 |
| api/openapi.yaml | Client 管理 API とスキーマ定義を追加 |
| db/schema.sql | テーブル作成を IF NOT EXISTS に変更(テスト/初期化向け) |
| db/query/oidc.sql | DeleteAllClients を追加(sqlc 入力) |
| cmd/serve.go | Client 用の repo/usecase/handler を組み立ててルート登録 |
| go.mod | uuid/x-crypto 依存追加 |
| go.sum | 依存追加に伴う更新 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| func (h *Handler) CreateClient(ctx echo.Context) error { | ||
| var req gen.ClientCreate | ||
| if err := ctx.Bind(&req); err != nil { | ||
| return echo.NewHTTPError(http.StatusBadRequest, err.Error()) | ||
| } |
There was a problem hiding this comment.
OpenAPI では redirect_uris の minItems=1 や client_type の enum、URI format 等が定義されていますが、現状は Bind のみで不正入力を受け入れます。name/redirect_uris/client_type のバリデーションを追加して 400 を返すようにしてください(Update も同様)。
| clients, err := h.clientUseCase.List(ctx.Request().Context()) | ||
| if err != nil { | ||
| return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) | ||
| } |
There was a problem hiding this comment.
内部エラー時に err.Error() をそのままレスポンスに載せると、SQL エラー等の内部情報が外部に露出します。ログには詳細を残しつつ、クライアントへのメッセージは一般化(例: "internal server error")するなど情報漏えいを避けてください。
| uc := NewClientUseCase(repo) | ||
| ctx := context.Background() | ||
|
|
||
| created, _ := uc.Create(ctx, "original", domain.ClientTypeConfidential, []string{"http://localhost:3000/callback"}) |
There was a problem hiding this comment.
テスト内で uc.Create のエラーを捨てています。失敗時に created が nil になり後続がパニックする可能性があるため、ここも if err != nil { t.Fatalf(...) } の形で検証してください。
| created, _ := uc.Create(ctx, "original", domain.ClientTypeConfidential, []string{"http://localhost:3000/callback"}) | |
| created, err := uc.Create(ctx, "original", domain.ClientTypeConfidential, []string{"http://localhost:3000/callback"}) | |
| if err != nil { | |
| t.Fatalf("Create failed: %v", err) | |
| } |
| created, _ := uc.Create(ctx, "test-client", domain.ClientTypeConfidential, []string{"http://localhost:3000/callback"}) | ||
| originalHash := repo.getSecretHash(created.ClientID) | ||
|
|
||
| newSecret, err := uc.RegenerateSecret(ctx, created.ClientID) | ||
| if err != nil { |
There was a problem hiding this comment.
テスト内で uc.Create のエラーを捨てています。失敗時に created が nil になり後続がパニックする可能性があるため、ここも if err != nil { t.Fatalf(...) } の形で検証してください。
| paths: | ||
| /api/v1/admin/clients: | ||
| get: |
There was a problem hiding this comment.
PR 説明では /api/v1/admin/client/{clientId}(client が単数)となっていますが、OpenAPI/実装は /api/v1/admin/clients/{clientId}(複数形)です。どちらが正かを揃えて、PR 説明またはパス定義を修正してください。
| uc := NewClientUseCase(repo) | ||
| ctx := context.Background() | ||
|
|
||
| created, _ := uc.Create(ctx, "test-client", domain.ClientTypeConfidential, []string{"http://localhost:3000/callback"}) |
There was a problem hiding this comment.
テスト内で uc.Create のエラーを捨てています。失敗時に created が nil になり後続がパニックする可能性があるため、ここも if err != nil { t.Fatalf(...) } の形で検証してください。
| created, _ := uc.Create(ctx, "test-client", domain.ClientTypeConfidential, []string{"http://localhost:3000/callback"}) | |
| created, err := uc.Create(ctx, "test-client", domain.ClientTypeConfidential, []string{"http://localhost:3000/callback"}) | |
| if err != nil { | |
| t.Fatalf("Create failed: %v", err) | |
| } |
| created, _ := uc.Create(ctx, "test-client", domain.ClientTypeConfidential, []string{"http://localhost:3000/callback"}) | ||
|
|
||
| err := uc.Delete(ctx, created.ClientID) | ||
| if err != nil { |
There was a problem hiding this comment.
テスト内で uc.Create のエラーを捨てています。失敗時に created が nil になり後続がパニックする可能性があるため、ここも if err != nil { t.Fatalf(...) } の形で検証してください。
| secret, err := generateSecret() | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| secretHash, err := hashSecret(secret) | ||
| if err != nil { | ||
| return nil, err |
There was a problem hiding this comment.
client_type が public の場合でも無条件にシークレットを生成しています。public client はシークレットを保持しない前提とするなら、confidential のみ生成・保存し、public は client_secret_hash を NULL にする/そもそも public を受け付けない等、型ごとの扱いを明確にしてください。
| secret, err := generateSecret() | |
| if err != nil { | |
| return nil, err | |
| } | |
| secretHash, err := hashSecret(secret) | |
| if err != nil { | |
| return nil, err | |
| var ( | |
| secret string | |
| secretHash string | |
| ) | |
| switch clientType { | |
| case domain.ClientTypeConfidential: | |
| s, err := generateSecret() | |
| if err != nil { | |
| return nil, err | |
| } | |
| h, err := hashSecret(s) | |
| if err != nil { | |
| return nil, err | |
| } | |
| secret = s | |
| secretHash = h | |
| case domain.ClientTypePublic: | |
| // Public clients do not have a client secret. | |
| // Leave secret and secretHash as empty values so that | |
| // the repository can persist a NULL/empty secret hash. | |
| default: | |
| return nil, errors.New("invalid client type") |
There was a problem hiding this comment.
これは確実に対応しておいていただきたいです
#53 で対応されている fosite の実装の都合や、また secret が public client でも必ず返ってしまうため、仕様の範囲外になります
| func (u *clientUseCase) RegenerateSecret(ctx context.Context, clientID uuid.UUID) (string, error) { | ||
| _, err := u.repo.Get(ctx, clientID) | ||
| if err != nil { | ||
| if errors.Is(err, repository.ErrClientNotFound) { | ||
| return "", ErrClientNotFound |
There was a problem hiding this comment.
RegenerateSecret は clientType を確認せず再生成しています。public client を許可する設計なら、public では拒否する(400/409 等)か、secret を保持しない方針に合わせて挙動を定義してください。
| clientRepo := repository.NewClientRepository(queries) | ||
| clientUC := usecase.NewClientUseCase(clientRepo) | ||
| handler := v1.NewHandler(clientUC) | ||
|
|
There was a problem hiding this comment.
ここで admin 系の CRUD ハンドラを組み立てていますが、ルーティング前段の認証・認可ミドルウェアが見当たりません(/api/v1/admin/* が無条件で公開されます)。admin 専用の認証と権限チェックを追加してください。
OAuth2/OIDC における Client の CRUD を /api/v1/admin/client/{clientId} のパスで実装しました。