From 38b85c7291709294e323fd55764f093e5e16e024 Mon Sep 17 00:00:00 2001 From: Max Meinhold Date: Fri, 12 Sep 2025 15:13:47 -0400 Subject: [PATCH 01/12] Fixup Dockerfile for OKD Use shell interpretation in CMD, so that variables are interpreted from the environment Allow git to run with eac dir in container, since OKD uses random UIDs --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 512d0bf..38e5580 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,9 +16,10 @@ RUN --mount=type=bind,source=requirements.txt,target=requirements.txt \ COPY . /opt/eac RUN ln -sf /usr/share/zoneinfo/America/New_York /etc/localtime +RUN git config --system --add safe.directory /opt/eac ARG PORT=8080 ENV PORT=${PORT} EXPOSE ${PORT} -CMD ["gunicorn app:application --bind=0.0.0.0:${PORT} --access-logfile=- --timeout=600"] +CMD ["sh", "-c", "gunicorn app:application --bind=0.0.0.0:${PORT} --access-logfile=- --timeout=600"] From 4144e7083a4fa49da0b288732270d9468024c9c9 Mon Sep 17 00:00:00 2001 From: Max Meinhold Date: Fri, 12 Sep 2025 15:53:12 -0400 Subject: [PATCH 02/12] Update pylint config --- .pylintrc | 14 ++------------ config.env.py | 3 +++ eac/__init__.py | 47 ++++++++++++++++++++++++++++++++++------------- 3 files changed, 39 insertions(+), 25 deletions(-) diff --git a/.pylintrc b/.pylintrc index 9de855b..1878f7a 100644 --- a/.pylintrc +++ b/.pylintrc @@ -9,7 +9,6 @@ disable = duplicate-code, no-member, parse-error, - bad-continuation, too-few-public-methods, global-statement, cyclic-import, @@ -17,20 +16,14 @@ disable = file-ignored [REPORTS] -output-format = text -files-output = no +output-format = colorized reports = no [FORMAT] max-line-length = 120 -max-statement-lines = 75 single-line-if-stmt = no -no-space-check = trailing-comma,dict-separator max-module-lines = 1000 indent-string = ' ' -string-quote=single-avoid-escape -triple-quote=single -docstring-quote=double [MISCELLANEOUS] notes = FIXME,XXX,TODO @@ -76,9 +69,6 @@ good-names=logger,id,ID # Bad variable names which should always be refused, separated by a comma bad-names=foo,bar,baz,toto,tutu,tata -# List of builtins function names that should not be used, separated by a comma -bad-functions=apply,input - [DESIGN] max-args = 10 ignored-argument-names = _.* @@ -92,4 +82,4 @@ min-public-methods = 2 max-public-methods = 20 [EXCEPTIONS] -overgeneral-exceptions = Exception +overgeneral-exceptions = builtins.Exception diff --git a/config.env.py b/config.env.py index e863de5..3aecf23 100644 --- a/config.env.py +++ b/config.env.py @@ -42,3 +42,6 @@ # Common secrets STATE = os.environ.get('STATE', 'auth') + +# Connection controls +REQUEST_TIMEOUT = os.environ.get("EAC_REQUEST_TIMEOUT", 60) # default to a minute timeout diff --git a/eac/__init__.py b/eac/__init__.py index 34147a6..22f1736 100644 --- a/eac/__init__.py +++ b/eac/__init__.py @@ -12,7 +12,7 @@ from flask import Flask, request, redirect, session, render_template, send_from_directory, jsonify from flask_pyoidc.flask_pyoidc import OIDCAuthentication -from flask_pyoidc.provider_configuration import * +from flask_pyoidc.provider_configuration import ProviderConfiguration, ClientMetadata import csh_ldap import requests import sentry_sdk @@ -126,7 +126,9 @@ def _link_slack(): # pylint: disable=inconsistent-return-statements resp = requests.get(_SLACK_ACCESS_URI % (APP.config['SLACK_CLIENT_ID'], - APP.config['SLACK_SECRET'], request.args.get('code'))) + APP.config['SLACK_SECRET'], request.args.get('code')), + timeout=APP.config['REQUEST_TIMEOUT'], + ) uid = str(session['userinfo'].get('preferred_username', '')) member = _LDAP.get_member(uid, uid=True) member.slackUID = resp.json()['user']['id'] @@ -163,14 +165,18 @@ def _github_landing(): # pylint: disable=inconsistent-return-statements resp = requests.post(_GITHUB_TOKEN_URI % (APP.config['GITHUB_CLIENT_ID'], APP.config['GITHUB_SECRET'], request.args.get('code')), - headers={'Accept':'application/json'}) + headers={'Accept':'application/json'}, + timeout=APP.config['REQUEST_TIMEOUT'], + ) 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) + user_resp = requests.get('https://api.github.com/user', headers=header, + timeout=APP.config['REQUEST_TIMEOUT'], + ) user_resp_json = user_resp.json() - + github_username = user_resp_json['login'] github_id = user_resp_json['id'] @@ -192,7 +198,9 @@ 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) + requests.post('https://api.github.com/orgs/ComputerScienceHouse/invitations', headers=_ORG_HEADER, data=payload, + timeout=APP.config['REQUEST_TIMEOUT'], + ) member.github = github_username @@ -202,7 +210,9 @@ def _revoke_github(): """ 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) - requests.delete('https://api.github.com/orgs/ComputerScienceHouse/members/' + member.github, headers=_ORG_HEADER) + requests.delete('https://api.github.com/orgs/ComputerScienceHouse/members/' + member.github, headers=_ORG_HEADER, + timeout=APP.config['REQUEST_TIMEOUT'], + ) member.github = None return jsonify(success=True) @@ -226,11 +236,14 @@ def _twitch_landing(): # pylint: disable=inconsistent-return-statements resp = requests.post(_TWITCH_TOKEN_URI % (APP.config['TWITCH_CLIENT_ID'], APP.config['TWITCH_CLIENT_SECRET'], request.args.get('code')), - headers={'Accept':'application/json'}) + headers={'Accept':'application/json'}, + timeout=APP.config['REQUEST_TIMEOUT'], + ) header = {'Authorization' : 'OAuth ' + resp.json()['access_token'], } - resp = requests.get('https://id.twitch.tv/oauth2/validate', headers=header) - + resp = requests.get('https://id.twitch.tv/oauth2/validate', headers=header, + timeout=APP.config['REQUEST_TIMEOUT'], + ) # Pull member from LDAP uid = str(session['userinfo'].get('preferred_username', '')) @@ -280,7 +293,10 @@ def _auth_twitter(): resp = requests.post(_TWITTER_REQUEST_TOKEN_URI, headers={'Accept': '*/*', - 'Authorization': oauth_header}) + 'Authorization': oauth_header}, + timeout=APP.config['REQUEST_TIMEOUT'], + ) + if resp.status_code != 200: print(f'Status: {resp.status_code}\nMessage: {resp.text}') return 'Error fetching request_token', 500 @@ -326,7 +342,10 @@ def _twitter_landing(): # pylint: disable=inconsistent-return-statements data=f'oauth_verifier={oauth_verifier}', headers={'Accept': '*/*', 'Authorization': oauth_header, - 'Content-Type': 'application/x-www-form-urlencoded'}) + 'Content-Type': 'application/x-www-form-urlencoded'}, + timeout=APP.config['REQUEST_TIMEOUT'], + ) + returned_params = dict((key.strip(), val.strip()) for key, val in (element.split('=') for element in resp.text.split('&'))) @@ -358,7 +377,9 @@ def _twitter_landing(): # pylint: disable=inconsistent-return-statements f'oauth_version="1.0"' resp = requests.get(_TWITTER_ACCOUNT_INFO_URI, headers={'Accept': '*/*', - 'Authorization': oauth_header}) + 'Authorization': oauth_header}, + timeout=APP.config['REQUEST_TIMEOUT'], + ) # Pull member from LDAP uid = str(session['userinfo'].get('preferred_username', '')) member = _LDAP.get_member(uid, uid=True) From 1faf16626cf4073c0eedd72a8c8b6b35832610d5 Mon Sep 17 00:00:00 2001 From: Max Meinhold Date: Fri, 12 Sep 2025 16:21:45 -0400 Subject: [PATCH 03/12] Add mypy for typechecking --- .mypy.ini | 3 +++ eac/__init__.py | 51 +++++++++++++++++++++++++++--------------------- requirements.txt | 1 + 3 files changed, 33 insertions(+), 22 deletions(-) create mode 100644 .mypy.ini diff --git a/.mypy.ini b/.mypy.ini new file mode 100644 index 0000000..92469ba --- /dev/null +++ b/.mypy.ini @@ -0,0 +1,3 @@ +[mypy] +disable_error_code = import +disallow_untyped_defs = True diff --git a/eac/__init__.py b/eac/__init__.py index 22f1736..7bd62ce 100644 --- a/eac/__init__.py +++ b/eac/__init__.py @@ -9,7 +9,10 @@ import hmac from hashlib import sha1 import base64 +from typing import Any +import flask +import werkzeug from flask import Flask, request, redirect, session, render_template, send_from_directory, jsonify from flask_pyoidc.flask_pyoidc import OIDCAuthentication from flask_pyoidc.provider_configuration import ProviderConfiguration, ClientMetadata @@ -84,13 +87,13 @@ @APP.route('/static/', methods=['GET']) -def _send_static(path): +def _send_static(path: str) -> flask.wrappers.Response: return send_from_directory('static', path) @APP.route('/') @_AUTH.oidc_auth('default') -def _index(): +def _index() -> str: commit_hash = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).strip().decode('utf-8') uid = str(session['userinfo'].get('preferred_username', '')) member = _LDAP.get_member(uid, uid=True) @@ -109,14 +112,14 @@ def _index(): @APP.route('/slack', methods=['GET']) @_AUTH.oidc_auth('default') -def _auth_slack(): +def _auth_slack() -> werkzeug.Response: return redirect(_SLACK_AUTH_URI % (APP.config['SLACK_CLIENT_ID'], APP.config['STATE'])) @APP.route('/slack/return', methods=['GET']) @_AUTH.oidc_auth('default') -def _link_slack(): # pylint: disable=inconsistent-return-statements +def _link_slack() -> tuple[str, int]: """ Links Slack into LDAP via slackUID """ # Determine if we have a valid reason to do things @@ -132,12 +135,12 @@ def _link_slack(): # pylint: disable=inconsistent-return-statements uid = str(session['userinfo'].get('preferred_username', '')) member = _LDAP.get_member(uid, uid=True) member.slackUID = resp.json()['user']['id'] - return render_template('callback.html') + return render_template('callback.html'), 200 @APP.route('/slack', methods=['DELETE']) @_AUTH.oidc_auth('default') -def _revoke_slack(): +def _revoke_slack() -> werkzeug.Response: """ Revokes Slack by clearing slackUID """ uid = str(session['userinfo'].get('preferred_username', '')) member = _LDAP.get_member(uid, uid=True) @@ -147,7 +150,7 @@ def _revoke_slack(): @APP.route('/github', methods=['GET']) @_AUTH.oidc_auth('default') -def _auth_github(): +def _auth_github() -> werkzeug.Response: # Redirect to github for authorisation return redirect(_GITHUB_AUTH_URI % (APP.config['GITHUB_CLIENT_ID'], APP.config['STATE'])) @@ -155,7 +158,7 @@ def _auth_github(): @APP.route('/github/return', methods=['GET']) @_AUTH.oidc_auth('default') -def _github_landing(): # pylint: disable=inconsistent-return-statements +def _github_landing() -> tuple[str, int]: # Determine if we have a valid reason to do things state = request.args.get('state') if state != APP.config['STATE']: @@ -185,10 +188,10 @@ def _github_landing(): # pylint: disable=inconsistent-return-statements member = _LDAP.get_member(uid, uid=True) _link_github(github_username, github_id, member) - return render_template('callback.html') + return render_template('callback.html'), 200 -def _link_github(github_username, github_id, member): +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. :param github_username: the user's github username @@ -206,7 +209,7 @@ def _link_github(github_username, github_id, member): @APP.route('/github', methods=['DELETE']) @_AUTH.oidc_auth('default') -def _revoke_github(): +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) @@ -219,7 +222,7 @@ def _revoke_github(): @APP.route('/twitch', methods=['GET']) @_AUTH.oidc_auth('default') -def _auth_twitch(): +def _auth_twitch() -> werkzeug.Response: # Redirect to twitch for authorisation return redirect(_TWITCH_AUTH_URI % (APP.config['TWITCH_CLIENT_ID'], APP.config['STATE'])) @@ -227,7 +230,7 @@ def _auth_twitch(): @APP.route('/twitch/return', methods=['GET']) @_AUTH.oidc_auth('default') -def _twitch_landing(): # pylint: disable=inconsistent-return-statements +def _twitch_landing() -> tuple[str, int]: # Determine if we have a valid reason to do things state = request.args.get('state') if state != APP.config['STATE']: @@ -250,12 +253,12 @@ def _twitch_landing(): # pylint: disable=inconsistent-return-statements member = _LDAP.get_member(uid, uid=True) member.twitchlogin = resp.json()['login'] - return render_template('callback.html') + return render_template('callback.html'), 200 @APP.route('/twitch', methods=['DELETE']) @_AUTH.oidc_auth('default') -def _revoke_twitch(): +def _revoke_twitch() -> werkzeug.Response: """ Clear's a member's twitch login in LDAP.""" uid = str(session['userinfo'].get('preferred_username', '')) member = _LDAP.get_member(uid, uid=True) @@ -265,7 +268,7 @@ def _revoke_twitch(): @APP.route('/twitter', methods=['GET']) @_AUTH.oidc_auth('default') -def _auth_twitter(): +def _auth_twitter() -> werkzeug.Response: # 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()) @@ -299,7 +302,7 @@ def _auth_twitter(): if resp.status_code != 200: print(f'Status: {resp.status_code}\nMessage: {resp.text}') - return 'Error fetching request_token', 500 + return flask.make_response(('Error fetching request_token', 500)) returned_params = dict((key.strip(), val.strip()) for key, val in (element.split('=') for element in resp.text.split('&'))) @@ -311,14 +314,18 @@ def _auth_twitter(): @APP.route('/twitter/return', methods=['GET']) @_AUTH.oidc_auth('default') -def _twitter_landing(): # pylint: disable=inconsistent-return-statements +def _twitter_landing() -> tuple[str, int]: oauth_token = request.args.get('oauth_token') + if oauth_token is None: + return "Failed to get outh token", 400 oauth_verifier = request.args.get('oauth_verifier') + if oauth_verifier is None: + return "Failed to get outh verifier", 400 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_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="")}' \ @@ -384,12 +391,12 @@ def _twitter_landing(): # pylint: disable=inconsistent-return-statements 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') + return render_template('callback.html'), 200 @APP.route('/twitter', methods=['DELETE']) @_AUTH.oidc_auth('default') -def _revoke_twitter(): +def _revoke_twitter() -> werkzeug.Response: """ Clear's a member's twitter login in LDAP.""" uid = str(session['userinfo'].get('preferred_username', '')) member = _LDAP.get_member(uid, uid=True) @@ -399,5 +406,5 @@ def _revoke_twitter(): @APP.route('/logout') @_AUTH.oidc_logout -def logout(): +def logout() -> werkzeug.Response: return redirect('/', 302) diff --git a/requirements.txt b/requirements.txt index 72ae495..655b7e1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ csh-ldap==2.4.0 Flask==3.1.2 Flask-pyoidc==3.14.3 gunicorn==23.0.0 +mypy==1.18.1 pylint==3.3.8 pylint-quotes==0.2.3 requests==2.32.5 From dae7c0fb36488e830af65c4225deec6a496142c6 Mon Sep 17 00:00:00 2001 From: Max Meinhold Date: Fri, 12 Sep 2025 16:48:42 -0400 Subject: [PATCH 04/12] Add YAPF and linting docs --- README.md | 13 ++++ config.env.py | 12 ++-- eac/__init__.py | 163 ++++++++++++++++++++++++++++------------------- requirements.txt | 1 + 4 files changed, 121 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index 4cd456c..d25e181 100644 --- a/README.md +++ b/README.md @@ -35,3 +35,16 @@ pip3 install -r requirements.txt flask run -h localhost -p 5000 ``` + +### Linting + ``` +# Install types +mypy --install-types + +# Check linting +mypy app.py config.env.py eac +# Check Typing +pylint app.py config.env.py eac +# Format +yapf -ir app.py config.env.py eac + ``` diff --git a/config.env.py b/config.env.py index 3aecf23..3dd9ee2 100644 --- a/config.env.py +++ b/config.env.py @@ -5,10 +5,12 @@ IP = os.environ.get('IP', '127.0.0.1') PORT = os.environ.get('PORT', 5000) SERVER_NAME = os.environ.get('SERVER_NAME', 'localhost:5000') -SECRET_KEY = os.environ.get('SESSION_KEY', default=''.join(secrets.token_hex(16))) +SECRET_KEY = os.environ.get('SESSION_KEY', + default=''.join(secrets.token_hex(16))) # OpenID Connect SSO config -OIDC_ISSUER = os.environ.get('OIDC_ISSUER', 'https://sso.csh.rit.edu/auth/realms/csh') +OIDC_ISSUER = os.environ.get('OIDC_ISSUER', + 'https://sso.csh.rit.edu/auth/realms/csh') OIDC_CLIENT_CONFIG = { 'client_id': os.environ.get('OIDC_CLIENT_ID', ''), 'client_secret': os.environ.get('OIDC_CLIENT_SECRET', ''), @@ -36,7 +38,8 @@ # Twitter secrets TWITTER_CONSUMER_KEY = os.environ.get('TWITTER_OAUTH_CONSUMER_KEY', '') -TWITTER_CONSUMER_SECRET_KEY = os.environ.get('TWITTER_OAUTH_CONSUMER_SECRET_KEY', '') +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', '') @@ -44,4 +47,5 @@ STATE = os.environ.get('STATE', 'auth') # Connection controls -REQUEST_TIMEOUT = os.environ.get("EAC_REQUEST_TIMEOUT", 60) # default to a minute timeout +REQUEST_TIMEOUT = os.environ.get("EAC_REQUEST_TIMEOUT", + 60) # default to a minute timeout diff --git a/eac/__init__.py b/eac/__init__.py index 7bd62ce..20bc994 100644 --- a/eac/__init__.py +++ b/eac/__init__.py @@ -21,7 +21,6 @@ import sentry_sdk from sentry_sdk.integrations.flask import FlaskIntegration - APP = Flask(__name__) if os.path.exists(os.path.join(os.getcwd(), 'config.py')): @@ -29,19 +28,14 @@ else: APP.config.from_pyfile(os.path.join(os.getcwd(), 'config.env.py')) -sentry_sdk.init( - dsn=APP.config['SENTRY_DSN'], - integrations=[FlaskIntegration()] - ) +sentry_sdk.init(dsn=APP.config['SENTRY_DSN'], + integrations=[FlaskIntegration()]) APP.secret_key = APP.config['SECRET_KEY'] _CONFIG = ProviderConfiguration( APP.config['OIDC_ISSUER'], - client_metadata=ClientMetadata( - **APP.config['OIDC_CLIENT_CONFIG'] - ) -) + client_metadata=ClientMetadata(**APP.config['OIDC_CLIENT_CONFIG'])) _AUTH = OIDCAuthentication({'default': _CONFIG}, APP) _LDAP = csh_ldap.CSHLDAP(APP.config['LDAP_DN'], APP.config['LDAP_SECRET']) @@ -82,8 +76,10 @@ _TWITTER_ACCESS_TOKEN_URI = 'https://api.twitter.com/oauth/access_token' _TWITTER_ACCOUNT_INFO_URI = 'https://api.twitter.com/1.1/account/verify_credentials.json' _TWITTER_AUTH_TOKEN_CACHE = {} -_ORG_HEADER = {'Authorization' : 'token ' + APP.config['ORG_TOKEN'], - 'Accept' : 'application/vnd.github.v3+json'} +_ORG_HEADER = { + 'Authorization': 'token ' + APP.config['ORG_TOKEN'], + 'Accept': 'application/vnd.github.v3+json' +} @APP.route('/static/', methods=['GET']) @@ -94,7 +90,8 @@ def _send_static(path: str) -> flask.wrappers.Response: @APP.route('/') @_AUTH.oidc_auth('default') def _index() -> str: - commit_hash = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).strip().decode('utf-8') + commit_hash = subprocess.check_output( + ['git', 'rev-parse', '--short', 'HEAD']).strip().decode('utf-8') uid = str(session['userinfo'].get('preferred_username', '')) member = _LDAP.get_member(uid, uid=True) services = { @@ -127,9 +124,10 @@ def _link_slack() -> tuple[str, int]: if state != APP.config['STATE']: return 'Invalid state', 400 - resp = requests.get(_SLACK_ACCESS_URI % - (APP.config['SLACK_CLIENT_ID'], - APP.config['SLACK_SECRET'], request.args.get('code')), + resp = requests.get( + _SLACK_ACCESS_URI % + (APP.config['SLACK_CLIENT_ID'], APP.config['SLACK_SECRET'], + request.args.get('code')), timeout=APP.config['REQUEST_TIMEOUT'], ) uid = str(session['userinfo'].get('preferred_username', '')) @@ -165,17 +163,22 @@ 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'}, + 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'], ) 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, + user_resp = requests.get( + 'https://api.github.com/user', + headers=header, timeout=APP.config['REQUEST_TIMEOUT'], ) user_resp_json = user_resp.json() @@ -198,10 +201,11 @@ def _link_github(github_username: str, github_id: str, member: Any) -> None: :param github_id: the user's github id :param member: the member's LDAP object """ - payload={ - 'invitee_id': github_id - } - requests.post('https://api.github.com/orgs/ComputerScienceHouse/invitations', headers=_ORG_HEADER, data=payload, + payload = {'invitee_id': github_id} + requests.post( + 'https://api.github.com/orgs/ComputerScienceHouse/invitations', + headers=_ORG_HEADER, + data=payload, timeout=APP.config['REQUEST_TIMEOUT'], ) member.github = github_username @@ -213,7 +217,10 @@ 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) - requests.delete('https://api.github.com/orgs/ComputerScienceHouse/members/' + member.github, headers=_ORG_HEADER, + requests.delete( + 'https://api.github.com/orgs/ComputerScienceHouse/members/' + + member.github, + headers=_ORG_HEADER, timeout=APP.config['REQUEST_TIMEOUT'], ) member.github = None @@ -236,15 +243,20 @@ def _twitch_landing() -> tuple[str, int]: 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'}, + resp = requests.post( + _TWITCH_TOKEN_URI % + (APP.config['TWITCH_CLIENT_ID'], APP.config['TWITCH_CLIENT_SECRET'], + request.args.get('code')), + headers={'Accept': 'application/json'}, timeout=APP.config['REQUEST_TIMEOUT'], ) - header = {'Authorization' : 'OAuth ' + resp.json()['access_token'], } - resp = requests.get('https://id.twitch.tv/oauth2/validate', headers=header, + header = { + 'Authorization': 'OAuth ' + resp.json()['access_token'], + } + resp = requests.get( + 'https://id.twitch.tv/oauth2/validate', + headers=header, timeout=APP.config['REQUEST_TIMEOUT'], ) @@ -270,7 +282,9 @@ def _revoke_twitch() -> werkzeug.Response: @_AUTH.oidc_auth('default') def _auth_twitter() -> werkzeug.Response: # 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_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"]}' \ @@ -282,9 +296,10 @@ def _auth_twitter() -> werkzeug.Response: + 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_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"]}", ' \ @@ -294,22 +309,29 @@ def _auth_twitter() -> werkzeug.Response: f'oauth_timestamp="{oauth_timestamp}", ' \ f'oauth_version="1.0"' - resp = requests.post(_TWITTER_REQUEST_TOKEN_URI, - headers={'Accept': '*/*', - 'Authorization': oauth_header}, + resp = requests.post( + _TWITTER_REQUEST_TOKEN_URI, + headers={ + 'Accept': '*/*', + 'Authorization': oauth_header + }, timeout=APP.config['REQUEST_TIMEOUT'], ) if resp.status_code != 200: print(f'Status: {resp.status_code}\nMessage: {resp.text}') return flask.make_response(('Error fetching request_token', 500)) - returned_params = dict((key.strip(), val.strip()) - for key, val in (element.split('=') - for element in resp.text.split('&'))) + 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'] + _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"]}') + return redirect( + f'{_TWITTER_AUTHORIZATION_URI}?oauth_token={returned_params["oauth_token"]}' + ) @APP.route('/twitter/return', methods=['GET']) @@ -321,7 +343,9 @@ def _twitter_landing() -> tuple[str, int]: oauth_verifier = request.args.get('oauth_verifier') if oauth_verifier is None: return "Failed to get outh verifier", 400 - oauth_nonce = ''.join([random.choice(string.ascii_letters + string.digits) for n in range(32)]) + 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}' \ @@ -334,9 +358,10 @@ def _twitter_landing() -> tuple[str, int]: + 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_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}", ' \ @@ -345,21 +370,27 @@ def _twitter_landing() -> tuple[str, int]: 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'}, + 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' + }, timeout=APP.config['REQUEST_TIMEOUT'], ) - returned_params = dict((key.strip(), val.strip()) - for key, val in (element.split('=') - for element in resp.text.split('&'))) + 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_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}' \ @@ -371,9 +402,10 @@ def _twitter_landing() -> tuple[str, int]: + 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_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}", ' \ @@ -382,9 +414,12 @@ def _twitter_landing() -> tuple[str, int]: 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}, + resp = requests.get( + _TWITTER_ACCOUNT_INFO_URI, + headers={ + 'Accept': '*/*', + 'Authorization': oauth_header + }, timeout=APP.config['REQUEST_TIMEOUT'], ) # Pull member from LDAP diff --git a/requirements.txt b/requirements.txt index 655b7e1..5c28470 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ pylint==3.3.8 pylint-quotes==0.2.3 requests==2.32.5 sentry-sdk[flask]==2.37.1 +yapf==0.43.0 From 3b1510f5e17dd7a38b31e4a65a7b5d1993b8b242 Mon Sep 17 00:00:00 2001 From: Max Meinhold Date: Fri, 12 Sep 2025 16:49:09 -0400 Subject: [PATCH 05/12] Add github actions Also remove travis --- .github/workflows/python-app.yml | 49 ++++++++++++++++++++++++++++++++ .travis.yml | 7 ----- 2 files changed, 49 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/python-app.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml new file mode 100644 index 0000000..d8362a9 --- /dev/null +++ b/.github/workflows/python-app.yml @@ -0,0 +1,49 @@ +name: Linting and tests + +on: + push: + branches: [master, mom/dev] + pull_request: + branches: [master, mom/dev] + +jobs: + check: + runs-on: ubuntu-latest + + strategy: + matrix: + python-version: [3.13] + + steps: + - name: Install ldap dependencies + run: sudo apt-get update && sudo apt-get install libldap2-dev libsasl2-dev + - uses: actions/checkout@v5 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + mypy --non-interactive --install-types + - name: Lint with pylint + run: | + pylint app.py config.env.py eac + - name: Typecheck with mypy + run: | + mypy app.py config.env.py eac + - name: Format with yapf + run: | + yapf --diff -r app.py config.env.py eac + + docker-build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v5 + + - name: Build Image + run: | + docker build . --file Dockerfile diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 7d0d017..0000000 --- a/.travis.yml +++ /dev/null @@ -1,7 +0,0 @@ -language: python -python: - - "3.13" -script: - - "pylint --load-plugins pylint_quotes eac" -notifications: - email: false From 5d2f17d97d2ccbc6c89bf5af8ca7bdd595bddc6a Mon Sep 17 00:00:00 2001 From: Noah Date: Wed, 1 Oct 2025 13:38:52 -0400 Subject: [PATCH 06/12] 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 07/12] 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 08/12] 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 09/12] 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 10/12] 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 11/12] 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 12/12] 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