Skip to content

Commit 14403d9

Browse files
authored
Merge pull request #58 from OpenRailAssociation/support-app-auth
2 parents f4ed7d7 + a220e6c commit 14403d9

File tree

4 files changed

+101
-30
lines changed

4 files changed

+101
-30
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,20 @@ Inside [`config/example`](./config/example), you can find an example configurati
5454

5555
You may also be interested in the [live configuration of the OpenRail Association's organization](https://github.com/OpenRailAssociation/openrail-org-config).
5656

57+
### Authentication via token or app
58+
59+
As this tool issues many API requests (both on REST and GraphQL API), authentication is highly recommended. This is supported via personal access tokens of a user (PAT) or a GitHub App which you can setup yourself.
60+
61+
Access tokens and apps need the following permissions:
62+
* Repository permissions
63+
* Administration: read and write
64+
* Metadata: read
65+
* Organization permissions:
66+
* Administration: read and write
67+
* Members: read and write
68+
69+
You can set the required secrets in `config/app.yaml` or via environment variables (`GITHUB_TOKEN` or `GITHUB_APP_ID` and `GITHUB_APP_PRIVATE_KEY`).
70+
5771
## Run the program
5872

5973
You can execute the program using the command `gh-org-mgr`. `gh-org-mgr --help` shows all available arguments and options.

config/example/app.yaml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,34 @@
77

88
# Personal Oauth access token with required scopes
99
github_token: ghp_abcdefg123
10+
11+
# GitHub App (if this is set, the personal access token (github_token) will be ignored)
12+
github_app_id: 123456
13+
github_app_private_key: |
14+
-----BEGIN RSA PRIVATE KEY-----
15+
THIS IS JUST AN EXAMPLE KEY, DO NOT USE IT IN PRODUCTION 1234567
16+
MIIEowIBAAKCAQEAxkYIq1pp+OH+KTa9ZSAyY0PysSZEoZyXFnwWXLhi7ZDP6pPH
17+
9zUg+iVlSeEd2ZOwyBnfpWHG12QAFmbjojpcDT1mlkqv7N7YGS0z8kTp3DfgKBAd
18+
Uq0V8BvTubM78NClF4xQmWodfv0hjLmArWLP4cHKPnl1I5Ml7yPUtdwm9wEFT0oe
19+
QE7rnEgN8UFbjT4FQg4JvwKUNZsHfPetrXlSOyjztNE8wsRahCvzAaqUv+L//Y8I
20+
fX2wUW3A97TzpfiKrPXpb8u84Er7n8cHVSTiMWhksgnQZ4ymk3bLYjNyCN+gln6t
21+
qKtbsbynx77m0Q/ykYauWIfxPRBOLbEgmh9/bwIDAQABAoIBACGvOD3USHit/D4I
22+
PLj3dVgD7TFHbRV/wvNg9XOfJ79wgMI7hRdsgUO+Iq0gf6+9NaVpL+Oq7tsc9B7a
23+
MAYZoBXnvov9+FFnspLkaRTZvFlbbMuhoTmwii+Wqqu71Y0eBU4w2miV7JjsbEy6
24+
HzBVvzd9ctyWSd5XW3R7Q+H5mu0PhLKSaHnYitJs0OajFjHg9q28gQ9olnemkaDr
25+
IFOevK7LWWDEkvhV/ZyxnL+v/9oCSxgBVgmMGPe2aFHQ9Ej4cHiTrOxFvXH8D3LU
26+
jaIcNQ6NA3FWBYFADKA3wY46W8NmUSt3qgQDZttydu3RMzbukGYJ3W6nP9TxLrVE
27+
Yu2xIaEML8rCzUIUvwEKlADlkpr5Lz9d92NzhVndIps3FW5UFTHTz0G3fvIHNGWv
28+
IPTZDNQWVs3OI1xtMQV2MGPeDINNn3Wg3WIJxMn7zXnFlLxm0vhKEPcCgYEA1vMH
29+
f3NBdk9u1IclV+4dCYF7Par9ypcPE4DEutF4gtZu8pDBVmkxaqxXMS5LK++DIhQs
30+
YxRhwydMK2n+cYl9bvs9MdMxMh6ZdBVOVLcSL24CzCDSTjeKVmL26j3TTNq6Mi8P
31+
A/rG7CWaIDRMd4AuZZmR5M71BVy4rubK4CyzX0kCgYEAzg95WaMbMjyoYW/qjTzZ
32+
7XMxtsIjIQKNDIywVRsJZGb8UKmtCdoJEAoFCSiE/ut7xCvG+MpK1c5WhvFPQyiT
33+
sf6J3zME4l752Wy2WOxrPa/GFTPUj6ewBB1FjolAmSzsljTgX+e5NA7hxJ+Ly/uU
34+
jbswdRYg8jQx81HYlH7v+w8CgYB6noUmdY9geIvW/YmWEaXK6Gxvj33b9jSJgam4
35+
kQpYSQ9dnKpOKxAftFTBH5GObMG3zR5NHzFt7JsNIRgfmLlPeE8+fyXPW5lamVTo
36+
Cs968xzxab/PEuv9v9LvaXmCnDwfqKy+Lm8QA5tax7rfaOYO235Ysp8gAfbw/4O4
37+
QofI0QKBgAxXAlq/AfqpZHrz5B9V0EsnaIiSiDbFr6RVhoGN3zF4ObExee2MOmqA
38+
D9zgrlJ8D4bxPrwDrCuXHY7s/1/uCX3K+mS7CWpybOcJY4XzsNznOYcMQzw22fxl
39+
u1ioG/s3Ahhd778VIjj5d32Xbjj8vbSFj8vJe5bBNblYbelWfETg
40+
-----END RSA PRIVATE KEY-----

gh_org_mgr/_gh_api.py

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,15 @@
1313
import requests
1414

1515

16-
def get_github_token(token: str = "") -> str:
17-
"""Get the GitHub token from config or environment, while environment overrides"""
18-
if "GITHUB_TOKEN" in os.environ and os.environ["GITHUB_TOKEN"]:
19-
logging.debug("GitHub Token taken from environment variable GITHUB_TOKEN")
20-
token = os.environ["GITHUB_TOKEN"]
21-
elif token:
22-
logging.debug("GitHub Token taken from app configuration file")
23-
else:
24-
sys.exit(
25-
"No token set for GitHub authentication! Set it in config/app_config.yaml "
26-
"or via environment variable GITHUB_TOKEN"
27-
)
28-
29-
return token
16+
def get_github_secrets_from_env(env_variable: str, secret: str | int) -> str:
17+
"""Get GitHub secrets from config or environment, while environment overrides"""
18+
if env_variable in os.environ and os.environ[env_variable]:
19+
logging.debug("GitHub secret taken from environment variable %s", env_variable)
20+
secret = os.environ[env_variable]
21+
elif secret:
22+
logging.debug("GitHub secret taken from app configuration file")
23+
24+
return str(secret)
3025

3126

3227
# Function to execute GraphQL query
@@ -51,10 +46,12 @@ def run_graphql_query(query, variables, token):
5146
return json_return
5247

5348
# Debug information in case of errors
54-
print(
55-
f"Query failed with HTTP error code '{request.status_code}' when running "
56-
f"this query: {query}\n"
57-
f"Return: {json_return}\n"
58-
f"Headers: {request.headers}"
49+
logging.error(
50+
"Query failed with HTTP error code '%s' when running this query: %s\n"
51+
"Return: %s\nHeaders: %s",
52+
request.status_code,
53+
query,
54+
json_return,
55+
request.headers,
5956
)
6057
sys.exit(1)

gh_org_mgr/_gh_org.py

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,19 @@
88
import sys
99
from dataclasses import asdict, dataclass, field
1010

11-
from github import Github, GithubException, UnknownObjectException
11+
from github import (
12+
Auth,
13+
Github,
14+
GithubException,
15+
GithubIntegration,
16+
UnknownObjectException,
17+
)
1218
from github.NamedUser import NamedUser
1319
from github.Organization import Organization
1420
from github.Repository import Repository
1521
from github.Team import Team
1622

17-
from ._gh_api import get_github_token, run_graphql_query
23+
from ._gh_api import get_github_secrets_from_env, run_graphql_query
1824

1925

2026
@dataclass
@@ -24,6 +30,8 @@ class GHorg: # pylint: disable=too-many-instance-attributes, too-many-lines
2430
gh: Github = None # type: ignore
2531
org: Organization = None # type: ignore
2632
gh_token: str = ""
33+
gh_app_id: str | int = ""
34+
gh_app_private_key: str = ""
2735
default_repository_permission: str = ""
2836
current_org_owners: list[NamedUser] = field(default_factory=list)
2937
configured_org_owners: list[str] = field(default_factory=list)
@@ -56,18 +64,39 @@ def _sluggify_teamname(self, team: str) -> str:
5664
# supported, or multiple spaces etc.
5765
return team.replace(" ", "-")
5866

59-
def login(self, orgname: str, token: str) -> None:
60-
"""Login to GH, gather org data"""
61-
self.gh_token = get_github_token(token)
62-
self.gh = Github(self.gh_token)
63-
logging.debug("Logged in as %s", self.gh.get_user().login)
67+
def login(
68+
self, orgname: str, token: str = "", app_id: str | int = "", app_private_key: str = ""
69+
) -> None:
70+
"""Login to GH via PAT or App, gather org data"""
71+
# Get all login data from config and environment
72+
self.gh_token = get_github_secrets_from_env(env_variable="GITHUB_TOKEN", secret=token)
73+
self.gh_app_id = get_github_secrets_from_env(env_variable="GITHUB_APP_ID", secret=app_id)
74+
self.gh_app_private_key = get_github_secrets_from_env(
75+
env_variable="GITHUB_APP_PRIVATE_KEY", secret=app_private_key
76+
)
77+
78+
# Decide how to login. If app set, prefer this
79+
if self.gh_app_id and self.gh_app_private_key:
80+
logging.debug("Logged in via app %s", self.gh_app_id)
81+
auth = Auth.AppAuth(app_id=self.gh_app_id, private_key=self.gh_app_private_key)
82+
app = GithubIntegration(auth=auth)
83+
installation = app.get_installations()[0]
84+
self.gh = installation.get_github_for_installation()
85+
elif self.gh_token:
86+
logging.debug("Logging in as user with PAT")
87+
self.gh = Github(auth=Auth.Token(self.gh_token))
88+
logging.debug("Logged in as %s", self.gh.get_user().login)
89+
else:
90+
logging.error("No GitHub token or App ID+private key provided")
91+
sys.exit(1)
92+
6493
self.org = self.gh.get_organization(orgname)
6594
logging.debug("Gathered data from organization '%s' (%s)", self.org.login, self.org.name)
6695

6796
def ratelimit(self):
68-
"""Get current rate limit"""
97+
"""Print current rate limit"""
6998
core = self.gh.get_rate_limit().core
70-
logging.debug(
99+
logging.info(
71100
"Current rate limit: %s/%s (reset: %s)", core.remaining, core.limit, core.reset
72101
)
73102

@@ -917,7 +946,7 @@ def _fetch_collaborators_of_repo(self, repo: Repository):
917946
permissions using the GraphQL API"""
918947
# TODO: Consider doing this for all repositories at once, but calculate
919948
# costs beforehand
920-
query = """
949+
graphql_query = """
921950
query($owner: String!, $name: String!, $cursor: String) {
922951
repository(owner: $owner, name: $name) {
923952
collaborators(first: 100, after: $cursor) {
@@ -944,7 +973,7 @@ def _fetch_collaborators_of_repo(self, repo: Repository):
944973

945974
while has_next_page:
946975
logging.debug("Requesting collaborators for %s", repo.name)
947-
result = run_graphql_query(query, variables, self.gh_token)
976+
result = run_graphql_query(graphql_query, variables, self.gh_token)
948977
try:
949978
collaborators.extend(result["data"]["repository"]["collaborators"]["edges"])
950979
has_next_page = result["data"]["repository"]["collaborators"]["pageInfo"][

0 commit comments

Comments
 (0)