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:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
247
internal/db/pgcred_access.go
Normal file
247
internal/db/pgcred_access.go
Normal 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
|
||||
}
|
||||
@@ -87,18 +87,26 @@ type SystemToken struct {
|
||||
// PGCredential holds Postgres connection details for a system account.
|
||||
// The password is encrypted at rest; PGPassword is only populated after
|
||||
// decryption and must never be logged or included in API responses.
|
||||
//
|
||||
// OwnerID identifies the account permitted to update, delete, and manage
|
||||
// access grants for this credential set. A nil OwnerID means the credential
|
||||
// pre-dates ownership tracking; for backwards compatibility, nil is treated as
|
||||
// unowned (only admins can manage it via the UI).
|
||||
type PGCredential struct {
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
PGHost string `json:"host"`
|
||||
PGDatabase string `json:"database"`
|
||||
PGUsername string `json:"username"`
|
||||
PGPassword string `json:"-"`
|
||||
PGPasswordEnc []byte `json:"-"`
|
||||
PGPasswordNonce []byte `json:"-"`
|
||||
ID int64 `json:"-"`
|
||||
AccountID int64 `json:"-"`
|
||||
PGPort int `json:"port"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
OwnerID *int64 `json:"-"`
|
||||
ServiceAccountUUID string `json:"service_account_uuid,omitempty"`
|
||||
PGUsername string `json:"username"`
|
||||
PGPassword string `json:"-"`
|
||||
ServiceUsername string `json:"service_username,omitempty"`
|
||||
PGDatabase string `json:"database"`
|
||||
PGHost string `json:"host"`
|
||||
PGPasswordEnc []byte `json:"-"`
|
||||
PGPasswordNonce []byte `json:"-"`
|
||||
ID int64 `json:"-"`
|
||||
AccountID int64 `json:"-"`
|
||||
PGPort int `json:"port"`
|
||||
}
|
||||
|
||||
// AuditEvent represents a single entry in the append-only audit log.
|
||||
@@ -141,6 +149,26 @@ const (
|
||||
EventPolicyDeny = "policy_deny"
|
||||
)
|
||||
|
||||
// PGCredAccessGrant records that a specific account has been granted read
|
||||
// access to a pg_credentials set. Only the credential owner can manage
|
||||
// grants; grantees can view connection metadata but never the plaintext
|
||||
// password, and they cannot update or delete the credential set.
|
||||
type PGCredAccessGrant struct {
|
||||
GrantedAt time.Time `json:"granted_at"`
|
||||
GrantedBy *int64 `json:"-"`
|
||||
GranteeUUID string `json:"grantee_id"`
|
||||
GranteeName string `json:"grantee_username"`
|
||||
ID int64 `json:"-"`
|
||||
CredentialID int64 `json:"-"`
|
||||
GranteeID int64 `json:"-"`
|
||||
}
|
||||
|
||||
// Audit event type for pg_credential_access changes.
|
||||
const (
|
||||
EventPGCredAccessGranted = "pgcred_access_granted" //nolint:gosec // G101: audit event type, not a credential
|
||||
EventPGCredAccessRevoked = "pgcred_access_revoked" //nolint:gosec // G101: audit event type, not a credential
|
||||
)
|
||||
|
||||
// PolicyRuleRecord is the database representation of a policy rule.
|
||||
// RuleJSON holds a JSON-encoded policy.RuleBody (all match and effect fields).
|
||||
// The ID, Priority, and Description are stored as dedicated columns.
|
||||
|
||||
@@ -32,7 +32,7 @@ func (u *UIServer) handleAccountsList(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
u.render(w, "accounts", AccountsData{
|
||||
PageData: PageData{CSRFToken: csrfToken},
|
||||
PageData: PageData{CSRFToken: csrfToken, ActorName: u.actorName(r)},
|
||||
Accounts: accounts,
|
||||
})
|
||||
}
|
||||
@@ -132,15 +132,41 @@ func (u *UIServer) handleAccountDetail(w http.ResponseWriter, r *http.Request) {
|
||||
tokens = nil
|
||||
}
|
||||
|
||||
// Resolve the currently logged-in actor.
|
||||
claims := claimsFromContext(r.Context())
|
||||
var actorID *int64
|
||||
if claims != nil {
|
||||
if actor, err := u.db.GetAccountByUUID(claims.Subject); err == nil {
|
||||
actorID = &actor.ID
|
||||
}
|
||||
}
|
||||
|
||||
// Load PG credentials for system accounts only; leave nil for human accounts
|
||||
// and when no credentials have been stored yet.
|
||||
var pgCred *model.PGCredential
|
||||
var pgCredGrants []*model.PGCredAccessGrant
|
||||
var grantableAccounts []*model.Account
|
||||
if acct.AccountType == model.AccountTypeSystem {
|
||||
pgCred, err = u.db.ReadPGCredentials(acct.ID)
|
||||
if err != nil && !errors.Is(err, db.ErrNotFound) {
|
||||
u.logger.Warn("read pg credentials", "error", err)
|
||||
}
|
||||
// ErrNotFound is expected when no credentials have been stored yet.
|
||||
|
||||
// Load access grants; only show management controls when the actor is owner.
|
||||
if pgCred != nil {
|
||||
pgCredGrants, err = u.db.ListPGCredAccess(pgCred.ID)
|
||||
if err != nil {
|
||||
u.logger.Warn("list pg cred access", "error", err)
|
||||
}
|
||||
// Populate the "add grantee" dropdown only for the credential owner.
|
||||
if actorID != nil && pgCred.OwnerID != nil && *pgCred.OwnerID == *actorID {
|
||||
grantableAccounts, err = u.db.ListAccounts()
|
||||
if err != nil {
|
||||
u.logger.Warn("list accounts for pgcred grant", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tags, err := u.db.GetAccountTags(acct.ID)
|
||||
@@ -150,13 +176,16 @@ func (u *UIServer) handleAccountDetail(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
u.render(w, "account_detail", AccountDetailData{
|
||||
PageData: PageData{CSRFToken: csrfToken},
|
||||
Account: acct,
|
||||
Roles: roles,
|
||||
AllRoles: knownRoles,
|
||||
Tokens: tokens,
|
||||
PGCred: pgCred,
|
||||
Tags: tags,
|
||||
PageData: PageData{CSRFToken: csrfToken, ActorName: u.actorName(r)},
|
||||
Account: acct,
|
||||
Roles: roles,
|
||||
AllRoles: knownRoles,
|
||||
Tokens: tokens,
|
||||
PGCred: pgCred,
|
||||
PGCredGrants: pgCredGrants,
|
||||
GrantableAccounts: grantableAccounts,
|
||||
ActorID: actorID,
|
||||
Tags: tags,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -456,18 +485,417 @@ func (u *UIServer) handleSetPGCreds(w http.ResponseWriter, r *http.Request) {
|
||||
pgCred = nil
|
||||
}
|
||||
|
||||
// Security: set the credential owner to the actor on first write so that
|
||||
// subsequent grant/revoke operations can enforce ownership. If no actor
|
||||
// is present (e.g. bootstrap), the owner remains nil.
|
||||
if pgCred != nil && pgCred.OwnerID == nil && actorID != nil {
|
||||
if err := u.db.SetPGCredentialOwner(pgCred.ID, *actorID); err != nil {
|
||||
u.logger.Warn("set pg credential owner", "error", err)
|
||||
} else {
|
||||
pgCred.OwnerID = actorID
|
||||
}
|
||||
}
|
||||
|
||||
// Load existing access grants to re-render the full section.
|
||||
var grants []*model.PGCredAccessGrant
|
||||
if pgCred != nil {
|
||||
grants, err = u.db.ListPGCredAccess(pgCred.ID)
|
||||
if err != nil {
|
||||
u.logger.Warn("list pg cred access after write", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Load non-system accounts available to grant access to.
|
||||
grantableAccounts, err := u.db.ListAccounts()
|
||||
if err != nil {
|
||||
u.logger.Warn("list accounts for pgcred grant", "error", err)
|
||||
}
|
||||
|
||||
csrfToken, err := u.setCSRFCookies(w)
|
||||
if err != nil {
|
||||
csrfToken = ""
|
||||
}
|
||||
|
||||
u.render(w, "pgcreds_form", AccountDetailData{
|
||||
PageData: PageData{CSRFToken: csrfToken},
|
||||
Account: acct,
|
||||
PGCred: pgCred,
|
||||
PageData: PageData{CSRFToken: csrfToken},
|
||||
Account: acct,
|
||||
PGCred: pgCred,
|
||||
PGCredGrants: grants,
|
||||
GrantableAccounts: grantableAccounts,
|
||||
ActorID: actorID,
|
||||
})
|
||||
}
|
||||
|
||||
// handleGrantPGCredAccess grants another account read access to a pg_credentials
|
||||
// set owned by the actor. Only the credential owner may grant access; this is
|
||||
// enforced by comparing the stored owner_id with the logged-in actor.
|
||||
//
|
||||
// Security: ownership is re-verified server-side on every request; the form
|
||||
// field grantee_uuid is looked up in the accounts table (no ID injection).
|
||||
// Audit event EventPGCredAccessGranted is recorded on success.
|
||||
func (u *UIServer) handleGrantPGCredAccess(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxFormBytes)
|
||||
if err := r.ParseForm(); err != nil {
|
||||
u.renderError(w, r, http.StatusBadRequest, "invalid form")
|
||||
return
|
||||
}
|
||||
|
||||
id := r.PathValue("id")
|
||||
acct, err := u.db.GetAccountByUUID(id)
|
||||
if err != nil {
|
||||
u.renderError(w, r, http.StatusNotFound, "account not found")
|
||||
return
|
||||
}
|
||||
if acct.AccountType != model.AccountTypeSystem {
|
||||
u.renderError(w, r, http.StatusBadRequest, "postgres credentials are only available for system accounts")
|
||||
return
|
||||
}
|
||||
|
||||
pgCred, err := u.db.ReadPGCredentials(acct.ID)
|
||||
if err != nil {
|
||||
u.renderError(w, r, http.StatusNotFound, "no credentials stored for this account")
|
||||
return
|
||||
}
|
||||
|
||||
// Resolve the currently logged-in actor.
|
||||
claims := claimsFromContext(r.Context())
|
||||
var actorID *int64
|
||||
if claims != nil {
|
||||
if actor, err := u.db.GetAccountByUUID(claims.Subject); err == nil {
|
||||
actorID = &actor.ID
|
||||
}
|
||||
}
|
||||
|
||||
// Security: only the credential owner may grant access.
|
||||
if actorID == nil || pgCred.OwnerID == nil || *pgCred.OwnerID != *actorID {
|
||||
u.renderError(w, r, http.StatusForbidden, "only the credential owner may grant access")
|
||||
return
|
||||
}
|
||||
|
||||
granteeUUID := strings.TrimSpace(r.FormValue("grantee_uuid"))
|
||||
if granteeUUID == "" {
|
||||
u.renderError(w, r, http.StatusBadRequest, "grantee is required")
|
||||
return
|
||||
}
|
||||
grantee, err := u.db.GetAccountByUUID(granteeUUID)
|
||||
if err != nil {
|
||||
u.renderError(w, r, http.StatusNotFound, "grantee account not found")
|
||||
return
|
||||
}
|
||||
|
||||
if err := u.db.GrantPGCredAccess(pgCred.ID, grantee.ID, actorID); err != nil {
|
||||
u.logger.Error("grant pg cred access", "error", err)
|
||||
u.renderError(w, r, http.StatusInternalServerError, "failed to grant access")
|
||||
return
|
||||
}
|
||||
|
||||
u.writeAudit(r, model.EventPGCredAccessGranted, actorID, &grantee.ID,
|
||||
fmt.Sprintf(`{"credential_id":%d,"grantee":%q}`, pgCred.ID, grantee.UUID))
|
||||
|
||||
// If the caller requested a redirect (e.g. from the /pgcreds page), honour it.
|
||||
if next := r.FormValue("_next"); next == "/pgcreds" {
|
||||
http.Redirect(w, r, "/pgcreds", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
// Re-render the full pgcreds section so the new grant appears.
|
||||
grants, err := u.db.ListPGCredAccess(pgCred.ID)
|
||||
if err != nil {
|
||||
u.logger.Warn("list pg cred access after grant", "error", err)
|
||||
}
|
||||
grantableAccounts, err := u.db.ListAccounts()
|
||||
if err != nil {
|
||||
u.logger.Warn("list accounts for pgcred grant", "error", err)
|
||||
}
|
||||
csrfToken, err := u.setCSRFCookies(w)
|
||||
if err != nil {
|
||||
csrfToken = ""
|
||||
}
|
||||
u.render(w, "pgcreds_form", AccountDetailData{
|
||||
PageData: PageData{CSRFToken: csrfToken},
|
||||
Account: acct,
|
||||
PGCred: pgCred,
|
||||
PGCredGrants: grants,
|
||||
GrantableAccounts: grantableAccounts,
|
||||
ActorID: actorID,
|
||||
})
|
||||
}
|
||||
|
||||
// handleRevokePGCredAccess removes a grantee's read access to a pg_credentials set.
|
||||
// Only the credential owner may revoke grants; this is enforced server-side.
|
||||
//
|
||||
// Security: ownership re-verified on every request. grantee_uuid looked up
|
||||
// in accounts table — not taken from URL path to prevent enumeration.
|
||||
// Audit event EventPGCredAccessRevoked is recorded on success.
|
||||
func (u *UIServer) handleRevokePGCredAccess(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxFormBytes)
|
||||
if err := r.ParseForm(); err != nil {
|
||||
u.renderError(w, r, http.StatusBadRequest, "invalid form")
|
||||
return
|
||||
}
|
||||
|
||||
id := r.PathValue("id")
|
||||
acct, err := u.db.GetAccountByUUID(id)
|
||||
if err != nil {
|
||||
u.renderError(w, r, http.StatusNotFound, "account not found")
|
||||
return
|
||||
}
|
||||
if acct.AccountType != model.AccountTypeSystem {
|
||||
u.renderError(w, r, http.StatusBadRequest, "postgres credentials are only available for system accounts")
|
||||
return
|
||||
}
|
||||
|
||||
pgCred, err := u.db.ReadPGCredentials(acct.ID)
|
||||
if err != nil {
|
||||
u.renderError(w, r, http.StatusNotFound, "no credentials stored for this account")
|
||||
return
|
||||
}
|
||||
|
||||
// Resolve the currently logged-in actor.
|
||||
claims := claimsFromContext(r.Context())
|
||||
var actorID *int64
|
||||
if claims != nil {
|
||||
if actor, err := u.db.GetAccountByUUID(claims.Subject); err == nil {
|
||||
actorID = &actor.ID
|
||||
}
|
||||
}
|
||||
|
||||
// Security: only the credential owner may revoke access.
|
||||
if actorID == nil || pgCred.OwnerID == nil || *pgCred.OwnerID != *actorID {
|
||||
u.renderError(w, r, http.StatusForbidden, "only the credential owner may revoke access")
|
||||
return
|
||||
}
|
||||
|
||||
granteeUUID := r.PathValue("grantee")
|
||||
if granteeUUID == "" {
|
||||
u.renderError(w, r, http.StatusBadRequest, "grantee is required")
|
||||
return
|
||||
}
|
||||
grantee, err := u.db.GetAccountByUUID(granteeUUID)
|
||||
if err != nil {
|
||||
u.renderError(w, r, http.StatusNotFound, "grantee account not found")
|
||||
return
|
||||
}
|
||||
|
||||
if err := u.db.RevokePGCredAccess(pgCred.ID, grantee.ID); err != nil {
|
||||
u.logger.Error("revoke pg cred access", "error", err)
|
||||
u.renderError(w, r, http.StatusInternalServerError, "failed to revoke access")
|
||||
return
|
||||
}
|
||||
|
||||
u.writeAudit(r, model.EventPGCredAccessRevoked, actorID, &grantee.ID,
|
||||
fmt.Sprintf(`{"credential_id":%d,"grantee":%q}`, pgCred.ID, grantee.UUID))
|
||||
|
||||
// If the caller requested a redirect (e.g. from the /pgcreds page), honour it.
|
||||
if r.URL.Query().Get("_next") == "/pgcreds" {
|
||||
if isHTMX(r) {
|
||||
w.Header().Set("HX-Redirect", "/pgcreds")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/pgcreds", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
// Re-render the full pgcreds section with the grant removed.
|
||||
grants, err := u.db.ListPGCredAccess(pgCred.ID)
|
||||
if err != nil {
|
||||
u.logger.Warn("list pg cred access after revoke", "error", err)
|
||||
}
|
||||
grantableAccounts, err := u.db.ListAccounts()
|
||||
if err != nil {
|
||||
u.logger.Warn("list accounts for pgcred grant", "error", err)
|
||||
}
|
||||
csrfToken, err := u.setCSRFCookies(w)
|
||||
if err != nil {
|
||||
csrfToken = ""
|
||||
}
|
||||
u.render(w, "pgcreds_form", AccountDetailData{
|
||||
PageData: PageData{CSRFToken: csrfToken},
|
||||
Account: acct,
|
||||
PGCred: pgCred,
|
||||
PGCredGrants: grants,
|
||||
GrantableAccounts: grantableAccounts,
|
||||
ActorID: actorID,
|
||||
})
|
||||
}
|
||||
|
||||
// handlePGCredsList renders the "My PG Credentials" page, showing all
|
||||
// pg_credentials accessible to the currently logged-in user (owned + granted),
|
||||
// plus a create form for system accounts that have no credentials yet.
|
||||
func (u *UIServer) handlePGCredsList(w http.ResponseWriter, r *http.Request) {
|
||||
csrfToken, err := u.setCSRFCookies(w)
|
||||
if err != nil {
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
claims := claimsFromContext(r.Context())
|
||||
if claims == nil {
|
||||
u.redirectToLogin(w, r)
|
||||
return
|
||||
}
|
||||
actor, err := u.db.GetAccountByUUID(claims.Subject)
|
||||
if err != nil {
|
||||
u.renderError(w, r, http.StatusInternalServerError, "could not resolve actor")
|
||||
return
|
||||
}
|
||||
|
||||
creds, err := u.db.ListAccessiblePGCreds(actor.ID)
|
||||
if err != nil {
|
||||
u.renderError(w, r, http.StatusInternalServerError, "failed to load credentials")
|
||||
return
|
||||
}
|
||||
|
||||
// Build the list of system accounts that have no credentials at all
|
||||
// (not just those absent from this actor's accessible set) so the
|
||||
// create form remains available even when the actor has no existing creds.
|
||||
credAcctIDs, err := u.db.ListCredentialedAccountIDs()
|
||||
if err != nil {
|
||||
u.logger.Warn("list credentialed account ids", "error", err)
|
||||
credAcctIDs = map[int64]struct{}{}
|
||||
}
|
||||
allAccounts, err := u.db.ListAccounts()
|
||||
if err != nil {
|
||||
u.logger.Warn("list accounts for pgcreds create form", "error", err)
|
||||
}
|
||||
var uncredentialed []*model.Account
|
||||
for _, a := range allAccounts {
|
||||
if a.AccountType == model.AccountTypeSystem {
|
||||
if _, hasCredential := credAcctIDs[a.ID]; !hasCredential {
|
||||
uncredentialed = append(uncredentialed, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For each credential owned by the actor, load its access grants so the
|
||||
// /pgcreds page can render inline grant management controls.
|
||||
credGrants := make(map[int64][]*model.PGCredAccessGrant)
|
||||
for _, c := range creds {
|
||||
if c.OwnerID != nil && *c.OwnerID == actor.ID {
|
||||
grants, err := u.db.ListPGCredAccess(c.ID)
|
||||
if err != nil {
|
||||
u.logger.Warn("list pg cred access for owned cred", "cred_id", c.ID, "error", err)
|
||||
continue
|
||||
}
|
||||
credGrants[c.ID] = grants
|
||||
}
|
||||
}
|
||||
|
||||
u.render(w, "pgcreds", PGCredsData{
|
||||
PageData: PageData{CSRFToken: csrfToken, ActorName: u.actorName(r)},
|
||||
Creds: creds,
|
||||
UncredentialedAccounts: uncredentialed,
|
||||
CredGrants: credGrants,
|
||||
AllAccounts: allAccounts,
|
||||
ActorID: &actor.ID,
|
||||
})
|
||||
}
|
||||
|
||||
// handleCreatePGCreds creates a new PG credential set from the /pgcreds page.
|
||||
// The submitter selects a system account from the uncredentialed list and
|
||||
// provides connection details; on success they become the credential owner.
|
||||
//
|
||||
// Security: only system accounts may hold PG credentials; the submitted account
|
||||
// UUID is validated server-side. Password is encrypted with AES-256-GCM before
|
||||
// storage; the plaintext is never logged or included in any response.
|
||||
// Audit event EventPGCredUpdated is recorded on success.
|
||||
func (u *UIServer) handleCreatePGCreds(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxFormBytes)
|
||||
if err := r.ParseForm(); err != nil {
|
||||
u.renderError(w, r, http.StatusBadRequest, "invalid form")
|
||||
return
|
||||
}
|
||||
|
||||
accountUUID := strings.TrimSpace(r.FormValue("account_uuid"))
|
||||
if accountUUID == "" {
|
||||
u.renderError(w, r, http.StatusBadRequest, "account is required")
|
||||
return
|
||||
}
|
||||
acct, err := u.db.GetAccountByUUID(accountUUID)
|
||||
if err != nil {
|
||||
u.renderError(w, r, http.StatusNotFound, "account not found")
|
||||
return
|
||||
}
|
||||
if acct.AccountType != model.AccountTypeSystem {
|
||||
u.renderError(w, r, http.StatusBadRequest, "postgres credentials are only available for system accounts")
|
||||
return
|
||||
}
|
||||
|
||||
host := strings.TrimSpace(r.FormValue("host"))
|
||||
portStr := strings.TrimSpace(r.FormValue("port"))
|
||||
dbName := strings.TrimSpace(r.FormValue("database"))
|
||||
username := strings.TrimSpace(r.FormValue("username"))
|
||||
password := r.FormValue("password")
|
||||
|
||||
if host == "" {
|
||||
u.renderError(w, r, http.StatusBadRequest, "host is required")
|
||||
return
|
||||
}
|
||||
if dbName == "" {
|
||||
u.renderError(w, r, http.StatusBadRequest, "database is required")
|
||||
return
|
||||
}
|
||||
if username == "" {
|
||||
u.renderError(w, r, http.StatusBadRequest, "username is required")
|
||||
return
|
||||
}
|
||||
// Security: password is required on every write — the UI never carries an
|
||||
// existing password, so callers must supply it explicitly.
|
||||
if password == "" {
|
||||
u.renderError(w, r, http.StatusBadRequest, "password is required")
|
||||
return
|
||||
}
|
||||
|
||||
port := 5432
|
||||
if portStr != "" {
|
||||
port, err = strconv.Atoi(portStr)
|
||||
if err != nil || port < 1 || port > 65535 {
|
||||
u.renderError(w, r, http.StatusBadRequest, "port must be an integer between 1 and 65535")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Security: encrypt with AES-256-GCM; fresh nonce per call.
|
||||
enc, nonce, err := crypto.SealAESGCM(u.masterKey, []byte(password))
|
||||
if err != nil {
|
||||
u.logger.Error("encrypt pg password", "error", err)
|
||||
u.renderError(w, r, http.StatusInternalServerError, "internal error")
|
||||
return
|
||||
}
|
||||
|
||||
if err := u.db.WritePGCredentials(acct.ID, host, port, dbName, username, enc, nonce); err != nil {
|
||||
u.logger.Error("write pg credentials", "error", err)
|
||||
u.renderError(w, r, http.StatusInternalServerError, "failed to save credentials")
|
||||
return
|
||||
}
|
||||
|
||||
claims := claimsFromContext(r.Context())
|
||||
var actorID *int64
|
||||
if claims != nil {
|
||||
if actor, err := u.db.GetAccountByUUID(claims.Subject); err == nil {
|
||||
actorID = &actor.ID
|
||||
}
|
||||
}
|
||||
u.writeAudit(r, model.EventPGCredUpdated, actorID, &acct.ID, "")
|
||||
|
||||
// Security: set the credential owner to the actor on creation.
|
||||
pgCred, err := u.db.ReadPGCredentials(acct.ID)
|
||||
if err != nil {
|
||||
u.logger.Warn("re-read pg credentials after create", "error", err)
|
||||
}
|
||||
if pgCred != nil && pgCred.OwnerID == nil && actorID != nil {
|
||||
if err := u.db.SetPGCredentialOwner(pgCred.ID, *actorID); err != nil {
|
||||
u.logger.Warn("set pg credential owner", "error", err)
|
||||
} else {
|
||||
pgCred.OwnerID = actorID
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect to the pgcreds list so the new entry appears in context.
|
||||
http.Redirect(w, r, "/pgcreds", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// handleIssueSystemToken issues a long-lived service token for a system account.
|
||||
func (u *UIServer) handleIssueSystemToken(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
|
||||
@@ -86,7 +86,7 @@ func (u *UIServer) handleAuditDetail(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
u.render(w, "audit_detail", AuditDetailData{
|
||||
PageData: PageData{CSRFToken: csrfToken},
|
||||
PageData: PageData{CSRFToken: csrfToken, ActorName: u.actorName(r)},
|
||||
Event: event,
|
||||
})
|
||||
}
|
||||
@@ -116,7 +116,7 @@ func (u *UIServer) buildAuditData(r *http.Request, page int, csrfToken string) (
|
||||
}
|
||||
|
||||
return AuditData{
|
||||
PageData: PageData{CSRFToken: csrfToken},
|
||||
PageData: PageData{CSRFToken: csrfToken, ActorName: u.actorName(r)},
|
||||
Events: events,
|
||||
EventTypes: auditEventTypes,
|
||||
FilterType: filterType,
|
||||
|
||||
@@ -37,7 +37,7 @@ func (u *UIServer) handleDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
u.render(w, "dashboard", DashboardData{
|
||||
PageData: PageData{CSRFToken: csrfToken},
|
||||
PageData: PageData{CSRFToken: csrfToken, ActorName: u.actorName(r)},
|
||||
TotalAccounts: total,
|
||||
ActiveAccounts: active,
|
||||
RecentEvents: events,
|
||||
|
||||
@@ -60,7 +60,7 @@ func (u *UIServer) handlePoliciesPage(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
data := PoliciesData{
|
||||
PageData: PageData{CSRFToken: csrfToken},
|
||||
PageData: PageData{CSRFToken: csrfToken, ActorName: u.actorName(r)},
|
||||
Rules: views,
|
||||
AllActions: allActionStrings,
|
||||
}
|
||||
|
||||
@@ -141,6 +141,22 @@ func New(database *db.DB, cfg *config.Config, priv ed25519.PrivateKey, pub ed255
|
||||
return false
|
||||
},
|
||||
"not": func(b bool) bool { return !b },
|
||||
// derefInt64 safely dereferences a *int64, returning 0 for nil.
|
||||
// Used in templates to compare owner IDs without triggering nil panics.
|
||||
"derefInt64": func(p *int64) int64 {
|
||||
if p == nil {
|
||||
return 0
|
||||
}
|
||||
return *p
|
||||
},
|
||||
// isPGCredOwner returns true when actorID and cred are both non-nil
|
||||
// and actorID matches cred.OwnerID. Safe to call with nil arguments.
|
||||
"isPGCredOwner": func(actorID *int64, cred *model.PGCredential) bool {
|
||||
if actorID == nil || cred == nil || cred.OwnerID == nil {
|
||||
return false
|
||||
}
|
||||
return *actorID == *cred.OwnerID
|
||||
},
|
||||
"add": func(a, b int) int { return a + b },
|
||||
"sub": func(a, b int) int { return a - b },
|
||||
"gt": func(a, b int) bool { return a > b },
|
||||
@@ -190,6 +206,7 @@ func New(database *db.DB, cfg *config.Config, priv ed25519.PrivateKey, pub ed255
|
||||
"audit": "templates/audit.html",
|
||||
"audit_detail": "templates/audit_detail.html",
|
||||
"policies": "templates/policies.html",
|
||||
"pgcreds": "templates/pgcreds.html",
|
||||
}
|
||||
tmpls := make(map[string]*template.Template, len(pageFiles))
|
||||
for name, file := range pageFiles {
|
||||
@@ -264,6 +281,10 @@ func (u *UIServer) Register(mux *http.ServeMux) {
|
||||
uiMux.Handle("DELETE /token/{jti}", admin(u.handleRevokeToken))
|
||||
uiMux.Handle("POST /accounts/{id}/token", admin(u.handleIssueSystemToken))
|
||||
uiMux.Handle("PUT /accounts/{id}/pgcreds", admin(u.handleSetPGCreds))
|
||||
uiMux.Handle("POST /accounts/{id}/pgcreds/access", admin(u.handleGrantPGCredAccess))
|
||||
uiMux.Handle("DELETE /accounts/{id}/pgcreds/access/{grantee}", admin(u.handleRevokePGCredAccess))
|
||||
uiMux.Handle("GET /pgcreds", adminGet(u.handlePGCredsList))
|
||||
uiMux.Handle("POST /pgcreds", admin(u.handleCreatePGCreds))
|
||||
uiMux.Handle("GET /audit", adminGet(u.handleAuditPage))
|
||||
uiMux.Handle("GET /audit/rows", adminGet(u.handleAuditRows))
|
||||
uiMux.Handle("GET /audit/{id}", adminGet(u.handleAuditDetail))
|
||||
@@ -478,6 +499,21 @@ func clientIP(r *http.Request) string {
|
||||
return addr
|
||||
}
|
||||
|
||||
// actorName resolves the username of the currently authenticated user from the
|
||||
// request context. Returns an empty string if claims are absent or the account
|
||||
// cannot be found; callers should treat an empty string as "not logged in".
|
||||
func (u *UIServer) actorName(r *http.Request) string {
|
||||
claims := claimsFromContext(r.Context())
|
||||
if claims == nil {
|
||||
return ""
|
||||
}
|
||||
acct, err := u.db.GetAccountByUUID(claims.Subject)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return acct.Username
|
||||
}
|
||||
|
||||
// ---- Page data types ----
|
||||
|
||||
// PageData is embedded in all page-level view structs.
|
||||
@@ -485,6 +521,9 @@ type PageData struct {
|
||||
CSRFToken string
|
||||
Flash string
|
||||
Error string
|
||||
// ActorName is the username of the currently logged-in user, populated by
|
||||
// handlers so the base template can display it in the navigation bar.
|
||||
ActorName string
|
||||
}
|
||||
|
||||
// LoginData is the view model for the login page.
|
||||
@@ -514,7 +553,16 @@ type AccountsData struct {
|
||||
// AccountDetailData is the view model for the account detail page.
|
||||
type AccountDetailData struct {
|
||||
Account *model.Account
|
||||
PGCred *model.PGCredential // nil if none stored or account is not a system account
|
||||
// PGCred is nil if none stored or the account is not a system account.
|
||||
PGCred *model.PGCredential
|
||||
// PGCredGrants lists accounts that have been granted read access to PGCred.
|
||||
// Only populated when the viewing actor is the credential owner.
|
||||
PGCredGrants []*model.PGCredAccessGrant
|
||||
// GrantableAccounts is the list of accounts the owner may grant access to.
|
||||
GrantableAccounts []*model.Account
|
||||
// ActorID is the DB id of the currently logged-in user; used in templates
|
||||
// to decide whether to show the owner-only management controls.
|
||||
ActorID *int64
|
||||
PageData
|
||||
Roles []string
|
||||
AllRoles []string
|
||||
@@ -556,3 +604,21 @@ type PoliciesData struct {
|
||||
Rules []*PolicyRuleView
|
||||
AllActions []string
|
||||
}
|
||||
|
||||
// PGCredsData is the view model for the "My PG Credentials" list page.
|
||||
// It shows all pg_credentials sets accessible to the currently logged-in user:
|
||||
// those they own and those they have been granted access to.
|
||||
// UncredentialedAccounts is the list of system accounts that have no credentials
|
||||
// yet, populated to drive the "New Credentials" create form on the same page.
|
||||
// CredGrants maps credential ID to the list of access grants for that credential;
|
||||
// only populated for credentials the actor owns.
|
||||
// AllAccounts is used to populate the grant-access dropdown for owned credentials.
|
||||
// ActorID is the DB id of the currently logged-in user.
|
||||
type PGCredsData struct {
|
||||
CredGrants map[int64][]*model.PGCredAccessGrant
|
||||
ActorID *int64
|
||||
PageData
|
||||
Creds []*model.PGCredential
|
||||
UncredentialedAccounts []*model.Account
|
||||
AllAccounts []*model.Account
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user