147 lines
5.5 KiB
Go
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
|
|
}
|