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.
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.
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?
Because effective access is additive. A reviewer must know every grant path so that removing one source does not leave another hidden path behind.
Devon is removed from app-engineering but still has Maintain on release-ops. What is the likely cause in the lab?
The temporary direct repository grant remains. Team removal and direct-grant removal are separate authorization changes.
Why is organization deletion excluded from cleanup?
It is a destructive account-level action and is unnecessary to prove the lesson. Remove only the disposable grants/resources you created and verify their absence.
When can a live REST team-membership mutation fail even for an authenticated user?
The user may lack organization/team authority, the token may lack organization permissions, or an IdP-synchronized team may require membership to be changed in the identity provider instead.
What is the safest first step before a live grant?
Read and verify the exact organization, team, repository, current permission, and caller authority. Authentication alone is not proof that the intended mutation is authorized or correctly scoped.
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.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.