Files
Agent-System-Backend/internal/store/owner_initialization_audit.go
T

147 lines
5.5 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.IsSuperAdmin || 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.
// No local CLI path may appoint an administrator (R4, R5): isAdmin is
// rejected before any write; only the server-side super administrator can.
func (s *Store) CreateLocalPendingAccountWithAudit(owner, name string, isAdmin bool, instanceID string) (Account, AccountOutcome, error) {
var account Account
var out AccountOutcome
if isAdmin {
return account, out, fmt.Errorf("%w: administrator appointment is reserved to the super administrator", ErrAccountForbidden)
}
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
}