Skip to content

Commit eecddeb

Browse files
committed
Refactor and simply for GHES/EMU users, add type hints
1 parent fefd4d7 commit eecddeb

File tree

10 files changed

+221
-112
lines changed

10 files changed

+221
-112
lines changed

README.md

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,19 @@ You need to be an enterprise administrator to use these scripts!
2626
pip install -r requirements.txt
2727
```
2828

29-
1. Edit the inputs as arguments to the script as follows:
30-
31-
- the API endpoint (for GHES, EMU, or data residency) in `--api-url`. For GHEC this is not required.
32-
- Create a file and save your token there to read it, and call the script with `--token-file` argument, or call the script with the token in `GITHUB_TOKEN` in your environment.
33-
- Add the enterprise slug to `--enterprise-slug`. This is string URL version of the enterprise identity. It's easily available in the enterprise admin url (for cloud and server), e.g. `https://github.com/enterprises/ENTERPRISE-SLUG-HERE`.
34-
- For the security manager team script, the list of orgs output by `org-admin-promote.py` in `--unmanaged-orgs` and the name of the security manager team and the team members to add, in `--team-name` and `--team-members`. If you are using GHES 3.15 or below, please use the `--legacy` flag to use the legacy security managers API.
29+
1. Choose inputs as arguments to the script as follows:
30+
31+
- the server URL (for GHES, EMU, or data residency) in `--github-url`
32+
- For GHEC this is not required.
33+
- call the script with the correct GitHub PAT
34+
- place it in `GITHUB_TOKEN` in your environment, or
35+
- create a file and save your token there to read it, and call the script with the `--token-file` argument
36+
- use the enterprise slug as the first argument in the promote/demote scripts
37+
- this is string URL version of the enterprise identity. It's available in the enterprise admin url (for cloud and server), e.g. `https://github.com/enterprises/ENTERPRISE-SLUG-HERE`.
38+
- for the security manager team script:
39+
- use the list of orgs output by `org-admin-promote.py` in `--unmanaged-orgs`
40+
- put the name of the security manager team and the team members to add in `--team-name` and `--team-members`.
41+
- If you are using GHES 3.15 or below, please use the `--legacy` flag to use the legacy security managers API.
3542
3643
1. Run them in the following order:
3744
@@ -41,31 +48,31 @@ You need to be an enterprise administrator to use these scripts!
4148
4249
## Assumptions
4350
44-
- The security manager team isn't already an existing team that's using team sync [for enterprise](https://docs.github.com/en/enterprise-cloud@latest/admin/identity-and-access-management/using-saml-for-enterprise-iam/managing-team-synchronization-for-organizations-in-your-enterprise) or [for organizations](https://docs.github.com/en/enterprise-cloud@latest/organizations/organizing-members-into-teams/synchronizing-a-team-with-an-identity-provider-group). You may be able to edit the script a bit to make this work by adding an existing team to all orgs, but I wasn't going to dive deep into the weeds of identity management.
51+
- The security manager team isn't already an existing team that's using team sync [for enterprise](https://docs.github.com/en/enterprise-cloud@latest/admin/identity-and-access-management/using-saml-for-enterprise-iam/managing-team-synchronization-for-organizations-in-your-enterprise) or [for organizations](https://docs.github.com/en/enterprise-cloud@latest/organizations/organizing-members-into-teams/synchronizing-a-team-with-an-identity-provider-group).
4552
4653
## Any extra info?
4754
4855
This is what a successful run looks like. Here's the inputs:
4956

5057
- The enterprise admin is named `ghe-admin`.
51-
- The security team is named `spy-stuff` and has two members `luigi` and `hubot`.
58+
- The security team is named `security-managers` (the default) and has two members `luigi` and `hubot`.
5259
- The organizations break down as such:
5360
- `acme` org was already configured correctly.
5461
- `testorg-00001` needed the team created, with `ghe-admin` removed and `luigi` and `hubot` added.
5562
- `testorg-00002` was already created
5663

5764
```console
58-
$ ./manage-sec-team.py
59-
Team spy-stuff updated as a security manager for acme!
60-
Creating team spy-stuff
61-
Team spy-stuff updated as a security manager for testorg-00001!
62-
Removing ghe-admin from spy-stuff
63-
Adding luigi to spy-stuff
64-
Adding hubot to spy-stuff
65-
Creating team spy-stuff
66-
Team spy-stuff updated as a security manager for testorg-00002!
67-
Removing ghe-admin from spy-stuff
68-
Team spy-stuff updated as a security manager for testorg-00003!
65+
$ ./manage-sec-team.py --sec-team-members luigi hubot
66+
Team security-managers updated as a security manager for acme
67+
Creating team security-managers
68+
Team security-managers updated as a security manager for testorg-00001
69+
Removing ghe-admin from security-managers
70+
Adding luigi to security-managers
71+
Adding hubot to security-managers
72+
Creating team security-managers
73+
Team security-managers updated as a security manager for testorg-00002
74+
Removing ghe-admin from security-managers
75+
Team security-managers updated as a security manager for testorg-00003
6976
```
7077

7178
## Architecture Footnotes

manage-sec-team.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,29 +19,34 @@
1919
from argparse import ArgumentParser
2020
from typing import Any
2121
from defusedcsv import csv
22-
from src import teams, organizations, github_token
22+
from src import teams, organizations, util
2323
import logging
2424

25+
2526
LOG = logging.getLogger(__name__)
2627

2728

2829
def add_args(parser) -> None:
2930
"""Add arguments to the command line parser."""
3031
parser.add_argument(
31-
"--api-url",
32-
default="https://api.github.com",
33-
help="GitHub API URL (https://github-hostname-here/api/v3/ for GHES, EMU or data residency)",
32+
"--github-url",
33+
required=False,
34+
help="GitHub URL for GHES, EMU or data residency",
3435
)
3536
parser.add_argument(
3637
"--token-file",
3738
required=False,
3839
help="GitHub Personal Access Token file (or use GITHUB_TOKEN)",
3940
)
4041
parser.add_argument(
41-
"--org-list", default="all_orgs.csv", help="CSV file of organizations"
42+
"--org-list",
43+
default="all_orgs.csv",
44+
help="CSV file of organizations (default: all_orgs.csv)",
4245
)
4346
parser.add_argument(
44-
"--sec-team-name", default="security-managers", help="Security team name"
47+
"--sec-team-name",
48+
default="security-managers",
49+
help="Security team name (default: security-managers)",
4550
)
4651
parser.add_argument("--sec-team-members", nargs="*", help="Security team members")
4752
parser.add_argument(
@@ -215,7 +220,7 @@ def main() -> None:
215220
with open(args.org_list, "r") as f:
216221
orgs = list(csv.DictReader(f))
217222

218-
github_pat = github_token.read_token(args.token_file)
223+
github_pat = util.read_token(args.token_file)
219224

220225
if not github_pat:
221226
LOG.error("⨯ GitHub Personal Access Token not found")
@@ -237,6 +242,8 @@ def main() -> None:
237242
)
238243
return
239244

245+
api_url = util.rest_api_url_from_server_url(args.github_url)
246+
240247
# Set up the headers
241248
headers = {
242249
"Authorization": "token {}".format(github_pat),
@@ -247,10 +254,10 @@ def main() -> None:
247254
org_name = org["login"]
248255

249256
make_security_managers_team(
250-
org_name, args.sec_team_name, args.api_url, headers, legacy=args.legacy
257+
org_name, args.sec_team_name, api_url, headers, legacy=args.legacy
251258
)
252259
add_security_managers_to_team(
253-
org_name, args.sec_team_name, sec_team_members, args.api_url, headers
260+
org_name, args.sec_team_name, sec_team_members, api_url, headers
254261
)
255262

256263

org-admin-demote.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,33 +17,34 @@
1717

1818
from argparse import ArgumentParser
1919
from typing import Iterable, List
20-
from src import enterprises, github_token
20+
from src import enterprises, util
2121
import logging
2222

23+
2324
LOG = logging.getLogger(__name__)
2425

2526

2627
def add_args(parser: ArgumentParser) -> None:
2728
"""Add arguments to the command line parser."""
2829
parser.add_argument(
29-
"--api-url",
30-
default="https://api.github.com/graphql",
31-
help="GitHub GraphQL API endpoint (https://github-hostname-here/api/graphql for GHES, EMU or data residency)",
30+
"enterprise_slug",
31+
help="Enterprise slug (after /enterprises/ in the URL)",
3232
)
3333
parser.add_argument(
34-
"--token-file",
34+
"--github-url",
3535
required=False,
36-
help="File containing a GitHub Personal Access Token with admin:enterprise and read:org scope (or use GITHUB_TOKEN)",
36+
help="GitHub URL for GHES, EMU or data residency",
3737
)
3838
parser.add_argument(
39-
"--enterprise-slug",
40-
required=True,
41-
help="Enterprise slug (after /enterprises/ in the URL)",
39+
"--token-file",
40+
required=False,
41+
help="File containing a GitHub Personal Access Token with admin:enterprise and read:org scope (or use GITHUB_TOKEN)",
4242
)
43+
4344
parser.add_argument(
4445
"--unmanaged-orgs",
4546
default="unmanaged_orgs.txt",
46-
help="Path to newline-delimited list of organization IDs to demote from",
47+
help="Path to newline-delimited list of organization IDs to demote from (default: unmanaged_orgs.txt)",
4748
)
4849
parser.add_argument(
4950
"--debug",
@@ -84,7 +85,9 @@ def main() -> None:
8485

8586
logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO)
8687

87-
github_pat = github_token.read_token(args.token_file)
88+
github_pat = util.read_token(args.token_file)
89+
90+
api_url = util.graphql_api_url_from_server_url(args.github_url)
8891

8992
if not github_pat:
9093
LOG.error("⨯ GitHub Personal Access Token not found")
@@ -95,11 +98,11 @@ def main() -> None:
9598
}
9699

97100
enterprise_id = enterprises.get_enterprise_id(
98-
args.api_url, args.enterprise_slug, headers
101+
api_url, args.enterprise_slug, headers
99102
)
100103

101104
unmanaged_orgs = read_unmanaged_org_ids(args.unmanaged_orgs)
102-
demote_admin(args.api_url, headers, enterprise_id, unmanaged_orgs)
105+
demote_admin(api_url, headers, enterprise_id, unmanaged_orgs)
103106

104107

105108
if __name__ == "__main__": # pragma: no cover

org-admin-promote.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@
1818

1919
from argparse import ArgumentParser
2020
from typing import List
21-
from src import enterprises, organizations, github_token
21+
from urllib.parse import urlparse
22+
from src import enterprises, organizations, util
2223
import logging
2324

25+
2426
LOG = logging.getLogger(__name__)
2527

2628

@@ -31,9 +33,9 @@ def add_args(parser: ArgumentParser) -> None:
3133
help="Enterprise slug (after /enterprises/ in URL)",
3234
)
3335
parser.add_argument(
34-
"--api-url",
35-
default="https://api.github.com/graphql",
36-
help="GitHub GraphQL API endpoint (https://github-hostname-here/api/graphql for GHES, EMU or data residency)",
36+
"--github-url",
37+
required=False,
38+
help="GitHub URL for GHES, EMU or data residency",
3739
)
3840
parser.add_argument(
3941
"--token-file",
@@ -43,12 +45,12 @@ def add_args(parser: ArgumentParser) -> None:
4345
parser.add_argument(
4446
"--unmanaged-orgs",
4547
default="unmanaged_orgs.txt",
46-
help="Output file for previously unmanaged organization IDs",
48+
help="Output file for previously unmanaged organization IDs (default: unmanaged_orgs.txt)",
4749
)
4850
parser.add_argument(
4951
"--orgs-csv",
5052
default="all_orgs.csv",
51-
help="Output CSV file listing all organizations in the enterprise",
53+
help="Output CSV file listing all organizations in the enterprise (default: all_orgs.csv)",
5254
)
5355
parser.add_argument(
5456
"--debug",
@@ -119,14 +121,20 @@ def main() -> None:
119121

120122
logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO)
121123

122-
github_pat = github_token.read_token(args.token_file)
124+
api_url = (
125+
util.graphql_api_url_from_server_url(args.github_url)
126+
if args.github_url
127+
else "https://api.github.com/graphql"
128+
)
129+
130+
github_pat = util.read_token(args.token_file)
123131
headers: dict[str, str] = {
124132
"Authorization": f"token {github_pat}",
125133
}
126134

127135
if (
128136
promote_all(
129-
args.api_url,
137+
api_url,
130138
headers,
131139
args.enterprise_slug,
132140
args.unmanaged_orgs,
@@ -137,7 +145,7 @@ def main() -> None:
137145
return
138146

139147
# Refresh and write all orgs CSV after promotions
140-
orgs = organizations.list_orgs(args.api_url, args.enterprise_slug, headers)
148+
orgs = organizations.list_orgs(api_url, args.enterprise_slug, headers)
141149
organizations.write_orgs_to_csv(orgs, args.orgs_csv)
142150

143151

src/enterprises.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44
This file holds enterprise-related functions
55
"""
66

7+
from typing import Any
78
import requests
8-
from .headers import add_request_headers
9+
from .util import add_request_headers
910

1011

11-
def get_enterprise_id(api_endpoint, enterprise_slug, headers):
12+
def get_enterprise_id(
13+
api_endpoint: str, enterprise_slug: str, headers: dict[str, str]
14+
) -> str:
1215
"""
1316
Get the ID of an enterprise by its slug.
1417
"""
@@ -30,7 +33,7 @@ def get_enterprise_id(api_endpoint, enterprise_slug, headers):
3033
return response.json()["data"]["enterprise"]["id"]
3134

3235

33-
def make_promote_mutation(enterprise_id, org_id, role):
36+
def make_promote_mutation(enterprise_id: str, org_id: str, role: str) -> str:
3437
"""
3538
Create a GraphQL mutation to promote the Enterprise owner to owner in an organization.
3639
"""
@@ -49,7 +52,13 @@ def make_promote_mutation(enterprise_id, org_id, role):
4952
)
5053

5154

52-
def promote_admin(api_endpoint, headers, enterprise_id, org_id, role):
55+
def promote_admin(
56+
api_endpoint: str,
57+
headers: dict[str, str],
58+
enterprise_id: str,
59+
org_id: str,
60+
role: str,
61+
) -> dict[str, Any]:
5362
"""
5463
Promote an enterprise admin to an organization owner.
5564
"""

src/github_token.py

Lines changed: 0 additions & 26 deletions
This file was deleted.

src/headers.py

Lines changed: 0 additions & 15 deletions
This file was deleted.

0 commit comments

Comments
 (0)