Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate Firebase Auth #28

Merged
merged 12 commits into from
Mar 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Project Nalgonda is an advanced platform designed for the orchestration and oper
- **Tool Execution**: Executes tools within an established framework for accomplishing a wide range of tasks.
- **User Management**: User access management features for interaction with different agencies.
- **API and WebSocket Routers**: Lays down a comprehensive set of API endpoints and WebSocket routes for external interactions and real-time communications.
- **Security**: Basic implementation of JWT for user authentication and authorization with plans for further enhancements.
- **Security**: Basic implementation of Firebase Auth for user authentication with plans for further enhancements.

## Installation

Expand Down
5 changes: 2 additions & 3 deletions nalgonda/dependencies/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,5 @@
This directory is crucial for managing the external dependencies and service interfaces used throughout the project.
It contains:

- `auth.py`: Handles user authentication and authorization with OAuth2, including JWT token management.
- `dependencies.py`: Centralizes dependency injections for Redis, Agency, Agent, and Thread managers among others,
ensuring modularity and ease of use.
- `auth.py`: Handles user authentication with Firebase.
- `dependencies.py`: Centralizes dependency injections for Redis, Agency, Agent, and Thread managers among others.
66 changes: 21 additions & 45 deletions nalgonda/dependencies/auth.py
Original file line number Diff line number Diff line change
@@ -1,61 +1,37 @@
import logging
from http import HTTPStatus
from typing import Annotated

from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from starlette.status import HTTP_400_BAD_REQUEST, HTTP_403_FORBIDDEN
from fastapi import Depends, HTTPException
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from firebase_admin import auth
from firebase_admin.exceptions import InvalidArgumentError, UnknownError

from nalgonda.models.auth import TokenData, UserInDB
from nalgonda.repositories.user_repository import UserRepository
from nalgonda.settings import settings
from nalgonda.models.auth import User

logger = logging.getLogger(__name__)
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="v1/api/token")

security = HTTPBearer()

def get_user(username: str) -> UserInDB | None:
user = UserRepository().get_user_by_id(username)
if user:
return UserInDB(**user)


async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]) -> UserInDB:
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
async def get_current_user(credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)]) -> User:
try:
payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm])
username: str = payload.get("sub")
if username is None:
logger.error(f"Invalid token: {token}")
raise credentials_exception
token_data = TokenData(username=username)
except JWTError:
logger.error(f"Invalid token: {token}")
raise credentials_exception from None
user = get_user(username=token_data.username)
if user is None:
logger.error(f"User not found: {token_data.username}, token: {token}")
raise credentials_exception
return user


async def get_current_active_user(
current_user: Annotated[UserInDB, Depends(get_current_user)],
) -> UserInDB:
if current_user.disabled:
logger.error(f"User {current_user.id} is inactive")
raise HTTPException(status_code=HTTP_400_BAD_REQUEST, detail="Inactive user")
return current_user
user = auth.verify_id_token(credentials.credentials, check_revoked=True)
except (ValueError, InvalidArgumentError, UnknownError) as err:
logger.error(f"Invalid authentication credentials: {err}")
raise HTTPException(
status_code=HTTPStatus.UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": 'Bearer error="invalid_token"'},
) from None
logger.info(f"Authenticated user: {user['uid']} ({user['email']})")
return User(id=user["uid"], email=user["email"])


async def get_current_superuser(
current_user: Annotated[UserInDB, Depends(get_current_active_user)],
) -> UserInDB:
current_user: Annotated[User, Depends(get_current_user)],
) -> User:
if not current_user.is_superuser:
logger.error(f"User {current_user.id} is not a superuser")
raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="The user doesn't have enough privileges")
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="The user doesn't have enough privileges")
return current_user
18 changes: 2 additions & 16 deletions nalgonda/models/auth.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,7 @@
from pydantic import BaseModel


class Token(BaseModel):
access_token: str
token_type: str


class TokenData(BaseModel):
username: str


class User(BaseModel):
username: str # used as DB ID, FIXME
disabled: bool = False
is_superuser: bool = False


class UserInDB(User):
id: str
hashed_password: str
email: str
is_superuser: bool = False
17 changes: 0 additions & 17 deletions nalgonda/repositories/user_repository.py

This file was deleted.

4 changes: 2 additions & 2 deletions nalgonda/routers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ This directory defines the API routing logic for the project, organizing routes
and versions. Key components include:

- `__init__.py` & `v1/__init__.py`: Establish the application's routing logic, categorizing the APIs and handling errors.
- `agency.py`, `agent.py`, `auth.py`, `session.py`, `tool.py`: Define the endpoints for managing agencies, agents,
authentication, sessions, and tools, respectively.
- `agency.py`, `agent.py`, `session.py`, `tool.py`: Define the endpoints for managing agencies, agents,
sessions, and tools, respectively.
- `websocket.py`: Sets up WebSocket endpoints for real-time messaging.

The Swagger documentation is available at `/v1/docs`.
2 changes: 0 additions & 2 deletions nalgonda/routers/v1/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

from .agency import agency_router
from .agent import agent_router
from .auth import auth_router
from .message import message_router
from .session import session_router
from .tool import tool_router
Expand All @@ -15,7 +14,6 @@
responses={404: {"description": "Not found"}},
)

api_router.include_router(auth_router)
api_router.include_router(tool_router)
api_router.include_router(agent_router)
api_router.include_router(agency_router)
Expand Down
26 changes: 13 additions & 13 deletions nalgonda/routers/v1/api/agency.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import asyncio
import logging
from http import HTTPStatus
from typing import Annotated

from fastapi import APIRouter, Depends, HTTPException
from fastapi.params import Query
from starlette.status import HTTP_200_OK, HTTP_400_BAD_REQUEST, HTTP_403_FORBIDDEN, HTTP_404_NOT_FOUND

from nalgonda.dependencies.auth import get_current_active_user
from nalgonda.dependencies.auth import get_current_user
from nalgonda.dependencies.dependencies import get_agency_manager
from nalgonda.models.agency_config import AgencyConfig
from nalgonda.models.auth import UserInDB
from nalgonda.models.auth import User
from nalgonda.repositories.agency_config_firestore_storage import AgencyConfigFirestoreStorage
from nalgonda.repositories.agent_config_firestore_storage import AgentConfigFirestoreStorage
from nalgonda.services.agency_manager import AgencyManager
Expand All @@ -24,7 +24,7 @@

@agency_router.get("/agency/list")
async def get_agency_list(
current_user: Annotated[UserInDB, Depends(get_current_active_user)],
current_user: Annotated[User, Depends(get_current_user)],
storage: AgencyConfigFirestoreStorage = Depends(AgencyConfigFirestoreStorage),
) -> list[AgencyConfig]:
agencies = storage.load_by_owner_id(current_user.id) + storage.load_by_owner_id(None)
Expand All @@ -33,25 +33,25 @@

@agency_router.get("/agency")
async def get_agency_config(
current_user: Annotated[UserInDB, Depends(get_current_active_user)],
current_user: Annotated[User, Depends(get_current_user)],
agency_id: str = Query(..., description="The unique identifier of the agency"),
storage: AgencyConfigFirestoreStorage = Depends(AgencyConfigFirestoreStorage),
) -> AgencyConfig:
agency_config = storage.load_by_agency_id(agency_id)
if not agency_config:
logger.warning(f"Agency not found: {agency_id}, user: {current_user.id}")
raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail="Agency not found")
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Agency not found")
# check if the current_user has permissions to get the agency config
if agency_config.owner_id and agency_config.owner_id != current_user.id:
logger.warning(f"User {current_user.id} does not have permissions to get agency {agency_id}")
raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="Forbidden")
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Forbidden")
return agency_config


@agency_router.put("/agency", status_code=HTTP_200_OK)
@agency_router.put("/agency", status_code=HTTPStatus.OK)
async def update_or_create_agency(
agency_config: AgencyConfig,
current_user: Annotated[UserInDB, Depends(get_current_active_user)],
current_user: Annotated[User, Depends(get_current_user)],
agency_manager: AgencyManager = Depends(get_agency_manager),
agency_storage: AgencyConfigFirestoreStorage = Depends(AgencyConfigFirestoreStorage),
agent_storage: AgentConfigFirestoreStorage = Depends(AgentConfigFirestoreStorage),
Expand All @@ -67,22 +67,22 @@
agency_config_db = agency_storage.load_by_agency_id(agency_config.agency_id)
if not agency_config_db:
logger.warning(f"Agency not found: {agency_config.agency_id}, user: {current_user.id}")
raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail="Agency not found")
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Agency not found")

Check warning on line 70 in nalgonda/routers/v1/api/agency.py

View check run for this annotation

Codecov / codecov/patch

nalgonda/routers/v1/api/agency.py#L70

Added line #L70 was not covered by tests
if agency_config_db.owner_id != current_user.id:
logger.warning(
f"User {current_user.id} does not have permissions to update agency {agency_config.agency_id}"
)
raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="Forbidden")
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Forbidden")

# check that all used agents belong to the current user
for agent_id in agency_config.agents:
agent_config = await asyncio.to_thread(agent_storage.load_by_agent_id, agent_id)
if not agent_config:
logger.error(f"Agent not found: {agent_id}, user: {current_user.id}")
raise HTTPException(status_code=HTTP_400_BAD_REQUEST, detail=f"Agent not found: {agent_id}")
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=f"Agent not found: {agent_id}")
if agent_config.owner_id != current_user.id:
logger.warning(f"User {current_user.id} does not have permissions to use agent {agent_id}")
raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="Forbidden")
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Forbidden")
# FIXME: current limitation: all agents must belong to the current user.
# to fix: If the agent is a template (agent_config.owner_id is None), it should be copied for the current user
# (reuse the code from api/agent.py)
Expand Down
22 changes: 11 additions & 11 deletions nalgonda/routers/v1/api/agent.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import logging
from http import HTTPStatus
from typing import Annotated

from fastapi import APIRouter, Body, Depends, HTTPException
from fastapi.params import Query
from starlette.status import HTTP_400_BAD_REQUEST, HTTP_403_FORBIDDEN, HTTP_404_NOT_FOUND

from nalgonda.dependencies.auth import get_current_active_user
from nalgonda.dependencies.auth import get_current_user
from nalgonda.dependencies.dependencies import get_agent_manager
from nalgonda.models.agent_config import AgentConfig
from nalgonda.models.auth import UserInDB
from nalgonda.models.auth import User
from nalgonda.repositories.agent_config_firestore_storage import AgentConfigFirestoreStorage
from nalgonda.services.agent_manager import AgentManager
from nalgonda.services.env_vars_manager import ContextEnvVarsManager
Expand All @@ -24,7 +24,7 @@

@agent_router.get("/agent/list")
async def get_agent_list(
current_user: Annotated[UserInDB, Depends(get_current_active_user)],
current_user: Annotated[User, Depends(get_current_user)],
storage: AgentConfigFirestoreStorage = Depends(AgentConfigFirestoreStorage),
) -> list[AgentConfig]:
agents = storage.load_by_owner_id(current_user.id) + storage.load_by_owner_id(None)
Expand All @@ -33,24 +33,24 @@

@agent_router.get("/agent")
async def get_agent_config(
current_user: Annotated[UserInDB, Depends(get_current_active_user)],
current_user: Annotated[User, Depends(get_current_user)],
agent_id: str = Query(..., description="The unique identifier of the agent"),
storage: AgentConfigFirestoreStorage = Depends(AgentConfigFirestoreStorage),
) -> AgentConfig:
agent_config = storage.load_by_agent_id(agent_id)
if not agent_config:
logger.warning(f"Agent not found: {agent_id}, user: {current_user.id}")
raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail="Agent not found")
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Agent not found")

Check warning on line 43 in nalgonda/routers/v1/api/agent.py

View check run for this annotation

Codecov / codecov/patch

nalgonda/routers/v1/api/agent.py#L43

Added line #L43 was not covered by tests
# check if the current user is the owner of the agent
if agent_config.owner_id and agent_config.owner_id != current_user.id:
logger.warning(f"User {current_user.id} does not have permissions to access agent: {agent_id}")
raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="Forbidden")
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Forbidden")

Check warning on line 47 in nalgonda/routers/v1/api/agent.py

View check run for this annotation

Codecov / codecov/patch

nalgonda/routers/v1/api/agent.py#L47

Added line #L47 was not covered by tests
return agent_config


@agent_router.put("/agent")
async def create_or_update_agent(
current_user: Annotated[UserInDB, Depends(get_current_active_user)],
current_user: Annotated[User, Depends(get_current_user)],
agent_config: AgentConfig = Body(...),
agent_manager: AgentManager = Depends(get_agent_manager),
storage: AgentConfigFirestoreStorage = Depends(AgentConfigFirestoreStorage),
Expand All @@ -65,18 +65,18 @@
agent_config_db = storage.load_by_agent_id(agent_config.agent_id)
if not agent_config_db:
logger.warning(f"Agent not found: {agent_config.agent_id}, user: {current_user.id}")
raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail="Agent not found")
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Agent not found")

Check warning on line 68 in nalgonda/routers/v1/api/agent.py

View check run for this annotation

Codecov / codecov/patch

nalgonda/routers/v1/api/agent.py#L68

Added line #L68 was not covered by tests
if agent_config_db.owner_id != current_user.id:
logger.warning(
f"User {current_user.id} does not have permissions to access agent: {agent_config.agent_id}"
)
raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="Forbidden")
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Forbidden")
# Ensure the agent name has not been changed
if agent_config.name != agent_config_db.name:
logger.warning(
f"Renaming agents is not supported yet: {agent_config.agent_id}, user: {current_user.id}"
)
raise HTTPException(status_code=HTTP_400_BAD_REQUEST, detail="Renaming agents is not supported yet")
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="Renaming agents is not supported yet")

Check warning on line 79 in nalgonda/routers/v1/api/agent.py

View check run for this annotation

Codecov / codecov/patch

nalgonda/routers/v1/api/agent.py#L79

Added line #L79 was not covered by tests

# Ensure the agent is associated with the current user
agent_config.owner_id = current_user.id
Expand Down
61 changes: 0 additions & 61 deletions nalgonda/routers/v1/api/auth.py

This file was deleted.

Loading
Loading