sync: 同步 v1.0.4 开源代码并脱敏
This commit is contained in:
@@ -2,6 +2,8 @@ package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
@@ -138,7 +140,13 @@ func (s *sMiddleware) ApiAuth(r *ghttp.Request) {
|
||||
response.JsonExit(r, gcode.CodeNotAuthorized.Code(), consts.CodeLoginExpired.Message())
|
||||
return
|
||||
}
|
||||
if !s.hasOpenApiPermission(ctx, r.Method, path) {
|
||||
allowed, err := s.hasOpenApiPermission(ctx, r.Method, path)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMiddleware.ApiAuth permission check failed, method: %s, path: %s, err: %v", r.Method, path, err)
|
||||
response.JsonExit(r, consts.CodePermissionCheckFailed.Code(), consts.CodePermissionCheckFailed.Message())
|
||||
return
|
||||
}
|
||||
if !allowed {
|
||||
response.JsonExit(r, consts.CodeNoPermission.Code(), consts.CodeNoPermission.Message())
|
||||
return
|
||||
}
|
||||
@@ -162,33 +170,36 @@ func openApiMethodValues(method string) []string {
|
||||
return values
|
||||
}
|
||||
|
||||
func (s *sMiddleware) hasOpenApiPermission(ctx context.Context, method, path string) bool {
|
||||
// hasOpenApiPermission 判定当前 API 账号对指定路由是否已授权。
|
||||
// 返回值 error 仅代表权限数据查询本身的系统故障,与「无权限」(false, nil) 严格区分:
|
||||
// 调用方对 error 返回系统异常码,对 false 返回无权限码,避免把数据库故障误报成授权问题。
|
||||
// 未登记资源、资源停用、账号不可用、未绑定权限组均按无权限处理。
|
||||
func (s *sMiddleware) hasOpenApiPermission(ctx context.Context, method, path string) (bool, error) {
|
||||
agentUser := contexts.GetAgentUser(ctx)
|
||||
if agentUser == nil {
|
||||
return false
|
||||
}
|
||||
if path == "/openapi/v3/query/token" {
|
||||
return true
|
||||
return false, nil
|
||||
}
|
||||
|
||||
path = strings.TrimSpace(path)
|
||||
if path != "/" {
|
||||
path = strings.TrimRight(path, "/")
|
||||
}
|
||||
// 每条受保护路由(含 /common 兼容前缀的别名路由)都必须显式登记资源并完成授权,
|
||||
// 资源查询失败不得降级为放行。
|
||||
var apiResource entity.TApiResource
|
||||
if err := dao.TApiResource.Ctx(ctx).
|
||||
err := dao.TApiResource.Ctx(ctx).
|
||||
WhereIn(dao.TApiResource.Columns().Method, openApiMethodValues(method)).
|
||||
Where(dao.TApiResource.Columns().Path, path).
|
||||
Scan(&apiResource); err != nil {
|
||||
g.Log().Warningf(ctx, "sMiddleware.hasOpenApiPermission resource not configured, method: %s, path: %s, err: %v", method, path, err)
|
||||
return true
|
||||
Scan(&apiResource)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return false, gerror.Wrapf(err, "query t_api_resource failed, method: %s, path: %s", method, path)
|
||||
}
|
||||
if apiResource.Id == 0 {
|
||||
if err != nil || apiResource.Id == 0 {
|
||||
g.Log().Warningf(ctx, "sMiddleware.hasOpenApiPermission resource not configured, method: %s, path: %s", method, path)
|
||||
return true
|
||||
return false, nil
|
||||
}
|
||||
if apiResource.Status == 1 {
|
||||
return false
|
||||
return false, nil
|
||||
}
|
||||
|
||||
var tApi entity.TApi
|
||||
@@ -199,19 +210,22 @@ func (s *sMiddleware) hasOpenApiPermission(ctx context.Context, method, path str
|
||||
apiMod = apiMod.Where(dao.TApi.Columns().AppKey, agentUser.AppKey)
|
||||
} else {
|
||||
g.Log().Warningf(ctx, "sMiddleware.hasOpenApiPermission api identity is empty, resourceApiId: %d, path: %s", apiResource.Id, path)
|
||||
return false
|
||||
return false, nil
|
||||
}
|
||||
if err := apiMod.Scan(&tApi); err != nil {
|
||||
g.Log().Errorf(ctx, "sMiddleware.hasOpenApiPermission scan t_api error: %v", err)
|
||||
return false
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
g.Log().Warningf(ctx, "sMiddleware.hasOpenApiPermission t_api not found, resourceApiId: %d, path: %s", apiResource.Id, path)
|
||||
return false, nil
|
||||
}
|
||||
return false, gerror.Wrapf(err, "query t_api failed, resourceApiId: %d, path: %s", apiResource.Id, path)
|
||||
}
|
||||
if tApi.Id == 0 || tApi.Status == 1 {
|
||||
g.Log().Warningf(ctx, "sMiddleware.hasOpenApiPermission t_api unavailable, tApiId: %d, status: %d, resourceApiId: %d, path: %s", tApi.Id, tApi.Status, apiResource.Id, path)
|
||||
return false
|
||||
return false, nil
|
||||
}
|
||||
if tApi.GroupId <= 0 {
|
||||
g.Log().Warningf(ctx, "sMiddleware.hasOpenApiPermission groupId is empty, tApiId: %d, appKey: %s, resourceApiId: %d, path: %s", tApi.Id, tApi.AppKey, apiResource.Id, path)
|
||||
return false
|
||||
return false, nil
|
||||
}
|
||||
|
||||
count, err := dao.TApiGroupResource.Ctx(ctx).
|
||||
@@ -219,10 +233,9 @@ func (s *sMiddleware) hasOpenApiPermission(ctx context.Context, method, path str
|
||||
Where(dao.TApiGroupResource.Columns().ApiId, apiResource.Id).
|
||||
Count()
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "sMiddleware.hasOpenApiPermission scan relation error: %v", err)
|
||||
return false
|
||||
return false, gerror.Wrapf(err, "query t_api_group_resource failed, groupId: %d, resourceApiId: %d, path: %s", tApi.GroupId, apiResource.Id, path)
|
||||
}
|
||||
return count > 0
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (m *sMiddleware) OldResponseHandler(r *ghttp.Request) {
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-openapi/internal/model"
|
||||
)
|
||||
|
||||
func TestOpenAPIPermission(t *testing.T) {
|
||||
ctx := contexts.Set(context.Background(), &model.Context{
|
||||
AgentUser: &model.BaseClaimsApi{ApiId: 1, AppKey: "test-key"},
|
||||
})
|
||||
if err := gdb.SetConfig(gdb.Config{"default": gdb.ConfigGroup{{Type: "sqlite", Link: "sqlite::@file(:memory:)", MaxOpenConnCount: 1}}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = g.DB().Close(ctx) })
|
||||
if err := g.DB().GetCore().ClearTableFieldsAll(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
exec := func(sql string) {
|
||||
t.Helper()
|
||||
if _, err := g.DB().Exec(ctx, sql); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
exec(`CREATE TABLE t_api_resource (id INTEGER PRIMARY KEY, method TEXT, path TEXT, status INTEGER)`)
|
||||
exec(`CREATE TABLE t_api (id INTEGER PRIMARY KEY, app_key TEXT, group_id INTEGER, status INTEGER)`)
|
||||
exec(`CREATE TABLE t_api_group_resource (group_id INTEGER, api_id INTEGER)`)
|
||||
exec(`INSERT INTO t_api VALUES (1, 'test-key', 7, 0)`)
|
||||
exec(`INSERT INTO t_api_resource VALUES
|
||||
(1, '1', '/openapi/v3/change/sim-subscribe', 0),
|
||||
(2, 'POST', '/openapi/v3/query/sim-info', 0),
|
||||
(3, '1', '/openapi/v3/query/sim-status', 1),
|
||||
(4, '1', '/openapi/v3/query/sim-imei-batch', 0)`)
|
||||
exec(`INSERT INTO t_api_group_resource VALUES (7, 1), (7, 2), (7, 3)`)
|
||||
|
||||
middleware := New()
|
||||
check := func(t *testing.T, method, path string) (bool, error) {
|
||||
t.Helper()
|
||||
return middleware.hasOpenApiPermission(ctx, method, path)
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
name, method, path string
|
||||
want bool
|
||||
}{
|
||||
{"authorized numeric method", "POST", "/openapi/v3/change/sim-subscribe", true},
|
||||
{"authorized text method", "POST", "/openapi/v3/query/sim-info", true},
|
||||
{"normalized path", "post", "/openapi/v3/query/sim-info/", true},
|
||||
{"unregistered resource", "POST", "/openapi/v3/query/batch-status", false},
|
||||
{"legacy alias path never inherits grants", "POST", "/openapi/v3/common/sim-subscribe", false},
|
||||
{"wrong method", "GET", "/openapi/v3/change/sim-subscribe", false},
|
||||
{"disabled resource", "POST", "/openapi/v3/query/sim-status", false},
|
||||
{"missing group binding", "POST", "/openapi/v3/query/sim-imei-batch", false},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := check(t, tc.method, tc.path)
|
||||
if err != nil {
|
||||
t.Fatalf("business denial must not report a system failure: %v", err)
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Fatalf("permission = %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Token endpoints are exempted by router.exceptLogin only; the middleware must not
|
||||
// carry its own whitelist, otherwise the two lists drift apart.
|
||||
if got, err := check(t, "POST", "/openapi/v3/query/token"); err != nil || got {
|
||||
t.Fatalf("token path must not be whitelisted inside middleware, got=%v err=%v", got, err)
|
||||
}
|
||||
|
||||
// The /common alias prefix has been removed from the router (router/common.go deleted).
|
||||
// Keep the path-level guard anyway: if an alias is ever re-introduced, it must still be
|
||||
// registered and granted on its own full path instead of inheriting /change permissions.
|
||||
exec(`INSERT INTO t_api_resource VALUES (5, '1', '/openapi/v3/common/sim-subscribe', 0)`)
|
||||
if got, err := check(t, "POST", "/openapi/v3/common/sim-subscribe"); err != nil || got {
|
||||
t.Fatalf("registered compatibility route without grant must be denied, got=%v err=%v", got, err)
|
||||
}
|
||||
exec(`INSERT INTO t_api_group_resource VALUES (7, 5)`)
|
||||
if got, err := check(t, "POST", "/openapi/v3/common/sim-subscribe"); err != nil || !got {
|
||||
t.Fatalf("explicitly granted compatibility route must be allowed, got=%v err=%v", got, err)
|
||||
}
|
||||
|
||||
// Unusable accounts are a business denial, not a system failure.
|
||||
for _, sql := range []string{
|
||||
`UPDATE t_api SET status = 1`,
|
||||
`UPDATE t_api SET status = 0, group_id = 0`,
|
||||
`DELETE FROM t_api`,
|
||||
} {
|
||||
exec(sql)
|
||||
got, err := check(t, "POST", "/openapi/v3/query/sim-info")
|
||||
if err != nil {
|
||||
t.Fatalf("account rejection must not be reported as a system failure after %s: %v", sql, err)
|
||||
}
|
||||
if got {
|
||||
t.Fatalf("invalid account allowed after %s", sql)
|
||||
}
|
||||
}
|
||||
|
||||
// A broken permission lookup is a system failure: it must deny AND surface the error,
|
||||
// so callers can answer 10016 instead of misreporting it as 10014 no-permission.
|
||||
exec(`DROP TABLE t_api_resource`)
|
||||
got, err := check(t, "POST", "/openapi/v3/query/sim-info")
|
||||
if err == nil {
|
||||
t.Fatal("resource lookup failure must return an error")
|
||||
}
|
||||
if got {
|
||||
t.Fatal("resource lookup failure must deny access")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user