Files
Agent-System-Backend/internal/store/auth.go
T
weichen 02e2036fc6 Add dual-tier storage (embedded SQLite + optional PostgreSQL)
Details (original message):
Add dual-tier storage (embedded SQLite + optional PostgreSQL), tenant owner scoping, API key auth, versioned dual-dialect migrations, integrity hardening, and batched event persistence
2026-09-30 02:39:50 +00:00

147 lines
4.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package store
import (
"crypto/rand"
"crypto/sha256"
"database/sql"
"encoding/hex"
"errors"
"time"
)
// ---- 认证与租户(阶段 1:API key 认证,user 即租户 owner)----
// key 明文仅创建时返回一次;库中只存 SHA-256 摘要(数据可靠性标准:不落明文凭证)。
// User 是租户账户(owner 全局唯一;id 供后续扩展用户目录)。
type User struct {
ID string `json:"id"`
Owner string `json:"owner"`
Name string `json:"name"`
CreatedAt time.Time `json:"createdAt"`
}
// APIKeyInfo 是密钥的展示视图(不含摘要)。
type APIKeyInfo struct {
Name string `json:"name"`
Owner string `json:"owner"`
CreatedAt time.Time `json:"createdAt"`
LastUsedAt *time.Time `json:"lastUsedAt,omitempty"`
Revoked bool `json:"revoked"`
}
func hashKey(plain string) string {
sum := sha256.Sum256([]byte(plain))
return hex.EncodeToString(sum[:])
}
// EnsureUser 幂等创建租户账户(owner 唯一)。
func (s *Store) EnsureUser(owner, name string) error {
_, err := s.db.Exec(s.q(`INSERT INTO users(id,owner,name,created_at) VALUES(?,?,?,?)
ON CONFLICT(owner) DO NOTHING`), newID("usr"), owner, name, nowISO())
return err
}
func (s *Store) GetUser(owner string) (User, error) {
var u User
var ca string
err := s.db.QueryRow(s.q(`SELECT id,owner,name,created_at FROM users WHERE owner=?`), owner).Scan(&u.ID, &u.Owner, &u.Name, &ca)
if errors.Is(err, sql.ErrNoRows) {
return u, ErrNotFound
}
if err != nil {
return u, err
}
u.CreatedAt, _ = parseTime(ca)
return u, nil
}
func (s *Store) ListUsers() ([]User, error) {
rows, err := s.db.Query(s.q(`SELECT id,owner,name,created_at FROM users ORDER BY created_at`))
if err != nil {
return nil, err
}
defer rows.Close()
var out []User
for rows.Next() {
var u User
var ca string
if err := rows.Scan(&u.ID, &u.Owner, &u.Name, &ca); err != nil {
return nil, err
}
u.CreatedAt, _ = parseTime(ca)
out = append(out, u)
}
return out, rows.Err()
}
// CreateAPIKey 创建密钥:明文 `ask_<hex32>` 仅此一次返回。
func (s *Store) CreateAPIKey(owner, name string) (string, error) {
if err := s.EnsureUser(owner, ""); err != nil {
return "", err
}
raw := make([]byte, 16)
if _, err := rand.Read(raw); err != nil {
return "", err
}
plain := "ask_" + hex.EncodeToString(raw)
if name == "" {
name = "key-" + nowISO()[:10]
}
_, err := s.db.Exec(s.q(`INSERT INTO api_keys(key_hash,owner,name,created_at,revoked) VALUES(?,?,?,?,0)`),
hashKey(plain), owner, name, nowISO())
if err != nil {
return "", err
}
return plain, nil
}
// RevokeAPIKeyByName 撤销指定名称的未撤销密钥(返回是否命中)。
func (s *Store) RevokeAPIKeyByName(owner, name string) (bool, error) {
res, err := s.db.Exec(s.q(`UPDATE api_keys SET revoked=1 WHERE owner=? AND name=? AND revoked=0`), owner, name)
if err != nil {
return false, err
}
n, _ := res.RowsAffected()
return n > 0, nil
}
func (s *Store) ListAPIKeys(owner string) ([]APIKeyInfo, error) {
rows, err := s.db.Query(s.q(`SELECT name,owner,created_at,last_used_at,revoked FROM api_keys WHERE owner=? ORDER BY created_at`), owner)
if err != nil {
return nil, err
}
defer rows.Close()
var out []APIKeyInfo
for rows.Next() {
var k APIKeyInfo
var ca string
var last sql.NullString
if err := rows.Scan(&k.Name, &k.Owner, &ca, &last, &k.Revoked); err != nil {
return nil, err
}
k.CreatedAt, _ = parseTime(ca)
if last.Valid && last.String != "" {
t, _ := parseTime(last.String)
k.LastUsedAt = &t
}
out = append(out, k)
}
return out, rows.Err()
}
// AuthOwner 校验 API key:命中返回 owner;未命中/已撤销返回 false。
// last_used_at 尽力刷新(失败不影响认证结果)。
func (s *Store) AuthOwner(plainKey string) (string, bool) {
if plainKey == "" {
return "", false
}
var owner string
var revoked int
err := s.db.QueryRow(s.q(`SELECT owner,revoked FROM api_keys WHERE key_hash=?`), hashKey(plainKey)).Scan(&owner, &revoked)
if err != nil || revoked == 1 {
return "", false
}
s.db.Exec(s.q(`UPDATE api_keys SET last_used_at=? WHERE key_hash=?`), nowISO(), hashKey(plainKey))
return owner, true
}