- 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/吊销即时失效/禁用清空/删除回收)
76 lines
2.5 KiB
Go
76 lines
2.5 KiB
Go
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
|
||
}
|