From ff4f43cc0930f139844a6f44326cae22d31c9415 Mon Sep 17 00:00:00 2001 From: Aniket Singh Rawat Date: Sat, 4 Nov 2023 12:11:25 +0530 Subject: [PATCH 1/2] Seperated out code for auth service. Signed-off-by: Aniket Singh Rawat --- src/teuthology_api/routes/login.py | 63 ++++++----------------- src/teuthology_api/services/auth.py | 80 +++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 47 deletions(-) create mode 100644 src/teuthology_api/services/auth.py diff --git a/src/teuthology_api/routes/login.py b/src/teuthology_api/routes/login.py index c4916a2..3deafc7 100644 --- a/src/teuthology_api/routes/login.py +++ b/src/teuthology_api/routes/login.py @@ -1,17 +1,14 @@ import logging import os -from fastapi import APIRouter, HTTPException, Request +from fastapi import APIRouter, HTTPException, Request, Depends from fastapi.responses import RedirectResponse from dotenv import load_dotenv -import httpx +from teuthology_api.services.auth import get_github_auth_service, AuthService load_dotenv() GH_CLIENT_ID = os.getenv("GH_CLIENT_ID") -GH_CLIENT_SECRET = os.getenv("GH_CLIENT_SECRET") GH_AUTHORIZATION_BASE_URL = os.getenv("GH_AUTHORIZATION_BASE_URL") -GH_TOKEN_URL = os.getenv("GH_TOKEN_URL") -GH_FETCH_MEMBERSHIP_URL = os.getenv("GH_FETCH_MEMBERSHIP_URL") PULPITO_URL = os.getenv("PULPITO_URL") log = logging.getLogger(__name__) @@ -38,56 +35,28 @@ async def github_login(): @router.get("/callback", status_code=200) -async def handle_callback(code: str, request: Request): +async def handle_callback(code: str, request: Request, auth_service: AuthService = Depends(get_github_auth_service)): """ Call back route after user login & authorize the app for access. """ - params = { - "client_id": GH_CLIENT_ID, - "client_secret": GH_CLIENT_SECRET, - "code": code, - } - headers = {"Accept": "application/json"} - async with httpx.AsyncClient() as client: - response_token = await client.post( - url=GH_TOKEN_URL, params=params, headers=headers - ) - log.info(response_token.json()) - response_token_dic = dict(response_token.json()) - token = response_token_dic.get("access_token") - if response_token_dic.get("error") or not token: - log.error("The code is incorrect or expired.") - raise HTTPException( - status_code=401, detail="The code is incorrect or expired." - ) - headers = {"Authorization": "token " + token} - response_org = await client.get(url=GH_FETCH_MEMBERSHIP_URL, headers=headers) - log.info(response_org.json()) - if response_org.status_code == 404: - log.error("User is not part of the Ceph Organization") - raise HTTPException( - status_code=404, - detail="User is not part of the Ceph Organization, please contact ", - ) - if response_org.status_code == 403: - log.error("The application doesn't have permission to view github org") - raise HTTPException( - status_code=403, - detail="The application doesn't have permission to view github org", - ) - response_org_dic = dict(response_org.json()) - data = { - "id": response_org_dic.get("user", {}).get("id"), - "username": response_org_dic.get("user", {}).get("login"), - "state": response_org_dic.get("state"), - "role": response_org_dic.get("role"), + token = await auth_service._get_token(status_code=code) + + response_org_dict = await auth_service._get_org(token=token) + + data = { + "id": response_org_dict.get("user", {}).get("id"), + "username": response_org_dict.get("user", {}).get("login"), + "state": response_org_dict.get("state"), + "role": response_org_dict.get("role"), "access_token": token, } - request.session["user"] = data + + request.session["user"] = data + cookie_data = { "username": data["username"], - "avatar_url": response_org_dic.get("user", {}).get("avatar_url"), + "avatar_url": response_org_dict.get("user", {}).get("avatar_url"), } cookie = "; ".join( [f"{str(key)}={str(value)}" for key, value in cookie_data.items()] diff --git a/src/teuthology_api/services/auth.py b/src/teuthology_api/services/auth.py new file mode 100644 index 0000000..3b425ad --- /dev/null +++ b/src/teuthology_api/services/auth.py @@ -0,0 +1,80 @@ +import abc +import os +import httpx +import logging +from dotenv import load_dotenv +from fastapi import HTTPException + +load_dotenv() +log = logging.getLogger(__name__) + +class AuthService(abc.ABC): + @abc.abstractmethod + async def _get_token(self, status_code: int) -> dict: + """Returns a dict of response JSON from GH.""" + pass + + @abc.abstractmethod + async def _get_org(self, token: str) -> dict: + """Returns org info of user.""" + pass + +class AuthServiceGH(AuthService): + + def __init__(self): + self.GH_CLIENT_ID = os.getenv("GH_CLIENT_ID") + self.GH_CLIENT_SECRET = os.getenv("GH_CLIENT_SECRET") + self.GH_AUTHORIZATION_BASE_URL = os.getenv("GH_AUTHORIZATION_BASE_URL") + self.GH_TOKEN_URL = os.getenv("GH_TOKEN_URL") + self.GH_FETCH_MEMBERSHIP_URL = os.getenv("GH_FETCH_MEMBERSHIP_URL") + self.PULPITO_URL = os.getenv("PULPITO_URL") + + async def _get_token(self, status_code: int) -> str: + params = { + "client_id": self.GH_CLIENT_ID, + "client_secret": self.GH_CLIENT_SECRET, + "code": status_code, + } + headers = {"Accept": "application/json"} + async with httpx.AsyncClient as client: + response_token = await client.post( + url=self.GH_TOKEN_URL, params=params, headers=headers + ) + log.info(response_token.json()) + response_token_dict = dict(response_token.json()) + token = response_token_dict.get("access_token") + if response_token_dict.get("error") or not token: + log.error("The code is incorrect or expired.") + raise HTTPException( + status_code=401, detail="The code is incorrect or expired." + ) + return token + + async def _get_org(self, token: str) -> dict: + headers = {"Authorization": "token " + token} + async with httpx.AsyncClient as client: + response_org = await client.get(url=self.GH_FETCH_MEMBERSHIP_URL, headers=headers) + response_org_dict = dict(response_org.json()) + log.info(response_org) + if response_org.status_code == 404: + log.error("User is not part of the Ceph Organization") + raise HTTPException( + status_code=404, + detail="User is not part of the Ceph Organization, please contact .", + ) + if response_org.status_code == 403: + log.error("The application doesn't have permission to view github org.") + raise HTTPException( + status_code=403, + detail="The application doesn't have permission to view github org.", + ) + return response_org_dict + +class AuthServiceMock(AuthService): + pass + +def get_github_auth_service(): + return AuthServiceGH() + +def get_mock_auth_service(): + return AuthServiceMock() From 9cd792bdcbac155f5d199dc2429a85f9b1d27410 Mon Sep 17 00:00:00 2001 From: Aniket Singh Rawat Date: Sat, 4 Nov 2023 12:18:48 +0530 Subject: [PATCH 2/2] Added mock auth service. Signed-off-by: Aniket Singh Rawat --- src/teuthology_api/services/auth.py | 36 ++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/teuthology_api/services/auth.py b/src/teuthology_api/services/auth.py index 3b425ad..5cd67de 100644 --- a/src/teuthology_api/services/auth.py +++ b/src/teuthology_api/services/auth.py @@ -71,7 +71,41 @@ async def _get_org(self, token: str) -> dict: return response_org_dict class AuthServiceMock(AuthService): - pass + async def _get_token(self, status_code: int) -> dict: + if status_code == 200: + return "admin" + elif status_code == 403: + return "user" + elif status_code == 404: + return "" + elif status_code == 500: + raise HTTPException( + status_code=401, detail="The code is incorrect or expired." + ) + else: + return "" + + async def _get_org(self, token: str) -> dict: + if token == "admin": + return { + "id": "admin_id", + "username": "admin", + "state": "state", + "role": "admin" + } + elif token == "": + log.error("The application doesn't have permission to view github org.") + raise HTTPException( + status_code=403, + detail="The application doesn't have permission to view github org.", + ) + else: + log.error("User is not part of the Ceph Organization") + raise HTTPException( + status_code=404, + detail="User is not part of the Ceph Organization, please contact .", + ) + def get_github_auth_service(): return AuthServiceGH()