feat/all-pa #3

Merged
daniel.w merged 5 commits from feat/all-pa into main 2024-08-18 14:09:52 +00:00
31 changed files with 2296 additions and 62 deletions

View File

@ -16,11 +16,20 @@ type (
AuthorizationReq = permission.AuthorizationReq
CancelOneTimeTokenReq = permission.CancelOneTimeTokenReq
CancelTokenReq = permission.CancelTokenReq
CheckPermissionByRoleReq = permission.CheckPermissionByRoleReq
CreateOneTimeTokenReq = permission.CreateOneTimeTokenReq
CreateOneTimeTokenResp = permission.CreateOneTimeTokenResp
DoTokenByDeviceIDReq = permission.DoTokenByDeviceIDReq
DoTokenByUIDReq = permission.DoTokenByUIDReq
GetPermissionStatusByPathReq = permission.GetPermissionStatusByPathReq
ListPermissionResp = permission.ListPermissionResp
ListPermissionStatusResp = permission.ListPermissionStatusResp
MapPermissionStatusResp = permission.MapPermissionStatusResp
NoneReq = permission.NoneReq
OKResp = permission.OKResp
PermissionItem = permission.PermissionItem
PermissionResp = permission.PermissionResp
PermissionStatusItem = permission.PermissionStatusItem
QueryTokenByUIDReq = permission.QueryTokenByUIDReq
RefreshTokenReq = permission.RefreshTokenReq
RefreshTokenResp = permission.RefreshTokenResp
@ -31,6 +40,14 @@ type (
ValidationTokenResp = permission.ValidationTokenResp
PermissionService interface {
// ListPermissionStatus 取得所有權限狀態列表,給前端表演用
ListPermissionStatus(ctx context.Context, in *NoneReq, opts ...grpc.CallOption) (*ListPermissionStatusResp, error)
// MapPermissionStatus 取得所有權限開閉狀態,簡易版,給前端表演用
MapPermissionStatus(ctx context.Context, in *NoneReq, opts ...grpc.CallOption) (*MapPermissionStatusResp, error)
// CheckPermissionByRole 透過角色 ID 來檢視權限,後台要通過時真的看這個
CheckPermissionByRole(ctx context.Context, in *CheckPermissionByRoleReq, opts ...grpc.CallOption) (*PermissionResp, error)
// GetPermissionStatusByPath 透過資源拿取角色的狀態
GetPermissionStatusByPath(ctx context.Context, in *GetPermissionStatusByPathReq, opts ...grpc.CallOption) (*PermissionStatusItem, error)
}
defaultPermissionService struct {
@ -43,3 +60,27 @@ func NewPermissionService(cli zrpc.Client) PermissionService {
cli: cli,
}
}
// ListPermissionStatus 取得所有權限狀態列表,給前端表演用
func (m *defaultPermissionService) ListPermissionStatus(ctx context.Context, in *NoneReq, opts ...grpc.CallOption) (*ListPermissionStatusResp, error) {
client := permission.NewPermissionServiceClient(m.cli.Conn())
return client.ListPermissionStatus(ctx, in, opts...)
}
// MapPermissionStatus 取得所有權限開閉狀態,簡易版,給前端表演用
func (m *defaultPermissionService) MapPermissionStatus(ctx context.Context, in *NoneReq, opts ...grpc.CallOption) (*MapPermissionStatusResp, error) {
client := permission.NewPermissionServiceClient(m.cli.Conn())
return client.MapPermissionStatus(ctx, in, opts...)
}
// CheckPermissionByRole 透過角色 ID 來檢視權限,後台要通過時真的看這個
func (m *defaultPermissionService) CheckPermissionByRole(ctx context.Context, in *CheckPermissionByRoleReq, opts ...grpc.CallOption) (*PermissionResp, error) {
client := permission.NewPermissionServiceClient(m.cli.Conn())
return client.CheckPermissionByRole(ctx, in, opts...)
}
// GetPermissionStatusByPath 透過資源拿取角色的狀態
func (m *defaultPermissionService) GetPermissionStatusByPath(ctx context.Context, in *GetPermissionStatusByPathReq, opts ...grpc.CallOption) (*PermissionStatusItem, error) {
client := permission.NewPermissionServiceClient(m.cli.Conn())
return client.GetPermissionStatusByPath(ctx, in, opts...)
}

View File

@ -16,11 +16,20 @@ type (
AuthorizationReq = permission.AuthorizationReq
CancelOneTimeTokenReq = permission.CancelOneTimeTokenReq
CancelTokenReq = permission.CancelTokenReq
CheckPermissionByRoleReq = permission.CheckPermissionByRoleReq
CreateOneTimeTokenReq = permission.CreateOneTimeTokenReq
CreateOneTimeTokenResp = permission.CreateOneTimeTokenResp
DoTokenByDeviceIDReq = permission.DoTokenByDeviceIDReq
DoTokenByUIDReq = permission.DoTokenByUIDReq
GetPermissionStatusByPathReq = permission.GetPermissionStatusByPathReq
ListPermissionResp = permission.ListPermissionResp
ListPermissionStatusResp = permission.ListPermissionStatusResp
MapPermissionStatusResp = permission.MapPermissionStatusResp
NoneReq = permission.NoneReq
OKResp = permission.OKResp
PermissionItem = permission.PermissionItem
PermissionResp = permission.PermissionResp
PermissionStatusItem = permission.PermissionStatusItem
QueryTokenByUIDReq = permission.QueryTokenByUIDReq
RefreshTokenReq = permission.RefreshTokenReq
RefreshTokenResp = permission.RefreshTokenResp

View File

@ -16,11 +16,20 @@ type (
AuthorizationReq = permission.AuthorizationReq
CancelOneTimeTokenReq = permission.CancelOneTimeTokenReq
CancelTokenReq = permission.CancelTokenReq
CheckPermissionByRoleReq = permission.CheckPermissionByRoleReq
CreateOneTimeTokenReq = permission.CreateOneTimeTokenReq
CreateOneTimeTokenResp = permission.CreateOneTimeTokenResp
DoTokenByDeviceIDReq = permission.DoTokenByDeviceIDReq
DoTokenByUIDReq = permission.DoTokenByUIDReq
GetPermissionStatusByPathReq = permission.GetPermissionStatusByPathReq
ListPermissionResp = permission.ListPermissionResp
ListPermissionStatusResp = permission.ListPermissionStatusResp
MapPermissionStatusResp = permission.MapPermissionStatusResp
NoneReq = permission.NoneReq
OKResp = permission.OKResp
PermissionItem = permission.PermissionItem
PermissionResp = permission.PermissionResp
PermissionStatusItem = permission.PermissionStatusItem
QueryTokenByUIDReq = permission.QueryTokenByUIDReq
RefreshTokenReq = permission.RefreshTokenReq
RefreshTokenResp = permission.RefreshTokenResp

View File

@ -10,6 +10,6 @@ RedisCluster:
Type: cluster
Token:
Expired: 300
RefreshExpires: 86500
Expired: 300s
RefreshExpires: 86500s
Secret: gg88g88

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS `permission`;

View File

@ -0,0 +1,15 @@
-- 通常會把整個表都放到記憶體當中,不常搜尋,不需要加其他搜尋的 index
CREATE TABLE `permission`
(
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'PK',
`parent` bigint unsigned DEFAULT NULL,
`name` varchar(255) NOT NULL,
`http_method` varchar(255) NOT NULL,
`http_path` text NOT NULL,
`status` tinyint NOT NULL DEFAULT '1' COMMENT '狀態 1: 啟用, 2: 關閉',
`type` tinyint NOT NULL DEFAULT '1' COMMENT '狀態 1: 後台, 2: 前台',
`create_time` bigint DEFAULT 0 NOT NULL COMMENT '創建時間',
`update_time` bigint DEFAULT 0 NOT NULL COMMENT '更新時間',
PRIMARY KEY (`id`),
UNIQUE KEY `name_unique_key` (`name`)
) ENGINE = InnoDB COMMENT ='權限表';

View File

@ -0,0 +1,15 @@
-- 一級分類
INSERT INTO `permission` (`parent`, `name`, `http_method`, `http_path`, `create_time`, `update_time`)
VALUES (0, 'user.info.management', '', '', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); -- 用戶資訊管理
# -- 二級分類 用戶資訊管理
SET @id := (SELECT id FROM `permission` where name = 'user.info.management');
INSERT INTO `permission` (`parent`, `name`, `http_method`, `http_path`, `create_time`, `update_time`)
VALUES (@id, 'user.basic.info', '', '', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); -- 用戶基礎資訊查詢
# -- 三級分類 用戶基礎資訊管理-基礎資訊查詢表
SET @id := (SELECT id FROM `permission` where name = 'user.basic.info');
INSERT INTO `permission` (`parent`, `name`, `http_method`, `http_path`, `create_time`, `update_time`)
VALUES (@id, 'user.info.select', 'GET', '/v1/user', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), -- 查詢
(@id, 'user.info.select.plain_code', 'GET', '/v1/user', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); -- 明碼查詢

View File

@ -6,6 +6,8 @@ option go_package="./permission";
// OKResp
message OKResp {}
// NoneReq
message NoneReq {}
// AuthorizationReq
message AuthorizationReq {
@ -156,8 +158,77 @@ service TokenService {
}
// --------------------------------------------------------------------------------
enum PermissionStatus {
PERMISSION_STATUS_NONE = 0; //
PERMISSION_STATUS_OPEN = 1;
PERMISSION_STATUS_CLOSE = 2;
}
message PermissionStatusItem {
int64 id =1;
int64 parent_id =2;
string name =3;
PermissionStatus status = 4;
string type =5;
bool approval = 6;
}
message ListPermissionStatusResp {
repeated PermissionStatusItem data = 1;
}
message PermissionItem{
int64 id = 1;
string name = 2;
string http_method = 3;
string http_path = 4;
optional PermissionItem parent= 5;
repeated PermissionItem children =6;
repeated int64 path_ids=7;
}
message ListPermissionResp {
repeated PermissionItem data = 1;
}
message CheckPermissionByRoleReq {
string role = 1;
string path = 2;
string method = 3;
}
message PermissionResp {
bool allow = 1;
string permission_name =2;
bool plain_code =3;
}
message GetPermissionStatusByPathReq {
string path = 2;
string method = 3;
}
message MapPermissionStatusResp {
map<int64,PermissionStatus> data = 1; // permission id : open close
}
service PermissionService {
// ListPermissionStatus ,
rpc ListPermissionStatus(NoneReq)returns(ListPermissionStatusResp);
// ListPermission
rpc ListPermission(NoneReq)returns(MapPermissionStatusResp);
// CheckPermissionByRole ID
rpc CheckPermissionByRole(CheckPermissionByRoleReq)returns(PermissionResp);
// GetPermissionStatusByPath
rpc GetPermissionStatusByPath(GetPermissionStatusByPathReq)returns(PermissionStatusItem);
}
service RoleService {
rpc Ping(OKResp) returns(OKResp);
}
service PermissionService {}

34
go.mod
View File

@ -7,6 +7,8 @@ require (
github.com/go-playground/validator/v10 v10.22.0
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/google/uuid v1.6.0
github.com/open-policy-agent/opa v0.67.1
github.com/stretchr/testify v1.9.0
github.com/zeromicro/go-zero v1.7.0
go.uber.org/mock v0.4.0
google.golang.org/grpc v1.65.0
@ -14,16 +16,20 @@ require (
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/OneOfOne/xxhash v1.2.8 // indirect
github.com/agnivade/levenshtein v1.1.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/coreos/go-semver v0.3.1 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
@ -31,12 +37,15 @@ require (
github.com/go-openapi/swag v0.22.4 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
@ -49,25 +58,32 @@ require (
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/openzipkin/zipkin-go v0.4.3 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.19.1 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
github.com/redis/go-redis/v9 v9.6.1 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/tchap/go-patricia/v2 v2.3.1 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/yashtewari/glob-intersection v0.2.0 // indirect
go.etcd.io/etcd/api/v3 v3.5.15 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.15 // indirect
go.etcd.io/etcd/client/v3 v3.5.15 // indirect
go.opentelemetry.io/otel v1.24.0 // indirect
go.opentelemetry.io/otel v1.28.0 // indirect
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 // indirect
go.opentelemetry.io/otel/exporters/zipkin v1.24.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.opentelemetry.io/otel/sdk v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect
go.opentelemetry.io/otel/metric v1.28.0 // indirect
go.opentelemetry.io/otel/sdk v1.28.0 // indirect
go.opentelemetry.io/otel/trace v1.28.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/automaxprocs v1.5.3 // indirect
@ -93,5 +109,5 @@ require (
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)

View File

@ -14,4 +14,8 @@ type Config struct {
Expired time.Duration
Secret string
}
// 加上DB結構體
DB struct {
DsnString string
}
}

View File

@ -13,6 +13,7 @@ import (
// full code 12009 只會有 系統以及錯誤碼category 是給系統判定用的
// 目前 Scope 以及分類要系統共用,係向的錯誤各自服務實作就好
// token error 方面
const (
TokenUnexpectedSigningErrorCode = iota + 1
TokenValidateErrorCode
@ -25,6 +26,11 @@ const (
RedisErrorCode
)
const (
PermissionNotFoundCode = iota + 30
PermissionGetDataErrorCode
)
// TokenUnexpectedSigningErr 30001 Token 簽名錯誤
func TokenUnexpectedSigningErr(msg string) *ers.Err {
mts.AppErrorMetrics.AddFailure("token", "token_unexpected_sign")
@ -63,3 +69,15 @@ func RedisError(msg string) *ers.Err {
mts.AppErrorMetrics.AddFailure("redis", "error")
return ers.NewErr(code.CloudEPPermission, code.CatInput, RedisErrorCode, msg)
}
// PermissionNotFoundError 30030 權限錯誤
func PermissionNotFoundError(msg string) *ers.Err {
// 看需要建立哪些 Metrics
return ers.NewErr(code.CloudEPPermission, code.Forbidden, PermissionNotFoundCode, msg)
}
// PermissionGetDataError 30031 解析權限時錯誤
func PermissionGetDataError(msg string) *ers.Err {
// 看需要建立哪些 Metrics
return ers.NewErr(code.CloudEPPermission, code.InvalidFormat, PermissionGetDataErrorCode, msg)
}

View File

@ -0,0 +1,41 @@
package domain
type PermissionType int8
const (
PermissionTypeBackendUser PermissionType = iota + 1
PermissionTypeFrontendUser
)
type PermissionTypeCode string
const (
PermissionTypeBackCode PermissionTypeCode = "back"
PermissionTypeFrontCode PermissionTypeCode = "front"
)
var permissionMap = map[int64]PermissionTypeCode{
1: PermissionTypeFrontCode,
2: PermissionTypeBackCode,
}
func ToPermissionTypeCode(code int64) (PermissionTypeCode, bool) {
result, ok := permissionMap[code]
if !ok {
return "", false
}
return result, true
}
func (t *PermissionTypeCode) ToString() string {
return string(*t)
}
type PermissionStatus string
type Permissions map[string]PermissionStatus
const (
PermissionStatusOpenCode PermissionStatus = "open"
PermissionStatusCloseCode PermissionStatus = "close"
)

View File

@ -0,0 +1,47 @@
package usecase
import (
"context"
)
type OpaUseCase interface {
// CheckRBACPermission 確認有無權限
CheckRBACPermission(ctx context.Context, req CheckReq) (CheckOPAResp, error)
// LoadPolicy 將 Policy 從其他地方加載到 opa 的 policy 當中
LoadPolicy(ctx context.Context, input []Policy) error
GetPolicy(ctx context.Context) []map[string]any
}
type CheckReq struct {
ID string
Roles []string
Path string
Method string
}
type Grant struct {
ID string
Path string
Method string
}
type Policy struct {
Methods []string `json:"methods"`
Name string `json:"name"`
Path string `json:"path"`
Role string `json:"role"`
}
type RuleRequest struct {
Method string `json:"method"`
Path string `json:"path"`
Policies []Policy `json:"policies"`
Roles []string `json:"roles"`
}
type CheckOPAResp struct {
Allow bool `json:"allow"`
PolicyName string `json:"policy_name"`
PlainCode bool `json:"plain_code"` // 是否為明碼顯示
Request RuleRequest `json:"request"`
}

View File

@ -0,0 +1,38 @@
package usecase
import (
"ark-permission/internal/domain"
"ark-permission/internal/entity"
)
// PermissionTreeManager 定義一組操作權限樹的接口
// 這個名稱說明它是專門負責管理和操作權限樹的管理器
type PermissionTreeManager interface {
// AddPermission 將一個新的權限節點插入到樹中
// key 是父節點的IDvalue 是要插入的 Permission 資料
// 此方法應該能處理節點是否存在於父節點下的情況
AddPermission(parentID int64, permission entity.Permission) error
// FindPermissionByID 根據權限 ID 查詢樹中的某個節點
// 如果節點存在,返回對應的 Permission 資料,否則返回 nil
FindPermissionByID(permissionID int64) (*Permission, error)
// GetAllParentPermissionIDs 根據傳入的 permissions 列表
// 找出每個權限的完整父節點權限 ID 路徑
// 例如,如果 B 的父權限是 A並且給了 B 權限,則返回 A 和 B 的權限 ID
GetAllParentPermissionIDs(permissions domain.Permissions) ([]int64, error)
// GetAllParentPermissionStatuses 返回給定權限下的所有完整父節點權限狀態
// 例如,若給 B 權限,該方法將返回所有與 B 相關的父權限的狀態
GetAllParentPermissionStatuses(permissions domain.Permissions) (domain.Permissions, error)
// GetRolePermissionTree 根據角色權限找出所有父節點和子節點權限狀態
// 角色權限是傳入的一個列表,該方法會根據每個角色的權限,返回所有相關的權限狀態
GetRolePermissionTree(rolePermissions []entity.RolePermission) domain.Permissions
}
type Permission struct {
ID int64 `json:"-"`
Name string `json:"name"`
HTTPMethod string `json:"http_method"`
HTTPPath string `json:"http_path"`
Parent *Permission `json:"-"`
Children []*Permission `json:"children"`
PathIDs []int64 `json:"-"` // full path id
}

View File

@ -0,0 +1,22 @@
package entity
import "ark-permission/internal/domain"
type Permission struct {
ID int64 `gorm:"column:id"`
Parent int64 `gorm:"column:parent"`
Name string `gorm:"column:name"`
HTTPMethod string `gorm:"column:http_method"`
HTTPPath string `gorm:"column:http_path"`
Status int `gorm:"column:status"`
Type domain.PermissionType `gorm:"column:type"`
CreateTime int64 `gorm:"column:create_time;autoCreateTime"`
UpdateTime int64 `gorm:"column:update_time;autoUpdateTime"`
}
func (c *Permission) TableName() string {
return "permission"
}

View File

@ -0,0 +1,14 @@
package entity
type RolePermission struct {
ID int64 `gorm:"column:id"`
RoleID int64 `gorm:"column:role_id"`
PermissionID int64 `gorm:"column:permission_id"`
CreateTime int64 `gorm:"column:create_time;autoCreateTime"`
UpdateTime int64 `gorm:"column:update_time;autoUpdateTime"`
}
func (c *RolePermission) TableName() string {
return "role_permission"
}

View File

@ -1,7 +1,7 @@
package middleware
import (
ers "ark-permission/internal/lib/error"
ers "code.30cm.net/wanderland/library-go/errors"
"context"
"errors"
"time"

View File

@ -0,0 +1,59 @@
package permissionservicelogic
import (
"ark-permission/gen_result/pb/permission"
"ark-permission/internal/domain/usecase"
"ark-permission/internal/svc"
ers "code.30cm.net/wanderland/library-go/errors"
"context"
"github.com/zeromicro/go-zero/core/logx"
)
type CheckPermissionByRoleLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
logx.Logger
}
func NewCheckPermissionByRoleLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CheckPermissionByRoleLogic {
return &CheckPermissionByRoleLogic{
ctx: ctx,
svcCtx: svcCtx,
Logger: logx.WithContext(ctx),
}
}
type checkPermissionReq struct {
Role string `json:"role" validate:"required"`
Method string `json:"method" validate:"required"`
Path string `json:"path" validate:"required"`
}
// CheckPermissionByRole 透過角色 ID 來檢視權限
func (l *CheckPermissionByRoleLogic) CheckPermissionByRole(in *permission.CheckPermissionByRoleReq) (*permission.PermissionResp, error) {
// 驗證所需
if err := l.svcCtx.Validate.ValidateAll(&checkPermissionReq{
Role: in.GetRole(),
Method: in.GetMethod(),
Path: in.GetPath(),
}); err != nil {
return nil, ers.InvalidFormat(err.Error())
}
rbacPermission, err := l.svcCtx.PolicyAgent.CheckRBACPermission(l.ctx, usecase.CheckReq{
Roles: []string{in.GetRole()},
Method: in.GetMethod(),
Path: in.GetPath(),
})
if err != nil {
return nil, ers.Forbidden(err.Error())
}
return &permission.PermissionResp{
Allow: rbacPermission.Allow,
PermissionName: rbacPermission.PolicyName,
PlainCode: rbacPermission.PlainCode,
}, nil
}

View File

@ -0,0 +1,31 @@
package permissionservicelogic
import (
"context"
"ark-permission/gen_result/pb/permission"
"ark-permission/internal/svc"
"github.com/zeromicro/go-zero/core/logx"
)
type GetPermissionStatusByPathLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
logx.Logger
}
func NewGetPermissionStatusByPathLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetPermissionStatusByPathLogic {
return &GetPermissionStatusByPathLogic{
ctx: ctx,
svcCtx: svcCtx,
Logger: logx.WithContext(ctx),
}
}
// GetPermissionStatusByPath 透過資源拿取角色的狀態
func (l *GetPermissionStatusByPathLogic) GetPermissionStatusByPath(in *permission.GetPermissionStatusByPathReq) (*permission.PermissionStatusItem, error) {
// todo: add your logic here and delete this line
return &permission.PermissionStatusItem{}, nil
}

View File

@ -0,0 +1,55 @@
package permissionservicelogic
import (
"ark-permission/internal/domain"
ers "code.30cm.net/wanderland/library-go/errors"
"context"
"ark-permission/gen_result/pb/permission"
"ark-permission/internal/svc"
"github.com/zeromicro/go-zero/core/logx"
)
type ListPermissionStatusLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
logx.Logger
}
func NewListPermissionStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListPermissionStatusLogic {
return &ListPermissionStatusLogic{
ctx: ctx,
svcCtx: svcCtx,
Logger: logx.WithContext(ctx),
}
}
// ListPermissionStatus 取得
func (l *ListPermissionStatusLogic) ListPermissionStatus(in *permission.NoneReq) (*permission.ListPermissionStatusResp, error) {
// 搜尋所有權限
permissions, err := l.svcCtx.Permission.FindAllOpenPermission(l.ctx)
if err != nil {
return nil, ers.DBError(err.Error())
}
exist := make(map[string]struct{})
status := make([]*permission.PermissionStatusItem, 0, len(permissions))
for _, v := range permissions {
if _, ok := exist[v.Name]; !ok {
t, _ := domain.ToPermissionTypeCode(v.Type)
status = append(status, &permission.PermissionStatusItem{
Id: v.Id, // 權限 ID
ParentId: v.Parent.Int64, // 上級權限的ID
Name: v.Name, // 權限名稱
Status: permission.PermissionStatus(v.Status), // 權限開啟或關閉,判斷時上級權限如果關閉,下級也應該關閉對此人關閉 // TODO 還沒做到,目前忠實呈現
Type: t.ToString(), // 前台權限,還是後台權限,還是其他中台之類的
})
exist[v.Name] = struct{}{}
}
}
return &permission.ListPermissionStatusResp{
Data: status,
}, nil
}

View File

@ -0,0 +1,31 @@
package permissionservicelogic
import (
"context"
"ark-permission/gen_result/pb/permission"
"ark-permission/internal/svc"
"github.com/zeromicro/go-zero/core/logx"
)
type MapPermissionStatusLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
logx.Logger
}
func NewMapPermissionStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) *MapPermissionStatusLogic {
return &MapPermissionStatusLogic{
ctx: ctx,
svcCtx: svcCtx,
Logger: logx.WithContext(ctx),
}
}
// MapPermissionStatus 取得所有權限開閉狀態,簡易版,給前端表演用
func (l *MapPermissionStatusLogic) MapPermissionStatus(in *permission.NoneReq) (*permission.MapPermissionStatusResp, error) {
// todo: add your logic here and delete this line
return &permission.MapPermissionStatusResp{}, nil
}

View File

@ -0,0 +1,47 @@
package model
import (
"context"
"errors"
"fmt"
"github.com/zeromicro/go-zero/core/stores/sqlc"
"github.com/zeromicro/go-zero/core/stores/sqlx"
)
var _ PermissionModel = (*customPermissionModel)(nil)
type (
// PermissionModel is an interface to be customized, add more methods here,
// and implement the added methods in customPermissionModel.
PermissionModel interface {
permissionModel
FindAllOpenPermission(ctx context.Context) ([]*Permission, error)
}
customPermissionModel struct {
*defaultPermissionModel
}
)
// NewPermissionModel 者裡不用快取版本,因為快取我想要自己控制,也就是 local cache 不想上升至 redis 的層級
// 因為我 permission 設計是由 sql 新增重啟服務就可以重啟或者是未來可以放一個mq 來同步
func NewPermissionModel(conn sqlx.SqlConn) PermissionModel {
return &customPermissionModel{
defaultPermissionModel: newPermissionModel(conn),
}
}
func (m *customPermissionModel) FindAllOpenPermission(ctx context.Context) ([]*Permission, error) {
query := fmt.Sprintf("select %s from %s where `status` = ? order by `create_time` asc", permissionRows, m.table)
var resp []*Permission
err := m.conn.QueryRowsCtx(ctx, &resp, query, 1)
switch {
case err == nil:
return resp, nil
case errors.Is(err, sqlc.ErrNotFound):
return nil, ErrNotFound
default:
return nil, err
}
}

View File

@ -0,0 +1,113 @@
// Code generated by goctl. DO NOT EDIT.
package model
import (
"context"
"database/sql"
"fmt"
"strings"
"github.com/zeromicro/go-zero/core/stores/builder"
"github.com/zeromicro/go-zero/core/stores/sqlc"
"github.com/zeromicro/go-zero/core/stores/sqlx"
"github.com/zeromicro/go-zero/core/stringx"
)
var (
permissionFieldNames = builder.RawFieldNames(&Permission{})
permissionRows = strings.Join(permissionFieldNames, ",")
permissionRowsExpectAutoSet = strings.Join(stringx.Remove(permissionFieldNames, "`id`"), ",")
permissionRowsWithPlaceHolder = strings.Join(stringx.Remove(permissionFieldNames, "`id`"), "=?,") + "=?"
)
type (
permissionModel interface {
Insert(ctx context.Context, data *Permission) (sql.Result, error)
FindOne(ctx context.Context, id int64) (*Permission, error)
FindOneByName(ctx context.Context, name string) (*Permission, error)
Update(ctx context.Context, data *Permission) error
Delete(ctx context.Context, id int64) error
}
defaultPermissionModel struct {
conn sqlx.SqlConn
table string
}
Permission struct {
Id int64 `db:"id"` // PK
Parent sql.NullInt64 `db:"parent"`
Name string `db:"name"`
HttpMethod string `db:"http_method"`
HttpPath string `db:"http_path"`
Status int64 `db:"status"` // 狀態 1: 啟用, 2: 關閉
Type int64 `db:"type"` // 狀態 1: 後台, 2: 前台
CreateTime int64 `db:"create_time"` // 創建時間
UpdateTime int64 `db:"update_time"` // 更新時間
}
)
func newPermissionModel(conn sqlx.SqlConn) *defaultPermissionModel {
return &defaultPermissionModel{
conn: conn,
table: "`permission`",
}
}
func (m *defaultPermissionModel) withSession(session sqlx.Session) *defaultPermissionModel {
return &defaultPermissionModel{
conn: sqlx.NewSqlConnFromSession(session),
table: "`permission`",
}
}
func (m *defaultPermissionModel) Delete(ctx context.Context, id int64) error {
query := fmt.Sprintf("delete from %s where `id` = ?", m.table)
_, err := m.conn.ExecCtx(ctx, query, id)
return err
}
func (m *defaultPermissionModel) FindOne(ctx context.Context, id int64) (*Permission, error) {
query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", permissionRows, m.table)
var resp Permission
err := m.conn.QueryRowCtx(ctx, &resp, query, id)
switch err {
case nil:
return &resp, nil
case sqlc.ErrNotFound:
return nil, ErrNotFound
default:
return nil, err
}
}
func (m *defaultPermissionModel) FindOneByName(ctx context.Context, name string) (*Permission, error) {
var resp Permission
query := fmt.Sprintf("select %s from %s where `name` = ? limit 1", permissionRows, m.table)
err := m.conn.QueryRowCtx(ctx, &resp, query, name)
switch err {
case nil:
return &resp, nil
case sqlc.ErrNotFound:
return nil, ErrNotFound
default:
return nil, err
}
}
func (m *defaultPermissionModel) Insert(ctx context.Context, data *Permission) (sql.Result, error) {
query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?, ?)", m.table, permissionRowsExpectAutoSet)
ret, err := m.conn.ExecCtx(ctx, query, data.Parent, data.Name, data.HttpMethod, data.HttpPath, data.Status, data.Type, data.CreateTime, data.UpdateTime)
return ret, err
}
func (m *defaultPermissionModel) Update(ctx context.Context, newData *Permission) error {
query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, permissionRowsWithPlaceHolder)
_, err := m.conn.ExecCtx(ctx, query, newData.Parent, newData.Name, newData.HttpMethod, newData.HttpPath, newData.Status, newData.Type, newData.CreateTime, newData.UpdateTime, newData.Id)
return err
}
func (m *defaultPermissionModel) tableName() string {
return m.table
}

5
internal/model/vars.go Normal file
View File

@ -0,0 +1,5 @@
package model
import "github.com/zeromicro/go-zero/core/stores/sqlx"
var ErrNotFound = sqlx.ErrNotFound

View File

@ -4,7 +4,10 @@
package server
import (
"context"
"ark-permission/gen_result/pb/permission"
"ark-permission/internal/logic/permissionservice"
"ark-permission/internal/svc"
)
@ -18,3 +21,27 @@ func NewPermissionServiceServer(svcCtx *svc.ServiceContext) *PermissionServiceSe
svcCtx: svcCtx,
}
}
// ListPermissionStatus 取得所有權限狀態列表,給前端表演用
func (s *PermissionServiceServer) ListPermissionStatus(ctx context.Context, in *permission.NoneReq) (*permission.ListPermissionStatusResp, error) {
l := permissionservicelogic.NewListPermissionStatusLogic(ctx, s.svcCtx)
return l.ListPermissionStatus(in)
}
// MapPermissionStatus 取得所有權限開閉狀態,簡易版,給前端表演用
func (s *PermissionServiceServer) MapPermissionStatus(ctx context.Context, in *permission.NoneReq) (*permission.MapPermissionStatusResp, error) {
l := permissionservicelogic.NewMapPermissionStatusLogic(ctx, s.svcCtx)
return l.MapPermissionStatus(in)
}
// CheckPermissionByRole 透過角色 ID 來檢視權限,後台要通過時真的看這個
func (s *PermissionServiceServer) CheckPermissionByRole(ctx context.Context, in *permission.CheckPermissionByRoleReq) (*permission.PermissionResp, error) {
l := permissionservicelogic.NewCheckPermissionByRoleLogic(ctx, s.svcCtx)
return l.CheckPermissionByRole(in)
}
// GetPermissionStatusByPath 透過資源拿取角色的狀態
func (s *PermissionServiceServer) GetPermissionStatusByPath(ctx context.Context, in *permission.GetPermissionStatusByPathReq) (*permission.PermissionStatusItem, error) {
l := permissionservicelogic.NewGetPermissionStatusByPathLogic(ctx, s.svcCtx)
return l.GetPermissionStatusByPath(in)
}

View File

@ -3,11 +3,16 @@ package svc
import (
"ark-permission/internal/config"
"ark-permission/internal/domain/repository"
domainUseCase "ark-permission/internal/domain/usecase"
"ark-permission/internal/lib/required"
"ark-permission/internal/model"
repo "ark-permission/internal/repository"
"ark-permission/internal/usecase"
ers "code.30cm.net/wanderland/library-go/errors"
"code.30cm.net/wanderland/library-go/errors/code"
"context"
"github.com/zeromicro/go-zero/core/stores/redis"
"github.com/zeromicro/go-zero/core/stores/sqlx"
)
type ServiceContext struct {
@ -16,6 +21,8 @@ type ServiceContext struct {
Validate required.Validate
Redis redis.Redis
TokenRedisRepo repository.TokenRepository
Permission model.PermissionModel
PolicyAgent domainUseCase.OpaUseCase
}
func NewServiceContext(c config.Config) *ServiceContext {
@ -23,7 +30,19 @@ func NewServiceContext(c config.Config) *ServiceContext {
if err != nil {
panic(err)
}
ers.Scope = code.CloudEPPermission
sqlConn := sqlx.NewMysql(c.DB.DsnString)
pa, err := usecase.NewOpaUseCase(usecase.OpaUseCaseParam{})
if err != nil {
panic(err)
}
// TODO policy 權限還要再組合過,我的角度會把 UID 當成一種 RoleID 這樣就可以針對每一個人克制權限,,初期也可以使用最簡安的來做統一,再想一下
err = pa.LoadPolicy(context.Background(), []domainUseCase.Policy{})
if err != nil {
panic(err)
}
return &ServiceContext{
Config: c,
@ -32,5 +51,7 @@ func NewServiceContext(c config.Config) *ServiceContext {
TokenRedisRepo: repo.NewTokenRepository(repo.TokenRepositoryParam{
Store: newRedis,
}),
Permission: model.NewPermissionModel(sqlConn),
PolicyAgent: pa,
}
}

198
internal/usecase/opa.go Normal file
View File

@ -0,0 +1,198 @@
package usecase
import (
"ark-permission/internal/domain"
"ark-permission/internal/domain/usecase"
ers "code.30cm.net/wanderland/library-go/errors"
"context"
_ "embed"
"fmt"
"github.com/open-policy-agent/opa/rego"
"github.com/zeromicro/go-zero/core/logx"
"strings"
)
//go:embed "rule.rego"
var policy []byte
type OpaUseCaseParam struct{}
type opaUseCase struct {
// 查詢這個角色是否可用
allowQuery rego.PreparedEvalQuery
policies []map[string]any
}
func (o *opaUseCase) GetPolicy(ctx context.Context) []map[string]any {
return o.policies
}
func (o *opaUseCase) CheckRBACPermission(ctx context.Context, req usecase.CheckReq) (usecase.CheckOPAResp, error) {
results, err := o.allowQuery.Eval(ctx, rego.EvalInput(map[string]any{
"roles": req.Roles,
"path": req.Path,
"method": req.Method,
"policies": o.policies,
}))
if err != nil {
return usecase.CheckOPAResp{}, domain.PermissionGetDataError(fmt.Sprintf("failed to evaluate policy: %v", err))
}
if len(results) == 0 {
logx.WithCallerSkip(1).WithFields(
logx.Field("roles", req.Roles),
logx.Field("path", req.Path),
logx.Field("method", req.Method),
logx.Field("policies", o.policies),
).Error("empty RBAC policy result, possibly due to an incorrect query string or policy")
return usecase.CheckOPAResp{}, domain.PermissionGetDataError("no results returned from policy evaluation")
}
data, ok := results[0].Expressions[0].Value.(map[string]any)
if !ok {
return usecase.CheckOPAResp{}, domain.PermissionGetDataError("unexpected data format in policy evaluation result")
}
resp, err := convertToCheckOPAResp(data)
if !ok {
return usecase.CheckOPAResp{}, domain.PermissionGetDataError(err.Error())
}
return resp, nil
}
// LoadPolicy 逐一處理 Policy 並且處理超時
func (o *opaUseCase) LoadPolicy(ctx context.Context, input []usecase.Policy) error {
mapped := make([]map[string]any, 0, len(input))
for i, policy := range input {
select {
case <-ctx.Done(): // 監控是否超時或取消
logx.WithCallerSkip(1).WithFields(
logx.Field("input", input),
).Error("LoadPolicy context time out")
// TODO 部分完成後處理,記錄日誌並返回成功的部分,或應該要重新 Loading....
o.policies = append(o.policies, mapped...)
return ers.SystemTimeoutError(fmt.Sprintf("operation timed out after processing %d policies: %v", i, ctx.Err()))
default:
// 繼續處理
mapped = append(mapped, policiesToMap(policy))
}
}
// 完成所有更新後紀錄,整個取代 policies
o.policies = mapped
return nil
}
func NewOpaUseCase(param OpaUseCaseParam) (usecase.OpaUseCase, error) {
module := rego.Module("policy", string(policy))
ctx := context.Background()
var allowQueryErr error
uc := &opaUseCase{}
uc.allowQuery, allowQueryErr = rego.New(
rego.Query("data.rbac"), // 要尋找的話 data 必帶, rbac = rego package allow 是要query 啥
module,
).PrepareForEval(ctx)
if allowQueryErr != nil {
return &opaUseCase{}, domain.PermissionGetDataError(allowQueryErr.Error())
}
return uc, nil
}
// 內部使用
func policiesToMap(policy usecase.Policy) map[string]any {
return map[string]any{
"methods": policy.Methods,
"name": policy.Name,
"path": policy.Path,
"role": policy.Role,
}
}
func convertToCheckOPAResp(data map[string]any) (usecase.CheckOPAResp, error) {
var response usecase.CheckOPAResp
// 解析 allow 欄位
allow, ok := data["allow"].(bool)
if !ok {
return usecase.CheckOPAResp{}, fmt.Errorf("missing or invalid 'allow' field")
}
response.Allow = allow
// 解析 policy_name 欄位
policyData, ok := data["policy_name"].(map[string]any)
if ok {
if name, ok := policyData["name"].(string); ok {
response.PolicyName = name
response.PlainCode = strings.HasSuffix(name, ".plan_code")
}
} else {
return usecase.CheckOPAResp{}, fmt.Errorf("missing or invalid 'policy_name' field")
}
// 解析 request 欄位
requestData, ok := data["request"].(map[string]any)
if !ok {
return usecase.CheckOPAResp{}, fmt.Errorf("missing or invalid 'request' field")
}
// 解析 method 和 path
response.Request.Method, ok = requestData["method"].(string)
if !ok {
return usecase.CheckOPAResp{}, fmt.Errorf("missing or invalid 'method' field")
}
response.Request.Path, ok = requestData["path"].(string)
if !ok {
return usecase.CheckOPAResp{}, fmt.Errorf("missing or invalid 'path' field")
}
// 解析 policies 欄位
policiesData, ok := requestData["policies"].([]any)
if !ok {
return usecase.CheckOPAResp{}, fmt.Errorf("missing or invalid 'policies' field")
}
response.Request.Policies = make([]usecase.Policy, len(policiesData))
for i, policyData := range policiesData {
policyMap, ok := policyData.(map[string]any)
if !ok {
return usecase.CheckOPAResp{}, fmt.Errorf("invalid policy format")
}
// 解析 methods
methodsData, ok := policyMap["methods"].([]any)
if !ok {
return usecase.CheckOPAResp{}, fmt.Errorf("missing or invalid 'methods' field in policy")
}
methods := make([]string, len(methodsData))
for j, m := range methodsData {
methods[j], ok = m.(string)
if !ok {
return usecase.CheckOPAResp{}, fmt.Errorf("invalid method format in policy")
}
}
// 組裝 policy
response.Request.Policies[i] = usecase.Policy{
Methods: methods,
Name: policyMap["name"].(string),
Path: policyMap["path"].(string),
Role: policyMap["role"].(string),
}
}
// 解析 roles 欄位
rolesData, ok := requestData["roles"].([]any)
if !ok {
return usecase.CheckOPAResp{}, fmt.Errorf("missing or invalid 'roles' field")
}
response.Request.Roles = make([]string, len(rolesData))
for i, r := range rolesData {
response.Request.Roles[i], ok = r.(string)
if !ok {
return usecase.CheckOPAResp{}, fmt.Errorf("invalid role format")
}
}
return response, nil
}

View File

@ -0,0 +1,332 @@
package usecase
import (
"ark-permission/internal/domain/usecase"
"context"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zeromicro/go-zero/core/logx"
"testing"
"time"
)
func TestMustOpaUseCase(t *testing.T) {
// 初始化 OPA UseCase
got, err := NewOpaUseCase(OpaUseCaseParam{})
assert.NoError(t, err)
ctx := context.Background()
// 加载 Policy
err = got.LoadPolicy(ctx, []usecase.Policy{
{
Role: "admin",
Path: "/admin/.*",
Methods: []string{"GET", "POST"},
Name: "Admin access",
},
{
Role: "user",
Path: "/user/.*",
Methods: []string{"GET"},
Name: "User read access",
},
{
Role: "editor",
Path: "/editor/.*",
Methods: []string{"PUT", "POST"},
Name: "Editor access",
},
})
assert.NoError(t, err)
// 定義測試表
tests := []struct {
name string
req usecase.CheckReq
expect bool
expectError bool
}{
{
name: "單一角色,應允許通過",
req: usecase.CheckReq{
Roles: []string{"user"},
Path: "/user/profile",
Method: "GET",
},
expect: true,
},
{
name: "多角色其中一個有配到,應允許通過",
req: usecase.CheckReq{
Roles: []string{"user", "admin"},
Path: "/user/profile",
Method: "GET",
},
expect: true,
},
{
name: "角色不匹配,應拒絕通過",
req: usecase.CheckReq{
Roles: []string{"editor"},
Path: "/user/profile",
Method: "GET",
},
expect: false,
},
{
name: "路徑不匹配,應拒絕通過",
req: usecase.CheckReq{
Roles: []string{"user"},
Path: "/editor/dashboard",
Method: "GET",
},
expect: false,
},
{
name: "方法不匹配,應拒絕通過",
req: usecase.CheckReq{
Roles: []string{"user"},
Path: "/user/profile",
Method: "POST",
},
expect: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
check, err := got.CheckRBACPermission(ctx, tt.req)
if tt.expectError {
assert.Error(t, err, "expected an error but got none")
} else {
assert.NoError(t, err, "did not expect an error but got one")
assert.Equal(t, tt.expect, check.Allow)
}
})
}
}
func TestLoadPolicy(t *testing.T) {
// 初始化 OPA UseCase
got, err := NewOpaUseCase(OpaUseCaseParam{})
require.NoError(t, err)
tests := []struct {
name string
input []usecase.Policy
ctxTimeout time.Duration
expectErr bool
}{
{
name: "正常加載多個Policy",
input: []usecase.Policy{
{
Role: "admin",
Path: "/admin/.*",
Methods: []string{"GET", "POST"},
Name: "Admin access",
},
{
Role: "user",
Path: "/user/.*",
Methods: []string{"GET"},
Name: "User read access",
},
},
ctxTimeout: 3 * time.Second, // 足夠的時間來執行
expectErr: false,
},
{
name: "加載策略超時",
input: []usecase.Policy{
{
Role: "admin",
Path: "/admin/.*",
Methods: []string{"GET", "POST"},
Name: "Admin access",
},
{
Role: "user",
Path: "/user/.*",
Methods: []string{"GET"},
Name: "User read access",
},
},
ctxTimeout: 1 * time.Nanosecond, // 超時
expectErr: true,
},
{
name: "空策略加載",
input: []usecase.Policy{},
ctxTimeout: 3 * time.Second, // 足夠的時間
expectErr: false,
},
}
// 遍歷所有測試用例
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 設置具有超時的 Context
ctx, cancel := context.WithTimeout(context.Background(), tt.ctxTimeout)
defer cancel()
// 調用 LoadPolicy
err := got.LoadPolicy(ctx, tt.input)
// 檢查是否符合預期錯誤
if tt.expectErr {
assert.Error(t, err, "預期發生錯誤,但沒有發生")
} else {
assert.NoError(t, err, "不預期發生錯誤,但卻發生了")
}
// 如果沒有錯誤,檢查 policies 是否被正確加載
if !tt.expectErr {
assert.Equal(t, len(tt.input), len(got.GetPolicy(ctx)), "policies 加載的數量與輸入數量不一致")
}
})
}
}
func BenchmarkLoadPolicy(b *testing.B) {
// 初始化 OPA UseCase
got, _ := NewOpaUseCase(OpaUseCaseParam{})
logx.Disable()
policiesSmall := []usecase.Policy{
{
Role: "admin",
Path: "/admin/.*",
Methods: []string{"GET", "POST"},
Name: "Admin access",
},
{
Role: "user",
Path: "/user/.*",
Methods: []string{"GET"},
Name: "User read access",
},
}
policiesLarge := make([]usecase.Policy, 1000)
for i := 0; i < 1000; i++ {
policiesLarge[i] = usecase.Policy{
Role: "admin",
Path: "/admin/.*",
Methods: []string{"GET", "POST"},
Name: "Admin access",
}
}
benchmarks := []struct {
name string
policies []usecase.Policy
ctxTimeout time.Duration
}{
{
name: "SmallPolicy_NoTimeout",
policies: policiesSmall,
ctxTimeout: 1 * time.Second, // 沒有超時
},
{
name: "LargePolicy_NoTimeout",
policies: policiesLarge,
ctxTimeout: 1 * time.Second, // 沒有超時
},
{
name: "SmallPolicy_WithTimeout",
policies: policiesSmall,
ctxTimeout: 1 * time.Millisecond, // 沒有超時
},
{
name: "LargePolicy_WithTimeout",
policies: policiesLarge,
ctxTimeout: 1 * time.Millisecond, // 沒有超時
},
}
for _, bm := range benchmarks {
b.Run(bm.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
ctx, cancel := context.WithTimeout(context.Background(), bm.ctxTimeout)
defer cancel()
_ = got.LoadPolicy(ctx, bm.policies)
}
})
}
}
func BenchmarkCheckRBACPermission(b *testing.B) {
got, _ := NewOpaUseCase(OpaUseCaseParam{})
logx.Disable()
// 定義測試用 Policy
policies := []usecase.Policy{
{
Role: "admin",
Path: "/admin/.*",
Methods: []string{"GET", "POST"},
Name: "Admin access",
},
{
Role: "user",
Path: "/user/.*",
Methods: []string{"GET"},
Name: "User read access",
},
{
Role: "editor",
Path: "/editor/.*",
Methods: []string{"PUT", "POST"},
Name: "Editor access",
},
}
// 加載 Policy
_ = got.LoadPolicy(context.Background(), policies)
// 定義不同測試基準場景
benchmarks := []struct {
name string
req usecase.CheckReq
}{
{
name: "SingleRole_SimplePath",
req: usecase.CheckReq{
Roles: []string{"user"},
Path: "/user/profile",
Method: "GET",
},
},
{
name: "MultipleRoles_ComplexPath",
req: usecase.CheckReq{
Roles: []string{"admin", "user", "editor"},
Path: "/editor/dashboard",
Method: "PUT",
},
},
{
name: "NoRoles_InvalidPath",
req: usecase.CheckReq{
Roles: []string{},
Path: "/invalid/path",
Method: "POST",
},
},
}
// 走訪所有場景
for _, bm := range benchmarks {
b.Run(bm.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
// 設置一個超時的 ctx
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond)
defer cancel()
_, _ = got.CheckRBACPermission(ctx, bm.req)
}
})
}
}

View File

@ -0,0 +1,300 @@
package usecase
import (
"ark-permission/internal/domain"
"ark-permission/internal/domain/usecase"
"ark-permission/internal/entity"
ers "code.30cm.net/wanderland/library-go/errors"
"fmt"
"sort"
)
// NewPermissionTree 創建新的權限樹
func NewPermissionTree() *PermissionTree {
// 插入虛擬跟節點,讓其他人都放在這個底下,行為一致,無需處理邊界
root := &usecase.Permission{
ID: 0, // 根節點 ID 通常設為 0 或其他標示符號
Name: "root", // 虛擬根節點名稱
Children: []*usecase.Permission{},
}
return &PermissionTree{
root: root,
nodes: map[int64]*usecase.Permission{0: root}, // 根節點也加入 nodes 記錄
paths: make(map[int64][]int),
names: make(map[string][]int64),
ids: make(map[int64]string),
}
}
// PermissionTree 將初始化與方法分開
// 不考慮使用其樹型結構原因是,在我們的的場景下,
// 因為深度不超過 100 層,且資料量在 300 到 500 筆之間,至多不超過一萬筆
// (考慮到一萬條權限已很複雜) 讀取操作是主要需求,而寫入與刪除操作相對較少,
// 這樣的場景適合空間換時間的策略。
// 優點:
// 1. 空間換時間:
// • 使用 map 來保存節點和它們的路徑、名稱等,可以在 O(1) 的時間複雜度內查找節點、名稱、路徑等資料,非常適合頻繁讀取的場景。
// • 雖然消耗了更多的空間儲存多個映射但這樣的空間開銷在你的資料量規模300-500 筆)下是可以接受的。
// 2. 高效查找:
// • 查找節點nodes map、路徑paths map和名稱names map都是透過直接查找 map 實現的,這會極大提升查找效率,尤其是在讀取大量資料的情況下。
// • map 在 Golang 中的查找操作具有平均 O(1) 的時間複雜度,非常適合大量查找的場景。
// 3. 樹結構較小,寫入與刪除頻率較低:
// • 由於你的資料量不大,且寫入和刪除操作較少,這樣的設計不會過度影響性能。即使有少量的寫入或刪除操作,更新 map 的開銷也是可以接受的。
// 為何設計 names map[string][]int64
// 雖然目前sql 層面來說 name 是唯一的
// 這是因為在某些情況下,權限名稱並不是唯一的,可能會有多個權限使用同一個名稱。例如:
// • 你可能在多個模組中使用相同的權限名稱,例如 “Read” 或 “Write”但它們的實際權限 ID 是不同的。
// • 使用 map[string][]int64 結構來儲存這些同名權限,可以快速查找所有對應的權限 ID並有效管理不同模組的相同名稱權限。
type PermissionTree struct {
root *usecase.Permission // 根節點,表示權限樹的最頂層,所有其他權限都從這裡派生
nodes map[int64]*usecase.Permission // 透過 permission ID 快速查找權限節點,允許 O(1) 查找
paths map[int64][]int // 每個權限節點的完整路徑,儲存節點 ID 對應的索引路徑,方便快速查找()
names map[string][]int64 // 權限名稱對應權限 ID支持多個同名權限允許快速根據名稱查找所有相關 ID
ids map[int64]string // 權限 ID 對應權限名稱,允許根據權限 ID 反向查找權限名稱
}
// AddPermission 插入新的權限節點
func (tree *PermissionTree) AddPermission(parentID int64, value entity.Permission) error {
node := &usecase.Permission{
ID: value.ID,
Name: value.Name,
HTTPPath: value.HTTPPath,
HTTPMethod: value.HTTPMethod,
Children: []*usecase.Permission{},
}
// 查找父節點,找不到回傳錯誤。
parentNode, err := tree.FindPermissionByID(parentID)
if err != nil {
return ers.ResourceNotFound(err.Error())
}
parentNode.Children = append(parentNode.Children, node)
node.Parent = parentNode
// 設置路徑 ID
if node.Parent.ID >= 0 {
node.PathIDs = append(node.Parent.PathIDs, node.Parent.ID)
}
// 更新樹結構中的數據
tree.nodes[value.ID] = node // 節點加入紀錄
tree.names[value.Name] = append(tree.names[value.Name], value.ID)
tree.ids[value.ID] = value.Name
// 計算完整路徑
tree.buildNodePath(node, value.ID)
return nil
}
// buildNodePath 構建節點的完整路徑
// paths 是一個 map 結構,儲存每個權限節點的完整路徑
// key 是節點的權限 ID (int64)
// value 是從根節點到該節點的索引路徑 ([]int)
// 每個 int 值代表該節點在其父節點的 Children 列表中的索引位置。
//
// 例如:
// 假設有以下的權限樹結構:
// root (ID: 1)
// ├── child_permission_1 (ID: 2)
// │ └── grandchild_permission_1 (ID: 4)
// └── child_permission_2 (ID: 3)
//
// 那麼:
// paths[4] = []int{0, 0}
// 表示 grandchild_permission_1 的完整路徑:
// - 它是根節點的第一個子節點(索引 0
// - 它的父節點child_permission_1也是根節點的第一個子節點索引 0
//
// 類似的:
// paths[2] = []int{0}
// 表示 child_permission_1 是根節點的第一個子節點。
func (tree *PermissionTree) buildNodePath(node *usecase.Permission, insertID int64) {
// 找出該node完整的path路徑
var path []int
for {
if node.Parent == nil {
sort.SliceStable(path, func(_, _ int) bool {
return true
})
tree.paths[insertID] = path
break
}
for i, v := range node.Parent.Children {
if node.ID == v.ID {
path = append(path, i)
node = node.Parent
}
}
}
}
// FindPermissionByID 根據 ID 查找權限節點
// 如果權限節點存在,返回對應的 Permission 資料
func (tree *PermissionTree) FindPermissionByID(permissionID int64) (*usecase.Permission, error) {
// 直接從 nodes map 中查找對應的節點O(1) 時間複雜度
node, ok := tree.nodes[permissionID]
if !ok {
return nil, fmt.Errorf("failed to find ID %d", permissionID)
}
// 返回找到的節點
return node, nil
}
// GetAllParentPermissionIDs 返回所有父節點權限 ID
func (tree *PermissionTree) GetAllParentPermissionIDs(permissions domain.Permissions) ([]int64, error) {
exist := make(map[int64]bool) // 用於記錄已處理的權限 ID
var ids []int64
for name, status := range permissions {
if status != domain.PermissionStatusOpenCode {
continue // 只處理開啟狀態的權限
}
// 根據名稱查找對應的權限 ID 列表
pIDs, ok := tree.names[name]
if !ok {
return nil, ers.ResourceNotFound(fmt.Sprintf("permission with name %s not found", name))
}
for _, pID := range pIDs {
// 檢查是否已經處理過這個權限 ID
if exist[pID] {
continue
}
node, err := tree.FindPermissionByID(pID)
if err != nil {
return nil, err
}
// 如果該節點有子節點且父節點未開啟,則跳過該節點
if len(node.Children) > 0 && !tree.isParentPermissionOpen(node, permissions) {
continue
}
// 加入該節點的父節點路徑和自身 ID
ids = append(ids, node.PathIDs...)
ids = append(ids, pID)
// 將所有相關的 ID 記錄為已處理
for _, id := range append(node.PathIDs, pID) {
exist[id] = true
}
}
}
return ids, nil
}
// GetAllParentPermissionStatuses 返回所有父節點的權限狀態
// 如果一個權限開啟,返回所有相關父節點的狀態
func (tree *PermissionTree) GetAllParentPermissionStatuses(permissions domain.Permissions) (domain.Permissions, error) {
// 用來存儲已經處理過的節點,避免重複處理
exist := make(map[int64]bool)
// 用來存儲返回的所有權限狀態
resultStatuses := make(domain.Permissions)
// 遍歷每個權限
for name, status := range permissions {
// 只處理已開啟的權限
if status != domain.PermissionStatusOpenCode {
continue
}
// 根據權限名稱查找對應的權限 ID 列表
pIDs, ok := tree.names[name]
if !ok {
return nil, ers.ResourceNotFound(fmt.Sprintf("permission with name %s not found", name))
}
// 遍歷所有權限 ID
for _, pID := range pIDs {
// 檢查是否已處理過這個 ID
if exist[pID] {
continue
}
node, err := tree.FindPermissionByID(pID)
if err != nil {
return nil, err
}
// 處理該節點的父節點路徑和自身 ID
allIDs := append(node.PathIDs, pID)
for _, id := range allIDs {
// 避免重複處理
if exist[id] {
continue
}
if permissionName, exists := tree.ids[id]; exists {
// 設置權限狀態為已開啟
resultStatuses[permissionName] = domain.PermissionStatusOpenCode
exist[id] = true
}
}
}
}
return resultStatuses, nil
}
// isParentPermissionOpen 檢查父節點的權限是否已開啟
func (tree *PermissionTree) isParentPermissionOpen(node *usecase.Permission, permissions domain.Permissions) bool {
for _, child := range node.Children {
// 檢查子節點是否有開啟的權限
if status, ok := permissions[child.Name]; ok && status == domain.PermissionStatusOpenCode {
return true
}
}
return false
}
// GetRolePermissionTree 根據角色權限找出所有父節點和子節點權限狀態
// 角色權限是傳入的一個列表,該方法會根據每個角色的權限,返回所有相關的權限狀態
func (tree *PermissionTree) GetRolePermissionTree(rolePermissions []entity.RolePermission) (domain.Permissions, error) {
// 初始化用來存儲所有權限狀態的 map
resultStatuses := make(domain.Permissions)
// 初始化用來記錄已處理的權限 ID避免重複處理
exist := make(map[int64]bool)
// 遍歷所有角色的權限
for _, rolePermission := range rolePermissions {
// 根據權限 ID 找到對應的節點
node, err := tree.FindPermissionByID(rolePermission.PermissionID)
if err != nil {
return nil, fmt.Errorf("permission with ID %d not found: %w", rolePermission.PermissionID, err)
}
// 將該節點及其父節點的權限狀態加入 resultStatuses
allIDs := append(node.PathIDs, rolePermission.PermissionID) // 包含所有父節點和當前節點
for _, id := range allIDs {
if !exist[id] { // 檢查是否已經處理過
if permissionName, ok := tree.ids[id]; ok {
// 設置權限狀態為已開啟(或者根據 rolePermission 狀態設置)
resultStatuses[permissionName] = domain.PermissionStatusOpenCode
exist[id] = true // 記錄該 ID 已處理過
}
}
}
// 遍歷該節點的所有子節點,並設置權限狀態
for _, child := range node.Children {
if !exist[child.ID] {
if permissionName, ok := tree.ids[child.ID]; ok {
resultStatuses[permissionName] = domain.PermissionStatusOpenCode
exist[child.ID] = true
}
}
}
}
return resultStatuses, nil
}

View File

@ -0,0 +1,611 @@
package usecase
import (
"ark-permission/internal/domain"
"ark-permission/internal/domain/usecase"
"ark-permission/internal/entity"
ers "code.30cm.net/wanderland/library-go/errors"
"fmt"
"github.com/stretchr/testify/assert"
"reflect"
"testing"
)
func TestNewPermissionTree(t *testing.T) {
tests := []struct {
name string
want *PermissionTree
}{
{
name: "ok",
want: &PermissionTree{
root: &usecase.Permission{
ID: 0,
Name: "root",
Children: []*usecase.Permission{},
},
nodes: map[int64]*usecase.Permission{0: {
ID: 0,
Name: "root",
Children: []*usecase.Permission{},
}}, // 根節點也加入 nodes 記錄
paths: make(map[int64][]int),
names: make(map[string][]int64),
ids: make(map[int64]string),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tree := NewPermissionTree()
// 驗證 Root 的值
assert.Equal(t, tree.root.ID, tt.want.root.ID)
assert.Equal(t, tree.root.Name, tt.want.root.Name)
assert.Equal(t, len(tree.root.Children), len(tt.want.root.Children))
assert.Equal(t, len(tree.nodes), len(tt.want.nodes))
assert.Equal(t, len(tree.paths), len(tt.want.paths))
assert.Equal(t, len(tree.ids), len(tt.want.ids))
})
}
}
// 測試 AddPermission 函數
func TestAddPermission(t *testing.T) {
tree := NewPermissionTree()
tests := []struct {
name string
parentID int64
permission entity.Permission
expectedError error
}{
{
name: "ok",
parentID: 0,
permission: entity.Permission{ID: 2, Name: "new_permission", HTTPPath: "/new", HTTPMethod: "POST"},
},
{
name: "Invalid Parent ID",
parentID: 99, // 無效的 parentID
permission: entity.Permission{ID: 3, Name: "invalid_parent", HTTPPath: "/invalid", HTTPMethod: "GET"},
expectedError: ers.ResourceNotFound("failed to find ID 99"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tree.AddPermission(tt.parentID, tt.permission)
// 錯誤檢查
if !reflect.DeepEqual(err, tt.expectedError) {
t.Errorf("expected error %v, got %v", tt.expectedError, err)
}
if tt.expectedError == nil {
// 檢查是否已正確插入到 nodes 中
node, ok := tree.nodes[tt.permission.ID]
if !ok {
t.Errorf("expected permission with ID %d to be in nodes map", tt.permission.ID)
}
if node.Name != tt.permission.Name {
t.Errorf("expected permission name %s, got %s", tt.permission.Name, node.Name)
}
// 檢查父節點的子節點是否正確加入
parentNode, _ := tree.FindPermissionByID(tt.parentID)
found := false
for _, child := range parentNode.Children {
if child.ID == tt.permission.ID {
found = true
break
}
}
if !found {
t.Errorf("expected permission ID %d to be child of parent ID %d", tt.permission.ID, tt.parentID)
}
}
})
}
}
// 測試 buildNodePath 函數
func TestBuildNodePath(t *testing.T) {
// ======== 準備測試 ========
tree := NewPermissionTree()
// 插入一些節點
err := tree.AddPermission(0, entity.Permission{ID: 1, Name: "root_permission"})
assert.NoError(t, err)
err = tree.AddPermission(1, entity.Permission{ID: 2, Name: "child_permission_1"})
assert.NoError(t, err)
err = tree.AddPermission(1, entity.Permission{ID: 3, Name: "child_permission_2"})
assert.NoError(t, err)
err = tree.AddPermission(2, entity.Permission{ID: 4, Name: "grandchild_permission"})
assert.NoError(t, err)
// ======== 準備測試 ========
tests := []struct {
name string
nodeID int64
expectedPath []int
}{
{
name: "Grandchild Permission Path",
nodeID: 4,
expectedPath: []int{0, 0, 0}, // 根 -> 子 -> 孫節點的索引
},
{
name: "Child Permission 1 Path",
nodeID: 2,
expectedPath: []int{0, 0}, // 根 -> 子節點的索引
},
{
name: "Child Permission 2 Path",
nodeID: 3,
expectedPath: []int{0, 1}, // 根 -> 子節點的索引
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 查找要測試的節點
node, err := tree.FindPermissionByID(tt.nodeID)
if err != nil {
t.Fatalf("failed to find node with ID %d: %v", tt.nodeID, err)
}
// 構建節點的完整路徑
// Grandchild Permission Path 測試孫節點的完整路徑,這應該包含根節點和子節點的索引,結果為 [0, 0],表示它是根節點的第一個子節點的第一個子節點。
// Child Permission 1 Path 測試子節點 1 的路徑,應該是 [0],表示它是根節點的第一個子節點。
// Child Permission 2 Path 測試子節點 2 的路徑,應該是 [1],表示它是根節點的第二個子節點。
tree.buildNodePath(node, tt.nodeID)
// 從 paths 中獲取構建好的路徑
resultPath, ok := tree.paths[tt.nodeID]
if !ok {
t.Fatalf("path not found for node ID %d", tt.nodeID)
}
// 比較結果路徑與預期路徑
if !reflect.DeepEqual(resultPath, tt.expectedPath) {
t.Errorf("expected path %v, got %v", tt.expectedPath, resultPath)
}
})
}
}
// 測試 GetAllParentPermissionIDs 函數
func TestGetAllParentPermissionIDs(t *testing.T) {
tree := NewPermissionTree()
// ======== 準備測試 ========
// 添加節點
err := tree.AddPermission(0, entity.Permission{ID: 1, Name: "root_permission"})
assert.NoError(t, err)
err = tree.AddPermission(1, entity.Permission{ID: 2, Name: "child_permission_1"})
assert.NoError(t, err)
err = tree.AddPermission(1, entity.Permission{ID: 3, Name: "child_permission_2"})
assert.NoError(t, err)
err = tree.AddPermission(2, entity.Permission{ID: 4, Name: "grandchild_permission_1"})
assert.NoError(t, err)
err = tree.AddPermission(3, entity.Permission{ID: 5, Name: "grandchild_permission_2"})
assert.NoError(t, err)
err = tree.AddPermission(0, entity.Permission{ID: 6, Name: "root_permission_2"})
assert.NoError(t, err)
err = tree.AddPermission(6, entity.Permission{ID: 6, Name: "child_permission_3"})
assert.NoError(t, err)
// ======== 準備測試 ========
tests := []struct {
name string
permissions domain.Permissions
expectedResult []int64
expectedError error
}{
{
name: "Valid permissions with open status",
permissions: domain.Permissions{
"grandchild_permission_1": domain.PermissionStatusOpenCode,
},
expectedResult: []int64{0, 1, 2, 4}, // 根 -> 子 -> 孫節點
expectedError: nil,
},
{
name: "Valid multiple permissions with open status",
permissions: domain.Permissions{
"grandchild_permission_1": domain.PermissionStatusOpenCode,
"grandchild_permission_2": domain.PermissionStatusOpenCode,
},
expectedResult: []int64{0, 1, 2, 4, 0, 1, 3, 5}, // 多個權限
expectedError: nil,
},
{
name: "Permission name not found",
permissions: domain.Permissions{
"unknown_permission": domain.PermissionStatusOpenCode,
},
expectedResult: nil,
expectedError: fmt.Errorf("permission with name unknown_permission not found"),
},
{
name: "Permission close by parent node",
permissions: domain.Permissions{
"root_permission_2": domain.PermissionStatusCloseCode,
},
expectedResult: nil,
expectedError: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := tree.GetAllParentPermissionIDs(tt.permissions)
// 檢查返回結果
if !reflect.DeepEqual(result, tt.expectedResult) {
t.Errorf("expected %v, got %v", tt.expectedResult, result)
}
// 檢查錯誤
if (err == nil && tt.expectedError != nil) || (err != nil && tt.expectedError == nil) {
t.Errorf("expected error %v, got %v", tt.expectedError, err)
}
})
}
}
// 測試 GetAllParentPermissionStatuses 函數
func TestGetAllParentPermissionStatuses(t *testing.T) {
tree := NewPermissionTree()
// 添加節點
err := tree.AddPermission(0, entity.Permission{ID: 1, Name: "root_permission"})
assert.NoError(t, err)
err = tree.AddPermission(1, entity.Permission{ID: 2, Name: "child_permission_1"})
assert.NoError(t, err)
err = tree.AddPermission(1, entity.Permission{ID: 3, Name: "child_permission_2"})
assert.NoError(t, err)
err = tree.AddPermission(2, entity.Permission{ID: 4, Name: "grandchild_permission_1"})
assert.NoError(t, err)
err = tree.AddPermission(3, entity.Permission{ID: 5, Name: "grandchild_permission_2"})
assert.NoError(t, err)
tests := []struct {
name string
permissions domain.Permissions
expectedResult domain.Permissions
expectedError error
}{
{
name: "Valid permissions with open status",
permissions: domain.Permissions{
"grandchild_permission_1": domain.PermissionStatusOpenCode,
},
expectedResult: domain.Permissions{
"root_permission": domain.PermissionStatusOpenCode,
"child_permission_1": domain.PermissionStatusOpenCode,
"grandchild_permission_1": domain.PermissionStatusOpenCode,
},
expectedError: nil,
},
{
name: "Multiple permissions with open status",
permissions: domain.Permissions{
"grandchild_permission_1": domain.PermissionStatusOpenCode,
"grandchild_permission_2": domain.PermissionStatusOpenCode,
},
expectedResult: domain.Permissions{
"root_permission": domain.PermissionStatusOpenCode,
"child_permission_1": domain.PermissionStatusOpenCode,
"grandchild_permission_1": domain.PermissionStatusOpenCode,
"child_permission_2": domain.PermissionStatusOpenCode,
"grandchild_permission_2": domain.PermissionStatusOpenCode,
},
expectedError: nil,
},
{
name: "Permission name not found",
permissions: domain.Permissions{
"unknown_permission": domain.PermissionStatusOpenCode,
},
expectedResult: nil,
expectedError: fmt.Errorf("permission with name unknown_permission not found"),
},
{
name: "Closed permissions are ignored",
permissions: domain.Permissions{
"grandchild_permission_1": domain.PermissionStatusCloseCode,
},
expectedResult: domain.Permissions{},
expectedError: nil,
},
{
name: "Multiple permissions with close status",
permissions: domain.Permissions{
"grandchild_permission_1": domain.PermissionStatusCloseCode,
},
expectedResult: domain.Permissions{},
expectedError: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := tree.GetAllParentPermissionStatuses(tt.permissions)
// 檢查返回結果
if !reflect.DeepEqual(result, tt.expectedResult) {
t.Errorf("expected %v, got %v", tt.expectedResult, result)
}
// 檢查錯誤
if (err == nil && tt.expectedError != nil) || (err != nil && tt.expectedError == nil) {
t.Errorf("expected error %v, got %v", tt.expectedError, err)
}
})
}
}
// 測試 isParentPermissionOpen 函數
func TestIsParentPermissionOpen(t *testing.T) {
tree := NewPermissionTree()
// 添加節點
err := tree.AddPermission(0, entity.Permission{ID: 1, Name: "root_permission"})
assert.NoError(t, err)
err = tree.AddPermission(1, entity.Permission{ID: 2, Name: "child_permission_1"})
assert.NoError(t, err)
err = tree.AddPermission(1, entity.Permission{ID: 3, Name: "child_permission_2"})
assert.NoError(t, err)
err = tree.AddPermission(2, entity.Permission{ID: 4, Name: "grandchild_permission_1"})
assert.NoError(t, err)
err = tree.AddPermission(2, entity.Permission{ID: 5, Name: "grandchild_permission_2"})
assert.NoError(t, err)
tests := []struct {
name string
nodeID int64
permissions domain.Permissions
expectedOpen bool
}{
{
name: "Parent has open child permission",
nodeID: 2,
permissions: domain.Permissions{
"grandchild_permission_1": domain.PermissionStatusOpenCode,
},
expectedOpen: true,
},
{
name: "Parent has no open child permissions",
nodeID: 2,
permissions: domain.Permissions{
"grandchild_permission_1": domain.PermissionStatusCloseCode,
},
expectedOpen: false,
},
{
name: "Parent with multiple children, one open",
nodeID: 2,
permissions: domain.Permissions{
"grandchild_permission_1": domain.PermissionStatusCloseCode,
"grandchild_permission_2": domain.PermissionStatusOpenCode,
},
expectedOpen: true,
},
{
name: "Parent with no child permissions in list",
nodeID: 3,
permissions: domain.Permissions{
"grandchild_permission_1": domain.PermissionStatusOpenCode,
},
expectedOpen: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
node, err := tree.FindPermissionByID(tt.nodeID)
if err != nil {
t.Fatalf("failed to find node with ID %d: %v", tt.nodeID, err)
}
result := tree.isParentPermissionOpen(node, tt.permissions)
if result != tt.expectedOpen {
t.Errorf("expected %v, got %v", tt.expectedOpen, result)
}
})
}
}
// 測試 GetRolePermissionTree 函數
func TestGetRolePermissionTree(t *testing.T) {
tree := NewPermissionTree()
// 添加節點
err := tree.AddPermission(0, entity.Permission{ID: 1, Name: "root_permission"})
assert.NoError(t, err)
err = tree.AddPermission(1, entity.Permission{ID: 2, Name: "child_permission_1"})
assert.NoError(t, err)
err = tree.AddPermission(1, entity.Permission{ID: 3, Name: "child_permission_2"})
assert.NoError(t, err)
err = tree.AddPermission(2, entity.Permission{ID: 4, Name: "grandchild_permission_1"})
assert.NoError(t, err)
err = tree.AddPermission(3, entity.Permission{ID: 5, Name: "grandchild_permission_2"})
assert.NoError(t, err)
// 測試數據
tests := []struct {
name string
rolePermissions []entity.RolePermission
expectedResult domain.Permissions
expectedError error
}{
{
name: "Single role permission with parent and child",
rolePermissions: []entity.RolePermission{
{PermissionID: 4}, // grandchild_permission_1
},
expectedResult: domain.Permissions{
"root_permission": domain.PermissionStatusOpenCode,
"child_permission_1": domain.PermissionStatusOpenCode,
"grandchild_permission_1": domain.PermissionStatusOpenCode,
},
expectedError: nil,
},
{
name: "Multiple role permissions with parents and children",
rolePermissions: []entity.RolePermission{
{PermissionID: 4}, // grandchild_permission_1
{PermissionID: 5}, // grandchild_permission_2
},
expectedResult: domain.Permissions{
"root_permission": domain.PermissionStatusOpenCode,
"child_permission_1": domain.PermissionStatusOpenCode,
"grandchild_permission_1": domain.PermissionStatusOpenCode,
"child_permission_2": domain.PermissionStatusOpenCode,
"grandchild_permission_2": domain.PermissionStatusOpenCode,
},
expectedError: nil,
},
{
name: "Role permission with no children",
rolePermissions: []entity.RolePermission{
{PermissionID: 2}, // child_permission_1
},
expectedResult: domain.Permissions{
"root_permission": domain.PermissionStatusOpenCode,
"child_permission_1": domain.PermissionStatusOpenCode,
"grandchild_permission_1": domain.PermissionStatusOpenCode,
},
expectedError: nil,
},
{
name: "Role permission not found",
rolePermissions: []entity.RolePermission{
{PermissionID: 99}, // non-existent permission
},
expectedResult: nil,
expectedError: fmt.Errorf("permission with ID %d not found", 99),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := tree.GetRolePermissionTree(tt.rolePermissions)
// 檢查返回結果
if !reflect.DeepEqual(result, tt.expectedResult) {
t.Errorf("expected %v, got %v", tt.expectedResult, result)
}
// 檢查錯誤
if (err == nil && tt.expectedError != nil) || (err != nil && tt.expectedError == nil) {
t.Errorf("expected error %v, got %v", tt.expectedError, err)
}
})
}
}
func BenchmarkGetAllParentPermissionIDs(b *testing.B) {
tree := NewPermissionTree()
// 添加節點
tree.AddPermission(0, entity.Permission{ID: 1, Name: "root_permission"})
tree.AddPermission(1, entity.Permission{ID: 2, Name: "child_permission_1"})
tree.AddPermission(1, entity.Permission{ID: 3, Name: "child_permission_2"})
tree.AddPermission(2, entity.Permission{ID: 4, Name: "grandchild_permission_1"})
tree.AddPermission(3, entity.Permission{ID: 5, Name: "grandchild_permission_2"})
permissions := domain.Permissions{
"grandchild_permission_1": domain.PermissionStatusOpenCode,
"grandchild_permission_2": domain.PermissionStatusOpenCode,
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = tree.GetAllParentPermissionIDs(permissions)
}
}
func BenchmarkGetAllParentPermissionStatuses(b *testing.B) {
tree := NewPermissionTree()
// 添加節點
tree.AddPermission(0, entity.Permission{ID: 1, Name: "root_permission"})
tree.AddPermission(1, entity.Permission{ID: 2, Name: "child_permission_1"})
tree.AddPermission(1, entity.Permission{ID: 3, Name: "child_permission_2"})
tree.AddPermission(2, entity.Permission{ID: 4, Name: "grandchild_permission_1"})
tree.AddPermission(3, entity.Permission{ID: 5, Name: "grandchild_permission_2"})
permissions := domain.Permissions{
"grandchild_permission_1": domain.PermissionStatusOpenCode,
"grandchild_permission_2": domain.PermissionStatusOpenCode,
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = tree.GetAllParentPermissionStatuses(permissions)
}
}
func BenchmarkIsParentPermissionOpen(b *testing.B) {
tree := NewPermissionTree()
// 添加節點
tree.AddPermission(0, entity.Permission{ID: 1, Name: "root_permission"})
tree.AddPermission(1, entity.Permission{ID: 2, Name: "child_permission_1"})
tree.AddPermission(1, entity.Permission{ID: 3, Name: "child_permission_2"})
tree.AddPermission(2, entity.Permission{ID: 4, Name: "grandchild_permission_1"})
tree.AddPermission(2, entity.Permission{ID: 5, Name: "grandchild_permission_2"})
permissions := domain.Permissions{
"grandchild_permission_1": domain.PermissionStatusOpenCode,
"grandchild_permission_2": domain.PermissionStatusCloseCode,
}
node, _ := tree.FindPermissionByID(2)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = tree.isParentPermissionOpen(node, permissions)
}
}
func BenchmarkGetRolePermissionTree(b *testing.B) {
tree := NewPermissionTree()
// 添加節點
tree.AddPermission(0, entity.Permission{ID: 1, Name: "root_permission"})
tree.AddPermission(1, entity.Permission{ID: 2, Name: "child_permission_1"})
tree.AddPermission(1, entity.Permission{ID: 3, Name: "child_permission_2"})
tree.AddPermission(2, entity.Permission{ID: 4, Name: "grandchild_permission_1"})
tree.AddPermission(3, entity.Permission{ID: 5, Name: "grandchild_permission_2"})
rolePermissions := []entity.RolePermission{
{PermissionID: 4}, // grandchild_permission_1
{PermissionID: 5}, // grandchild_permission_2
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = tree.GetRolePermissionTree(rolePermissions)
}
}
func BenchmarkBuildNodePath(b *testing.B) {
tree := NewPermissionTree()
// 添加節點
tree.AddPermission(0, entity.Permission{ID: 1, Name: "root_permission"})
tree.AddPermission(1, entity.Permission{ID: 2, Name: "child_permission_1"})
tree.AddPermission(1, entity.Permission{ID: 3, Name: "child_permission_2"})
tree.AddPermission(2, entity.Permission{ID: 4, Name: "grandchild_permission_1"})
node, _ := tree.FindPermissionByID(4)
b.ResetTimer()
for i := 0; i < b.N; i++ {
tree.buildNodePath(node, 4)
}
}

View File

@ -0,0 +1,43 @@
package rbac
import rego.v1
request = {
"roles": input.roles,
"path": input.path,
"method": input.method,
"policies": input.policies,
}
default allow = false
key_match(request_path, policy_path) if {
regex.match(policy_path, request_path)
}
# 方法函數的驗證
method_match(request_method, policy_methods) if {
policy_methods[_] == request_method
}
# 檢驗是不是匹配或繼承
valid_role(user_role, policy_role) if {
user_role[_] == policy_role
}
# 定義一個策略
allow if {
policy := input.policies[_]
key_match(input.path, policy.path)
valid_role(input.roles, policy.role)
method_match(input.method, policy.methods)
}
# 返回當前符合的策略名稱
policy_name := {
"name": policy.name|
policy := input.policies[_]
key_match(input.path, policy.path);
valid_role(input.roles, policy.role);
method_match(input.method, policy.methods)
}