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
92 lines
2.8 KiB
Go
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
|
|
}
|