From 78a5fcd27f7c1a64a03db3d42f4a6235c6c57d45 Mon Sep 17 00:00:00 2001 From: MasterKenth Date: Wed, 22 Jan 2025 11:59:14 +0100 Subject: [PATCH 1/7] =?UTF-8?q?=E2=9C=A8=20Feat(auth):=20add=20new=20auth?= =?UTF-8?q?=20system=20base?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fai_backend/auth_v2/__init__.py | 0 .../auth_v2/authentication/__init__.py | 0 .../auth_v2/authentication/factory.py | 17 +++ .../auth_v2/authentication/fastapi_sources.py | 18 +++ .../auth_v2/authentication/impl/__init__.py | 0 .../auth_v2/authentication/impl/api_key.py | 10 ++ .../authentication/impl/bearer_token.py | 11 ++ .../auth_v2/authentication/models.py | 19 +++ .../auth_v2/authorization/__init__.py | 0 .../auth_v2/authorization/factory.py | 8 + .../auth_v2/authorization/impl/__init__.py | 0 .../auth_v2/authorization/impl/repo.py | 8 + .../auth_v2/authorization/models.py | 8 + .../fai_backend/auth_v2/fastapi_auth.py | 138 ++++++++++++++++++ 14 files changed, 237 insertions(+) create mode 100644 fai-rag-app/fai-backend/fai_backend/auth_v2/__init__.py create mode 100644 fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/__init__.py create mode 100644 fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/factory.py create mode 100644 fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/fastapi_sources.py create mode 100644 fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/impl/__init__.py create mode 100644 fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/impl/api_key.py create mode 100644 fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/impl/bearer_token.py create mode 100644 fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/models.py create mode 100644 fai-rag-app/fai-backend/fai_backend/auth_v2/authorization/__init__.py create mode 100644 fai-rag-app/fai-backend/fai_backend/auth_v2/authorization/factory.py create mode 100644 fai-rag-app/fai-backend/fai_backend/auth_v2/authorization/impl/__init__.py create mode 100644 fai-rag-app/fai-backend/fai_backend/auth_v2/authorization/impl/repo.py create mode 100644 fai-rag-app/fai-backend/fai_backend/auth_v2/authorization/models.py create mode 100644 fai-rag-app/fai-backend/fai_backend/auth_v2/fastapi_auth.py diff --git a/fai-rag-app/fai-backend/fai_backend/auth_v2/__init__.py b/fai-rag-app/fai-backend/fai_backend/auth_v2/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/__init__.py b/fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/factory.py b/fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/factory.py new file mode 100644 index 00000000..a358a67d --- /dev/null +++ b/fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/factory.py @@ -0,0 +1,17 @@ +from typing import Callable + +from fai_backend.auth_v2.authentication.impl.api_key import ApiKeyProvider +from fai_backend.auth_v2.authentication.impl.bearer_token import BearerTokenProvider +from fai_backend.auth_v2.authentication.models import IAuthenticationProvider, AuthenticationType + + +class AuthenticationFactory: + @staticmethod + async def get(auth_type: str) -> IAuthenticationProvider: + mapping: dict[str, Callable[[], IAuthenticationProvider]] = { + AuthenticationType.API_KEY: ApiKeyProvider, + AuthenticationType.BEARER_TOKEN: BearerTokenProvider, + } + if auth_type in mapping: + return mapping[auth_type]() + raise ValueError(f"Authentication type '{auth_type}' not supported") diff --git a/fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/fastapi_sources.py b/fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/fastapi_sources.py new file mode 100644 index 00000000..15326f3e --- /dev/null +++ b/fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/fastapi_sources.py @@ -0,0 +1,18 @@ +from fastapi.security import APIKeyHeader + +from fai_backend.auth.security import access_security + +api_key_source = APIKeyHeader( + name='X-Api-Key', + description='API Key. Can be created/revoked by an administrator.', + auto_error=False +) + +# bearer_token_source = HTTPBearer( +# scheme_name='User Token', +# description='JWT Token associated with an user. Generated by logging in.', +# auto_error=False +# ) + +# TODO: don't use fastapi_jwt +bearer_token_source = access_security diff --git a/fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/impl/__init__.py b/fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/impl/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/impl/api_key.py b/fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/impl/api_key.py new file mode 100644 index 00000000..979096cd --- /dev/null +++ b/fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/impl/api_key.py @@ -0,0 +1,10 @@ +from fai_backend.auth_v2.authentication.models import AuthenticatedIdentity, IAuthenticationProvider, AuthenticationType + + +class ApiKeyProvider(IAuthenticationProvider): + async def validate(self, data: str | None) -> AuthenticatedIdentity | None: + # TODO + return AuthenticatedIdentity( + type=AuthenticationType.API_KEY, + uid="fai-blablabla" + ) diff --git a/fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/impl/bearer_token.py b/fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/impl/bearer_token.py new file mode 100644 index 00000000..5917c2a8 --- /dev/null +++ b/fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/impl/bearer_token.py @@ -0,0 +1,11 @@ +import json + +from fai_backend.auth_v2.authentication.models import IAuthenticationProvider, AuthenticatedIdentity, AuthenticationType + + +class BearerTokenProvider(IAuthenticationProvider): + async def validate(self, data: str | None) -> AuthenticatedIdentity | None: + return AuthenticatedIdentity( + type=AuthenticationType.BEARER_TOKEN, + uid=json.loads(data)['email'] + ) diff --git a/fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/models.py b/fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/models.py new file mode 100644 index 00000000..8419453e --- /dev/null +++ b/fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/models.py @@ -0,0 +1,19 @@ +from typing import Protocol + +from pydantic import BaseModel + + +class AuthenticationType: + NONE = 'NONE' + API_KEY = 'api_key' + BEARER_TOKEN = 'bearer_token' + + +class AuthenticatedIdentity(BaseModel): + type: str + uid: str + + +class IAuthenticationProvider(Protocol): + async def validate(self, data: str | None) -> AuthenticatedIdentity | None: + ... diff --git a/fai-rag-app/fai-backend/fai_backend/auth_v2/authorization/__init__.py b/fai-rag-app/fai-backend/fai_backend/auth_v2/authorization/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fai-rag-app/fai-backend/fai_backend/auth_v2/authorization/factory.py b/fai-rag-app/fai-backend/fai_backend/auth_v2/authorization/factory.py new file mode 100644 index 00000000..27051145 --- /dev/null +++ b/fai-rag-app/fai-backend/fai_backend/auth_v2/authorization/factory.py @@ -0,0 +1,8 @@ +from fai_backend.auth_v2.authorization.impl.repo import RepoAuthorizationProvider +from fai_backend.auth_v2.authorization.models import IAuthorizationProvider + + +class AuthorizationFactory: + @staticmethod + async def get() -> IAuthorizationProvider: + return RepoAuthorizationProvider() diff --git a/fai-rag-app/fai-backend/fai_backend/auth_v2/authorization/impl/__init__.py b/fai-rag-app/fai-backend/fai_backend/auth_v2/authorization/impl/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fai-rag-app/fai-backend/fai_backend/auth_v2/authorization/impl/repo.py b/fai-rag-app/fai-backend/fai_backend/auth_v2/authorization/impl/repo.py new file mode 100644 index 00000000..c4cb05db --- /dev/null +++ b/fai-rag-app/fai-backend/fai_backend/auth_v2/authorization/impl/repo.py @@ -0,0 +1,8 @@ +from fai_backend.auth_v2.authentication.models import AuthenticatedIdentity, AuthenticationType +from fai_backend.auth_v2.authorization.models import IAuthorizationProvider + + +class RepoAuthorizationProvider(IAuthorizationProvider): + async def has_scopes(self, identity: AuthenticatedIdentity, scopes: list[str]) -> bool: + # TODO + return True diff --git a/fai-rag-app/fai-backend/fai_backend/auth_v2/authorization/models.py b/fai-rag-app/fai-backend/fai_backend/auth_v2/authorization/models.py new file mode 100644 index 00000000..5fa84a87 --- /dev/null +++ b/fai-rag-app/fai-backend/fai_backend/auth_v2/authorization/models.py @@ -0,0 +1,8 @@ +from typing import Protocol + +from fai_backend.auth_v2.authentication.models import AuthenticatedIdentity + + +class IAuthorizationProvider(Protocol): + async def has_scopes(self, identity: AuthenticatedIdentity, scopes: list[str]) -> bool: + ... diff --git a/fai-rag-app/fai-backend/fai_backend/auth_v2/fastapi_auth.py b/fai-rag-app/fai-backend/fai_backend/auth_v2/fastapi_auth.py new file mode 100644 index 00000000..5a3b18cd --- /dev/null +++ b/fai-rag-app/fai-backend/fai_backend/auth_v2/fastapi_auth.py @@ -0,0 +1,138 @@ +import json + +from fastapi import Depends, HTTPException, status +from fastapi.security import SecurityScopes +from fastapi_jwt import JwtAuthorizationCredentials +from pydantic import BaseModel + +from fai_backend.auth_v2.authentication.factory import AuthenticationFactory +from fai_backend.auth_v2.authentication.fastapi_sources import api_key_source, bearer_token_source +from fai_backend.auth_v2.authentication.models import AuthenticationType, AuthenticatedIdentity +from fai_backend.auth_v2.authorization.factory import AuthorizationFactory +from fai_backend.config import settings + + +class CommonHTTPErrorResponse(BaseModel): + detail: str + + +class AuthenticationChallengeAdapter: + @staticmethod + def to_challenge(authentication_method: str) -> str: + mapping: dict[str, str] = { + AuthenticationType.API_KEY: 'Api-Key realm="main"', + AuthenticationType.BEARER_TOKEN: 'Bearer realm="main"', + } + return mapping.get(authentication_method, f'{authentication_method} realm="main"') + + +auth_responses = { + 400: { + "description": "(Auth) too many credentials provided; only one type should be used.", + "model": CommonHTTPErrorResponse, + "content": { + "application/json": { + "example": CommonHTTPErrorResponse( + detail="Too many authentication credentials provided (x+y)" + ) + } + } + }, + 401: { + "description": '''No credentials provided or provided credentials are invalid (e.g. expired). + \nAvailable authentication methods are provided in the WWW-Authenticate header.''', + "model": CommonHTTPErrorResponse, + "content": { + "application/json": { + "example": CommonHTTPErrorResponse( + detail="Not Authenticated" + ) + } + } + + }, + 403: { + "description": "Credentials are missing one or more required scopes (operation permissions).", + "model": CommonHTTPErrorResponse, + "content": { + "application/json": { + "example": CommonHTTPErrorResponse( + detail="Missing one or more required scopes (x, y, z)" + ) + } + } + }, +} + + +async def auth( + security_scopes: SecurityScopes, + api_key: str | None = Depends(api_key_source), + bearer_token: JwtAuthorizationCredentials | None = Depends(bearer_token_source), +) -> AuthenticatedIdentity: + if settings.HTTP_AUTHENTICATION_TYPE == 'none': + return AuthenticatedIdentity(type=AuthenticationType.NONE, uid='') + + credential_types = [ + (AuthenticationType.API_KEY, api_key), + (AuthenticationType.BEARER_TOKEN, json.dumps(bearer_token.subject) if bearer_token else None), + ] + + valid_credentials_provided = [(c, v) for c, v in credential_types if v is not None] + + available_challenges = ', '.join([ + AuthenticationChallengeAdapter.to_challenge(a) for a, _ in credential_types]) + + # Check for missing credentials + if len(valid_credentials_provided) == 0: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Not Authenticated", + headers={"WWW-Authenticate": available_challenges} + ) + + # Check for superfluous credentials + if len(valid_credentials_provided) > 1: + provided_credential_types = [c for c, _ in valid_credentials_provided] + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Too many authentication credentials provided ({'+'.join(provided_credential_types)})", + headers={"WWW-Authenticate": available_challenges} + ) + + # Check for valid credentials + auth_type, auth_payload = valid_credentials_provided[0] + try: + authentication_provider = await AuthenticationFactory.get(auth_type) + authenticated_credentials = await authentication_provider.validate(auth_payload) + except ValueError as ex: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(ex)) + + if authenticated_credentials is None: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=f"Invalid Credentials") + + # Check for valid permissions + required_scopes = security_scopes.scopes + authorization_provider = await AuthorizationFactory.get() + valid_authorization = await authorization_provider.has_scopes(authenticated_credentials, required_scopes) + + if not valid_authorization: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"Missing one or more required scopes ({', '.join(required_scopes)})" + ) + + return authenticated_credentials + + +def get_auth_responses(additional_400_description: str | None = None) -> dict[int, dict]: + result = dict(auth_responses) + + if additional_400_description: + result[400]["description"] = f"{additional_400_description}\n\n{auth_responses[400]['description']}" + + return result + + +def make_auth_path_description(path_description: str, scopes: list[str]) -> str: + return f'{path_description}\n\n*(Auth) required scopes: {", ".join(scopes)}*' From 2c73bde7c153d06125490efd8c73e131db02e496 Mon Sep 17 00:00:00 2001 From: MasterKenth Date: Thu, 23 Jan 2025 14:07:20 +0100 Subject: [PATCH 2/7] =?UTF-8?q?=E2=9C=A8=20Feat(auth):=20add=20custom=20au?= =?UTF-8?q?th=20router=20decorator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fai_backend/auth_v2/fastapi_auth.py | 155 +++++++++++++++++- 1 file changed, 152 insertions(+), 3 deletions(-) diff --git a/fai-rag-app/fai-backend/fai_backend/auth_v2/fastapi_auth.py b/fai-rag-app/fai-backend/fai_backend/auth_v2/fastapi_auth.py index 5a3b18cd..a5638de9 100644 --- a/fai-rag-app/fai-backend/fai_backend/auth_v2/fastapi_auth.py +++ b/fai-rag-app/fai-backend/fai_backend/auth_v2/fastapi_auth.py @@ -1,6 +1,9 @@ +import inspect import json +from functools import partial +from typing import Callable, Any -from fastapi import Depends, HTTPException, status +from fastapi import Depends, HTTPException, status, APIRouter, Security from fastapi.security import SecurityScopes from fastapi_jwt import JwtAuthorizationCredentials from pydantic import BaseModel @@ -65,7 +68,7 @@ def to_challenge(authentication_method: str) -> str: } -async def auth( +async def auth_dependency( security_scopes: SecurityScopes, api_key: str | None = Depends(api_key_source), bearer_token: JwtAuthorizationCredentials | None = Depends(bearer_token_source), @@ -135,4 +138,150 @@ def get_auth_responses(additional_400_description: str | None = None) -> dict[in def make_auth_path_description(path_description: str, scopes: list[str]) -> str: - return f'{path_description}\n\n*(Auth) required scopes: {", ".join(scopes)}*' + if len(scopes) == 0: + return f'{path_description}\n\n*__(Auth) no scopes required__*' + return f'{path_description}\n\n*__(Auth) required scope(s): {", ".join(scopes)}__*' + + +class AuthRouterDecorator: + def __init__(self, api_router: APIRouter): + self.api_router = api_router + + @staticmethod + def route( + router_method: Callable, + path: str, + required_scopes: list[str] = None, + summary: str | None = None, + description: str | None = None, + response_model: Any | None = None, + response_description: str | None = None, + response_400_description: str | None = None, + ): + if required_scopes is None: + required_scopes = [] + + def inner_decorator(func): + security_dependency = Security(auth_dependency, scopes=required_scopes) + parameters = inspect.signature(func).parameters + function_has_identity_parameter = 'auth_identity' in parameters + fn = partial(func, + auth_identity=security_dependency) if function_has_identity_parameter else func + deps = [] if function_has_identity_parameter else [security_dependency] + + router_method( + path, + summary=summary, + description=make_auth_path_description(description, scopes=required_scopes), + response_model=response_model, + response_description=response_description, + responses=get_auth_responses(response_400_description), + dependencies=deps, + )(fn) + + return inner_decorator + + def get( + self, + path: str, + required_scopes: list[str] = None, + summary: str | None = None, + description: str | None = None, + response_model: Any | None = None, + response_description: str | None = None, + response_400_description: str | None = None, + ): + return AuthRouterDecorator.route( + router_method=self.api_router.get, + path=path, + required_scopes=required_scopes, + summary=summary, + description=description, + response_model=response_model, + response_description=response_description, + response_400_description=response_400_description, + ) + + def post( + self, + path: str, + required_scopes: list[str] = None, + summary: str | None = None, + description: str | None = None, + response_model: Any | None = None, + response_description: str | None = None, + response_400_description: str | None = None, + ): + return AuthRouterDecorator.route( + router_method=self.api_router.post, + path=path, + required_scopes=required_scopes, + summary=summary, + description=description, + response_model=response_model, + response_description=response_description, + response_400_description=response_400_description, + ) + + def put( + self, + path: str, + required_scopes: list[str] = None, + summary: str | None = None, + description: str | None = None, + response_model: Any | None = None, + response_description: str | None = None, + response_400_description: str | None = None, + ): + return AuthRouterDecorator.route( + router_method=self.api_router.put, + path=path, + required_scopes=required_scopes, + summary=summary, + description=description, + response_model=response_model, + response_description=response_description, + response_400_description=response_400_description, + ) + + def patch( + self, + path: str, + required_scopes: list[str] = None, + summary: str | None = None, + description: str | None = None, + response_model: Any | None = None, + response_description: str | None = None, + response_400_description: str | None = None, + ): + return AuthRouterDecorator.route( + router_method=self.api_router.patch, + path=path, + required_scopes=required_scopes, + summary=summary, + description=description, + response_model=response_model, + response_description=response_description, + response_400_description=response_400_description, + ) + + def delete( + self, + path: str, + required_scopes: list[str] = None, + summary: str | None = None, + description: str | None = None, + response_model: Any | None = None, + response_description: str | None = None, + response_400_description: str | None = None, + ): + return AuthRouterDecorator.route( + router_method=self.api_router.delete, + path=path, + required_scopes=required_scopes, + summary=summary, + description=description, + response_model=response_model, + response_description=response_description, + response_400_description=response_400_description, + ) From 3ee19f279931d177904c36ef7c5a23787af35960 Mon Sep 17 00:00:00 2001 From: MasterKenth Date: Thu, 23 Jan 2025 14:09:58 +0100 Subject: [PATCH 3/7] =?UTF-8?q?=E2=9C=A8=20Feat(auth):=20add=20test/sample?= =?UTF-8?q?=20route?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fai_backend/auth_v2/test_routes.py | 32 +++++++++++++++++++ fai-rag-app/fai-backend/fai_backend/main.py | 2 ++ 2 files changed, 34 insertions(+) create mode 100644 fai-rag-app/fai-backend/fai_backend/auth_v2/test_routes.py diff --git a/fai-rag-app/fai-backend/fai_backend/auth_v2/test_routes.py b/fai-rag-app/fai-backend/fai_backend/auth_v2/test_routes.py new file mode 100644 index 00000000..e2b03f93 --- /dev/null +++ b/fai-rag-app/fai-backend/fai_backend/auth_v2/test_routes.py @@ -0,0 +1,32 @@ +from fastapi import APIRouter +from pydantic import BaseModel + +from fai_backend.auth_v2.authentication.models import AuthenticatedIdentity +from fai_backend.auth_v2.fastapi_auth import AuthRouterDecorator + +router = APIRouter( + prefix="/api/auth", + tags=["Auth Test"] +) +auth = AuthRouterDecorator(router) + + +class AuthTestReturnModel(BaseModel): + message: str + auth_identity: AuthenticatedIdentity + + +@auth.get( + '/test', + ['can_ask_questions'], + summary='Test authentication/authorization endpoint', + description='''This endpoint does nothing except showcase how auth endpoints work. + +It also serves as a code example of how to implement an endpoint with auth (see source code).''', + response_model=AuthTestReturnModel, + response_description='Success. Returns the authentication details.' +) +async def auth_test(auth_identity: AuthenticatedIdentity): + return AuthTestReturnModel( + message='If you see this then everything works!', + auth_identity=auth_identity) diff --git a/fai-rag-app/fai-backend/fai_backend/main.py b/fai-rag-app/fai-backend/fai_backend/main.py index 4eba205b..cc6b9372 100644 --- a/fai-rag-app/fai-backend/fai_backend/main.py +++ b/fai-rag-app/fai-backend/fai_backend/main.py @@ -24,6 +24,7 @@ from fai_backend.setup import setup_db, setup_project, setup_sentry, setup_file_parser, setup_settings from fai_backend.collection_v2.routes import router as collection_router from fai_backend.collection_v2.view_routes import router as collection_view_router +from fai_backend.auth_v2.test_routes import router as auth_test_router @asynccontextmanager @@ -57,6 +58,7 @@ async def lifespan(_app: FastAPI): app.include_router(settings_router) app.include_router(collection_router) app.include_router(collection_view_router) +app.include_router(auth_test_router) app.middleware('http')(add_git_revision_to_request_header) app.middleware('http')(remove_trailing_slash) From f1834a5baffefb1fd30357e7534c9a67c04cd5c1 Mon Sep 17 00:00:00 2001 From: MasterKenth Date: Fri, 24 Jan 2025 16:12:07 +0100 Subject: [PATCH 4/7] =?UTF-8?q?=E2=9C=A8=20Feat(auth):=20check=20scopes=20?= =?UTF-8?q?(for=20users)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fai_backend/auth_v2/authorization/impl/repo.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/fai-rag-app/fai-backend/fai_backend/auth_v2/authorization/impl/repo.py b/fai-rag-app/fai-backend/fai_backend/auth_v2/authorization/impl/repo.py index c4cb05db..fdee2926 100644 --- a/fai-rag-app/fai-backend/fai_backend/auth_v2/authorization/impl/repo.py +++ b/fai-rag-app/fai-backend/fai_backend/auth_v2/authorization/impl/repo.py @@ -1,8 +1,14 @@ from fai_backend.auth_v2.authentication.models import AuthenticatedIdentity, AuthenticationType from fai_backend.auth_v2.authorization.models import IAuthorizationProvider +from fai_backend.repositories import users_repo class RepoAuthorizationProvider(IAuthorizationProvider): async def has_scopes(self, identity: AuthenticatedIdentity, scopes: list[str]) -> bool: - # TODO - return True + user = await users_repo.get_user_by_email(identity.uid) + + if user is not None: + permissions = user.projects[0].permissions + return all(key in permissions and permissions[key] is True for key in scopes) + + return False From 75c3ea034a4116ac5a31c536773b830ddc510a84 Mon Sep 17 00:00:00 2001 From: MasterKenth Date: Fri, 24 Jan 2025 17:32:49 +0100 Subject: [PATCH 5/7] =?UTF-8?q?=E2=9C=A8=20Feat(auth):=20implement=20api?= =?UTF-8?q?=20key=20CRUD=20+=20auth?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth_v2/api_key/dependencies.py | 6 ++ .../fai_backend/auth_v2/api_key/models.py | 20 ++++++ .../fai_backend/auth_v2/api_key/routes.py | 71 +++++++++++++++++++ .../fai_backend/auth_v2/api_key/service.py | 47 ++++++++++++ .../auth_v2/authentication/impl/api_key.py | 10 ++- .../auth_v2/authorization/impl/repo.py | 7 ++ .../fai_backend/auth_v2/fastapi_auth.py | 12 ++++ fai-rag-app/fai-backend/fai_backend/main.py | 2 + .../fai-backend/fai_backend/repositories.py | 15 +++- fai-rag-app/fai-backend/fai_backend/setup.py | 4 +- 10 files changed, 189 insertions(+), 5 deletions(-) create mode 100644 fai-rag-app/fai-backend/fai_backend/auth_v2/api_key/dependencies.py create mode 100644 fai-rag-app/fai-backend/fai_backend/auth_v2/api_key/models.py create mode 100644 fai-rag-app/fai-backend/fai_backend/auth_v2/api_key/routes.py create mode 100644 fai-rag-app/fai-backend/fai_backend/auth_v2/api_key/service.py diff --git a/fai-rag-app/fai-backend/fai_backend/auth_v2/api_key/dependencies.py b/fai-rag-app/fai-backend/fai_backend/auth_v2/api_key/dependencies.py new file mode 100644 index 00000000..170731ee --- /dev/null +++ b/fai-rag-app/fai-backend/fai_backend/auth_v2/api_key/dependencies.py @@ -0,0 +1,6 @@ +from fai_backend.auth_v2.api_key.service import ApiKeyService +from fai_backend.repositories import api_key_repo + + +async def get_api_key_service() -> ApiKeyService: + return ApiKeyService(repo=api_key_repo) diff --git a/fai-rag-app/fai-backend/fai_backend/auth_v2/api_key/models.py b/fai-rag-app/fai-backend/fai_backend/auth_v2/api_key/models.py new file mode 100644 index 00000000..d2d4d926 --- /dev/null +++ b/fai-rag-app/fai-backend/fai_backend/auth_v2/api_key/models.py @@ -0,0 +1,20 @@ +from beanie import Document +from pydantic import BaseModel + + +class ApiKeyModel(BaseModel): + key: str + scopes: list[str] + + +class ReadOnlyApiKeyModel(BaseModel): + revoke_id: str + redacted_key: str + scopes: list[str] + + +class ApiKeyDocumentModel(Document): + api_key: ApiKeyModel + + class Settings: + name = 'api_key' diff --git a/fai-rag-app/fai-backend/fai_backend/auth_v2/api_key/routes.py b/fai-rag-app/fai-backend/fai_backend/auth_v2/api_key/routes.py new file mode 100644 index 00000000..0cee6b37 --- /dev/null +++ b/fai-rag-app/fai-backend/fai_backend/auth_v2/api_key/routes.py @@ -0,0 +1,71 @@ +from fastapi import APIRouter, status +from pydantic import BaseModel + +from fai_backend.auth_v2.api_key.dependencies import get_api_key_service +from fai_backend.auth_v2.api_key.models import ReadOnlyApiKeyModel +from fai_backend.auth_v2.fastapi_auth import AuthRouterDecorator + +router = APIRouter( + prefix='/api/auth', + tags=['Auth', 'API Key'] +) +auth = AuthRouterDecorator(router) + + +class CreateApiKeyRequest(BaseModel): + scopes: list[str] + + +class CreateApiKeyResponse(BaseModel): + key: str + revoke_id: str + + +@auth.post( + '/apikey', + ['can_manage_api_keys'], + response_model=CreateApiKeyResponse, + description=''' + Create a new API key with the given scopes. + ''', + status_code=status.HTTP_201_CREATED, +) +async def create_api_key(body: CreateApiKeyRequest): + service = await get_api_key_service() + revoke_id, key = await service.create(scopes=body.scopes) + return CreateApiKeyResponse(key=key, revoke_id=str(revoke_id)) + + +class ListApiKeyResponse(BaseModel): + api_keys: list[ReadOnlyApiKeyModel] + + +@auth.get( + '/apikey', + ['can_manage_api_keys'], + response_model=ListApiKeyResponse, + description=''' +List information about all API keys. + +Keys themselves are redacted for security purposes. + ''', +) +async def list_api_keys(): + service = await get_api_key_service() + keys = await service.list() + return ListApiKeyResponse(api_keys=keys) + + +@auth.delete( + '/apikey/{revoke_id}', + ['can_manage_api_keys'], + description=''' +Revoke a specific API key (permanently delete it). + +The `revoke_id` can be found through the `create` and `list` operations. + ''', + status_code=status.HTTP_204_NO_CONTENT, +) +async def revoke_api_key(revoke_id: str): + service = await get_api_key_service() + await service.revoke(revoke_id) diff --git a/fai-rag-app/fai-backend/fai_backend/auth_v2/api_key/service.py b/fai-rag-app/fai-backend/fai_backend/auth_v2/api_key/service.py new file mode 100644 index 00000000..696741b7 --- /dev/null +++ b/fai-rag-app/fai-backend/fai_backend/auth_v2/api_key/service.py @@ -0,0 +1,47 @@ +import uuid + +from fai_backend.auth_v2.api_key.models import ApiKeyDocumentModel, ApiKeyModel, ReadOnlyApiKeyModel +from fai_backend.repository.interface import IAsyncRepo + + +class ApiKeyService: + def __init__(self, repo: IAsyncRepo[ApiKeyDocumentModel]): + self._repo = repo + + async def create(self, scopes: list[str]) -> (str, str): + key = f'fai-{uuid.uuid4().hex}' + api_key = ApiKeyModel(key=key, scopes=scopes) + new_entry = await self._repo.create(ApiKeyDocumentModel(api_key=api_key)) + return new_entry.id, key + + async def revoke(self, key_repo_id: str): + await self._repo.delete(key_repo_id) + + async def list(self) -> list[ReadOnlyApiKeyModel]: + all_keys = await self._repo.list() + return [ApiKeyService._to_read_only_api_key(key_data) for key_data in all_keys] + + async def find_by_key(self, key: str) -> ReadOnlyApiKeyModel | None: + all_keys = await self._repo.list() + return next( + (ApiKeyService._to_read_only_api_key(key_data) for key_data in all_keys if key_data.api_key.key == key), + None) + + async def find_by_revoke_id(self, revoke_id: str) -> ReadOnlyApiKeyModel | None: + all_keys = await self._repo.list() + return next( + (ApiKeyService._to_read_only_api_key(key_data) for key_data in all_keys if + str(key_data.id) == revoke_id), + None) + + @staticmethod + def _to_read_only_api_key(key_data): + return ReadOnlyApiKeyModel( + revoke_id=str(key_data.id), + redacted_key=ApiKeyService._redact_key(key_data.api_key.key), + scopes=key_data.api_key.scopes, + ) + + @staticmethod + def _redact_key(key: str) -> str: + return key[:8] + "..." + key[-4:] diff --git a/fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/impl/api_key.py b/fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/impl/api_key.py index 979096cd..21dc3921 100644 --- a/fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/impl/api_key.py +++ b/fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/impl/api_key.py @@ -1,10 +1,16 @@ +from fai_backend.auth_v2.api_key.dependencies import get_api_key_service from fai_backend.auth_v2.authentication.models import AuthenticatedIdentity, IAuthenticationProvider, AuthenticationType class ApiKeyProvider(IAuthenticationProvider): async def validate(self, data: str | None) -> AuthenticatedIdentity | None: - # TODO + service = await get_api_key_service() + key = await service.find_by_key(data) + + if not key: + return None + return AuthenticatedIdentity( type=AuthenticationType.API_KEY, - uid="fai-blablabla" + uid=key.revoke_id ) diff --git a/fai-rag-app/fai-backend/fai_backend/auth_v2/authorization/impl/repo.py b/fai-rag-app/fai-backend/fai_backend/auth_v2/authorization/impl/repo.py index fdee2926..2e8d231b 100644 --- a/fai-rag-app/fai-backend/fai_backend/auth_v2/authorization/impl/repo.py +++ b/fai-rag-app/fai-backend/fai_backend/auth_v2/authorization/impl/repo.py @@ -1,3 +1,4 @@ +from fai_backend.auth_v2.api_key.dependencies import get_api_key_service from fai_backend.auth_v2.authentication.models import AuthenticatedIdentity, AuthenticationType from fai_backend.auth_v2.authorization.models import IAuthorizationProvider from fai_backend.repositories import users_repo @@ -11,4 +12,10 @@ async def has_scopes(self, identity: AuthenticatedIdentity, scopes: list[str]) - permissions = user.projects[0].permissions return all(key in permissions and permissions[key] is True for key in scopes) + api_key_service = await get_api_key_service() + api_key = await api_key_service.find_by_revoke_id(identity.uid) + + if api_key is not None: + return all(scope in api_key.scopes for scope in scopes) + return False diff --git a/fai-rag-app/fai-backend/fai_backend/auth_v2/fastapi_auth.py b/fai-rag-app/fai-backend/fai_backend/auth_v2/fastapi_auth.py index a5638de9..576ae78d 100644 --- a/fai-rag-app/fai-backend/fai_backend/auth_v2/fastapi_auth.py +++ b/fai-rag-app/fai-backend/fai_backend/auth_v2/fastapi_auth.py @@ -157,6 +157,7 @@ def route( response_model: Any | None = None, response_description: str | None = None, response_400_description: str | None = None, + status_code: int | None = None, ): if required_scopes is None: required_scopes = [] @@ -177,6 +178,7 @@ def inner_decorator(func): response_description=response_description, responses=get_auth_responses(response_400_description), dependencies=deps, + status_code=status_code, )(fn) return inner_decorator @@ -190,6 +192,7 @@ def get( response_model: Any | None = None, response_description: str | None = None, response_400_description: str | None = None, + status_code: int | None = None, ): return AuthRouterDecorator.route( router_method=self.api_router.get, @@ -200,6 +203,7 @@ def get( response_model=response_model, response_description=response_description, response_400_description=response_400_description, + status_code=status_code, ) def post( @@ -211,6 +215,7 @@ def post( response_model: Any | None = None, response_description: str | None = None, response_400_description: str | None = None, + status_code: int | None = None, ): return AuthRouterDecorator.route( router_method=self.api_router.post, @@ -221,6 +226,7 @@ def post( response_model=response_model, response_description=response_description, response_400_description=response_400_description, + status_code=status_code, ) def put( @@ -232,6 +238,7 @@ def put( response_model: Any | None = None, response_description: str | None = None, response_400_description: str | None = None, + status_code: int | None = None, ): return AuthRouterDecorator.route( router_method=self.api_router.put, @@ -242,6 +249,7 @@ def put( response_model=response_model, response_description=response_description, response_400_description=response_400_description, + status_code=status_code, ) def patch( @@ -253,6 +261,7 @@ def patch( response_model: Any | None = None, response_description: str | None = None, response_400_description: str | None = None, + status_code: int | None = None, ): return AuthRouterDecorator.route( router_method=self.api_router.patch, @@ -263,6 +272,7 @@ def patch( response_model=response_model, response_description=response_description, response_400_description=response_400_description, + status_code=status_code, ) def delete( @@ -274,6 +284,7 @@ def delete( response_model: Any | None = None, response_description: str | None = None, response_400_description: str | None = None, + status_code: int | None = None, ): return AuthRouterDecorator.route( router_method=self.api_router.delete, @@ -284,4 +295,5 @@ def delete( response_model=response_model, response_description=response_description, response_400_description=response_400_description, + status_code=status_code, ) diff --git a/fai-rag-app/fai-backend/fai_backend/main.py b/fai-rag-app/fai-backend/fai_backend/main.py index cc6b9372..1ec1ed0f 100644 --- a/fai-rag-app/fai-backend/fai_backend/main.py +++ b/fai-rag-app/fai-backend/fai_backend/main.py @@ -25,6 +25,7 @@ from fai_backend.collection_v2.routes import router as collection_router from fai_backend.collection_v2.view_routes import router as collection_view_router from fai_backend.auth_v2.test_routes import router as auth_test_router +from fai_backend.auth_v2.api_key.routes import router as api_key_router @asynccontextmanager @@ -59,6 +60,7 @@ async def lifespan(_app: FastAPI): app.include_router(collection_router) app.include_router(collection_view_router) app.include_router(auth_test_router) +app.include_router(api_key_router) app.middleware('http')(add_git_revision_to_request_header) app.middleware('http')(remove_trailing_slash) diff --git a/fai-rag-app/fai-backend/fai_backend/repositories.py b/fai-rag-app/fai-backend/fai_backend/repositories.py index e467ef27..ee4b8363 100644 --- a/fai-rag-app/fai-backend/fai_backend/repositories.py +++ b/fai-rag-app/fai-backend/fai_backend/repositories.py @@ -1,11 +1,12 @@ -import time from typing import Protocol +import time from beanie import Document, Indexed from pydantic import EmailStr, Field from fai_backend.assistant.models import AssistantChatHistoryModel, StoredQuestionModel from fai_backend.auth.security import is_mail_pattern, try_match_email +from fai_backend.auth_v2.api_key.models import ApiKeyDocumentModel from fai_backend.collection.models import CollectionMetadataModel from fai_backend.conversations.models import Conversation from fai_backend.projects.schema import Project, ProjectMember @@ -59,7 +60,11 @@ def filter_user_projects(project_list: list[Project]) -> list[Project]: return exact_match_projects for proj in project_list: - pattern = next(pattern for pattern in extract_member_emails(proj.members) if is_mail_pattern(pattern)) + pattern = next((pattern for pattern in extract_member_emails(proj.members) if is_mail_pattern(pattern)), + None) + + if pattern is None: + return [] if try_match_email(email, pattern): new_project = proj.model_copy(deep=True) @@ -126,6 +131,10 @@ class StoredQuestionsRepository(IAsyncRepo[StoredQuestionModel]): pass +class ApiKeyRepository(IAsyncRepo[ApiKeyDocumentModel]): + pass + + repo_factory.register_builder( { ProjectRepository: lambda: create_repo_from_env(ProjectModel, ProjectModel), @@ -135,6 +144,7 @@ class StoredQuestionsRepository(IAsyncRepo[StoredQuestionModel]): ChatHistoryRepository: lambda: create_repo_from_env(AssistantChatHistoryModel, AssistantChatHistoryModel), CollectionMetadataRepository: lambda: create_repo_from_env(CollectionMetadataModel, CollectionMetadataModel), StoredQuestionsRepository: lambda: create_repo_from_env(StoredQuestionModel, StoredQuestionModel), + ApiKeyRepository: lambda: create_repo_from_env(ApiKeyDocumentModel, ApiKeyDocumentModel), } ) @@ -145,3 +155,4 @@ class StoredQuestionsRepository(IAsyncRepo[StoredQuestionModel]): chat_history_repo = repo_factory.create(ChatHistoryRepository) collection_metadata_repo = repo_factory.create(CollectionMetadataRepository) stored_questions_repo = repo_factory.create(StoredQuestionsRepository) +api_key_repo = repo_factory.create(ApiKeyRepository) diff --git a/fai-rag-app/fai-backend/fai_backend/setup.py b/fai-rag-app/fai-backend/fai_backend/setup.py index 6cd1e170..e9e0ecc2 100644 --- a/fai-rag-app/fai-backend/fai_backend/setup.py +++ b/fai-rag-app/fai-backend/fai_backend/setup.py @@ -8,6 +8,7 @@ from motor.motor_asyncio import AsyncIOMotorClient from fai_backend.assistant.models import AssistantChatHistoryModel, StoredQuestionModel, AssistantTemplate +from fai_backend.auth_v2.api_key.models import ApiKeyDocumentModel from fai_backend.collection.models import CollectionMetadataModel from fai_backend.config import settings from fai_backend.projects.dependencies import get_project_service @@ -103,7 +104,8 @@ async def setup_db(): ConversationDocument, AssistantChatHistoryModel, CollectionMetadataModel, - StoredQuestionModel + StoredQuestionModel, + ApiKeyDocumentModel, ] ) From 55c998f638d46379022801f7319be240ec9397aa Mon Sep 17 00:00:00 2001 From: MasterKenth Date: Wed, 29 Jan 2025 14:23:12 +0100 Subject: [PATCH 6/7] =?UTF-8?q?=E2=9C=A8=20Feat(api=20keys):=20limit=20sco?= =?UTF-8?q?pes=20of=20new=20API=20keys=20to=20a=20subset=20of=20creators?= =?UTF-8?q?=20scopes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fai_backend/auth_v2/api_key/routes.py | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/fai-rag-app/fai-backend/fai_backend/auth_v2/api_key/routes.py b/fai-rag-app/fai-backend/fai_backend/auth_v2/api_key/routes.py index 0cee6b37..d4f64017 100644 --- a/fai-rag-app/fai-backend/fai_backend/auth_v2/api_key/routes.py +++ b/fai-rag-app/fai-backend/fai_backend/auth_v2/api_key/routes.py @@ -1,8 +1,10 @@ -from fastapi import APIRouter, status +from fastapi import APIRouter, status, HTTPException from pydantic import BaseModel from fai_backend.auth_v2.api_key.dependencies import get_api_key_service from fai_backend.auth_v2.api_key.models import ReadOnlyApiKeyModel +from fai_backend.auth_v2.authentication.models import AuthenticatedIdentity +from fai_backend.auth_v2.authorization.factory import AuthorizationFactory from fai_backend.auth_v2.fastapi_auth import AuthRouterDecorator router = APIRouter( @@ -25,14 +27,32 @@ class CreateApiKeyResponse(BaseModel): '/apikey', ['can_manage_api_keys'], response_model=CreateApiKeyResponse, + summary='Create API Key', description=''' Create a new API key with the given scopes. ''', status_code=status.HTTP_201_CREATED, + response_400_description='No scopes provided.' ) -async def create_api_key(body: CreateApiKeyRequest): +async def create_api_key(body: CreateApiKeyRequest, auth_identity: AuthenticatedIdentity): + desired_scopes = [scope for scope in body.scopes if len(scope) > 0] + + if len(desired_scopes) == 0: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='No scopes provided.' + ) + service = await get_api_key_service() - revoke_id, key = await service.create(scopes=body.scopes) + + authorization_provider = await AuthorizationFactory.get() + if not await authorization_provider.has_scopes(auth_identity, desired_scopes): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"Can not assign scope(s) outside of creator's scopes to an API key." + ) + + revoke_id, key = await service.create(scopes=desired_scopes) return CreateApiKeyResponse(key=key, revoke_id=str(revoke_id)) From 8650fab33b574f038a38a7d9c4307a8f21c2ed90 Mon Sep 17 00:00:00 2001 From: MasterKenth Date: Thu, 30 Jan 2025 11:40:55 +0100 Subject: [PATCH 7/7] =?UTF-8?q?=E2=9C=A8=20Feat(auth):=20hash=20api=20key?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fai_backend/auth_v2/api_key/models.py | 5 +++-- .../fai_backend/auth_v2/api_key/service.py | 22 ++++++++++++++----- .../auth_v2/authentication/impl/api_key.py | 1 - .../authentication/impl/bearer_token.py | 1 - .../auth_v2/authentication/models.py | 1 - .../fai_backend/auth_v2/fastapi_auth.py | 2 +- 6 files changed, 21 insertions(+), 11 deletions(-) diff --git a/fai-rag-app/fai-backend/fai_backend/auth_v2/api_key/models.py b/fai-rag-app/fai-backend/fai_backend/auth_v2/api_key/models.py index d2d4d926..53ea9ffb 100644 --- a/fai-rag-app/fai-backend/fai_backend/auth_v2/api_key/models.py +++ b/fai-rag-app/fai-backend/fai_backend/auth_v2/api_key/models.py @@ -3,13 +3,14 @@ class ApiKeyModel(BaseModel): - key: str + key_hash: str + key_hint: str scopes: list[str] class ReadOnlyApiKeyModel(BaseModel): revoke_id: str - redacted_key: str + key_hint: str scopes: list[str] diff --git a/fai-rag-app/fai-backend/fai_backend/auth_v2/api_key/service.py b/fai-rag-app/fai-backend/fai_backend/auth_v2/api_key/service.py index 696741b7..b9dce0b0 100644 --- a/fai-rag-app/fai-backend/fai_backend/auth_v2/api_key/service.py +++ b/fai-rag-app/fai-backend/fai_backend/auth_v2/api_key/service.py @@ -1,6 +1,10 @@ +import crypt +import hashlib +import hmac import uuid from fai_backend.auth_v2.api_key.models import ApiKeyDocumentModel, ApiKeyModel, ReadOnlyApiKeyModel +from fai_backend.config import settings from fai_backend.repository.interface import IAsyncRepo @@ -10,7 +14,9 @@ def __init__(self, repo: IAsyncRepo[ApiKeyDocumentModel]): async def create(self, scopes: list[str]) -> (str, str): key = f'fai-{uuid.uuid4().hex}' - api_key = ApiKeyModel(key=key, scopes=scopes) + key_hash = self._hash_api_key(key) + key_hint = self._create_key_hint(key) + api_key = ApiKeyModel(key_hash=key_hash, key_hint=key_hint, scopes=scopes) new_entry = await self._repo.create(ApiKeyDocumentModel(api_key=api_key)) return new_entry.id, key @@ -23,8 +29,10 @@ async def list(self) -> list[ReadOnlyApiKeyModel]: async def find_by_key(self, key: str) -> ReadOnlyApiKeyModel | None: all_keys = await self._repo.list() + key_hash = self._hash_api_key(key) return next( - (ApiKeyService._to_read_only_api_key(key_data) for key_data in all_keys if key_data.api_key.key == key), + (ApiKeyService._to_read_only_api_key(key_data) for key_data in all_keys if + key_data.api_key.key_hash == key_hash), None) async def find_by_revoke_id(self, revoke_id: str) -> ReadOnlyApiKeyModel | None: @@ -35,13 +43,17 @@ async def find_by_revoke_id(self, revoke_id: str) -> ReadOnlyApiKeyModel | None: None) @staticmethod - def _to_read_only_api_key(key_data): + def _hash_api_key(key: str) -> str: + return hmac.new(settings.SECRET_KEY.encode(), key.encode(), hashlib.sha256).hexdigest() + + @staticmethod + def _to_read_only_api_key(key_data: ApiKeyDocumentModel): return ReadOnlyApiKeyModel( revoke_id=str(key_data.id), - redacted_key=ApiKeyService._redact_key(key_data.api_key.key), + key_hint=key_data.api_key.key_hint, scopes=key_data.api_key.scopes, ) @staticmethod - def _redact_key(key: str) -> str: + def _create_key_hint(key: str) -> str: return key[:8] + "..." + key[-4:] diff --git a/fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/impl/api_key.py b/fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/impl/api_key.py index 21dc3921..c17db316 100644 --- a/fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/impl/api_key.py +++ b/fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/impl/api_key.py @@ -11,6 +11,5 @@ async def validate(self, data: str | None) -> AuthenticatedIdentity | None: return None return AuthenticatedIdentity( - type=AuthenticationType.API_KEY, uid=key.revoke_id ) diff --git a/fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/impl/bearer_token.py b/fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/impl/bearer_token.py index 5917c2a8..2205411a 100644 --- a/fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/impl/bearer_token.py +++ b/fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/impl/bearer_token.py @@ -6,6 +6,5 @@ class BearerTokenProvider(IAuthenticationProvider): async def validate(self, data: str | None) -> AuthenticatedIdentity | None: return AuthenticatedIdentity( - type=AuthenticationType.BEARER_TOKEN, uid=json.loads(data)['email'] ) diff --git a/fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/models.py b/fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/models.py index 8419453e..9a95466b 100644 --- a/fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/models.py +++ b/fai-rag-app/fai-backend/fai_backend/auth_v2/authentication/models.py @@ -10,7 +10,6 @@ class AuthenticationType: class AuthenticatedIdentity(BaseModel): - type: str uid: str diff --git a/fai-rag-app/fai-backend/fai_backend/auth_v2/fastapi_auth.py b/fai-rag-app/fai-backend/fai_backend/auth_v2/fastapi_auth.py index 576ae78d..6f93ad79 100644 --- a/fai-rag-app/fai-backend/fai_backend/auth_v2/fastapi_auth.py +++ b/fai-rag-app/fai-backend/fai_backend/auth_v2/fastapi_auth.py @@ -74,7 +74,7 @@ async def auth_dependency( bearer_token: JwtAuthorizationCredentials | None = Depends(bearer_token_source), ) -> AuthenticatedIdentity: if settings.HTTP_AUTHENTICATION_TYPE == 'none': - return AuthenticatedIdentity(type=AuthenticationType.NONE, uid='') + return AuthenticatedIdentity(uid='') credential_types = [ (AuthenticationType.API_KEY, api_key),