Draft: Compute effective permissions from role grants and evaluate

Related to gitlab-org/gitlab#600048

Draft / stacked on !20 (dr/policy-granted-permissions). Targets that branch, so the diff here is just the two commits on top of it — review/merge !20 first. The organization-administrator foundation + the engine compute are this MR.

What

Implements GLAZ's effective-permissions check for the Artifact Registry interim (proposal 014): given the role grants a subject holds, the action, and the resource, decide allow/deny. GLAZ stays stateless about assignments — the host (AR) pre-assembles the inputs (it fetches the subject's grants from the IAM Relationships API, filtered to the resource and its ancestors) and passes them in the Check request's relationships.

The algorithm (glaz-engine::Engine::check)

check(subject, action, object, role_ids, context):
  1. action ∉ catalogue (union of role permissions)  → DENY  "not a valid action"
  2. role_ids contains the organization-administrator role id          → ALLOW "organization administrator"
  3. resolve role_ids → union of permissions          (unknown role id → error)
  4. policy.check(... granted ...)                     → ALLOW / DENY  (Cedar permit_role_granted)
  • Validity gate first so even an organization administrator can't perform a bogus/typo action.
  • Org-owner bypass is a fixed allow, not a permission set (see below).
  • Membership runs through Cedar (permit_role_granted), evaluated schema-less for now.

Organization administrator = identity, not a role

glaz-roles/roles/organization_admin.yml is an identity-only role: a stable UUID with empty permissions. An organization administrator's implicit Artifact Admin access is conferred by the bypass in step 2 (keyed on the role id), not by a permission list — so it cannot be reduced or removed by editing role data. It lives outside the AR catalogue, so it never enters the permission union. AR sends it as an ordinary grant in relationships (on the org); since the engine unions all grants regardless of object, an org-level grant counts for a resource check.

Catalogue / valid actions

glaz_roles::defined_permissions() = the union of all default-role permissions — the set of valid actions for the validity gate. There's no standalone permission catalogue yet, so the roles are the catalogue. A test asserts Artifact Admin == the union: organization administrators are effectively Admin, so if Admin ever lacks a permission some role grants, organization administrators would silently lose that action — the invariant guards against that.

Schema-less Cedar now; schema later (host-provided)

The MVP is pure membership (no resource conditions), so glaz-policy is made schema-optional and runs schema-less: load_policies validates against a schema only if one is present, and check evaluates with None schema + Entities::empty() otherwise. load_schema is kept for when AR ships its own Cedar policies + schema (e.g. forbid on a quarantined repository) — at which point GLAZ loads and validates them. The schema is AR's to provide, not GLAZ's to generate.

This also replaces the earlier behavior where an unrecognized action returned an Err (the GitLab Duo note on !20): the Rust validity gate now denies an invalid action gracefully before Cedar.

Changes

  • glaz-roles: organization_admin.yml (identity-only) + organization_admin_role_id(); defined_permissions() catalogue accessor; Admin == union + organization-administrator-identity tests.
  • glaz-engine: Engine<P: PolicyEngine> holding a Resolver + a PolicyEngine; new() loads the embedded roles + policies (schema-less); the check algorithm above; EngineError::UnknownRole. Dropped the S: Storage generic and the graph Builder for now (re-added when graph traversal lands).
  • glaz-policy: schema optional (schema-less default); removed the placeholder schema + forbid_archived_project demo; load_policies no longer requires a schema; check handles both modes.
  • glaz-module: extracts role.id from the request's relationships and calls the engine (no more placeholder granted).

Tests

cargo test --locked --workspace — all pass. Engine: invalid-action deny, organization-administrator allow, role-granted allow, valid-but-not-granted deny, unknown-role error. Module: same via the proto boundary. Policy: schema-less membership + that a present schema still validates.

Notes / follow-ups

  • Relaxes the explicit schema-init work recently merged on main (schema requiredoptional) — a relax, not a revert (load_schema stays). Heads-up to @bmarjanovic warranted before review.
  • Role-id UUIDs are v4; relationships.proto requires v7 — separate follow-up to migrate (the ids are owned by glaz-roles).
  • glaz-graph is now referenced by no crate (only glaz-engine used it) — left as a workspace member for the future graph design.
  • Minor: defined_permissions() rebuilds its set per check — worth a LazyLock cache on the hot path.

Naming: follows the org-role ADR (gitlab-com/content-sites/handbook!20059 (merged)) — the org access level "Organization Owner" is now "Organization Administrator" (file/identifier organization_admin, mirroring artifact_admin).

🤖 Generated with Claude Code

Edited by Diane Russel

Merge request reports

Loading