Skip to content

Check access in code

Middleware covers routes. Inside handlers, services and background jobs, check directly.

if !w.Can(ctx, userID, "loan:approve", loan.Branch()) {
return ErrForbidden
}

Can returns false on any error, including an undefined permission. Use Check when you need to know why.

d, err := w.Check(ctx, userID, "loan:approve", branch)
if err != nil {
return err // undefined permission, invalid scope, database error…
}
if !d.Allowed {
log.Info("denied", "reason", d.Reason) // e.g. denied by role "ops_admin" at organisation:acme (rule "loan:approve")
}

A Decision carries Allowed, Role, At (the scope where that role is assigned), Rule (the pattern that matched) and Reason. On error it always denies.

w.CanAny(ctx, userID, branch, "loan:view", "loan:approve") // at least one
w.CanAll(ctx, userID, branch, "loan:create", "loan:view") // every one

An undefined permission anywhere in the list makes both return false. CanAll with an empty list returns false.

To show only the items a user may see, such as the products in an organisation, check them all at once:

scopes := make([]wardn.Scope, len(products))
for i, p := range products {
scopes[i] = wardn.NewScope("product", p.ID)
}
allowed := w.CanEach(ctx, userID, "product:view", scopes) // one bool per scope, in order
visible := products[:0]
for i, p := range products {
if allowed[i] {
visible = append(visible, p)
}
}

CanEach reads the user’s assignments for every scope and its ancestors together: a few queries for the whole list, not a few per item. CheckEach returns a Decision per scope, with the same reasons as Check. Any error, such as an invalid scope or more than wardn.MaxCheckEach (1,000) scopes, denies every entry, so page long lists.

Filtering in Go suits lists you have already loaded and paged. When the list is long and most items are hidden, filter in your query instead, for example by joining the products to the user’s grants, and use CanEach as the check on what you return.

Send the frontend everything the user may do at a scope, and let it hide the buttons:

perms, err := w.PermissionsOf(ctx, userID, branch)
// ["customer:view", "loan:create", "loan:view"]

Wildcards are expanded and denies applied. Hiding a button is a convenience, not security: the route must still Require the permission.

RolesOf(ctx, userID, scope) returns the assignments in effect at a scope, including inherited ones, with their time windows. It suits “your access” pages.

Inside routes protected by wardn’s middleware, store reads are memoised for the rest of the request automatically. Several Require layers and in-handler Can calls share one database read.

Elsewhere, for example in a batch job, ask for it explicitly. Use the memo for one unit of work only:

ctx = w.WithMemo(ctx)
for _, loan := range loans {
if w.Can(ctx, userID, "loan:view", loan.Branch()) { /* ... */ }
}

A change made through wardn inside the same context clears the memo, so later checks see it.

Questions about this page?

Ask below, point out something wrong or unclear, or suggest an improvement. Reading needs no account; to post, sign in with GitHub.