- 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/吊销即时失效/禁用清空/删除回收)
151 lines
4.8 KiB
Go
151 lines
4.8 KiB
Go
package api
|
||
|
||
import (
|
||
"errors"
|
||
"net/http"
|
||
"strconv"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
|
||
"ws_usernode/internal/config"
|
||
"ws_usernode/internal/model"
|
||
"ws_usernode/internal/service"
|
||
)
|
||
|
||
// KeyHandler SSH 公钥接口:外部用户自助管理(/me/keys)+ 管理员查看(/users/:id/keys)。
|
||
// 密钥仅用户上传(管理员不代签);吊销后立即从 authorized_keys 移除。
|
||
type KeyHandler struct {
|
||
svc *service.KeyService
|
||
cfg *config.Config
|
||
h *Handler // 访问审计 helper
|
||
}
|
||
|
||
// KeyCreateRequest 上传公钥。
|
||
type KeyCreateRequest struct {
|
||
Name string `json:"name" binding:"required"` // 显示名称,1~64 字符
|
||
PublicKey string `json:"public_key" binding:"required"`
|
||
}
|
||
|
||
// KeyRenameRequest 重命名。
|
||
type KeyRenameRequest struct {
|
||
Name string `json:"name" binding:"required"`
|
||
}
|
||
|
||
// currentUserID 从会话取当前外部用户 ID(/me/keys 均为 user 会话)。
|
||
func (h *KeyHandler) currentUserID(c *gin.Context) uint {
|
||
if sess := sessionFrom(c); sess != nil {
|
||
return sess.RefID
|
||
}
|
||
return 0
|
||
}
|
||
|
||
// ListMine GET /me/keys —— 我的密钥列表。
|
||
func (h *KeyHandler) ListMine(c *gin.Context) {
|
||
keys, err := h.svc.ListByUser(c.Request.Context(), h.currentUserID(c))
|
||
if err != nil {
|
||
fail(c, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
ok(c, gin.H{"items": keys})
|
||
}
|
||
|
||
// Create POST /me/keys —— 上传公钥(类型/长度/重复校验 + 同步 authorized_keys)。
|
||
func (h *KeyHandler) Create(c *gin.Context) {
|
||
var req KeyCreateRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
fail(c, http.StatusBadRequest, "请求参数不合法: "+err.Error())
|
||
return
|
||
}
|
||
uid := h.currentUserID(c)
|
||
k, err := h.svc.Create(c.Request.Context(), uid, req.Name, req.PublicKey, uid)
|
||
if err != nil {
|
||
h.h.audit(c, "key.create", "ssh_key", "", map[string]any{"name": req.Name, "err": err.Error()}, model.ResultFailed)
|
||
switch {
|
||
case errors.Is(err, service.ErrKeyInvalid):
|
||
fail(c, http.StatusBadRequest, err.Error())
|
||
case errors.Is(err, service.ErrKeyDuplicate):
|
||
fail(c, http.StatusConflict, err.Error())
|
||
case errors.Is(err, service.ErrUserNotFound):
|
||
fail(c, http.StatusNotFound, err.Error())
|
||
case errors.Is(err, service.ErrUserNotActive):
|
||
fail(c, http.StatusConflict, err.Error())
|
||
default:
|
||
fail(c, http.StatusInternalServerError, err.Error())
|
||
}
|
||
return
|
||
}
|
||
h.h.audit(c, "key.create", "ssh_key", strconv.FormatUint(uint64(k.ID), 10), map[string]any{"name": k.Name, "fingerprint": k.Fingerprint}, model.ResultSuccess)
|
||
ok(c, k)
|
||
}
|
||
|
||
// Rename PATCH /me/keys/:id —— 重命名(仅元数据,不影响 authorized_keys)。
|
||
func (h *KeyHandler) Rename(c *gin.Context) {
|
||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
fail(c, http.StatusBadRequest, "无效的密钥 ID")
|
||
return
|
||
}
|
||
var req KeyRenameRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
fail(c, http.StatusBadRequest, "请求参数不合法: "+err.Error())
|
||
return
|
||
}
|
||
k, err := h.svc.Rename(c.Request.Context(), uint(id), h.currentUserID(c), req.Name)
|
||
if err != nil {
|
||
h.h.audit(c, "key.rename", "ssh_key", c.Param("id"), map[string]any{"err": err.Error()}, model.ResultFailed)
|
||
switch {
|
||
case errors.Is(err, service.ErrKeyNotFound):
|
||
fail(c, http.StatusNotFound, err.Error())
|
||
case errors.Is(err, service.ErrKeyInvalid):
|
||
fail(c, http.StatusBadRequest, err.Error())
|
||
default:
|
||
fail(c, http.StatusInternalServerError, err.Error())
|
||
}
|
||
return
|
||
}
|
||
h.h.audit(c, "key.rename", "ssh_key", c.Param("id"), map[string]any{"name": k.Name}, model.ResultSuccess)
|
||
ok(c, k)
|
||
}
|
||
|
||
// Revoke DELETE /me/keys/:id —— 吊销(从 authorized_keys 移除,立即失效)。
|
||
func (h *KeyHandler) Revoke(c *gin.Context) {
|
||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
fail(c, http.StatusBadRequest, "无效的密钥 ID")
|
||
return
|
||
}
|
||
k, err := h.svc.Revoke(c.Request.Context(), uint(id), h.currentUserID(c))
|
||
if err != nil {
|
||
h.h.audit(c, "key.revoke", "ssh_key", c.Param("id"), map[string]any{"err": err.Error()}, model.ResultFailed)
|
||
switch {
|
||
case errors.Is(err, service.ErrKeyNotFound):
|
||
fail(c, http.StatusNotFound, err.Error())
|
||
default:
|
||
fail(c, http.StatusInternalServerError, err.Error())
|
||
}
|
||
return
|
||
}
|
||
h.h.audit(c, "key.revoke", "ssh_key", c.Param("id"), map[string]any{"fingerprint": k.Fingerprint}, model.ResultSuccess)
|
||
ok(c, k)
|
||
}
|
||
|
||
// ListForUser GET /users/:id/keys —— 管理员查看用户密钥。
|
||
func (h *KeyHandler) ListForUser(c *gin.Context) {
|
||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||
if err != nil {
|
||
fail(c, http.StatusBadRequest, "无效的用户 ID")
|
||
return
|
||
}
|
||
keys, err := h.svc.ListByUser(c.Request.Context(), uint(id))
|
||
if err != nil {
|
||
switch {
|
||
case errors.Is(err, service.ErrUserNotFound):
|
||
fail(c, http.StatusNotFound, err.Error())
|
||
default:
|
||
fail(c, http.StatusInternalServerError, err.Error())
|
||
}
|
||
return
|
||
}
|
||
ok(c, gin.H{"items": keys})
|
||
}
|