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.
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.
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}'
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.
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.
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?
Not necessarily. That is the predicted inherited fallback from the parent Reporter membership. Full offboarding requires removing that source too.
The direct project endpoint has no Alex row. Can you claim Alex has no project access?
No. Check the effective endpoint and UI source because access can be inherited or shared.
Why does the mandatory checkpoint use a fixture for the second identity?
To keep the course single-account and free-compatible without asking learners to invite real people or create unnecessary identities.
An Ultimate-only custom role would fit the scenario perfectly. Must a Free learner upgrade?
No. Use built-in roles and the policy/decision exercise. Paid capabilities are optional learning extensions.
What should an offboarding record say about SSO/SCIM when the lab never configured them?
Mark them not applicable to the lab scope. Do not claim global absence or successful deprovisioning without evidence.
After removing all memberships, a release bot still has a project token. Is human offboarding complete?
The human membership path may be removed, but residual automation ownership/credential risk remains. Inventory and reassign/revoke it according to policy.
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
- GitLab Docs — Roles and permissions
- GitLab Docs — Subgroups
- GitLab Docs — Group members API
- GitLab Docs — Project members API
- GitLab Docs — Project members
- GitLab Docs — Sharing projects and groups
- GitLab Docs — Custom roles
- GitLab Docs — Group access and permissions
- GitLab Docs — GitLab.com SAML SSO
- GitLab Docs — GitLab.com SCIM
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.