chore: 并入 iotlcm-admin-svr 历史到 admin-svr/(原提交 49818c6)
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
)
|
||||
|
||||
func TestChannelTestErrorResponsePreservesOriginalError(t *testing.T) {
|
||||
const raw = `{"code":50,"message":"channel is not accessible","data":null}`
|
||||
server := g.Server(t.Name())
|
||||
server.SetAddr("127.0.0.1:0")
|
||||
server.SetDumpRouterMap(false)
|
||||
server.Group("/", func(group *ghttp.RouterGroup) {
|
||||
group.Middleware(New().ResponseHandler)
|
||||
group.GET("/plain", func(r *ghttp.Request) {
|
||||
r.SetError(gerror.New("channel is not accessible"))
|
||||
})
|
||||
group.GET("/diagnostic", func(r *ghttp.Request) {
|
||||
r.SetError(&model.TSimChannelsTestError{Err: gerror.New("channel is not accessible"), GatewayResponseRaw: raw})
|
||||
})
|
||||
group.GET("/coded", func(r *ghttp.Request) {
|
||||
r.SetError(&model.TSimChannelsTestError{Err: gerror.NewCode(gcode.CodeInvalidParameter, "invalid input"), GatewayResponseRaw: raw})
|
||||
})
|
||||
})
|
||||
if err := server.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = server.Shutdown() })
|
||||
for _, tc := range []struct {
|
||||
path string
|
||||
code int
|
||||
message string
|
||||
raw string
|
||||
}{
|
||||
{"/plain", gcode.CodeInternalError.Code(), "channel is not accessible", ""},
|
||||
{"/diagnostic", gcode.CodeInternalError.Code(), "channel is not accessible", raw},
|
||||
{"/coded", gcode.CodeInvalidParameter.Code(), "invalid input", raw},
|
||||
} {
|
||||
t.Run(tc.path, func(t *testing.T) {
|
||||
res, err := http.Get("http://127.0.0.1:" + strconv.Itoa(server.GetListenedPort()) + tc.path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
var body struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
GatewayResponseRaw string `json:"gatewayResponseRaw"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.NewDecoder(res.Body).Decode(&body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res.StatusCode != http.StatusOK || body.Code != tc.code || body.Message != tc.message || body.Data.GatewayResponseRaw != tc.raw {
|
||||
t.Fatalf("unexpected response: status=%d body=%+v", res.StatusCode, body)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/bcache"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/mcache"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/rolepolicy"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/packed/response"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/queue"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/util/gmeta"
|
||||
)
|
||||
|
||||
type sMiddleware struct{}
|
||||
|
||||
func New() *sMiddleware {
|
||||
return &sMiddleware{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterMiddleware(New())
|
||||
}
|
||||
|
||||
func getContentType(r *ghttp.Request) (contentType string) {
|
||||
contentType = r.Response.Header().Get("Content-Type")
|
||||
if contentType != "" {
|
||||
return
|
||||
}
|
||||
|
||||
mime := gmeta.Get(r.GetHandlerResponse(), "mime").String()
|
||||
if mime == "" {
|
||||
contentType = consts.HTTPContentTypeJson
|
||||
} else {
|
||||
contentType = mime
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (m *sMiddleware) ResponseHandler(r *ghttp.Request) {
|
||||
r.Middleware.Next()
|
||||
if r.Response.BufferLength() > 0 {
|
||||
return
|
||||
}
|
||||
if contexts.IsWebSocket(r.GetCtx()) {
|
||||
return
|
||||
}
|
||||
if disposition := r.Response.Header().Get("Content-Disposition"); strings.Contains(strings.ToLower(disposition), "attachment") {
|
||||
r.Response.Status = http.StatusOK
|
||||
return
|
||||
}
|
||||
contentType := getContentType(r)
|
||||
if contentType == consts.HTTPContentTypeExcel {
|
||||
r.Response.Status = http.StatusOK
|
||||
return
|
||||
}
|
||||
|
||||
err := r.GetError()
|
||||
res := r.GetHandlerResponse()
|
||||
var code gcode.Code = gcode.CodeOK
|
||||
|
||||
if err != nil {
|
||||
code = gerror.Code(err)
|
||||
if code == gcode.CodeNil {
|
||||
code = gcode.CodeInternalError
|
||||
}
|
||||
var channelTestErr *model.TSimChannelsTestError
|
||||
if errors.As(err, &channelTestErr) {
|
||||
response.JsonExit(r, code.Code(), err.Error(), &model.TSimChannelsTestOutput{
|
||||
GatewayResponseRaw: channelTestErr.GatewayResponseRaw,
|
||||
})
|
||||
}
|
||||
response.JsonExit(r, code.Code(), err.Error())
|
||||
} else {
|
||||
response.Json(r, code.Code(), "", res)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *sMiddleware) Ctx(r *ghttp.Request) {
|
||||
ctx := r.GetCtx()
|
||||
r.SetCtx(r.GetNeverDoneCtx())
|
||||
|
||||
data, err := service.SysAuth().GetCurrentUser(ctx)
|
||||
if err != nil {
|
||||
r.Middleware.Next()
|
||||
return
|
||||
}
|
||||
if data != nil {
|
||||
contextModel := new(model.Context)
|
||||
contextModel.User = &model.Identity{
|
||||
BaseClaims: data.BaseClaims,
|
||||
}
|
||||
contexts.Init(r, contextModel)
|
||||
}
|
||||
r.Middleware.Next()
|
||||
}
|
||||
|
||||
func (s *sMiddleware) CORS(r *ghttp.Request) {
|
||||
r.SetCtx(r.GetNeverDoneCtx())
|
||||
if contexts.IsWebSocket(r.GetCtx()) {
|
||||
r.Middleware.Next()
|
||||
return
|
||||
}
|
||||
corsOptions := r.Response.DefaultCORSOptions()
|
||||
corsConfig := g.Cfg().MustGet(context.Background(), "server.allowedDomains").Strings()
|
||||
if len(corsConfig) == 0 {
|
||||
r.Response.CORSDefault()
|
||||
} else {
|
||||
corsOptions.AllowDomain = corsConfig
|
||||
r.Response.CORS(corsOptions)
|
||||
}
|
||||
r.Middleware.Next()
|
||||
}
|
||||
|
||||
func (s *sMiddleware) Auth(r *ghttp.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
method = r.Method
|
||||
path = ""
|
||||
)
|
||||
if r.URL != nil {
|
||||
path = r.URL.Path
|
||||
}
|
||||
if s.IsExceptLogin(ctx, path) {
|
||||
r.Middleware.Next()
|
||||
return
|
||||
}
|
||||
|
||||
userId := contexts.GetUserId(ctx)
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
g.Log().Debugf(ctx, "sMiddleware.Auth userId: %d tenantId: %s method: %s path: %s, ================>>", userId, tenantId, method, path)
|
||||
if userId == 0 {
|
||||
g.Log().Error(ctx, "sMiddleware.Auth userId is 0", "path", path, "clientIp", r.GetClientIp(), "User-Agent", utility.GetUserAgent(ctx), "Authorization", r.Header.Get("Authorization"))
|
||||
response.JsonExit(r, gcode.CodeNotAuthorized.Code(), consts.CodeLoginExpired.Message())
|
||||
return
|
||||
}
|
||||
if contexts.IsWebSocket(ctx) {
|
||||
r.Middleware.Next()
|
||||
return
|
||||
}
|
||||
|
||||
forceLogout, err := mcache.GetSystemConfig(ctx, consts.ConfigOnlineForceLogout, consts.ConfigOnlineForceLogoutFalse)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMiddleware.Auth GetSystemConfig error: %v", err)
|
||||
response.JsonExit(r, gcode.CodeNotAuthorized.Code(), consts.CodeLoginExpired.Message())
|
||||
return
|
||||
}
|
||||
if forceLogout == consts.ConfigOnlineForceLogoutTrue {
|
||||
uuid := contexts.GetUserUuid(ctx)
|
||||
token, err := bcache.GetSysAuthTokenReject(ctx, userId, uuid)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMiddleware.Auth GetSysAuthTokenReject error: %v", err)
|
||||
response.JsonExit(r, gcode.CodeNotAuthorized.Code(), consts.CodeLoginExpired.Message())
|
||||
return
|
||||
}
|
||||
if token != "" {
|
||||
g.Log().Infof(ctx, "sMiddleware.Auth token is reject, userId: %d, uuid: %s, token: %s", userId, uuid, token)
|
||||
response.JsonExit(r, gcode.CodeNotAuthorized.Code(), consts.CodeLoginExpired.Message())
|
||||
return
|
||||
}
|
||||
}
|
||||
if contexts.IsSuperAdmin(r.GetCtx()) && tenantId == consts.DefaultSystemTenantCode {
|
||||
isAdmin, checkErr := rolepolicy.CurrentSuperAdmin(ctx, userId)
|
||||
if checkErr != nil {
|
||||
g.Log().Errorf(ctx, "verify super admin failed: %v", checkErr)
|
||||
response.JsonExit(r, gcode.CodeNotAuthorized.Code(), consts.CodeLoginExpired.Message())
|
||||
return
|
||||
}
|
||||
if isAdmin {
|
||||
r.Middleware.Next()
|
||||
return
|
||||
}
|
||||
}
|
||||
if contexts.IsSuperAdmin(ctx) {
|
||||
user := contexts.GetUser(ctx)
|
||||
user.Roles = slices.DeleteFunc(user.Roles, func(role model.Role) bool {
|
||||
return role.RoleId == consts.SuperAdminRoleId
|
||||
})
|
||||
}
|
||||
|
||||
serveHandler := r.GetServeHandler()
|
||||
userAccessCodeList, menuRoleDataAccessCodeList, err := service.SysAuth().GetUserAccessCodeList(ctx, userId)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMiddleware.Auth GetUserAccessCodeList error: %v", err)
|
||||
response.JsonExit(r, gcode.CodeNotAuthorized.Code(), consts.CodeLoginExpired.Message())
|
||||
return
|
||||
}
|
||||
// 回填到请求上下文:Identity 由 Ctx 中间件只带 BaseClaims 构造,权限码仅在此处查出。
|
||||
// 若不回填,contexts.GetAccessCodeList 在后续业务里恒为空(下载中心按导出权限过滤任务、
|
||||
// 下载/删除二次校验都依赖它),表现为非超管看不到自己的导出任务、下载被拒。
|
||||
if user := contexts.GetUser(ctx); user != nil {
|
||||
user.AccessCodeList = userAccessCodeList
|
||||
}
|
||||
if serveHandler != nil {
|
||||
g.Log().Infof(ctx, "sMiddleware.Auth serveHandler GetMetaTag(x-check-permission): %+v", serveHandler.GetMetaTag("x-check-permission"))
|
||||
accessCode := serveHandler.GetMetaTag("x-check-permission")
|
||||
if accessCode != "" {
|
||||
accessCodeList := strings.Split(accessCode, ",")
|
||||
hasPermission := false
|
||||
for _, code := range accessCodeList {
|
||||
if strings.HasPrefix(code, consts.SysCheckPermissionPrefix) {
|
||||
accessCodeItemList := strings.Split(code, "&")
|
||||
hasItem := true
|
||||
for _, item := range accessCodeItemList {
|
||||
if !hasAccessCode(userAccessCodeList, item) {
|
||||
g.Log().Errorf(ctx, "sMiddleware.Auth userAccessCodeList not contains item: %v, userAccessCodeList: %v", item, userAccessCodeList)
|
||||
hasItem = false
|
||||
break
|
||||
}
|
||||
for _, roleDataAccessCode := range menuRoleDataAccessCodeList {
|
||||
roleDataAccessCodeItemList := strings.Split(roleDataAccessCode, "|")
|
||||
if len(roleDataAccessCodeItemList) > 0 && matchAccessCode(roleDataAccessCodeItemList[0], item) {
|
||||
g.Log().Debugf(ctx, "sMiddleware.Auth roleDataAccessCodeItemList: %v", roleDataAccessCodeItemList)
|
||||
if len(roleDataAccessCodeItemList) == 3 {
|
||||
roleId := roleDataAccessCodeItemList[1]
|
||||
roleDataScope := roleDataAccessCodeItemList[2]
|
||||
contexts.SetDataValue(ctx, "currentRoleId", roleId)
|
||||
contexts.SetDataValue(ctx, "currentRoleDataScope", roleDataScope)
|
||||
g.Log().Debugf(ctx, "sMiddleware.Auth currentRoleId: %s, currentRoleDataScope: %s", roleId, roleDataScope)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if hasItem {
|
||||
hasPermission = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !hasPermission {
|
||||
response.JsonExit(r, consts.CodeNoPermission.Code(), consts.CodeNoPermission.Message())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
r.Middleware.Next()
|
||||
}
|
||||
|
||||
func matchAccessCode(pattern, code string) bool {
|
||||
if pattern == code {
|
||||
return true
|
||||
}
|
||||
if strings.HasSuffix(pattern, "*") {
|
||||
prefix := pattern[:len(pattern)-1]
|
||||
return strings.HasPrefix(code, prefix)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func hasAccessCode(userAccessCodeList []string, requiredCode string) bool {
|
||||
for _, userCode := range userAccessCodeList {
|
||||
if matchAccessCode(userCode, requiredCode) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *sMiddleware) IsExceptLogin(ctx context.Context, path string) bool {
|
||||
pathList := g.Cfg().MustGet(ctx, "router.exceptLogin").Strings()
|
||||
return slices.Contains(pathList, path)
|
||||
}
|
||||
|
||||
func (s *sMiddleware) OperationLog(r *ghttp.Request) {
|
||||
r.Middleware.Next()
|
||||
ctx := r.Context()
|
||||
enabledDBOperationLog := g.Cfg().MustGet(ctx, "server.enabledDBOperationLog").Bool()
|
||||
if !enabledDBOperationLog {
|
||||
return
|
||||
}
|
||||
// Replay accepts only a Run ID. Rejecting extra request fields at the
|
||||
// controller protects the Worker; redact the generic operation log as an
|
||||
// additional guard against clients submitting card/token data anyway.
|
||||
if strings.HasPrefix(r.URL.Path, "/api/v1/iot/fullSyncRuns/replay") {
|
||||
return
|
||||
}
|
||||
data, err := service.SysOperLog().AnalysisLog(ctx)
|
||||
if err != nil {
|
||||
g.Log().Errorf(context.TODO(), "sMiddleware.OperationLog error:%v", err)
|
||||
return
|
||||
}
|
||||
queue.Push(r.GetCtx(), consts.QueueSysOptLog, data)
|
||||
}
|
||||
|
||||
func (s *sMiddleware) ResponseHandlerRaw(r *ghttp.Request) {
|
||||
r.Middleware.Next()
|
||||
if r.Response.BufferLength() > 0 {
|
||||
g.Log().Infof(r.Context(), "ResponseHandler r.Response.BufferLength() %d buf:%s", r.Response.BufferLength(), r.Response.BufferString())
|
||||
return
|
||||
}
|
||||
|
||||
res := r.GetHandlerResponse()
|
||||
if r.GetError() != nil {
|
||||
r.Response.WriteJson(res)
|
||||
r.ExitAll()
|
||||
} else {
|
||||
r.Response.WriteJson(res)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user