Chapter 04Lesson 04~175 minutes

Groups, Subgroups, Namespaces, Membership, Roles, Custom Roles, and Access Governance: Diagnostics, Failure Modes, Security, and Performance

Diagnose access that survives removal, higher inherited roles, over-broad group sharing, unavailable custom roles, and incomplete offboarding with an evidence-first, least-destructive workflow.

DiagnosticsInherited accessOffboardingSharingCustom rolesAuditability

Learning objectives

  • Use a repeatable evidence-first sequence to diagnose unexpected GitLab access without reflexively broadening roles or deleting resources.
  • Explain why removing direct membership can leave inherited access intact and why a higher ancestor role dominates a lower child assignment.
  • Detect group/project sharing as a hidden access path and distinguish tier/version absence from a permission error for custom roles.
  • Run a safe intentionally broken membership example and repair only the actual access source.
  • Build an offboarding checklist that inventories memberships, shares, service identities, credentials, identity-provider state, and audit evidence.
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. Diagnostic sequence: preserve → scope → enumerate → correct → verify

Unexpected access is dangerous precisely because the fastest “fixes” often make the graph harder to understand. Do not grant Owner/Maintainer to test whether permissions are the problem. Do not delete a group or transfer a project to shake loose stale access. Start with evidence.

Step Question Evidence
1. Preserve What failed or remained accessible? UI message, API status/body, timestamp, target path/ID, user ID; no secret values.
2. Scope Which host, top group, subgroup, project, and resource? glab auth status, group/project IDs and full paths.
3. Enumerate Which direct, inherited, shared, IdP, service/token paths exist? Members UI, /members, /members/all, share settings, token/service inventory.
4. Classify Is this role hierarchy, tier/offering/version, SSO policy, protected-resource policy, or credential state? Official docs + current settings + status code.
5. Correct What is the least destructive source-level change? Change/remove the actual parent/share/direct membership or policy.
6. Verify Did effective access change, and did unrelated access stay intact? Independent effective-membership/API/UI test.

2. Failure mode: “I removed the project member, but they still have access”

First inspect the direct project membership endpoint. Then inspect the effective endpoint. If the direct row is gone but /members/all still returns the user, that is evidence of another access path—not evidence that GitLab failed to delete the row.

PROJECT_ID="789012"
USER_ID="246810"

set +e
glab api "projects/$PROJECT_ID/members/$USER_ID"
DIRECT_RC=$?
set -e
printf 'direct_lookup_exit=%s
' "$DIRECT_RC"

glab api "projects/$PROJECT_ID/members/all/$USER_ID"   --jq '{id,username,access_level,expires_at}' 

Next walk upward through namespace ancestry and inspect group shares. Change the source membership that actually grants access. Do not add another lower project role in an attempt to “override” inheritance.

3. Failure mode: a parent Maintainer silently beats a child Developer

GitLab keeps permissions from the highest applicable role. This protects users from accidentally losing capabilities when nested teams add lower child memberships, but it surprises operators who expect child configuration to be subtractive.

Source Assigned role Effective child impact
Top group Maintainer (40) Maintainer capability reaches descendants.
Subgroup direct Developer (30) Does not reduce the inherited Maintainer.
Project direct Reporter (20) Still does not reduce the inherited Maintainer.
Correction Lower/remove parent role if scope is wrong Then re-evaluate needed narrower membership.

4. Failure mode: group sharing grants broader access than intended

A target project can be shared with another group. Removing a direct user row from the target does not remove membership in the invited group. Inspect the project’s Groups tab/share settings and the source group’s membership. The share’s maximum role caps access, but it is still an independent authorization edge.

For private groups, visibility of the invited group/member details depends on the requester’s own relationship and current GitLab behavior. Do not interpret an incomplete UI listing as proof that no share exists; use the documented project/group sharing APIs/settings available to your role.

5. Failure mode: “Custom roles are missing, so I need Owner”

If a Free or Premium namespace does not expose custom roles, the cause is entitlement, not insufficient Owner role. Current custom roles are Ultimate. Escalating a user cannot create a tier-gated feature. On Self-Managed, version and license matter too.

The repair is a design decision: choose the nearest safe built-in role, redesign the scope so that role is sufficient, or—if the organization already has Ultimate and a stable use case—go through the custom-role governance process. Never purchase or upgrade merely to complete this course lab.

6. Intentionally broken example: direct removal with inherited fallback

The following local fixture reproduces the logic without touching a real user. The broken offboarding function removes only direct project membership and incorrectly reports success.

cat > /tmp/ch04-offboarding.json <<'JSON'
{
  "user": "alex-test",
  "memberships": [
    {"kind": "ancestor_group", "scope": "platform", "access_level": 30},
    {"kind": "direct_project", "scope": "platform/service-a/api", "access_level": 20},
    {"kind": "shared_group", "scope": "platform/service-a/api", "access_level": 10}
  ],
  "credentials": ["project-bot-token-fixture"],
  "service_accounts": ["release-bot-fixture"]
}
JSON

python - <<'PY'
import json
p=json.load(open('/tmp/ch04-offboarding.json'))
# BROKEN logic: remove only direct_project.
remaining=[m for m in p['memberships'] if m['kind'] != 'direct_project']
print('remaining_membership_paths=', remaining)
print('effective_access_level=', max(m['access_level'] for m in remaining))
print('residual_credentials=', p['credentials'])
print('residual_service_accounts=', p['service_accounts'])
assert remaining, 'This assertion intentionally shows access remains.'
PY

Interpretation: effective access remains Developer-level (30) from the ancestor, plus another shared path and non-human residuals. The fix is not “run delete again.” The fix is to inventory each source and revoke/remove according to ownership and operational dependency.

7. Authentication succeeds but SSO/authorization still fails

A user can authenticate to GitLab yet lack the required group authorization. On GitLab.com SAML groups, SSO enforcement, identity linking, SCIM state, group membership, and protected-resource policy are distinct controls. On Self-Managed, instance SAML/LDAP configuration can add different failure points.

Evidence should answer: Did the user authenticate? Is the correct SAML identity linked? Is the user a member of the required top-level group? Did SCIM deactivate or re-add the identity? Does group sync map the IdP group to the intended GitLab role? Is the resource requiring SSO authorization? Avoid “resetting the password” when the failure is a group authorization path.

8. Production offboarding is a residual-access investigation

Do not define offboarding as “Remove user from project.” Use a structured inventory:

Access surface Question Typical evidence/action
Ancestor/direct memberships Which groups/projects grant access? Direct/effective Members API, UI Source column; remove at actual source.
Group/project shares Is access inherited from another group invitation? Groups tabs/share settings; remove share or source membership.
SSO/SCIM/LDAP Is the enterprise identity disabled and synchronization healthy? IdP + GitLab identity state; verify deprovision propagation.
Personal credentials Any PATs/SSH keys tied to the departing user? Chapter 02 credential inventory; revoke/disable according to policy.
Project/group/deploy tokens Did the human own or know automation credentials? Inventory token/service ownership; rotate/revoke when risk warrants.
Service accounts/bots Does automation remain intentionally owned? Reassign owner, scope, expiration, and audit contact.
Protected resources Did role removal actually block sensitive actions? Read-only permission tests; avoid destructive push/deploy tests.
Audit evidence Can you prove what changed and when? Audit events where available, API snapshots, ticket/runbook record.
Credential incident order: if offboarding discovers an exposed token or private key, revoke/rotate/disable the credential first. Deleting logs or rewriting Git history cannot invalidate a secret that somebody already copied.

9. Scale, rate limits, and API completeness

Large groups can have many members and nested projects. Members APIs are paginated. A script that checks only page 1 can falsely certify offboarding. Use --paginate with glab api or implement REST pagination explicitly. Cache or parallelize only after correctness; stale authorization evidence is worse than a slower complete inventory.

Do not dump full member records into public CI logs: names, email fields, SAML/SCIM identifiers, and security-sensitive metadata can be private. Select only the fields required for the audit and store evidence according to retention policy.

10. Repair pattern: remove one source, verify fallback, then continue

When multiple access paths are intentional, remove them one at a time and observe the effective role after each change. This makes causality visible and reduces the chance of deleting too much access. In an incident, you may need faster containment, but still preserve evidence and document the emergency path.

# Example evidence-only sequence; do not run DELETE until the target is verified.
glab api "projects/$PROJECT_ID/members/all/$USER_ID" --jq '{id,username,access_level,expires_at}'
# Inspect ancestor group direct membership and any share source here.
# Perform the narrow approved source-level change.
# Re-run effective membership lookup and compare the access level/status.

Knowledge check

A direct project lookup returns 404 but /members/all returns Developer. Is GitLab inconsistent?

Why is adding a Reporter project membership not a fix for an inherited Maintainer role?

A Free user cannot find custom roles. Should you grant Owner to reveal the setting?

What is the first correction when an offboarded user has a leaked PAT?

Why must an access-inventory script paginate?

Summary

Authorization failures are graph problems. Preserve evidence, scope the resource, enumerate direct/inherited/shared/non-human paths, distinguish entitlement from permission, correct the true source, and verify effective access independently. The right recovery is usually narrower than “grant Owner” and safer than deleting or transferring resources.

Official references

Next lesson

Prove access governance end to end

Lesson 5 builds the hierarchy, predicts role changes, proves inheritance from two surfaces, runs an offboarding simulation, and produces a reusable access matrix and cleanup record.

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.