workflow-plugin-authz
⚠️ Experimental — This plugin compiles and passes its unit tests but has not been validated in any active GoCodeAlone-internal production deployment. Use with caution. Please open an issue if you adopt it so we can promote it to verified status.
RBAC authorization plugin for the workflow engine using Casbin.
Capabilities
| Type |
Name |
| Module |
authz.casbin |
| Step |
step.authz_check_casbin |
| Step |
step.authz_add_policy |
| Step |
step.authz_remove_policy |
| Step |
step.authz_role_assign |
Reusable admin API
Go hosts can mount the provider-neutral admin backend from adminapi.NewHandler
instead of rebuilding authz JSON routes per application. The package serves the
route contract consumed by workflow-plugin-authz-ui under Options.BasePath
(default /api/authz):
/api/authz/roles
/api/authz/scopes
/api/authz/capabilities
/api/authz/declarations
/api/authz/projection-inputs
/api/authz/model
/api/authz/policies
/api/authz/abac/policies
/api/authz/rebac/tuples
/api/authz/rebac/check
/api/authz/enforce
The host supplies typed adapters for principal resolution, authorization, and
provider data. Enforcement remains server-side: the handler authorizes the
authenticated principal for each backend action before reading request bodies or
calling the provider. Client-supplied subjects are decision inputs, not proof of
authority.
handler, err := adminapi.NewHandler(adminapi.Options{
BasePath: "/api/v1/admin/authz",
PrincipalResolver: hostPrincipalResolver,
Authorizer: hostAuthorizer,
Provider: hostAuthzProvider,
})
New read-only hosts should use adminapi/v2.NewHandler with the standalone
authz.NewRuntime. Version 2 exposes only the authenticated principal,
capabilities, role assignments, immutable policies, and decisions:
GET /api/authz/principal
GET /api/authz/capabilities
GET /api/authz/roles
GET /api/authz/policies
POST /api/authz/decision
The decision body contains only object and action; the provider derives the
effective subject or role from the trusted principal. Known role and policy
mutation methods return 405 Method Not Allowed. Version 1 remains unchanged
for existing consumers that intentionally use its mutable provider interface.
The standalone runtime accepts exactly one policy source: PostgreSQL/SQLite
storage with an explicit migration mode, or an immutable in-memory seed. It
loads one deterministic snapshot, performs no watcher reloads, and reports
storage degradation without invalidating loaded decisions.
authz.casbin module
Loads a Casbin PERM model and policy from inline YAML config. The enforcer is thread-safe and shared with all step.authz_check_casbin steps that reference the module by name.
modules:
- name: authz
type: authz.casbin
config:
model: |
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
policies:
- ["admin", "/*", "*"]
- ["editor", "/api/*", "GET"]
- ["editor", "/api/*", "POST"]
- ["viewer", "/api/*", "GET"]
roleAssignments:
- ["alice", "admin"]
- ["bob", "editor"]
- ["carol", "viewer"]
step.authz_check_casbin pipeline step
Checks whether the authenticated user (injected by step.auth_required) has permission to perform the configured action on the configured object. Returns HTTP 403 and stops the pipeline on denial.
steps:
- type: step.auth_required # sets auth_user_id in output
config: {}
- type: step.authz_check_casbin
config:
module: authz # authz.casbin module name (default: "authz")
subject_key: auth_user_id # step output key for the subject (default: "auth_user_id")
object: "/api/v1/tenants" # static path, or Go template: "{{.request_path}}"
action: "POST" # static method, or Go template: "{{.request_method}}"
On success the step outputs:
{
"authz_subject": "alice",
"authz_object": "/api/v1/tenants",
"authz_action": "POST",
"authz_allowed": true
}
On denial (HTTP 403):
{
"response_status": 403,
"response_body": "{\"error\":\"forbidden: bob is not permitted to POST /api/v1/tenants\"}",
"response_headers": {"Content-Type": "application/json"},
"authz_allowed": false
}
step.authz_add_policy pipeline step
Adds a policy rule to the Casbin enforcer at runtime; when the rule actually changes the policy (that is, it is newly added), the step saves the updated policy via the module's configured Casbin adapter (file/GORM adapters persist to their backing store, while the in-memory adapter keeps changes for the lifetime of the process). Each element of rule may be a static string or a Go template rendered against the merged pipeline context (trigger data, prior step outputs, and current context).
steps:
- type: step.authz_add_policy
config:
module: authz # authz.casbin module name (default: "authz")
rule: ["editor", "/api/posts", "POST"] # policy rule; each element may be a Go template
Template-based rule (values resolved from the pipeline context at runtime):
steps:
- type: step.authz_add_policy
config:
module: authz
rule: ["{{.role}}", "{{.resource}}", "{{.method}}"]
On success the step outputs:
{
"authz_policy_added": true,
"authz_rule": ["editor", "/api/posts", "POST"]
}
authz_policy_added is false when the rule already existed in the enforcer.
step.authz_remove_policy pipeline step
Removes a policy rule from the Casbin enforcer at runtime. Mirrors step.authz_add_policy in configuration; each element of rule may be a static string or a Go template.
steps:
- type: step.authz_remove_policy
config:
module: authz # authz.casbin module name (default: "authz")
rule: ["editor", "/api/posts", "POST"] # policy rule to remove; elements may be Go templates
Template-based rule:
steps:
- type: step.authz_remove_policy
config:
module: authz
rule: ["{{.role}}", "{{.resource}}", "{{.method}}"]
On success the step outputs:
{
"authz_policy_removed": true,
"authz_rule": ["editor", "/api/posts", "POST"]
}
authz_policy_removed is false when the rule did not exist in the enforcer.
step.authz_role_assign pipeline step
Adds or removes role mappings (grouping policies) in the Casbin enforcer at runtime. Useful for provisioning authorization when new users or tenants are onboarded.
| Config field |
Type |
Default |
Description |
module |
string |
"authz" |
Name of the authz.casbin module |
action |
string |
"add" |
"add" to assign a role, "remove" to revoke it |
assignments |
list of grouping policy rows |
— |
One or more grouping policy rows, each with at least [user, role]; each value may be a Go template |
Assign roles (static):
steps:
- type: step.authz_role_assign
config:
module: authz
action: add # "add" (default) or "remove"
assignments:
- ["alice", "admin"]
- ["bob", "editor"]
Assign a role using templates (values resolved from the pipeline context at runtime):
steps:
- type: step.authz_role_assign
config:
module: authz
action: add
assignments:
- ["{{.new_user_id}}", "{{.tenant_role}}"]
Revoke a role:
steps:
- type: step.authz_role_assign
config:
module: authz
action: remove
assignments:
- ["bob", "editor"]
On success the step outputs:
{
"authz_role_action": "add",
"authz_role_assignments": [["alice", "admin"], ["bob", "editor"]]
}
Unified authorization decisions
step.authz_check routes one Workflow gate through the configured provider and
mode. Use it for route guards, workflow action guards, and module-owned admin
actions without coupling YAML to Casbin, Keto, or Permit-specific APIs.
steps:
- type: step.authz_require_capabilities
config:
provider: keto
module: authz
requirements:
- mode: rebac
operations: [check, manage_relations]
- type: step.authz_check
config:
provider: casbin
module: authz
mode: rbac
subject: "{{.auth_user_id}}"
context: admin
scope: admin:authz.roles:update
Go modules can call the same service surface through Workflow's module/service
registry; the request shape is provider-neutral:
decision, err := authzService.InvokeMethod("CheckRelation", map[string]any{
"subject": userID,
"context": "frontend",
"object": documentID,
"relation": "owner",
})
Build
make build # outputs bin/workflow-plugin-authz
make test # run tests with race detector
Install
make install INSTALL_DIR=data/plugins/workflow-plugin-authz