Auth Rules
A rule is a small interface. The app calls Check with the user your provider returned; nil means allow.
type AuthRule interface {
Check(user any) error
IsPublic() bool
}
The built-in rules
| Rule | Requires |
|---|---|
ezz.Public() | Nothing. Authenticate is not even called |
ezz.Authenticated() | A non-nil user, so any valid token |
ezz.Role("admin") | HasRole("admin") returns true |
ezz.AnyRole("admin", "mod") | At least one of the roles |
ezz.AllRoles("billing", "admin") | Every listed role |
ezz.Permission("posts:write") | HasPermission("posts:write") returns true |
ezz.Custom(fn) | fn(user) returns true |
app.GET("/health", health).Public()
app.GET("/me", profile).Auth(ezz.Authenticated())
app.GET("/admin", panel).Auth(ezz.Role("admin"))
app.GET("/reports", reports).Auth(ezz.AnyRole("admin", "analyst"))
app.POST("/invoices", create).Auth(ezz.AllRoles("billing", "admin"))
app.DELETE("/posts/{id}", remove).Auth(ezz.Permission("posts:delete"))
Know what the rules check against
Role and permission rules use interface assertions on the user value:
type RoleChecker interface {
HasRole(role string) bool
}
type PermissionChecker interface {
HasPermission(permission string) bool
}
If the user does not implement the interface the rule needs, the request is denied with 403. The *auth.AuthorizedUser returned by the JWT provider implements both, so this only bites when you write a custom provider that returns your own type. Add the two methods and every rule works.
Where the roles come from depends on your setup:
- With an authorization resolver, roles and permissions come from your store, and the token's
roleclaim is ignored. - Without one,
HasRolereads therole(string) androles(array) claims from the token, andHasPermissionalways returns false, since a token carries no permission set.
Set rules at three levels
Precedence runs from broad to narrow.
app := ezz.New().Auth(provider) // 1. app default: Authenticated
api := app.Group("/api").Auth(ezz.Role("member")) // 2. group rule
api.GET("/feed", feed) // member
api.GET("/admin", adminPanel).Auth(ezz.Role("admin")) // 3. route rule wins
api.GET("/status", status).Public() // route rule wins
A nested group inherits the parent's rule at the moment it is created:
api := app.Group("/api").Auth(ezz.Role("member"))
admin := api.Group("/admin").Auth(ezz.Role("admin"))
Write a custom rule
ezz.Custom covers predicates over the user value:
app.GET("/beta", betaFeature).Auth(ezz.Custom(func(user any) bool {
u, ok := user.(auth.User)
if !ok {
return false
}
flag, _ := u.GetAttribute("beta_enabled")
enabled, _ := flag.(bool)
return enabled
}))
For anything with its own status code or message, implement the interface directly:
type tenantRule struct{ tenant string }
func (r tenantRule) Check(user any) error {
u, ok := user.(*Account)
if !ok {
return ezz.ErrForbidden
}
if u.Tenant != r.tenant {
return ezz.NewHTTPError(http.StatusNotFound, "not found") // hide existence
}
if u.Suspended {
return ezz.NewHTTPError(http.StatusPaymentRequired, "account suspended")
}
return nil
}
func (tenantRule) IsPublic() bool { return false }
func Tenant(name string) ezz.AuthRule { return tenantRule{tenant: name} }
An *ezz.HTTPError returned from Check keeps its status and message. Any other error becomes a plain 403.
Check ownership in the handler
app.GET("/documents/{id}", ezz.H(func(ctx *ezz.Context, req GetDocRequest) (*Document, error) {
user := ctx.User().(auth.User)
doc, err := store.Find(ctx, req.ID)
if err != nil {
return nil, ezz.NotFound("document not found")
}
if doc.OwnerID != user.ID() {
return nil, ezz.NotFound("document not found") // don't confirm it exists
}
return doc, nil
}))
What the caller sees
| Outcome | Response |
|---|---|
| No or invalid credentials | 401 with {"error":true,"message":"Unauthorized","status":401} |
| Rule denied the user | 403 with {"error":true,"message":"Forbidden","status":403} |
Rule returned an *ezz.HTTPError | That status and message |
| Authorization resolver failed | 500 with {"error":true,"message":"Authorization failed","status":500} |
Next steps
- JWT providers for the values these rules receive.
- Roles and permissions for loading them from your database.