Skip to content

Commit 8a9ab65

Browse files
committed
separate out github authentication methods
1 parent 7050389 commit 8a9ab65

File tree

3 files changed

+188
-179
lines changed

3 files changed

+188
-179
lines changed

abcclassroom/auth.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
"""
2+
abc-classroom.auth
3+
==================
4+
"""
5+
6+
# Methods for setting up authorization to the GitHub API. See
7+
# github.py for methods that use the API.
8+
9+
import requests
10+
11+
import os.path as op
12+
from ruamel.yaml import YAML
13+
14+
from .utils import get_request
15+
16+
17+
def get_github_auth():
18+
"""
19+
Check to see if there is an existing github authentication
20+
and load the authentication.
21+
22+
Returns
23+
-------
24+
ruamel.yaml.comments.CommentedMap
25+
Yaml object that contains the token and id for a github session.
26+
If yaml doesn't exists, return an empty dictionary.
27+
"""
28+
yaml = YAML()
29+
try:
30+
with open(op.expanduser("~/.abc-classroom.tokens.yml")) as f:
31+
config = yaml.load(f)
32+
return config["github"]
33+
34+
except FileNotFoundError:
35+
return {}
36+
37+
38+
def set_github_auth(auth_info):
39+
"""
40+
Set the github authentication information. Put the token and id
41+
authentication information into a yaml file if it doesn't already exist.
42+
43+
Parameters
44+
----------
45+
auth_info : dictionary
46+
The token and id authentication information from github stored in a
47+
dictionary object.
48+
"""
49+
yaml = YAML()
50+
config = {}
51+
if get_github_auth():
52+
with open(op.expanduser("~/.abc-classroom.tokens.yml")) as f:
53+
config = yaml.load(f)
54+
55+
config["github"] = auth_info
56+
57+
with open(op.expanduser("~/.abc-classroom.tokens.yml"), "w") as f:
58+
yaml.dump(config, f)
59+
60+
61+
def get_access_token():
62+
"""Get a GitHub access token for the API
63+
64+
First tries to read from local token file. If token does not exist,
65+
or is not valid, generates a new token using the OAuth Device Flow.
66+
https://docs.github.com/en/free-pro-team@latest/developers/apps/
67+
identifying-and-authorizing-users-for-github-apps#device-flow
68+
69+
Returns an access token (string).
70+
"""
71+
# first, we see if we have a saved token
72+
auth_info = get_github_auth()
73+
if auth_info:
74+
try:
75+
access_token = auth_info["access_token"]
76+
# if so, is it valid?
77+
user = _get_authenticated_user(access_token)
78+
if user is not None:
79+
print(
80+
"Access token is present and valid; successfully "
81+
"authenticated as user {}".format(user)
82+
)
83+
return access_token
84+
except KeyError:
85+
pass
86+
87+
# otherwise, generate a new token
88+
print("Generating new access token")
89+
# client id for the abc-classroom-bot GitHub App
90+
client_id = "Iv1.8df72ad9560c774c"
91+
92+
# TODO need to handle cases where the device call fails - wrong client_id,
93+
# the user could ^C or the internet could be out, or some other
94+
# unanticipated woe)
95+
device_code = _get_login_code(client_id)
96+
access_token = _poll_for_status(client_id, device_code)
97+
98+
# test the new access token
99+
user = _get_authenticated_user(access_token)
100+
if user is not None:
101+
print("""Successfully authenticated as user {}""".format(user))
102+
return access_token
103+
104+
105+
def _get_authenticated_user(token):
106+
"""Test the validity of an access token.
107+
108+
Given a github access token, test that it is valid by making an
109+
API call to get the authenticated user.
110+
111+
Returns the GitHub username of the authenticated user if token valid,
112+
otherwise returns None.
113+
"""
114+
115+
url = "https://api.github.com/user"
116+
(status, body) = get_request(url, token)
117+
try:
118+
user = body["login"]
119+
return user
120+
except KeyError:
121+
return None
122+
123+
124+
def _get_login_code(client_id):
125+
"""Prompts the user to authorize abc-classroom-bot.
126+
127+
First part of the Device Flow workflow. Asks user to visit a URL and
128+
enter the provided code. Waits for user to hit RETURN to continue.
129+
Returns the device code.
130+
"""
131+
132+
# make the device call
133+
header = {"Content-Type": "application/json", "Accept": "application/json"}
134+
payload = {"client_id": client_id}
135+
link = "https://github.com/login/device/code"
136+
r = requests.post(link, headers=header, json=payload)
137+
138+
# process the response
139+
data = r.json()
140+
status = r.status_code
141+
if status != 200:
142+
# print the response if the call failed
143+
print(r.json())
144+
return None
145+
146+
device_code = data["device_code"]
147+
uri = data["verification_uri"]
148+
user_code = data["user_code"]
149+
150+
# prompt the user to enter the code
151+
print(
152+
"To authorize this app, go to {} and enter the code {}".format(
153+
uri, user_code
154+
)
155+
)
156+
input("\nPress RETURN to continue after inputting the code successfully")
157+
158+
return device_code
159+
160+
161+
def _poll_for_status(client_id, device_code):
162+
"""Polls API to see if user entered the device code
163+
164+
This is the second step of the Device Flow. Returns an access token, and
165+
also writes the token to a file in the user's home directory.
166+
"""
167+
168+
header = {"Content-Type": "application/json", "Accept": "application/json"}
169+
payload = {
170+
"client_id": client_id,
171+
"device_code": device_code,
172+
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
173+
}
174+
r = requests.post(
175+
"https://github.com/login/oauth/access_token",
176+
headers=header,
177+
json=payload,
178+
)
179+
180+
data = r.json()
181+
access_token = data["access_token"]
182+
set_github_auth({"access_token": access_token})
183+
return access_token

abcclassroom/git.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""
2-
abc-classroom.github
3-
====================
2+
abc-classroom.git
3+
=================
44
"""
55

66
# Methods for command line git operations. See github.py for

abcclassroom/github.py

Lines changed: 3 additions & 177 deletions
Original file line numberDiff line numberDiff line change
@@ -3,186 +3,12 @@
33
====================
44
"""
55

6-
# Methods for setting up and accessing the GitHub API. See
7-
# git.py for command line git operations.
6+
# Methods for accessing the GitHub API. See auth.py for methods
7+
# related to setting up authorization and git.py
8+
# for command line git operations.
89

9-
import requests
10-
11-
import os.path as op
12-
from ruamel.yaml import YAML
1310
import github3 as gh3
1411

15-
from .utils import get_request
16-
17-
18-
def get_github_auth():
19-
"""
20-
Check to see if there is an existing github authentication
21-
and load the authentication.
22-
23-
Returns
24-
-------
25-
ruamel.yaml.comments.CommentedMap
26-
Yaml object that contains the token and id for a github session.
27-
If yaml doesn't exists, return an empty dictionary.
28-
"""
29-
yaml = YAML()
30-
try:
31-
with open(op.expanduser("~/.abc-classroom.tokens.yml")) as f:
32-
config = yaml.load(f)
33-
return config["github"]
34-
35-
except FileNotFoundError:
36-
return {}
37-
38-
39-
def set_github_auth(auth_info):
40-
"""
41-
Set the github authentication information. Put the token and id
42-
authentication information into a yaml file if it doesn't already exist.
43-
44-
Parameters
45-
----------
46-
auth_info : dictionary
47-
The token and id authentication information from github stored in a
48-
dictionary object.
49-
"""
50-
yaml = YAML()
51-
config = {}
52-
if get_github_auth():
53-
with open(op.expanduser("~/.abc-classroom.tokens.yml")) as f:
54-
config = yaml.load(f)
55-
56-
config["github"] = auth_info
57-
58-
with open(op.expanduser("~/.abc-classroom.tokens.yml"), "w") as f:
59-
yaml.dump(config, f)
60-
61-
62-
def get_access_token():
63-
"""Get a GitHub access token for the API
64-
65-
First tries to read from local token file. If token does not exist,
66-
or is not valid, generates a new token using the OAuth Device Flow.
67-
https://docs.github.com/en/free-pro-team@latest/developers/apps/
68-
identifying-and-authorizing-users-for-github-apps#device-flow
69-
70-
Returns an access token (string).
71-
"""
72-
# first, we see if we have a saved token
73-
auth_info = get_github_auth()
74-
if auth_info:
75-
try:
76-
access_token = auth_info["access_token"]
77-
# if so, is it valid?
78-
user = _get_authenticated_user(access_token)
79-
if user is not None:
80-
print(
81-
"Access token is present and valid; successfully "
82-
"authenticated as user {}".format(user)
83-
)
84-
return access_token
85-
except KeyError:
86-
pass
87-
88-
# otherwise, generate a new token
89-
print("Generating new access token")
90-
# client id for the abc-classroom-bot GitHub App
91-
client_id = "Iv1.8df72ad9560c774c"
92-
93-
# TODO need to handle cases where the device call fails - wrong client_id,
94-
# the user could ^C or the internet could be out, or some other
95-
# unanticipated woe)
96-
device_code = _get_login_code(client_id)
97-
access_token = _poll_for_status(client_id, device_code)
98-
99-
# test the new access token
100-
user = _get_authenticated_user(access_token)
101-
if user is not None:
102-
print("""Successfully authenticated as user {}""".format(user))
103-
return access_token
104-
105-
106-
def _get_authenticated_user(token):
107-
"""Test the validity of an access token.
108-
109-
Given a github access token, test that it is valid by making an
110-
API call to get the authenticated user.
111-
112-
Returns the GitHub username of the authenticated user if token valid,
113-
otherwise returns None.
114-
"""
115-
116-
url = "https://api.github.com/user"
117-
(status, body) = get_request(url, token)
118-
try:
119-
user = body["login"]
120-
return user
121-
except KeyError:
122-
return None
123-
124-
125-
def _get_login_code(client_id):
126-
"""Prompts the user to authorize abc-classroom-bot.
127-
128-
First part of the Device Flow workflow. Asks user to visit a URL and
129-
enter the provided code. Waits for user to hit RETURN to continue.
130-
Returns the device code.
131-
"""
132-
133-
# make the device call
134-
header = {"Content-Type": "application/json", "Accept": "application/json"}
135-
payload = {"client_id": client_id}
136-
link = "https://github.com/login/device/code"
137-
r = requests.post(link, headers=header, json=payload)
138-
139-
# process the response
140-
data = r.json()
141-
status = r.status_code
142-
if status != 200:
143-
# print the response if the call failed
144-
print(r.json())
145-
return None
146-
147-
device_code = data["device_code"]
148-
uri = data["verification_uri"]
149-
user_code = data["user_code"]
150-
151-
# prompt the user to enter the code
152-
print(
153-
"To authorize this app, go to {} and enter the code {}".format(
154-
uri, user_code
155-
)
156-
)
157-
input("\nPress RETURN to continue after inputting the code successfully")
158-
159-
return device_code
160-
161-
162-
def _poll_for_status(client_id, device_code):
163-
"""Polls API to see if user entered the device code
164-
165-
This is the second step of the Device Flow. Returns an access token, and
166-
also writes the token to a file in the user's home directory.
167-
"""
168-
169-
header = {"Content-Type": "application/json", "Accept": "application/json"}
170-
payload = {
171-
"client_id": client_id,
172-
"device_code": device_code,
173-
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
174-
}
175-
r = requests.post(
176-
"https://github.com/login/oauth/access_token",
177-
headers=header,
178-
json=payload,
179-
)
180-
181-
data = r.json()
182-
access_token = data["access_token"]
183-
set_github_auth({"access_token": access_token})
184-
return access_token
185-
18612

18713
def remote_repo_exists(org, repository, token=None):
18814
"""Check if the remote repository exists for the organization."""

0 commit comments

Comments
 (0)