2024-08-15 08:16:48 +00:00
|
|
|
|
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"
|
2024-08-18 14:07:14 +00:00
|
|
|
|
"strings"
|
2024-08-15 08:16:48 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
//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
|
|
|
|
|
|
2024-08-18 14:07:14 +00:00
|
|
|
|
// 解析 allow 欄位
|
|
|
|
|
allow, ok := data["allow"].(bool)
|
|
|
|
|
if !ok {
|
2024-08-15 08:16:48 +00:00
|
|
|
|
return usecase.CheckOPAResp{}, fmt.Errorf("missing or invalid 'allow' field")
|
|
|
|
|
}
|
2024-08-18 14:07:14 +00:00
|
|
|
|
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")
|
|
|
|
|
}
|
2024-08-15 08:16:48 +00:00
|
|
|
|
|
2024-08-18 14:07:14 +00:00
|
|
|
|
// 解析 request 欄位
|
2024-08-15 08:16:48 +00:00
|
|
|
|
requestData, ok := data["request"].(map[string]any)
|
|
|
|
|
if !ok {
|
|
|
|
|
return usecase.CheckOPAResp{}, fmt.Errorf("missing or invalid 'request' field")
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-18 14:07:14 +00:00
|
|
|
|
// 解析 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")
|
|
|
|
|
}
|
2024-08-15 08:16:48 +00:00
|
|
|
|
|
2024-08-18 14:07:14 +00:00
|
|
|
|
// 解析 policies 欄位
|
2024-08-15 08:16:48 +00:00
|
|
|
|
policiesData, ok := requestData["policies"].([]any)
|
|
|
|
|
if !ok {
|
|
|
|
|
return usecase.CheckOPAResp{}, fmt.Errorf("missing or invalid 'policies' field")
|
|
|
|
|
}
|
2024-08-18 14:07:14 +00:00
|
|
|
|
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")
|
|
|
|
|
}
|
2024-08-15 08:16:48 +00:00
|
|
|
|
methods := make([]string, len(methodsData))
|
2024-08-18 14:07:14 +00:00
|
|
|
|
for j, m := range methodsData {
|
|
|
|
|
methods[j], ok = m.(string)
|
|
|
|
|
if !ok {
|
|
|
|
|
return usecase.CheckOPAResp{}, fmt.Errorf("invalid method format in policy")
|
|
|
|
|
}
|
2024-08-15 08:16:48 +00:00
|
|
|
|
}
|
2024-08-18 14:07:14 +00:00
|
|
|
|
// 組裝 policy
|
|
|
|
|
response.Request.Policies[i] = usecase.Policy{
|
2024-08-15 08:16:48 +00:00
|
|
|
|
Methods: methods,
|
2024-08-18 14:07:14 +00:00
|
|
|
|
Name: policyMap["name"].(string),
|
|
|
|
|
Path: policyMap["path"].(string),
|
|
|
|
|
Role: policyMap["role"].(string),
|
2024-08-15 08:16:48 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-18 14:07:14 +00:00
|
|
|
|
// 解析 roles 欄位
|
2024-08-15 08:16:48 +00:00
|
|
|
|
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 {
|
2024-08-18 14:07:14 +00:00
|
|
|
|
response.Request.Roles[i], ok = r.(string)
|
|
|
|
|
if !ok {
|
|
|
|
|
return usecase.CheckOPAResp{}, fmt.Errorf("invalid role format")
|
|
|
|
|
}
|
2024-08-15 08:16:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return response, nil
|
|
|
|
|
}
|