From b99b24ae5b2a015881ed8f50e343251d0ede841e Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Thu, 26 Feb 2026 16:30:38 -0500 Subject: [PATCH] refactor: remove OPA/Cedar/permit.io stubs from engine These will be reimplemented as external plugins (workflow-plugin-policy-opa, workflow-plugin-policy-cedar) consistent with the plugin architecture. Co-Authored-By: Claude Opus 4.6 --- cmd/wfctl/type_registry.go | 14 +--- module/policy_engine.go | 126 +----------------------------------- plugin/rbac/builtin_test.go | 18 +----- plugin/rbac/permit.go | 39 ----------- plugins/policy/plugin.go | 54 +++------------- 5 files changed, 15 insertions(+), 236 deletions(-) delete mode 100644 plugin/rbac/permit.go diff --git a/cmd/wfctl/type_registry.go b/cmd/wfctl/type_registry.go index 38abbe4e..813bcf4c 100644 --- a/cmd/wfctl/type_registry.go +++ b/cmd/wfctl/type_registry.go @@ -440,19 +440,7 @@ func KnownModuleTypes() map[string]ModuleTypeInfo { ConfigKeys: []string{"account", "region", "service_role", "compute_type", "image", "source_type"}, }, - // policy plugin - "policy.opa": { - Type: "policy.opa", - Plugin: "policy", - Stateful: false, - ConfigKeys: []string{"endpoint", "policies"}, - }, - "policy.cedar": { - Type: "policy.cedar", - Plugin: "policy", - Stateful: false, - ConfigKeys: []string{"policies"}, - }, + // policy plugin (OPA and Cedar are external plugins: workflow-plugin-policy-opa, workflow-plugin-policy-cedar) "policy.mock": { Type: "policy.mock", Plugin: "policy", diff --git a/module/policy_engine.go b/module/policy_engine.go index b1943f43..f2df66bb 100644 --- a/module/policy_engine.go +++ b/module/policy_engine.go @@ -3,7 +3,6 @@ package module import ( "context" "fmt" - "log/slog" "sync" "github.com/CrisisTextLine/modular" @@ -31,7 +30,7 @@ type PolicyInfo struct { } // PolicyEngineModule is a workflow module wrapping a pluggable PolicyEngine backend. -// Supported backends: "mock", "opa", "cedar". +// Supported backends: "mock". For OPA or Cedar, use external plugins. type PolicyEngineModule struct { name string config map[string]any @@ -54,20 +53,13 @@ func (m *PolicyEngineModule) Init(app modular.Application) error { m.backend = "mock" } - allowStub := isTruthy(m.config["allow_stub_backends"]) - switch m.backend { case "mock": m.engine = newMockPolicyEngine() case "opa": - endpoint, _ := m.config["endpoint"].(string) - m.engine = newOPAPolicyEngine(endpoint, allowStub) - slog.Warn("WARNING: using stub policy engine — all requests will be DENIED. Set allow_stub_backends: true in config to use stub backends for testing.", - "module", m.name, "backend", "opa", "allow_stub_backends", allowStub) + return fmt.Errorf("opa backend not built-in; use the workflow-plugin-policy-opa external plugin") case "cedar": - m.engine = newCedarPolicyEngine(allowStub) - slog.Warn("WARNING: using stub policy engine — all requests will be DENIED. Set allow_stub_backends: true in config to use stub backends for testing.", - "module", m.name, "backend", "cedar", "allow_stub_backends", allowStub) + return fmt.Errorf("cedar backend not built-in; use the workflow-plugin-policy-cedar external plugin") default: return fmt.Errorf("policy.engine %q: unsupported backend %q", m.name, m.backend) } @@ -172,17 +164,6 @@ func (e *mockPolicyEngine) Evaluate(_ context.Context, input map[string]any) (*P }, nil } -// isTruthy returns true if v is a bool true, or a string "true"/"1"/"yes". -func isTruthy(v any) bool { - switch val := v.(type) { - case bool: - return val - case string: - return val == "true" || val == "1" || val == "yes" - } - return false -} - func containsString(s, substr string) bool { return len(s) >= len(substr) && (s == substr || len(substr) == 0 || func() bool { @@ -195,104 +176,3 @@ func containsString(s, substr string) bool { }()) } -// ─── OPA backend stub ──────────────────────────────────────────────────────── - -// opaPolicyEngine is a stub for OPA (Open Policy Agent) integration. -// Production: POST to the OPA REST API at /v1/data/. -type opaPolicyEngine struct { - endpoint string - allowStub bool - mu sync.RWMutex - policies map[string]string -} - -func newOPAPolicyEngine(endpoint string, allowStub bool) *opaPolicyEngine { - if endpoint == "" { - endpoint = "http://localhost:8181" - } - return &opaPolicyEngine{endpoint: endpoint, allowStub: allowStub, policies: make(map[string]string)} -} - -func (e *opaPolicyEngine) LoadPolicy(name, content string) error { - e.mu.Lock() - defer e.mu.Unlock() - // Production: PUT to /v1/policies/ with content as Rego source. - e.policies[name] = content - return nil -} - -func (e *opaPolicyEngine) ListPolicies() []PolicyInfo { - e.mu.RLock() - defer e.mu.RUnlock() - out := make([]PolicyInfo, 0, len(e.policies)) - for n, c := range e.policies { - out = append(out, PolicyInfo{Name: n, Backend: "opa", Content: c}) - } - return out -} - -func (e *opaPolicyEngine) Evaluate(_ context.Context, input map[string]any) (*PolicyDecision, error) { - // Production: POST {"input": input} to /v1/data/ - // and parse the result body for {"result": {"allow": true}}. - if e.allowStub { - return &PolicyDecision{ - Allowed: true, - Reasons: []string{"opa stub: allow_stub_backends enabled"}, - Metadata: map[string]any{"backend": "opa", "endpoint": e.endpoint, "input": input}, - }, nil - } - return &PolicyDecision{ - Allowed: false, - Reasons: []string{"STUB IMPLEMENTATION - not connected to real backend - denied for safety"}, - Metadata: map[string]any{"backend": "opa", "endpoint": e.endpoint, "input": input}, - }, nil -} - -// ─── Cedar backend stub ────────────────────────────────────────────────────── - -// cedarPolicyEngine is a stub for Cedar policy language integration. -// Production: use the cedar-go library (github.com/cedar-policy/cedar-go). -type cedarPolicyEngine struct { - allowStub bool - mu sync.RWMutex - policies map[string]string -} - -func newCedarPolicyEngine(allowStub bool) *cedarPolicyEngine { - return &cedarPolicyEngine{allowStub: allowStub, policies: make(map[string]string)} -} - -func (e *cedarPolicyEngine) LoadPolicy(name, content string) error { - e.mu.Lock() - defer e.mu.Unlock() - // Production: parse and compile Cedar policy set via cedar-go. - e.policies[name] = content - return nil -} - -func (e *cedarPolicyEngine) ListPolicies() []PolicyInfo { - e.mu.RLock() - defer e.mu.RUnlock() - out := make([]PolicyInfo, 0, len(e.policies)) - for n, c := range e.policies { - out = append(out, PolicyInfo{Name: n, Backend: "cedar", Content: c}) - } - return out -} - -func (e *cedarPolicyEngine) Evaluate(_ context.Context, input map[string]any) (*PolicyDecision, error) { - // Production: build a cedar.Request from input (principal, action, resource, context) - // and call policySet.IsAuthorized(request). - if e.allowStub { - return &PolicyDecision{ - Allowed: true, - Reasons: []string{"cedar stub: allow_stub_backends enabled"}, - Metadata: map[string]any{"backend": "cedar", "input": input}, - }, nil - } - return &PolicyDecision{ - Allowed: false, - Reasons: []string{"STUB IMPLEMENTATION - not connected to real backend - denied for safety"}, - Metadata: map[string]any{"backend": "cedar", "input": input}, - }, nil -} diff --git a/plugin/rbac/builtin_test.go b/plugin/rbac/builtin_test.go index 306fd37f..507eeca9 100644 --- a/plugin/rbac/builtin_test.go +++ b/plugin/rbac/builtin_test.go @@ -224,23 +224,7 @@ func TestBuiltinProvider_WithPermissionManager(t *testing.T) { } } -func TestStubProviders_ReturnErrors(t *testing.T) { - ctx := context.Background() - - permit := NewPermitProvider("key", "https://pdp.permit.io") - if permit.Name() != "permit" { - t.Errorf("expected name 'permit', got %q", permit.Name()) - } - if _, err := permit.CheckPermission(ctx, "s", "r", "a"); err == nil { - t.Error("expected error from permit stub") - } - if _, err := permit.ListPermissions(ctx, "s"); err == nil { - t.Error("expected error from permit stub") - } - if err := permit.SyncRoles(ctx, nil); err == nil { - t.Error("expected error from permit stub") - } - +func TestAWSIAMProvider_NameCheck(t *testing.T) { aws := NewAWSIAMProviderWithClient("us-east-1", "arn:aws:iam::role/test", &mockIAMClient{}) if aws.Name() != "aws-iam" { t.Errorf("expected name 'aws-iam', got %q", aws.Name()) diff --git a/plugin/rbac/permit.go b/plugin/rbac/permit.go deleted file mode 100644 index 215b7681..00000000 --- a/plugin/rbac/permit.go +++ /dev/null @@ -1,39 +0,0 @@ -package rbac - -import ( - "context" - "fmt" - - "github.com/GoCodeAlone/workflow/auth" -) - -// PermitProvider is a stub for permit.io integration. -// It defines the interface shape; the full SDK integration is left for -// when the permit.io dependency is added. -type PermitProvider struct { - apiKey string - endpoint string -} - -// NewPermitProvider creates a PermitProvider with the given API key and endpoint. -func NewPermitProvider(apiKey, endpoint string) *PermitProvider { - return &PermitProvider{apiKey: apiKey, endpoint: endpoint} -} - -// Name returns the provider identifier. -func (p *PermitProvider) Name() string { return "permit" } - -// CheckPermission calls the permit.io PDP to evaluate access. -func (p *PermitProvider) CheckPermission(_ context.Context, _, _, _ string) (bool, error) { - return false, fmt.Errorf("permit.io provider not implemented: configure API key at %s", p.endpoint) -} - -// ListPermissions retrieves permissions from permit.io for the subject. -func (p *PermitProvider) ListPermissions(_ context.Context, _ string) ([]auth.Permission, error) { - return nil, fmt.Errorf("permit.io provider not implemented") -} - -// SyncRoles pushes role definitions to permit.io. -func (p *PermitProvider) SyncRoles(_ context.Context, _ []auth.RoleDefinition) error { - return fmt.Errorf("permit.io provider not implemented") -} diff --git a/plugins/policy/plugin.go b/plugins/policy/plugin.go index 2429b04d..32e85504 100644 --- a/plugins/policy/plugin.go +++ b/plugins/policy/plugin.go @@ -1,6 +1,7 @@ -// Package policy provides an EnginePlugin that registers policy engine module -// types (policy.opa, policy.cedar, policy.mock) and policy pipeline step types -// (step.policy_evaluate, step.policy_load, step.policy_list, step.policy_test). +// Package policy provides an EnginePlugin that registers the policy.mock module +// type and policy pipeline step types (step.policy_evaluate, step.policy_load, +// step.policy_list, step.policy_test). For OPA or Cedar backends, use the +// workflow-plugin-policy-opa or workflow-plugin-policy-cedar external plugins. package policy import ( @@ -23,17 +24,15 @@ func New() *Plugin { BaseNativePlugin: plugin.BaseNativePlugin{ PluginName: "policy", PluginVersion: "1.0.0", - PluginDescription: "External policy engine adapter supporting OPA (Open Policy Agent) and Cedar", + PluginDescription: "Policy engine plugin providing mock backend; OPA and Cedar via external plugins", }, Manifest: plugin.PluginManifest{ Name: "policy", Version: "1.0.0", Author: "GoCodeAlone", - Description: "Policy engine adapter for OPA (Open Policy Agent) and Cedar authorization policies", + Description: "Policy engine plugin with mock backend for testing and development", Tier: plugin.TierCore, ModuleTypes: []string{ - "policy.opa", - "policy.cedar", "policy.mock", }, StepTypes: []string{ @@ -55,28 +54,16 @@ func (p *Plugin) Capabilities() []capability.Contract { return []capability.Contract{ { Name: "policy-enforcement", - Description: "Policy evaluation using OPA (Rego), Cedar, or mock backends for authorization decisions", + Description: "Policy evaluation using mock backend for testing and development", }, } } -// ModuleFactories returns factories for policy.opa, policy.cedar, and policy.mock. +// ModuleFactories returns factories for policy.mock. +// For OPA or Cedar backends, use the workflow-plugin-policy-opa or +// workflow-plugin-policy-cedar external plugins. func (p *Plugin) ModuleFactories() map[string]plugin.ModuleFactory { return map[string]plugin.ModuleFactory{ - "policy.opa": func(name string, cfg map[string]any) modular.Module { - if cfg == nil { - cfg = map[string]any{} - } - cfg["backend"] = "opa" - return module.NewPolicyEngineModule(name, cfg) - }, - "policy.cedar": func(name string, cfg map[string]any) modular.Module { - if cfg == nil { - cfg = map[string]any{} - } - cfg["backend"] = "cedar" - return module.NewPolicyEngineModule(name, cfg) - }, "policy.mock": func(name string, cfg map[string]any) modular.Module { if cfg == nil { cfg = map[string]any{} @@ -112,27 +99,6 @@ func (p *Plugin) ModuleSchemas() []*schema.ModuleSchema { } return []*schema.ModuleSchema{ - { - Type: "policy.opa", - Label: "OPA Policy Engine", - Category: "security", - Description: "Open Policy Agent (OPA) policy engine. Evaluates Rego policies via OPA REST API.", - Inputs: []schema.ServiceIODef{{Name: "input", Type: "PolicyInput", Description: "JSON input document for policy evaluation"}}, - Outputs: []schema.ServiceIODef{{Name: "decision", Type: "PolicyDecision", Description: "Policy decision: allowed/denied with reasons"}}, - ConfigFields: append([]schema.ConfigFieldDef{ - {Key: "endpoint", Label: "OPA Endpoint", Type: schema.FieldTypeString, DefaultValue: "http://localhost:8181", Description: "OPA REST API endpoint URL", Placeholder: "http://localhost:8181"}, - }, sharedPolicyFields...), - DefaultConfig: map[string]any{"endpoint": "http://localhost:8181"}, - }, - { - Type: "policy.cedar", - Label: "Cedar Policy Engine", - Category: "security", - Description: "Cedar policy language engine for authorization. Uses the cedar-go library.", - Inputs: []schema.ServiceIODef{{Name: "input", Type: "PolicyInput", Description: "Cedar request: principal, action, resource, context"}}, - Outputs: []schema.ServiceIODef{{Name: "decision", Type: "PolicyDecision", Description: "Policy decision: allowed/denied with reasons"}}, - ConfigFields: sharedPolicyFields, - }, { Type: "policy.mock", Label: "Mock Policy Engine",