Skip to content

Restrict distribution & machine access

By default any authenticated user whose global role permits an action can reach every distribution and every inventory machine. This guide shows how to scope that down to specific roles or groups — for a distribution's package tree (distribution_access) and/or a set of inventory machines (machine_access). For how these two layers combine with the global role system, see The RBAC model; this page only covers the setup steps.


1. Prerequisites

  • You must be logged in as admin — both distribution_access and machine_access management endpoints are admin-only, and only admin bypasses the restrictions once created.
  • A custom role or group already created, to use as the principal_id. List existing ones with:
    curl -s http://repod.example.com:8000/api/v1/roles \
      -H "Authorization: Bearer repod_xxxxxxxxxxxxxxxx" | jq .
    
    curl -s http://repod.example.com:8000/api/v1/groups \
      -H "Authorization: Bearer repod_xxxxxxxxxxxxxxxx" | jq .
    
    Note the id of the role or group you want to grant access to — that value is the principal_id in every request below.

This is opt-in and irreversible-by-omission

A distribution or machine with zero access rules stays fully open. The moment you add the first rule for a given codename or machine/tag, it becomes restricted to only the principals listed in rules for it. Make sure admin accounts (which always bypass) or the intended team's role/group is covered before you add the first rule — there is no separate "default allow" entry to fall back on.


2. Restrict a distribution

2.1 Check the current state

curl -s http://repod.example.com:8000/api/v1/distributions/jammy/access \
  -H "Authorization: Bearer repod_xxxxxxxxxxxxxxxx" | jq .

An empty access array means the distribution is currently open to everyone.

2.2 Add an access rule

curl -s -X POST http://repod.example.com:8000/api/v1/distributions/jammy/access \
  -H "Authorization: Bearer repod_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "principal_type": "group",
    "principal_id": "grp_a1b2c3d4"
  }' | jq .
Field Values Meaning
principal_type role | group Whether principal_id refers to a custom role or a group
principal_id string custom_roles.id or groups.id — the id you looked up in step 1

Response (201):

{
  "access": {
    "id": "...",
    "codename": "jammy",
    "principal_type": "group",
    "principal_id": "grp_a1b2c3d4",
    "created_by": "admin",
    "created_at": "..."
  }
}

Worked example — give the security-team group write access to jammy only:

  1. Look up the group's id: GET /api/v1/groups → find security-team, note its id.
  2. Add one rule for jammy with that principal_id (as above). No other distribution is affected — distribution_access rules are per-codename, so noble and almalinux9 remain open until you add rules for them too.
  3. Any user not in security-team (and not admin) now gets 404 Not Found on GET /distributions/jammy/packages, POST /upload/ targeting jammy, and promote/migrate operations touching jammy as either source or destination.

Multiple rules for the same codename combine as a union — any one matching role or group grant is enough. There is no intersection mode.

2.3 Remove an access rule

curl -s -X DELETE http://repod.example.com:8000/api/v1/distributions/jammy/access/<entry_id> \
  -H "Authorization: Bearer repod_xxxxxxxxxxxxxxxx"

Returns 204 on success, 404 if the entry doesn't exist. Removing the last rule for a codename reopens it to everyone.


3. Restrict a machine or a tag of machines

Machine-level access uses a separate table with two independent axes: who gets access (user_principal_type/user_principal_id, same role/group vocabulary as distributions) and which machine(s) the rule applies to (machine_principal_type/machine_principal_id, either tag or client).

3.1 Check the current state

curl -s "http://repod.example.com:8000/api/v1/inventory/machine-access" \
  -H "Authorization: Bearer repod_xxxxxxxxxxxxxxxx" | jq .

Filter by machine principal with query params:

curl -s "http://repod.example.com:8000/api/v1/inventory/machine-access?machine_principal_type=tag&machine_principal_id=prod" \
  -H "Authorization: Bearer repod_xxxxxxxxxxxxxxxx" | jq .

3.2 Add a machine-access rule

curl -s -X POST http://repod.example.com:8000/api/v1/inventory/machine-access \
  -H "Authorization: Bearer repod_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "user_principal_type": "group",
    "user_principal_id": "grp_a1b2c3d4",
    "machine_principal_type": "tag",
    "machine_principal_id": "prod"
  }' | jq .
Field Values Meaning
user_principal_type role | group Who the rule grants access to
user_principal_id string custom_roles.id or groups.id
machine_principal_type tag | client Whether the rule targets every machine carrying a tag, or one specific machine
machine_principal_id string The tag name (e.g. prod), or the client's id

Worked example — give the ops-team group access to all machines tagged prod:

curl -s -X POST http://repod.example.com:8000/api/v1/inventory/machine-access \
  -H "Authorization: Bearer repod_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "user_principal_type": "group",
    "user_principal_id": "<ops-team group id>",
    "machine_principal_type": "tag",
    "machine_principal_id": "prod"
  }' | jq .

Once this rule exists, any machine tagged prod is restricted to ops-team members (plus admin). Machines without the prod tag remain open, unless they carry another tag that also has a rule.

Client override replaces tag rules, it never merges with them

If you add a machine_principal_type: "client" rule for one specific machine, that rule set becomes the entire effective rule set for that machine — its tag-derived rules are ignored completely, not combined with the client rule. Without a client-level override, all rules from every tag the machine carries combine as a union (any one matching grant is sufficient). See The RBAC model for the full reasoning behind this choice.

3.3 Remove a machine-access rule

curl -s -X DELETE http://repod.example.com:8000/api/v1/inventory/machine-access/<entry_id> \
  -H "Authorization: Bearer repod_xxxxxxxxxxxxxxxx"

Returns 204 on success, 404 if the entry doesn't exist.


4. Verify it worked

As a user who is not in the granted role/group and not admin:

# Distribution restriction — expect 404, not 403
curl -s -o /dev/null -w "%{http_code}\n" \
  http://repod.example.com:8000/api/v1/distributions/jammy/packages \
  -H "Authorization: Bearer <token of an unauthorized user>"
# → 404

# Machine restriction — expect the client filtered out of the list, not an error
curl -s http://repod.example.com:8000/api/v1/inventory/clients \
  -H "Authorization: Bearer <token of an unauthorized user>" | jq '.items[] | select(.id=="<restricted client id>")'
# → no output — the client is silently omitted, not returned with an error

As a user who is in the granted role/group:

curl -s -o /dev/null -w "%{http_code}\n" \
  http://repod.example.com:8000/api/v1/distributions/jammy/packages \
  -H "Authorization: Bearer <token of an authorized user>"
# → 200

A 404 on a restricted resource is deliberate and identical to the response for a resource that genuinely doesn't exist — this is the anti-leak design described in The RBAC model. Don't use a 404 here as a signal that you mistyped a codename or client id; check GET /distributions/{codename}/access or GET /inventory/machine-access first to confirm whether a restriction is actually in place.