Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion gh-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1345,7 +1345,16 @@ joshjohanning,admin

### get-repository-users-permission-and-source.sh

Returns the permission for everyone who can access the repo and how they access it (direct, team, org)
Returns the permission for everyone who can access a repository and how they access it (direct, team, organization). Uses the REST API to get accurate team role names including maintain, triage, and custom roles.

Example output:

```text
USER EFFECTIVE SOURCES
joshjohanning ADMIN org-member(ADMIN), team:admin-team(WRITE), team:approver-team(WRITE)
FluffyCarlton MAINTAIN direct(MAINTAIN), org-member(READ)
joshgoldfishturtle ADMIN org-member(READ), team:compliance-team(ADMIN)
```

### get-repository.sh

Expand Down
98 changes: 92 additions & 6 deletions gh-cli/get-repository-users-permission-and-source.sh
Original file line number Diff line number Diff line change
@@ -1,13 +1,74 @@
#!/bin/bash

# gh cli's token needs to be able to admin org - run this first if it can't
# gh auth refresh -h github.com -s admin:org
# Returns the permission for everyone who can access a repository and how they
# access it (direct, team, organization)
#
# Uses the REST API to get accurate team role names (maintain, triage) since the
# GraphQL permissionSources API only returns READ, WRITE, and ADMIN. A heuristic
# is also applied to direct sources to correct MAINTAIN/TRIAGE labels.
#
# gh cli's token needs to be able to admin the organization - run this first if needed:
# gh auth refresh -h github.com -s admin:org
#
# Usage:
# ./get-repository-users-permission-and-source.sh <org> <repo> [affiliation] [hostname]
#
# affiliation can be: OUTSIDE, DIRECT, ALL (default: ALL)
# hostname: GitHub hostname (default: github.com), e.g. github.example.com

# affiliation can be: OUTSIDE, DIRECT, ALL
# Example output:
#
# USER EFFECTIVE SOURCES
# joshjohanning ADMIN org-member(ADMIN), team:admin-team(WRITE), team:approver-team(WRITE)
# FluffyCarlton MAINTAIN direct(MAINTAIN), org-member(READ)
# joshgoldfishturtle ADMIN org-member(READ), team:compliance-team(ADMIN)

# returns the permission for everyone who can access the repo and how they access it (direct, team, org)

gh api graphql --paginate -f owner='joshjohanning-org' -f repo='ghas-demo' -f affiliation='ALL' -f query='
if [ -z "$2" ]; then
echo "Usage: $0 <org> <repo> [affiliation] [hostname]"
echo " affiliation: OUTSIDE, DIRECT, ALL (default: ALL)"
echo " hostname: GitHub hostname (default: github.com)"
exit 1
fi

org="$1"
repo="$2"
affiliation_input="${3:-ALL}"
affiliation="$(echo "$affiliation_input" | tr '[:lower:]' '[:upper:]')"
hostname="${4:-github.com}"

case "$affiliation" in
OUTSIDE|DIRECT|ALL)
;;
*)
echo "Error: invalid affiliation '$affiliation_input'. Must be OUTSIDE, DIRECT, or ALL."
exit 1
;;
esac

# Map REST permission names (pull/push) to GraphQL-style names (READ/WRITE)
map_permission() {
case "$1" in
pull) echo "READ" ;;
triage) echo "TRIAGE" ;;
push) echo "WRITE" ;;
maintain) echo "MAINTAIN" ;;
admin) echo "ADMIN" ;;
*) echo "$1" | tr '[:lower:]' '[:upper:]' ;;
esac
}

# Get true team permissions via REST API and build a sed command to fix labels
sed_cmd=""
while IFS=$'\t' read -r slug perm; do
mapped=$(map_permission "$perm")
sed_cmd="${sed_cmd}s/team:${slug}\([^)]*\)/team:${slug}(${mapped})/g;"
done <<EOF
$(gh api --hostname "$hostname" --paginate "/repos/$org/$repo/teams?per_page=100" --jq '.[] | [.slug, .permission] | @tsv')
EOF

# Get source details via GraphQL
raw_output=$(gh api graphql --hostname "$hostname" --paginate -f owner="$org" -f repo="$repo" -f affiliation="$affiliation" -f query='
query ($owner: String!, $repo: String!, $affiliation: CollaboratorAffiliation!, $endCursor: String) {
repository(owner:$owner, name:$repo) {
name
Expand Down Expand Up @@ -44,4 +105,29 @@ query ($owner: String!, $repo: String!, $affiliation: CollaboratorAffiliation!,
}
}
}
}'
}' --jq '
.data.repository.collaborators.edges[] |
.node.login as $user |
.permission as $effective |
(.permissionSources | map(select(.source.type == "Organization") | .permission)) as $org_perms |
[.permissionSources[] |
if .source.type == "Organization" then "org-member(\(.permission))"
elif .source.type == "Team" then "team:\(.source.name)(\(.permission))"
elif (.permission as $p | $org_perms | any(. == $p)) | not then
# permissionSources only returns READ/WRITE/ADMIN - use effective for MAINTAIN/TRIAGE
if .permission == "WRITE" and $effective == "MAINTAIN" then "direct(MAINTAIN)"
elif .permission == "READ" and $effective == "TRIAGE" then "direct(TRIAGE)"
else "direct(\(.permission))"
end
else empty
end
] | unique | join(", ") |
"\($user) | \($effective) | \(.)"
')

# Fix team permission labels using REST data
if [ -n "$sed_cmd" ]; then
raw_output=$(echo "$raw_output" | sed -E "$sed_cmd")
fi

(echo "USER | EFFECTIVE | SOURCES" && echo "$raw_output") | column -t -s '|'