UI: pgcreds create button; show logged-in user

* web/templates/pgcreds.html: New Credentials card is now always
  rendered; Add Credentials toggle button reveals the create form
  (hidden by default). Shows a message when all system accounts
  already have credentials. Previously the card was hidden when
  UncredentialedAccounts was empty.
* internal/ui/ui.go: added ActorName string field to PageData;
  added actorName(r) helper resolving username from JWT claims
  via DB lookup, returns empty string if unauthenticated.
* internal/ui/handlers_*.go: all full-page PageData constructors
  now pass ActorName: u.actorName(r).
* web/templates/base.html: nav bar renders actor username as a
  muted label before the Logout button when logged in.
* web/static/style.css: added .nav-actor rule (muted grey, 0.85rem).
This commit is contained in:
2026-03-12 11:38:57 -07:00
parent bbf9f6fe3f
commit b2f2f04646
19 changed files with 1152 additions and 49 deletions

View File

@@ -450,16 +450,17 @@ func (db *DB) WritePGCredentials(accountID int64, host string, port int, dbName,
func (db *DB) ReadPGCredentials(accountID int64) (*model.PGCredential, error) {
var cred model.PGCredential
var createdAtStr, updatedAtStr string
var ownerID sql.NullInt64
err := db.sql.QueryRow(`
SELECT id, account_id, pg_host, pg_port, pg_database, pg_username,
pg_password_enc, pg_password_nonce, created_at, updated_at
pg_password_enc, pg_password_nonce, created_at, updated_at, owner_id
FROM pg_credentials WHERE account_id = ?
`, accountID).Scan(
&cred.ID, &cred.AccountID, &cred.PGHost, &cred.PGPort,
&cred.PGDatabase, &cred.PGUsername,
&cred.PGPasswordEnc, &cred.PGPasswordNonce,
&createdAtStr, &updatedAtStr,
&createdAtStr, &updatedAtStr, &ownerID,
)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
@@ -476,6 +477,10 @@ func (db *DB) ReadPGCredentials(accountID int64) (*model.PGCredential, error) {
if err != nil {
return nil, err
}
if ownerID.Valid {
v := ownerID.Int64
cred.OwnerID = &v
}
return &cred, nil
}

View File

@@ -162,6 +162,35 @@ CREATE TABLE IF NOT EXISTS policy_rules (
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
);
`,
},
{
id: 5,
sql: `
-- Track which accounts own each set of pg_credentials and which other
-- accounts have been granted read access to them.
--
-- owner_id: the account that administers the credentials and may grant/revoke
-- access. Defaults to the system account itself. This column is
-- nullable so that rows created before migration 5 are not broken.
ALTER TABLE pg_credentials ADD COLUMN owner_id INTEGER REFERENCES accounts(id);
-- pg_credential_access records an explicit "all-or-nothing" read grant from
-- the credential owner to another account. Grantees may view connection
-- metadata (host, port, database, username) but the password is never
-- decrypted for them in the UI. Only the owner may update or delete the
-- credential set.
CREATE TABLE IF NOT EXISTS pg_credential_access (
id INTEGER PRIMARY KEY,
credential_id INTEGER NOT NULL REFERENCES pg_credentials(id) ON DELETE CASCADE,
grantee_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
granted_by INTEGER REFERENCES accounts(id),
granted_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
UNIQUE (credential_id, grantee_id)
);
CREATE INDEX IF NOT EXISTS idx_pgcred_access_cred ON pg_credential_access (credential_id);
CREATE INDEX IF NOT EXISTS idx_pgcred_access_grantee ON pg_credential_access (grantee_id);
`,
},
}

View File

@@ -0,0 +1,247 @@
package db
import (
"database/sql"
"errors"
"fmt"
"time"
"git.wntrmute.dev/kyle/mcias/internal/model"
)
// ListCredentialedAccountIDs returns the set of account IDs that already have
// a pg_credentials row. Used to filter the "uncredentialed system accounts"
// list on the /pgcreds create form without leaking credential content.
func (db *DB) ListCredentialedAccountIDs() (map[int64]struct{}, error) {
rows, err := db.sql.Query(`SELECT account_id FROM pg_credentials`)
if err != nil {
return nil, fmt.Errorf("db: list credentialed account ids: %w", err)
}
defer func() { _ = rows.Close() }()
ids := make(map[int64]struct{})
for rows.Next() {
var id int64
if err := rows.Scan(&id); err != nil {
return nil, fmt.Errorf("db: scan credentialed account id: %w", err)
}
ids[id] = struct{}{}
}
return ids, rows.Err()
}
// SetPGCredentialOwner records the owning account for a pg_credentials row.
// This is called on first write so that pre-migration rows retain a nil owner.
// It is idempotent: if the owner is already set it is overwritten.
func (db *DB) SetPGCredentialOwner(credentialID, ownerID int64) error {
_, err := db.sql.Exec(`
UPDATE pg_credentials SET owner_id = ? WHERE id = ?
`, ownerID, credentialID)
if err != nil {
return fmt.Errorf("db: set pg credential owner: %w", err)
}
return nil
}
// GetPGCredentialByID retrieves a single pg_credentials row by its primary key.
// Returns ErrNotFound if no such credential exists.
func (db *DB) GetPGCredentialByID(id int64) (*model.PGCredential, error) {
var cred model.PGCredential
var createdAtStr, updatedAtStr string
var ownerID sql.NullInt64
err := db.sql.QueryRow(`
SELECT p.id, p.account_id, p.pg_host, p.pg_port, p.pg_database, p.pg_username,
p.pg_password_enc, p.pg_password_nonce, p.created_at, p.updated_at, p.owner_id
FROM pg_credentials p WHERE p.id = ?
`, id).Scan(
&cred.ID, &cred.AccountID, &cred.PGHost, &cred.PGPort,
&cred.PGDatabase, &cred.PGUsername,
&cred.PGPasswordEnc, &cred.PGPasswordNonce,
&createdAtStr, &updatedAtStr, &ownerID,
)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
if err != nil {
return nil, fmt.Errorf("db: get pg credential by id: %w", err)
}
cred.CreatedAt, err = parseTime(createdAtStr)
if err != nil {
return nil, err
}
cred.UpdatedAt, err = parseTime(updatedAtStr)
if err != nil {
return nil, err
}
if ownerID.Valid {
v := ownerID.Int64
cred.OwnerID = &v
}
return &cred, nil
}
// GrantPGCredAccess grants an account read access to a pg_credentials set.
// If the grant already exists the call is a no-op (UNIQUE constraint).
// grantedBy may be nil if the grant is made programmatically.
func (db *DB) GrantPGCredAccess(credentialID, granteeID int64, grantedBy *int64) error {
n := now()
_, err := db.sql.Exec(`
INSERT INTO pg_credential_access (credential_id, grantee_id, granted_by, granted_at)
VALUES (?, ?, ?, ?)
ON CONFLICT(credential_id, grantee_id) DO NOTHING
`, credentialID, granteeID, grantedBy, n)
if err != nil {
return fmt.Errorf("db: grant pg cred access: %w", err)
}
return nil
}
// RevokePGCredAccess removes a grantee's access to a pg_credentials set.
func (db *DB) RevokePGCredAccess(credentialID, granteeID int64) error {
_, err := db.sql.Exec(`
DELETE FROM pg_credential_access WHERE credential_id = ? AND grantee_id = ?
`, credentialID, granteeID)
if err != nil {
return fmt.Errorf("db: revoke pg cred access: %w", err)
}
return nil
}
// ListPGCredAccess returns all access grants for a pg_credentials set,
// joining against accounts to populate grantee username and UUID.
func (db *DB) ListPGCredAccess(credentialID int64) ([]*model.PGCredAccessGrant, error) {
rows, err := db.sql.Query(`
SELECT pca.id, pca.credential_id, pca.grantee_id, pca.granted_by, pca.granted_at,
a.uuid, a.username
FROM pg_credential_access pca
JOIN accounts a ON a.id = pca.grantee_id
WHERE pca.credential_id = ?
ORDER BY pca.granted_at ASC
`, credentialID)
if err != nil {
return nil, fmt.Errorf("db: list pg cred access: %w", err)
}
defer func() { _ = rows.Close() }()
var grants []*model.PGCredAccessGrant
for rows.Next() {
g, err := scanPGCredAccessGrant(rows)
if err != nil {
return nil, err
}
grants = append(grants, g)
}
return grants, rows.Err()
}
// CheckPGCredAccess reports whether accountID has an explicit access grant for
// credentialID. The credential owner always has access implicitly; callers
// must check ownership separately.
func (db *DB) CheckPGCredAccess(credentialID, accountID int64) (bool, error) {
var count int
err := db.sql.QueryRow(`
SELECT COUNT(*) FROM pg_credential_access
WHERE credential_id = ? AND grantee_id = ?
`, credentialID, accountID).Scan(&count)
if err != nil {
return false, fmt.Errorf("db: check pg cred access: %w", err)
}
return count > 0, nil
}
// PGCredWithAccount extends PGCredential with the owning system account's
// username, used for the "My PG Credentials" listing view.
type PGCredWithAccount struct {
model.PGCredential
}
// ListAccessiblePGCreds returns all pg_credentials rows that accountID may
// view: those where accountID is the owner, plus those where an explicit
// access grant exists. The ServiceUsername and ServiceAccountUUID fields are
// populated from the owning system account for display and navigation.
func (db *DB) ListAccessiblePGCreds(accountID int64) ([]*model.PGCredential, error) {
rows, err := db.sql.Query(`
SELECT p.id, p.account_id, p.pg_host, p.pg_port, p.pg_database, p.pg_username,
p.pg_password_enc, p.pg_password_nonce, p.created_at, p.updated_at, p.owner_id,
a.username, a.uuid
FROM pg_credentials p
JOIN accounts a ON a.id = p.account_id
WHERE p.owner_id = ?
OR EXISTS (
SELECT 1 FROM pg_credential_access pca
WHERE pca.credential_id = p.id AND pca.grantee_id = ?
)
ORDER BY a.username ASC
`, accountID, accountID)
if err != nil {
return nil, fmt.Errorf("db: list accessible pg creds: %w", err)
}
defer func() { _ = rows.Close() }()
var creds []*model.PGCredential
for rows.Next() {
cred, err := scanPGCredWithUsername(rows)
if err != nil {
return nil, err
}
creds = append(creds, cred)
}
return creds, rows.Err()
}
func scanPGCredWithUsername(rows *sql.Rows) (*model.PGCredential, error) {
var cred model.PGCredential
var createdAtStr, updatedAtStr string
var ownerID sql.NullInt64
err := rows.Scan(
&cred.ID, &cred.AccountID, &cred.PGHost, &cred.PGPort,
&cred.PGDatabase, &cred.PGUsername,
&cred.PGPasswordEnc, &cred.PGPasswordNonce,
&createdAtStr, &updatedAtStr, &ownerID,
&cred.ServiceUsername, &cred.ServiceAccountUUID,
)
if err != nil {
return nil, fmt.Errorf("db: scan pg cred with username: %w", err)
}
cred.CreatedAt, err = parseTime(createdAtStr)
if err != nil {
return nil, err
}
cred.UpdatedAt, err = parseTime(updatedAtStr)
if err != nil {
return nil, err
}
if ownerID.Valid {
v := ownerID.Int64
cred.OwnerID = &v
}
return &cred, nil
}
func scanPGCredAccessGrant(rows *sql.Rows) (*model.PGCredAccessGrant, error) {
var g model.PGCredAccessGrant
var grantedAtStr string
var grantedBy sql.NullInt64
err := rows.Scan(
&g.ID, &g.CredentialID, &g.GranteeID, &grantedBy, &grantedAtStr,
&g.GranteeUUID, &g.GranteeName,
)
if err != nil {
return nil, fmt.Errorf("db: scan pg cred access grant: %w", err)
}
g.GrantedAt, err = time.Parse("2006-01-02T15:04:05Z", grantedAtStr)
if err != nil {
return nil, fmt.Errorf("db: parse pg cred access grant time %q: %w", grantedAtStr, err)
}
if grantedBy.Valid {
v := grantedBy.Int64
g.GrantedBy = &v
}
return &g, nil
}