Check access in code
Middleware covers routes. Inside handlers, services and background jobs, check directly.
Yes or no
Section titled “Yes or no”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.
With the reason
Section titled “With the reason”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.
Several permissions at once
Section titled “Several permissions at once”w.CanAny(ctx, userID, branch, "loan:view", "loan:approve") // at least onew.CanAll(ctx, userID, branch, "loan:create", "loan:view") // every oneAn undefined permission anywhere in the list makes both return false. CanAll with an empty list returns false.
Filter a list
Section titled “Filter a list”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 ordervisible := 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.
Show and hide actions in the UI
Section titled “Show and hide actions in the UI”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.
Many checks in one request
Section titled “Many checks in one request”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.