-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #96 from eosnetworkfoundation/ehp/auth_flow
Enable Authentication and Access Control Via OAuth and Team Membership
- Loading branch information
Showing
22 changed files
with
607 additions
and
271 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
client_id=11111111111111111111 | ||
scope=read:org | ||
client_secret=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | ||
authorize_url=https://github.com/login/oauth/authorize | ||
registered_callback=https://example.com/oauthback | ||
access_token=https://github.com/login/oauth/access_token | ||
user_info_url=https://api.github.com/user | ||
team=ORG/TEAM_1, ORG/TEAM_2 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
"""Module writes and puts env vars""" | ||
import sys | ||
import os | ||
|
||
class EnvStore(): | ||
"""Class to manage env vars""" | ||
|
||
def __init__(self, file): | ||
"""initialize from file""" | ||
self.env_name_values = {} | ||
if os.path.exists(file): | ||
with open('env', 'r', encoding='utf-8') as properties_file: | ||
# Read and parse each line into a list of tuples (name, value) | ||
for line in properties_file: | ||
line = line.strip() | ||
# Skip empty lines | ||
if not line: | ||
continue | ||
# Assuming the line format is "name=value" or "name:value" | ||
if '=' in line: | ||
name, value = line.split('=', 1) | ||
elif ':' in line: | ||
name, value = line.split(':', 1) | ||
else: | ||
print(f"Line format in env file not recognized: {line}") | ||
continue | ||
self.env_name_values[name.strip()] = value.strip() | ||
else: | ||
sys.exit(f"Can't find file {file} in current directory, not able to parse env properties, exiting.") | ||
|
||
def get(self, key): | ||
"""get values""" | ||
return self.env_name_values[key] | ||
|
||
def has(self,key): | ||
"""false if key not set or no value; otherwise true""" | ||
if key not in self.env_name_values or not self.env_name_values[key]: | ||
return False | ||
return True | ||
|
||
def set(self, key, value): | ||
"""set values""" | ||
self.env_name_values[key] = value | ||
|
||
def set_default(self, key, default_value): | ||
"""if key does not exist nor has previous value set""" | ||
if key not in self.env_name_values or not self.env_name_values[key]: | ||
self.env_name_values[key] = default_value |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
"""modules to support oauth functions""" | ||
import json | ||
import requests | ||
|
||
class GitHubOauth(): | ||
"""helper functions to manage git hub oauth""" | ||
|
||
@staticmethod | ||
def assemble_oauth_url(state, properties): | ||
"""assemble url for initial oauth request""" | ||
return properties.get('authorize_url') \ | ||
+ f"?client_id={properties.get('client_id')}" \ | ||
+ f"&redirect_uri={properties.get('registered_callback')}" \ | ||
+ f"&scope={properties.get('scope')}" \ | ||
+ f"&state={state}" \ | ||
+ f"&allow_signup=false" | ||
|
||
@staticmethod | ||
def get_oauth_access_token(code, properties): | ||
"""build url for the get token request""" | ||
# construct http call to exchange tempory code for access token | ||
params = { | ||
'client_id': properties.get('client_id'), | ||
'client_secret': properties.get('client_secret'), | ||
'code': code, | ||
'redirect_uri': properties.get('registered_callback') | ||
} | ||
# make post call to do exchange | ||
exchange_response = requests.post(properties.get('access_token'), | ||
params=params, | ||
timeout=3, | ||
headers={ | ||
'Accept': 'application/json', | ||
'Content-Type': 'application/json' | ||
}) | ||
# if good get the token otherwise fail | ||
# returns following params access_token, scope, token_type | ||
if exchange_response.status_code == 200: | ||
exchange_data = json.loads(exchange_response.content.decode('utf-8')) | ||
return exchange_data['access_token'] | ||
return None | ||
|
||
@staticmethod | ||
def create_auth_string(bearer_token, user_info_url): | ||
"""get public profile information using token""" | ||
# https request to get public profile data, login and avatar_url | ||
user_avatar_response = requests.get(user_info_url, | ||
timeout=3, | ||
headers={ | ||
'Accept': 'application/vnd.github+json', | ||
'Authorization': f'Bearer {bearer_token}', | ||
'X-GitHub-Api-Version': '2022-11-28' | ||
}) | ||
if user_avatar_response.status_code == 200: | ||
user_data = json.loads(user_avatar_response.content.decode('utf-8')) | ||
return GitHubOauth.credentials_to_str(user_data['login'],user_data['avatar_url'],bearer_token) | ||
return None | ||
|
||
@staticmethod | ||
def check_membership(bearer_token, login, team_string): | ||
"""Check for team membership""" | ||
if not login: | ||
return False | ||
# many contain many teams | ||
for unit in team_string.split(','): | ||
org, team = unit.split('/',1) | ||
org = org.strip() | ||
team = team.strip() | ||
url = f'https://api.github.com/orgs/{org}/teams/{team}/members' | ||
membership_check = requests.get(url, | ||
timeout=3, | ||
headers={ | ||
'Accept': 'application/vnd.github+json', | ||
'Authorization': f'Bearer {bearer_token}', | ||
'X-GitHub-Api-Version': '2022-11-28', | ||
'User-Agent': 'App/OAuth/ReplayTest' | ||
}) | ||
if membership_check.status_code == 200: | ||
members_list = json.loads(membership_check.content.decode('utf-8')) | ||
for member in members_list: | ||
if member['login'] == login: | ||
return True | ||
return False | ||
|
||
@staticmethod | ||
def is_authorized(cookies, header_token, user_info_url, team_string): | ||
"""check for authorized token or cookie""" | ||
|
||
token = None | ||
if 'replay_auth' in cookies and cookies['replay_auth']: | ||
token = GitHubOauth.extract_token(cookies['replay_auth']) | ||
elif header_token: | ||
token = header_token.replace("Bearer ","") | ||
if not token: | ||
return False | ||
|
||
auth_string = GitHubOauth.create_auth_string(token, user_info_url) | ||
login = GitHubOauth.extract_login(auth_string) | ||
return GitHubOauth.check_membership(token, login, team_string) | ||
|
||
@staticmethod | ||
def credentials_to_str(login, avatar_url, token): | ||
"""converts profile data to string sep by :""" | ||
return token + ":" + login + ":" + avatar_url | ||
|
||
@staticmethod | ||
def str_to_public_profile(data): | ||
"""converts str to array of profile data""" | ||
if not data: | ||
return [] | ||
# return public profile data leaving off bearer token | ||
return data.split(':', 2)[1:] | ||
|
||
@staticmethod | ||
def extract_token(data): | ||
"""grabs the bearer token from string""" | ||
if not data: | ||
return [] | ||
return data.split(':', 2)[0] | ||
|
||
@staticmethod | ||
def extract_login(data): | ||
"""grabs the bearer token from string""" | ||
if not data: | ||
return [] | ||
return data.split(':', 2)[1] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
"""modules for assembling HTML pages from files""" | ||
|
||
class HtmlPage: | ||
"""class for assembling HTML pages from files""" | ||
def __init__(self, html_dir): | ||
self.html_dir = html_dir | ||
if not self.html_dir.endswith('/'): | ||
self.html_dir = self.html_dir + '/' | ||
|
||
def contents(self, file_name="progress.html"): | ||
"""Return contents of html files""" | ||
|
||
if file_name == '/progress': | ||
file_name = 'progress.html' | ||
elif file_name == '/grid': | ||
file_name = 'grid.html' | ||
elif file_name == '/control': | ||
file_name = 'control.html' | ||
elif file_name == '/detail': | ||
file_name = 'detail.html' | ||
|
||
file_path = self.html_dir + file_name | ||
with open(file_path, 'r', encoding='utf-8') as file: | ||
# Read the file's contents into a string | ||
file_contents = file.read() | ||
return file_contents | ||
|
||
def profile_top_bar_html(self, login, avatar_url): | ||
"""return top bar with profile""" | ||
return f''' <div class="topbar"> | ||
<a href="/logout"> | ||
<span class="material-symbols-outlined"> | ||
<img src="{avatar_url}"> | ||
</span> | ||
</a> | ||
<p>{login}</p> | ||
</div>''' | ||
|
||
def default_top_bar_html(self, oauth_url): | ||
"""return top bar with no profile""" | ||
return f''' <div class="topbar"> <a href="{oauth_url}"> | ||
<span class="material-symbols-outlined">account_circle</span> | ||
</a> </div>''' | ||
|
||
def not_authorized(self, message="Not Authorized: Please Log In"): | ||
"""return not autorized page contents""" | ||
return f''' <div class="maincontent"> | ||
<h2>{message}</h2> | ||
</div>''' |
Oops, something went wrong.