认证: - 图形验证码 GET /auth/captcha(内置 PNG 渲染,零第三方依赖) - 外部用户 OTP 双通道:DB 存储(otp_codes)使邮件与 CLI 共用同一验证码/冷却/失败限速 - 管理员 bcrypt 登录 + 连续失败限速锁定;admin/forgot + admin/reset 邮件重置(SMTP 或日志) - cookie 会话(HttpOnly/SameSite)、me/logout、admin/user 鉴权中间件 用户管理(admin): - CRUD + disable/enable/extend/delete,对接 system 层真实 useradd/usermod/userdel/passwd - system 层三执行模式:dry-run(默认,安全)/ direct(容器/测试用户)/ sudo(生产 sudoers 白名单) - Exists 系统账号一致性检查;deploy/sudoers.example 白名单模板 - 关键操作接入 append-only 审计 其他: - CLI user otp 改 DB store,与邮件通道真正对齐 - 容器镜像补 shadow(alpine 无 useradd);Makefile VERSION 0.2.0-m1 - 测试:auth/service 单测 + api httptest 集成 + 容器内真实系统账号端到端验证
219 lines
9.1 KiB
Go
219 lines
9.1 KiB
Go
// Package model 定义 GORM 模型与数据库接入。
|
||
//
|
||
// 兼容性约束(见 PLAN §6):
|
||
// - 避免平台特有类型,时间统一 UTC,字段尽量用通用类型;
|
||
// - detail 等 JSON 内容以 string 存储(入库前序列化),避免依赖
|
||
// datatypes.JSON 的平台差异;
|
||
// - 由本包 AutoMigrate 保证 SQLite / MySQL 均可平滑迁移。
|
||
package model
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"time"
|
||
|
||
"github.com/glebarez/sqlite"
|
||
"gorm.io/driver/mysql"
|
||
"gorm.io/gorm"
|
||
gormlogger "gorm.io/gorm/logger"
|
||
)
|
||
|
||
// 外部用户账号状态。
|
||
const (
|
||
UserStatusActive = "active" // 正常可用
|
||
UserStatusDisabled = "disabled" // 管理员禁用
|
||
UserStatusExpired = "expired" // 已到期,等待回收或延期
|
||
)
|
||
|
||
// 密钥 / 申请 / 审计结果等通用状态。
|
||
const (
|
||
StatusActive = "active"
|
||
StatusRevoked = "revoked"
|
||
StatusPending = "pending"
|
||
StatusApproved = "approved"
|
||
StatusRejected = "rejected"
|
||
ResultSuccess = "success"
|
||
ResultFailed = "failed"
|
||
)
|
||
|
||
// AdminUser 管理端账号。
|
||
type AdminUser struct {
|
||
ID uint `gorm:"primaryKey" json:"id"`
|
||
Username string `gorm:"size:64;uniqueIndex;not null" json:"username"`
|
||
PasswordHash string `gorm:"size:255;not null" json:"-"`
|
||
Email string `gorm:"size:255" json:"email"`
|
||
Role string `gorm:"size:32;not null;default:admin" json:"role"`
|
||
Status string `gorm:"size:16;not null;default:active" json:"status"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
UpdatedAt time.Time `json:"updated_at"`
|
||
}
|
||
|
||
// User 外部用户(1:1 对应系统账号 ext_<name>)。
|
||
type User struct {
|
||
ID uint `gorm:"primaryKey" json:"id"`
|
||
Username string `gorm:"size:64;uniqueIndex;not null" json:"username"` // 含 ext_ 前缀
|
||
Email string `gorm:"size:255;not null" json:"email"`
|
||
Supervisor string `gorm:"size:128" json:"supervisor"` // 挂靠老师
|
||
Purpose string `gorm:"size:512" json:"purpose"` // 用途
|
||
Status string `gorm:"size:16;not null;default:active;index" json:"status"`
|
||
ExpireAt *time.Time `gorm:"index" json:"expire_at"`
|
||
Shell string `gorm:"size:64;not null" json:"shell"`
|
||
CreatedBy uint `json:"created_by"`
|
||
LastLoginAt *time.Time `json:"last_login_at"`
|
||
RecycledAt *time.Time `json:"recycled_at"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
UpdatedAt time.Time `json:"updated_at"`
|
||
}
|
||
|
||
// SSHKey 用户上传的 SSH 公钥。仅用户自行上传(source=user_uploaded),管理员不代签。
|
||
type SSHKey struct {
|
||
ID uint `gorm:"primaryKey" json:"id"`
|
||
UserID uint `gorm:"not null;index" json:"user_id"`
|
||
Name string `gorm:"size:64;not null" json:"name"`
|
||
KeyType string `gorm:"size:32;not null" json:"key_type"` // ssh-ed25519 / ssh-rsa ...
|
||
PublicKey string `gorm:"type:text;not null" json:"public_key"`
|
||
Fingerprint string `gorm:"size:64;index" json:"fingerprint"`
|
||
Status string `gorm:"size:16;not null;default:active;index" json:"status"`
|
||
Source string `gorm:"size:32;not null;default:user_uploaded" json:"source"`
|
||
CreatedBy uint `json:"created_by"`
|
||
RevokedAt *time.Time `json:"revoked_at"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
UpdatedAt time.Time `json:"updated_at"`
|
||
}
|
||
|
||
// Approval 新账号申请单(仅新账号申请走审批流,延期由管理员直接操作)。
|
||
type Approval struct {
|
||
ID uint `gorm:"primaryKey" json:"id"`
|
||
UsernameRequested string `gorm:"size:64;index;not null" json:"username_requested"` // 不含 ext_ 前缀
|
||
Email string `gorm:"size:255;not null" json:"email"`
|
||
Supervisor string `gorm:"size:128" json:"supervisor"`
|
||
Purpose string `gorm:"size:512" json:"purpose"`
|
||
Status string `gorm:"size:16;not null;default:pending;index" json:"status"`
|
||
ReviewerID *uint `json:"reviewer_id"`
|
||
ReviewedAt *time.Time `json:"reviewed_at"`
|
||
Reason string `gorm:"size:512" json:"reason"` // 拒绝理由
|
||
CreatedAt time.Time `json:"created_at"`
|
||
UpdatedAt time.Time `json:"updated_at"`
|
||
}
|
||
|
||
// AuditLog 审计日志,append-only:业务代码只允许 Create,禁止 Update/Delete。
|
||
type AuditLog struct {
|
||
ID uint `gorm:"primaryKey" json:"id"`
|
||
ActorID uint `gorm:"index" json:"actor_id"`
|
||
ActorName string `gorm:"size:64" json:"actor_name"`
|
||
Action string `gorm:"size:64;index;not null" json:"action"`
|
||
ResourceType string `gorm:"size:32;index" json:"resource_type"`
|
||
ResourceID string `gorm:"size:64;index" json:"resource_id"`
|
||
Detail string `gorm:"type:text" json:"detail"` // JSON 序列化后的详情
|
||
IP string `gorm:"size:64" json:"ip"`
|
||
Result string `gorm:"size:16;not null" json:"result"`
|
||
CreatedAt time.Time `gorm:"index" json:"created_at"`
|
||
}
|
||
|
||
// Session 服务端会话(cookie 会话,DB 存储,兼容多实例)。
|
||
type Session struct {
|
||
ID string `gorm:"primaryKey;size:64" json:"session_id"`
|
||
UserType string `gorm:"size:16;not null" json:"user_type"` // admin / user
|
||
RefID uint `gorm:"index;not null" json:"ref_id"`
|
||
ExpireAt time.Time `gorm:"index;not null" json:"expire_at"`
|
||
IP string `gorm:"size:64" json:"ip"`
|
||
UserAgent string `gorm:"size:255" json:"user_agent"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
}
|
||
|
||
// Setting 系统设置(SMTP、默认有效期等,config 提供默认值,settings 表可覆盖)。
|
||
type Setting struct {
|
||
Key string `gorm:"primaryKey;size:128" json:"key"`
|
||
Value string `gorm:"type:text" json:"value"`
|
||
UpdatedAt time.Time `json:"updated_at"`
|
||
}
|
||
|
||
// MailLog 邮件发送记录(队列 + 重试 + 失败记录)。
|
||
type MailLog struct {
|
||
ID uint `gorm:"primaryKey" json:"id"`
|
||
To string `gorm:"size:255;index;not null" json:"to"`
|
||
Subject string `gorm:"size:255" json:"subject"`
|
||
Status string `gorm:"size:16;not null;default:pending;index" json:"status"`
|
||
Error string `gorm:"type:text" json:"error"`
|
||
RetryCount int `gorm:"not null;default:0" json:"retry_count"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
UpdatedAt time.Time `json:"updated_at"`
|
||
}
|
||
|
||
// OTPCode 外部用户 OTP 验证码(DB 存储,邮件与 CLI 双通道共享)。
|
||
//
|
||
// 每用户一行(username 唯一):邮件通道经 Send 生成,CLI 通道经 Current
|
||
// 读取同一验证码,保证"同一验证码、同一有效期、同一冷却与失败限速"(PLAN §2.2)。
|
||
// 验证码为 6 位数字,生命周期短(10 分钟)且一次性消费,存储明文以便 CLI
|
||
// 复用返回;并发写由单实例部署的串行事务保证(多实例需改 DB 行锁/Redis,PLAN §6)。
|
||
type OTPCode struct {
|
||
ID uint `gorm:"primaryKey" json:"id"`
|
||
Username string `gorm:"size:64;uniqueIndex;not null" json:"username"`
|
||
Code string `gorm:"size:16;not null" json:"-"`
|
||
ExpiresAt time.Time `gorm:"index;not null" json:"expires_at"`
|
||
CooldownUntil time.Time `json:"cooldown_until"` // Send 冷却截止
|
||
Failures int `json:"failures"`
|
||
FailedAt *time.Time `json:"failed_at"` // 失败计数窗口起点(窗口内达阈值限速)
|
||
ConsumedAt *time.Time `json:"consumed_at"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
UpdatedAt time.Time `json:"updated_at"`
|
||
}
|
||
|
||
// PasswordResetToken 管理员密码重置令牌(邮件重置)。DB 只存哈希,
|
||
// 明文令牌仅经邮件/日志发送给管理员,一次有效。
|
||
type PasswordResetToken struct {
|
||
ID uint `gorm:"primaryKey" json:"id"`
|
||
AdminID uint `gorm:"index;not null" json:"admin_id"`
|
||
TokenHash string `gorm:"size:64;not null" json:"-"`
|
||
ExpiresAt time.Time `gorm:"index;not null" json:"expires_at"`
|
||
UsedAt *time.Time `json:"used_at"`
|
||
IP string `gorm:"size:64" json:"ip"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
}
|
||
|
||
// AllModels 供 AutoMigrate 使用的全部模型。
|
||
func AllModels() []any {
|
||
return []any{
|
||
&AdminUser{}, &User{}, &SSHKey{}, &Approval{},
|
||
&AuditLog{}, &Session{}, &Setting{}, &MailLog{},
|
||
&OTPCode{}, &PasswordResetToken{},
|
||
}
|
||
}
|
||
|
||
// Open 按 driver 打开 GORM 连接。
|
||
//
|
||
// driver: sqlite | mysql
|
||
// dsn: sqlite 为文件路径(自动创建父目录);mysql 为 DSN 字符串
|
||
func Open(driver, dsn string, debug bool) (*gorm.DB, error) {
|
||
logLevel := gormlogger.Warn
|
||
if debug {
|
||
logLevel = gormlogger.Info
|
||
}
|
||
cfg := &gorm.Config{
|
||
Logger: gormlogger.Default.LogMode(logLevel),
|
||
// 表名默认复数(users / ssh_keys / approvals / ...),与 PLAN §6 一致。
|
||
}
|
||
|
||
var dialector gorm.Dialector
|
||
switch driver {
|
||
case "sqlite":
|
||
if dsn != ":memory:" {
|
||
if err := os.MkdirAll(filepath.Dir(dsn), 0o755); err != nil {
|
||
return nil, fmt.Errorf("model: create sqlite dir: %w", err)
|
||
}
|
||
}
|
||
dialector = sqlite.Open(dsn)
|
||
case "mysql":
|
||
dialector = mysql.Open(dsn)
|
||
default:
|
||
return nil, fmt.Errorf("model: unsupported driver %q", driver)
|
||
}
|
||
return gorm.Open(dialector, cfg)
|
||
}
|
||
|
||
// Migrate 执行 AutoMigrate(CLI migrate 子命令入口)。
|
||
func Migrate(db *gorm.DB) error {
|
||
return db.AutoMigrate(AllModels()...)
|
||
}
|