From 6afebea7e1c6c33fa4755891b99abf8f5056cd21 Mon Sep 17 00:00:00 2001 From: Micha Albert Date: Fri, 23 Aug 2024 17:40:25 +0000 Subject: [PATCH] working submission (oauth) flow! --- onboard_live_backend/main.py | 73 +++++++++++-------- .../migration.sql | 10 +++ .../migration.sql | 26 +++++++ .../migration.sql | 2 + .../migration.sql | 25 +++++++ .../migration.sql | 21 ++++++ .../migration.sql | 22 ++++++ .../migration.sql | 22 ++++++ onboard_live_backend/schema.prisma | 27 +++---- 9 files changed, 183 insertions(+), 45 deletions(-) create mode 100644 onboard_live_backend/migrations/20240821135629_prep_for_oauth/migration.sql create mode 100644 onboard_live_backend/migrations/20240823141450_refactor_schema_for_oauth/migration.sql create mode 100644 onboard_live_backend/migrations/20240823141852_add_gh_token_field/migration.sql create mode 100644 onboard_live_backend/migrations/20240823145449_more_pr_schema_work/migration.sql create mode 100644 onboard_live_backend/migrations/20240823151408_remove_duplicated_field/migration.sql create mode 100644 onboard_live_backend/migrations/20240823151722_make_gh_user_id_required/migration.sql create mode 100644 onboard_live_backend/migrations/20240823152458_make_gh_user_id_an_int/migration.sql diff --git a/onboard_live_backend/main.py b/onboard_live_backend/main.py index 1aa2f52..3469d79 100644 --- a/onboard_live_backend/main.py +++ b/onboard_live_backend/main.py @@ -7,6 +7,7 @@ from random import choice from secrets import token_hex from typing import Dict, List +from fastapi.responses import HTMLResponse, RedirectResponse import httpx import uvicorn from apscheduler.schedulers.asyncio import AsyncIOScheduler @@ -17,6 +18,7 @@ from fastapi.middleware.cors import CORSMiddleware from prisma import Prisma from slack_bolt.adapter.fastapi.async_handler import AsyncSlackRequestHandler from slack_bolt.async_app import AsyncAck, AsyncApp +from cryptography.fernet import Fernet load_dotenv(dotenv_path="./.env") @@ -25,6 +27,8 @@ active_streams: List[Dict[str, str | bool]] = [] scheduler = AsyncIOScheduler() +FERNET = Fernet(os.environ["FERNET_KEY"]) + def verify_gh_signature(payload_body, secret_token, signature_header): """Verify that the payload was sent from GitHub by validating SHA256. @@ -155,6 +159,41 @@ bolt = AsyncApp( bolt_handler = AsyncSlackRequestHandler(bolt) +@api.get("/auth/github/login") +async def github_redirect(request: Request): + return RedirectResponse( + f"https://github.com/login/oauth/authorize?client_id={os.environ['GH_CLIENT_ID']}&redirect_uri=https://live.onboard.hackclub.com/auth/github/callback&scopes=read:user&state={request.query_params["state"]}" + ) + + +@api.get("/auth/github/callback") +async def github_callback(request: Request): + code: str = request.query_params["code"] + state: str = request.query_params["state"] + user_id, pr_id = FERNET.decrypt(bytes.fromhex(state)).decode().split("+") + db_user = await db.user.find_first_or_raise(where={"slack_id": user_id}) + db_pr = await db.pullrequest.find_first_or_raise(where={"github_id": int(pr_id)}) + async with httpx.AsyncClient() as client: + token = ( + await client.post( + "https://github.com/login/oauth/access_token", + json={ + "client_id": os.environ["GH_CLIENT_ID"], + "client_secret": os.environ["GH_CLIENT_SECRET"], + "code": code, + "redirect_uri": "https://live.onboard.hackclub.com/auth/github/callback", + }, + headers={"Accept": "application/json"}, + ) + ).json()["access_token"] + + gh_user = (await client.get("https://api.github.com/user", headers={"Accept": "application/vnd.github.v3+json", "Authorization": f"Bearer {token}"})).json()["id"] + if gh_user == db_pr.gh_user_id: + await db.pullrequest.update({"user": {"connect": {"id": db_user.id}}, "gh_user_id": gh_user}, {"id": db_pr.id}) + return HTMLResponse("

Success! Your PR has been linked to your account. Check your Slack DMs for the next steps!

") + return HTMLResponse("

Looks like something went wrong! DM @mra on slack.

", status_code=403) + + @api.post("/api/v1/github/pr_event") async def pr_event(request: Request): verify_gh_signature( @@ -166,31 +205,7 @@ async def pr_event(request: Request): if body["action"] == "labeled": if body["label"]["id"] == 7336079497: print("Added label has same id as OBL label!") - async with httpx.AsyncClient() as client: - db_pr = await db.pullrequest.create({"github_id": body["number"]}) - db_pr_token = db_pr.token - await client.post( - f"https://api.github.com/repos/hackclub/OnBoard/issues/{body["issue"]["number"]}/comments", - headers={ - "Authorization": f"token {os.environ['GH_TOKEN']}", - "Accept": "application/vnd.github.v3+json", - }, - json={ - "body": f"Hey, I'm Micha, a.k.a `@mra` on Slack! It looks like this is an OnBoard Live submission. If that sounds right, then go to the #onboard-live channel on Slack and send the message `/onboard-live-submit {db_pr_token}`. Doing that helps us link this pull request to your Slack account lets you select your sessions for review.\n###### If you have no clue what OnBoard Live is, please disregard this automated message!" - }, - ) - elif "created" in body and "comment" in body: - if body["comment"]["user"]["id"] == body["issue"]["user"]["id"]: - db_pr = await db.pullrequest.find_first(where={"github_id": body["issue"]["number"]}) - if db_pr: - if db_pr.possible_users: - for user in db_pr.possible_users: - if hashlib.sha256(bytes(f"{db_pr.secondary_token}+{user.slack_id}", encoding="utf-8")).hexdigest() in body["comment"]["body"]: - # Yay, the user who ran the Slack submit command is the same user who submitted the PR! - db_pr.user = user - break - else: - print("possible users was none") + await db.pullrequest.create({"github_id": body["pull_request"]["number"], "gh_user_id": body["pull_request"]["user"]["id"]}) return @@ -409,20 +424,18 @@ async def submit(ack: AsyncAck, command): user_id = command["user_id"] channel_id = command["channel_id"] text = command["text"] - db_pr = await db.pullrequest.find_first(where={"token": text}) - db_user = await db.user.find_first_or_raise(where={"slack_id": user_id}) + db_pr = await db.pullrequest.find_first(where={"github_id": int(text)}) if db_pr is None: await bolt.client.chat_postEphemeral( channel=channel_id, user=user_id, - text="There doesn't seem to be a PR open with that token! If this seems like a mistake, please message <@U05C64XMMHV> about it!", + text="There doesn't seem to be a PR open with that ID! If this seems like a mistake, please message <@U05C64XMMHV> about it!", ) return - await db.pullrequest.update(where={"id": db_pr.id}, data={"possible_users": {"set": [{"id": db_user.id}]}}) await bolt.client.chat_postEphemeral( channel=channel_id, user=user_id, - text=f"Please go to and add a comment containing the secret code `{hashlib.sha256(bytes(f"{db_pr.secondary_token}+{user_id}", encoding="utf-8")).hexdigest()}`. This helps us make sure this is your PR!", + text=f"Please to authenticate with GitHub. This helps us verify that this is your PR!", ) diff --git a/onboard_live_backend/migrations/20240821135629_prep_for_oauth/migration.sql b/onboard_live_backend/migrations/20240821135629_prep_for_oauth/migration.sql new file mode 100644 index 0000000..4b1ef4e --- /dev/null +++ b/onboard_live_backend/migrations/20240821135629_prep_for_oauth/migration.sql @@ -0,0 +1,10 @@ +/* + Warnings: + + - You are about to drop the `_PullRequestToPossibleUser` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropTable +PRAGMA foreign_keys=off; +DROP TABLE "_PullRequestToPossibleUser"; +PRAGMA foreign_keys=on; diff --git a/onboard_live_backend/migrations/20240823141450_refactor_schema_for_oauth/migration.sql b/onboard_live_backend/migrations/20240823141450_refactor_schema_for_oauth/migration.sql new file mode 100644 index 0000000..09deab9 --- /dev/null +++ b/onboard_live_backend/migrations/20240823141450_refactor_schema_for_oauth/migration.sql @@ -0,0 +1,26 @@ +/* + Warnings: + + - You are about to drop the column `known_user_id` on the `PullRequest` table. All the data in the column will be lost. + - You are about to drop the column `secondary_token` on the `PullRequest` table. All the data in the column will be lost. + - You are about to drop the column `token` on the `PullRequest` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "github_user_id" TEXT; + +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_PullRequest" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "github_id" INTEGER NOT NULL, + "user_id" TEXT, + CONSTRAINT "PullRequest_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); +INSERT INTO "new_PullRequest" ("github_id", "id") SELECT "github_id", "id" FROM "PullRequest"; +DROP TABLE "PullRequest"; +ALTER TABLE "new_PullRequest" RENAME TO "PullRequest"; +CREATE UNIQUE INDEX "PullRequest_github_id_key" ON "PullRequest"("github_id"); +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/onboard_live_backend/migrations/20240823141852_add_gh_token_field/migration.sql b/onboard_live_backend/migrations/20240823141852_add_gh_token_field/migration.sql new file mode 100644 index 0000000..994913c --- /dev/null +++ b/onboard_live_backend/migrations/20240823141852_add_gh_token_field/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "github_token" TEXT; diff --git a/onboard_live_backend/migrations/20240823145449_more_pr_schema_work/migration.sql b/onboard_live_backend/migrations/20240823145449_more_pr_schema_work/migration.sql new file mode 100644 index 0000000..cce7526 --- /dev/null +++ b/onboard_live_backend/migrations/20240823145449_more_pr_schema_work/migration.sql @@ -0,0 +1,25 @@ +/* + Warnings: + + - You are about to drop the column `github_token` on the `User` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "PullRequest" ADD COLUMN "gh_user_id" TEXT; + +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_User" ( + "id" TEXT NOT NULL PRIMARY KEY, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "slack_id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "github_user_id" TEXT +); +INSERT INTO "new_User" ("created_at", "github_user_id", "id", "name", "slack_id") SELECT "created_at", "github_user_id", "id", "name", "slack_id" FROM "User"; +DROP TABLE "User"; +ALTER TABLE "new_User" RENAME TO "User"; +CREATE UNIQUE INDEX "User_slack_id_key" ON "User"("slack_id"); +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/onboard_live_backend/migrations/20240823151408_remove_duplicated_field/migration.sql b/onboard_live_backend/migrations/20240823151408_remove_duplicated_field/migration.sql new file mode 100644 index 0000000..815c31e --- /dev/null +++ b/onboard_live_backend/migrations/20240823151408_remove_duplicated_field/migration.sql @@ -0,0 +1,21 @@ +/* + Warnings: + + - You are about to drop the column `github_user_id` on the `User` table. All the data in the column will be lost. + +*/ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_User" ( + "id" TEXT NOT NULL PRIMARY KEY, + "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "slack_id" TEXT NOT NULL, + "name" TEXT NOT NULL +); +INSERT INTO "new_User" ("created_at", "id", "name", "slack_id") SELECT "created_at", "id", "name", "slack_id" FROM "User"; +DROP TABLE "User"; +ALTER TABLE "new_User" RENAME TO "User"; +CREATE UNIQUE INDEX "User_slack_id_key" ON "User"("slack_id"); +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/onboard_live_backend/migrations/20240823151722_make_gh_user_id_required/migration.sql b/onboard_live_backend/migrations/20240823151722_make_gh_user_id_required/migration.sql new file mode 100644 index 0000000..57b2450 --- /dev/null +++ b/onboard_live_backend/migrations/20240823151722_make_gh_user_id_required/migration.sql @@ -0,0 +1,22 @@ +/* + Warnings: + + - Made the column `gh_user_id` on table `PullRequest` required. This step will fail if there are existing NULL values in that column. + +*/ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_PullRequest" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "github_id" INTEGER NOT NULL, + "user_id" TEXT, + "gh_user_id" TEXT NOT NULL, + CONSTRAINT "PullRequest_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); +INSERT INTO "new_PullRequest" ("gh_user_id", "github_id", "id", "user_id") SELECT "gh_user_id", "github_id", "id", "user_id" FROM "PullRequest"; +DROP TABLE "PullRequest"; +ALTER TABLE "new_PullRequest" RENAME TO "PullRequest"; +CREATE UNIQUE INDEX "PullRequest_github_id_key" ON "PullRequest"("github_id"); +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/onboard_live_backend/migrations/20240823152458_make_gh_user_id_an_int/migration.sql b/onboard_live_backend/migrations/20240823152458_make_gh_user_id_an_int/migration.sql new file mode 100644 index 0000000..ccfb293 --- /dev/null +++ b/onboard_live_backend/migrations/20240823152458_make_gh_user_id_an_int/migration.sql @@ -0,0 +1,22 @@ +/* + Warnings: + + - You are about to alter the column `gh_user_id` on the `PullRequest` table. The data in that column could be lost. The data in that column will be cast from `String` to `Int`. + +*/ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_PullRequest" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "github_id" INTEGER NOT NULL, + "user_id" TEXT, + "gh_user_id" INTEGER NOT NULL, + CONSTRAINT "PullRequest_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); +INSERT INTO "new_PullRequest" ("gh_user_id", "github_id", "id", "user_id") SELECT "gh_user_id", "github_id", "id", "user_id" FROM "PullRequest"; +DROP TABLE "PullRequest"; +ALTER TABLE "new_PullRequest" RENAME TO "PullRequest"; +CREATE UNIQUE INDEX "PullRequest_github_id_key" ON "PullRequest"("github_id"); +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/onboard_live_backend/schema.prisma b/onboard_live_backend/schema.prisma index f10598b..59ff94a 100644 --- a/onboard_live_backend/schema.prisma +++ b/onboard_live_backend/schema.prisma @@ -1,7 +1,7 @@ generator client { provider = "prisma-client-py" - interface = "asyncio" recursive_type_depth = "5" + interface = "asyncio" } datasource db { @@ -10,13 +10,12 @@ datasource db { } model User { - id String @id @default(cuid()) - created_at DateTime @default(now()) - slack_id String @unique - name String - stream Stream? - possible_pulls PullRequest[] @relation(name: "PullRequestToPossibleUser") // pull requests that this user has tried to claim via the /onboard-live-submit command on Slack - known_pulls PullRequest[] @relation(name: "PullRequestToKnownUser") // pull requests that have been verified to belong to this user + id String @id @default(cuid()) + created_at DateTime @default(now()) + slack_id String @unique + name String + pull_requests PullRequest[] @relation("PullRequestToUser") + stream Stream? } model Stream { @@ -30,11 +29,9 @@ model Stream { } model PullRequest { - id Int @id @default(autoincrement()) - github_id Int @unique - user User? @relation(name: "PullRequestToKnownUser", fields: [known_user_id], references: [id]) - known_user_id String? - token String @unique @default(uuid()) - secondary_token String @unique @default(uuid()) - possible_users User[] @relation(name: "PullRequestToPossibleUser") + id Int @id @default(autoincrement()) + github_id Int @unique + user_id String? + gh_user_id Int + user User? @relation("PullRequestToUser", fields: [user_id], references: [id]) }