Skip to content

Authorization

There are three places to decide who may call what, from the broadest to the narrowest. All three can be used together.

1. Rules in security/rules.go. They are checked top to bottom, and the first match wins:

func Rules(cfg *Config) []Rule {
rules := []Rule{
Path("/").PermitAll(),
Path("/health/**").PermitAll(),
Path("/auth/register", "/auth/login", "/auth/refresh", "/auth/logout").PermitAll(),
Path("/admin/**").HasRole("admin"),
Method("GET").Path("/courses", "/courses/**").PermitAll(),
Method("POST", "PUT", "PATCH", "DELETE").Path("/products/**").HasAuthority("products:write"),
}
if !cfg.Production() {
rules = append(rules, Path("/docs", "/openapi.json").PermitAll())
}
return append(rules, AnyRequest().Authenticated())
}

Patterns are route paths, as registered (with the global prefix if you set one): * is one segment and ** any number. A route that no rule matches needs a signed in user, because the guard adds AnyRequest().Authenticated() at the end even if you remove it.

2. Route options, like @PreAuthorize on a controller method:

func (c *Controller) Routes(r dygo.Router) {
r = r.Prefix("/orders")
r.Get("/", dygo.HandleNoInput(c.List)) // a signed in user
r.Get("/catalog", dygo.HandleNoInput(c.Catalog), security.Public()) // anyone
r.Delete("/{id}", dygo.Handle(c.Delete), security.Roles("admin")) // admins
r.Put("/{id}", dygo.Handle(c.Update), security.Authorities("orders:write"))
}

Public() also marks the route as public in the OpenAPI document. On r.Use(...) an option applies to the whole controller.

dygo g security also sets BearerAuth: true in the app’s openapi.Options, so /docs shows an Authorize button. Log in with POST /auth/login, paste the accessToken into Authorize (without the Bearer prefix), and every “Try it out” call sends it. Routes marked Public() show no lock.

3. Checks in code, for decisions that need data:

func (s *Service) Cancel(ctx context.Context, id int) error {
if err := security.Require(ctx, security.HasAuthority("orders:cancel")); err != nil {
return err // 401 or 403
}
order, err := s.repo.Find(ctx, id)
if err != nil {
return err
}
if err := security.RequireOwner(ctx, order.UserID); err != nil {
return err // 403 unless it is the caller's order, or the caller is an admin
}
...
}

p, ok := security.CurrentUser(ctx) returns the signed in user anywhere below the guard: p.Subject (the user ID), p.Email, p.Roles, p.Authorities.

security/roles.go holds the role hierarchy and what each role grants:

var Hierarchy = map[string][]string{
"admin": {"user"}, // an admin passes HasRole("user") too
}
var RoleAuthorities = map[string][]string{
"admin": {"orders:write", "orders:cancel"},
}

RequireOwner lets OwnerBypassRole ("admin") through; set it to "" to make owners the only ones allowed.