InfraGuard

Roles, Teams & Owners

Roles and teams are informational labels only — level is the sole access gate — plus the combined user/team Owner picker.

Overview — what it does

Roles and Teams are informational labels for classifying people. The one and only thing that opens access to screens and actions is the level (user_level). What a user can see, and what they may save or delete, is decided 100% by level; roles ("DBA", "DevOps", "Security") and teams ("Infra Team", "Security Team") are display/organization tags that grant no permissions whatsoever.

This keeps the permission model simple. To give someone admin power you raise their level — you do not change their role. Tagging a user "Security" does not open the security screens; their level must be Staff (5) or higher.

This article covers three things:

  1. Roles (infr_role) — six job labels (DBA, DevOps, Security, Admin, Staff, Developer).
  2. Teams (infr_team) — organizational units, used for user membership and asset ownership.
  3. The Owner picker — a combined dropdown that lets you assign either a user or a team, with App\Support\Owners as its single source of truth. The "owner" field on Instances, Hardware and Storage uses it.

Access control = level (user_level) only. Roles and teams open no permissions programmatically. See Understanding levels (RBAC) for the level system.

When to use

Layout

/admin/users (Administration ▸ Users & Roles) has two tabs.

Tab Contents
Users User list (login ID · name · email · role · team · level · status · last login) + modal CRUD
Teams Team list (name · color · lead · description · member count) + modal CRUD

The user edit modal has level / role / team selectors, followed by this hint:

Level decides access — Demo (read) · Staff (operate) · Admin (everything). Roles are for display.

How to use

Roles — job labels

The role master is six entries, upserted idempotently by key by InfrRoleSeeder.

key Label Color token seq
dba DBA info 10
devops DevOps accent 20
security Security crit 30
admin Admin warn 40
staff Staff muted 50
developer Developer good 60

The role admin (label "Admin") has nothing to do with the admin level (10). A role is just a label, so a user whose role is "Admin" but whose level is Demo (1) can save nothing. Conversely, a level-10 admin has full administration even with an empty role.

Teams — organizational units

Teams are freely CRUD-managed in the Teams tab. The four seed teams:

Name Color Description
Infra Team accent Server / network / storage operations
Security Team crit Detection · response · blocking
Dev Team good Service development · deployment
DBA Team info Database operations · tuning

Team edit fields:

Field Meaning
Name (name) Required. Team name
Color (color) Badge tone (accent/crit/good/info/muted …)
Lead (lead_user_id) One user (optional)
Description (description) Free text
Active (is_active) Inactive teams are hidden from the Owner picker

The Owner picker — users + teams combined

Assets (server instances, hardware, storage) have an "owner" field that may be either a person or a team. The rules for this combined picker live in App\Support\Owners (the SOT).

Storage shape = two columns

The asset table stores the owner in two columns.

Column Meaning
owner_id Integer PK of a user or a team
owner_type 'user' or 'team'

Front-end exchange shape = a single ref string

The browser (forms, filters) exchanges owner_type and owner_id as one string.

user:5     → user id 5
team:3     → team id 3

The bootstrap options (Owners::options()) return the two groups for optgroup rendering (20-second cache). Closed users (is_close=1) and inactive teams (is_active=0) are excluded.

{
  "users": [ { "ref": "user:5", "id": 5, "name": "Jane Doe" } ],
  "teams": [ { "ref": "team:3", "id": 3, "name": "Infra Team" } ]
}

Server-side split / rebuild

Helper Behavior
Owners::split($ref) "user:5" → ['user', 5]. Malformed → ['user', 0] (= unassigned)
Owners::ref($ownerId, $ownerType) (5, 'user') → "user:5". Empty ownerId → null
Owners::resolveName($ownerId, $ownerType) Display name for cards/lists (null if none, 20-second cache)
Owners::flush() Invalidate the cache after user/team changes

For example, the instance save controller does this:

// owner ref ("user:5"/"team:3") → owner_id + owner_type
[$ownerType, $ownerId] = Owners::split($request->input('owner_ref'));
$payload['owner_id']   = $ownerId ?: null;
$payload['owner_type'] = $ownerId ? $ownerType : 'user';   // default 'user' when unset

The same ref is used when filtering by owner — the list query splits the owner parameter (user:5/team:3) with split() and narrows to WHERE owner_id = ? AND owner_type = ?.

The Owner picker is reused identically on the Instances, Hardware and Storage & Files asset screens. Assigning a team as owner puts the whole team on the responsibility line (robust to any one person leaving).

Avatar · initials · color

The avatar shown next to a label is generated from the name by RolePolicy.

Data sources

Table Purpose
infr_role Role master (6 informational labels) — key·name·color·seq·is_active
infr_team Team master — name·color·lead_user_id·description·is_active·seq
users Users — user_level (decides access), role_id (nullable), team_id (nullable)
asset tables Owner stored as owner_id + owner_type (user/team) two columns (Instances/Hardware/Storage etc.)

SOT helpers:

Access

Safeguards (last-admin protection)

Situation Result
Demote / deactivate / delete the last active admin Rejected — "The last active admin cannot be demoted, deactivated, or deleted."
Lower or deactivate your own admin level Rejected — "You cannot lower or deactivate your own admin privileges."
Delete yourself Rejected — "You cannot delete yourself."
Login ID already in use Rejected — "That login ID is already in use."

Troubleshooting & FAQ

Q. I gave someone the "Security" role but the security screens won't open. Roles are labels; they open no permissions. The security screens (mostly read) need a level — raise them to Staff (5) or higher. See RBAC levels.

Q. Some user/team is missing from the owner dropdown. Closed users (is_close=1) and inactive teams (is_active=0) are excluded from the picker. Re-activate the user/team, or if the list is stale wait for the 20-second cache to expire (Owners::flush() invalidates it immediately).

Q. If I delete a team, do its people disappear? No. The users remain; only their team_id becomes null (unassigned). An asset's owner_type='team' reference is separate, so the asset is not deleted either (its owner display just becomes empty).

Q. I saved an owner but no name shows. resolveName() is a 20-second cache. If you just renamed something it refreshes shortly. Also confirm the ref format is user:5/team:3 — a broken format is treated by split() as unassigned (user:0).

Q. Can a user have two roles? No. role_id is a single (nullable) value. If you need organizational grouping too, use a team (also a single value).

Related

Users and teams
Users and teams