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

allow accessing public project data #759

Merged
merged 1 commit into from
Jan 19, 2025
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
10 changes: 10 additions & 0 deletions carbonserver/carbonserver/api/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ class NotAllowedError(ErrorBase):
code: NotAllowedErrorEnum


class NotFoundErrorEnum(str, Enum):
NOT_FOUND = "NOT_FOUND"


class NotFoundError(ErrorBase):
code: NotFoundErrorEnum


class UserException(Exception):
def __init__(self, error):
self.error = error
Expand All @@ -63,4 +71,6 @@ def get_http_exception(exception) -> HTTPException:
if isinstance(exception, UserException):
if isinstance(error := exception.error, NotAllowedError):
return HTTPException(status_code=403, detail=error.message)
elif isinstance(error := exception.error, NotFoundError):
return HTTPException(status_code=404, detail=error.message)
return HTTPException(status_code=500)
1 change: 1 addition & 0 deletions carbonserver/carbonserver/api/infra/database/sql_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ class Project(Base):
id = Column(UUID(as_uuid=True), primary_key=True, index=True, default=uuid.uuid4)
name = Column(String)
description = Column(String)
public = Column(Boolean, default=False)
organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id"))
experiments = relationship("Experiment", back_populates="project")
organization = relationship("Organization", back_populates="projects")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
from typing import List

from dependency_injector.providers import Callable
from fastapi import HTTPException
from sqlalchemy import Text, and_, cast, func

from carbonserver.api.domain.projects import Projects
from carbonserver.api.errors import NotFoundError, NotFoundErrorEnum, UserException
from carbonserver.api.infra.database.sql_models import Emission as SqlModelEmission
from carbonserver.api.infra.database.sql_models import Experiment as SqlModelExperiment
from carbonserver.api.infra.database.sql_models import Project as SqlModelProject
Expand Down Expand Up @@ -38,8 +38,11 @@ def delete_project(self, project_id: str) -> None:
.first()
)
if db_project is None:
raise HTTPException(
status_code=404, detail=f"Project {project_id} not found"
raise UserException(
NotFoundError(
code=NotFoundErrorEnum.NOT_FOUND,
message=f"Project not found: {project_id}",
)
)
session.delete(db_project)
session.commit()
Expand All @@ -52,8 +55,11 @@ def get_one_project(self, project_id) -> Project:
.first()
)
if e is None:
raise HTTPException(
status_code=404, detail=f"Project {project_id} not found"
raise UserException(
NotFoundError(
code=NotFoundErrorEnum.NOT_FOUND,
message=f"Project not found: {project_id}",
)
)
experiments = (
session.query(cast(SqlModelExperiment.id, Text))
Expand All @@ -64,6 +70,22 @@ def get_one_project(self, project_id) -> Project:
project.experiments = [experiment[0] for experiment in experiments]
return project

def is_project_public(self, project_id) -> bool:
with self.session_factory() as session:
db_project = (
session.query(SqlModelProject)
.filter(SqlModelProject.id == project_id)
.first()
)
if db_project is None:
raise UserException(
NotFoundError(
code=NotFoundErrorEnum.NOT_FOUND,
message=f"Project not found: {project_id}",
)
)
return db_project.public

def get_projects_from_organization(self, organization_id) -> List[Project]:
"""Find the list of projects from a organization in database and return it

Expand Down Expand Up @@ -149,8 +171,11 @@ def patch_project(self, project_id, project) -> Project:
.first()
)
if db_project is None:
raise HTTPException(
status_code=404, detail=f"Project {project_id} not found"
raise UserException(
NotFoundError(
code=NotFoundErrorEnum.NOT_FOUND,
message=f"Project not found: {project_id}",
)
)
for attr, value in project.dict().items():
if value is not None:
Expand All @@ -171,5 +196,6 @@ def map_sql_to_schema(project: SqlModelProject) -> Project:
id=str(project.id),
name=project.name,
description=project.description,
public=project.public,
organization_id=str(project.organization_id),
)
2 changes: 1 addition & 1 deletion carbonserver/carbonserver/api/routers/authenticate.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ async def get_login(
login and redirect to frontend app with token
"""
login_url = request.url_for("login")
print("login_url", login_url)

if code:
res = requests.post(
f"{settings.fief_url}/api/token",
Expand Down
7 changes: 4 additions & 3 deletions carbonserver/carbonserver/api/routers/experiments.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from carbonserver.api.schemas import Experiment, ExperimentCreate, ExperimentReport
from carbonserver.api.services.auth_service import (
MandatoryUserWithAuthDependency,
OptionalUserWithAuthDependency,
UserWithAuthDependency,
)
from carbonserver.api.services.experiments_service import ExperimentService
Expand Down Expand Up @@ -48,7 +49,7 @@ def add_experiment(
@inject
def read_experiment(
experiment_id: str,
auth_user: UserWithAuthDependency = Depends(MandatoryUserWithAuthDependency),
auth_user: UserWithAuthDependency = Depends(OptionalUserWithAuthDependency),
experiment_service: ExperimentService = Depends(
Provide[ServerContainer.experiment_service]
),
Expand All @@ -65,7 +66,7 @@ def read_experiment(
@inject
def read_project_experiments(
project_id: str,
auth_user: UserWithAuthDependency = Depends(MandatoryUserWithAuthDependency),
auth_user: UserWithAuthDependency = Depends(OptionalUserWithAuthDependency),
experiment_service: ExperimentService = Depends(
Provide[ServerContainer.experiment_service]
),
Expand All @@ -83,7 +84,7 @@ def read_project_experiments(
@inject
def read_project_detailed_sums_by_experiment(
project_id: str,
auth_user: UserWithAuthDependency = Depends(MandatoryUserWithAuthDependency),
auth_user: UserWithAuthDependency = Depends(OptionalUserWithAuthDependency),
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None,
project_global_sum_by_experiment_usecase: ProjectSumsByExperimentUsecase = Depends(
Expand Down
3 changes: 2 additions & 1 deletion carbonserver/carbonserver/api/routers/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from carbonserver.api.schemas import Project, ProjectCreate, ProjectPatch, ProjectReport
from carbonserver.api.services.auth_service import (
MandatoryUserWithAuthDependency,
OptionalUserWithAuthDependency,
UserWithAuthDependency,
)
from carbonserver.api.services.project_service import ProjectService
Expand Down Expand Up @@ -75,7 +76,7 @@ def patch_project(
@inject
def read_project(
project_id: str,
auth_user: UserWithAuthDependency = Depends(MandatoryUserWithAuthDependency),
auth_user: UserWithAuthDependency = Depends(OptionalUserWithAuthDependency),
project_service=Depends(Provide[ServerContainer.project_service]),
) -> Project:
return project_service.get_one_project(project_id, auth_user.db_user)
Expand Down
2 changes: 2 additions & 0 deletions carbonserver/carbonserver/api/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ class ProjectCreate(ProjectBase):
class ProjectPatch(BaseModel):
name: Optional[str]
description: Optional[str]
public: Optional[bool]

# do not allow the organization_id

Expand Down Expand Up @@ -342,6 +343,7 @@ class Config:
class Project(ProjectBase):
id: UUID
experiments: Optional[List[str]] = []
public: Optional[bool]


class ProjectReport(ProjectBase):
Expand Down
32 changes: 26 additions & 6 deletions carbonserver/carbonserver/api/services/auth_context.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,58 @@
from typing import Optional
from uuid import UUID

from carbonserver.api.infra.repositories.repository_projects import (
SqlAlchemyRepository as ProjectRepository,
)
from carbonserver.api.infra.repositories.repository_projects_tokens import (
SqlAlchemyRepository as ProjectTokensRepository,
)
from carbonserver.api.infra.repositories.repository_users import (
SqlAlchemyRepository as UserRepository,
)
from carbonserver.api.schemas import User


class AuthContext:

def __init__(
self, user_repository: UserRepository, token_repository: ProjectTokensRepository
self,
user_repository: UserRepository,
token_repository: ProjectTokensRepository,
project_repository: ProjectRepository,
):
self._user_repository = user_repository
self._token_repository = token_repository
self._project_repository = project_repository

def isOperationAuthorizedOnOrg(self, organization_id, user):
def isOperationAuthorizedOnOrg(self, organization_id, user: User):
return self._user_repository.is_user_in_organization(
organization_id=organization_id, user=user
)

def isOperationAuthorizedOnProject(self, project_id, user):
def isOperationAuthorizedOnProject(self, project_id: UUID, user: User):
return self._user_repository.is_user_authorized_on_project(project_id, user.id)

def can_read_organization(self, organization_id, user):
def can_read_project(self, project_id: UUID, user: Optional[User]):
if self._project_repository.is_project_public(project_id):
return True
if user is None:
raise Exception("Not authenticated")
Copy link
Contributor

@inimaz inimaz Jan 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to know the full flow, why an exception instead of return False ? It will be caught later somewhere?

return self._user_repository.is_user_read_authorized_on_project(
project_id, user.id
)

def can_read_organization(self, organization_id: UUID, user: User):
return self._user_repository.is_user_in_organization(
organization_id=organization_id, user=user
)

def can_write_organization(self, organization_id, user):
def can_write_organization(self, organization_id: UUID, user: User):
return self._user_repository.is_admin_in_organization(
organization_id=organization_id, user=user
)

def can_create_run(self, experiment_id, user):
def can_create_run(self, experiment_id: UUID, user: User):
return self._user_repository.is_user_authorized_on_experiment(
experiment_id, user.id
)
18 changes: 8 additions & 10 deletions carbonserver/carbonserver/api/services/experiments_service.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import List
from typing import List, Optional

from carbonserver.api.errors import NotAllowedError, NotAllowedErrorEnum, UserException
from carbonserver.api.infra.repositories.repository_experiments import (
Expand Down Expand Up @@ -28,26 +28,24 @@ def add_experiment(self, experiment: ExperimentCreate, user: User) -> Experiment
else:
return self._repository.add_experiment(experiment)

def get_one_experiment(self, experiment_id, user: User) -> Experiment:
def get_one_experiment(self, experiment_id, user: Optional[User]) -> Experiment:
experiment = self._repository.get_one_experiment(experiment_id)
if not self._auth_context.isOperationAuthorizedOnProject(
experiment.project_id, user
):
if not self._auth_context.can_read_project(experiment.project_id, user):
raise UserException(
NotAllowedError(
code=NotAllowedErrorEnum.NOT_IN_ORGANISATION,
message="Operation not authorized on organization",
code=NotAllowedErrorEnum.OPERATION_NOT_ALLOWED,
message="Operation not authorized",
)
)
else:
return experiment

def get_experiments_from_project(self, project_id, user: User) -> List[Experiment]:
if not self._auth_context.isOperationAuthorizedOnProject(project_id, user):
if not self._auth_context.can_read_project(project_id, user):
raise UserException(
NotAllowedError(
code=NotAllowedErrorEnum.NOT_IN_ORGANISATION,
message="Operation not authorized on organization",
code=NotAllowedErrorEnum.OPERATION_NOT_ALLOWED,
message="Operation not authorized",
)
)
else:
Expand Down
5 changes: 3 additions & 2 deletions carbonserver/carbonserver/api/services/project_service.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from typing import Optional

from carbonserver.api.errors import NotAllowedError, NotAllowedErrorEnum, UserException
from carbonserver.api.infra.repositories.repository_projects import (
Expand Down Expand Up @@ -52,8 +53,8 @@ def patch_project(self, project_id, project: ProjectPatch, user: User):
else:
return self._repository.patch_project(project_id, project)

def get_one_project(self, project_id: str, user: User):
if not self._auth_context.isOperationAuthorizedOnProject(project_id, user):
def get_one_project(self, project_id: str, user: Optional[User]):
if not self._auth_context.can_read_project(project_id, user):
raise UserException(
NotAllowedError(
code=NotAllowedErrorEnum.NOT_IN_ORGANISATION,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Add public tag to project

Revision ID: f3a10c95079f
Revises: 9d5ff5377b63
Create Date: 2025-01-14 22:01:12.694786

"""

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "f3a10c95079f"
down_revision = "54d9cae546ad"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"projects", sa.Column("public", sa.Boolean(), server_default=sa.sql.False_())
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("projects", "public")
# ### end Alembic commands ###
1 change: 1 addition & 0 deletions carbonserver/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ class ServerContainer(containers.DeclarativeContainer):
AuthContext,
user_repository=user_repository,
token_repository=project_token_repository,
project_repository=project_repository,
)

emission_service = providers.Factory(
Expand Down
4 changes: 4 additions & 0 deletions carbonserver/tests/api/mocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,7 @@ def can_read_organization(*args, **kwargs):
@staticmethod
def can_write_organization(*args, **kwargs):
return True

@staticmethod
def can_read_project(*args, **kwargs):
return True
Loading
Loading