working submission (oauth) flow!

This commit is contained in:
Micha Albert 2024-08-23 17:40:25 +00:00
parent 25dbca5c56
commit 6afebea7e1
9 changed files with 183 additions and 45 deletions

View file

@ -7,6 +7,7 @@ from random import choice
from secrets import token_hex from secrets import token_hex
from typing import Dict, List from typing import Dict, List
from fastapi.responses import HTMLResponse, RedirectResponse
import httpx import httpx
import uvicorn import uvicorn
from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.schedulers.asyncio import AsyncIOScheduler
@ -17,6 +18,7 @@ from fastapi.middleware.cors import CORSMiddleware
from prisma import Prisma from prisma import Prisma
from slack_bolt.adapter.fastapi.async_handler import AsyncSlackRequestHandler from slack_bolt.adapter.fastapi.async_handler import AsyncSlackRequestHandler
from slack_bolt.async_app import AsyncAck, AsyncApp from slack_bolt.async_app import AsyncAck, AsyncApp
from cryptography.fernet import Fernet
load_dotenv(dotenv_path="./.env") load_dotenv(dotenv_path="./.env")
@ -25,6 +27,8 @@ active_streams: List[Dict[str, str | bool]] = []
scheduler = AsyncIOScheduler() scheduler = AsyncIOScheduler()
FERNET = Fernet(os.environ["FERNET_KEY"])
def verify_gh_signature(payload_body, secret_token, signature_header): def verify_gh_signature(payload_body, secret_token, signature_header):
"""Verify that the payload was sent from GitHub by validating SHA256. """Verify that the payload was sent from GitHub by validating SHA256.
@ -155,6 +159,41 @@ bolt = AsyncApp(
bolt_handler = AsyncSlackRequestHandler(bolt) 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("<h1>Success! Your PR has been linked to your account. Check your Slack DMs for the next steps!</h1>")
return HTMLResponse("<h1>Looks like something went wrong! DM @mra on slack.</h1>", status_code=403)
@api.post("/api/v1/github/pr_event") @api.post("/api/v1/github/pr_event")
async def pr_event(request: Request): async def pr_event(request: Request):
verify_gh_signature( verify_gh_signature(
@ -166,31 +205,7 @@ async def pr_event(request: Request):
if body["action"] == "labeled": if body["action"] == "labeled":
if body["label"]["id"] == 7336079497: if body["label"]["id"] == 7336079497:
print("Added label has same id as OBL label!") print("Added label has same id as OBL label!")
async with httpx.AsyncClient() as client: await db.pullrequest.create({"github_id": body["pull_request"]["number"], "gh_user_id": body["pull_request"]["user"]["id"]})
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")
return return
@ -409,20 +424,18 @@ async def submit(ack: AsyncAck, command):
user_id = command["user_id"] user_id = command["user_id"]
channel_id = command["channel_id"] channel_id = command["channel_id"]
text = command["text"] text = command["text"]
db_pr = await db.pullrequest.find_first(where={"token": text}) db_pr = await db.pullrequest.find_first(where={"github_id": int(text)})
db_user = await db.user.find_first_or_raise(where={"slack_id": user_id})
if db_pr is None: if db_pr is None:
await bolt.client.chat_postEphemeral( await bolt.client.chat_postEphemeral(
channel=channel_id, channel=channel_id,
user=user_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 return
await db.pullrequest.update(where={"id": db_pr.id}, data={"possible_users": {"set": [{"id": db_user.id}]}})
await bolt.client.chat_postEphemeral( await bolt.client.chat_postEphemeral(
channel=channel_id, channel=channel_id,
user=user_id, user=user_id,
text=f"Please go to <https://github.com/hackclub/OnBoard/pull/{db_pr.github_id}|your pull request> 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 <https://live.onboard.hackclub.com/auth/github/login?state={FERNET.encrypt(bytes(f"{user_id}+{db_pr.github_id}", 'utf-8')).hex()}|click here> to authenticate with GitHub. This helps us verify that this is your PR!",
) )

View file

@ -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;

View file

@ -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;

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "github_token" TEXT;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -1,7 +1,7 @@
generator client { generator client {
provider = "prisma-client-py" provider = "prisma-client-py"
interface = "asyncio"
recursive_type_depth = "5" recursive_type_depth = "5"
interface = "asyncio"
} }
datasource db { datasource db {
@ -10,13 +10,12 @@ datasource db {
} }
model User { model User {
id String @id @default(cuid()) id String @id @default(cuid())
created_at DateTime @default(now()) created_at DateTime @default(now())
slack_id String @unique slack_id String @unique
name String name String
stream Stream? pull_requests PullRequest[] @relation("PullRequestToUser")
possible_pulls PullRequest[] @relation(name: "PullRequestToPossibleUser") // pull requests that this user has tried to claim via the /onboard-live-submit command on Slack stream Stream?
known_pulls PullRequest[] @relation(name: "PullRequestToKnownUser") // pull requests that have been verified to belong to this user
} }
model Stream { model Stream {
@ -30,11 +29,9 @@ model Stream {
} }
model PullRequest { model PullRequest {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
github_id Int @unique github_id Int @unique
user User? @relation(name: "PullRequestToKnownUser", fields: [known_user_id], references: [id]) user_id String?
known_user_id String? gh_user_id Int
token String @unique @default(uuid()) user User? @relation("PullRequestToUser", fields: [user_id], references: [id])
secondary_token String @unique @default(uuid())
possible_users User[] @relation(name: "PullRequestToPossibleUser")
} }