guard/internal/usecase/opa.go

199 lines
5.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}