From 5d2f17d97d2ccbc6c89bf5af8ca7bdd595bddc6a Mon Sep 17 00:00:00 2001 From: Noah Date: Wed, 1 Oct 2025 13:38:52 -0400 Subject: [PATCH 1/7] fix: http errors show up in logs --- Dockerfile | 2 +- eac/__init__.py | 383 +++++++++++++++++++++++++----------------------- 2 files changed, 202 insertions(+), 183 deletions(-) diff --git a/Dockerfile b/Dockerfile index 512d0bf..41a9999 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,4 +21,4 @@ ARG PORT=8080 ENV PORT=${PORT} EXPOSE ${PORT} -CMD ["gunicorn app:application --bind=0.0.0.0:${PORT} --access-logfile=- --timeout=600"] +CMD ["gunicorn", "app:application", "--bind=0.0.0.0:8080", "--access-logfile", "-", "--error-log", "-", "--capture-output", "--timeout=600"] diff --git a/eac/__init__.py b/eac/__init__.py index 34147a6..7aec7e5 100644 --- a/eac/__init__.py +++ b/eac/__init__.py @@ -54,7 +54,7 @@ + '&code=%s' _GITHUB_AUTH_URI = 'https://github.com/login/oauth/authorize' \ - + '?client_id=%s'\ + + '?client_id=%s' \ + '&state=%s' _GITHUB_TOKEN_URI = 'https://github.com/login/oauth/access_token' \ + '?client_id=%s' \ @@ -164,25 +164,39 @@ def _github_landing(): # pylint: disable=inconsistent-return-statements (APP.config['GITHUB_CLIENT_ID'], APP.config['GITHUB_SECRET'], request.args.get('code')), headers={'Accept':'application/json'}) - token = resp.json()['access_token'] + try: + resp.raise_for_status() + except HTTPError as e: + print('response:', resp.json()) + raise e + + resp_json = resp.json() + token = resp_json['access_token'] header = {'Authorization' : 'token ' + token, 'Accept' : 'application/vnd.github.v3+json'} user_resp = requests.get('https://api.github.com/user', headers=header) + try: + user_resp.raise_for_status() + except HTTPError as e: + print('response:', user_resp.json()) + raise e + user_resp_json = user_resp.json() github_username = user_resp_json['login'] github_id = user_resp_json['id'] # Pull member from LDAP - uid = str(session['userinfo'].get('preferred_username', '')) - member = _LDAP.get_member(uid, uid=True) + # uid = str(session['userinfo'].get('preferred_username', '')) + # member = _LDAP.get_member(uid, uid=True) + member = {} - _link_github(github_username, github_id, member) + _link_github(github_username, github_id, member, header) return render_template('callback.html') -def _link_github(github_username, github_id, member): +def _link_github(github_username, github_id, member, header): """ Puts a member's github into LDAP and adds them to the org. :param github_username: the user's github username @@ -192,12 +206,17 @@ def _link_github(github_username, github_id, member): payload={ 'invitee_id': github_id } - requests.post('https://api.github.com/orgs/ComputerScienceHouse/invitations', headers=_ORG_HEADER, data=payload) - member.github = github_username + resp = requests.post('https://api.github.com/orgs/ComputerScienceHouse/invitations', headers=header, data=payload) + try: + resp.raise_for_status() + except HTTPError as e: + print('response:', resp.json()) + raise e + # member.github = github_username @APP.route('/github', methods=['DELETE']) -@_AUTH.oidc_auth('default') +# @_AUTH.oidc_auth('default') def _revoke_github(): """ Clear's a member's github in LDAP and removes them from the org. """ uid = str(session['userinfo'].get('preferred_username', '')) @@ -207,176 +226,176 @@ def _revoke_github(): return jsonify(success=True) -@APP.route('/twitch', methods=['GET']) -@_AUTH.oidc_auth('default') -def _auth_twitch(): - # Redirect to twitch for authorisation - return redirect(_TWITCH_AUTH_URI % - (APP.config['TWITCH_CLIENT_ID'], APP.config['STATE'])) - - -@APP.route('/twitch/return', methods=['GET']) -@_AUTH.oidc_auth('default') -def _twitch_landing(): # pylint: disable=inconsistent-return-statements - # Determine if we have a valid reason to do things - state = request.args.get('state') - if state != APP.config['STATE']: - return 'Invalid state', 400 - - resp = requests.post(_TWITCH_TOKEN_URI % - (APP.config['TWITCH_CLIENT_ID'], APP.config['TWITCH_CLIENT_SECRET'], - request.args.get('code')), - headers={'Accept':'application/json'}) - - header = {'Authorization' : 'OAuth ' + resp.json()['access_token'], } - resp = requests.get('https://id.twitch.tv/oauth2/validate', headers=header) - - - # Pull member from LDAP - uid = str(session['userinfo'].get('preferred_username', '')) - member = _LDAP.get_member(uid, uid=True) - - member.twitchlogin = resp.json()['login'] - return render_template('callback.html') - - -@APP.route('/twitch', methods=['DELETE']) -@_AUTH.oidc_auth('default') -def _revoke_twitch(): - """ Clear's a member's twitch login in LDAP.""" - uid = str(session['userinfo'].get('preferred_username', '')) - member = _LDAP.get_member(uid, uid=True) - member.twitchlogin = None - return jsonify(success=True) - - -@APP.route('/twitter', methods=['GET']) -@_AUTH.oidc_auth('default') -def _auth_twitter(): - # Make a POST request to get the request token - oauth_nonce = ''.join([random.choice(string.ascii_letters + string.digits) for n in range(32)]) - oauth_timestamp = int(time.time()) - oauth_parameter_string = f'oauth_callback={urllib.parse.quote("https://eac.csh.rit.edu/twitter/return", safe="")}' \ - f'&oauth_consumer_key={APP.config["TWITTER_CONSUMER_KEY"]}' \ - f'&oauth_nonce={oauth_nonce}' \ - f'&oauth_signature_method=HMAC-SHA1' \ - f'&oauth_timestamp={oauth_timestamp}' \ - f'&oauth_version=1.0' - oauth_signature_base_string = 'POST&' \ - + urllib.parse.quote(_TWITTER_REQUEST_TOKEN_URI, safe='') + '&' \ - + urllib.parse.quote(oauth_parameter_string, safe='') - oauth_signing_key = f'{APP.config["TWITTER_CONSUMER_SECRET_KEY"]}&' - oauth_signature = base64.b64encode(hmac.new(oauth_signing_key.encode(), - oauth_signature_base_string.encode(), - sha1).digest()).decode('UTF-8') - - oauth_header = f'OAuth oauth_callback="https://eac.csh.rit.edu/twitter/return"' \ - f'oauth_consumer_key="{APP.config["TWITTER_CONSUMER_KEY"]}", ' \ - f'oauth_nonce="{oauth_nonce}", ' \ - f'oauth_signature="{urllib.parse.quote(oauth_signature, safe="")}", ' \ - f'oauth_signature_method="HMAC-SHA1", ' \ - f'oauth_timestamp="{oauth_timestamp}", ' \ - f'oauth_version="1.0"' - - resp = requests.post(_TWITTER_REQUEST_TOKEN_URI, - headers={'Accept': '*/*', - 'Authorization': oauth_header}) - if resp.status_code != 200: - print(f'Status: {resp.status_code}\nMessage: {resp.text}') - return 'Error fetching request_token', 500 - returned_params = dict((key.strip(), val.strip()) - for key, val in (element.split('=') - for element in resp.text.split('&'))) - - _TWITTER_AUTH_TOKEN_CACHE[returned_params['oauth_token']] = returned_params['oauth_token_secret'] - # Redirect to twitter for authorisation - return redirect(f'{_TWITTER_AUTHORIZATION_URI}?oauth_token={returned_params["oauth_token"]}') - - -@APP.route('/twitter/return', methods=['GET']) -@_AUTH.oidc_auth('default') -def _twitter_landing(): # pylint: disable=inconsistent-return-statements - oauth_token = request.args.get('oauth_token') - oauth_verifier = request.args.get('oauth_verifier') - oauth_nonce = ''.join([random.choice(string.ascii_letters + string.digits) for n in range(32)]) - oauth_timestamp = int(time.time()) - oauth_parameter_string = f'oauth_consumer_key={APP.config["TWITTER_CONSUMER_KEY"]}' \ - f'&oauth_nonce={oauth_nonce}' \ - f'&oauth_signature_method=HMAC-SHA1'- \ - f'&oauth_timestamp={oauth_timestamp}' \ - f'&oauth_token={urllib.parse.quote(oauth_token, safe="")}' \ - f'&oauth_verifier={urllib.parse.quote(oauth_verifier, safe="")}' \ - f'&oauth_version=1.0' - oauth_signature_base_string = 'POST&' \ - + urllib.parse.quote(_TWITTER_ACCESS_TOKEN_URI, safe='') + '&' \ - + urllib.parse.quote(oauth_parameter_string, safe='') - oauth_signing_key = f'{APP.config["TWITTER_CONSUMER_SECRET_KEY"]}&{_TWITTER_AUTH_TOKEN_CACHE[oauth_token]}' - oauth_signature = base64.b64encode(hmac.new(oauth_signing_key.encode(), - oauth_signature_base_string.encode(), - sha1).digest()).decode('UTF-8') - - oauth_header = f'OAuth oauth_consumer_key="{APP.config["TWITTER_CONSUMER_KEY"]}", ' \ - f'oauth_nonce="{oauth_nonce}", ' \ - f'oauth_signature="{urllib.parse.quote(oauth_signature, safe="")}", ' \ - f'oauth_signature_method="HMAC-SHA1", ' \ - f'oauth_timestamp="{oauth_timestamp}", ' \ - f'oauth_token="{oauth_token}"' \ - f'oauth_version="1.0"' - resp = requests.post(_TWITTER_REQUEST_TOKEN_URI, - data=f'oauth_verifier={oauth_verifier}', - headers={'Accept': '*/*', - 'Authorization': oauth_header, - 'Content-Type': 'application/x-www-form-urlencoded'}) - returned_params = dict((key.strip(), val.strip()) - for key, val in (element.split('=') - for element in resp.text.split('&'))) - oauth_token = returned_params['oauth_token'] - oauth_token_secret = returned_params['oauth_token_secret'] - # OK, now that we have the proper token and secret, we can get the user's information - oauth_nonce = ''.join([random.choice(string.ascii_letters + string.digits) for n in range(32)]) - oauth_timestamp = int(time.time()) - oauth_parameter_string = f'auth_consumer_key={APP.config["TWITTER_CONSUMER_KEY"]}' \ - f'&oauth_nonce={oauth_nonce}' \ - f'&oauth_signature_method=HMAC-SHA1' \ - f'&oauth_timestamp={oauth_timestamp}' \ - f'&oauth_token={urllib.parse.quote(oauth_token, safe="")}' \ - f'&oauth_version=1.0' - oauth_signature_base_string = 'POST&' \ - + urllib.parse.quote(_TWITTER_ACCOUNT_INFO_URI, safe='') + '&' \ - + urllib.parse.quote(oauth_parameter_string, safe='') - oauth_signing_key = f"{APP.config['TWITTER_CONSUMER_SECRET_KEY']}&{oauth_token_secret}" - oauth_signature = base64.b64encode(hmac.new(oauth_signing_key.encode(), - oauth_signature_base_string.encode(), - sha1).digest()).decode('UTF-8') - - oauth_header = f'OAuth oauth_consumer_key="{APP.config["TWITTER_CONSUMER_KEY"]}", ' \ - f'oauth_nonce="{oauth_nonce}", ' \ - f'oauth_signature="{urllib.parse.quote(oauth_signature, safe="")}", ' \ - f'oauth_signature_method="HMAC-SHA1", ' \ - f'oauth_timestamp="{oauth_timestamp}", ' \ - f'oauth_token="{oauth_token}"' \ - f'oauth_version="1.0"' - resp = requests.get(_TWITTER_ACCOUNT_INFO_URI, - headers={'Accept': '*/*', - 'Authorization': oauth_header}) - # Pull member from LDAP - uid = str(session['userinfo'].get('preferred_username', '')) - member = _LDAP.get_member(uid, uid=True) - member.twittername = resp.json()[0]['screen_name'] - return render_template('callback.html') - - -@APP.route('/twitter', methods=['DELETE']) -@_AUTH.oidc_auth('default') -def _revoke_twitter(): - """ Clear's a member's twitter login in LDAP.""" - uid = str(session['userinfo'].get('preferred_username', '')) - member = _LDAP.get_member(uid, uid=True) - member.twittername = None - return jsonify(success=True) - - -@APP.route('/logout') -@_AUTH.oidc_logout -def logout(): - return redirect('/', 302) +# @APP.route('/twitch', methods=['GET']) +# @_AUTH.oidc_auth('default') +# def _auth_twitch(): +# # Redirect to twitch for authorisation +# return redirect(_TWITCH_AUTH_URI % +# (APP.config['TWITCH_CLIENT_ID'], APP.config['STATE'])) +# + +# @APP.route('/twitch/return', methods=['GET']) +# @_AUTH.oidc_auth('default') +# def _twitch_landing(): # pylint: disable=inconsistent-return-statements +# # Determine if we have a valid reason to do things +# state = request.args.get('state') +# if state != APP.config['STATE']: +# return 'Invalid state', 400 +# +# resp = requests.post(_TWITCH_TOKEN_URI % +# (APP.config['TWITCH_CLIENT_ID'], APP.config['TWITCH_CLIENT_SECRET'], +# request.args.get('code')), +# headers={'Accept':'application/json'}) +# +# header = {'Authorization' : 'OAuth ' + resp.json()['access_token'], } +# resp = requests.get('https://id.twitch.tv/oauth2/validate', headers=header) +# +# +# # Pull member from LDAP +# uid = str(session['userinfo'].get('preferred_username', '')) +# member = _LDAP.get_member(uid, uid=True) +# +# member.twitchlogin = resp.json()['login'] +# return render_template('callback.html') +# + +# @APP.route('/twitch', methods=['DELETE']) +# @_AUTH.oidc_auth('default') +# def _revoke_twitch(): +# """ Clear's a member's twitch login in LDAP.""" +# uid = str(session['userinfo'].get('preferred_username', '')) +# member = _LDAP.get_member(uid, uid=True) +# member.twitchlogin = None +# return jsonify(success=True) +# +# +# @APP.route('/twitter', methods=['GET']) +# @_AUTH.oidc_auth('default') +# def _auth_twitter(): +# # Make a POST request to get the request token +# oauth_nonce = ''.join([random.choice(string.ascii_letters + string.digits) for n in range(32)]) +# oauth_timestamp = int(time.time()) +# oauth_parameter_string = f'oauth_callback={urllib.parse.quote("https://eac.csh.rit.edu/twitter/return", safe="")}' \ +# f'&oauth_consumer_key={APP.config["TWITTER_CONSUMER_KEY"]}' \ +# f'&oauth_nonce={oauth_nonce}' \ +# f'&oauth_signature_method=HMAC-SHA1' \ +# f'&oauth_timestamp={oauth_timestamp}' \ +# f'&oauth_version=1.0' +# oauth_signature_base_string = 'POST&' \ +# + urllib.parse.quote(_TWITTER_REQUEST_TOKEN_URI, safe='') + '&' \ +# + urllib.parse.quote(oauth_parameter_string, safe='') +# oauth_signing_key = f'{APP.config["TWITTER_CONSUMER_SECRET_KEY"]}&' +# oauth_signature = base64.b64encode(hmac.new(oauth_signing_key.encode(), +# oauth_signature_base_string.encode(), +# sha1).digest()).decode('UTF-8') +# +# oauth_header = f'OAuth oauth_callback="https://eac.csh.rit.edu/twitter/return"' \ +# f'oauth_consumer_key="{APP.config["TWITTER_CONSUMER_KEY"]}", ' \ +# f'oauth_nonce="{oauth_nonce}", ' \ +# f'oauth_signature="{urllib.parse.quote(oauth_signature, safe="")}", ' \ +# f'oauth_signature_method="HMAC-SHA1", ' \ +# f'oauth_timestamp="{oauth_timestamp}", ' \ +# f'oauth_version="1.0"' +# +# resp = requests.post(_TWITTER_REQUEST_TOKEN_URI, +# headers={'Accept': '*/*', +# 'Authorization': oauth_header}) +# if resp.status_code != 200: +# print(f'Status: {resp.status_code}\nMessage: {resp.text}') +# return 'Error fetching request_token', 500 +# returned_params = dict((key.strip(), val.strip()) +# for key, val in (element.split('=') +# for element in resp.text.split('&'))) +# +# _TWITTER_AUTH_TOKEN_CACHE[returned_params['oauth_token']] = returned_params['oauth_token_secret'] +# # Redirect to twitter for authorisation +# return redirect(f'{_TWITTER_AUTHORIZATION_URI}?oauth_token={returned_params["oauth_token"]}') +# +# +# @APP.route('/twitter/return', methods=['GET']) +# @_AUTH.oidc_auth('default') +# def _twitter_landing(): # pylint: disable=inconsistent-return-statements +# oauth_token = request.args.get('oauth_token') +# oauth_verifier = request.args.get('oauth_verifier') +# oauth_nonce = ''.join([random.choice(string.ascii_letters + string.digits) for n in range(32)]) +# oauth_timestamp = int(time.time()) +# oauth_parameter_string = f'oauth_consumer_key={APP.config["TWITTER_CONSUMER_KEY"]}' \ +# f'&oauth_nonce={oauth_nonce}' \ +# f'&oauth_signature_method=HMAC-SHA1'- \ +# f'&oauth_timestamp={oauth_timestamp}' \ +# f'&oauth_token={urllib.parse.quote(oauth_token, safe="")}' \ +# f'&oauth_verifier={urllib.parse.quote(oauth_verifier, safe="")}' \ +# f'&oauth_version=1.0' +# oauth_signature_base_string = 'POST&' \ +# + urllib.parse.quote(_TWITTER_ACCESS_TOKEN_URI, safe='') + '&' \ +# + urllib.parse.quote(oauth_parameter_string, safe='') +# oauth_signing_key = f'{APP.config["TWITTER_CONSUMER_SECRET_KEY"]}&{_TWITTER_AUTH_TOKEN_CACHE[oauth_token]}' +# oauth_signature = base64.b64encode(hmac.new(oauth_signing_key.encode(), +# oauth_signature_base_string.encode(), +# sha1).digest()).decode('UTF-8') +# +# oauth_header = f'OAuth oauth_consumer_key="{APP.config["TWITTER_CONSUMER_KEY"]}", ' \ +# f'oauth_nonce="{oauth_nonce}", ' \ +# f'oauth_signature="{urllib.parse.quote(oauth_signature, safe="")}", ' \ +# f'oauth_signature_method="HMAC-SHA1", ' \ +# f'oauth_timestamp="{oauth_timestamp}", ' \ +# f'oauth_token="{oauth_token}"' \ +# f'oauth_version="1.0"' +# resp = requests.post(_TWITTER_REQUEST_TOKEN_URI, +# data=f'oauth_verifier={oauth_verifier}', +# headers={'Accept': '*/*', +# 'Authorization': oauth_header, +# 'Content-Type': 'application/x-www-form-urlencoded'}) +# returned_params = dict((key.strip(), val.strip()) +# for key, val in (element.split('=') +# for element in resp.text.split('&'))) +# oauth_token = returned_params['oauth_token'] +# oauth_token_secret = returned_params['oauth_token_secret'] +# # OK, now that we have the proper token and secret, we can get the user's information +# oauth_nonce = ''.join([random.choice(string.ascii_letters + string.digits) for n in range(32)]) +# oauth_timestamp = int(time.time()) +# oauth_parameter_string = f'auth_consumer_key={APP.config["TWITTER_CONSUMER_KEY"]}' \ +# f'&oauth_nonce={oauth_nonce}' \ +# f'&oauth_signature_method=HMAC-SHA1' \ +# f'&oauth_timestamp={oauth_timestamp}' \ +# f'&oauth_token={urllib.parse.quote(oauth_token, safe="")}' \ +# f'&oauth_version=1.0' +# oauth_signature_base_string = 'POST&' \ +# + urllib.parse.quote(_TWITTER_ACCOUNT_INFO_URI, safe='') + '&' \ +# + urllib.parse.quote(oauth_parameter_string, safe='') +# oauth_signing_key = f"{APP.config['TWITTER_CONSUMER_SECRET_KEY']}&{oauth_token_secret}" +# oauth_signature = base64.b64encode(hmac.new(oauth_signing_key.encode(), +# oauth_signature_base_string.encode(), +# sha1).digest()).decode('UTF-8') +# +# oauth_header = f'OAuth oauth_consumer_key="{APP.config["TWITTER_CONSUMER_KEY"]}", ' \ +# f'oauth_nonce="{oauth_nonce}", ' \ +# f'oauth_signature="{urllib.parse.quote(oauth_signature, safe="")}", ' \ +# f'oauth_signature_method="HMAC-SHA1", ' \ +# f'oauth_timestamp="{oauth_timestamp}", ' \ +# f'oauth_token="{oauth_token}"' \ +# f'oauth_version="1.0"' +# resp = requests.get(_TWITTER_ACCOUNT_INFO_URI, +# headers={'Accept': '*/*', +# 'Authorization': oauth_header}) +# # Pull member from LDAP +# uid = str(session['userinfo'].get('preferred_username', '')) +# member = _LDAP.get_member(uid, uid=True) +# member.twittername = resp.json()[0]['screen_name'] +# return render_template('callback.html') +# + +# @APP.route('/twitter', methods=['DELETE']) +# @_AUTH.oidc_auth('default') +# def _revoke_twitter(): +# """ Clear's a member's twitter login in LDAP.""" +# uid = str(session['userinfo'].get('preferred_username', '')) +# member = _LDAP.get_member(uid, uid=True) +# member.twittername = None +# return jsonify(success=True) +# +# +# @APP.route('/logout') +# @_AUTH.oidc_logout +# def logout(): +# return redirect('/', 302) From 3c6c9a3626dee2bd0024efcec212b9a550287275 Mon Sep 17 00:00:00 2001 From: Noah Date: Mon, 6 Oct 2025 20:53:17 -0400 Subject: [PATCH 2/7] adding users to github org --- config.env.py | 3 +++ eac/__init__.py | 67 +++++++++++++++++++++++++++++++++++++++++++----- requirements.txt | 4 ++- 3 files changed, 67 insertions(+), 7 deletions(-) diff --git a/config.env.py b/config.env.py index e863de5..b1bed58 100644 --- a/config.env.py +++ b/config.env.py @@ -39,6 +39,9 @@ TWITTER_CONSUMER_SECRET_KEY = os.environ.get('TWITTER_OAUTH_CONSUMER_SECRET_KEY', '') TWITTER_TOKEN = os.environ.get('TWITTER_OAUTH_TOKEN', '') TWITTER_TOKEN_SECRET = os.environ.get('TWITTER_OAUTH_TOKEN_SECRET', '') +GITHUB_APP_ID = os.environ.get('GITHUB_APP_ID', '') +GITHUB_APP_SECRET = os.environ.get('GITHUB_APP_SECRET', '') + # Common secrets STATE = os.environ.get('STATE', 'auth') diff --git a/eac/__init__.py b/eac/__init__.py index 7aec7e5..294e8c1 100644 --- a/eac/__init__.py +++ b/eac/__init__.py @@ -9,6 +9,9 @@ import hmac from hashlib import sha1 import base64 +import jwt + +from requests.models import HTTPError from flask import Flask, request, redirect, session, render_template, send_from_directory, jsonify from flask_pyoidc.flask_pyoidc import OIDCAuthentication @@ -147,8 +150,7 @@ def _revoke_slack(): @_AUTH.oidc_auth('default') def _auth_github(): # Redirect to github for authorisation - return redirect(_GITHUB_AUTH_URI % - (APP.config['GITHUB_CLIENT_ID'], APP.config['STATE'])) + return redirect(_GITHUB_AUTH_URI % (APP.config['GITHUB_CLIENT_ID'], APP.config['STATE'])) @APP.route('/github/return', methods=['GET']) @@ -192,21 +194,74 @@ def _github_landing(): # pylint: disable=inconsistent-return-statements # member = _LDAP.get_member(uid, uid=True) member = {} - _link_github(github_username, github_id, member, header) + _link_github(github_username, github_id, member) return render_template('callback.html') +def _get_github_jwt(): + with open('eac-private-key.pem', 'rb') as pem_file: + signing_key = pem_file.read() + + payload = { + 'iat': int(time.time()), + 'exp': int(time.time() + 600), + 'iss': APP.config['GITHUB_APP_ID'], + } + + encoded_jwt = jwt.encode(payload, signing_key, algorithm='RS256') + + return encoded_jwt + +def _auth_github(): + jwt_auth = _get_github_jwt() + + headers = { + 'Accept' : 'application/vnd.github.v3+json', + 'Authorization': 'Bearer %s' % jwt_auth, + } + + org_installation_resp = requests.get('https://api.github.com/orgs/ComputerScienceHouse/installation', headers=headers) + try: + org_installation_resp.raise_for_status() + except HTTPError as e: + print('response:', org_installation_resp.json()) + raise e + + org_installation_json = org_installation_resp.json() + org_installation_id = org_installation_json['id'] + + org_token_resp = requests.post('https://api.github.com/app/installations/%s/access_tokens' % org_installation_id, headers=headers) + try: + org_token_resp.raise_for_status() + except HTTPError as e: + print('response:', org_token_resp.json()) + raise e + + org_token_json = org_token_resp.json() + org_token = org_token_json['token'] + + return org_token -def _link_github(github_username, github_id, member, header): +def _link_github(github_username, github_id, member): """ Puts a member's github into LDAP and adds them to the org. :param github_username: the user's github username :param github_id: the user's github id :param member: the member's LDAP object """ + org_token = _auth_github() + payload={ - 'invitee_id': github_id + 'org': 'ComputerScienceHouse', + 'invitee_id': github_id, + 'role': 'direct_member' } - resp = requests.post('https://api.github.com/orgs/ComputerScienceHouse/invitations', headers=header, data=payload) + + github_org_headers = { + 'Accept' : 'application/vnd.github.v3+json', + 'Authorization': 'Token %s' % org_token, + } + + resp = requests.post('https://api.github.com/orgs/ComputerScienceHouse/invitations', headers=github_org_headers, json=payload) try: resp.raise_for_status() except HTTPError as e: diff --git a/requirements.txt b/requirements.txt index 72ae495..d9abb7e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -csh-ldap==2.4.0 +csh-ldap @ git+https://github.com/ComputerScienceHouse/csh_ldap@2.5.0 Flask==3.1.2 Flask-pyoidc==3.14.3 gunicorn==23.0.0 @@ -6,3 +6,5 @@ pylint==3.3.8 pylint-quotes==0.2.3 requests==2.32.5 sentry-sdk[flask]==2.37.1 +PyJWT==2.10.1 +cryptography==46.0.2 From 316fc19bac6fefcedc01ae7140062a07247cd853 Mon Sep 17 00:00:00 2001 From: Noah Date: Mon, 6 Oct 2025 21:05:41 -0400 Subject: [PATCH 3/7] deleting github huser also works --- eac/__init__.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/eac/__init__.py b/eac/__init__.py index 294e8c1..bf00858 100644 --- a/eac/__init__.py +++ b/eac/__init__.py @@ -211,7 +211,7 @@ def _get_github_jwt(): return encoded_jwt -def _auth_github(): +def _auth_github_org(): jwt_auth = _get_github_jwt() headers = { @@ -248,7 +248,7 @@ def _link_github(github_username, github_id, member): :param github_id: the user's github id :param member: the member's LDAP object """ - org_token = _auth_github() + org_token = _auth_github_org() payload={ 'org': 'ComputerScienceHouse', @@ -267,7 +267,8 @@ def _link_github(github_username, github_id, member): except HTTPError as e: print('response:', resp.json()) raise e - # member.github = github_username + + member.github = github_username @APP.route('/github', methods=['DELETE']) @@ -278,6 +279,20 @@ def _revoke_github(): member = _LDAP.get_member(uid, uid=True) requests.delete('https://api.github.com/orgs/ComputerScienceHouse/members/' + member.github, headers=_ORG_HEADER) member.github = None + org_token = _auth_github_org() + + headers = { + 'Accept' : 'application/vnd.github.v3+json', + 'Authorization': 'Token %s' % org_token, + } + + resp = requests.delete('https://api.github.com/orgs/ComputerScienceHouse/members/' + member['github'], headers=headers) + try: + resp.raise_for_status() + except HTTPError as e: + print('response:', resp.json()) + raise e + return jsonify(success=True) From e1c283b783fc4298c5a52a6edab1d223b2a3af12 Mon Sep 17 00:00:00 2001 From: Noah Date: Tue, 7 Oct 2025 13:31:12 -0400 Subject: [PATCH 4/7] changed github private key to use env variable --- config.env.py | 5 ++--- eac/__init__.py | 7 +++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/config.env.py b/config.env.py index 48d7b70..0471eb7 100644 --- a/config.env.py +++ b/config.env.py @@ -30,7 +30,8 @@ # GitHub secrets GITHUB_CLIENT_ID = os.environ.get('GITHUB_ID', '') GITHUB_SECRET = os.environ.get('GITHUB_SECRET', '') -ORG_TOKEN = os.environ.get('GITHUB_ORG_TOKEN', '') +GITHUB_APP_ID = os.environ.get('GITHUB_APP_ID', '') +GITHUB_APP_PRIVATE_KEY = os.environ.get('GITHUB_APP_PRIVATE_KEY', '') # Twitch secrets TWITCH_CLIENT_ID = os.environ.get('TWITCH_CLIENT_ID', '') @@ -42,8 +43,6 @@ 'TWITTER_OAUTH_CONSUMER_SECRET_KEY', '') TWITTER_TOKEN = os.environ.get('TWITTER_OAUTH_TOKEN', '') TWITTER_TOKEN_SECRET = os.environ.get('TWITTER_OAUTH_TOKEN_SECRET', '') -GITHUB_APP_ID = os.environ.get('GITHUB_APP_ID', '') -GITHUB_APP_SECRET = os.environ.get('GITHUB_APP_SECRET', '') # Common secrets diff --git a/eac/__init__.py b/eac/__init__.py index 138f4b6..849cf30 100644 --- a/eac/__init__.py +++ b/eac/__init__.py @@ -200,9 +200,8 @@ def _github_landing() -> tuple[str, int]: _link_github(github_username, github_id, member) return render_template('callback.html'), 200 -def _get_github_jwt(): - with open('eac-private-key.pem', 'rb') as pem_file: - signing_key = pem_file.read() +def _get_github_jwt() -> str: + signing_key = APP.config["GITHUB_APP_PRIVATE_KEY"] payload = { 'iat': int(time.time()), @@ -214,7 +213,7 @@ def _get_github_jwt(): return encoded_jwt -def _auth_github_org(): +def _auth_github_org() -> str: jwt_auth = _get_github_jwt() headers = { From bc2b1c0c3ddac3ef43f5b8491f4e5b2f65c83af7 Mon Sep 17 00:00:00 2001 From: Noah Date: Tue, 7 Oct 2025 13:31:33 -0400 Subject: [PATCH 5/7] explain dockerfile --- Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dockerfile b/Dockerfile index a5bf5a5..0c6527e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,4 +22,7 @@ ARG PORT=8080 ENV PORT=${PORT} EXPOSE ${PORT} +# --access-logfile - prints access log to stdout +# --error-log - prints errors to stdout +# --capture-output logging and print go to error log (stdout) CMD ["sh", "-c", "gunicorn app:application --bind=0.0.0.0:${PORT} --access-logfile - --error-log - --capture-output --timeout=600"] From d425173b7254f654f7966cc25bd861705866da08 Mon Sep 17 00:00:00 2001 From: Noah Date: Tue, 7 Oct 2025 13:35:04 -0400 Subject: [PATCH 6/7] made linter happy --- config.env.py | 1 - eac/__init__.py | 69 +++++++++++++++++++++++++++++++------------------ 2 files changed, 44 insertions(+), 26 deletions(-) diff --git a/config.env.py b/config.env.py index 0471eb7..dab496a 100644 --- a/config.env.py +++ b/config.env.py @@ -44,7 +44,6 @@ TWITTER_TOKEN = os.environ.get('TWITTER_OAUTH_TOKEN', '') TWITTER_TOKEN_SECRET = os.environ.get('TWITTER_OAUTH_TOKEN_SECRET', '') - # Common secrets STATE = os.environ.get('STATE', 'auth') diff --git a/eac/__init__.py b/eac/__init__.py index 849cf30..8c98ef3 100644 --- a/eac/__init__.py +++ b/eac/__init__.py @@ -9,10 +9,10 @@ import hmac from hashlib import sha1 import base64 -import jwt +from typing import Any +import jwt from requests.models import HTTPError -from typing import Any import flask import werkzeug @@ -153,7 +153,8 @@ def _revoke_slack() -> werkzeug.Response: @_AUTH.oidc_auth('default') def _auth_github() -> werkzeug.Response: # Redirect to github for authorisation - return redirect(_GITHUB_AUTH_URI % (APP.config['GITHUB_CLIENT_ID'], APP.config['STATE'])) + return redirect(_GITHUB_AUTH_URI % + (APP.config['GITHUB_CLIENT_ID'], APP.config['STATE'])) @APP.route('/github/return', methods=['GET']) @@ -165,11 +166,12 @@ def _github_landing() -> tuple[str, int]: return 'Invalid state', 400 # Get token from github - resp = requests.post(_GITHUB_TOKEN_URI % - (APP.config['GITHUB_CLIENT_ID'], APP.config['GITHUB_SECRET'], - request.args.get('code')), - headers={'Accept':'application/json'}, - timeout=APP.config['REQUEST_TIMEOUT']) + resp = requests.post( + _GITHUB_TOKEN_URI % + (APP.config['GITHUB_CLIENT_ID'], APP.config['GITHUB_SECRET'], + request.args.get('code')), + headers={'Accept': 'application/json'}, + timeout=APP.config['REQUEST_TIMEOUT']) try: resp.raise_for_status() except HTTPError as e: @@ -178,10 +180,14 @@ def _github_landing() -> tuple[str, int]: resp_json = resp.json() token = resp_json['access_token'] - header = {'Authorization' : 'token ' + token, - 'Accept' : 'application/vnd.github.v3+json'} + header = { + 'Authorization': 'token ' + token, + 'Accept': 'application/vnd.github.v3+json' + } - user_resp = requests.get('https://api.github.com/user', headers=header, timeout=APP.config['REQUEST_TIMEOUT']) + user_resp = requests.get('https://api.github.com/user', + headers=header, + timeout=APP.config['REQUEST_TIMEOUT']) try: user_resp.raise_for_status() except HTTPError as e: @@ -200,6 +206,7 @@ def _github_landing() -> tuple[str, int]: _link_github(github_username, github_id, member) return render_template('callback.html'), 200 + def _get_github_jwt() -> str: signing_key = APP.config["GITHUB_APP_PRIVATE_KEY"] @@ -213,15 +220,19 @@ def _get_github_jwt() -> str: return encoded_jwt + def _auth_github_org() -> str: jwt_auth = _get_github_jwt() headers = { - 'Accept' : 'application/vnd.github.v3+json', - 'Authorization': 'Bearer %s' % jwt_auth, + 'Accept': 'application/vnd.github.v3+json', + 'Authorization': f'Bearer {jwt_auth}', } - org_installation_resp = requests.get('https://api.github.com/orgs/ComputerScienceHouse/installation', headers=headers, timeout=APP.config['REQUEST_TIMEOUT']) + org_installation_resp = requests.get( + 'https://api.github.com/orgs/ComputerScienceHouse/installation', + headers=headers, + timeout=APP.config['REQUEST_TIMEOUT']) try: org_installation_resp.raise_for_status() except HTTPError as e: @@ -231,7 +242,10 @@ def _auth_github_org() -> str: org_installation_json = org_installation_resp.json() org_installation_id = org_installation_json['id'] - org_token_resp = requests.post('https://api.github.com/app/installations/%s/access_tokens' % org_installation_id, headers=headers, timeout=APP.config['REQUEST_TIMEOUT']) + org_token_resp = requests.post( + f'https://api.github.com/app/installations/{org_installation_id}/access_tokens', + headers=headers, + timeout=APP.config['REQUEST_TIMEOUT']) try: org_token_resp.raise_for_status() except HTTPError as e: @@ -243,6 +257,7 @@ def _auth_github_org() -> str: return org_token + def _link_github(github_username: str, github_id: str, member: Any) -> None: """ Puts a member's github into LDAP and adds them to the org. @@ -252,18 +267,22 @@ def _link_github(github_username: str, github_id: str, member: Any) -> None: """ org_token = _auth_github_org() - payload={ + payload = { 'org': 'ComputerScienceHouse', 'invitee_id': github_id, 'role': 'direct_member' } github_org_headers = { - 'Accept' : 'application/vnd.github.v3+json', - 'Authorization': 'Token %s' % org_token, + 'Accept': 'application/vnd.github.v3+json', + 'Authorization': f'Token {org_token}', } - resp = requests.post('https://api.github.com/orgs/ComputerScienceHouse/invitations', headers=github_org_headers, json=payload, timeout=APP.config['REQUEST_TIMEOUT']) + resp = requests.post( + 'https://api.github.com/orgs/ComputerScienceHouse/invitations', + headers=github_org_headers, + json=payload, + timeout=APP.config['REQUEST_TIMEOUT']) try: resp.raise_for_status() except HTTPError as e: @@ -279,27 +298,27 @@ def _revoke_github() -> werkzeug.Response: """ Clear's a member's github in LDAP and removes them from the org. """ uid = str(session['userinfo'].get('preferred_username', '')) member = _LDAP.get_member(uid, uid=True) - + org_token = _auth_github_org() headers = { - 'Accept' : 'application/vnd.github.v3+json', - 'Authorization': 'Token %s' % org_token, + 'Accept': 'application/vnd.github.v3+json', + 'Authorization': f'Token {org_token}', } - + resp = requests.delete( 'https://api.github.com/orgs/ComputerScienceHouse/members/' + member.github, headers=headers, timeout=APP.config['REQUEST_TIMEOUT'], ) - + try: resp.raise_for_status() except HTTPError as e: print('response:', resp.json()) raise e - + member.github = None return jsonify(success=True) From 97a1fb1ad08042667d03324fc75bee8c8d5d1f99 Mon Sep 17 00:00:00 2001 From: Noah Date: Fri, 10 Oct 2025 11:08:17 -0400 Subject: [PATCH 7/7] bumped csh ldap --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2046a36..57641c8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -csh-ldap @ git+https://github.com/ComputerScienceHouse/csh_ldap@2.5.0 +csh-ldap @ git+https://github.com/ComputerScienceHouse/csh_ldap@2.5.1 Flask==3.1.2 Flask-pyoidc==3.14.3 gunicorn==23.0.0