From 4dd8b4c79b4640b885bb65926f04521800dbdc5a Mon Sep 17 00:00:00 2001 From: kompotkot Date: Wed, 21 Jul 2021 14:30:29 +0000 Subject: [PATCH] User and token endpoints with updated middleware --- backend/{moonstream => }/README.md | 0 backend/moonstream/actions.py | 2 + backend/moonstream/api.py | 39 ++------ backend/moonstream/middleware.py | 63 +++++++++++++ backend/moonstream/routes/__init__.py | 0 backend/moonstream/routes/users.py | 108 ++++++++++++++++++++++ backend/moonstream/settings.py | 21 ++++- backend/{moonstream => }/requirements.txt | 4 +- backend/sample.env | 4 +- 9 files changed, 208 insertions(+), 33 deletions(-) rename backend/{moonstream => }/README.md (100%) create mode 100644 backend/moonstream/actions.py create mode 100644 backend/moonstream/middleware.py create mode 100644 backend/moonstream/routes/__init__.py create mode 100644 backend/moonstream/routes/users.py rename backend/{moonstream => }/requirements.txt (92%) diff --git a/backend/moonstream/README.md b/backend/README.md similarity index 100% rename from backend/moonstream/README.md rename to backend/README.md diff --git a/backend/moonstream/actions.py b/backend/moonstream/actions.py new file mode 100644 index 00000000..139597f9 --- /dev/null +++ b/backend/moonstream/actions.py @@ -0,0 +1,2 @@ + + diff --git a/backend/moonstream/api.py b/backend/moonstream/api.py index 06dfa581..506187f3 100644 --- a/backend/moonstream/api.py +++ b/backend/moonstream/api.py @@ -2,43 +2,21 @@ The Moonstream HTTP API """ import logging -from typing import Any, Dict, List, Optional -import uuid -from fastapi import ( - BackgroundTasks, - Depends, - FastAPI, - Form, - HTTPException, - Path, - Query, - Request, - Response, -) +from bugout.data import BugoutUser +from fastapi import FastAPI, Form from fastapi.middleware.cors import CORSMiddleware -from fastapi.security import OAuth2PasswordRequestForm from . import data -from .settings import DOCS_TARGET_PATH, ORIGINS +from .routes.users import app as users_api +from .settings import ORIGINS, bugout_client as bc, MOONSTREAM_APPLICATION_ID from .version import MOONSTREAM_VERSION logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -tags_metadata = [{"name": "users", "description": "Operations with users."}] +app = FastAPI(openapi_url=None) -app = FastAPI( - title=f"Moonstream API.", - description="The Bugout blockchain inspector API.", - version=MOONSTREAM_VERSION, - openapi_tags=tags_metadata, - openapi_url="/openapi.json", - docs_url=None, - redoc_url=f"/{DOCS_TARGET_PATH}", -) - -# CORS settings app.add_middleware( CORSMiddleware, allow_origins=ORIGINS, @@ -49,10 +27,13 @@ app.add_middleware( @app.get("/ping", response_model=data.PingResponse) -async def ping() -> data.PingResponse: +async def ping_handler() -> data.PingResponse: return data.PingResponse(status="ok") @app.get("/version", response_model=data.VersionResponse) -async def version() -> data.VersionResponse: +async def version_handler() -> data.VersionResponse: return data.VersionResponse(version=MOONSTREAM_VERSION) + + +app.mount("/users", users_api) diff --git a/backend/moonstream/middleware.py b/backend/moonstream/middleware.py new file mode 100644 index 00000000..cb9919b0 --- /dev/null +++ b/backend/moonstream/middleware.py @@ -0,0 +1,63 @@ +import logging +from typing import Awaitable, Callable, Dict, List, Optional + +from bugout.data import BugoutUser +from bugout.exceptions import BugoutResponseException +from starlette.middleware.base import BaseHTTPMiddleware +from fastapi import Request, Response + +from .settings import bugout_client as bc + +logger = logging.getLogger(__name__) + + +class BroodAuthMiddleware(BaseHTTPMiddleware): + """ + Checks the authorization header on the request. If it represents a verified Brood user, + create another request and get groups user belongs to, after this + adds a brood_user attribute to the request.state. Otherwise raises a 403 error. + """ + + def __init__(self, app, whitelist: Optional[Dict[str, str]] = None): + self.whitelist: Dict[str, str] = {} + if whitelist is not None: + self.whitelist = whitelist + super().__init__(app) + + async def dispatch( + self, request: Request, call_next: Callable[[Request], Awaitable[Response]] + ): + path = request.url.path.rstrip("/") + method = request.method + if path in self.whitelist.keys() and self.whitelist[path] == method: + return await call_next(request) + + authorization_header = request.headers.get("authorization") + if authorization_header is None: + return Response( + status_code=403, content="No authorization header passed with request" + ) + user_token_list = authorization_header.split() + if len(user_token_list) != 2: + return Response(status_code=403, content="Wrong authorization header") + user_token: str = user_token_list[-1] + + try: + user: BugoutUser = bc.get_user(user_token) + if not user.verified: + logger.info( + f"Attempted journal access by unverified Brood account: {user.id}" + ) + return Response( + status_code=403, + content="Only verified accounts can access journals", + ) + except BugoutResponseException as e: + return Response(status_code=e.status_code, content=e.detail) + except Exception as e: + logger.error(f"Error processing Brood response: {str(e)}") + return Response(status_code=500, content="Internal server error") + + request.state.user = user + request.state.token = user_token + return await call_next(request) diff --git a/backend/moonstream/routes/__init__.py b/backend/moonstream/routes/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/moonstream/routes/users.py b/backend/moonstream/routes/users.py new file mode 100644 index 00000000..ee2d942c --- /dev/null +++ b/backend/moonstream/routes/users.py @@ -0,0 +1,108 @@ +""" +The Moonstream users HTTP API +""" +import logging +from typing import Any, Dict +import uuid + +from bugout.data import BugoutToken, BugoutUser +from bugout.exceptions import BugoutResponseException +from fastapi import ( + FastAPI, + Form, + HTTPException, + Request, +) +from fastapi.middleware.cors import CORSMiddleware + +from ..middleware import BroodAuthMiddleware +from ..settings import ( + MOONSTREAM_APPLICATION_ID, + DOCS_TARGET_PATH, + ORIGINS, + DOCS_PATHS, + bugout_client as bc, +) +from ..version import MOONSTREAM_VERSION + +logger = logging.getLogger(__name__) + +tags_metadata = [ + {"name": "users", "description": "Operations with users."}, + {"name": "tokens", "description": "Operations with user tokens."}, +] + +app = FastAPI( + title=f"Moonstream API.", + description="The Bugout blockchain inspector API.", + version=MOONSTREAM_VERSION, + openapi_tags=tags_metadata, + openapi_url="/openapi.json", + docs_url=None, + redoc_url=f"/{DOCS_TARGET_PATH}", +) + +app.add_middleware( + CORSMiddleware, + allow_origins=ORIGINS, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +whitelist_paths: Dict[str, str] = {} +whitelist_paths.update(DOCS_PATHS) +whitelist_paths.update({"/users": "POST", "/users/tokens": "POST"}) +app.add_middleware(BroodAuthMiddleware, whitelist=whitelist_paths) + + +@app.post("/", tags=["users"], response_model=BugoutUser) +async def create_user_handler( + username: str = Form(...), email: str = Form(...), password: str = Form(...) +) -> BugoutUser: + try: + user: BugoutUser = bc.create_user( + username, email, password, MOONSTREAM_APPLICATION_ID + ) + except BugoutResponseException as e: + return HTTPException(status_code=e.status_code, detail=e.detail) + except Exception as e: + return HTTPException(status_code=500) + return user + + +@app.get("/", tags=["users"], response_model=BugoutUser) +async def get_user_handler(request: Request) -> BugoutUser: + user: BugoutUser = request.state.user + if str(user.application_id) != str(MOONSTREAM_APPLICATION_ID): + raise HTTPException( + status_code=403, detail="User does not belong to this application" + ) + return user + + +@app.post("/tokens", tags=["tokens"], response_model=BugoutToken) +async def login_handler( + username: str = Form(...), password: str = Form(...) +) -> BugoutToken: + try: + token: BugoutToken = bc.create_token( + username, password, MOONSTREAM_APPLICATION_ID + ) + except BugoutResponseException as e: + return HTTPException(status_code=e.status_code) + except Exception as e: + return HTTPException(status_code=500) + return token + + +@app.delete("/tokens", tags=["tokens"], response_model=uuid.UUID) +async def logout_handler(request: Request) -> uuid.UUID: + token = request.state.token + try: + token_id: uuid.UUID = bc.revoke_token(token) + except BugoutResponseException as e: + return HTTPException(status_code=e.status_code, detail=e.detail) + except Exception as e: + return HTTPException(status_code=500) + return token_id diff --git a/backend/moonstream/settings.py b/backend/moonstream/settings.py index ae8f3cbe..d94a0f31 100644 --- a/backend/moonstream/settings.py +++ b/backend/moonstream/settings.py @@ -1,10 +1,24 @@ import os +from bugout.app import Bugout + +# Bugout +# TODO(kompotkot): CHANGE TO PROD!!!!!!! +bugout_client = Bugout("http://127.0.0.1:7474", "http://127.0.0.1:7475") + +MOONSTREAM_APPLICATION_ID = os.environ.get("MOONSTREAM_APPLICATION_ID") +if MOONSTREAM_APPLICATION_ID is None: + raise ValueError("MOONSTREAM_APPLICATION_ID environment variable must be set") + +MOONSTREAM_DATA_JOURNAL_ID = os.environ.get("MOONSTREAM_DATA_JOURNAL_ID") +if MOONSTREAM_DATA_JOURNAL_ID is None: + raise ValueError("MOONSTREAM_DATA_JOURNAL_ID environment variable must be set") + # Origin RAW_ORIGIN = os.environ.get("MOONSTREAM_CORS_ALLOWED_ORIGINS") if RAW_ORIGIN is None: raise ValueError( - "MOONSTREAM_CORS_ALLOWED_ORIGINS environment variable must be set (comma-separated list of CORS allowed origins" + "MOONSTREAM_CORS_ALLOWED_ORIGINS environment variable must be set (comma-separated list of CORS allowed origins)" ) ORIGINS = RAW_ORIGIN.split(",") @@ -14,3 +28,8 @@ MOONSTREAM_OPENAPI_LIST = [] MOONSTREAM_OPENAPI_LIST_RAW = os.environ.get("MOONSTREAM_OPENAPI_LIST") if MOONSTREAM_OPENAPI_LIST_RAW is not None: MOONSTREAM_OPENAPI_LIST = MOONSTREAM_OPENAPI_LIST_RAW.split(",") + +DOCS_PATHS = {} +for path in MOONSTREAM_OPENAPI_LIST: + DOCS_PATHS[f"/{path}/{DOCS_TARGET_PATH}"] = "GET" + DOCS_PATHS[f"/{path}/{DOCS_TARGET_PATH}/openapi.json"] = "GET" diff --git a/backend/moonstream/requirements.txt b/backend/requirements.txt similarity index 92% rename from backend/moonstream/requirements.txt rename to backend/requirements.txt index fa0f14a6..fd708234 100644 --- a/backend/moonstream/requirements.txt +++ b/backend/requirements.txt @@ -3,7 +3,7 @@ asgiref==3.4.1 black==21.7b0 boto3==1.18.1 botocore==1.21.1 -bugout==0.1.12 +bugout==0.1.13 certifi==2021.5.30 charset-normalizer==2.0.3 click==8.0.1 @@ -14,9 +14,9 @@ jmespath==0.10.0 mypy==0.910 mypy-extensions==0.4.3 pathspec==0.9.0 -pkg-resources==0.0.0 pydantic==1.8.2 python-dateutil==2.8.2 +python-multipart-0.0.5 regex==2021.7.6 requests==2.26.0 s3transfer==0.5.0 diff --git a/backend/sample.env b/backend/sample.env index 58995f0e..9b631c8b 100644 --- a/backend/sample.env +++ b/backend/sample.env @@ -1,2 +1,4 @@ export MOONSTREAM_CORS_ALLOWED_ORIGINS="http://localhost:3000,https://moonstream.to" -export MOONSTREAM_OPENAPI_LIST="" +export MOONSTREAM_OPENAPI_LIST="subscriptions" +export MOONSTREAM_APPLICATION_ID="" +export MOONSTREAM_DATA_JOURNAL_ID=""