Files
usernode/cmd/usernode/user.go
T
caowangrenbo a5f501dba4 feat(M2): SSH 密钥管理 — 公钥上传/重命名/吊销、authorized_keys 原子同步与吊销即时失效
- KeyService:crypto/ssh 解析校验(单行/类型/长度/去重指纹,拒 ssh-dss 与 RSA<2048),
  Create/Rename/Revoke/List,变更后以 DB 状态全量重写 authorized_keys(同步失败回滚)
- system 层:SyncAuthorizedKeys 完善 —— sudo 模式经白名单命令(mkdir/chown/chmod/install)
  落位并修正属主(sshd StrictModes),direct 模式 root 时同样修正属主;dry-run 计划日志
- API:GET/POST /me/keys、PATCH/DELETE /me/keys/:id(user 会话)、GET /users/:id/keys(admin),
  密钥操作带审计;deploy/sudoers.example 补充密钥同步白名单
- 版本 0.3.0-m2;测试:service 单元(校验/生命周期/回滚/权限)、system 直写落盘、
  API 全流程集成;容器 E2E 32 项 PASS(真实 useradd/authorized_keys/吊销即时失效/禁用清空/删除回收)
2026-08-29 23:55:39 +08:00

76 lines
2.5 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 main
import (
"context"
"flag"
"fmt"
"strings"
"time"
"ws_usernode/internal/auth"
"ws_usernode/internal/service"
"ws_usernode/internal/system"
)
// cmdUser 处理 user 子命令(当前仅 otp)。
func cmdUser(args []string) error {
if len(args) == 0 {
return fmt.Errorf("用法: usernode user <otp> [flags]")
}
switch args[0] {
case "otp":
return userOTP(args[1:])
default:
return fmt.Errorf("未知 user 子命令 %q", args[0])
}
}
// userOTP 获取外部用户 OTP 验证码。与邮件通道共用同一 DB 存储与限速:
// 已有有效验证码时直接复用(同一验证码),无则生成(受同一 60s 冷却约束)。
func userOTP(args []string) error {
fs := flag.NewFlagSet("user otp", flag.ContinueOnError)
cfgPath, debug := commonFlags(fs)
username := fs.String("username", "", "外部用户系统账号(如 ext_zhangsan)")
if err := fs.Parse(args); err != nil {
return err
}
cfg, log, db, err := bootstrap(*cfgPath, *debug)
if err != nil {
return err
}
if *username == "" {
return fmt.Errorf("--username 必填")
}
// 规范化:补全 ext_ 前缀
name := strings.TrimSpace(*username)
if !strings.HasPrefix(name, cfg.System.UserPrefix) {
name = cfg.System.UserPrefix + name
}
// 校验用户存在(不存在时返回友好错误,避免暴露账号是否存在的枚举)
if _, err := service.NewUserService(db, system.New(cfg.System, log), cfg).GetByUsername(context.Background(), name); err != nil {
return fmt.Errorf("用户不存在或不可用: %w", err)
}
store := auth.NewDBOTPStore(db, auth.DefaultMaxFailures, auth.DefaultFailureWin)
ctx := context.Background()
code, err := store.Current(ctx, name)
if err != nil {
// 无有效验证码:生成(先到先得,覆盖旧码;冷却期内返回 ErrCooldown)
code, err = store.Send(ctx, name, cfg.Policy.OTPTTL, cfg.Policy.OTPCooldown)
if err != nil {
if err == auth.ErrCooldown {
return fmt.Errorf("发送冷却中,请稍后重试(冷却 %s)", cfg.Policy.OTPCooldown)
}
return err
}
log.Info("otp generated", "username", name, "valid_for", cfg.Policy.OTPTTL.String())
} else {
log.Info("otp reused (与邮件通道同一验证码)", "username", name)
}
fmt.Printf("OTP for %s: %s\n", name, code)
fmt.Printf("有效期至 %s,登录后立即失效;如需重发请等待冷却 %s 或稍后在网页重新请求。\n",
time.Now().Add(cfg.Policy.OTPTTL).Format(time.RFC3339), cfg.Policy.OTPCooldown)
return nil
}