Skip to content

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:

./myapp set-role alice staff

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:

func (a *MyAuth) RequireStaff() func(http.Handler) http.Handler {
    return a.RequireAdmin()
}

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:

r.Group(func(r chi.Router) {
    r.Use(auth.RequireStaff())
    r.Get("/studio", studioHandler)
})

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.

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.