Migrating to v0.23¶
v0.23 splits admin access into three role tiers and pushes admin-only gating from the /admin/ frame down into the individual HasAdmin apps. Existing two-tier setups (user vs admin) keep working, but apps that ship admin routes must self-gate, the auth CLI changes shape, and the burrow.AdminAuth interface gains a method.
For the full list of changes, see the v0.23 changelog.
Quick orientation¶
| What you used | What replaces it | Section |
|---|---|---|
RoleUser / RoleAdmin |
RoleUser / RoleStaff / RoleAdmin; staff enters /admin/, admin gates the rest |
Three-tier roles |
/admin/ frame middleware: RequireAuth + RequireAdmin |
RequireAuth + RequireStaff; apps self-gate admin routes |
Frame opens to staff |
burrow.AdminAuth { RequireAuth, RequireAdmin } |
burrow.AdminAuth { RequireAuth, RequireStaff, RequireAdmin } |
AdminAuth gains RequireStaff |
auth promote <user> / auth demote <user> |
auth set-role <user> <user\|staff\|admin> |
CLI rename |
NavItem{AuthOnly, AdminOnly} |
NavItem{AuthOnly, StaffOnly, AdminOnly} |
NavItem.StaffOnly |
Three-tier roles¶
contrib/auth now ships three roles:
auth.RoleUser— the default for newly registered users. Can log in, manage their own credentials, see public pages. Cannot enter/admin/.auth.RoleStaff— can enter the admin shell. Sees only the admin views their app exposes to staff (per-route gating decides the rest).auth.RoleAdmin— full access.User.IsAdmin()is true;User.IsStaff()is also true (admins are implicit staff).
Existing data: nothing to migrate. Existing RoleAdmin users stay admin and keep their access. Existing RoleUser users keep their role, and since they were already rejected by RequireAdmin under v0.22 their /admin/ access doesn't change — the gate is just labelled RequireStaff now. The new tier opens up for users you explicitly promote:
If you have a ./myapp promote alice (or demote) call in a deploy script, cron job, or runbook, it will fail at runtime as an unknown command — there is no shim. Grep your infra for auth promote / auth demote and update those call sites to auth set-role <user> admin / auth set-role <user> user.
Frame opens to staff¶
Before v0.23, the /admin/ route tree was gated by RequireAuth + RequireAdmin, so any route mounted via HasAdmin.AdminRoutes was automatically admin-only. From v0.23 on, the frame middleware is RequireAuth + RequireStaff and HasAdmin apps decide their own gating:
// before — every route inside AdminRoutes was implicitly admin-only
func (a *App) AdminRoutes(r chi.Router) {
r.Get("/users", a.adminListUsers)
r.Post("/users/{id}", a.adminUpdateUser)
}
// after — wrap admin-only routes in a sub-group with RequireAdmin()
func (a *App) AdminRoutes(r chi.Router) {
r.Group(func(r chi.Router) {
r.Use(auth.RequireAdmin())
r.Get("/users", a.adminListUsers)
r.Post("/users/{id}", a.adminUpdateUser)
})
}
Routes that should remain visible to all staff (e.g. content authoring) stay outside the inner group. Tag the matching AdminNavItems entries with AdminOnly: true so non-admin staff don't see them in the dashboard:
func (a *App) AdminNavItems() []burrow.NavItem {
return []burrow.NavItem{
{Label: "Posts", URL: "/admin/posts"}, // every staff member
{Label: "Settings", URL: "/admin/settings", AdminOnly: true}, // admin only
}
}
The admin dashboard filters nav items per request and drops empty groups, so a non-admin staff user sees a clean cards-grid with only the sections they can reach.
The in-tree contrib/auth (users, invites) and contrib/jobs (job queue) admin routes are already wrapped this way — no action needed if you only consume them.
AdminAuth gains RequireStaff¶
The burrow.AdminAuth interface grew a method:
type AdminAuth interface {
RequireAuth() func(http.Handler) http.Handler
RequireStaff() func(http.Handler) http.Handler // new in v0.23
RequireAdmin() func(http.Handler) http.Handler
}
If you use contrib/auth as your AdminAuth provider, nothing to do — the new method is wired automatically. Custom AdminAuth implementations must add RequireStaff() to satisfy the interface; the compiler will tell you. If you want to keep the v0.22 "admin-only" behaviour for the frame, the one-liner is:
That keeps everything below /admin/ admin-only until you decide to introduce a real staff tier.
A complementary auth.RequireStaff() package-level middleware is available for direct use in your own routes:
CLI rename¶
auth promote <username> and auth demote <username> have been removed and replaced with a single subcommand that takes the target role:
# v0.22
./myapp promote alice
./myapp demote alice
# v0.23
./myapp set-role alice admin # promote
./myapp set-role alice user # demote
./myapp set-role alice staff # new tier
Invalid role strings now fail with a clear error rather than being silently coerced.
NavItem.StaffOnly¶
burrow.NavItem gained a StaffOnly bool flag next to AuthOnly and AdminOnly. Use it on the public navLinks to hide entries from logged-in users that aren't staff (e.g. a "Studio" link that takes authors into /admin/):
return []burrow.NavItem{
{Label: "Profile", URL: "/profile", AuthOnly: true},
{Label: "Studio", URL: "/admin/", StaffOnly: true},
}
Semantics: AuthOnly hides from anonymous; StaffOnly hides from anonymous and RoleUser; AdminOnly hides from everyone except RoleAdmin. Combine freely — the most restrictive flag wins. Inside /admin/ itself, only AdminOnly is honoured (the frame already guarantees staff).
Three new helpers expose the underlying state to application code without importing contrib/auth:
burrow.IsAuthenticated(ctx) // any logged-in user
burrow.IsStaff(ctx) // staff or admin
burrow.IsAdmin(ctx) // admin only
Auth providers wire these via burrow.AuthChecker (now with an IsStaff closure). The standard contrib/auth middleware populates all three automatically.