chore: initial commit of ai site platform
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
396
platform/internal/authx/authx.go
Normal file
396
platform/internal/authx/authx.go
Normal file
@@ -0,0 +1,396 @@
|
||||
package authx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
type ctxKey string
|
||||
|
||||
const (
|
||||
CtxTenantID ctxKey = "tenant_id"
|
||||
CtxUserID ctxKey = "user_id"
|
||||
CtxRole ctxKey = "role"
|
||||
CtxAgentID ctxKey = "agent_id"
|
||||
CtxPerms ctxKey = "perms"
|
||||
CtxOrgUnitID ctxKey = "org_unit_id"
|
||||
)
|
||||
|
||||
// 平台级角色 → 权限映射(角色、权限一律中文命名)
|
||||
var RolePermissions = map[string][]string{
|
||||
Role超级管理员: {
|
||||
Perm管理租户, // 只管公司一级:列表/新建等;不介入公司内模块、成员、同步
|
||||
},
|
||||
Role管理员: {
|
||||
Perm读取模块, Perm写入模块, Perm发布模块,
|
||||
Perm新增数据, Perm查询数据, Perm更新数据, Perm删除数据, Perm导出数据, Perm导入数据,
|
||||
Perm查看审计, Perm上传文件, Perm下载文件,
|
||||
Perm管理智能体, Perm邀请成员, Perm管理组织,
|
||||
Perm数据同步,
|
||||
},
|
||||
Role编辑: {
|
||||
Perm读取模块, Perm写入模块,
|
||||
Perm新增数据, Perm查询数据, Perm更新数据, Perm导出数据, Perm导入数据,
|
||||
Perm上传文件, Perm下载文件,
|
||||
},
|
||||
Role只读: {
|
||||
Perm读取模块, Perm查询数据, Perm导出数据, Perm下载文件, Perm查看审计,
|
||||
},
|
||||
Role待加入: {},
|
||||
}
|
||||
|
||||
type Claims struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
Role string `json:"role,omitempty"`
|
||||
OrgUnitID int64 `json:"org_unit_id,omitempty"`
|
||||
AgentID int64 `json:"agent_id,omitempty"`
|
||||
Perms []string `json:"perms,omitempty"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
type JWTConfig struct {
|
||||
AccessSecret string
|
||||
AccessExpire int64
|
||||
}
|
||||
|
||||
func (c JWTConfig) Expire() time.Duration {
|
||||
if c.AccessExpire <= 0 {
|
||||
return 24 * time.Hour
|
||||
}
|
||||
return time.Duration(c.AccessExpire) * time.Second
|
||||
}
|
||||
|
||||
func IssueToken(cfg JWTConfig, tenantID, userID int64, role string, orgUnitID int64) (string, int64, error) {
|
||||
if cfg.AccessSecret == "" {
|
||||
return "", 0, errors.New("auth access secret empty")
|
||||
}
|
||||
if userID <= 0 {
|
||||
return "", 0, errors.New("user_id required")
|
||||
}
|
||||
if role == "" {
|
||||
role = Role管理员
|
||||
}
|
||||
role = NormalizeRole(role)
|
||||
if _, ok := RolePermissions[role]; !ok {
|
||||
return "", 0, fmt.Errorf("unknown role: %s", role)
|
||||
}
|
||||
// 待加入 / 超级管理员可无租户;其他角色必须带 tenant_id
|
||||
if tenantID <= 0 && role != Role待加入 && role != Role超级管理员 {
|
||||
return "", 0, errors.New("tenant_id and user_id required")
|
||||
}
|
||||
if tenantID < 0 {
|
||||
tenantID = 0
|
||||
}
|
||||
return signClaims(cfg, Claims{
|
||||
TenantID: tenantID,
|
||||
UserID: userID,
|
||||
Role: role,
|
||||
OrgUnitID: orgUnitID,
|
||||
})
|
||||
}
|
||||
|
||||
// IssueAgentToken 签发智能体 JWT(role=agent,权限写在 perms 声明中)。
|
||||
func IssueAgentToken(cfg JWTConfig, tenantID, agentID int64, perms []string) (string, int64, error) {
|
||||
if cfg.AccessSecret == "" {
|
||||
return "", 0, errors.New("auth access secret empty")
|
||||
}
|
||||
if tenantID <= 0 || agentID <= 0 {
|
||||
return "", 0, errors.New("tenant_id and agent_id required")
|
||||
}
|
||||
return signClaims(cfg, Claims{
|
||||
TenantID: tenantID,
|
||||
UserID: agentID,
|
||||
Role: RoleAgent,
|
||||
AgentID: agentID,
|
||||
Perms: NormalizePerms(perms),
|
||||
})
|
||||
}
|
||||
|
||||
func signClaims(cfg JWTConfig, claims Claims) (string, int64, error) {
|
||||
exp := time.Now().Add(cfg.Expire())
|
||||
claims.RegisteredClaims = jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(exp),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
Issuer: "aijianzhan-platform",
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
signed, err := token.SignedString([]byte(cfg.AccessSecret))
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
return signed, exp.Unix(), nil
|
||||
}
|
||||
|
||||
func ParseToken(cfg JWTConfig, tokenStr string) (*Claims, error) {
|
||||
if cfg.AccessSecret == "" {
|
||||
return nil, errors.New("auth access secret empty")
|
||||
}
|
||||
parser := jwt.NewParser(jwt.WithValidMethods([]string{"HS256"}))
|
||||
token, err := parser.ParseWithClaims(tokenStr, &Claims{}, func(t *jwt.Token) (any, error) {
|
||||
return []byte(cfg.AccessSecret), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
claims, ok := token.Claims.(*Claims)
|
||||
if !ok || !token.Valid {
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
||||
if claims.UserID <= 0 {
|
||||
return nil, errors.New("token missing user")
|
||||
}
|
||||
if claims.Role == "" {
|
||||
claims.Role = Role只读
|
||||
}
|
||||
claims.Role = NormalizeRole(claims.Role)
|
||||
if claims.TenantID <= 0 && claims.Role != Role待加入 && claims.Role != Role超级管理员 {
|
||||
return nil, errors.New("token missing tenant/user")
|
||||
}
|
||||
if claims.TenantID < 0 {
|
||||
claims.TenantID = 0
|
||||
}
|
||||
if claims.Role == Role智能体 && claims.AgentID <= 0 {
|
||||
claims.AgentID = claims.UserID
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func Middleware(jwtCfg JWTConfig, devAuth bool) func(http.HandlerFunc) http.HandlerFunc {
|
||||
return func(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
auth := r.Header.Get("Authorization")
|
||||
if strings.HasPrefix(strings.ToLower(auth), "bearer ") {
|
||||
raw := strings.TrimSpace(auth[7:])
|
||||
claims, err := ParseToken(jwtCfg, raw)
|
||||
if err != nil {
|
||||
WriteError(w, http.StatusUnauthorized, "invalid token: "+err.Error())
|
||||
return
|
||||
}
|
||||
ctx := WithFullClaims(r.Context(), claims.TenantID, claims.UserID, claims.Role, claims.AgentID, claims.OrgUnitID, claims.Perms)
|
||||
next(w, r.WithContext(ctx))
|
||||
return
|
||||
}
|
||||
|
||||
if devAuth {
|
||||
tenantID, _ := strconv.ParseInt(r.Header.Get("X-Tenant-Id"), 10, 64)
|
||||
userID, _ := strconv.ParseInt(r.Header.Get("X-User-Id"), 10, 64)
|
||||
role := r.Header.Get("X-Role")
|
||||
if tenantID <= 0 {
|
||||
tenantID = 1
|
||||
}
|
||||
if userID <= 0 {
|
||||
userID = 1
|
||||
}
|
||||
if role == "" {
|
||||
role = Role管理员
|
||||
}
|
||||
role = NormalizeRole(role)
|
||||
ctx := WithClaims(r.Context(), tenantID, userID, role)
|
||||
next(w, r.WithContext(ctx))
|
||||
return
|
||||
}
|
||||
|
||||
WriteError(w, http.StatusUnauthorized, "missing Authorization Bearer token")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RequirePermission 校验平台角色或智能体 JWT 内嵌权限。
|
||||
func RequirePermission(perm string) func(http.HandlerFunc) http.HandlerFunc {
|
||||
return func(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if !HasPermissionCtx(r.Context(), perm) {
|
||||
role := Role(r.Context())
|
||||
WriteError(w, http.StatusForbidden, "permission denied: "+perm+" (role="+role+")")
|
||||
return
|
||||
}
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RequireTenant 要求已加入租户;pending / 无 tenant 不可访问业务数据。
|
||||
// 超级管理员可在「进入某公司」后携带 tenant_id 访问公司内接口(可看可改,前端会多重确认)。
|
||||
func RequireTenant() func(http.HandlerFunc) http.HandlerFunc {
|
||||
return func(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if Role(r.Context()) == Role智能体 {
|
||||
if TenantID(r.Context()) <= 0 {
|
||||
WriteError(w, http.StatusForbidden, "agent missing tenant")
|
||||
return
|
||||
}
|
||||
next(w, r)
|
||||
return
|
||||
}
|
||||
if Role(r.Context()) == Role超级管理员 {
|
||||
if TenantID(r.Context()) <= 0 {
|
||||
WriteError(w, http.StatusForbidden, "请先在平台管理中进入某公司后再操作公司内部功能")
|
||||
return
|
||||
}
|
||||
next(w, r)
|
||||
return
|
||||
}
|
||||
if TenantID(r.Context()) <= 0 || Role(r.Context()) == Role待加入 {
|
||||
WriteError(w, http.StatusForbidden, "join a tenant first (pending membership)")
|
||||
return
|
||||
}
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RequirePlatformAdmin 仅平台超级管理员。
|
||||
func RequirePlatformAdmin() func(http.HandlerFunc) http.HandlerFunc {
|
||||
return func(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if !IsPlatformAdmin(Role(r.Context())) {
|
||||
WriteError(w, http.StatusForbidden, "需要超级管理员")
|
||||
return
|
||||
}
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RequireTenantBound 保留兼容:业务接口要求已绑定租户。
|
||||
func RequireTenantBound() func(http.HandlerFunc) http.HandlerFunc {
|
||||
return func(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if IsPlatformAdmin(Role(r.Context())) {
|
||||
if TenantID(r.Context()) <= 0 {
|
||||
WriteError(w, http.StatusForbidden, "请先进入某公司")
|
||||
return
|
||||
}
|
||||
next(w, r)
|
||||
return
|
||||
}
|
||||
if TenantID(r.Context()) <= 0 {
|
||||
WriteError(w, http.StatusForbidden, "请先加入公司")
|
||||
return
|
||||
}
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func HasPermission(role, perm string) bool {
|
||||
perms, ok := RolePermissions[NormalizeRole(role)]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
want := NormalizePerm(perm)
|
||||
for _, p := range perms {
|
||||
if NormalizePerm(p) == want {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func HasPermissionCtx(ctx context.Context, perm string) bool {
|
||||
role := NormalizeRole(Role(ctx))
|
||||
want := NormalizePerm(perm)
|
||||
if role == Role超级管理员 {
|
||||
// 进入公司后:超管可使用全部公司权限(平台专属「管理租户」仍保留)
|
||||
if want == Perm管理租户 {
|
||||
return true
|
||||
}
|
||||
for _, p := range CompanyPermCatalog() {
|
||||
if NormalizePerm(p) == want {
|
||||
return TenantID(ctx) > 0
|
||||
}
|
||||
}
|
||||
return HasPermission(role, want)
|
||||
}
|
||||
if role == Role智能体 {
|
||||
ok := false
|
||||
for _, p := range Perms(ctx) {
|
||||
if NormalizePerm(p) == want {
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
} else if !HasPermission(role, want) {
|
||||
return false
|
||||
}
|
||||
tid := TenantID(ctx)
|
||||
if tid > 0 && EntitlementChecker != nil {
|
||||
if !EntitlementChecker(ctx, tid, want) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func withClaims(ctx context.Context, tenantID, userID int64, role string) context.Context {
|
||||
return WithClaims(ctx, tenantID, userID, role)
|
||||
}
|
||||
|
||||
// WithClaims 写入租户/用户/角色(公开展示等场景用)。
|
||||
func WithClaims(ctx context.Context, tenantID, userID int64, role string) context.Context {
|
||||
return WithFullClaims(ctx, tenantID, userID, role, 0, 0, nil)
|
||||
}
|
||||
|
||||
func WithFullClaims(ctx context.Context, tenantID, userID int64, role string, agentID, orgUnitID int64, perms []string) context.Context {
|
||||
ctx = context.WithValue(ctx, CtxTenantID, tenantID)
|
||||
ctx = context.WithValue(ctx, CtxUserID, userID)
|
||||
ctx = context.WithValue(ctx, CtxRole, role)
|
||||
ctx = context.WithValue(ctx, CtxAgentID, agentID)
|
||||
ctx = context.WithValue(ctx, CtxOrgUnitID, orgUnitID)
|
||||
if perms != nil {
|
||||
ctx = context.WithValue(ctx, CtxPerms, append([]string{}, perms...))
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
func TenantID(ctx context.Context) int64 {
|
||||
v, _ := ctx.Value(CtxTenantID).(int64)
|
||||
return v
|
||||
}
|
||||
|
||||
func UserID(ctx context.Context) int64 {
|
||||
v, _ := ctx.Value(CtxUserID).(int64)
|
||||
return v
|
||||
}
|
||||
|
||||
func AgentID(ctx context.Context) int64 {
|
||||
v, _ := ctx.Value(CtxAgentID).(int64)
|
||||
return v
|
||||
}
|
||||
|
||||
func OrgUnitID(ctx context.Context) int64 {
|
||||
v, _ := ctx.Value(CtxOrgUnitID).(int64)
|
||||
return v
|
||||
}
|
||||
|
||||
func Role(ctx context.Context) string {
|
||||
v, _ := ctx.Value(CtxRole).(string)
|
||||
if v == "" {
|
||||
return Role只读
|
||||
}
|
||||
return NormalizeRole(v)
|
||||
}
|
||||
|
||||
func Perms(ctx context.Context) []string {
|
||||
v, _ := ctx.Value(CtxPerms).([]string)
|
||||
return v
|
||||
}
|
||||
|
||||
func WriteError(w http.ResponseWriter, code int, msg string) {
|
||||
httpx.WriteJson(w, code, map[string]any{
|
||||
"code": code,
|
||||
"message": msg,
|
||||
})
|
||||
}
|
||||
78
platform/internal/authx/authx_test.go
Normal file
78
platform/internal/authx/authx_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package authx_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"aijianzhan/platform/internal/authx"
|
||||
)
|
||||
|
||||
func TestIssueAndParseToken(t *testing.T) {
|
||||
cfg := authx.JWTConfig{AccessSecret: "test-secret", AccessExpire: 3600}
|
||||
tok, exp, err := authx.IssueToken(cfg, 9, 42, "owner", 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if tok == "" || exp <= 0 {
|
||||
t.Fatal("empty token")
|
||||
}
|
||||
claims, err := authx.ParseToken(cfg, tok)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if claims.TenantID != 9 || claims.UserID != 42 {
|
||||
t.Fatalf("claims mismatch: %+v", claims)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiddlewareRequiresJWT(t *testing.T) {
|
||||
cfg := authx.JWTConfig{AccessSecret: "test-secret", AccessExpire: 3600}
|
||||
mw := authx.Middleware(cfg, false)
|
||||
called := false
|
||||
h := mw(func(w http.ResponseWriter, r *http.Request) {
|
||||
called = true
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/x", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
h(rr, req)
|
||||
if rr.Code != http.StatusUnauthorized || called {
|
||||
t.Fatalf("expected 401 without token, got %d called=%v", rr.Code, called)
|
||||
}
|
||||
|
||||
tok, _, err := authx.IssueToken(cfg, 1, 2, "owner", 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req2 := httptest.NewRequest(http.MethodGet, "/x", nil)
|
||||
req2.Header.Set("Authorization", "Bearer "+tok)
|
||||
rr2 := httptest.NewRecorder()
|
||||
h(rr2, req2)
|
||||
if rr2.Code != http.StatusOK || !called {
|
||||
t.Fatalf("expected 200 with token, got %d", rr2.Code)
|
||||
}
|
||||
if authx.TenantID(req2.Context()) != 0 {
|
||||
// context is on the request passed to next; check via handler
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiddlewareInjectsClaims(t *testing.T) {
|
||||
cfg := authx.JWTConfig{AccessSecret: "test-secret", AccessExpire: 3600}
|
||||
tok, _, _ := authx.IssueToken(cfg, 7, 8, "editor", 0)
|
||||
mw := authx.Middleware(cfg, false)
|
||||
var gotTenant, gotUser int64
|
||||
h := mw(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotTenant = authx.TenantID(r.Context())
|
||||
gotUser = authx.UserID(r.Context())
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
req := httptest.NewRequest(http.MethodGet, "/x", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+tok)
|
||||
rr := httptest.NewRecorder()
|
||||
h(rr, req)
|
||||
if gotTenant != 7 || gotUser != 8 {
|
||||
t.Fatalf("got tenant=%d user=%d", gotTenant, gotUser)
|
||||
}
|
||||
}
|
||||
117
platform/internal/authx/catalog.go
Normal file
117
platform/internal/authx/catalog.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package authx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CompanyPermCatalog 公司可被授予的权限模块(不含平台专属「管理租户」)。
|
||||
func CompanyPermCatalog() []string {
|
||||
return []string{
|
||||
Perm读取模块, Perm写入模块, Perm发布模块,
|
||||
Perm查询数据, Perm新增数据, Perm更新数据, Perm删除数据, Perm导入数据, Perm导出数据,
|
||||
Perm下载文件, Perm上传文件,
|
||||
Perm查看审计,
|
||||
Perm管理智能体, Perm邀请成员, Perm管理组织,
|
||||
Perm数据同步,
|
||||
}
|
||||
}
|
||||
|
||||
// PermModule 权限模块分组。
|
||||
type PermModule struct {
|
||||
Title string `json:"title"`
|
||||
Items []PermItem `json:"items"`
|
||||
}
|
||||
|
||||
type PermItem struct {
|
||||
Perm string `json:"perm"`
|
||||
Desc string `json:"desc"`
|
||||
}
|
||||
|
||||
// PermDesc 权限说明(超管授权 / 公司分配时展示)。
|
||||
func PermDesc(p string) string {
|
||||
switch NormalizePerm(p) {
|
||||
case Perm读取模块:
|
||||
return "查看模块列表、蓝图与智能体胶囊"
|
||||
case Perm写入模块:
|
||||
return "保存在建草稿(不发布)"
|
||||
case Perm发布模块:
|
||||
return "发布/更新线上模块"
|
||||
case Perm查询数据:
|
||||
return "查询业务列表与详情"
|
||||
case Perm新增数据:
|
||||
return "新增业务数据行"
|
||||
case Perm更新数据:
|
||||
return "更新业务数据行"
|
||||
case Perm删除数据:
|
||||
return "删除业务数据行"
|
||||
case Perm导入数据:
|
||||
return "导入 Excel/CSV/JSON"
|
||||
case Perm导出数据:
|
||||
return "导出业务数据"
|
||||
case Perm下载文件:
|
||||
return "下载已上传文件"
|
||||
case Perm上传文件:
|
||||
return "上传附件与素材"
|
||||
case Perm查看审计:
|
||||
return "查看操作审计日志"
|
||||
case Perm管理智能体:
|
||||
return "管理智能体账号与角色权限"
|
||||
case Perm邀请成员:
|
||||
return "生成邀请码、管理成员角色"
|
||||
case Perm管理组织:
|
||||
return "管理组织单元与成员归属"
|
||||
case Perm数据同步:
|
||||
return "配置跨库数据同步通道"
|
||||
case Perm管理租户:
|
||||
return "平台级:管理公司与权限额度"
|
||||
default:
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
func PermModules() []PermModule {
|
||||
mk := func(title string, perms ...string) PermModule {
|
||||
items := make([]PermItem, 0, len(perms))
|
||||
for _, p := range perms {
|
||||
items = append(items, PermItem{Perm: p, Desc: PermDesc(p)})
|
||||
}
|
||||
return PermModule{Title: title, Items: items}
|
||||
}
|
||||
return []PermModule{
|
||||
mk("模块", Perm读取模块, Perm写入模块, Perm发布模块),
|
||||
mk("数据", Perm查询数据, Perm新增数据, Perm更新数据, Perm删除数据, Perm导入数据, Perm导出数据),
|
||||
mk("文件", Perm下载文件, Perm上传文件),
|
||||
mk("审计", Perm查看审计),
|
||||
mk("公司管理", Perm管理智能体, Perm邀请成员, Perm管理组织),
|
||||
mk("数据同步", Perm数据同步),
|
||||
}
|
||||
}
|
||||
|
||||
// FilterWithinAllowance 只保留额度内的权限。
|
||||
func FilterWithinAllowance(want, allowance []string) ([]string, []string) {
|
||||
allow := map[string]struct{}{}
|
||||
for _, p := range NormalizePerms(allowance) {
|
||||
allow[p] = struct{}{}
|
||||
}
|
||||
var ok, denied []string
|
||||
for _, p := range NormalizePerms(want) {
|
||||
if _, hit := allow[p]; hit {
|
||||
ok = append(ok, p)
|
||||
} else {
|
||||
denied = append(denied, p)
|
||||
}
|
||||
}
|
||||
return ok, denied
|
||||
}
|
||||
|
||||
var EntitlementChecker func(ctx context.Context, tenantID int64, perm string) bool
|
||||
|
||||
func AssertWithinEntitlement(want, allowance []string) error {
|
||||
_, denied := FilterWithinAllowance(want, allowance)
|
||||
if len(denied) == 0 {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("超出公司权限额度: %s", strings.Join(denied, "、"))
|
||||
}
|
||||
86
platform/internal/authx/perms.go
Normal file
86
platform/internal/authx/perms.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package authx
|
||||
|
||||
// 权限统一用中文命名。英文旧码仍可通过 NormalizePerm 兼容。
|
||||
const (
|
||||
Perm读取模块 = "读取模块"
|
||||
Perm写入模块 = "写入模块"
|
||||
Perm发布模块 = "发布模块"
|
||||
Perm新增数据 = "新增数据"
|
||||
Perm查询数据 = "查询数据"
|
||||
Perm更新数据 = "更新数据"
|
||||
Perm删除数据 = "删除数据"
|
||||
Perm导出数据 = "导出数据"
|
||||
Perm导入数据 = "导入数据"
|
||||
Perm查看审计 = "查看审计"
|
||||
Perm上传文件 = "上传文件"
|
||||
Perm下载文件 = "下载文件"
|
||||
Perm管理智能体 = "管理智能体"
|
||||
Perm邀请成员 = "邀请成员"
|
||||
Perm管理组织 = "管理组织"
|
||||
Perm数据同步 = "数据同步"
|
||||
Perm管理租户 = "管理租户" // 平台超级管理员:跨公司
|
||||
)
|
||||
|
||||
// permAlias:英文旧码 → 中文规范名;中文自身映射到自身。
|
||||
var permAlias = map[string]string{
|
||||
"app.read": Perm读取模块,
|
||||
"app.write": Perm写入模块,
|
||||
"app.admin": Perm发布模块,
|
||||
"row.create": Perm新增数据,
|
||||
"row.read": Perm查询数据,
|
||||
"row.update": Perm更新数据,
|
||||
"row.delete": Perm删除数据,
|
||||
"row.export": Perm导出数据,
|
||||
"row.import": Perm导入数据,
|
||||
"audit.read": Perm查看审计,
|
||||
"storage.write": Perm上传文件,
|
||||
"storage.read": Perm下载文件,
|
||||
"agent.admin": Perm管理智能体,
|
||||
"tenant.invite": Perm邀请成员,
|
||||
"org.admin": Perm管理组织,
|
||||
"sync.admin": Perm数据同步,
|
||||
"tenant.admin": Perm管理租户,
|
||||
Perm读取模块: Perm读取模块,
|
||||
Perm写入模块: Perm写入模块,
|
||||
Perm发布模块: Perm发布模块,
|
||||
Perm新增数据: Perm新增数据,
|
||||
Perm查询数据: Perm查询数据,
|
||||
Perm更新数据: Perm更新数据,
|
||||
Perm删除数据: Perm删除数据,
|
||||
Perm导出数据: Perm导出数据,
|
||||
Perm导入数据: Perm导入数据,
|
||||
Perm查看审计: Perm查看审计,
|
||||
Perm上传文件: Perm上传文件,
|
||||
Perm下载文件: Perm下载文件,
|
||||
Perm管理智能体: Perm管理智能体,
|
||||
Perm邀请成员: Perm邀请成员,
|
||||
Perm管理组织: Perm管理组织,
|
||||
Perm数据同步: Perm数据同步,
|
||||
Perm管理租户: Perm管理租户,
|
||||
}
|
||||
|
||||
// NormalizePerm 将权限统一为中文规范名(未知则原样返回)。
|
||||
func NormalizePerm(p string) string {
|
||||
if c, ok := permAlias[p]; ok {
|
||||
return c
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// NormalizePerms 去重并转为中文规范名。
|
||||
func NormalizePerms(perms []string) []string {
|
||||
seen := map[string]struct{}{}
|
||||
out := make([]string, 0, len(perms))
|
||||
for _, p := range perms {
|
||||
c := NormalizePerm(p)
|
||||
if c == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[c]; ok {
|
||||
continue
|
||||
}
|
||||
seen[c] = struct{}{}
|
||||
out = append(out, c)
|
||||
}
|
||||
return out
|
||||
}
|
||||
93
platform/internal/authx/roles.go
Normal file
93
platform/internal/authx/roles.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package authx
|
||||
|
||||
// 平台账号角色(JWT role)——统一中文命名。
|
||||
const (
|
||||
Role超级管理员 = "超级管理员" // 平台级:可管理所有公司
|
||||
Role管理员 = "管理员" // 公司顶级
|
||||
Role编辑 = "编辑"
|
||||
Role只读 = "只读"
|
||||
Role待加入 = "待加入"
|
||||
Role智能体 = "智能体"
|
||||
)
|
||||
|
||||
// RoleAgent 智能体 JWT 角色(与 Role智能体 相同)。
|
||||
const RoleAgent = Role智能体
|
||||
|
||||
// 智能体租户角色编码(roles 表 code)——统一中文。
|
||||
const (
|
||||
AgentRole生成发布 = "生成发布"
|
||||
AgentRole只读 = "只读"
|
||||
AgentRole读写 = "读写"
|
||||
AgentRole运维 = "运维"
|
||||
)
|
||||
|
||||
// NormalizeRole 将平台角色统一为中文(兼容英文旧码)。
|
||||
func NormalizeRole(role string) string {
|
||||
switch role {
|
||||
case "platform_admin", "super_admin", Role超级管理员:
|
||||
return Role超级管理员
|
||||
case "owner", Role管理员:
|
||||
return Role管理员
|
||||
case "editor", Role编辑:
|
||||
return Role编辑
|
||||
case "viewer", Role只读:
|
||||
return Role只读
|
||||
case "pending", Role待加入:
|
||||
return Role待加入
|
||||
case "agent", Role智能体:
|
||||
return Role智能体
|
||||
default:
|
||||
return role
|
||||
}
|
||||
}
|
||||
|
||||
// NormalizeAgentRoleCode 智能体角色编码 → 中文(兼容英文旧码)。
|
||||
func NormalizeAgentRoleCode(code string) string {
|
||||
switch code {
|
||||
case "publisher", AgentRole生成发布:
|
||||
return AgentRole生成发布
|
||||
case "viewer", AgentRole只读:
|
||||
return AgentRole只读
|
||||
case "editor", AgentRole读写:
|
||||
return AgentRole读写
|
||||
case "operator", AgentRole运维:
|
||||
return AgentRole运维
|
||||
default:
|
||||
return code
|
||||
}
|
||||
}
|
||||
|
||||
// IsPlatformAdmin 是否平台超级管理员(可跨公司)。
|
||||
func IsPlatformAdmin(role string) bool {
|
||||
return NormalizeRole(role) == Role超级管理员
|
||||
}
|
||||
|
||||
// IsOwner 是否公司顶级管理员(不含超级管理员)。
|
||||
func IsOwner(role string) bool {
|
||||
return NormalizeRole(role) == Role管理员
|
||||
}
|
||||
|
||||
// IsCompanyAdmin 公司内顶级管理员(超级管理员不介入公司内部)。
|
||||
func IsCompanyAdmin(role string) bool {
|
||||
return NormalizeRole(role) == Role管理员
|
||||
}
|
||||
|
||||
// IsAgent 是否智能体账号。
|
||||
func IsAgent(role string) bool {
|
||||
return NormalizeRole(role) == Role智能体
|
||||
}
|
||||
|
||||
// RoleLabel 平台角色 → 中文显示(已是中文则原样)。
|
||||
func RoleLabel(role string) string {
|
||||
return NormalizeRole(role)
|
||||
}
|
||||
|
||||
// ValidPlatformRole 是否可赋予人类成员的角色(不含待加入/智能体/超级管理员)。
|
||||
func ValidPlatformRole(role string) bool {
|
||||
switch NormalizeRole(role) {
|
||||
case Role管理员, Role编辑, Role只读:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user