2026-07-16 18:51:58 +08:00

229 lines
5.8 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 middleware
import (
"errors"
"net/http"
"slices"
"strings"
"hyapp-admin-server/internal/appctx"
"hyapp-admin-server/internal/config"
"hyapp-admin-server/internal/model"
"hyapp-admin-server/internal/repository"
"hyapp-admin-server/internal/response"
"hyapp-admin-server/internal/service"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type AppAccessStore interface {
AppAccessForUser(userID uint) (repository.AppAccess, error)
}
type AuthorizationStore interface {
CurrentAuthorizationForUser(userID uint) (repository.UserAuthorization, error)
}
const (
ContextUserID = "userID"
ContextUsername = "username"
ContextPermissions = "permissions"
)
func CORS(cfg config.Config) gin.HandlerFunc {
allowed := map[string]struct{}{}
for _, origin := range cfg.CORSAllowedOrigins {
allowed[origin] = struct{}{}
}
return func(c *gin.Context) {
origin := c.Request.Header.Get("Origin")
if _, ok := allowed[origin]; ok {
c.Header("Access-Control-Allow-Origin", origin)
c.Header("Vary", "Origin")
c.Header("Access-Control-Allow-Credentials", "true")
c.Header("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Request-ID, "+appctx.HeaderAppCode+", X-CSRF-Token")
c.Header("Access-Control-Expose-Headers", "X-Request-ID, X-CSRF-Token")
c.Header("Access-Control-Allow-Methods", "GET, POST, PATCH, PUT, DELETE, OPTIONS")
}
if c.Request.Method == http.MethodOptions {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
}
func AppCode() gin.HandlerFunc {
return func(c *gin.Context) {
appCode := appctx.Normalize(c.GetHeader(appctx.HeaderAppCode))
c.Request = c.Request.WithContext(appctx.WithContext(c.Request.Context(), appCode))
c.Header(appctx.HeaderAppCode, appCode)
c.Next()
}
}
func AuthRequired(auth *service.AuthService, store AuthorizationStore) gin.HandlerFunc {
return func(c *gin.Context) {
header := c.GetHeader("Authorization")
if !strings.HasPrefix(header, "Bearer ") {
response.Unauthorized(c, "缺少访问凭证")
c.Abort()
return
}
if auth == nil || store == nil {
// Authorization cannot safely fall back to claims.Permissions: doing so
// would restore removed privileges until the access token expires.
response.ServerError(c, "认证权限服务不可用")
c.Abort()
return
}
claims, err := auth.ParseAccessToken(strings.TrimPrefix(header, "Bearer "))
if err != nil {
response.Unauthorized(c, "访问凭证已失效")
c.Abort()
return
}
authorization, err := store.CurrentAuthorizationForUser(claims.UserID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
response.Unauthorized(c, "用户不存在")
} else {
// Database errors fail closed. A stale JWT permission snapshot is not
// an acceptable availability fallback for privileged admin actions.
response.ServerError(c, "校验登录权限失败")
}
c.Abort()
return
}
if authorization.Status != model.UserStatusActive {
response.Unauthorized(c, "账号不可登录")
c.Abort()
return
}
c.Set(ContextUserID, claims.UserID)
c.Set(ContextUsername, authorization.Username)
c.Set(ContextPermissions, authorization.Permissions)
c.Next()
}
}
func RequireAppScope(store AppAccessStore) gin.HandlerFunc {
return func(c *gin.Context) {
// App 级路由必须显式携带 header不能复用 appctx 的 lalu 默认值;否则无 App 用户省略 header 仍会落入 Lalu 数据域。
appCode := strings.ToLower(strings.TrimSpace(c.GetHeader(appctx.HeaderAppCode)))
if appCode == "" {
response.Forbidden(c, "当前用户没有可访问的 App")
c.Abort()
return
}
if store == nil {
response.ServerError(c, "App 权限服务不可用")
c.Abort()
return
}
access, err := store.AppAccessForUser(CurrentUserID(c))
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
response.Unauthorized(c, "用户不存在")
} else {
response.ServerError(c, "校验 App 权限失败")
}
c.Abort()
return
}
if !access.Allows(appCode) {
response.Forbidden(c, "当前用户无权访问该 App")
c.Abort()
return
}
c.Next()
}
}
func RequirePermission(code string) gin.HandlerFunc {
return func(c *gin.Context) {
if HasPermission(c, code) {
c.Next()
return
}
response.Forbidden(c, "没有操作权限")
c.Abort()
}
}
// RequireAnyPermission is used by transitional RBAC routes where the new
// button-level permission and the old broad permission must both work.
func RequireAnyPermission(codes ...string) gin.HandlerFunc {
return func(c *gin.Context) {
if HasAnyPermission(c, codes...) {
c.Next()
return
}
response.Forbidden(c, "没有操作权限")
c.Abort()
}
}
// HasAnyPermission keeps privilege checks in handlers readable when one action
// is allowed by multiple compatible permission codes.
func HasAnyPermission(c *gin.Context, codes ...string) bool {
for _, code := range codes {
if HasPermission(c, code) {
return true
}
}
return false
}
// HasPermission reads the request-scoped permission list resolved by
// AuthRequired from current database state. JWT permission claims are retained
// for wire compatibility only and are never trusted for server authorization.
func HasPermission(c *gin.Context, code string) bool {
return slices.Contains(CurrentPermissions(c), code)
}
func CurrentUserID(c *gin.Context) uint {
value, ok := c.Get(ContextUserID)
if !ok {
return 0
}
id, ok := value.(uint)
if !ok {
return 0
}
return id
}
func CurrentUsername(c *gin.Context) string {
value, ok := c.Get(ContextUsername)
if !ok {
return ""
}
username, ok := value.(string)
if !ok {
return ""
}
return username
}
func CurrentPermissions(c *gin.Context) []string {
value, ok := c.Get(ContextPermissions)
if !ok {
return nil
}
permissions, ok := value.([]string)
if !ok {
return nil
}
return permissions
}