Move SSO clients from config to database

- Add sso_clients table (migration 000010) with client_id, redirect_uri,
  tags (JSON), enabled flag, and audit timestamps
- Add SSOClient model struct and audit events
- Implement DB CRUD with 10 unit tests
- Add REST API: GET/POST/PATCH/DELETE /v1/sso/clients (policy-gated)
- Add gRPC SSOClientService with 5 RPCs (admin-only)
- Add mciasctl sso list/create/get/update/delete commands
- Add web UI admin page at /sso-clients with HTMX create/toggle/delete
- Migrate handleSSOAuthorize and handleSSOTokenExchange to use DB
- Remove SSOConfig, SSOClient struct, lookup methods from config
- Simplify: client_id = service_name for policy evaluation

Security:
- SSO client CRUD is admin-only (policy-gated REST, requireAdmin gRPC)
- redirect_uri must use https:// (validated at DB layer)
- Disabled clients are rejected at both authorize and token exchange
- All mutations write audit events

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-31 23:47:53 -07:00
parent 4430ce38a4
commit df7773229c
24 changed files with 2284 additions and 217 deletions

View File

@@ -6,5 +6,5 @@
//
// Prerequisites: protoc, protoc-gen-go, protoc-gen-go-grpc must be in PATH.
//
//go:generate protoc --proto_path=../proto --go_out=../gen --go_opt=paths=source_relative --go-grpc_out=../gen --go-grpc_opt=paths=source_relative mcias/v1/common.proto mcias/v1/admin.proto mcias/v1/auth.proto mcias/v1/token.proto mcias/v1/account.proto mcias/v1/policy.proto
//go:generate protoc --proto_path=../proto --go_out=../gen --go_opt=paths=source_relative --go-grpc_out=../gen --go-grpc_opt=paths=source_relative mcias/v1/common.proto mcias/v1/admin.proto mcias/v1/auth.proto mcias/v1/token.proto mcias/v1/account.proto mcias/v1/policy.proto mcias/v1/sso_client.proto
package proto

View File

@@ -0,0 +1,86 @@
// SSOClientService: CRUD management of SSO client registrations.
syntax = "proto3";
package mcias.v1;
option go_package = "git.wntrmute.dev/mc/mcias/gen/mcias/v1;mciasv1";
// SSOClient is the wire representation of an SSO client registration.
message SSOClient {
string client_id = 1;
string redirect_uri = 2;
repeated string tags = 3;
bool enabled = 4;
string created_at = 5; // RFC3339
string updated_at = 6; // RFC3339
}
// --- List ---
message ListSSOClientsRequest {}
message ListSSOClientsResponse {
repeated SSOClient clients = 1;
}
// --- Create ---
message CreateSSOClientRequest {
string client_id = 1;
string redirect_uri = 2;
repeated string tags = 3;
}
message CreateSSOClientResponse {
SSOClient client = 1;
}
// --- Get ---
message GetSSOClientRequest {
string client_id = 1;
}
message GetSSOClientResponse {
SSOClient client = 1;
}
// --- Update ---
message UpdateSSOClientRequest {
string client_id = 1;
optional string redirect_uri = 2;
repeated string tags = 3;
optional bool enabled = 4;
bool update_tags = 5; // when true, tags field is applied (allows clearing)
}
message UpdateSSOClientResponse {
SSOClient client = 1;
}
// --- Delete ---
message DeleteSSOClientRequest {
string client_id = 1;
}
message DeleteSSOClientResponse {}
// SSOClientService manages SSO client registrations (admin only).
service SSOClientService {
// ListSSOClients returns all registered SSO clients.
rpc ListSSOClients(ListSSOClientsRequest) returns (ListSSOClientsResponse);
// CreateSSOClient registers a new SSO client.
rpc CreateSSOClient(CreateSSOClientRequest) returns (CreateSSOClientResponse);
// GetSSOClient returns a single SSO client by client_id.
rpc GetSSOClient(GetSSOClientRequest) returns (GetSSOClientResponse);
// UpdateSSOClient applies a partial update to an SSO client.
rpc UpdateSSOClient(UpdateSSOClientRequest) returns (UpdateSSOClientResponse);
// DeleteSSOClient removes an SSO client registration.
rpc DeleteSSOClient(DeleteSSOClientRequest) returns (DeleteSSOClientResponse);
}