diff --git a/README.md b/README.md index 39207db..1c5836d 100644 --- a/README.md +++ b/README.md @@ -21,12 +21,19 @@ Select `LowEndOrchestrator` and use the default template. ![OrchTemplaceSelect](docs/images/CDOrchTemplateSelect.png) ## Configuring OAuth -Authentication and Access control is managed through an OAuth to GitHub. Starting the system for the first time requires a file named `env` in current working directory. An example `env.development` is provided that you may copy, and update to match the `secret`, `client_id`, and `callback_url` of your OAuth app. If the `env` file is not present, the application will not start, and it will emit the error `Can't find file env in current directory, not able to parse env properties, exiting.` +Authentication and Access control is managed through an OAuth to GitHub. Starting the system for the first time requires a file named `env` in current working directory. An example `env.development` is provided that you may copy, and update to match the `secret`, `client_id`, and `callback_url` of your OAuth app. -If no `env` file is present in the working directory, the AWS User Data script will create one, in the home directory, using the contents of `env.defaults`. The default configuration is not correct, and OAuth will fail. Using the default configuration will allow the application to start, and respond to healthchecks. Please make sure to review the `env` file if you have any issues with authentication. +If the `env` file is not present, the application will not start, and it will emit the error `Can't find file env in current directory, not able to parse env properties, exiting.` If no `env` file is present in the working directory, when you deploy a new orchestration instances in AWS, the AWS User Data script will create one, in the home directory, using the contents of `env.defaults`. The default configuration is not correct, and OAuth will fail. Using the default configuration will allow the application to start, and respond to healthchecks. Please make sure to review the `env` file if you have any issues with authentication. ### Access Control -To gain access to the application, a user must have write access to specific GitHub repositories. These repositories are also found in the `env` file. +To gain access to the application, a user must have membership in specific GitHub teams. The org and teams checked for membership are found in the `env` file. You may use multiple teams for access control by providing a comma separated list in the `env` file. Access to the application is checked on every HTTP request, and the application makes HTTP calls to GitHub to ensure the user has sufficient privileges to perform the requested action. There are two methods of access control: +- Using a web browser via OAuth: Click on the person icon in the top right corner to login. You will be redirected to GitHub to authenticate. +- Using HTTP command line: Pass the header `Authorization` with your valid GitHub token. The GitHub token must have `read:org` scope for the organization specified in the `env` file. + +Example of command line access +``` +curl -H 'Accept: application/json' -H 'Authorization: gho_bBB1bB1BBbbBbb1BBbBbBB1bbbb1BbbBB' http://127.0.0.1:4000/status +``` ## Updating Orchestrator Job Configuration By default the setup will spin up a webservice with [Production Run from Jan 2024](meta-data/full-production-run-20240101.json). To change the job configuration you need to create your own JSON configuration, and restart the service to use the new JSON. **Note** need to use `nohup` on python webservice to keep the process running after ssh-shell exit. diff --git a/env.development b/env.development index 4ea72b2..8a22f4a 100644 --- a/env.development +++ b/env.development @@ -5,4 +5,4 @@ authorize_url=https://github.com/login/oauth/authorize registered_callback=https://example.com/oauthback access_token=https://github.com/login/oauth/access_token user_info_url=https://api.github.com/user -team=ORG/TEAM +team=ORG/TEAM_1, ORG/TEAM_2 diff --git a/orchestration-service/github_oauth.py b/orchestration-service/github_oauth.py index 58aec47..c95a757 100644 --- a/orchestration-service/github_oauth.py +++ b/orchestration-service/github_oauth.py @@ -61,21 +61,25 @@ def check_membership(bearer_token, login, team_string): """Check for team membership""" if not login: return False - org, team = team_string.split('/',1) - url = f'https://api.github.com/orgs/{org}/teams/{team}/members' - membership_check = requests.get(url, - timeout=3, - headers={ - 'Accept': 'application/vnd.github+json', - 'Authorization': f'Bearer {bearer_token}', - 'X-GitHub-Api-Version': '2022-11-28', - 'User-Agent': 'App/OAuth/ReplayTest' - }) - if membership_check.status_code == 200: - members_list = json.loads(membership_check.content.decode('utf-8')) - for member in members_list: - if member['login'] == login: - return True + # many contain many teams + for unit in team_string.split(','): + org, team = unit.split('/',1) + org = org.strip() + team = team.strip() + url = f'https://api.github.com/orgs/{org}/teams/{team}/members' + membership_check = requests.get(url, + timeout=3, + headers={ + 'Accept': 'application/vnd.github+json', + 'Authorization': f'Bearer {bearer_token}', + 'X-GitHub-Api-Version': '2022-11-28', + 'User-Agent': 'App/OAuth/ReplayTest' + }) + if membership_check.status_code == 200: + members_list = json.loads(membership_check.content.decode('utf-8')) + for member in members_list: + if member['login'] == login: + return True return False @staticmethod @@ -85,8 +89,8 @@ def is_authorized(cookies, header_token, user_info_url, team_string): token = None if 'replay_auth' in cookies and cookies['replay_auth']: token = GitHubOauth.extract_token(cookies['replay_auth']) - if header_token: - token = header_token + elif header_token: + token = header_token.replace("Bearer ","") if not token: return False diff --git a/orchestration-service/web_service.py b/orchestration-service/web_service.py index 59321dc..9124814 100644 --- a/orchestration-service/web_service.py +++ b/orchestration-service/web_service.py @@ -47,12 +47,15 @@ def application(request): ETag {request.headers.get('ETag')}""") # auth check /progress /grid /control /detail are HTML pages - # healthcheck does not require auth + # /healthcheck does not require acess control + # /oauthback is called before access control is avalible # they have their own auth flow and messages, so we skip them for out auth check # this protects API calls if request.path not in ['/progress', '/grid', '/control', '/detail', '/healthcheck', '/oauthback'] and \ - not (ALWAYS_ALLOW or GitHubOauth.is_authorized(request.cookies, None, - env_name_values.get('user_info_url'), env_name_values.get('team'))): + not (ALWAYS_ALLOW or GitHubOauth.is_authorized(request.cookies, + request.headers.get('Authorization'), + env_name_values.get('user_info_url'), + env_name_values.get('team'))): return Response("Not Authorized", status=403) if request.path == '/job': @@ -254,8 +257,10 @@ def application(request): referring_url = request.path if ALWAYS_ALLOW or \ - GitHubOauth.is_authorized(request.cookies, None, - env_name_values.get('user_info_url'), env_name_values.get('team')): + GitHubOauth.is_authorized(request.cookies, + request.headers.get('Authorization'), + env_name_values.get('user_info_url'), + env_name_values.get('team')): # Retrieve the auth cookie cookie_value = request.cookies.get('replay_auth') login, avatar_url = GitHubOauth.str_to_public_profile(cookie_value)