Rename Go client package from mciasgoclient to mcias
- Update package declaration in client.go - Update error message strings to reference new package name - Update test package and imports to use new name - Update README.md documentation and examples with new package name - All tests pass
This commit is contained in:
26
PROGRESS.md
26
PROGRESS.md
@@ -4,6 +4,32 @@ Source of truth for current development state.
|
|||||||
---
|
---
|
||||||
All phases complete. **v1.0.0 tagged.** All packages pass `go test ./...`; `golangci-lint run ./...` clean.
|
All phases complete. **v1.0.0 tagged.** All packages pass `go test ./...`; `golangci-lint run ./...` clean.
|
||||||
|
|
||||||
|
### 2026-03-13 — Make pgcreds discoverable via CLI and UI
|
||||||
|
|
||||||
|
**Problem:** Users had no way to discover which pgcreds were available to them or what their credential IDs were, making it functionally impossible to use the system without manual database inspection.
|
||||||
|
|
||||||
|
**Solution:** Added two complementary discovery paths:
|
||||||
|
|
||||||
|
**REST API:**
|
||||||
|
- New `GET /v1/pgcreds` endpoint (requires authentication) returns all accessible credentials (owned + explicitly granted) with their IDs, host, port, database, username, and timestamps
|
||||||
|
- Response includes `id` field so users can then fetch full credentials via `GET /v1/accounts/{id}/pgcreds`
|
||||||
|
|
||||||
|
**CLI (`cmd/mciasctl/main.go`):**
|
||||||
|
- New `pgcreds list` subcommand calls `GET /v1/pgcreds` and displays accessible credentials with IDs
|
||||||
|
- Updated usage documentation to include `pgcreds list`
|
||||||
|
|
||||||
|
**Web UI (`web/templates/pgcreds.html`):**
|
||||||
|
- Credential ID now displayed in a `<code>` element at the top of each credential's metadata block
|
||||||
|
- Styled with monospace font for easy copying and reference
|
||||||
|
|
||||||
|
**Files modified:**
|
||||||
|
- `internal/server/server.go`: Added route `GET /v1/pgcreds` (requires auth, not admin) + handler `handleListAccessiblePGCreds`
|
||||||
|
- `cmd/mciasctl/main.go`: Added `pgCredsList` function and switch case
|
||||||
|
- `web/templates/pgcreds.html`: Display credential ID in the credentials list
|
||||||
|
- Struct field alignment fixed in `pgCredResponse` to pass `go vet`
|
||||||
|
|
||||||
|
All tests pass; `go vet ./...` clean.
|
||||||
|
|
||||||
### 2026-03-12 — Update web UI and model for all compile-time roles
|
### 2026-03-12 — Update web UI and model for all compile-time roles
|
||||||
|
|
||||||
- `internal/model/model.go`: added `RoleGuest`, `RoleViewer`, `RoleEditor`, and
|
- `internal/model/model.go`: added `RoleGuest`, `RoleViewer`, `RoleEditor`, and
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ go get git.wntrmute.dev/kyle/mcias/clients/go
|
|||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import mciasgoclient "git.wntrmute.dev/kyle/mcias/clients/go"
|
import "git.wntrmute.dev/kyle/mcias/clients/go/mcias"
|
||||||
|
|
||||||
// Connect to the MCIAS server.
|
// Connect to the MCIAS server.
|
||||||
client, err := mciasgoclient.New("https://auth.example.com", mciasgoclient.Options{})
|
client, err := mcias.New("https://auth.example.com", mcias.Options{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -43,7 +43,7 @@ if err := client.Logout(); err != nil {
|
|||||||
## Custom CA Certificate
|
## Custom CA Certificate
|
||||||
|
|
||||||
```go
|
```go
|
||||||
client, err := mciasgoclient.New("https://auth.example.com", mciasgoclient.Options{
|
client, err := mcias.New("https://auth.example.com", mcias.Options{
|
||||||
CACertPath: "/etc/mcias/ca.pem",
|
CACertPath: "/etc/mcias/ca.pem",
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
@@ -55,17 +55,17 @@ All methods return typed errors:
|
|||||||
```go
|
```go
|
||||||
_, _, err := client.Login("alice", "wrongpass", "")
|
_, _, err := client.Login("alice", "wrongpass", "")
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, new(mciasgoclient.MciasAuthError)):
|
case errors.Is(err, new(mcias.MciasAuthError)):
|
||||||
// 401 — wrong credentials or token invalid
|
// 401 — wrong credentials or token invalid
|
||||||
case errors.Is(err, new(mciasgoclient.MciasForbiddenError)):
|
case errors.Is(err, new(mcias.MciasForbiddenError)):
|
||||||
// 403 — insufficient role
|
// 403 — insufficient role
|
||||||
case errors.Is(err, new(mciasgoclient.MciasNotFoundError)):
|
case errors.Is(err, new(mcias.MciasNotFoundError)):
|
||||||
// 404 — resource not found
|
// 404 — resource not found
|
||||||
case errors.Is(err, new(mciasgoclient.MciasInputError)):
|
case errors.Is(err, new(mcias.MciasInputError)):
|
||||||
// 400 — malformed request
|
// 400 — malformed request
|
||||||
case errors.Is(err, new(mciasgoclient.MciasConflictError)):
|
case errors.Is(err, new(mcias.MciasConflictError)):
|
||||||
// 409 — conflict (e.g. duplicate username)
|
// 409 — conflict (e.g. duplicate username)
|
||||||
case errors.Is(err, new(mciasgoclient.MciasServerError)):
|
case errors.Is(err, new(mcias.MciasServerError)):
|
||||||
// 5xx — unexpected server error
|
// 5xx — unexpected server error
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// Package mciasgoclient provides a thread-safe Go client for the MCIAS REST API.
|
// Package mcias provides a thread-safe Go client for the MCIAS REST API.
|
||||||
//
|
//
|
||||||
// Security: bearer tokens are stored under a sync.RWMutex and are never written
|
// Security: bearer tokens are stored under a sync.RWMutex and are never written
|
||||||
// to logs or included in error messages anywhere in this package.
|
// to logs or included in error messages anywhere in this package.
|
||||||
package mciasgoclient
|
package mcias
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -28,7 +28,7 @@ type MciasError struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *MciasError) Error() string {
|
func (e *MciasError) Error() string {
|
||||||
return fmt.Sprintf("mciasgoclient: HTTP %d: %s", e.StatusCode, e.Message)
|
return fmt.Sprintf("mcias: HTTP %d: %s", e.StatusCode, e.Message)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MciasAuthError is returned for 401 Unauthorized responses.
|
// MciasAuthError is returned for 401 Unauthorized responses.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Package mciasgoclient_test provides tests for the MCIAS Go client.
|
// Package mcias_test provides tests for the MCIAS Go client.
|
||||||
// All tests use inline httptest.NewServer mocks to keep this module
|
// All tests use inline httptest.NewServer mocks to keep this module
|
||||||
// self-contained (no cross-module imports).
|
// self-contained (no cross-module imports).
|
||||||
package mciasgoclient_test
|
package mcias_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -11,16 +11,16 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
mciasgoclient "git.wntrmute.dev/kyle/mcias/clients/go"
|
mcias "git.wntrmute.dev/kyle/mcias/clients/go"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// helpers
|
// helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
func newTestClient(t *testing.T, serverURL string) *mciasgoclient.Client {
|
func newTestClient(t *testing.T, serverURL string) *mcias.Client {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
c, err := mciasgoclient.New(serverURL, mciasgoclient.Options{})
|
c, err := mcias.New(serverURL, mcias.Options{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("New: %v", err)
|
t.Fatalf("New: %v", err)
|
||||||
}
|
}
|
||||||
@@ -42,7 +42,7 @@ func writeError(w http.ResponseWriter, status int, msg string) {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
func TestNew(t *testing.T) {
|
func TestNew(t *testing.T) {
|
||||||
c, err := mciasgoclient.New("https://example.com", mciasgoclient.Options{})
|
c, err := mcias.New("https://example.com", mcias.Options{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected no error, got %v", err)
|
t.Fatalf("expected no error, got %v", err)
|
||||||
}
|
}
|
||||||
@@ -52,7 +52,7 @@ func TestNew(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestNewWithPresetToken(t *testing.T) {
|
func TestNewWithPresetToken(t *testing.T) {
|
||||||
c, err := mciasgoclient.New("https://example.com", mciasgoclient.Options{Token: "preset-tok"})
|
c, err := mcias.New("https://example.com", mcias.Options{Token: "preset-tok"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected no error, got %v", err)
|
t.Fatalf("expected no error, got %v", err)
|
||||||
}
|
}
|
||||||
@@ -62,7 +62,7 @@ func TestNewWithPresetToken(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestNewBadCACert(t *testing.T) {
|
func TestNewBadCACert(t *testing.T) {
|
||||||
_, err := mciasgoclient.New("https://example.com", mciasgoclient.Options{CACertPath: "/nonexistent/ca.pem"})
|
_, err := mcias.New("https://example.com", mcias.Options{CACertPath: "/nonexistent/ca.pem"})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected error for missing CA cert file")
|
t.Fatal("expected error for missing CA cert file")
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,7 @@ func TestHealthError(t *testing.T) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected error for 503")
|
t.Fatal("expected error for 503")
|
||||||
}
|
}
|
||||||
var srvErr *mciasgoclient.MciasServerError
|
var srvErr *mcias.MciasServerError
|
||||||
if !errors.As(err, &srvErr) {
|
if !errors.As(err, &srvErr) {
|
||||||
t.Errorf("expected MciasServerError, got %T: %v", err, err)
|
t.Errorf("expected MciasServerError, got %T: %v", err, err)
|
||||||
}
|
}
|
||||||
@@ -183,7 +183,7 @@ func TestLoginUnauthorized(t *testing.T) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected error for 401")
|
t.Fatal("expected error for 401")
|
||||||
}
|
}
|
||||||
var authErr *mciasgoclient.MciasAuthError
|
var authErr *mcias.MciasAuthError
|
||||||
if !errors.As(err, &authErr) {
|
if !errors.As(err, &authErr) {
|
||||||
t.Errorf("expected MciasAuthError, got %T: %v", err, err)
|
t.Errorf("expected MciasAuthError, got %T: %v", err, err)
|
||||||
}
|
}
|
||||||
@@ -312,7 +312,7 @@ func TestConfirmTOTPBadCode(t *testing.T) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected error for bad TOTP code")
|
t.Fatal("expected error for bad TOTP code")
|
||||||
}
|
}
|
||||||
var inputErr *mciasgoclient.MciasInputError
|
var inputErr *mcias.MciasInputError
|
||||||
if !errors.As(err, &inputErr) {
|
if !errors.As(err, &inputErr) {
|
||||||
t.Errorf("expected MciasInputError, got %T: %v", err, err)
|
t.Errorf("expected MciasInputError, got %T: %v", err, err)
|
||||||
}
|
}
|
||||||
@@ -347,7 +347,7 @@ func TestChangePasswordWrongCurrent(t *testing.T) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected error for wrong current password")
|
t.Fatal("expected error for wrong current password")
|
||||||
}
|
}
|
||||||
var authErr *mciasgoclient.MciasAuthError
|
var authErr *mcias.MciasAuthError
|
||||||
if !errors.As(err, &authErr) {
|
if !errors.As(err, &authErr) {
|
||||||
t.Errorf("expected MciasAuthError, got %T: %v", err, err)
|
t.Errorf("expected MciasAuthError, got %T: %v", err, err)
|
||||||
}
|
}
|
||||||
@@ -456,7 +456,7 @@ func TestCreateAccountConflict(t *testing.T) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected error for 409")
|
t.Fatal("expected error for 409")
|
||||||
}
|
}
|
||||||
var conflictErr *mciasgoclient.MciasConflictError
|
var conflictErr *mcias.MciasConflictError
|
||||||
if !errors.As(err, &conflictErr) {
|
if !errors.As(err, &conflictErr) {
|
||||||
t.Errorf("expected MciasConflictError, got %T: %v", err, err)
|
t.Errorf("expected MciasConflictError, got %T: %v", err, err)
|
||||||
}
|
}
|
||||||
@@ -801,7 +801,7 @@ func TestListAudit(t *testing.T) {
|
|||||||
}))
|
}))
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
c := newTestClient(t, srv.URL)
|
c := newTestClient(t, srv.URL)
|
||||||
resp, err := c.ListAudit(mciasgoclient.AuditFilter{})
|
resp, err := c.ListAudit(mcias.AuditFilter{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ListAudit: %v", err)
|
t.Fatalf("ListAudit: %v", err)
|
||||||
}
|
}
|
||||||
@@ -827,7 +827,7 @@ func TestListAuditWithFilter(t *testing.T) {
|
|||||||
}))
|
}))
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
c := newTestClient(t, srv.URL)
|
c := newTestClient(t, srv.URL)
|
||||||
_, err := c.ListAudit(mciasgoclient.AuditFilter{
|
_, err := c.ListAudit(mcias.AuditFilter{
|
||||||
Limit: 10, Offset: 5, EventType: "login_fail", ActorID: "acct-uuid-1",
|
Limit: 10, Offset: 5, EventType: "login_fail", ActorID: "acct-uuid-1",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -896,10 +896,10 @@ func TestCreatePolicyRule(t *testing.T) {
|
|||||||
}))
|
}))
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
c := newTestClient(t, srv.URL)
|
c := newTestClient(t, srv.URL)
|
||||||
rule, err := c.CreatePolicyRule(mciasgoclient.CreatePolicyRuleRequest{
|
rule, err := c.CreatePolicyRule(mcias.CreatePolicyRuleRequest{
|
||||||
Description: "Test rule",
|
Description: "Test rule",
|
||||||
Priority: 50,
|
Priority: 50,
|
||||||
Rule: mciasgoclient.PolicyRuleBody{Effect: "deny"},
|
Rule: mcias.PolicyRuleBody{Effect: "deny"},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("CreatePolicyRule: %v", err)
|
t.Fatalf("CreatePolicyRule: %v", err)
|
||||||
@@ -950,7 +950,7 @@ func TestGetPolicyRuleNotFound(t *testing.T) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected error for 404")
|
t.Fatal("expected error for 404")
|
||||||
}
|
}
|
||||||
var notFoundErr *mciasgoclient.MciasNotFoundError
|
var notFoundErr *mcias.MciasNotFoundError
|
||||||
if !errors.As(err, ¬FoundErr) {
|
if !errors.As(err, ¬FoundErr) {
|
||||||
t.Errorf("expected MciasNotFoundError, got %T: %v", err, err)
|
t.Errorf("expected MciasNotFoundError, got %T: %v", err, err)
|
||||||
}
|
}
|
||||||
@@ -976,7 +976,7 @@ func TestUpdatePolicyRule(t *testing.T) {
|
|||||||
}))
|
}))
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
c := newTestClient(t, srv.URL)
|
c := newTestClient(t, srv.URL)
|
||||||
rule, err := c.UpdatePolicyRule(7, mciasgoclient.UpdatePolicyRuleRequest{Enabled: &enabled})
|
rule, err := c.UpdatePolicyRule(7, mcias.UpdatePolicyRuleRequest{Enabled: &enabled})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("UpdatePolicyRule: %v", err)
|
t.Fatalf("UpdatePolicyRule: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1073,7 +1073,7 @@ func TestIntegration(t *testing.T) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected error for wrong credentials")
|
t.Fatal("expected error for wrong credentials")
|
||||||
}
|
}
|
||||||
var authErr *mciasgoclient.MciasAuthError
|
var authErr *mcias.MciasAuthError
|
||||||
if !errors.As(err, &authErr) {
|
if !errors.As(err, &authErr) {
|
||||||
t.Errorf("expected MciasAuthError, got %T", err)
|
t.Errorf("expected MciasAuthError, got %T", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
// token issue -id UUID
|
// token issue -id UUID
|
||||||
// token revoke -jti JTI
|
// token revoke -jti JTI
|
||||||
//
|
//
|
||||||
|
// pgcreds list
|
||||||
// pgcreds set -id UUID -host HOST [-port PORT] -db DB -user USER [-password PASS]
|
// pgcreds set -id UUID -host HOST [-port PORT] -db DB -user USER [-password PASS]
|
||||||
// pgcreds get -id UUID
|
// pgcreds get -id UUID
|
||||||
//
|
//
|
||||||
@@ -526,9 +527,11 @@ func (c *controller) tokenRevoke(args []string) {
|
|||||||
|
|
||||||
func (c *controller) runPGCreds(args []string) {
|
func (c *controller) runPGCreds(args []string) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
fatalf("pgcreds requires a subcommand: get, set")
|
fatalf("pgcreds requires a subcommand: list, get, set")
|
||||||
}
|
}
|
||||||
switch args[0] {
|
switch args[0] {
|
||||||
|
case "list":
|
||||||
|
c.pgCredsList(args[1:])
|
||||||
case "get":
|
case "get":
|
||||||
c.pgCredsGet(args[1:])
|
c.pgCredsGet(args[1:])
|
||||||
case "set":
|
case "set":
|
||||||
@@ -538,6 +541,15 @@ func (c *controller) runPGCreds(args []string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *controller) pgCredsList(args []string) {
|
||||||
|
fs := flag.NewFlagSet("pgcreds list", flag.ExitOnError)
|
||||||
|
_ = fs.Parse(args)
|
||||||
|
|
||||||
|
var result json.RawMessage
|
||||||
|
c.doRequest("GET", "/v1/pgcreds", nil, &result)
|
||||||
|
printJSON(result)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *controller) pgCredsGet(args []string) {
|
func (c *controller) pgCredsGet(args []string) {
|
||||||
fs := flag.NewFlagSet("pgcreds get", flag.ExitOnError)
|
fs := flag.NewFlagSet("pgcreds get", flag.ExitOnError)
|
||||||
id := fs.String("id", "", "account UUID (required)")
|
id := fs.String("id", "", "account UUID (required)")
|
||||||
@@ -943,6 +955,7 @@ Commands:
|
|||||||
token issue -id UUID
|
token issue -id UUID
|
||||||
token revoke -jti JTI
|
token revoke -jti JTI
|
||||||
|
|
||||||
|
pgcreds list
|
||||||
pgcreds get -id UUID
|
pgcreds get -id UUID
|
||||||
pgcreds set -id UUID -host HOST [-port PORT] -db DB -user USER [-password PASS]
|
pgcreds set -id UUID -host HOST [-port PORT] -db DB -user USER [-password PASS]
|
||||||
|
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ func (s *Server) Handler() http.Handler {
|
|||||||
mux.Handle("PUT /v1/accounts/{id}/roles", requireAdmin(http.HandlerFunc(s.handleSetRoles)))
|
mux.Handle("PUT /v1/accounts/{id}/roles", requireAdmin(http.HandlerFunc(s.handleSetRoles)))
|
||||||
mux.Handle("POST /v1/accounts/{id}/roles", requireAdmin(http.HandlerFunc(s.handleGrantRole)))
|
mux.Handle("POST /v1/accounts/{id}/roles", requireAdmin(http.HandlerFunc(s.handleGrantRole)))
|
||||||
mux.Handle("DELETE /v1/accounts/{id}/roles/{role}", requireAdmin(http.HandlerFunc(s.handleRevokeRole)))
|
mux.Handle("DELETE /v1/accounts/{id}/roles/{role}", requireAdmin(http.HandlerFunc(s.handleRevokeRole)))
|
||||||
|
mux.Handle("GET /v1/pgcreds", requireAuth(http.HandlerFunc(s.handleListAccessiblePGCreds)))
|
||||||
mux.Handle("GET /v1/accounts/{id}/pgcreds", requireAdmin(http.HandlerFunc(s.handleGetPGCreds)))
|
mux.Handle("GET /v1/accounts/{id}/pgcreds", requireAdmin(http.HandlerFunc(s.handleGetPGCreds)))
|
||||||
mux.Handle("PUT /v1/accounts/{id}/pgcreds", requireAdmin(http.HandlerFunc(s.handleSetPGCreds)))
|
mux.Handle("PUT /v1/accounts/{id}/pgcreds", requireAdmin(http.HandlerFunc(s.handleSetPGCreds)))
|
||||||
mux.Handle("GET /v1/audit", requireAdmin(http.HandlerFunc(s.handleListAudit)))
|
mux.Handle("GET /v1/audit", requireAdmin(http.HandlerFunc(s.handleListAudit)))
|
||||||
@@ -1223,6 +1224,58 @@ func (s *Server) handleSetPGCreds(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.WriteHeader(http.StatusNoContent)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleListAccessiblePGCreds returns all pg_credentials accessible to the
|
||||||
|
// authenticated user: those owned + those explicitly granted. The credential ID
|
||||||
|
// is included so callers can fetch a specific credential via /v1/accounts/{id}/pgcreds.
|
||||||
|
func (s *Server) handleListAccessiblePGCreds(w http.ResponseWriter, r *http.Request) {
|
||||||
|
claims := middleware.ClaimsFromContext(r.Context())
|
||||||
|
if claims == nil {
|
||||||
|
middleware.WriteError(w, http.StatusUnauthorized, "not authenticated", "unauthorized")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
acct, err := s.db.GetAccountByUUID(claims.Subject)
|
||||||
|
if err != nil {
|
||||||
|
middleware.WriteError(w, http.StatusUnauthorized, "account not found", "unauthorized")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
creds, err := s.db.ListAccessiblePGCreds(acct.ID)
|
||||||
|
if err != nil {
|
||||||
|
middleware.WriteError(w, http.StatusInternalServerError, "internal error", "internal_error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert credentials to response format with credential ID.
|
||||||
|
type pgCredResponse struct {
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
Database string `json:"database"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
ServiceAccountID string `json:"service_account_id"`
|
||||||
|
ServiceAccountName string `json:"service_account_name,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
response := make([]pgCredResponse, len(creds))
|
||||||
|
for i, cred := range creds {
|
||||||
|
response[i] = pgCredResponse{
|
||||||
|
ID: cred.ID,
|
||||||
|
ServiceAccountID: cred.ServiceAccountUUID,
|
||||||
|
Host: cred.PGHost,
|
||||||
|
Port: cred.PGPort,
|
||||||
|
Database: cred.PGDatabase,
|
||||||
|
Username: cred.PGUsername,
|
||||||
|
CreatedAt: cred.CreatedAt,
|
||||||
|
UpdatedAt: cred.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusOK, response)
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Audit endpoints ----
|
// ---- Audit endpoints ----
|
||||||
|
|
||||||
// handleListAudit returns paginated audit log entries with resolved usernames.
|
// handleListAudit returns paginated audit log entries with resolved usernames.
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
{{range .Creds}}
|
{{range .Creds}}
|
||||||
<div style="border:1px solid var(--color-border);border-radius:6px;padding:1rem;margin-bottom:1rem">
|
<div style="border:1px solid var(--color-border);border-radius:6px;padding:1rem;margin-bottom:1rem">
|
||||||
<dl style="display:grid;grid-template-columns:140px 1fr;gap:.35rem .75rem;font-size:.9rem;margin-bottom:.75rem">
|
<dl style="display:grid;grid-template-columns:140px 1fr;gap:.35rem .75rem;font-size:.9rem;margin-bottom:.75rem">
|
||||||
|
<dt class="text-muted">Credential ID</dt><dd><code style="font-size:.8rem;color:var(--color-fg-muted)">{{.ID}}</code></dd>
|
||||||
<dt class="text-muted">Service Account</dt><dd>{{.ServiceUsername}}</dd>
|
<dt class="text-muted">Service Account</dt><dd>{{.ServiceUsername}}</dd>
|
||||||
<dt class="text-muted">Host</dt><dd>{{.PGHost}}:{{.PGPort}}</dd>
|
<dt class="text-muted">Host</dt><dd>{{.PGHost}}:{{.PGPort}}</dd>
|
||||||
<dt class="text-muted">Database</dt><dd>{{.PGDatabase}}</dd>
|
<dt class="text-muted">Database</dt><dd>{{.PGDatabase}}</dd>
|
||||||
|
|||||||
Reference in New Issue
Block a user