Roles & permissions

Portrait gives you a mechanism, not a prescription. This page explains that mechanism and the current shape of the permission surface, so you can design a role structure for your own organization.

The model

Four objects:

  • Administrator - a person or service identity that logs in.
  • Role - a named bag of permission-key strings (e.g. package_profile.manage). A role grants nothing on its own; it matters only once assigned.
  • Access group - a node in your hierarchy of managed hosts (root, root.servers, root.servers.web). Every host belongs to exactly one.
  • Role assignment - the actual grant: this administrator holds this role at this access group. An administrator can hold many roles at many access groups at once. There is no single “the” role per person.

Access groups are hierarchical and grants flow downward only: a role assigned at root.servers also applies to root.servers.web, but not to root.workstations. There is no “deny” - only grant, and default-deny for anything not explicitly granted.

What is actually scoped today

The model supports granting a role at any access group and having it apply only to that subtree. In practice, as of this release, most permission checks are account-wide - holding a key anywhere in your account is enough to use it anywhere - because most resources (profiles, tags, alert rules, scripts-as-metadata, activities, roles) do not yet have meaningful per-subtree semantics.

The permissions that are genuinely access-group-scoped today - where where you hold the role changes what it applies to - are:

  • access_group.read (and its write-capable variant)
  • role_assignment.read / role_assignment.manage
  • script.read / script.write
  • script.execute_as_system - checked against two access groups at once: the script’s own group and the target host’s group
  • localaccount.read

If you are designing a role to restrict someone to a subset of your fleet, check this list first. Assigning a role at a narrower access group only narrows the permissions above; the rest of that role’s grants still apply account-wide.

Permission catalog

.read keys are non-mutating; .manage / .write / .approve / .cancel / .decommission keys mutate.

KeyGrants
access_group.readView the access-group hierarchy
agent.readView enrolled hosts and their inventory / state
agent.approveSensitive. Admit a pending host into a chosen access group
agent.decommissionRemove a host from management (reversible)
agent.deleteSensitive. Permanently remove a decommissioned host and its entire history - irreversible
activity.readView activities (installs, reboots, script runs…)
activity.createDispatch new activities
activity.cancelCancel a pending or in-progress activity
tag.read / tag.manageView / assign host tags
package_profile.read / .manageView / author WinGet package install-or-absent policy
upgrade_profile.read / .manageView / author Windows Update policy
reboot_profile.read / .manageView / author scheduled-reboot policy
removal_profile.read / .manageView / author software-removal policy
script_profile.read / .manageView / author scheduled script-run policy (also needs script.execute_as_system)
script.read / script.writeView / author script definitions (metadata only - does not authorise running one)
script.execute_as_systemSensitive. Actually authorises running a script as SYSTEM, ad hoc or via a profile
localaccount.readView local Windows accounts on a host
alert_rule.read / .manageView / author alert rules
alert_event.readView fired alerts
notification_sink.read / .manageView / configure where alerts are delivered
audit.readView the audit log
role.read / .manageView / author roles and their permission sets
role_assignment.read / .manageView / grant roles to administrators

A few keys exist in the catalog but are not yet checked by any route - bitlocker.read_key, laps.read_password, localuser.mutate, agent.update_channel, script.execute - reserved for capabilities still on the roadmap.

role_assignment.manage also gates seeing the list of administrators in your account and sending invites.

Sensitive permissions

script.execute_as_system, agent.approve, agent.delete, and the reserved credential-escrow keys are called out separately in the code because each is a materially larger blast radius than an ordinary .manage key - arbitrary code as SYSTEM, letting an unvetted host in, or permanently destroying history. Nothing stops you putting one in a broadly held role. The separation exists so you notice you are doing it.

The self-escalation guard

You can only grant a permission key to a role - whether by editing the role or by sending an invite - if you already hold that exact key yourself, on any access group. So:

  • An administrator without agent.delete cannot create a role carrying it and assign that role to themselves, even holding role.manage and role_assignment.manage generally.
  • Your most-privileged administrators need to hold every key any role in your account will ever grant, before they can grant it onward. There is no “grant on behalf of” override.

Revoking a key needs no such check.

One sharp edge. There is currently no protection against deleting the last role that grants role.manage. Doing so leaves the account with no administrator able to create or edit roles at all (existing assignments keep working). Treat whichever roles grant role.manage as something to be deliberate about deleting.

Worked examples

Illustrations of what the mechanism lets you compose - not prescriptions.

Full-privilege role

Every key in the catalog, assigned at root. This is what the /setup bootstrap owner role looks like. Reasonable for a small team where everyone with API access is trusted equally.

Read-only auditor

agent.read, activity.read, audit.read, alert_event.read, and the .read half of every profile type, at root. Sees everything, changes nothing.

Helpdesk operator, scoped to one subtree

agent.read, activity.read/.create, script.read, script.execute_as_system - assigned at root.workstations rather than root. Because script.execute_as_system is genuinely scoped, this operator can run scripts against workstations but gets a 403 against root.servers - even though their other grants apply account-wide. Note the two-access-group check: they also need visibility into whichever group the script definition itself lives in.

Patch-policy author with no execution rights

package_profile, upgrade_profile, removal_profile (read + manage) and agent.read. Can define what should happen but holds no script.execute_as_system, so cannot run anything ad hoc - the scheduler dispatches profile-driven activities on its own authority, not the author’s.

Bringing on administrators

There is no route that creates an administrator account directly. The only two ways in are the one-time /setup bootstrap (administrator zero) and an invite: POST /administrators/invites with an email and an existing role returns a one-time token you send however you like. An invite names an existing role - design the role first, then invite against it.