-
Notifications
You must be signed in to change notification settings - Fork 0
feat: support custom request dimensions in step.authz_check_casbin via extra_fields #15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,13 +23,17 @@ import ( | |
| // object: "/api/v1/tenants" # static object, or Go template: "{{.request_path}}" | ||
| // action: "POST" # static action, or Go template: "{{.request_method}}" | ||
| // audit: false # when true, adds audit_event to output (default: false) | ||
| // extra_fields: # optional extra Casbin request dimensions (inserted between sub and obj/act) | ||
| // - key: tenant # field name (used as audit key) | ||
| // value: "{{.steps.auth.affiliate_id}}" # static value or Go template | ||
| type authzCheckStep struct { | ||
| name string | ||
| moduleName string | ||
| subjectKey string | ||
| object string | ||
| action string | ||
| audit bool | ||
| name string | ||
| moduleName string | ||
| subjectKey string | ||
| object string | ||
| action string | ||
| audit bool | ||
| extraFields []extraField | ||
|
|
||
| // parsed templates (nil when static string is used) | ||
| objectTmpl *template.Template | ||
|
|
@@ -39,6 +43,14 @@ type authzCheckStep struct { | |
| registry moduleRegistry | ||
| } | ||
|
|
||
| // extraField represents one additional request dimension for the Casbin Enforce | ||
| // call. The value can be a static string or a Go template expression. | ||
| type extraField struct { | ||
| key string | ||
| value string | ||
| tmpl *template.Template | ||
| } | ||
|
|
||
| // moduleRegistry abstracts module look-up so tests can inject a fake enforcer. | ||
| type moduleRegistry interface { | ||
| // GetEnforcer returns the CasbinModule for the given module name. | ||
|
|
@@ -125,6 +137,41 @@ func newAuthzCheckStep(name string, config map[string]any) (*authzCheckStep, err | |
| s.action = action | ||
| } | ||
|
|
||
| // Parse optional extra_fields for multi-dimensional enforcement (e.g. tenant). | ||
| if raw, exists := config["extra_fields"]; exists { | ||
| rawFields, ok := raw.([]any) | ||
| if !ok { | ||
| return nil, fmt.Errorf("step.authz_check_casbin %q: extra_fields must be a list", name) | ||
| } | ||
| seen := make(map[string]bool, len(rawFields)) | ||
| for i, rawItem := range rawFields { | ||
| item, ok := rawItem.(map[string]any) | ||
| if !ok { | ||
| return nil, fmt.Errorf("step.authz_check_casbin %q: extra_fields[%d] must be a map with \"key\" and \"value\"", name, i) | ||
| } | ||
| key, _ := item["key"].(string) | ||
| val, _ := item["value"].(string) | ||
| if key == "" { | ||
| return nil, fmt.Errorf("step.authz_check_casbin %q: extra_fields[%d] missing required \"key\"", name, i) | ||
| } | ||
| if val == "" { | ||
| return nil, fmt.Errorf("step.authz_check_casbin %q: extra_fields[%d] missing required \"value\"", name, i) | ||
| } | ||
| if seen[key] { | ||
| return nil, fmt.Errorf("step.authz_check_casbin %q: extra_fields has duplicate key %q", name, key) | ||
| } | ||
| seen[key] = true | ||
| ef := extraField{key: key, value: val} | ||
| if isTemplate(val) { | ||
| ef.tmpl, err = template.New(fmt.Sprintf("extra_field_%d", i)).Parse(val) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("step.authz_check_casbin %q: parse extra_fields[%d] template: %w", name, i, err) | ||
|
Comment on lines
+164
to
+168
|
||
| } | ||
| } | ||
| s.extraFields = append(s.extraFields, ef) | ||
| } | ||
| } | ||
|
|
||
| return s, nil | ||
| } | ||
|
|
||
|
|
@@ -164,21 +211,31 @@ func (s *authzCheckStep) Execute( | |
| return nil, fmt.Errorf("step.authz_check_casbin %q: resolve action: %w", s.name, err) | ||
| } | ||
|
|
||
| // Resolve optional extra request dimensions (e.g. tenant). | ||
| extraVals := make([]string, len(s.extraFields)) | ||
| for i, ef := range s.extraFields { | ||
| v, err := resolve(ef.value, ef.tmpl, tmplData) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("step.authz_check_casbin %q: resolve extra_fields[%d] (%s): %w", s.name, i, ef.key, err) | ||
| } | ||
| extraVals[i] = v | ||
| } | ||
|
|
||
| // Look up the Casbin enforcer. | ||
| mod, ok := s.registry.GetEnforcer(s.moduleName) | ||
| if !ok { | ||
| return nil, fmt.Errorf("step.authz_check_casbin %q: authz module %q not found; check module name in config", s.name, s.moduleName) | ||
| } | ||
|
|
||
| allowed, err := mod.Enforce(subject, object, action) | ||
| allowed, err := mod.Enforce(subject, object, action, extraVals...) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("step.authz_check_casbin %q: enforce: %w", s.name, err) | ||
| } | ||
|
|
||
| if !allowed { | ||
| result := forbiddenResult(fmt.Sprintf("forbidden: %s is not permitted to %s %s", subject, action, object)) | ||
| if s.audit { | ||
| result.Output["audit_event"] = map[string]any{ | ||
| evt := map[string]any{ | ||
| "type": "authz_decision", | ||
| "subject": subject, | ||
| "object": object, | ||
|
|
@@ -187,6 +244,14 @@ func (s *authzCheckStep) Execute( | |
| "timestamp": time.Now().UTC().Format(time.RFC3339), | ||
| "module": s.moduleName, | ||
| } | ||
| if len(s.extraFields) > 0 { | ||
| extras := make(map[string]any, len(s.extraFields)) | ||
| for i, ef := range s.extraFields { | ||
| extras[ef.key] = extraVals[i] | ||
| } | ||
| evt["extra_fields"] = extras | ||
| } | ||
| result.Output["audit_event"] = evt | ||
|
Comment on lines
246
to
+254
|
||
| } | ||
| return result, nil | ||
| } | ||
|
|
@@ -198,7 +263,7 @@ func (s *authzCheckStep) Execute( | |
| "authz_allowed": true, | ||
| } | ||
| if s.audit { | ||
| output["audit_event"] = map[string]any{ | ||
| evt := map[string]any{ | ||
| "type": "authz_decision", | ||
| "subject": subject, | ||
| "object": object, | ||
|
|
@@ -207,6 +272,14 @@ func (s *authzCheckStep) Execute( | |
| "timestamp": time.Now().UTC().Format(time.RFC3339), | ||
| "module": s.moduleName, | ||
| } | ||
| if len(s.extraFields) > 0 { | ||
| extras := make(map[string]any, len(s.extraFields)) | ||
| for i, ef := range s.extraFields { | ||
| extras[ef.key] = extraVals[i] | ||
| } | ||
| evt["extra_fields"] = extras | ||
| } | ||
| output["audit_event"] = evt | ||
| } | ||
| return &sdk.StepResult{Output: output}, nil | ||
| } | ||
|
|
@@ -245,15 +318,33 @@ func resolveSubject(key string, stepOutputs map[string]map[string]any, current, | |
|
|
||
| // buildTemplateData merges all context maps into a single flat map for template | ||
| // execution. Later sources overwrite earlier ones: triggerData < stepOutputs < current. | ||
| // Step outputs are also available under a nested "steps" key (set only when | ||
| // triggerData does not already define one) so templates can reference individual | ||
| // step outputs with {{.steps.stepName.fieldKey}}. | ||
| // | ||
| // Note: dot-notation ({{.steps.stepName.key}}) only works when the step name is | ||
| // a valid Go identifier. For step names containing dashes or other special | ||
| // characters use index notation instead: | ||
| // | ||
| // {{ index .steps "step-name" "fieldKey" }} | ||
| func buildTemplateData(triggerData map[string]any, stepOutputs map[string]map[string]any, current map[string]any) map[string]any { | ||
| data := make(map[string]any) | ||
| for k, v := range triggerData { | ||
| data[k] = v | ||
| } | ||
| for _, out := range stepOutputs { | ||
| steps := make(map[string]any, len(stepOutputs)) | ||
| for name, out := range stepOutputs { | ||
| for k, v := range out { | ||
| data[k] = v | ||
| } | ||
| steps[name] = out | ||
| } | ||
| // Only inject the "steps" key when the caller has not already supplied one | ||
| // in triggerData, to avoid silently overwriting an existing value. | ||
| if len(steps) > 0 { | ||
| if _, exists := data["steps"]; !exists { | ||
| data["steps"] = steps | ||
| } | ||
| } | ||
| for k, v := range current { | ||
| data[k] = v | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PR description/examples refer to
step.authz_check, but this repository’s step type appears to bestep.authz_check_casbin(as reflected in the step’s error messages here). Consider aligning the PR description/README/examples so users don’t configure a non-existent step type.