Files
Agent-System-Backend/internal/store/owner_initialization_audit.go
T
weichen ff6b44bc32 Integrate U3 account contract with serialized account transactions
Why: U7 account audit and identity gates advanced on main while U3 account initialization and recovery were in progress.

Evidence: SELF merge-tree checks passed with go test ./... and isolated PostgreSQL store/server tests; exact-head replay follows.

Impact: Preserve pending-account grants, proof-linked recovery, protected CLI delivery, account serialization, and atomic audit.

Refs: IAM U3 r1; U7 PR #185
2026-10-14 05:27:42 +00:00

142 lines
5.2 KiB
Go

package store
import (
"crypto/rand"
"database/sql"
"encoding/hex"
"fmt"
"os"
"strconv"
"time"
)
func localOwnerActor() string { return fmt.Sprintf("local-owner:uid:%d", os.Geteuid()) }
func newActivationPlain() (string, error) {
var raw [32]byte
if _, err := rand.Read(raw[:]); err != nil {
return "", err
}
return "alk_" + hex.EncodeToString(raw[:]), nil
}
// InitializePendingAccountWithAudit reissues only a trusted pending account's
// activation grant. The account mutation, grant, and success audit commit together.
func (s *Store) InitializePendingAccountWithAudit(userID string, expectedStateVersion, expectedGeneration int64, instanceID string) (AccountOutcome, error) {
var out AccountOutcome
if userID == "" || expectedStateVersion < 1 || expectedGeneration < 0 {
return out, ErrConflict
}
pre, err := s.GetAccount(userID)
if err != nil {
return out, err
}
plain, err := newActivationPlain()
if err != nil {
return out, err
}
operator := localOwnerActor()
w := AccountWrite{Owner: pre.Owner, Actor: strconv.Itoa(os.Geteuid()), InstanceID: instanceID, Action: "account.initialize"}
err = s.accountTx(w, func(tx *sql.Tx, opID string) error {
locked, err := s.lockAccountUsers(tx, pre.Owner)
if err != nil {
return err
}
account := locked[pre.Owner]
if account.ID != userID || account.StateVersion != expectedStateVersion || account.InitializationGeneration != expectedGeneration {
return ErrConflict
}
if !account.Enabled || account.InitializationState != InitializationPending || account.HasPassword || account.CredentialOrigin != CredentialOriginLegacyUnknown {
return ErrAccountForbidden
}
for _, query := range []string{
`SELECT COUNT(1) FROM api_keys WHERE owner=? AND revoked=0`,
`SELECT COUNT(1) FROM sessions WHERE owner=? AND scope='full'`,
`SELECT COUNT(1) FROM login_keys WHERE owner=? AND grant_origin<>'owner_initialization'`,
} {
var unsafeCreds int
if err := tx.QueryRow(s.q(query), account.Owner).Scan(&unsafeCreds); err != nil {
return err
}
if unsafeCreds != 0 {
return ErrAccountForbidden
}
}
nextGeneration := account.InitializationGeneration + 1
nextAuthVersion := account.AuthVersion + 1
result, err := tx.Exec(s.q(`UPDATE users SET initialization_generation=?,state_version=state_version+1,auth_version=auth_version+1
WHERE id=? AND state_version=? AND initialization_generation=? AND initialization_state=? AND password_hash='' AND enabled=?`),
nextGeneration, userID, expectedStateVersion, expectedGeneration, InitializationPending, true)
if err != nil {
return err
}
if n, err := result.RowsAffected(); err != nil || n != 1 {
return ErrConflict
}
now := time.Now().UTC()
id := newID("lk")
if _, err := s.insertLoginKeyTx(tx, account.Owner, LoginKeyPurposeActivate, operator, plain, id, now, now.Add(defaultLoginKeyTTL),
grantBinding{origin: CredentialOriginOwnerInitialization, generation: nextGeneration, version: nextAuthVersion}); err != nil {
return err
}
if err := s.stampLoginKeyInstance(tx, id, instanceID); err != nil {
return err
}
if err := s.insertAccountEvent(tx, operator, "local-owner-cli", account.Owner, "initialization_issued", fmt.Sprintf("user_id=%s generation=%d", userID, nextGeneration)); err != nil {
return err
}
out = AccountOutcome{Plain: plain, CredentialID: id, OperationID: opID}
return nil
})
return out, err
}
// CreateLocalPendingAccountWithAudit admits a new pending account through the
// same serialized, audited account transaction as other maintenance actions.
func (s *Store) CreateLocalPendingAccountWithAudit(owner, name string, isAdmin bool, instanceID string) (Account, AccountOutcome, error) {
var account Account
var out AccountOutcome
plain, err := newActivationPlain()
if err != nil {
return account, out, err
}
operator := localOwnerActor()
w := AccountWrite{Owner: owner, Actor: strconv.Itoa(os.Geteuid()), InstanceID: instanceID, Action: "account.create.local_pending"}
err = s.accountTx(w, func(tx *sql.Tx, opID string) error {
if err := s.dialect.lockBootstrap(tx); err != nil {
return err
}
var users int
if err := tx.QueryRow(`SELECT COUNT(1) FROM users`).Scan(&users); err != nil {
return err
}
if users == 0 {
return ErrConflict
}
var err error
account, err = s.insertPendingAccountTx(tx, owner, AccountProfile{Name: name})
if err != nil {
return err
}
if _, err := tx.Exec(s.q(`UPDATE users SET is_admin=?,initialization_generation=? WHERE id=?`), isAdmin, 1, account.ID); err != nil {
return err
}
now := time.Now().UTC()
id := newID("lk")
if _, err := s.insertLoginKeyTx(tx, owner, LoginKeyPurposeActivate, operator, plain, id, now, now.Add(defaultLoginKeyTTL),
grantBinding{origin: CredentialOriginOwnerInitialization, generation: 1, version: 0}); err != nil {
return err
}
if err := s.stampLoginKeyInstance(tx, id, instanceID); err != nil {
return err
}
if err := s.insertAccountEvent(tx, operator, "local-owner-cli", owner, "local_account_created", account.ID); err != nil {
return err
}
account.IsAdmin, account.InitializationGeneration = isAdmin, 1
out = AccountOutcome{Plain: plain, CredentialID: id, OperationID: opID}
return nil
})
return account, out, err
}