Chapter 04Lesson 05~205 minutes

Checkpoint Lab — Groups, Subgroups, Namespaces, Membership, Roles, Custom Roles, and Access Governance

Model a small disposable GitLab organization, predict and prove effective access, demonstrate direct versus inherited membership, simulate offboarding, produce an access matrix, and clean up without requiring paid features.

CheckpointAccess matrixInheritance proofOffboardingCleanupProduction model

Learning objectives

  • Build a disposable top-level group, subgroup, and project and document the namespace/ownership boundary before assigning access.
  • Predict at least two effective-role transitions before performing or simulating them, then verify the predictions independently.
  • Prove direct versus inherited access using both GitLab UI evidence and Members REST API semantics.
  • Produce an access matrix and offboarding evidence packet that includes residual group/share/non-human paths rather than only direct memberships.
  • Clean up temporary membership changes and bridge the resulting governance model into Chapter 05 work-planning permissions.
Availability baseline (verified 2026-08-21). Groups, subgroups, built-in roles, direct/inherited membership, membership expiration, group/project sharing, and the group/project Members REST APIs are documented across Free, Premium, and Ultimate on GitLab.com, Self-Managed, and Dedicated. The Planner role was introduced in GitLab 17.7. Security Manager is documented as Beta, introduced in GitLab 18.11 and enabled on GitLab.com; treat its Self-Managed status/version carefully. Custom roles are Ultimate. GitLab.com group SAML SSO and SCIM are Premium/Ultimate; Self-Managed identity-provider capabilities have different configuration and tier boundaries. Exact UI labels, seat rules, role-promotion behavior, and administrator restrictions can vary by version and instance policy.

1. Checkpoint mission and preflight

You are modeling a tiny software organization. The hierarchy is ch04-governance-lab → payments → ledger-api. Your own account owns the disposable top-level group. A synthetic engineer alex-test should be Reporter at the parent and Developer only in payments. The exercise must prove how access flows before and after one membership is removed.

  • Mandatory path: one GitLab Free account plus a deterministic local membership fixture.
  • Optional live extension: a second GitLab Free account that you personally control; never a real coworker/customer.
  • No paid tier, custom role, SSO/SCIM, runner, pipeline compute, registry, cloud, Kubernetes, or AI credit is required.
  • Do not delete/transfer production groups, widen visibility, create real tokens, or modify protected resources.

2. Write predictions before any membership mutation

Record these expected transitions first. The point is to test your mental model, not to discover it after the API responds.

Prediction Before change After removing subgroup direct Developer
Top-group direct role for Alex Reporter (20) Reporter (20) remains.
Payments subgroup effective role Developer (30) Falls back to inherited Reporter (20).
Ledger project effective role Developer (30) Falls back to inherited Reporter (20).
Direct project membership None Still none.
Access after removing parent Reporter too Not applicable yet No membership path remains, assuming no share/service/token path exists.

3. Create or reuse the real disposable namespace hierarchy

If you kept Lesson 2’s disposable group, reuse it and map the IDs. Otherwise create a new hierarchy. Inspect the host first.

glab auth status
ME_ID="$(glab api user --jq .id)"
STAMP="$(date +%Y%m%d%H%M%S)"
TOP_PATH="ch04-governance-lab-$STAMP"

TOP_ID="$(glab api groups -X POST   -f "name=$TOP_PATH" -f "path=$TOP_PATH" -f visibility=private --jq .id)"
SUB_ID="$(glab api groups -X POST   -f name=payments -f path=payments -f "parent_id=$TOP_ID" -f visibility=private --jq .id)"
PROJECT_ID="$(glab api projects -X POST   -f name=ledger-api -f path=ledger-api -f "namespace_id=$SUB_ID" -f visibility=private --jq .id)"

glab api "groups/$TOP_ID" --jq '{id,full_path,parent_id,visibility}'
glab api "groups/$SUB_ID" --jq '{id,full_path,parent_id,visibility}'
glab api "projects/$PROJECT_ID" --jq '{id,path_with_namespace,namespace,visibility}' 
If top-level group creation is blocked: use an existing disposable group you own. Record the policy limitation and continue. Do not bypass administrator restrictions or repurpose a valuable namespace.

4. First independent proof: your own inherited Owner access

Before adding any synthetic identity, prove that your own membership demonstrates inheritance.

# Direct at top group.
glab api "groups/$TOP_ID/members/$ME_ID" --jq '{username,access_level,expires_at}'

# Usually not direct at subgroup/project, but effective through inheritance.
set +e
glab api "groups/$SUB_ID/members/$ME_ID"
SUB_DIRECT_RC=$?
glab api "projects/$PROJECT_ID/members/$ME_ID"
PROJ_DIRECT_RC=$?
set -e
printf 'sub_direct=%s project_direct=%s
' "$SUB_DIRECT_RC" "$PROJ_DIRECT_RC"

glab api "groups/$SUB_ID/members/all/$ME_ID" --jq '{username,access_level,expires_at}'
glab api "projects/$PROJECT_ID/members/all/$ME_ID" --jq '{username,access_level,expires_at}' 

Now confirm the same fact in Manage → Members on the subgroup/project. This gives the lab two independent evidence surfaces: the UI Source/inheritance view and the direct-versus-effective REST distinction.

5. Mandatory single-account access matrix and role-transition simulation

Create the synthetic Alex paths. This fixture is intentionally explicit about source scope so the offboarding algorithm cannot hide access provenance.

cat > /tmp/ch04-checkpoint.json <<'JSON'
{
  "user": "alex-test",
  "memberships": [
    {"source":"direct_top_group", "scope":"ch04-governance-lab", "role":"Reporter", "access_level":20, "expires_at":"2099-12-31"},
    {"source":"direct_subgroup", "scope":"ch04-governance-lab/payments", "role":"Developer", "access_level":30, "expires_at":null}
  ],
  "shares": [],
  "service_identities": [],
  "tokens": []
}
JSON

python - <<'PY'
import json
p=json.load(open('/tmp/ch04-checkpoint.json'))
roles={m['source']:m for m in p['memberships']}
effective=max(p['memberships'], key=lambda m:m['access_level'])
print('BEFORE effective payments/ledger=', effective['role'])

# Simulate removing only the direct subgroup membership.
remaining=[m for m in p['memberships'] if m['source']!='direct_subgroup']
effective2=max(remaining, key=lambda m:m['access_level'])
print('AFTER subgroup removal effective payments/ledger=', effective2['role'])

# Simulate full human membership offboarding.
remaining=[]
print('AFTER parent removal memberships=', remaining)
print('residual shares=', p['shares'])
print('residual service identities=', p['service_identities'])
print('residual tokens=', p['tokens'])
PY

Compare the output to your written prediction. The important state transition is Developer → Reporter after removing the subgroup path. “Removed one membership” is not equivalent to “removed access.”

6. Optional live role assignment and inheritance proof

If and only if you control a second disposable account, set its numeric ID. Add Reporter at the parent and Developer at the subgroup. Use a near-term expiration appropriate to your lab rather than the long synthetic date shown here.

Preflight: print and visually verify TOP_ID, SUB_ID, PROJECT_ID, and TEST_USER_ID before mutation. The user must be a disposable account you control.
TEST_USER_ID="246810"   # disposable second account only
EXPIRES_AT="2099-12-31"

printf 'top=%s sub=%s project=%s test_user=%s
'   "$TOP_ID" "$SUB_ID" "$PROJECT_ID" "$TEST_USER_ID"

# Assign least-privilege paths for the scenario.
glab api "groups/$TOP_ID/members" -X POST   -f "user_id=$TEST_USER_ID" -f access_level=20 -f "expires_at=$EXPIRES_AT"
glab api "groups/$SUB_ID/members" -X POST   -f "user_id=$TEST_USER_ID" -f access_level=30

# Direct proof at each source.
glab api "groups/$TOP_ID/members/$TEST_USER_ID" --jq '{username,access_level,expires_at}'
glab api "groups/$SUB_ID/members/$TEST_USER_ID" --jq '{username,access_level,expires_at}'

# Effective proof at the project; there is no direct project membership.
set +e
glab api "projects/$PROJECT_ID/members/$TEST_USER_ID"
DIRECT_PROJECT_RC=$?
set -e
printf 'direct_project_lookup_exit=%s
' "$DIRECT_PROJECT_RC"
glab api "projects/$PROJECT_ID/members/all/$TEST_USER_ID"   --jq '{username,access_level,expires_at}' 

Expected: direct project lookup is absent while effective project access is Developer from the subgroup path. Verify the same Source information in the UI.

7. Required transition: remove one path and prove fallback

Predict again before running the optional live mutation: removing the subgroup direct Developer should expose the inherited parent Reporter, not remove all access.

# OPTIONAL live extension.
glab api "groups/$SUB_ID/members/$TEST_USER_ID" -X DELETE

glab api "projects/$PROJECT_ID/members/all/$TEST_USER_ID"   --jq '{username,access_level,expires_at}'
# Expected effective role: Reporter (20) from the parent group.

If the result remains higher than expected, stop. Do not repeatedly delete rows. Search for another ancestor or share path. That discrepancy is diagnostic evidence.

8. Offboarding simulation: inventory residual access before declaring success

Build the final access matrix. A production offboarding record should distinguish “no direct project row” from “no effective access.”

Surface Expected before full offboarding Proof
Top group membership Reporter direct for Alex (fixture/optional live). /groups/:top/members/:user
Payments subgroup membership After transition: no direct Alex row. Direct subgroup lookup absent.
Ledger project membership No direct Alex row; effective Reporter until parent removed. Compare /members vs /members/all.
Shared groups None for mandatory lab. Inspect Groups/share settings; document if any exist.
Human credentials No lab token/key created. Chapter 02 credential inventory as applicable.
Service identities/tokens None for mandatory lab. Explicit fixture/inventory; do not assume membership deletion covers them.
SSO/SCIM/LDAP Not used in free lab. Mark Not Applicable, not “verified absent everywhere.”

9. Complete membership cleanup and verify zero effective path

In the optional live extension, remove the remaining parent Reporter membership. Then query the effective project endpoint. An authorization/not-found response for that user in this context is evidence that the membership path is gone; interpret the exact response according to your caller’s permissions.

if [ -n "${TEST_USER_ID:-}" ]; then
  glab api "groups/$TOP_ID/members/$TEST_USER_ID" -X DELETE || true

  set +e
  glab api "projects/$PROJECT_ID/members/all/$TEST_USER_ID"
  POST_OFFBOARD_RC=$?
  set -e
  printf 'post_offboarding_effective_lookup_exit=%s
' "$POST_OFFBOARD_RC"
fi

# Local fixture cleanup.
rm -f /tmp/ch04-checkpoint.json

Verify in the UI too. If access remains, inspect shares/ancestor paths rather than broadening or deleting project policy. No credential was created by the mandatory lab, so there is no hidden secret revocation step.

10. Resource cleanup / rollback

Keep the disposable hierarchy if you want to reuse it for later chapters. Otherwise, first verify all contained projects and members are synthetic. Prefer the GitLab UI for deleting the top-level training group because it displays the exact full path and the instance’s current delayed-deletion/restoration behavior.

Group deletion is destructive. Do not automate it with a copy-pasted ID in this checkpoint. Confirm the full path, export any evidence you want to retain, verify there is no valuable data, then follow the current UI deletion flow only for the disposable training namespace.

11. Checkpoint evidence packet

Save a compact, privacy-conscious handoff. Do not store email addresses, tokens, cookies, SSH private keys, or full enterprise identity attributes.

mkdir -p ch04-evidence

glab api "groups/$TOP_ID" --jq '{id,full_path,parent_id,visibility}'   > ch04-evidence/top-group.json
glab api "groups/$SUB_ID" --jq '{id,full_path,parent_id,visibility}'   > ch04-evidence/subgroup.json
glab api "projects/$PROJECT_ID" --jq '{id,path_with_namespace,visibility}'   > ch04-evidence/project.json

# For your own lab identity, retain only fields needed to prove inheritance.
glab api "projects/$PROJECT_ID/members/all/$ME_ID"   --jq '{id,username,access_level,expires_at}'   > ch04-evidence/self-effective-membership.json

python - <<'PY'
from pathlib import Path
import hashlib, json
root=Path('ch04-evidence')
manifest=[]
for f in sorted(root.glob('*.json')):
    manifest.append({'file':f.name,'sha256':hashlib.sha256(f.read_bytes()).hexdigest()})
(root/'manifest.json').write_text(json.dumps(manifest,indent=2)+'
')
PY
cat ch04-evidence/manifest.json

The hashes prove local evidence files did not change after capture; they do not prove GitLab state remained unchanged later. State evidence always has a capture-time scope.

Knowledge check

After deleting Alex’s subgroup Developer membership, the project still returns Reporter. Is cleanup incomplete?

The direct project endpoint has no Alex row. Can you claim Alex has no project access?

Why does the mandatory checkpoint use a fixture for the second identity?

An Ultimate-only custom role would fit the scenario perfectly. Must a Free learner upgrade?

What should an offboarding record say about SSO/SCIM when the lab never configured them?

After removing all memberships, a release bot still has a project token. Is human offboarding complete?

Checkpoint summary

You built an explainable authorization graph: top-level group → subgroup → project, with direct versus inherited evidence, predicted role transitions, a deliberate fallback after one membership removal, and a complete offboarding model that includes shares and non-human paths. This is the access foundation on which work planning, merge requests, approvals, CI/CD, registries, environments, and security controls will rely.

Official references

Chapter 05

Issues, labels, milestones, iterations, boards, epics, and work planning

With namespace ownership and effective roles understood, the next chapter uses those authorization boundaries to structure work intake, planning metadata, and delivery coordination without granting broader access than the workflow needs.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.