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:
@@ -15,6 +15,7 @@
|
||||
{{if .IsAdmin}}<li><a href="/accounts">Accounts</a></li>
|
||||
<li><a href="/audit">Audit</a></li>
|
||||
<li><a href="/policies">Policies</a></li>
|
||||
<li><a href="/sso-clients">SSO Clients</a></li>
|
||||
<li><a href="/pgcreds">PG Creds</a></li>{{else}}<li><a href="/service-accounts">Service Accounts</a></li>{{end}}
|
||||
{{if .ActorName}}<li><a href="/profile">{{.ActorName}}</a></li>{{end}}
|
||||
<li><form method="POST" action="/logout" style="margin:0"><button class="btn btn-sm btn-secondary" type="submit">Logout</button></form></li>
|
||||
|
||||
31
web/templates/fragments/sso_client_row.html
Normal file
31
web/templates/fragments/sso_client_row.html
Normal file
@@ -0,0 +1,31 @@
|
||||
{{define "sso_client_row"}}
|
||||
<tr id="sso-client-row-{{.ClientID}}">
|
||||
<td><strong>{{.ClientID}}</strong></td>
|
||||
<td><code style="font-size:0.85em">{{.RedirectURI}}</code></td>
|
||||
<td>{{range .Tags}}<span class="badge">{{.}}</span> {{end}}</td>
|
||||
<td>
|
||||
{{if .Enabled}}<span class="badge badge-active">enabled</span>
|
||||
{{else}}<span class="badge badge-inactive">disabled</span>{{end}}
|
||||
</td>
|
||||
<td class="d-flex gap-1">
|
||||
{{if .Enabled}}
|
||||
<button class="btn btn-sm btn-secondary"
|
||||
hx-patch="/sso-clients/{{.ClientID}}/toggle"
|
||||
hx-vals='{"enabled":"false"}'
|
||||
hx-target="#sso-client-row-{{.ClientID}}"
|
||||
hx-swap="outerHTML">Disable</button>
|
||||
{{else}}
|
||||
<button class="btn btn-sm btn-primary"
|
||||
hx-patch="/sso-clients/{{.ClientID}}/toggle"
|
||||
hx-vals='{"enabled":"true"}'
|
||||
hx-target="#sso-client-row-{{.ClientID}}"
|
||||
hx-swap="outerHTML">Enable</button>
|
||||
{{end}}
|
||||
<button class="btn btn-sm btn-danger"
|
||||
hx-delete="/sso-clients/{{.ClientID}}"
|
||||
hx-target="#sso-client-row-{{.ClientID}}"
|
||||
hx-swap="outerHTML"
|
||||
hx-confirm="Delete SSO client '{{.ClientID}}'? This cannot be undone.">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
53
web/templates/sso_clients.html
Normal file
53
web/templates/sso_clients.html
Normal file
@@ -0,0 +1,53 @@
|
||||
{{define "title"}} - SSO Clients{{end}}
|
||||
{{define "content"}}
|
||||
<div class="page-header d-flex justify-between align-center">
|
||||
<div>
|
||||
<h2>SSO Clients</h2>
|
||||
<p class="text-muted text-small">Registered applications that use MCIAS for single sign-on.</p>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="var f=document.getElementById('create-form');f.hidden=!f.hidden;this.textContent=f.hidden?'Add Client':'Cancel'">Add Client</button>
|
||||
</div>
|
||||
|
||||
<div id="create-form" class="card mt-2" hidden>
|
||||
<div class="card-title">Register SSO Client</div>
|
||||
<form hx-post="/sso-clients" hx-target="#sso-clients-tbody" hx-swap="afterbegin">
|
||||
<div class="d-flex gap-1" style="flex-wrap:wrap">
|
||||
<div class="form-group" style="flex:1;min-width:200px">
|
||||
<label class="form-label">Client ID / Service Name</label>
|
||||
<input class="form-control" type="text" name="client_id" required placeholder="e.g. mcr">
|
||||
</div>
|
||||
<div class="form-group" style="flex:2;min-width:300px">
|
||||
<label class="form-label">Redirect URI</label>
|
||||
<input class="form-control" type="url" name="redirect_uri" required placeholder="https://service.example.com/sso/callback">
|
||||
</div>
|
||||
<div class="form-group" style="flex:1;min-width:200px">
|
||||
<label class="form-label">Tags <span class="text-muted text-small">(comma-separated)</span></label>
|
||||
<input class="form-control" type="text" name="tags" placeholder="env:prod,tier:web">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" type="submit">Create</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Client ID</th>
|
||||
<th>Redirect URI</th>
|
||||
<th>Tags</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="sso-clients-tbody">
|
||||
{{range .Clients}}
|
||||
{{template "sso_client_row" .}}
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{if not .Clients}}<p class="text-muted" style="text-align:center;padding:2rem">No SSO clients registered.</p>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user