Skip to content

Commit 7e2f0b5

Browse files
Scripts for role and membership validation (#110)
* added 4 scripts * listed to README * chore: chmod +x *.sh * chore: spell out organization * refactor: clean up README --------- Co-authored-by: Josh Johanning <joshjohanning@github.com>
1 parent 891dd7b commit 7e2f0b5

File tree

5 files changed

+434
-1
lines changed

5 files changed

+434
-1
lines changed

gh-cli/README.md

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,52 @@ octocat/octocat
230230

231231
Change a repository visibility to internal, for example
232232

233+
### check-enterprise-owner.sh
234+
235+
Checks if a user is an enterprise admin (owner) using the GitHub GraphQL API.
236+
237+
Usage:
238+
239+
```shell
240+
./check-enterprise-owner.sh <ENTERPRISE_SLUG> <USERNAME>
241+
```
242+
243+
### check-enterprise-team-membership.sh
244+
245+
Checks if a user is a member of an enterprise team using the GitHub API (private preview feature).
246+
247+
Usage:
248+
249+
```shell
250+
./check-enterprise-team-membership.sh <enterprise> <team-slug> <username>
251+
```
252+
253+
> [!NOTE]
254+
> This script uses a private preview API for enterprise teams, which may change without notice.
255+
256+
### check-organization-team-membership.sh
257+
258+
Checks if a user is a member of a specific team in an organization using the GitHub API.
259+
260+
Usage:
261+
262+
```shell
263+
./check-organization-team-membership.sh <organization> <team-slug> <username>
264+
```
265+
266+
> [!NOTE]
267+
> Your token must have the `read:org` scope to view team membership.
268+
269+
### check-repository-admin.sh
270+
271+
Checks if a user is a collaborator in a given repository and determines if they have admin access.
272+
273+
Usage:
274+
275+
```shell
276+
./check-repository-admin.sh <OWNER> <REPOSITORY> <USERNAME>
277+
```
278+
233279
### copy-organization-members.sh
234280

235281
Copy organization members from one organization to the other, the member will **retain** the source role (owner or member), member cannot be demoted, if they already exist at the target with an owner role they cannot be demoted to member.
@@ -1016,7 +1062,7 @@ Gets the list of organization secrets that are available by repository (all repo
10161062

10171063
Public repositories are ignored and not listed.
10181064

1019-
A repository can only use a max of [100 organization secrets](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions#limits-for-secrets) that are available to it. The purpose of this script is to get list of repositories and the number of organization secrets available to them mostly to figure out if you are hitting the limit and not all secrets are really available to the repository (only first 100 secrets sorted by secret name are available).
1065+
A repository can only use a max of [100 organization secrets](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions#limits-for-secrets) that are available to it. The purpose of this script is to get list of repositories and the number of organization secrets available to them mostly to figure out if you are hitting the limit and not all secrets are really available.
10201066

10211067
usage:
10221068

gh-cli/check-enterprise-owner.sh

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
#!/bin/bash
2+
3+
# Check if a user is an enterprise admin
4+
# API: https://docs.github.com/en/enterprise-cloud@latest/graphql/reference/objects#enterpriseownerinfo
5+
# Reference: https://docs.github.com/en/enterprise-cloud@latest/graphql/reference/queries#enterprise
6+
7+
# Usage: ./check-enterprise-owner.sh <ENTERPRISE_SLUG> <USERNAME>
8+
# Example: ./check-enterprise-owner.sh octocat johndoe
9+
10+
set -e
11+
12+
# Check if required parameters are provided
13+
if [ $# -lt 2 ]; then
14+
echo "Usage: $0 <ENTERPRISE_SLUG> <USERNAME>"
15+
echo "Example: $0 octocat johndoe"
16+
echo ""
17+
echo "Note: Requires 'gh' CLI to be installed and authenticated"
18+
exit 1
19+
fi
20+
21+
ENTERPRISE_SLUG="$1"
22+
TARGET_USER="$2"
23+
24+
# Check if gh CLI is available
25+
if ! command -v gh &> /dev/null; then
26+
echo "Error: GitHub CLI (gh) is required but not installed."
27+
echo "Install it from: https://cli.github.com/"
28+
exit 1
29+
fi
30+
31+
# Check if gh is authenticated
32+
if ! gh auth status &> /dev/null; then
33+
echo "Error: GitHub CLI is not authenticated."
34+
echo "Run 'gh auth login' to authenticate."
35+
exit 1
36+
fi
37+
38+
# Check if jq is available for JSON parsing
39+
if ! command -v jq &> /dev/null; then
40+
echo "Error: jq is required for JSON parsing but not installed."
41+
echo "Install it with: brew install jq (on macOS) or your package manager"
42+
exit 1
43+
fi
44+
45+
USER_FOUND=false
46+
CURSOR=""
47+
HAS_NEXT_PAGE=true
48+
49+
echo "Checking if user '$TARGET_USER' is an enterprise admin for '$ENTERPRISE_SLUG'..."
50+
51+
while [ "$HAS_NEXT_PAGE" = "true" ]; do
52+
# Build the GraphQL query with optional cursor
53+
if [ -z "$CURSOR" ]; then
54+
QUERY='
55+
query CheckEnterpriseAdmin($slug: String!) {
56+
enterprise(slug: $slug) {
57+
id
58+
name
59+
slug
60+
ownerInfo {
61+
admins(first: 100) {
62+
totalCount
63+
pageInfo {
64+
hasNextPage
65+
endCursor
66+
}
67+
nodes {
68+
login
69+
name
70+
}
71+
}
72+
}
73+
}
74+
}'
75+
RESPONSE=$(gh api graphql -f query="$QUERY" -f slug="$ENTERPRISE_SLUG" 2>/dev/null)
76+
else
77+
QUERY='
78+
query CheckEnterpriseAdmin($slug: String!, $cursor: String!) {
79+
enterprise(slug: $slug) {
80+
id
81+
name
82+
slug
83+
ownerInfo {
84+
admins(first: 100, after: $cursor) {
85+
totalCount
86+
pageInfo {
87+
hasNextPage
88+
endCursor
89+
}
90+
nodes {
91+
login
92+
name
93+
}
94+
}
95+
}
96+
}
97+
}'
98+
RESPONSE=$(gh api graphql -f query="$QUERY" -f slug="$ENTERPRISE_SLUG" -f cursor="$CURSOR" 2>/dev/null)
99+
fi
100+
101+
# Check for errors in the response
102+
if [ $? -ne 0 ] || echo "$RESPONSE" | jq -e '.errors' > /dev/null 2>&1; then
103+
ERROR_MSG=$(echo "$RESPONSE" | jq -r '.errors[0].message // "Unknown error"' 2>/dev/null || echo "API call failed")
104+
echo "✗ Error accessing enterprise '$ENTERPRISE_SLUG': $ERROR_MSG"
105+
106+
# Check for common error types
107+
if echo "$ERROR_MSG" | grep -q "Could not resolve to an Enterprise"; then
108+
echo " Enterprise slug '$ENTERPRISE_SLUG' not found or not accessible"
109+
elif echo "$ERROR_MSG" | grep -q "Must have admin access"; then
110+
echo " Current user does not have admin access to view enterprise admins"
111+
fi
112+
exit 1
113+
fi
114+
115+
# Check if enterprise data exists
116+
if ! echo "$RESPONSE" | jq -e '.data.enterprise' > /dev/null 2>&1; then
117+
echo "✗ Enterprise '$ENTERPRISE_SLUG' not found or not accessible"
118+
exit 1
119+
fi
120+
121+
# Extract data from response
122+
TOTAL_COUNT=$(echo "$RESPONSE" | jq -r '.data.enterprise.ownerInfo.admins.totalCount')
123+
HAS_NEXT_PAGE=$(echo "$RESPONSE" | jq -r '.data.enterprise.ownerInfo.admins.pageInfo.hasNextPage')
124+
CURSOR=$(echo "$RESPONSE" | jq -r '.data.enterprise.ownerInfo.admins.pageInfo.endCursor')
125+
126+
# Check if target user is in current page
127+
ADMINS=$(echo "$RESPONSE" | jq -r '.data.enterprise.ownerInfo.admins.nodes[].login')
128+
129+
PAGE_COUNT=$(echo "$RESPONSE" | jq -r '.data.enterprise.ownerInfo.admins.nodes | length')
130+
echo "Checking page with $PAGE_COUNT admins..."
131+
132+
for admin in $ADMINS; do
133+
# Case-insensitive comparison
134+
if [ "$(echo "$admin" | tr '[:upper:]' '[:lower:]')" = "$(echo "$TARGET_USER" | tr '[:upper:]' '[:lower:]')" ]; then
135+
USER_FOUND=true
136+
echo "✓ User '$TARGET_USER' found as enterprise admin!"
137+
138+
# Get full admin details
139+
ADMIN_NAME=$(echo "$RESPONSE" | jq -r --arg login "$admin" '.data.enterprise.ownerInfo.admins.nodes[] | select(.login == $login) | .name')
140+
echo " Login: $admin"
141+
echo " Name: $ADMIN_NAME"
142+
break 2 # Break out of both loops
143+
fi
144+
done
145+
146+
# If no next page, break
147+
if [ "$HAS_NEXT_PAGE" = "false" ]; then
148+
break
149+
fi
150+
done
151+
152+
# Final result
153+
echo ""
154+
echo "=== SUMMARY ==="
155+
echo "Enterprise: $ENTERPRISE_SLUG"
156+
echo "Total admins checked: $TOTAL_COUNT"
157+
if [ "$USER_FOUND" = "true" ]; then
158+
echo "Result: ✓ '$TARGET_USER' IS an enterprise admin"
159+
exit 0
160+
else
161+
echo "Result: ✗ '$TARGET_USER' is NOT an enterprise admin"
162+
exit 1
163+
fi
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#!/bin/bash
2+
3+
# API GET /enterprises/{enterprise}/teams/{enterprise-team}/memberships/{username}
4+
# NOTE: Enterprise Teams is a private preview feature, API may subject to change without notice
5+
#
6+
# Check if a user is a member of an enterprise team
7+
# Response Code: 200 if user is a member, otherwise not
8+
9+
# Function to display usage
10+
usage() {
11+
echo "Usage: $0 <enterprise> <team-slug> <username>"
12+
echo ""
13+
echo "Parameters:"
14+
echo " enterprise - The enterprise slug"
15+
echo " team-slug - The enterprise team slug"
16+
echo " username - The username to check"
17+
echo ""
18+
echo "Example:"
19+
echo " $0 my-enterprise dev-team octocat"
20+
exit 1
21+
}
22+
23+
# Check if required parameters are provided
24+
if [ $# -ne 3 ]; then
25+
echo "Error: Missing required parameters"
26+
usage
27+
fi
28+
29+
ENTERPRISE="$1"
30+
TEAM_SLUG="$2"
31+
USERNAME="$3"
32+
33+
echo "Checking if user '$USERNAME' is a member of enterprise team '$TEAM_SLUG' in enterprise '$ENTERPRISE'..."
34+
35+
# Make the API call using gh api and capture both response and HTTP status code
36+
TEMP_DIR=$(mktemp -d)
37+
RESPONSE_FILE="$TEMP_DIR/response.json"
38+
HEADERS_FILE="$TEMP_DIR/headers.txt"
39+
40+
# Use gh api with --include flag to get headers, then parse status code
41+
gh api "/enterprises/$ENTERPRISE/teams/$TEAM_SLUG/memberships/$USERNAME" \
42+
--include > "$TEMP_DIR/full_response.txt" 2>&1
43+
API_EXIT_CODE=$?
44+
45+
# Extract HTTP status code from the response headers
46+
if [ $API_EXIT_CODE -eq 0 ]; then
47+
# Split headers and body
48+
sed '/^$/q' "$TEMP_DIR/full_response.txt" > "$HEADERS_FILE"
49+
sed '1,/^$/d' "$TEMP_DIR/full_response.txt" > "$RESPONSE_FILE"
50+
51+
# Extract status code from first line of headers
52+
HTTP_STATUS=$(head -n1 "$HEADERS_FILE" | grep -o '[0-9]\{3\}' | head -n1)
53+
RESPONSE=$(cat "$RESPONSE_FILE")
54+
else
55+
# If gh api failed, set status as non-200
56+
RESPONSE=$(cat "$TEMP_DIR/full_response.txt")
57+
HTTP_STATUS="non-200"
58+
fi
59+
60+
echo "HTTP Status Code: $HTTP_STATUS"
61+
62+
# Check response based on HTTP status code - only 200 indicates membership
63+
if [ "$HTTP_STATUS" = "200" ]; then
64+
# 200 OK - User is a member
65+
TYPE=$(echo "$RESPONSE" | jq -r '.type // "unknown"' 2>/dev/null)
66+
67+
echo "✅ User '$USERNAME' is a member of team '$TEAM_SLUG'"
68+
if [ "$TYPE" != "null" ] && [ "$TYPE" != "unknown" ]; then
69+
echo " Type: $TYPE"
70+
fi
71+
72+
# Display full response if verbose
73+
if [ "$VERBOSE" = "true" ]; then
74+
echo ""
75+
echo "Full response:"
76+
echo "$RESPONSE" | jq . 2>/dev/null || echo "$RESPONSE"
77+
fi
78+
79+
# Cleanup
80+
rm -rf "$TEMP_DIR"
81+
exit 0
82+
else
83+
# Any non-200 status code means user is not a member
84+
echo "❌ User '$USERNAME' is not a member of team '$TEAM_SLUG' in enterprise '$ENTERPRISE'"
85+
rm -rf "$TEMP_DIR"
86+
exit 1
87+
fi
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#!/bin/bash
2+
3+
# Checks if a user is a member of a specific team in an organization using the GitHub API.
4+
#
5+
# Usage:
6+
# ./check-org-team-membership.sh <organization> <team-slug> <username>
7+
#
8+
# Example:
9+
# ./check-org-team-membership.sh my-organization security johndoe
10+
#
11+
# Requirements:
12+
# - GitHub CLI (`gh`) must be installed and authenticated
13+
# - Token must have `read:org` scope to view team membership
14+
# - Uses GitHub API endpoint: /orgs/{organization}/teams/{team-slug}/memberships/{username}
15+
# - This script does not paginate as the endpoint is for a single user/team
16+
#
17+
# Notes:
18+
# - Organization, team-slug, and username must be provided as input parameters
19+
20+
set -e
21+
22+
if [ $# -lt 3 ]; then
23+
echo "Usage: $0 <organization> <team-slug> <username>"
24+
echo "Example: $0 my-organization security johndoe"
25+
echo ""
26+
echo "Note: Requires 'gh' CLI to be installed and authenticated"
27+
exit 1
28+
fi
29+
30+
organization="$1"
31+
team_slug="$2"
32+
username="$3"
33+
34+
if ! command -v gh &> /dev/null; then
35+
echo "Error: GitHub CLI (gh) is required but not installed."
36+
echo "Install it from: https://cli.github.com/"
37+
exit 1
38+
fi
39+
40+
if ! gh auth status &> /dev/null; then
41+
echo "Error: GitHub CLI is not authenticated."
42+
echo "Run 'gh auth login' to authenticate."
43+
exit 1
44+
fi
45+
46+
echo "Checking if user '$username' is a member of team '$team_slug' in organization '$organization'..."
47+
48+
if gh api "orgs/$organization/teams/$team_slug/memberships/$username" --silent 2>/dev/null; then
49+
echo "✓ User '$username' is a member of team '$team_slug' in organization '$organization'"
50+
exit 0
51+
else
52+
echo "✗ User '$username' is NOT a member of team '$team_slug' in organization '$organization'"
53+
exit 1
54+
fi

0 commit comments

Comments
 (0)