Groups, Subgroups, Namespaces, Membership, Roles, Custom Roles, and Access Governance: Guided Hands-On Workflow and Core Operations
Create a disposable group, subgroup, and project; inspect direct versus inherited access; verify effective roles through UI and API evidence; and safely exercise one membership change or deterministic fixture.
Learning objectives
- Create or reuse a disposable GitLab Free group, subgroup, and project without involving production identities or paid capabilities.
- Inspect namespace hierarchy, project creation settings, direct membership, inherited membership, and effective roles before changing anything.
- Use the Groups and Members REST APIs through glab to prove the difference between direct and effective access.
- Change one role/expiration safely with a test identity you control, or use a deterministic local fixture when no second identity is available.
- Verify every change from at least two surfaces and clean up the membership mutation without exposing credentials.
1. Scenario, preflight, and safety boundary
You are building a disposable namespace called
ch04-access-lab-... with one subgroup
service-a and one project api. Your own
account is the training administrator/Owner. The mandatory path
proves your own inherited access and uses a local fixture for a
second identity, so a second account is
not required. If you already control a separate
GitLab test account, an optional live extension demonstrates role
and expiration changes.
-
Required: GitLab Free,
glabauthenticated to the intended host, and permission to create a disposable group or access to an existing disposable group you own. - Never invite a coworker, customer, or employer account for this exercise.
- No PAT creation, runner, pipeline, registry, cloud, Kubernetes, or paid feature is required.
- If your GitLab.com account or Self-Managed policy blocks top-level-group creation, use an existing disposable group you own and record that instance-policy constraint.
2. Inspect authentication and host before creating namespaces
Authentication success does not prove you are targeting the expected GitLab host. Reuse Chapter 02’s safe inspection pattern:
glab auth status
glab api user --jq '{id,username,name,state}'
# Record the host displayed by glab auth status. Do not continue if it is not the lab host.
3. Create the disposable hierarchy and capture stable IDs
The API path makes state transitions easy to observe. The top-level group call may be blocked by account or instance policy; if so, create the top-level group in the UI or substitute an existing disposable group, then continue with its numeric ID.
STAMP="$(date +%Y%m%d%H%M%S)"
TOP_PATH="ch04-access-lab-$STAMP"
# Create a PRIVATE disposable top-level group when your account/instance permits it.
TOP_ID="$(glab api groups -X POST -f "name=$TOP_PATH" -f "path=$TOP_PATH" -f visibility=private --jq .id)"
# Create a subgroup under it.
SUB_ID="$(glab api groups -X POST -f name=service-a -f path=service-a -f "parent_id=$TOP_ID" -f visibility=private --jq .id)"
# Create a project owned by the subgroup.
PROJECT_ID="$(glab api projects -X POST -f name=api -f path=api -f "namespace_id=$SUB_ID" -f visibility=private --jq .id)"
printf 'top=%s subgroup=%s project=%s
' "$TOP_ID" "$SUB_ID" "$PROJECT_ID"
glab api "groups/$TOP_ID" --jq '{id,full_path,parent_id,visibility,project_creation_level,subgroup_creation_level}'
glab api "groups/$SUB_ID" --jq '{id,full_path,parent_id,visibility,project_creation_level,subgroup_creation_level}'
glab api "projects/$PROJECT_ID" --jq '{id,path_with_namespace,namespace,visibility}'
If an optional field is absent on your version, do not treat that as a security failure. The stable evidence here is ID/path/parent/visibility. For creation permissions and other policy fields, use the current UI plus the documented API shape for your GitLab version.
4. Prove your own inherited access without changing it
Your account should be a direct member/Owner of the top-level group you created. It should then appear as effective access in the subgroup and project even though you did not add yourself directly there.
ME_ID="$(glab api user --jq .id)"
printf '%s
' '--- top direct ---'
glab api "groups/$TOP_ID/members/$ME_ID" --jq '{id,username,access_level,expires_at}'
printf '%s
' '--- subgroup direct (404 is expected if access is only inherited) ---'
set +e
glab api "groups/$SUB_ID/members/$ME_ID"
DIRECT_RC=$?
set -e
printf 'subgroup_direct_exit=%s
' "$DIRECT_RC"
printf '%s
' '--- subgroup effective ---'
glab api "groups/$SUB_ID/members/all/$ME_ID" --jq '{id,username,access_level,expires_at}'
printf '%s
' '--- project effective ---'
glab api "projects/$PROJECT_ID/members/all/$ME_ID" --jq '{id,username,access_level,expires_at}'
This is a useful intentionally asymmetric result: a direct-membership lookup can fail while the effective-membership lookup succeeds. The cause is inheritance, not stale caching and not an authentication bug.
5. Confirm the same source in the web UI
Open the subgroup and project and select Manage → Members. Find your user and inspect the Source/Inherited indication. The UI answers the human question “where did this role come from?” while the REST endpoints prove the difference in machine-readable membership scope.
Also inspect the top-level group’s Settings → General → Permissions and group features. Note who may create subgroups/projects on this namespace. Do not change the settings simply to make the lab work; the current policy is itself evidence.
6. Mandatory single-account path: model role changes with a deterministic fixture
If you do not own a second GitLab test account, do not invite a real
person. Use this fixture to reason about the same hierarchy. It
intentionally gives alex-test Reporter at the parent
and Developer directly on the subgroup.
cat > /tmp/ch04-membership.json <<'JSON'
{
"user": "alex-test",
"paths": [
{"source": "top-group", "scope": "platform", "role": "Reporter", "access_level": 20, "expires_at": "2099-12-31"},
{"source": "direct-subgroup", "scope": "platform/service-a", "role": "Developer", "access_level": 30, "expires_at": null}
]
}
JSON
python - <<'PY'
import json
p=json.load(open('/tmp/ch04-membership.json'))
for row in p['paths']:
print(f"{row['source']}: {row['role']} ({row['access_level']})")
effective=max(p['paths'], key=lambda x:x['access_level'])
print('effective_on_subgroup_and_project=', effective['role'])
PY
Prediction: removing only the direct subgroup Developer row lowers effective access to inherited Reporter; it does not remove access. That exact pattern is the core failure mode you will diagnose in Lessons 4 and 5.
7. Optional live extension: one test identity you control
Use this only if you own a separate disposable GitLab account.
Obtain its numeric user ID through the UI/API without sharing its
credentials. Set TEST_USER_ID locally. The example
first grants Reporter at the top group, then adds Developer at the
subgroup, then proves that the project effective role is the higher
Developer role.
TEST_USER_ID="246810" # replace only with the ID of a disposable account YOU control
EXPIRES_AT="2099-12-31" # lab-safe example; use a reasonable near-term date in real governance
# 20 = Reporter at the parent.
glab api "groups/$TOP_ID/members" -X POST -f "user_id=$TEST_USER_ID" -f access_level=20 -f "expires_at=$EXPIRES_AT"
# 30 = Developer directly at the subgroup.
glab api "groups/$SUB_ID/members" -X POST -f "user_id=$TEST_USER_ID" -f access_level=30
# Compare direct and effective evidence.
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}'
glab api "projects/$PROJECT_ID/members/all/$TEST_USER_ID" --jq '{username,access_level,expires_at}'
# Change only the subgroup direct membership back to Reporter.
glab api "groups/$SUB_ID/members/$TEST_USER_ID" -X PUT -f access_level=20
glab api "projects/$PROJECT_ID/members/all/$TEST_USER_ID" --jq '{username,access_level,expires_at}'
After the direct subgroup role is lowered to Reporter, the inherited parent Reporter still exists. If instead the parent were Developer and the subgroup direct role Reporter, effective access would stay Developer because the lower child role cannot subtract the higher ancestor access.
8. Challenge: choose the control, do not copy a sequence
A contractor needs read-oriented access to
service-a for 14 days, but must not see sibling
subgroup service-b. Where should you place membership?
Your task: choose the narrowest namespace, a built-in role, and an expiration strategy. Write the expected direct-membership endpoint and effective-membership endpoint that would prove the design. Do not add the user at the top-level group if the requirement is limited to one subgroup.
9. Cleanup and proof of rollback
If you used the optional test identity, remove the subgroup membership first, prove fallback behavior, then remove the parent membership and prove that the test identity no longer appears in the effective project view. Do not remove your own Owner membership from the lab group.
# OPTIONAL live-test cleanup only.
if [ -n "${TEST_USER_ID:-}" ]; then
glab api "groups/$SUB_ID/members/$TEST_USER_ID" -X DELETE || true
# At this point parent membership may still grant Reporter: verify it.
glab api "projects/$PROJECT_ID/members/all/$TEST_USER_ID" --jq '{username,access_level,expires_at}' || true
glab api "groups/$TOP_ID/members/$TEST_USER_ID" -X DELETE || true
set +e
glab api "projects/$PROJECT_ID/members/all/$TEST_USER_ID"
FINAL_RC=$?
set -e
printf 'post_offboarding_effective_lookup_exit=%s
' "$FINAL_RC"
fi
rm -f /tmp/ch04-membership.json
Keep the disposable namespace for Lesson 5 if desired. If you delete it later, use the GitLab UI, verify the exact full path, confirm no valuable projects exist, and understand the instance’s delayed-deletion/restoration policy before confirming. Deletion is not required by this lesson.
Knowledge check
Why does GET /groups/:subgroup/members/:user_id potentially return 404 while /members/all/:user_id succeeds?
The user may not be a direct subgroup member. The /all endpoint includes inherited/invited effective access.
You lower a subgroup direct membership from Developer to Reporter, but the project still shows Developer. What should you inspect next?
Ancestor memberships and shared-group paths. A higher access path elsewhere can continue to determine the effective role.
Should you invite a coworker merely to complete this lab?
No. Use an account you personally control or the deterministic fixture. Labs must not create real organizational access.
Why capture numeric group/project IDs after creation?
Paths can be ambiguous or change; stable IDs make API targeting and evidence clearer during the disposable workflow.
Summary
The workflow proved the core causality: membership attached at a parent can appear as effective access on a child without a direct child membership. A safe operator inspects direct and effective views separately, changes only the source that actually grants access, and verifies fallback or removal before declaring success.
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.