RBAC and scopes
Permissions are dynamic. Operators define roles by picking from a fixed permission list, and a user can hold several roles directly or pick them up from user groups. The seeded Admin and User roles are defaults you can replace, not the only options.
Per-permission scopes
Most permissions come in two flavours: full and scoped.
ListDevicesreturns every device.ListDevices:assignedreturns only devices assigned to the calling user.
Scopes are enforced at the handler layer via auth.EnforceSelfScope and userFilterID(). Handlers receive a *string filter (nil for unscoped admins, the user's ID for scoped users) and thread it into the underlying SQL through a sqlc OwnerScope parameter.
A new RPC that operates on per-device data has to wire userFilterID(ctx, "<RPCName>") into the read query if the permission supports :assigned. A parity-test sweep in internal/api catches a missing wire-up: every scoped permission must round-trip through a tagged query.
User groups
A user group is a named collection of users that grants roles (and through them, permissions) additively. Membership is either static (operator picks users) or dynamic (a query over user-profile fields — email, display_name, preferred_username, locale, disabled, totp_enabled, has_password — using the same grammar as device groups).
Permissions are unioned across a user's direct roles and all groups they belong to. There is no "deny" semantic. To take a permission away, remove the user from the group that grants it.
Identity providers and SCIM
For SSO, OIDC identity providers create users on first sign-in when auto_create_users is on. SCIM v2 endpoints accept full user and group provisioning from upstream IdPs. Both paths map onto the same event types the web UI emits, so the audit trail looks the same no matter how the change came in.