feat/all-pa (#3)
Co-authored-by: daniel.w <daniel.w@intteam.net> Reviewed-on: #3
This commit is contained in:
parent
8925a8e3c6
commit
1ad285bab1
|
@ -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...)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -10,6 +10,6 @@ RedisCluster:
|
|||
Type: cluster
|
||||
|
||||
Token:
|
||||
Expired: 300
|
||||
RefreshExpires: 86500
|
||||
Expired: 300s
|
||||
RefreshExpires: 86500s
|
||||
Secret: gg88g88
|
|
@ -0,0 +1 @@
|
|||
DROP TABLE IF EXISTS `permission`;
|
|
@ -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 ='權限表';
|
|
@ -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()); -- 明碼查詢
|
|
@ -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
34
go.mod
|
@ -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
|
||||
)
|
||||
|
|
|
@ -14,4 +14,8 @@ type Config struct {
|
|||
Expired time.Duration
|
||||
Secret string
|
||||
}
|
||||
// 加上DB結構體
|
||||
DB struct {
|
||||
DsnString string
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
)
|
|
@ -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"`
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"ark-permission/internal/domain"
|
||||
"ark-permission/internal/entity"
|
||||
)
|
||||
|
||||
// PermissionTreeManager 定義一組操作權限樹的接口
|
||||
// 這個名稱說明它是專門負責管理和操作權限樹的管理器
|
||||
type PermissionTreeManager interface {
|
||||
// AddPermission 將一個新的權限節點插入到樹中
|
||||
// key 是父節點的ID,value 是要插入的 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
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
ers "ark-permission/internal/lib/error"
|
||||
ers "code.30cm.net/wanderland/library-go/errors"
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package model
|
||||
|
||||
import "github.com/zeromicro/go-zero/core/stores/sqlx"
|
||||
|
||||
var ErrNotFound = sqlx.ErrNotFound
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
Loading…
Reference in New Issue