From 2606f2921be679c94edf43d997e060a64721ba9b Mon Sep 17 00:00:00 2001 From: Joey Grable Date: Thu, 21 Sep 2023 17:19:05 -0700 Subject: [PATCH] removed fastapi-auth0 in favor of a custom solution with better control --- ..._added_roles_to_user_table_and_removed_.py | 44 +++ ...f7e_updated_website_keyword_tables_and_.py | 42 +++ app/api/deps/permissions.py | 3 +- app/api/v1/endpoints/users.py | 12 +- app/api/v1/endpoints/web_pages.py | 6 +- app/core/auth.py | 9 - app/core/auth/__init__.py | 19 ++ app/core/auth/auth0.py | 297 ++++++++++++++++++ app/core/utilities/email.py | 2 +- app/crud/base.py | 2 +- app/models/geocoord.py | 3 +- app/models/user.py | 15 +- app/schemas/__init__.py | 1 + app/schemas/user.py | 5 +- app/schemas/user_roles.py | 10 + poetry.lock | 17 +- pyproject.toml | 2 +- tests/api/deps/test_get_current_user.py | 2 +- tests/api/v1/clients/test_clients_create.py | 11 +- tests/api/v1/clients/test_clients_update.py | 10 +- tests/api/v1/websites/test_websites_create.py | 9 +- tests/api/v1/websites/test_websites_update.py | 9 +- .../test_websites_pages_create.py | 4 +- .../test_websites_pages_update.py | 4 +- .../test_websites_sitemaps_create.py | 4 +- .../test_websites_sitemaps_update.py | 4 +- 26 files changed, 485 insertions(+), 61 deletions(-) create mode 100644 alembic/versions/7a37c4da2a88_added_roles_to_user_table_and_removed_.py create mode 100644 alembic/versions/8d2203595f7e_updated_website_keyword_tables_and_.py delete mode 100644 app/core/auth.py create mode 100644 app/core/auth/__init__.py create mode 100644 app/core/auth/auth0.py create mode 100644 app/schemas/user_roles.py diff --git a/alembic/versions/7a37c4da2a88_added_roles_to_user_table_and_removed_.py b/alembic/versions/7a37c4da2a88_added_roles_to_user_table_and_removed_.py new file mode 100644 index 00000000..e50b4989 --- /dev/null +++ b/alembic/versions/7a37c4da2a88_added_roles_to_user_table_and_removed_.py @@ -0,0 +1,44 @@ +"""added roles to user table and removed LONGTEXT to pass sqlite tests + +Revision ID: 7a37c4da2a88 +Revises: 8d2203595f7e +Create Date: 2023-09-21 17:06:50.116644 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '7a37c4da2a88' +down_revision = '8d2203595f7e' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('user', sa.Column('roles', sa.JSON(), nullable=False)) + op.alter_column('website_keywordcorpus', 'corpus', + existing_type=mysql.LONGTEXT(), + type_=sa.Text(length=4000000000), + existing_nullable=False) + op.alter_column('website_keywordcorpus', 'rawtext', + existing_type=mysql.LONGTEXT(), + type_=sa.Text(length=4000000000), + existing_nullable=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('website_keywordcorpus', 'rawtext', + existing_type=sa.Text(length=4000000000), + type_=mysql.LONGTEXT(), + existing_nullable=False) + op.alter_column('website_keywordcorpus', 'corpus', + existing_type=sa.Text(length=4000000000), + type_=mysql.LONGTEXT(), + existing_nullable=False) + op.drop_column('user', 'roles') + # ### end Alembic commands ### diff --git a/alembic/versions/8d2203595f7e_updated_website_keyword_tables_and_.py b/alembic/versions/8d2203595f7e_updated_website_keyword_tables_and_.py new file mode 100644 index 00000000..e6b2458b --- /dev/null +++ b/alembic/versions/8d2203595f7e_updated_website_keyword_tables_and_.py @@ -0,0 +1,42 @@ +"""updated website keyword tables and username in user table + +Revision ID: 8d2203595f7e +Revises: 4bb1a741a074 +Create Date: 2023-09-21 16:12:54.683735 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '8d2203595f7e' +down_revision = '4bb1a741a074' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('website_keywordcorpus', 'corpus', + existing_type=mysql.LONGTEXT(), + type_=sa.Text(length=4000000000), + existing_nullable=False) + op.alter_column('website_keywordcorpus', 'rawtext', + existing_type=mysql.LONGTEXT(), + type_=sa.Text(length=4000000000), + existing_nullable=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('website_keywordcorpus', 'rawtext', + existing_type=sa.Text(length=4000000000), + type_=mysql.LONGTEXT(), + existing_nullable=False) + op.alter_column('website_keywordcorpus', 'corpus', + existing_type=sa.Text(length=4000000000), + type_=mysql.LONGTEXT(), + existing_nullable=False) + # ### end Alembic commands ### diff --git a/app/api/deps/permissions.py b/app/api/deps/permissions.py index f90e7627..e796f059 100644 --- a/app/api/deps/permissions.py +++ b/app/api/deps/permissions.py @@ -1,12 +1,11 @@ from typing import Annotated, Any, List from fastapi import Depends, HTTPException, Security, status -from fastapi_auth0 import Auth0User from fastapi_permissions import Authenticated # type: ignore # noqa: E501 from fastapi_permissions import Everyone, configure_permissions, has_permission from app.api.errors import ErrorCode -from app.core.auth import auth +from app.core.auth import Auth0User, auth def get_current_user(user: Auth0User | None = Security(auth.get_user)) -> Auth0User: diff --git a/app/api/v1/endpoints/users.py b/app/api/v1/endpoints/users.py index 84f5dfa0..0ad64e4b 100644 --- a/app/api/v1/endpoints/users.py +++ b/app/api/v1/endpoints/users.py @@ -1,3 +1,5 @@ +from typing import List + from fastapi import APIRouter, Depends from app.api.deps import AsyncDatabaseSession, CurrentUser, get_async_db @@ -9,6 +11,7 @@ from app.models.user import User from app.schemas import UserRead from app.schemas.user import UserCreate +from app.schemas.user_roles import UserRole router: APIRouter = APIRouter() @@ -32,17 +35,16 @@ async def users_current( field_name="auth_id", field_value=current_user.id ) if not user: - is_admin: bool = False - if current_user.permissions and "access:admin" in current_user.permissions: - is_admin = True + user_roles: List[UserRole] = current_user.roles \ + if current_user.roles else [UserRole.USER] user = await users_repo.create( UserCreate( auth_id=current_user.id, email=current_user.email, - username=current_user.email, - is_superuser=is_admin, + is_superuser=False, is_verified=False, is_active=True, + roles=user_roles, ) ) return UserRead.model_validate(user) diff --git a/app/api/v1/endpoints/web_pages.py b/app/api/v1/endpoints/web_pages.py index abe2503e..db864cb9 100644 --- a/app/api/v1/endpoints/web_pages.py +++ b/app/api/v1/endpoints/web_pages.py @@ -47,7 +47,11 @@ async def website_page_list( website_id=query.website_id, sitemap_id=query.sitemap_id, ) - return [WebsitePageRead.model_validate(w) for w in website_list] if website_list else [] + return ( + [WebsitePageRead.model_validate(w) for w in website_list] + if website_list + else [] + ) @router.post( diff --git a/app/core/auth.py b/app/core/auth.py deleted file mode 100644 index f4ccd679..00000000 --- a/app/core/auth.py +++ /dev/null @@ -1,9 +0,0 @@ -from fastapi_auth0 import Auth0 - -from app.core.config import settings - -auth = Auth0( - domain=settings.AUTH0_DOMAIN, - api_audience=settings.AUTH0_API_AUDIENCE, - scopes=settings.BASE_PRINCIPALS, -) diff --git a/app/core/auth/__init__.py b/app/core/auth/__init__.py new file mode 100644 index 00000000..da8a0145 --- /dev/null +++ b/app/core/auth/__init__.py @@ -0,0 +1,19 @@ +from app.core.config import settings + +from .auth0 import ( + Auth0, + Auth0HTTPBearer, + Auth0UnauthenticatedException, + Auth0UnauthorizedException, + Auth0User, + HTTPAuth0Error, + JwksDict, + JwksKeyDict, + OAuth2ImplicitBearer, +) + +auth = Auth0( + domain=settings.AUTH0_DOMAIN, + api_audience=settings.AUTH0_API_AUDIENCE, + scopes=settings.BASE_PRINCIPALS, +) diff --git a/app/core/auth/auth0.py b/app/core/auth/auth0.py new file mode 100644 index 00000000..b09d739a --- /dev/null +++ b/app/core/auth/auth0.py @@ -0,0 +1,297 @@ +import json +import logging +import os +import urllib.parse +import urllib.request +from datetime import datetime +from typing import Any, Dict, List, Optional, Type + +from fastapi import Depends, HTTPException, Request +from fastapi.openapi.models import OAuthFlowImplicit, OAuthFlows +from fastapi.security import ( + HTTPAuthorizationCredentials, + HTTPBearer, + OAuth2, + OAuth2AuthorizationCodeBearer, + OAuth2PasswordBearer, + OpenIdConnect, + SecurityScopes, +) +from jose import jwt # type: ignore +from pydantic import BaseModel, Field, ValidationError +from typing_extensions import TypedDict + +from app.schemas import UserRole + +logger = logging.getLogger("fastapi_auth0") + +auth0_rule_namespace: str = os.getenv( + "AUTH0_RULE_NAMESPACE", "https://github.com/dorinclisu/fastapi-auth0" +) + + +class Auth0UnauthenticatedException(HTTPException): + def __init__(self, detail: str, **kwargs: Any) -> None: + """Returns HTTP 401""" + super().__init__(401, detail, **kwargs) + + +class Auth0UnauthorizedException(HTTPException): + def __init__(self, detail: str, **kwargs: Any) -> None: + """Returns HTTP 403""" + super().__init__(403, detail, **kwargs) + + +class HTTPAuth0Error(BaseModel): + detail: str + + +unauthenticated_response: Dict = {401: {"model": HTTPAuth0Error}} +unauthorized_response: Dict = {403: {"model": HTTPAuth0Error}} +security_responses: Dict = {**unauthenticated_response, **unauthorized_response} + + +class Auth0User(BaseModel): + id: str = Field(..., alias="sub") + permissions: Optional[List[str]] + email: Optional[str] = Field( # type: ignore [literal-required] + None, alias=f"{auth0_rule_namespace}/email" + ) + is_verified: Optional[bool] = Field( # type: ignore [literal-required] + None, alias=f"{auth0_rule_namespace}/is_verified" + ) + created_on: Optional[datetime] = Field( # type: ignore [literal-required] + None, alias=f"{auth0_rule_namespace}/created_on" + ) + updated_on: Optional[datetime] = Field( # type: ignore [literal-required] + None, alias=f"{auth0_rule_namespace}/updated_on" + ) + roles: Optional[List[UserRole]] = Field( # type: ignore [literal-required] + None, alias=f"{auth0_rule_namespace}/roles" + ) + + +class Auth0HTTPBearer(HTTPBearer): + async def __call__( + self, request: Request + ) -> Optional[HTTPAuthorizationCredentials]: + return await super().__call__(request) + + +class OAuth2ImplicitBearer(OAuth2): + def __init__( + self, + authorizationUrl: str, + scopes: Dict[str, str] = {}, + scheme_name: Optional[str] = None, + auto_error: bool = True, + ): + flows = OAuthFlows( + implicit=OAuthFlowImplicit(authorizationUrl=authorizationUrl, scopes=scopes) + ) + super().__init__(flows=flows, scheme_name=scheme_name, auto_error=auto_error) + + async def __call__(self, request: Request) -> Optional[str]: + """ + Overwrite parent call to prevent useless overhead, + the actual auth is done in Auth0.get_user + This scheme is just for Swagger UI + """ + return None + + +class JwksKeyDict(TypedDict): + kid: str + kty: str + use: str + n: str + e: str + + +class JwksDict(TypedDict): + keys: List[JwksKeyDict] + + +class Auth0: + def __init__( + self, + domain: str, + api_audience: str, + scopes: Dict[str, str] = {}, + auto_error: bool = True, + scope_auto_error: bool = True, + email_auto_error: bool = False, + auth0user_model: Type[Auth0User] = Auth0User, + ): + self.domain = domain + self.audience = api_audience + + self.auto_error = auto_error + self.scope_auto_error = scope_auto_error + self.email_auto_error = email_auto_error + + self.auth0_user_model = auth0user_model + + self.algorithms = ["RS256"] + r = urllib.request.urlopen(f"https://{domain}/.well-known/jwks.json") + self.jwks: JwksDict = json.loads(r.read()) + + authorization_url_qs = urllib.parse.urlencode({"audience": api_audience}) + authorization_url = f"https://{domain}/authorize?{authorization_url_qs}" + self.implicit_scheme = OAuth2ImplicitBearer( + authorizationUrl=authorization_url, + scopes=scopes, + scheme_name="Auth0ImplicitBearer", + ) + self.password_scheme = OAuth2PasswordBearer( + tokenUrl=f"https://{domain}/oauth/token", scopes=scopes + ) + self.authcode_scheme = OAuth2AuthorizationCodeBearer( + authorizationUrl=authorization_url, + tokenUrl=f"https://{domain}/oauth/token", + scopes=scopes, + ) + self.oidc_scheme = OpenIdConnect( + openIdConnectUrl=f"https://{domain}/.well-known/openid-configuration" + ) + + async def get_user( + self, + security_scopes: SecurityScopes, + creds: Optional[HTTPAuthorizationCredentials] = Depends( + Auth0HTTPBearer(auto_error=False) + ), + ) -> Optional[Auth0User]: + """ + Verify the Authorization: Bearer token and return the user. + If there is any problem and auto_error = True then + raise Auth0UnauthenticatedException or Auth0UnauthorizedException, + otherwise return None. + + Not to be called directly, but to be placed within a Depends() or + Security() wrapper. + + Example: def path_op_func(user: Auth0User = Security(auth.get_user)). + + if auto_error = return 403 until solving. + see: https://github.com/tiangolo/fastapi/pull/2120 + """ + if creds is None: + if self.auto_error: + raise HTTPException(403, detail="Missing bearer token") + else: + return None + + token = creds.credentials + payload: Dict = {} + try: + unverified_header = jwt.get_unverified_header(token) + + if "kid" not in unverified_header: + msg = "Malformed token header" + if self.auto_error: + raise Auth0UnauthenticatedException(detail=msg) + else: + logger.warning(msg) + return None + + rsa_key = {} + for key in self.jwks["keys"]: + if key["kid"] == unverified_header["kid"]: + rsa_key = { + "kty": key["kty"], + "kid": key["kid"], + "use": key["use"], + "n": key["n"], + "e": key["e"], + } + break + if rsa_key: + payload = jwt.decode( + token, + rsa_key, + algorithms=self.algorithms, + audience=self.audience, + issuer=f"https://{self.domain}/", + ) + else: + msg = "Invalid kid header (wrong tenant or rotated public key)" + if self.auto_error: + raise Auth0UnauthenticatedException(detail=msg) + else: + logger.warning(msg) + return None + + except jwt.ExpiredSignatureError: + msg = "Expired token" + if self.auto_error: + raise Auth0UnauthenticatedException(detail=msg) + else: + logger.warning(msg) + return None + + except jwt.JWTClaimsError: + msg = "Invalid token claims (wrong issuer or audience)" + if self.auto_error: + raise Auth0UnauthenticatedException(detail=msg) + else: + logger.warning(msg) + return None + + except jwt.JWTError: + msg = "Malformed token" + if self.auto_error: + raise Auth0UnauthenticatedException(detail=msg) + else: + logger.warning(msg) + return None + + except Auth0UnauthenticatedException: + raise + + except Exception as e: + # This is an unlikely case but handle it just to be safe + # (maybe the token is specially crafted to bug our code) + logger.error(f'Handled exception decoding token: "{e}"', exc_info=True) + if self.auto_error: + raise Auth0UnauthenticatedException(detail="Error decoding token") + else: + return None + + if self.scope_auto_error: + token_scope_str: str = payload.get("scope", "") + + if isinstance(token_scope_str, str): + token_scopes = token_scope_str.split() + + for scope in security_scopes.scopes: + if scope not in token_scopes: + raise Auth0UnauthorizedException( + detail=f'Missing "{scope}" scope', + headers={ + "WWW-Authenticate": f'Bearer scope="{security_scopes.scope_str}"' # noqa: E501 + }, + ) + else: + # This is an unlikely case but handle it just to be safe + # (perhaps auth0 will change the scope format) + raise Auth0UnauthorizedException( + detail='Token "scope" field must be a string' + ) + + try: + user = self.auth0_user_model(**payload) + + if self.email_auto_error and not user.email: + raise Auth0UnauthorizedException( + detail='Missing email claim (check auth0 rule "Add email to access token")' # noqa: E501 + ) + + return user + + except ValidationError as e: + logger.error(f'Handled exception parsing Auth0User: "{e}"', exc_info=True) + if self.auto_error: + raise Auth0UnauthorizedException(detail="Error parsing Auth0User") + else: + return None diff --git a/app/core/utilities/email.py b/app/core/utilities/email.py index 71083310..acb580f8 100644 --- a/app/core/utilities/email.py +++ b/app/core/utilities/email.py @@ -1,4 +1,4 @@ -from typing import Any, Optional +from typing import Optional from fastapi_mail import FastMail # type: ignore from fastapi_mail import MessageSchema, MessageType diff --git a/app/crud/base.py b/app/crud/base.py index d0c74829..a20f7e17 100644 --- a/app/crud/base.py +++ b/app/crud/base.py @@ -59,7 +59,7 @@ async def list( return await self._list(skip=skip, limit=limit) async def create(self, schema: Union[SCHEMA_CREATE, Any]) -> TABLE: - entry: Any = self._table(id=self.gen_uuid(), **schema.model_dump()) # type: ignore + entry: Any = self._table(id=self.gen_uuid(), **schema.model_dump()) # type: ignore # noqa: E501 self._db.add(entry) await self._db.commit() await self._db.refresh(entry) diff --git a/app/models/geocoord.py b/app/models/geocoord.py index 04f837e7..6bf6ced6 100755 --- a/app/models/geocoord.py +++ b/app/models/geocoord.py @@ -36,8 +36,9 @@ class Geocoord(Base): ) address: Mapped[str] = mapped_column( String(255), - nullable=False, + unique=True, primary_key=True, + nullable=False, default="135-145, South Olive Street, Orange, \ Orange County, California, 92866, United States", ) diff --git a/app/models/user.py b/app/models/user.py index d7773006..07d9367e 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -2,13 +2,14 @@ from typing import TYPE_CHECKING, Any, List from pydantic import UUID4 -from sqlalchemy import Boolean, DateTime, String, func +from sqlalchemy import JSON, Boolean, DateTime, String, func from sqlalchemy.orm import Mapped, backref, mapped_column, relationship from sqlalchemy_utils import UUIDType # type: ignore from app.core.utilities.uuids import get_random_username # type: ignore from app.core.utilities.uuids import get_uuid from app.db.base_class import Base +from app.schemas import UserRole if TYPE_CHECKING: # pragma: no cover from .client import Client # noqa: F401 @@ -44,16 +45,14 @@ class User(Base): String(320), nullable=False, ) - username: Mapped[str] = mapped_column( - String(255), - nullable=False, - unique=True, - index=True, - default=get_random_username(), - ) is_active: Mapped[bool] = mapped_column(Boolean(), nullable=False, default=True) is_verified: Mapped[bool] = mapped_column(Boolean(), nullable=False, default=False) is_superuser: Mapped[bool] = mapped_column(Boolean(), nullable=False, default=False) + roles: Mapped[List[UserRole]] = mapped_column( + JSON, + nullable=False, + default=[UserRole.USER.value], + ) # relationships clients: Mapped[List["Client"]] = relationship( diff --git a/app/schemas/__init__.py b/app/schemas/__init__.py index 60c5499d..e6780d57 100755 --- a/app/schemas/__init__.py +++ b/app/schemas/__init__.py @@ -96,6 +96,7 @@ from .user import UserCreate, UserRead, UserReadRelations, UserUpdate from .user_client import UserClientCreate, UserClientRead, UserClientUpdate from .user_ipaddress import UserIpaddressCreate, UserIpaddressRead, UserIpaddressUpdate +from .user_roles import UserRole from .website import ( WebsiteCreate, WebsiteCreateProcessing, diff --git a/app/schemas/user.py b/app/schemas/user.py index e6ac4560..ae1c8909 100644 --- a/app/schemas/user.py +++ b/app/schemas/user.py @@ -12,6 +12,7 @@ ValidateSchemaUsernameRequired, ) from app.schemas.base import BaseSchema, BaseSchemaRead +from app.schemas.user_roles import UserRole # schemas @@ -23,7 +24,7 @@ class UserBase( ): auth_id: str email: str - username: str + roles: List[UserRole] = [UserRole.USER] class UserCreate(UserBase): @@ -36,10 +37,10 @@ class UserUpdate( ValidateSchemaUsernameOptional, BaseSchema, ): - username: Optional[str] = None is_active: Optional[bool] = None is_verified: Optional[bool] = None is_superuser: Optional[bool] = None + roles: Optional[List[UserRole]] = None class UserRead(UserACL, UserBase, BaseSchemaRead): diff --git a/app/schemas/user_roles.py b/app/schemas/user_roles.py new file mode 100644 index 00000000..a39a1a29 --- /dev/null +++ b/app/schemas/user_roles.py @@ -0,0 +1,10 @@ +from enum import Enum + + +# schemas +class UserRole(Enum): + USER = "user" # default + EMPLOYEE = "employee" + MANAGER = "manager" + CLIENT = "client" + ADMIN = "admin" diff --git a/poetry.lock b/poetry.lock index ab0f0069..5d00a3f5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -977,21 +977,6 @@ typing-extensions = ">=4.5.0" [package.extras] all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] -[[package]] -name = "fastapi-auth0" -version = "0.4.0" -description = "Easy auth0.com integration for FastAPI" -optional = false -python-versions = ">=3.7" -files = [ - {file = "fastapi-auth0-0.4.0.tar.gz", hash = "sha256:21834caad60645ab16bd1d6ac912100eac6b9e67346b27f75ec76d3767bedca3"}, - {file = "fastapi_auth0-0.4.0-py3-none-any.whl", hash = "sha256:c0f30f5cf9f4efbfa4f3160a2a57d36f937b034930fba97f7d7116476a92ba19"}, -] - -[package.dependencies] -fastapi = ">=0.60.0" -python-jose = ">=3.2.0" - [[package]] name = "fastapi-mail" version = "1.4.1" @@ -3009,4 +2994,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "c7d88978df264464386241130092d03173bc178bd939ab08f036b144f0ecab67" +content-hash = "ee5f6ced028ca9201df9b971505aee802ae44cd8dec380191dba33e604105f7b" diff --git a/pyproject.toml b/pyproject.toml index 4759bda1..5fba175e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,6 @@ bcrypt = "^4.0.0" passlib = "^1.7.4" fastapi = "0.103.1" fastapi-mail = {extras = ["aioredis"], version = "^1.4.1"} -fastapi-auth0 = "^0.4.0" pydantic = {version = "2.3.0", extras = ["email"]} python-multipart = "0.0.6" python-dotenv = "1.0.0" @@ -44,6 +43,7 @@ aioredis = "^2.0.1" ultimate-sitemap-parser = "^0.5" types-requests = "^2.28.11.16" tenacity = "^8.2.2" +python-jose = "^3.3.0" [tool.poetry.group.dev.dependencies] httpx = "0.23.3" diff --git a/tests/api/deps/test_get_current_user.py b/tests/api/deps/test_get_current_user.py index 327d70f8..48b1a5ec 100644 --- a/tests/api/deps/test_get_current_user.py +++ b/tests/api/deps/test_get_current_user.py @@ -2,12 +2,12 @@ import pytest from fastapi import HTTPException, status -from fastapi_auth0 import Auth0User from fastapi_permissions import Authenticated # type: ignore from fastapi_permissions import Everyone from app.api.deps import get_current_user, get_current_user_permissions from app.api.errors import ErrorCode +from app.core.auth import Auth0User from app.core.utilities.uuids import get_uuid_str diff --git a/tests/api/v1/clients/test_clients_create.py b/tests/api/v1/clients/test_clients_create.py index b6e45f9d..90cdd11c 100644 --- a/tests/api/v1/clients/test_clients_create.py +++ b/tests/api/v1/clients/test_clients_create.py @@ -69,7 +69,9 @@ async def test_create_client_as_superuser_client_title_too_short( ) assert response.status_code == 422 entry: Dict[str, Any] = response.json() - assert entry["detail"][0]["msg"] == "Value error, title must be 5 characters or more" + assert ( + entry["detail"][0]["msg"] == "Value error, title must be 5 characters or more" + ) async def test_create_client_as_superuser_client_title_too_long( @@ -86,7 +88,9 @@ async def test_create_client_as_superuser_client_title_too_long( ) assert response.status_code == 422 entry: Dict[str, Any] = response.json() - assert entry["detail"][0]["msg"] == "Value error, title must be 96 characters or less" + assert ( + entry["detail"][0]["msg"] == "Value error, title must be 96 characters or less" + ) async def test_create_client_as_superuser_client_description_too_long( @@ -104,5 +108,6 @@ async def test_create_client_as_superuser_client_description_too_long( assert response.status_code == 422 entry: Dict[str, Any] = response.json() assert ( - entry["detail"][0]["msg"] == "Value error, description must be 5000 characters or less" + entry["detail"][0]["msg"] + == "Value error, description must be 5000 characters or less" ) # noqa: E501 diff --git a/tests/api/v1/clients/test_clients_update.py b/tests/api/v1/clients/test_clients_update.py index 3e081dfa..394798ab 100644 --- a/tests/api/v1/clients/test_clients_update.py +++ b/tests/api/v1/clients/test_clients_update.py @@ -47,7 +47,10 @@ async def test_update_client_as_superuser_title_too_short( ) updated_entry: Dict[str, Any] = response.json() assert response.status_code == 422 - assert updated_entry["detail"][0]["msg"] == "Value error, title must be 5 characters or more" + assert ( + updated_entry["detail"][0]["msg"] + == "Value error, title must be 5 characters or more" + ) async def test_update_client_as_superuser_title_too_long( @@ -65,7 +68,10 @@ async def test_update_client_as_superuser_title_too_long( ) updated_entry: Dict[str, Any] = response.json() assert response.status_code == 422 - assert updated_entry["detail"][0]["msg"] == "Value error, title must be 96 characters or less" + assert ( + updated_entry["detail"][0]["msg"] + == "Value error, title must be 96 characters or less" + ) async def test_update_client_as_superuser_description_too_long( diff --git a/tests/api/v1/websites/test_websites_create.py b/tests/api/v1/websites/test_websites_create.py index 9548b530..19fc44c7 100644 --- a/tests/api/v1/websites/test_websites_create.py +++ b/tests/api/v1/websites/test_websites_create.py @@ -101,7 +101,9 @@ async def test_create_website_as_superuser_domain_too_short( ) assert response.status_code == 422 entry: Dict[str, Any] = response.json() - assert entry["detail"][0]["msg"] == "Value error, domain must be 5 characters or more" + assert ( + entry["detail"][0]["msg"] == "Value error, domain must be 5 characters or more" + ) async def test_create_website_as_superuser_domain_too_long( @@ -118,7 +120,10 @@ async def test_create_website_as_superuser_domain_too_long( ) assert response.status_code == 422 entry: Dict[str, Any] = response.json() - assert entry["detail"][0]["msg"] == "Value error, domain must be 255 characters or less" + assert ( + entry["detail"][0]["msg"] + == "Value error, domain must be 255 characters or less" + ) async def test_create_website_as_superuser_domain_is_not_valid_domain( diff --git a/tests/api/v1/websites/test_websites_update.py b/tests/api/v1/websites/test_websites_update.py index 76f782fa..5bb56033 100644 --- a/tests/api/v1/websites/test_websites_update.py +++ b/tests/api/v1/websites/test_websites_update.py @@ -65,7 +65,9 @@ async def test_update_website_as_superuser_domain_too_short( ) assert response.status_code == 422 entry: Dict[str, Any] = response.json() - assert entry["detail"][0]["msg"] == "Value error, domain must be 5 characters or more" + assert ( + entry["detail"][0]["msg"] == "Value error, domain must be 5 characters or more" + ) async def test_update_website_as_superuser_domain_too_long( @@ -83,7 +85,10 @@ async def test_update_website_as_superuser_domain_too_long( ) assert response.status_code == 422 entry: Dict[str, Any] = response.json() - assert entry["detail"][0]["msg"] == "Value error, domain must be 255 characters or less" + assert ( + entry["detail"][0]["msg"] + == "Value error, domain must be 255 characters or less" + ) async def test_update_website_as_superuser_domain_invalid( diff --git a/tests/api/v1/websites_pages/test_websites_pages_create.py b/tests/api/v1/websites_pages/test_websites_pages_create.py index 5d9dd6db..da962812 100644 --- a/tests/api/v1/websites_pages/test_websites_pages_create.py +++ b/tests/api/v1/websites_pages/test_websites_pages_create.py @@ -140,7 +140,9 @@ async def test_create_website_page_as_superuser_url_too_long( ) assert response.status_code == 422 entry: Dict[str, Any] = response.json() - assert entry["detail"][0]["msg"] == "Value error, url must be 2048 characters or less" + assert ( + entry["detail"][0]["msg"] == "Value error, url must be 2048 characters or less" + ) async def test_create_website_page_as_superuser_website_not_exists( diff --git a/tests/api/v1/websites_pages/test_websites_pages_update.py b/tests/api/v1/websites_pages/test_websites_pages_update.py index 1bcaee21..f1efdd08 100644 --- a/tests/api/v1/websites_pages/test_websites_pages_update.py +++ b/tests/api/v1/websites_pages/test_websites_pages_update.py @@ -53,4 +53,6 @@ async def test_update_website_page_as_superuser_url_too_long( ) assert response.status_code == 422 entry: Dict[str, Any] = response.json() - assert entry["detail"][0]["msg"] == "Value error, url must be 2048 characters or less" + assert ( + entry["detail"][0]["msg"] == "Value error, url must be 2048 characters or less" + ) diff --git a/tests/api/v1/websites_sitemaps/test_websites_sitemaps_create.py b/tests/api/v1/websites_sitemaps/test_websites_sitemaps_create.py index 7ecb3252..59b56587 100644 --- a/tests/api/v1/websites_sitemaps/test_websites_sitemaps_create.py +++ b/tests/api/v1/websites_sitemaps/test_websites_sitemaps_create.py @@ -115,4 +115,6 @@ async def test_create_website_sitemap_as_superuser_url_too_long( ) assert response.status_code == 422 entry: Dict[str, Any] = response.json() - assert entry["detail"][0]["msg"] == "Value error, url must be 2048 characters or less" + assert ( + entry["detail"][0]["msg"] == "Value error, url must be 2048 characters or less" + ) diff --git a/tests/api/v1/websites_sitemaps/test_websites_sitemaps_update.py b/tests/api/v1/websites_sitemaps/test_websites_sitemaps_update.py index b6c97ba8..61034c76 100644 --- a/tests/api/v1/websites_sitemaps/test_websites_sitemaps_update.py +++ b/tests/api/v1/websites_sitemaps/test_websites_sitemaps_update.py @@ -48,4 +48,6 @@ async def test_update_website_sitemaps_as_superuser_url_too_long( ) assert response.status_code == 422 entry: Dict[str, Any] = response.json() - assert entry["detail"][0]["msg"] == "Value error, url must be 2048 characters or less" + assert ( + entry["detail"][0]["msg"] == "Value error, url must be 2048 characters or less" + )