Chapter 04Lesson 02~185 minutes

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.

Disposable labGroups APIMembers APIEffective accessExpirationVerification

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.
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. 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, glab authenticated 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.

Security-sensitive membership change. Confirm the target group IDs and test-user identity before every POST/PUT/DELETE. Do not run this against an employer group or a real colleague.
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?

You lower a subgroup direct membership from Developer to Reporter, but the project still shows Developer. What should you inspect next?

Should you invite a coworker merely to complete this lab?

Why capture numeric group/project IDs after creation?

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

Next lesson

Design the hierarchy before it designs your blast radius

Lesson 3 compares flat versus nested groups, direct versus inherited membership, built-in versus custom roles, sharing, delegation, and identity governance through explicit tradeoffs.

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.