canery is a minimal, generic authorization engine built around a simple and explicit model:
- subject
- action
- resource
- scope
It focuses purely on authorization evaluation, not storage or product semantics.
It answers: “Can X do Y on Z within W?”
Most authorization solutions are either:
- too opinionated (roles, orgs, workspaces baked in)
- too heavy (full IAM platforms)
canery stays in the middle:
- ✅ explicit and predictable request model
- ✅ no framework or product lock-in
- ✅ pluggable data sources
- ✅ small, composable core
- ✅ easy to test
- ❌ does not store permissions
- ❌ does not define roles or org models
- ❌ does not include a database layer
- ❌ does not enforce product semantics
You bring those via adapters.
ok, err := authorizer.Check(ctx, canery.Request{
Subject: canery.Actor("user", userID),
Action: canery.Action("delete"),
Resource: canery.Resource("document", documentID),
Scope: canery.Scope("project", projectID),
})decision, err := authorizer.CheckDecision(ctx, canery.Request{
Subject: canery.Actor("user", userID),
Action: canery.Action("delete"),
Resource: canery.Resource("document", documentID),
Scope: canery.Scope("project", projectID),
})
if decision.Allowed {
fmt.Println(decision.Source) // "direct" or "group"
} else {
fmt.Println(decision.Source) // "none"
fmt.Println(decision.Reason) // e.g. "no matching permission"
}ok, err := authorizer.
For(canery.Actor("user", userID)).
Can(canery.Action("delete")).
Target(canery.Resource("document", documentID)).
In(canery.Scope("project", projectID)).
Check(ctx)result, err := authorizer.
For(canery.Actor("user", userID)).
CanMany(
canery.Action("view"),
canery.Action("update"),
canery.Action("delete"),
).
Target(canery.Resource("document", documentID)).
In(canery.Scope("project", projectID)).
Check(ctx)
canUpdate, _ := result.Allowed(canery.Action("update"))decisions, err := engine.BatchCheck(ctx, []canery.Request{
{
Subject: canery.Actor("user", userID),
Action: canery.Action("view"),
Resource: canery.Resource("document", documentID),
Scope: canery.Scope("project", projectID),
},
{
Subject: canery.Actor("user", userID),
Action: canery.Action("delete"),
Resource: canery.Resource("document", documentID),
Scope: canery.Scope("project", projectID),
},
})decision, trace, err := authorizer.CheckTrace(ctx, canery.Request{
Subject: canery.Actor("user", userID),
Action: canery.Action("delete"),
Resource: canery.Resource("document", documentID),
Scope: canery.Scope("project", projectID),
})
for _, step := range trace.Steps {
fmt.Println(step.Name, step.Result)
}
_ = decisionThe default Engine uses a membership-first evaluation flow:
flowchart TD
A[Check request] --> B[Validate request]
B -->|invalid| X[Return validation error]
B --> C{Resource ID present?}
C -- yes --> D[Verify resource belongs to scope]
D -- no --> L[Deny]
C -- no --> E[Verify subject belongs to scope]
D --> E
E -- no --> L
E --> F[Check direct subject permission]
F --> G{Allowed?}
G -- yes --> H[Allow]
G -- no --> I[Resolve groups in scope]
I --> J[Check group permissions]
J --> K{Any allowed?}
K -- yes --> H
K -- no --> L[Deny]
This is just the default engine — other evaluation strategies can be implemented.
flowchart LR
B[Builder] --> A[Authorizer]
A --> PA[PolicyAuthorizer]
A --> E[Engine]
PA --> E
B --> R[Request]
R --> A
E --> M[MembershipReader]
E --> G[GroupReader]
E --> PR[PermissionReader]
E --> S[ResourceScopeResolver]
All data access is delegated to interfaces:
MembershipReaderGroupReaderPermissionReaderResourceScopeResolver
This keeps the core:
- stateless
- storage-agnostic
- easy to test
authorizer := canery.NewPolicyAuthorizer(
baseAuthorizer,
canery.ForResourceType("document", canery.PolicyFunc(func(ctx context.Context, request canery.Request, next canery.DecisionEvaluator) (canery.Decision, error) {
if request.Action == canery.Action("archive") {
return canery.Decision{
Allowed: false,
Reason: "policy matched",
Source: canery.DecisionSourceNone,
}, nil
}
return next.CheckDecision(ctx, request)
})),
)Policies are:
- additive
- matcher-based
- optional
They do not introduce framework conventions or magic.
Keep your application semantics outside the core:
package projectauthz
import "github.com/rluders/canery"
const EditDocument = canery.Action("edit")
func User(id string) canery.Subject {
return canery.Actor("user", id)
}
func ProjectScope(id string) canery.ScopeRef {
return canery.Scope("project", id)
}
func Document(id string) canery.ResourceRef {
return canery.Resource("document", id)
}Usage:
ok, err := authorizer.
For(projectauthz.User(userID)).
Can(projectauthz.EditDocument).
Target(projectauthz.Document(documentID)).
In(projectauthz.ProjectScope(projectID)).
Check(ctx)examples/simple→ minimal core usageexamples/advanced→ realistic app setup (users, projects, roles, etc.)
Each example is a standalone Go module.
subject→ requiresTypeandIDaction→ must be non-emptyresource→ requiresTypescope→ requiresTypeandID
Invalid requests return a structured ValidationError
(errors.Is(err, canery.ErrInvalidRequest) still works)
Use it when you want:
- explicit, testable authorization logic
- full control over your data model
- no framework constraints
Avoid it if you need:
- a complete IAM system
- UI, policy language, or managed service
- out-of-the-box RBAC/ABAC models
MIT
