Chapter 28Lesson 02~230 minutes

Organizations, Teams, Roles, Repository Access, Enterprise Policies, and Delegated Administration: Guided Hands-On Workflow and Core Operations

Model effective GitHub access safely with a reproducible local fixture, then map the same operations to an optional disposable GitHub organization with read-before-write verification.

Access modelTeamsOnboardingOffboardingVerification

Learning objectives

  • Build a synthetic access model without requiring a second GitHub account or paid plan.
  • Calculate effective access from base permission, direct grants, and inherited team grants.
  • Inspect organization/repository state with UI, gh, and REST before any live mutation.
  • Map team grant/revoke and onboarding/offboarding to current GitHub resources and roles.
  • Verify that removing one grant is not proof that all access is gone.

1. Safe scenario and availability

Organization access labs normally need multiple identities and administrative authority. The mandatory path therefore uses a local synthetic model named atlas-c28. It is free, reproducible, contains no personal data, and lets you prove access causality before touching GitHub. An optional live extension uses a disposable GitHub Free organization that you own; never experiment on an employer or valuable organization.

Roles: the fixture models organization members and team grants. Live creation of teams/member access requires organization-owner or delegated permissions; repository team-access changes require repository Admin or an authorized role. Enterprise-only custom roles/internal visibility are read-only policy exercises here.

2. Build the access fixture

Create a clean directory and save this model as access-model.json. Three repositories represent application source, security policy, and release automation. The Engineering parent team intentionally has Read on all three; child teams inherit it.

{
  "base_permission": "none",
  "repositories": ["app-api", "security-policy", "release-ops"],
  "teams": {
    "engineering": {"parent": null, "repos": {"app-api":"read", "security-policy":"read", "release-ops":"read"}},
    "app-engineering": {"parent":"engineering", "repos":{"app-api":"write"}},
    "security": {"parent":null, "repos":{"security-policy":"maintain", "app-api":"read"}},
    "release": {"parent":null, "repos":{"release-ops":"maintain", "app-api":"read"}}
  },
  "users": {
    "maya": {"active":true, "relationship":"member", "teams":["app-engineering"], "direct":{}},
    "sam": {"active":true, "relationship":"member", "teams":["security"], "direct":{}},
    "riley": {"active":true, "relationship":"member", "teams":["release"], "direct":{}}
  }
}

Predict Maya's access before running anything: Write on app-api, but also inherited Read on security-policy and release-ops because app-engineering is nested under engineering. That prediction is the first governance test.

#!/usr/bin/env python3
"""c28_access.py — local, synthetic GitHub access evaluator."""
import json, sys
from pathlib import Path
RANK = {"none":0,"read":1,"triage":2,"write":3,"maintain":4,"admin":5}

def max_role(*roles):
    return max(roles, key=lambda r: RANK[r])

def ancestors(team, teams):
    out=[]; seen=set(); cur=team
    while cur and cur not in seen:
        seen.add(cur); parent=teams.get(cur,{}).get("parent")
        if parent: out.append(parent)
        cur=parent
    return out

def effective(model, user, repo):
    u=model["users"][user]
    if not u.get("active", True): return {"role":"none","sources":["identity inactive"]}
    sources=[]; roles=[]
    if u["relationship"]=="member":
        roles.append(model["base_permission"]); sources.append(f"base:{model['base_permission']}")
    if repo in u.get("direct",{}):
        roles.append(u["direct"][repo]); sources.append(f"direct:{u['direct'][repo]}")
    for t in u.get("teams",[]):
        for current in [t]+ancestors(t, model["teams"]):
            grant=model["teams"].get(current,{}).get("repos",{}).get(repo)
            if grant: roles.append(grant); sources.append(f"team:{current}:{grant}")
    return {"role":max_role(*(roles or ["none"])),"sources":sources}

m=json.loads(Path(sys.argv[1]).read_text())
for user in m["users"]:
    for repo in m["repositories"]:
        print(user, repo, json.dumps(effective(m,user,repo)))

3. Inspect before change and explain every source

python c28_access.py access-model.json

Expected Maya entries include team:app-engineering:write for app-api and team:engineering:read for the other repositories. The output deliberately reports sources, not only the winning role. Production access reviews need provenance: “Write because team A” is actionable; “Write” alone is not.

4. Onboarding: grant function, not person-by-person privilege

Add synthetic user devon as a member of app-engineering with no direct grants. Predict that Devon receives the same team-derived access as Maya. This models a good onboarding property: access follows job function, and changing team policy changes everyone consistently.

import json
p="access-model.json"
m=json.load(open(p))
m["users"]["devon"]={"active":True,"relationship":"member","teams":["app-engineering"],"direct":{}}
json.dump(m,open(p,"w"),indent=2)
python c28_access.py access-model.json | grep '^devon '

Do not create a PAT/SSH key as part of onboarding merely because the person joined. Credentials are separate assets. If your organization uses SSO/SCIM/Enterprise Managed Users, provision the identity through the authoritative IdP and apply GitHub team/repository access after identity state is correct.

5. Temporary elevation: explicit, narrow, expiring

Suppose Devon must repair release metadata for one incident. Do not make Devon an organization owner. Model a temporary direct maintain grant on release-ops, with an external ticket/expiry record. Then remove it and verify the role falls back to inherited Read.

import json
p="access-model.json"; m=json.load(open(p))
m["users"]["devon"]["direct"]["release-ops"]="maintain"
json.dump(m,open(p,"w"),indent=2)

Direct temporary access is acceptable when the exception is visible and time bounded. In a production organization, prefer a dedicated elevation team or eligible delegated role if exceptions recur; repeated direct grants are evidence that the role model is incomplete.

6. Offboarding: prove absence across every path

First remove Devon from app-engineering but leave the temporary direct grant. Run the evaluator. app-api access disappears, but release-ops remains Maintain. This intentionally broken intermediate state shows why “removed from the team” is not “offboarded.” Next remove the direct grant and mark the identity inactive in the fixture. The evaluator must report none everywhere.

Production offboarding also performs an ownership handoff: identify GitHub Apps, deploy keys, Actions/environment secrets, release responsibilities, CODEOWNERS/review ownership, bots, or external systems for which the departing person was the sole operator. Reassign service ownership before disabling the identity, then revoke or rotate any person-bound tokens/keys according to policy. Do not print those credential values while inventorying them.

import json
p="access-model.json"; m=json.load(open(p))
u=m["users"]["devon"]
u["teams"]=[]
u["direct"]={}
u["active"]=False
json.dump(m,open(p,"w"),indent=2)
python c28_access.py access-model.json | grep '^devon '

7. Optional live GitHub Free organization extension

If you already control a disposable organization, inspect first. Substitute only synthetic/disposable names. Team membership and collaborator changes are security-sensitive because they change authorization.

Current UI mapping: organization → Teams shows team hierarchy/members; organization Settings → Member privileges exposes base permission; a repository Settings → Collaborators and teams / Access shows repository grants. UI labels can move, so verify the underlying organization/team/repository resource with the API commands below before changing it.

ORG="octo-c28-governance-lab"
REPO="app-api"
TEAM="app-engineering"

gh api -H "X-GitHub-Api-Version: 2026-03-10" "orgs/$ORG"   --jq '{login,default_repository_permission}'
gh api -H "X-GitHub-Api-Version: 2026-03-10" "orgs/$ORG/teams/$TEAM"   --jq '{name,slug,parent:(.parent.slug // null)}'
gh api -H "X-GitHub-Api-Version: 2026-03-10" "orgs/$ORG/teams/$TEAM/repos/$ORG/$REPO"   --jq '{name,permissions}'

To grant or change team repository access, GitHub's REST endpoint accepts pull, triage, push, maintain, admin, and eligible custom role names. Do not run this mutation until you have verified the exact organization/team/repository and your authorization:

# OPTIONAL SECURITY-SENSITIVE mutation in a disposable org only.
gh api --method PUT -H "X-GitHub-Api-Version: 2026-03-10"   "orgs/$ORG/teams/$TEAM/repos/$ORG/$REPO" -f permission=push
# Verify after change.
gh api -H "X-GitHub-Api-Version: 2026-03-10"   "orgs/$ORG/teams/$TEAM/repos/$ORG/$REPO" --jq '{name,permissions}'

Cleanup is a symmetric remove-and-verify operation, not organization deletion:

# OPTIONAL: remove the team's direct access from the disposable repository.
gh api --method DELETE -H "X-GitHub-Api-Version: 2026-03-10"   "orgs/$ORG/teams/$TEAM/repos/$ORG/$REPO"

8. Challenge: which surface owns the change?

A contractor needs Triage on one repository for four weeks. Choose among organization membership, outside-collaborator repository access, a team, organization owner, or enterprise role. Explain why. A defensible answer is narrow repository access as an outside collaborator when ordinary GitHub.com policy allows it, with expiry/review recorded externally. If the person will repeatedly work across many repositories, membership plus a purpose-built team may be more maintainable. In Enterprise Managed Users, translate this to the managed repository-collaborator/guest model rather than ordinary outside-collaborator semantics.

Knowledge check

Why does the simulator print access sources as well as the maximum role?

Devon is removed from app-engineering but still has Maintain on release-ops. What is the likely cause in the lab?

Why is organization deletion excluded from cleanup?

When can a live REST team-membership mutation fail even for an authenticated user?

What is the safest first step before a live grant?

Summary

The guided workflow turns access into inspectable state. Team inheritance and direct grants are evaluated separately, onboarding is function-based, temporary privilege is explicit and reversible, and offboarding is complete only when every path is gone. The optional GitHub organization extension maps the same model to current REST resources without making live organization administration mandatory.

Next lesson

Organizations, Teams, Roles, Repository Access, Enterprise Policies, and Delegated Administration: Configuration, Design Choices, and Tradeoffs

Official references

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
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.