Backend

Users and permissions

Sign administrators in with JWT and cookie auth, declare permissions and navigation, and manage administrators from the console.

On this page

The admin keeps WinterCMS's backend user model: the backend_users and backend_user_roles tables, roles with permission grants, and superusers who pass every check. cabana signs administrators in and checks their permissions; the tables are created by the framework migrations that migrate runs.

Signing in#

POST <prefix>/api/v1/auth/login checks the login and password against backend_users and issues a JWT for the admin audience, signed with admin.jwt.secret. Login attempts are throttled per admin.login.max_attempts and admin.login.decay_minutes. POST .../auth/refresh reissues a token inside the refresh window, and POST .../auth/logout revokes the current token by blacklisting its ID in backend_jwt_blacklist.

The admin API accepts the token two ways:

  • API clients send Authorization: Bearer <token>.
  • The admin SPA sends X-Requested-With: XMLHttpRequest and receives the token in an HttpOnly, SameSite=Strict cookie. A cookie-authenticated request that changes state must carry that header, which blocks cross-site request forgery.

The guard is registered in bouncer under the name backend and is the middleware of every admin route except login, refresh and the language bundle. See Authentication for tokens and guards in general.

The admin keys go in config/admin.yaml, with the secret in the environment (SUMMER_ADMIN__JWT__SECRET):

jwt:
  ttl: 60
  refresh_ttl: 20160
password:
  bcrypt_cost: 12
login:
  max_attempts: 5
  decay_minutes: 1

The cookie carries the Secure attribute. backend.cookie_secure: false drops it for plain-HTTP development and is refused in the production environment.

Permissions#

A plugin declares its permissions with pact.HasPermissions, the Go form of registerPermissions, and its menu entries with pact.HasNavigation:

modules/cabana/example_controller_test.go#BlogPlugin.Permissions
// Permissions replaces registerPermissions().
func (p *BlogPlugin) Permissions() []pact.Permission {
	return []pact.Permission{
		{Code: "acme.blog.access_posts", Tab: "acme.blog::lang.plugin.name", Label: "acme.blog::lang.permissions.posts"},
		{Code: "acme.blog.access_settings", Tab: "acme.blog::lang.plugin.name", Label: "acme.blog::lang.permissions.settings"},
	}
}
modules/cabana/example_controller_test.go#BlogPlugin.Navigation
// Navigation replaces registerNavigation().
func (p *BlogPlugin) Navigation() []pact.NavigationItem {
	return []pact.NavigationItem{{
		Code:        "blog",
		Label:       "acme.blog::lang.plugin.name",
		Icon:        "icon-pencil",
		Permissions: []string{"acme.blog.access_posts"},
		Controller:  "acme.blog.posts",
		SideMenu: []pact.NavigationItem{
			{Code: "posts", Label: "acme.blog::lang.posts.title", Controller: "acme.blog.posts"},
		},
	}}
}

A controller's pact.AdminPermissioned.RequiredPermissions are checked before any schema is served or query runs, and navigation and settings entries are filtered by the permissions they name, so an administrator sees only what they may open. cabana.Allows is the check: superusers pass, a grant ending in .* matches every code with that prefix, and an empty requirement list allows any signed-in administrator. The last lines of the activation example on Admin controllers show it.

Actions registered through pact.HasAdminActions may name extra permissions, checked on top of the controller's.

Managing administrators#

The application binary has two commands for operators:

./bin/acme admin:create --email admin@example.com --password '<secret>' --superuser
./bin/acme admin:reset-password admin@example.com --password '<secret>'

admin:create creates an activated administrator; --login defaults to the lower-cased email and --role <code> assigns a role. admin:reset-password takes a login or an email, sets the password and revokes every token issued before the reset. Passwords are hashed with bcrypt at admin.password.bcrypt_cost, so hashes copied from a WinterCMS database keep working.