Files
Agent-System-Backend/internal/store/account_directory.go
T
weichen b9c0a344b6 Implement owner-confirmed account recovery and account controls
Why: Administrator-issued password and login-key paths could take over target accounts, while old credentials and plugin handles ignored current account state.

Evidence: SELF receipts /tmp/iam-u3-backend-all-replay.json, /tmp/iam-u3-backend-final-pg-core.json, /tmp/iam-u3-backend-build.json, and focused HTTP/concurrency receipts in /tmp/iam-u3-*.json.

Impact: Adds trusted pending-account initialization and reissue, preexisting owner-session proof, proposal/rotation/grant transactions, server-side admin search, current Host guards, and legacy HTTP/CLI closure. Production cutover and downstream pins remain pending.

Refs: IAM U3 r1 stages 4-5; Dev feature/iam-u3-account-directory-recovery
2026-10-14 04:20:48 +00:00

92 lines
2.8 KiB
Go

package store
import (
"context"
"database/sql"
"errors"
)
// DirectoryAccountRow is an internal candidate projection. It intentionally
// excludes contact details, notes, credentials and administrator privileges.
type DirectoryAccountRow struct {
UserID string
Owner string
Name string
StaffNo string
Enabled bool
}
// ListDirectoryAccountRows only supplies data to the authorized directory
// service; this method itself is not an authorization boundary.
func (s *Store) ListDirectoryAccountRows() ([]DirectoryAccountRow, error) {
rows, err := s.db.Query(`SELECT id,owner,COALESCE(name,''),staff_no,enabled FROM users ORDER BY owner,id`)
if err != nil {
return nil, err
}
defer rows.Close()
results := []DirectoryAccountRow{}
for rows.Next() {
var row DirectoryAccountRow
if err := rows.Scan(&row.UserID, &row.Owner, &row.Name, &row.StaffNo, &row.Enabled); err != nil {
return nil, err
}
results = append(results, row)
}
return results, rows.Err()
}
// ListAdminAccountsForActor takes a consistent snapshot for the protected
// system.users.view adapter. The actor's current admin/effective state is
// checked under lock before any profile fields are read.
func (s *Store) ListAdminAccountsForActor(actorOwner string) ([]Account, AccountDirectoryRevision, error) {
var tx *sql.Tx
var err error
if s.Dialect() == "postgres" {
tx, err = s.db.BeginTx(context.Background(), &sql.TxOptions{Isolation: sql.LevelRepeatableRead})
} else {
tx, err = s.db.Begin()
}
if err != nil {
return nil, AccountDirectoryRevision{}, err
}
defer tx.Rollback()
locked, err := s.lockAccountUsers(tx, actorOwner)
if errors.Is(err, ErrNotFound) {
return nil, AccountDirectoryRevision{}, ErrAccountForbidden
}
if err != nil {
return nil, AccountDirectoryRevision{}, err
}
actor := locked[actorOwner]
if !actor.Enabled || !actor.IsAdmin {
return nil, AccountDirectoryRevision{}, ErrAccountForbidden
}
rows, err := tx.Query(s.q(`SELECT ` + accountColumns + ` FROM users ORDER BY owner,id`))
if err != nil {
return nil, AccountDirectoryRevision{}, err
}
accounts := []Account{}
for rows.Next() {
account, err := scanAccount(rows)
if err != nil {
rows.Close()
return nil, AccountDirectoryRevision{}, err
}
accounts = append(accounts, account)
}
if err := rows.Err(); err != nil {
rows.Close()
return nil, AccountDirectoryRevision{}, err
}
rows.Close()
var revision AccountDirectoryRevision
if err := tx.QueryRow(`SELECT public_revision,staff_no_revision,private_revision FROM account_directory_revision WHERE id=1`).
Scan(&revision.Public, &revision.StaffNo, &revision.Private); err != nil {
return nil, AccountDirectoryRevision{}, err
}
if err := tx.Commit(); err != nil {
return nil, AccountDirectoryRevision{}, err
}
return accounts, revision, nil
}