diff --git a/.env.example b/.env.example index 9952ccf..1e15a55 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,8 @@ GITHUB_APP_CLIENT_ID="" GITHUB_APP_CLIENT_SECRET="" # Token for background jobs (a personal access token with read access to the repos) GITHUB_TOKEN="" +GITHUB_ORG="" +GITHUB_TEAM="" # Database config DB_NAME= DB_USER= diff --git a/README.md b/README.md index 215ed63..dd883fc 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,12 @@ A deployment orchestrator for GitHub-hosted projects. drawing +## Login + +Users log in with GitHub, and only active members of one GitHub team are let in. Set the team with `GITHUB_ORG` and `GITHUB_TEAM` (the team's slug from its URL, not its display name). If either is missing, nobody can log in. + +Membership is checked once at login, so someone removed from the team keeps access until their session ends. + ## Database setup & migrations `make create_db && make upgrade_db` should be all that's needed to get started. Those two commands will create the database if it does not exist and run the migrations. diff --git a/helm/makeitso/templates/deployment.yaml b/helm/makeitso/templates/deployment.yaml index ef2e9b1..bf20e6a 100644 --- a/helm/makeitso/templates/deployment.yaml +++ b/helm/makeitso/templates/deployment.yaml @@ -58,6 +58,10 @@ spec: secretKeyRef: name: {{ .Values.app.githubAppSecretName | quote }} key: ghWebhookSecret + - name: GITHUB_ORG + value: {{ .Values.app.githubOrg | quote }} + - name: GITHUB_TEAM + value: {{ .Values.app.githubTeam | quote }} readinessProbe: httpGet: path: /healthz diff --git a/helm/makeitso/values.yaml b/helm/makeitso/values.yaml index dfde01e..7bc953b 100644 --- a/helm/makeitso/values.yaml +++ b/helm/makeitso/values.yaml @@ -6,6 +6,8 @@ app: env: prod githubAppSecretName: makeitso-github-app-secret + githubOrg: bcgov + githubTeam: cas-developers # Runs deploys deployWorker: diff --git a/src/makeitso/auth/routes.py b/src/makeitso/auth/routes.py index e7e4f94..00f4477 100644 --- a/src/makeitso/auth/routes.py +++ b/src/makeitso/auth/routes.py @@ -1,5 +1,5 @@ from authlib.integrations.base_client import OAuthError -from flask import Blueprint, current_app, redirect, render_template, session, url_for +from flask import Blueprint, current_app, flash, redirect, render_template, session, url_for from makeitso.extensions import oauth @@ -24,11 +24,33 @@ def authorize(): response.raise_for_status() user_info = response.json() + + org = current_app.config["GITHUB_ORG"] + team = current_app.config["GITHUB_TEAM"] + if not _is_team_member(org, team, user_info["login"]): + session.clear() + flash(f"You need to be a member of the {org}/{team} GitHub team to log in", "danger") + return redirect("/") + session["user"] = user_info return redirect("/") +def _is_team_member(org: str | None, team: str | None, username: str) -> bool: + # Fail closed if the team isn't configured + if not org or not team: + return False + + response = oauth.github.get(f"orgs/{org}/teams/{team}/memberships/{username}") + # 404 means not a member, or the token can't see the team + if response.status_code != 200: + return False + + # Pending invites don't count + return response.json().get("state") == "active" + + @bp.route("/login") def login(): scheme = "https" if current_app.config["SESSION_COOKIE_SECURE"] else "http" diff --git a/src/makeitso/config.py b/src/makeitso/config.py index 557d900..f82b4c3 100644 --- a/src/makeitso/config.py +++ b/src/makeitso/config.py @@ -30,6 +30,9 @@ class Config: # Used by background jobs, which have no logged-in user GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN") GITHUB_WEBHOOK_SECRET = os.environ.get("GITHUB_WEBHOOK_SECRET") + # Only members of this team can log in + GITHUB_ORG = os.environ.get("GITHUB_ORG") + GITHUB_TEAM = os.environ.get("GITHUB_TEAM") # Where deploys check out code; Helm mounts /workspace DEPLOY_WORKSPACE = os.environ.get("DEPLOY_WORKSPACE", "/tmp/makeitso-workspace") diff --git a/src/makeitso/extensions.py b/src/makeitso/extensions.py index 13526ef..5277305 100644 --- a/src/makeitso/extensions.py +++ b/src/makeitso/extensions.py @@ -22,5 +22,6 @@ authorize_url="https://github.com/login/oauth/authorize", authorize_params=None, api_base_url="https://api.github.com/", - client_kwargs={"scope": "openid email profile"}, + # read:org is needed to check team membership at login + client_kwargs={"scope": "read:user read:org"}, ) diff --git a/src/makeitso/static/css/theme.css b/src/makeitso/static/css/theme.css index 6a1da55..f261e2f 100644 --- a/src/makeitso/static/css/theme.css +++ b/src/makeitso/static/css/theme.css @@ -14,7 +14,9 @@ font-display: swap; } -:root { +/* Also on [data-bs-theme=light], where Bootstrap resets its colours to the defaults */ +:root, +[data-bs-theme="light"] { --bs-body-font-family: "BC Sans", "Noto Sans", system-ui, -apple-system, "Segoe UI", sans-serif; --bc-primary-blue: #003366; --bc-bg-blue: #38598a; diff --git a/src/makeitso/templates/base.html b/src/makeitso/templates/base.html index 9260928..c441e51 100644 --- a/src/makeitso/templates/base.html +++ b/src/makeitso/templates/base.html @@ -25,13 +25,23 @@ {% block navbar_actions %}{% endblock %} {% if session.user %} {{ render_icon("activity") }} Jobs -