From fb7bfb1dc47323a84d282b4dbe9f4822ed1e5146 Mon Sep 17 00:00:00 2001 From: Niraj Adhikari Date: Mon, 22 Jan 2024 09:29:23 +0545 Subject: [PATCH 01/23] dependencies to cehck if user_exists --- src/backend/app/users/user_deps.py | 39 ++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/backend/app/users/user_deps.py diff --git a/src/backend/app/users/user_deps.py b/src/backend/app/users/user_deps.py new file mode 100644 index 0000000000..ed41a4f066 --- /dev/null +++ b/src/backend/app/users/user_deps.py @@ -0,0 +1,39 @@ +from fastapi import Depends +from sqlalchemy.orm import Session +from app.db.database import get_db +from app.db.db_models import DbUser +from loguru import logger as log +from app.users.user_crud import get_user, get_user_by_username +from app.models.enums import HTTPStatus +from fastapi.exceptions import HTTPException +from typing import Union + + +async def user_exists( + user_id: Union[str, int], + db: Session = Depends(get_db), +) -> DbUser: + """ + Check if user exists, else error. + """ + + try: + user_id = int(user_id) + except ValueError: + pass + + if isinstance(user_id, int): + log.debug(f"Getting user by id: {user_id}") + db_user = await get_user(db, user_id) + + if isinstance(user_id, str): + log.debug(f"Getting user by username: {user_id}") + db_user = await get_user_by_username(db, user_id) + + if not db_user: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail=f"User {user_id} does not exist", + ) + + return db_user From ad36a702fb83a80169e470134b38f9a164123899 Mon Sep 17 00:00:00 2001 From: Niraj Adhikari Date: Mon, 22 Jan 2024 09:30:17 +0545 Subject: [PATCH 02/23] endpoint to add organisation admin --- .../app/organisations/organisation_crud.py | 17 +++++++++++++++++ .../app/organisations/organisation_routes.py | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/backend/app/organisations/organisation_crud.py b/src/backend/app/organisations/organisation_crud.py index a058a70514..6da384d236 100644 --- a/src/backend/app/organisations/organisation_crud.py +++ b/src/backend/app/organisations/organisation_crud.py @@ -32,6 +32,8 @@ ) from app.organisations.organisation_schemas import OrganisationEdit, OrganisationIn from app.s3 import add_obj_to_bucket +from app.db import db_models +from app.users import user_crud def get_organisations( @@ -186,3 +188,18 @@ async def delete_organisation( db.commit() return Response(status_code=HTTPStatus.NO_CONTENT) + + +async def add_organisation_admin(db: Session, + user_id:int, + organization: db_models.DbOrganisation + ): + + # get the user model instance + user_model_instance = await user_crud.get_user(db, user_id) + + # add data to the managers field in organisation model + organization.managers.append(user_model_instance) + db.commit() + + return Response(status_code=HTTPStatus.OK) diff --git a/src/backend/app/organisations/organisation_routes.py b/src/backend/app/organisations/organisation_routes.py index b81dc8d763..f09dc31fab 100644 --- a/src/backend/app/organisations/organisation_routes.py +++ b/src/backend/app/organisations/organisation_routes.py @@ -29,6 +29,10 @@ from app.db.db_models import DbOrganisation from app.organisations import organisation_crud, organisation_schemas from app.organisations.organisation_deps import org_exists +from app.auth.osm import AuthUser, login_required +from app.users.user_deps import user_exists +from app.auth.roles import org_admin + router = APIRouter( prefix="/organisation", @@ -85,3 +89,16 @@ async def delete_organisations( ): """Delete an organisation.""" return await organisation_crud.delete_organisation(db, organisation) + + +@router.post("/add_organisation_admin/") +async def add_new_organisation_admin( + db: Session = Depends(database.get_db), + current_user: AuthUser = Depends(login_required), + user: AuthUser = Depends(user_exists), + organization: DbOrganisation = Depends(org_exists), +): + # check if the current_user is the organisation admin + org_admin(db, organization.id, current_user) + + return await organization_crud.add_organisation_admin(db, user, organization) From 984198d3e2a56aa2ab4834d391f39e42d01a869d Mon Sep 17 00:00:00 2001 From: Niraj Adhikari Date: Mon, 22 Jan 2024 10:28:45 +0545 Subject: [PATCH 03/23] added approved field in organisation model --- src/backend/app/db/db_models.py | 1 + .../migrations/004-add-approved-in-organisation.sql | 9 +++++++++ .../revert/004-remove-approved-in-organisation.sql | 9 +++++++++ 3 files changed, 19 insertions(+) create mode 100644 src/backend/migrations/004-add-approved-in-organisation.sql create mode 100644 src/backend/migrations/revert/004-remove-approved-in-organisation.sql diff --git a/src/backend/app/db/db_models.py b/src/backend/app/db/db_models.py index cf14d49d2b..3be832fa49 100644 --- a/src/backend/app/db/db_models.py +++ b/src/backend/app/db/db_models.py @@ -147,6 +147,7 @@ class DbOrganisation(Base): description = Column(String) url = Column(String) type = Column(Enum(OrganisationType), default=OrganisationType.FREE, nullable=False) + approved = Column(Boolean, default=False) # subscription_tier = Column(Integer) managers = relationship( diff --git a/src/backend/migrations/004-add-approved-in-organisation.sql b/src/backend/migrations/004-add-approved-in-organisation.sql new file mode 100644 index 0000000000..1e3f892c27 --- /dev/null +++ b/src/backend/migrations/004-add-approved-in-organisation.sql @@ -0,0 +1,9 @@ +-- Start a transaction +BEGIN; + +-- Add the 'approved' column with default value false to the 'organisation' table +ALTER TABLE organisation +ADD COLUMN approved BOOLEAN DEFAULT false; + +-- Commit the transaction +COMMIT; diff --git a/src/backend/migrations/revert/004-remove-approved-in-organisation.sql b/src/backend/migrations/revert/004-remove-approved-in-organisation.sql new file mode 100644 index 0000000000..54042d362a --- /dev/null +++ b/src/backend/migrations/revert/004-remove-approved-in-organisation.sql @@ -0,0 +1,9 @@ +-- Start a transaction +BEGIN; + +-- Revert changes to the 'organisation' table +ALTER TABLE IF EXISTS organisation + DROP COLUMN IF EXISTS approved; + +-- Commit the transaction +COMMIT; From ffaafd551231b9e5205f09e8249c055725137df7 Mon Sep 17 00:00:00 2001 From: Niraj Adhikari Date: Mon, 22 Jan 2024 10:31:02 +0545 Subject: [PATCH 04/23] fix: table name in migration file for organisations --- src/backend/migrations/004-add-approved-in-organisation.sql | 4 ++-- .../migrations/revert/004-remove-approved-in-organisation.sql | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/backend/migrations/004-add-approved-in-organisation.sql b/src/backend/migrations/004-add-approved-in-organisation.sql index 1e3f892c27..7756d9e533 100644 --- a/src/backend/migrations/004-add-approved-in-organisation.sql +++ b/src/backend/migrations/004-add-approved-in-organisation.sql @@ -2,8 +2,8 @@ BEGIN; -- Add the 'approved' column with default value false to the 'organisation' table -ALTER TABLE organisation -ADD COLUMN approved BOOLEAN DEFAULT false; +ALTER TABLE organisations + ADD COLUMN approved BOOLEAN DEFAULT false; -- Commit the transaction COMMIT; diff --git a/src/backend/migrations/revert/004-remove-approved-in-organisation.sql b/src/backend/migrations/revert/004-remove-approved-in-organisation.sql index 54042d362a..ca85b57a3b 100644 --- a/src/backend/migrations/revert/004-remove-approved-in-organisation.sql +++ b/src/backend/migrations/revert/004-remove-approved-in-organisation.sql @@ -2,7 +2,7 @@ BEGIN; -- Revert changes to the 'organisation' table -ALTER TABLE IF EXISTS organisation +ALTER TABLE IF EXISTS organisations DROP COLUMN IF EXISTS approved; -- Commit the transaction From 8b06c5fbd8120a059974df7d33d66fcd6f07d785 Mon Sep 17 00:00:00 2001 From: Niraj Adhikari Date: Mon, 22 Jan 2024 13:06:40 +0545 Subject: [PATCH 05/23] feat: organisations list api updated according to role --- src/backend/app/organisations/organisation_crud.py | 9 ++++++++- src/backend/app/organisations/organisation_routes.py | 6 ++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/backend/app/organisations/organisation_crud.py b/src/backend/app/organisations/organisation_crud.py index 6da384d236..22d89815d2 100644 --- a/src/backend/app/organisations/organisation_crud.py +++ b/src/backend/app/organisations/organisation_crud.py @@ -34,13 +34,20 @@ from app.s3 import add_obj_to_bucket from app.db import db_models from app.users import user_crud +from app.auth.osm import AuthUser +from app.models.enums import UserRole def get_organisations( db: Session, + current_user: AuthUser, + approved: bool ): """Get all orgs.""" - return db.query(db_models.DbOrganisation).all() + if db.query(db_models.DbUser).filter_by(id=current_user['id'], role=UserRole.ADMIN).first(): + return db.query(db_models.DbOrganisation).filter(db_models.DbOrganisation.approved==approved).all() + + return db.query(db_models.DbOrganisation).filter(db_models.DbOrganisation.approved==True).all() async def upload_logo_to_s3( diff --git a/src/backend/app/organisations/organisation_routes.py b/src/backend/app/organisations/organisation_routes.py index f09dc31fab..3a287cb107 100644 --- a/src/backend/app/organisations/organisation_routes.py +++ b/src/backend/app/organisations/organisation_routes.py @@ -31,7 +31,7 @@ from app.organisations.organisation_deps import org_exists from app.auth.osm import AuthUser, login_required from app.users.user_deps import user_exists -from app.auth.roles import org_admin +from app.auth.roles import org_admin, super_admin router = APIRouter( @@ -45,9 +45,11 @@ @router.get("/", response_model=list[organisation_schemas.OrganisationOut]) def get_organisations( db: Session = Depends(database.get_db), + current_user: AuthUser = Depends(login_required), + approved: bool = True ) -> list[organisation_schemas.OrganisationOut]: """Get a list of all organisations.""" - return organisation_crud.get_organisations(db) + return organisation_crud.get_organisations(db, current_user, approved) @router.get("/{org_id}", response_model=organisation_schemas.OrganisationOut) From 8a689bb355856a105a52940c662e847b81df597a Mon Sep 17 00:00:00 2001 From: Niraj Adhikari Date: Mon, 22 Jan 2024 13:07:19 +0545 Subject: [PATCH 06/23] feat: endpoint to approve organisations --- .../app/organisations/organisation_crud.py | 8 ++++++++ .../app/organisations/organisation_routes.py | 19 +++++++++++++++---- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/backend/app/organisations/organisation_crud.py b/src/backend/app/organisations/organisation_crud.py index 22d89815d2..dbc6dbc10f 100644 --- a/src/backend/app/organisations/organisation_crud.py +++ b/src/backend/app/organisations/organisation_crud.py @@ -210,3 +210,11 @@ async def add_organisation_admin(db: Session, db.commit() return Response(status_code=HTTPStatus.OK) + + +async def approve_organisation(db, organisation_id): + + db_org = db.query(db_models.DbOrganisation).filter(db_models.DbOrganisation.id == organisation_id).first() + db_org.approved = True + db.commit() + return Response(status_code=HTTPStatus.OK) diff --git a/src/backend/app/organisations/organisation_routes.py b/src/backend/app/organisations/organisation_routes.py index 3a287cb107..19a49f2348 100644 --- a/src/backend/app/organisations/organisation_routes.py +++ b/src/backend/app/organisations/organisation_routes.py @@ -93,14 +93,25 @@ async def delete_organisations( return await organisation_crud.delete_organisation(db, organisation) -@router.post("/add_organisation_admin/") +@router.post("/approve/") +async def approve_organisation( + organisation_id: int, + db: Session = Depends(database.get_db), + current_user: AuthUser = Depends(login_required), +): + # check if the current_user is the super admin + super_admin(db, current_user) + return await organisation_crud.approve_organisation(db, organisation_id) + + +@router.post("/add_admin/") async def add_new_organisation_admin( db: Session = Depends(database.get_db), current_user: AuthUser = Depends(login_required), user: AuthUser = Depends(user_exists), - organization: DbOrganisation = Depends(org_exists), + organisation: DbOrganisation = Depends(org_exists), ): # check if the current_user is the organisation admin - org_admin(db, organization.id, current_user) + org_admin(db, organisation.id, current_user) - return await organization_crud.add_organisation_admin(db, user, organization) + return await organisation_crud.add_organisation_admin(db, user, organisation) From f5329f5edda4b87d6f7dc361448c203e46c7344e Mon Sep 17 00:00:00 2001 From: Niraj Adhikari Date: Mon, 22 Jan 2024 13:07:59 +0545 Subject: [PATCH 07/23] update: get organisation endpoint for filtering approval --- src/backend/app/organisations/organisation_deps.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend/app/organisations/organisation_deps.py b/src/backend/app/organisations/organisation_deps.py index b4aad5f41b..4edda973c4 100644 --- a/src/backend/app/organisations/organisation_deps.py +++ b/src/backend/app/organisations/organisation_deps.py @@ -43,7 +43,7 @@ async def get_organisation_by_name(db: Session, org_name: str) -> DbOrganisation """ return ( db.query(DbOrganisation) - .filter(func.lower(DbOrganisation.name).like(func.lower(f"%{org_name}%"))) + .filter(DbOrganisation.approved==True, func.lower(DbOrganisation.name).like(func.lower(f"%{org_name}%"))) .first() ) @@ -58,7 +58,7 @@ async def get_organisation_by_id(db: Session, org_id: int) -> DbOrganisation: Returns: DbOrganisation: organisation with the given id """ - return db.query(DbOrganisation).filter(DbOrganisation.id == org_id).first() + return db.query(DbOrganisation).filter(DbOrganisation.id == org_id, DbOrganisation.approved==True).first() async def org_exists( From 00221edbcca9e94c859bad927e54557ff39e3577 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 Jan 2024 04:04:52 +0000 Subject: [PATCH 08/23] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/backend/app/organisations/organisation_crud.py | 9 +++------ .../app/organisations/organisation_routes.py | 7 +++---- src/backend/app/users/user_deps.py | 14 +++++++------- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/backend/app/organisations/organisation_crud.py b/src/backend/app/organisations/organisation_crud.py index dbc6dbc10f..8235d8924c 100644 --- a/src/backend/app/organisations/organisation_crud.py +++ b/src/backend/app/organisations/organisation_crud.py @@ -32,7 +32,6 @@ ) from app.organisations.organisation_schemas import OrganisationEdit, OrganisationIn from app.s3 import add_obj_to_bucket -from app.db import db_models from app.users import user_crud from app.auth.osm import AuthUser from app.models.enums import UserRole @@ -197,11 +196,9 @@ async def delete_organisation( return Response(status_code=HTTPStatus.NO_CONTENT) -async def add_organisation_admin(db: Session, - user_id:int, - organization: db_models.DbOrganisation - ): - +async def add_organisation_admin( + db: Session, user_id: int, organization: db_models.DbOrganisation +): # get the user model instance user_model_instance = await user_crud.get_user(db, user_id) diff --git a/src/backend/app/organisations/organisation_routes.py b/src/backend/app/organisations/organisation_routes.py index 19a49f2348..6368b613ac 100644 --- a/src/backend/app/organisations/organisation_routes.py +++ b/src/backend/app/organisations/organisation_routes.py @@ -25,14 +25,13 @@ ) from sqlalchemy.orm import Session +from app.auth.osm import AuthUser, login_required +from app.auth.roles import org_admin, super_admin from app.db import database from app.db.db_models import DbOrganisation from app.organisations import organisation_crud, organisation_schemas from app.organisations.organisation_deps import org_exists -from app.auth.osm import AuthUser, login_required from app.users.user_deps import user_exists -from app.auth.roles import org_admin, super_admin - router = APIRouter( prefix="/organisation", @@ -114,4 +113,4 @@ async def add_new_organisation_admin( # check if the current_user is the organisation admin org_admin(db, organisation.id, current_user) - return await organisation_crud.add_organisation_admin(db, user, organisation) + return await organisation_crud.add_organisation_admin(db, user, organization) diff --git a/src/backend/app/users/user_deps.py b/src/backend/app/users/user_deps.py index ed41a4f066..ee0cd39d0d 100644 --- a/src/backend/app/users/user_deps.py +++ b/src/backend/app/users/user_deps.py @@ -1,22 +1,22 @@ +from typing import Union + from fastapi import Depends +from fastapi.exceptions import HTTPException +from loguru import logger as log from sqlalchemy.orm import Session + from app.db.database import get_db from app.db.db_models import DbUser -from loguru import logger as log -from app.users.user_crud import get_user, get_user_by_username from app.models.enums import HTTPStatus -from fastapi.exceptions import HTTPException -from typing import Union +from app.users.user_crud import get_user, get_user_by_username async def user_exists( user_id: Union[str, int], db: Session = Depends(get_db), ) -> DbUser: + """Check if user exists, else error. """ - Check if user exists, else error. - """ - try: user_id = int(user_id) except ValueError: From cdc71408dfce884ef6eddf0740642885e36860a8 Mon Sep 17 00:00:00 2001 From: Niraj Adhikari Date: Mon, 22 Jan 2024 14:26:42 +0545 Subject: [PATCH 09/23] added docstrings --- .../app/organisations/organisation_crud.py | 53 ++++++++++++++----- .../app/organisations/organisation_deps.py | 11 +++- .../app/organisations/organisation_routes.py | 10 +++- src/backend/app/users/user_deps.py | 3 +- 4 files changed, 60 insertions(+), 17 deletions(-) diff --git a/src/backend/app/organisations/organisation_crud.py b/src/backend/app/organisations/organisation_crud.py index 8235d8924c..1ee661a9c5 100644 --- a/src/backend/app/organisations/organisation_crud.py +++ b/src/backend/app/organisations/organisation_crud.py @@ -24,29 +24,36 @@ from sqlalchemy import update from sqlalchemy.orm import Session +from app.auth.osm import AuthUser from app.config import settings from app.db import db_models -from app.models.enums import HTTPStatus +from app.models.enums import HTTPStatus, UserRole from app.organisations.organisation_deps import ( get_organisation_by_name, ) from app.organisations.organisation_schemas import OrganisationEdit, OrganisationIn from app.s3 import add_obj_to_bucket from app.users import user_crud -from app.auth.osm import AuthUser -from app.models.enums import UserRole -def get_organisations( - db: Session, - current_user: AuthUser, - approved: bool -): +def get_organisations(db: Session, current_user: AuthUser, approved: bool): """Get all orgs.""" - if db.query(db_models.DbUser).filter_by(id=current_user['id'], role=UserRole.ADMIN).first(): - return db.query(db_models.DbOrganisation).filter(db_models.DbOrganisation.approved==approved).all() + if ( + db.query(db_models.DbUser) + .filter_by(id=current_user["id"], role=UserRole.ADMIN) + .first() + ): + return ( + db.query(db_models.DbOrganisation) + .filter(db_models.DbOrganisation.approved == approved) + .all() + ) - return db.query(db_models.DbOrganisation).filter(db_models.DbOrganisation.approved==True).all() + return ( + db.query(db_models.DbOrganisation) + .filter(db_models.DbOrganisation.approved == True) + .all() + ) async def upload_logo_to_s3( @@ -199,6 +206,16 @@ async def delete_organisation( async def add_organisation_admin( db: Session, user_id: int, organization: db_models.DbOrganisation ): + """Adds a user as an admin to the specified organisation. + + Args: + db (Session): The database session. + user_id (int): The ID of the user to be added as an admin. + organization (DbOrganisation): The organisation model instance. + + Returns: + Response: The HTTP response with status code 200. + """ # get the user model instance user_model_instance = await user_crud.get_user(db, user_id) @@ -210,8 +227,20 @@ async def add_organisation_admin( async def approve_organisation(db, organisation_id): + """Approves an oranisation request made by the user . + + Args: + db: The database session. + organisation_id: The ID of the organisation to be approved. - db_org = db.query(db_models.DbOrganisation).filter(db_models.DbOrganisation.id == organisation_id).first() + Returns: + Response: An HTTP response with the status code 200. + """ + db_org = ( + db.query(db_models.DbOrganisation) + .filter(db_models.DbOrganisation.id == organisation_id) + .first() + ) db_org.approved = True db.commit() return Response(status_code=HTTPStatus.OK) diff --git a/src/backend/app/organisations/organisation_deps.py b/src/backend/app/organisations/organisation_deps.py index 4edda973c4..d21c0519a0 100644 --- a/src/backend/app/organisations/organisation_deps.py +++ b/src/backend/app/organisations/organisation_deps.py @@ -43,7 +43,10 @@ async def get_organisation_by_name(db: Session, org_name: str) -> DbOrganisation """ return ( db.query(DbOrganisation) - .filter(DbOrganisation.approved==True, func.lower(DbOrganisation.name).like(func.lower(f"%{org_name}%"))) + .filter( + DbOrganisation.approved == True, + func.lower(DbOrganisation.name).like(func.lower(f"%{org_name}%")), + ) .first() ) @@ -58,7 +61,11 @@ async def get_organisation_by_id(db: Session, org_id: int) -> DbOrganisation: Returns: DbOrganisation: organisation with the given id """ - return db.query(DbOrganisation).filter(DbOrganisation.id == org_id, DbOrganisation.approved==True).first() + return ( + db.query(DbOrganisation) + .filter(DbOrganisation.id == org_id, DbOrganisation.approved == True) + .first() + ) async def org_exists( diff --git a/src/backend/app/organisations/organisation_routes.py b/src/backend/app/organisations/organisation_routes.py index 6368b613ac..220a6cb3e8 100644 --- a/src/backend/app/organisations/organisation_routes.py +++ b/src/backend/app/organisations/organisation_routes.py @@ -45,7 +45,7 @@ def get_organisations( db: Session = Depends(database.get_db), current_user: AuthUser = Depends(login_required), - approved: bool = True + approved: bool = True, ) -> list[organisation_schemas.OrganisationOut]: """Get a list of all organisations.""" return organisation_crud.get_organisations(db, current_user, approved) @@ -98,6 +98,10 @@ async def approve_organisation( db: Session = Depends(database.get_db), current_user: AuthUser = Depends(login_required), ): + """Approve the organisation request made by the user. + + The logged in user must be super admin to perform this action . + """ # check if the current_user is the super admin super_admin(db, current_user) return await organisation_crud.approve_organisation(db, organisation_id) @@ -110,6 +114,10 @@ async def add_new_organisation_admin( user: AuthUser = Depends(user_exists), organisation: DbOrganisation = Depends(org_exists), ): + """Add a new organisation admin. + + The logged in user must be either the owner of the organisation or a super admin. + """ # check if the current_user is the organisation admin org_admin(db, organisation.id, current_user) diff --git a/src/backend/app/users/user_deps.py b/src/backend/app/users/user_deps.py index ee0cd39d0d..cc99a39ccd 100644 --- a/src/backend/app/users/user_deps.py +++ b/src/backend/app/users/user_deps.py @@ -15,8 +15,7 @@ async def user_exists( user_id: Union[str, int], db: Session = Depends(get_db), ) -> DbUser: - """Check if user exists, else error. - """ + """Check if user exists, else error.""" try: user_id = int(user_id) except ValueError: From e38f3d1274a41296954ec06d244ed7ae13a36eb9 Mon Sep 17 00:00:00 2001 From: Niraj Adhikari Date: Mon, 22 Jan 2024 14:40:16 +0545 Subject: [PATCH 10/23] fix: pre-commit linting errors --- src/backend/app/organisations/organisation_crud.py | 12 ++---------- src/backend/app/organisations/organisation_deps.py | 12 +++--------- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/src/backend/app/organisations/organisation_crud.py b/src/backend/app/organisations/organisation_crud.py index 1ee661a9c5..c0cebcb885 100644 --- a/src/backend/app/organisations/organisation_crud.py +++ b/src/backend/app/organisations/organisation_crud.py @@ -43,17 +43,9 @@ def get_organisations(db: Session, current_user: AuthUser, approved: bool): .filter_by(id=current_user["id"], role=UserRole.ADMIN) .first() ): - return ( - db.query(db_models.DbOrganisation) - .filter(db_models.DbOrganisation.approved == approved) - .all() - ) + return db.query(db_models.DbOrganisation).filter_by(approved=approved).all() - return ( - db.query(db_models.DbOrganisation) - .filter(db_models.DbOrganisation.approved == True) - .all() - ) + return db.query(db_models.DbOrganisation).filter_by(approved=True).all() async def upload_logo_to_s3( diff --git a/src/backend/app/organisations/organisation_deps.py b/src/backend/app/organisations/organisation_deps.py index d21c0519a0..0b32e7ad88 100644 --- a/src/backend/app/organisations/organisation_deps.py +++ b/src/backend/app/organisations/organisation_deps.py @@ -43,10 +43,8 @@ async def get_organisation_by_name(db: Session, org_name: str) -> DbOrganisation """ return ( db.query(DbOrganisation) - .filter( - DbOrganisation.approved == True, - func.lower(DbOrganisation.name).like(func.lower(f"%{org_name}%")), - ) + .filter_by(approved=True) + .filter(func.lower(DbOrganisation.name).like(func.lower(f"%{org_name}%"))) .first() ) @@ -61,11 +59,7 @@ async def get_organisation_by_id(db: Session, org_id: int) -> DbOrganisation: Returns: DbOrganisation: organisation with the given id """ - return ( - db.query(DbOrganisation) - .filter(DbOrganisation.id == org_id, DbOrganisation.approved == True) - .first() - ) + return db.query(DbOrganisation).filter_by(id=org_id, approved=True).first() async def org_exists( From 89700adc7f5a6c43cfadd02266aff7e85754748e Mon Sep 17 00:00:00 2001 From: Niraj Adhikari Date: Mon, 22 Jan 2024 14:47:16 +0545 Subject: [PATCH 11/23] added docstring in user_deps file --- src/backend/app/users/user_deps.py | 36 ++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/src/backend/app/users/user_deps.py b/src/backend/app/users/user_deps.py index cc99a39ccd..c689e91b9b 100644 --- a/src/backend/app/users/user_deps.py +++ b/src/backend/app/users/user_deps.py @@ -1,3 +1,24 @@ +# Copyright (c) 2022, 2023 Humanitarian OpenStreetMap Team +# +# This file is part of FMTM. +# +# FMTM is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# FMTM is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with FMTM. If not, see . +# + +"""User dependencies for use in Depends.""" + + from typing import Union from fastapi import Depends @@ -15,14 +36,25 @@ async def user_exists( user_id: Union[str, int], db: Session = Depends(get_db), ) -> DbUser: - """Check if user exists, else error.""" + """Check if a user exists, else Error. + + Args: + user_id (Union[str, int]): The user ID (integer) or username (string) to check. + db (Session, optional): The SQLAlchemy database session. + + Returns: + DbUser: The user if found. + + Raises: + HTTPException: Raised with a 404 status code if the user is not found. + """ try: user_id = int(user_id) except ValueError: pass if isinstance(user_id, int): - log.debug(f"Getting user by id: {user_id}") + log.debug(f"Getting user by ID: {user_id}") db_user = await get_user(db, user_id) if isinstance(user_id, str): From 2ec8980562cdbfa35d219e397ded18a9301c65c8 Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Sun, 21 Jan 2024 16:40:40 +0000 Subject: [PATCH 12/23] build: add default odk credentials to organisations --- src/backend/migrations/002-add-profile-img.sql | 2 +- src/backend/migrations/003-project-roles.sql | 2 +- .../migrations/004-organisation-odk-creds.sql | 12 ++++++++++++ src/backend/migrations/init/fmtm_base_schema.sql | 5 ++++- .../migrations/revert/002-add-profile-img.sql | 2 +- .../migrations/revert/004-organisation-odk-creds.sql | 11 +++++++++++ 6 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 src/backend/migrations/004-organisation-odk-creds.sql create mode 100644 src/backend/migrations/revert/004-organisation-odk-creds.sql diff --git a/src/backend/migrations/002-add-profile-img.sql b/src/backend/migrations/002-add-profile-img.sql index ae7f620918..361fc76e1e 100644 --- a/src/backend/migrations/002-add-profile-img.sql +++ b/src/backend/migrations/002-add-profile-img.sql @@ -7,4 +7,4 @@ BEGIN; ALTER TABLE IF EXISTS public.users ADD COLUMN IF NOT EXISTS profile_img VARCHAR; -- Commit the transaction -COMMIT; \ No newline at end of file +COMMIT; diff --git a/src/backend/migrations/003-project-roles.sql b/src/backend/migrations/003-project-roles.sql index c4f75e5c5d..6c4bf50c69 100644 --- a/src/backend/migrations/003-project-roles.sql +++ b/src/backend/migrations/003-project-roles.sql @@ -17,4 +17,4 @@ ALTER TABLE public.user_roles ALTER COLUMN "role" TYPE public.projectrole USING ALTER TYPE public.projectrole OWNER TO fmtm; -- Commit the transaction -COMMIT; \ No newline at end of file +COMMIT; diff --git a/src/backend/migrations/004-organisation-odk-creds.sql b/src/backend/migrations/004-organisation-odk-creds.sql new file mode 100644 index 0000000000..44a5eecb15 --- /dev/null +++ b/src/backend/migrations/004-organisation-odk-creds.sql @@ -0,0 +1,12 @@ +-- ## Migration to: +-- * Add odk central credentials (str) to organisations table. + +-- Start a transaction +BEGIN; + +ALTER TABLE IF EXISTS public.organisations + ADD COLUMN IF NOT EXISTS odk_central_url VARCHAR, + ADD COLUMN IF NOT EXISTS odk_central_user VARCHAR, + ADD COLUMN IF NOT EXISTS odk_central_password VARCHAR; +-- Commit the transaction +COMMIT; diff --git a/src/backend/migrations/init/fmtm_base_schema.sql b/src/backend/migrations/init/fmtm_base_schema.sql index 857ef6e430..3542951c56 100644 --- a/src/backend/migrations/init/fmtm_base_schema.sql +++ b/src/backend/migrations/init/fmtm_base_schema.sql @@ -276,7 +276,10 @@ CREATE TABLE public.organisations ( logo character varying, description character varying, url character varying, - type public.organisationtype NOT NULL + type public.organisationtype NOT NULL, + odk_central_url character varying, + odk_central_user character varying, + odk_central_password character varying ); ALTER TABLE public.organisations OWNER TO fmtm; CREATE SEQUENCE public.organisations_id_seq diff --git a/src/backend/migrations/revert/002-add-profile-img.sql b/src/backend/migrations/revert/002-add-profile-img.sql index 49b3e65f1d..5b02224dda 100644 --- a/src/backend/migrations/revert/002-add-profile-img.sql +++ b/src/backend/migrations/revert/002-add-profile-img.sql @@ -6,4 +6,4 @@ ALTER TABLE IF EXISTS public.users DROP COLUMN IF EXISTS profile_img; -- Commit the transaction -COMMIT; \ No newline at end of file +COMMIT; diff --git a/src/backend/migrations/revert/004-organisation-odk-creds.sql b/src/backend/migrations/revert/004-organisation-odk-creds.sql new file mode 100644 index 0000000000..9d0494064d --- /dev/null +++ b/src/backend/migrations/revert/004-organisation-odk-creds.sql @@ -0,0 +1,11 @@ +-- Start a transaction +BEGIN; + +-- Remove the odk central credentials columns from the public.organisations table +ALTER TABLE IF EXISTS public.organisations + DROP COLUMN IF EXISTS odk_central_url CASCADE, + DROP COLUMN IF EXISTS odk_central_user CASCADE, + DROP COLUMN IF EXISTS odk_central_password CASCADE; + +-- Commit the transaction +COMMIT; From 12acd85edc20d376efec75ca080958b54694c819 Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Mon, 22 Jan 2024 10:42:08 +0000 Subject: [PATCH 13/23] build: merge migrations to organisations table --- .../migrations/004-add-approved-in-organisation.sql | 9 --------- src/backend/migrations/004-organisation-odk-creds.sql | 2 ++ .../migrations/revert/004-organisation-odk-creds.sql | 4 +++- .../revert/004-remove-approved-in-organisation.sql | 9 --------- 4 files changed, 5 insertions(+), 19 deletions(-) delete mode 100644 src/backend/migrations/004-add-approved-in-organisation.sql delete mode 100644 src/backend/migrations/revert/004-remove-approved-in-organisation.sql diff --git a/src/backend/migrations/004-add-approved-in-organisation.sql b/src/backend/migrations/004-add-approved-in-organisation.sql deleted file mode 100644 index 7756d9e533..0000000000 --- a/src/backend/migrations/004-add-approved-in-organisation.sql +++ /dev/null @@ -1,9 +0,0 @@ --- Start a transaction -BEGIN; - --- Add the 'approved' column with default value false to the 'organisation' table -ALTER TABLE organisations - ADD COLUMN approved BOOLEAN DEFAULT false; - --- Commit the transaction -COMMIT; diff --git a/src/backend/migrations/004-organisation-odk-creds.sql b/src/backend/migrations/004-organisation-odk-creds.sql index 44a5eecb15..d65b61a8a1 100644 --- a/src/backend/migrations/004-organisation-odk-creds.sql +++ b/src/backend/migrations/004-organisation-odk-creds.sql @@ -1,10 +1,12 @@ -- ## Migration to: -- * Add odk central credentials (str) to organisations table. +-- * Add the approved (bool) field to organisations table. -- Start a transaction BEGIN; ALTER TABLE IF EXISTS public.organisations + ADD COLUMN IF NOT EXISTS approved BOOLEAN DEFAULT false, ADD COLUMN IF NOT EXISTS odk_central_url VARCHAR, ADD COLUMN IF NOT EXISTS odk_central_user VARCHAR, ADD COLUMN IF NOT EXISTS odk_central_password VARCHAR; diff --git a/src/backend/migrations/revert/004-organisation-odk-creds.sql b/src/backend/migrations/revert/004-organisation-odk-creds.sql index 9d0494064d..263595afa4 100644 --- a/src/backend/migrations/revert/004-organisation-odk-creds.sql +++ b/src/backend/migrations/revert/004-organisation-odk-creds.sql @@ -1,8 +1,10 @@ -- Start a transaction BEGIN; --- Remove the odk central credentials columns from the public.organisations table +-- Remove the odk central credentials columns and approved column +--- from the public.organisations table ALTER TABLE IF EXISTS public.organisations + DROP COLUMN IF EXISTS approved, DROP COLUMN IF EXISTS odk_central_url CASCADE, DROP COLUMN IF EXISTS odk_central_user CASCADE, DROP COLUMN IF EXISTS odk_central_password CASCADE; diff --git a/src/backend/migrations/revert/004-remove-approved-in-organisation.sql b/src/backend/migrations/revert/004-remove-approved-in-organisation.sql deleted file mode 100644 index ca85b57a3b..0000000000 --- a/src/backend/migrations/revert/004-remove-approved-in-organisation.sql +++ /dev/null @@ -1,9 +0,0 @@ --- Start a transaction -BEGIN; - --- Revert changes to the 'organisation' table -ALTER TABLE IF EXISTS organisations - DROP COLUMN IF EXISTS approved; - --- Commit the transaction -COMMIT; From 8bb9efbee1ad46004db158edcf84ddfe444d1d16 Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Mon, 22 Jan 2024 17:00:25 +0000 Subject: [PATCH 14/23] refactor: fix linting errors --- src/backend/app/organisations/organisation_crud.py | 6 +++--- src/backend/app/organisations/organisation_routes.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/backend/app/organisations/organisation_crud.py b/src/backend/app/organisations/organisation_crud.py index c0cebcb885..5af2de74ff 100644 --- a/src/backend/app/organisations/organisation_crud.py +++ b/src/backend/app/organisations/organisation_crud.py @@ -196,14 +196,14 @@ async def delete_organisation( async def add_organisation_admin( - db: Session, user_id: int, organization: db_models.DbOrganisation + db: Session, user_id: int, organisation: db_models.DbOrganisation ): """Adds a user as an admin to the specified organisation. Args: db (Session): The database session. user_id (int): The ID of the user to be added as an admin. - organization (DbOrganisation): The organisation model instance. + organisation (DbOrganisation): The organisation model instance. Returns: Response: The HTTP response with status code 200. @@ -212,7 +212,7 @@ async def add_organisation_admin( user_model_instance = await user_crud.get_user(db, user_id) # add data to the managers field in organisation model - organization.managers.append(user_model_instance) + organisation.managers.append(user_model_instance) db.commit() return Response(status_code=HTTPStatus.OK) diff --git a/src/backend/app/organisations/organisation_routes.py b/src/backend/app/organisations/organisation_routes.py index 220a6cb3e8..272091af11 100644 --- a/src/backend/app/organisations/organisation_routes.py +++ b/src/backend/app/organisations/organisation_routes.py @@ -121,4 +121,4 @@ async def add_new_organisation_admin( # check if the current_user is the organisation admin org_admin(db, organisation.id, current_user) - return await organisation_crud.add_organisation_admin(db, user, organization) + return await organisation_crud.add_organisation_admin(db, user, organisation) From 879f173d25941c4b237a9393274902c157b36f72 Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Mon, 22 Jan 2024 17:02:20 +0000 Subject: [PATCH 15/23] refactor: remove subscription_tier field for orgs --- src/backend/app/db/db_models.py | 2 +- src/frontend/src/models/createproject/createProjectModel.ts | 1 - src/frontend/src/models/organization/organisationModel.ts | 3 --- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/backend/app/db/db_models.py b/src/backend/app/db/db_models.py index 3be832fa49..d13e60d6ff 100644 --- a/src/backend/app/db/db_models.py +++ b/src/backend/app/db/db_models.py @@ -148,7 +148,7 @@ class DbOrganisation(Base): url = Column(String) type = Column(Enum(OrganisationType), default=OrganisationType.FREE, nullable=False) approved = Column(Boolean, default=False) - # subscription_tier = Column(Integer) + url = Column(String) managers = relationship( DbUser, diff --git a/src/frontend/src/models/createproject/createProjectModel.ts b/src/frontend/src/models/createproject/createProjectModel.ts index cd89ed1893..1c78c11d86 100755 --- a/src/frontend/src/models/createproject/createProjectModel.ts +++ b/src/frontend/src/models/createproject/createProjectModel.ts @@ -74,7 +74,6 @@ export interface OrganisationListModel { slug: string; description: string; type: number; - subscription_tier: null | string; id: number; logo: string; url: string; diff --git a/src/frontend/src/models/organization/organisationModel.ts b/src/frontend/src/models/organization/organisationModel.ts index 02278511d3..50e913c78e 100644 --- a/src/frontend/src/models/organization/organisationModel.ts +++ b/src/frontend/src/models/organization/organisationModel.ts @@ -14,7 +14,6 @@ export interface OrganisationListModel { slug: string; description: string; type: number; - subscription_tier: null | string; id: number; logo: string; url: string; @@ -25,7 +24,6 @@ export interface GetOrganisationDataModel { slug: string; description: string; type: number; - subscription_tier: null; id: number; logo: string; url: string; @@ -35,7 +33,6 @@ export interface PostOrganisationDataModel { slug: string; description: string; type: number; - subscription_tier: null; id: number; logo: string; url: string; From 9f3351bc5c3f0393dde2c5f52c9fe1d42634f2b1 Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Mon, 22 Jan 2024 17:18:59 +0000 Subject: [PATCH 16/23] build: add public.organisation.approved field to base schema --- src/backend/migrations/init/fmtm_base_schema.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/src/backend/migrations/init/fmtm_base_schema.sql b/src/backend/migrations/init/fmtm_base_schema.sql index 3542951c56..d6c328aa47 100644 --- a/src/backend/migrations/init/fmtm_base_schema.sql +++ b/src/backend/migrations/init/fmtm_base_schema.sql @@ -277,6 +277,7 @@ CREATE TABLE public.organisations ( description character varying, url character varying, type public.organisationtype NOT NULL, + approved BOOLEAN DEFAULT false, odk_central_url character varying, odk_central_user character varying, odk_central_password character varying From 70b505c4ca28b851572a600769ca84b6631c5d41 Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Tue, 23 Jan 2024 10:14:06 +0000 Subject: [PATCH 17/23] refactor: remove extra url field from DbOrganisation --- src/backend/app/db/db_models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/backend/app/db/db_models.py b/src/backend/app/db/db_models.py index d13e60d6ff..26494c594c 100644 --- a/src/backend/app/db/db_models.py +++ b/src/backend/app/db/db_models.py @@ -148,7 +148,6 @@ class DbOrganisation(Base): url = Column(String) type = Column(Enum(OrganisationType), default=OrganisationType.FREE, nullable=False) approved = Column(Boolean, default=False) - url = Column(String) managers = relationship( DbUser, From e56cbacbbcaa1d083b7acb45020213a380401aab Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Tue, 23 Jan 2024 10:18:01 +0000 Subject: [PATCH 18/23] refactor: fix organizationModel dir --> organisation for import --- .../models/{organization => organisation}/organisationModel.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/frontend/src/models/{organization => organisation}/organisationModel.ts (100%) diff --git a/src/frontend/src/models/organization/organisationModel.ts b/src/frontend/src/models/organisation/organisationModel.ts similarity index 100% rename from src/frontend/src/models/organization/organisationModel.ts rename to src/frontend/src/models/organisation/organisationModel.ts From 2395b5675494c0db56c5b7d8dae8615b85b9e206 Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Tue, 23 Jan 2024 12:07:00 +0000 Subject: [PATCH 19/23] fix: remove router get_db global dependency (on routes) --- src/backend/app/central/central_routes.py | 1 - src/backend/app/organisations/organisation_routes.py | 1 - src/backend/app/projects/project_routes.py | 1 - src/backend/app/submissions/submission_routes.py | 1 - src/backend/app/tasks/tasks_routes.py | 1 - src/backend/app/users/user_routes.py | 1 - 6 files changed, 6 deletions(-) diff --git a/src/backend/app/central/central_routes.py b/src/backend/app/central/central_routes.py index bc4b74d26a..122842460f 100644 --- a/src/backend/app/central/central_routes.py +++ b/src/backend/app/central/central_routes.py @@ -38,7 +38,6 @@ router = APIRouter( prefix="/central", tags=["central"], - dependencies=[Depends(database.get_db)], responses={404: {"description": "Not found"}}, ) diff --git a/src/backend/app/organisations/organisation_routes.py b/src/backend/app/organisations/organisation_routes.py index 272091af11..2f7e94a9f8 100644 --- a/src/backend/app/organisations/organisation_routes.py +++ b/src/backend/app/organisations/organisation_routes.py @@ -36,7 +36,6 @@ router = APIRouter( prefix="/organisation", tags=["organisation"], - dependencies=[Depends(database.get_db)], responses={404: {"description": "Not found"}}, ) diff --git a/src/backend/app/projects/project_routes.py b/src/backend/app/projects/project_routes.py index f6df1ba6ec..0282a0bc92 100644 --- a/src/backend/app/projects/project_routes.py +++ b/src/backend/app/projects/project_routes.py @@ -55,7 +55,6 @@ router = APIRouter( prefix="/projects", tags=["projects"], - dependencies=[Depends(database.get_db)], responses={404: {"description": "Not found"}}, ) diff --git a/src/backend/app/submissions/submission_routes.py b/src/backend/app/submissions/submission_routes.py index 3687b2afb5..08c04536ec 100644 --- a/src/backend/app/submissions/submission_routes.py +++ b/src/backend/app/submissions/submission_routes.py @@ -38,7 +38,6 @@ router = APIRouter( prefix="/submission", tags=["submission"], - dependencies=[Depends(database.get_db)], responses={404: {"description": "Not found"}}, ) diff --git a/src/backend/app/tasks/tasks_routes.py b/src/backend/app/tasks/tasks_routes.py index 8e5d0b3d3f..6df1d6f367 100644 --- a/src/backend/app/tasks/tasks_routes.py +++ b/src/backend/app/tasks/tasks_routes.py @@ -34,7 +34,6 @@ router = APIRouter( prefix="/tasks", tags=["tasks"], - dependencies=[Depends(database.get_db)], responses={404: {"description": "Not found"}}, ) diff --git a/src/backend/app/users/user_routes.py b/src/backend/app/users/user_routes.py index 9bfffd3fee..059b82a90a 100644 --- a/src/backend/app/users/user_routes.py +++ b/src/backend/app/users/user_routes.py @@ -29,7 +29,6 @@ router = APIRouter( prefix="/users", tags=["users"], - dependencies=[Depends(database.get_db)], responses={404: {"description": "Not found"}}, ) From b9b32300c453f3d76dbfbefab16bb2fe7b18f9b2 Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Tue, 23 Jan 2024 12:48:00 +0000 Subject: [PATCH 20/23] fix: use separate super_admin + check_super_admin deps --- src/backend/app/auth/roles.py | 22 ++++++++++++++----- .../app/organisations/organisation_crud.py | 16 +++++++------- .../app/organisations/organisation_routes.py | 4 ++-- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/backend/app/auth/roles.py b/src/backend/app/auth/roles.py index 96278f62be..b075aac472 100644 --- a/src/backend/app/auth/roles.py +++ b/src/backend/app/auth/roles.py @@ -45,17 +45,27 @@ async def get_uid(user_data: AuthUser) -> int: ) +async def check_super_admin( + db: Session, + user_data: AuthUser, +) -> DbUser: + """Database check to determine if super admin role.""" + user_id = await get_uid(user_data) + return db.query(DbUser).filter_by(id=user_id, role=UserRole.ADMIN).first() + + async def super_admin( - db: Session = Depends(get_db), user_data: AuthUser = Depends(login_required), + db: Session = Depends(get_db), ) -> AuthUser: """Super admin role, with access to all endpoints.""" - user_id = await get_uid(user_data) - - match = db.query(DbUser).filter_by(id=user_id, role=UserRole.ADMIN).first() + super_admin = await check_super_admin(db, user_data) - if not match: - log.error(f"User ID {user_id} requested an admin endpoint, but is not admin") + if not super_admin: + log.error( + f"User {user_data.get('username')} requested an admin endpoint, " + "but is not admin" + ) raise HTTPException( status_code=HTTPStatus.FORBIDDEN, detail="User must be an administrator" ) diff --git a/src/backend/app/organisations/organisation_crud.py b/src/backend/app/organisations/organisation_crud.py index 5af2de74ff..551190f28c 100644 --- a/src/backend/app/organisations/organisation_crud.py +++ b/src/backend/app/organisations/organisation_crud.py @@ -25,9 +25,10 @@ from sqlalchemy.orm import Session from app.auth.osm import AuthUser +from app.auth.roles import check_super_admin from app.config import settings from app.db import db_models -from app.models.enums import HTTPStatus, UserRole +from app.models.enums import HTTPStatus from app.organisations.organisation_deps import ( get_organisation_by_name, ) @@ -36,15 +37,14 @@ from app.users import user_crud -def get_organisations(db: Session, current_user: AuthUser, approved: bool): +async def get_organisations(db: Session, current_user: AuthUser, is_approved: bool): """Get all orgs.""" - if ( - db.query(db_models.DbUser) - .filter_by(id=current_user["id"], role=UserRole.ADMIN) - .first() - ): - return db.query(db_models.DbOrganisation).filter_by(approved=approved).all() + super_admin = await check_super_admin(db, current_user) + log.warning(super_admin) + if super_admin: + return db.query(db_models.DbOrganisation).filter_by(approved=is_approved).all() + # If user not admin, only show approved orgs return db.query(db_models.DbOrganisation).filter_by(approved=True).all() diff --git a/src/backend/app/organisations/organisation_routes.py b/src/backend/app/organisations/organisation_routes.py index 2f7e94a9f8..2af7d4afa0 100644 --- a/src/backend/app/organisations/organisation_routes.py +++ b/src/backend/app/organisations/organisation_routes.py @@ -41,13 +41,13 @@ @router.get("/", response_model=list[organisation_schemas.OrganisationOut]) -def get_organisations( +async def get_organisations( db: Session = Depends(database.get_db), current_user: AuthUser = Depends(login_required), approved: bool = True, ) -> list[organisation_schemas.OrganisationOut]: """Get a list of all organisations.""" - return organisation_crud.get_organisations(db, current_user, approved) + return await organisation_crud.get_organisations(db, current_user, approved) @router.get("/{org_id}", response_model=organisation_schemas.OrganisationOut) From 8d4f4d0bbcf6c1557a18de1d932560eb3e1ba4ae Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Tue, 23 Jan 2024 12:52:42 +0000 Subject: [PATCH 21/23] fix: update org_admin to also allow super_admin --- src/backend/app/auth/roles.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/backend/app/auth/roles.py b/src/backend/app/auth/roles.py index b075aac472..e72ee600a2 100644 --- a/src/backend/app/auth/roles.py +++ b/src/backend/app/auth/roles.py @@ -87,6 +87,10 @@ async def org_admin( detail="Both org_id and project_id cannot be passed at the same time", ) + # If user is admin, skip checks + if await check_super_admin(db, user_data): + return user_data + user_id = await get_uid(user_data) if project: From 931f8e9146cb3a45f4b4691806eda80f9a998861 Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Tue, 23 Jan 2024 12:56:29 +0000 Subject: [PATCH 22/23] refactor: remove missed log.warning from organisations endpoint --- src/backend/app/organisations/organisation_crud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/app/organisations/organisation_crud.py b/src/backend/app/organisations/organisation_crud.py index 551190f28c..8bc68eb080 100644 --- a/src/backend/app/organisations/organisation_crud.py +++ b/src/backend/app/organisations/organisation_crud.py @@ -40,7 +40,7 @@ async def get_organisations(db: Session, current_user: AuthUser, is_approved: bool): """Get all orgs.""" super_admin = await check_super_admin(db, current_user) - log.warning(super_admin) + if super_admin: return db.query(db_models.DbOrganisation).filter_by(approved=is_approved).all() From 7b8d65424e894166e711acfbd9605ee094ef5b43 Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Tue, 23 Jan 2024 14:04:39 +0000 Subject: [PATCH 23/23] fix: separate Depends from logic, working org approval --- src/backend/app/auth/roles.py | 56 +++++++++++++------ .../app/organisations/organisation_crud.py | 23 +++----- .../app/organisations/organisation_deps.py | 47 +++++++++++++--- .../app/organisations/organisation_routes.py | 22 +++----- src/backend/app/users/user_deps.py | 2 +- 5 files changed, 95 insertions(+), 55 deletions(-) diff --git a/src/backend/app/auth/roles.py b/src/backend/app/auth/roles.py index e72ee600a2..17c16c2191 100644 --- a/src/backend/app/auth/roles.py +++ b/src/backend/app/auth/roles.py @@ -22,6 +22,8 @@ and always return an AuthUser object in a standard format. """ +from typing import Optional + from fastapi import Depends, HTTPException from loguru import logger as log from sqlalchemy.orm import Session @@ -30,6 +32,7 @@ from app.db.database import get_db from app.db.db_models import DbProject, DbUser, DbUserRoles, organisation_managers from app.models.enums import HTTPStatus, ProjectRole, UserRole +from app.organisations.organisation_deps import check_org_exists from app.projects.project_deps import get_project_by_id @@ -47,10 +50,13 @@ async def get_uid(user_data: AuthUser) -> int: async def check_super_admin( db: Session, - user_data: AuthUser, + user: [AuthUser, int], ) -> DbUser: """Database check to determine if super admin role.""" - user_id = await get_uid(user_data) + if isinstance(user, int): + user_id = user + else: + user_id = await get_uid(user) return db.query(DbUser).filter_by(id=user_id, role=UserRole.ADMIN).first() @@ -73,6 +79,35 @@ async def super_admin( return user_data +async def check_org_admin( + db: Session, + user: [AuthUser, int], + project: Optional[DbProject], + org_id: Optional[int], +) -> DbUser: + """Database check to determine if org admin role.""" + if isinstance(user, int): + user_id = user + else: + user_id = await get_uid(user) + + if project: + org_id = db.query(DbProject).filter_by(id=project.id).first().organisation_id + + # Check org exists + await check_org_exists(db, org_id) + + # If user is admin, skip checks + if await check_super_admin(db, user): + return user + + return ( + db.query(organisation_managers) + .filter_by(organisation_id=org_id, user_id=user_id) + .first() + ) + + async def org_admin( project: DbProject = Depends(get_project_by_id), org_id: int = None, @@ -87,23 +122,10 @@ async def org_admin( detail="Both org_id and project_id cannot be passed at the same time", ) - # If user is admin, skip checks - if await check_super_admin(db, user_data): - return user_data - - user_id = await get_uid(user_data) - - if project: - org_id = db.query(DbProject).filter_by(id=project.id).first().organisation_id - - org_admin = ( - db.query(organisation_managers) - .filter_by(organisation_id=org_id, user_id=user_id) - .first() - ) + org_admin = await check_org_admin(db, user_data, project, org_id) if not org_admin: - log.error(f"User ID {user_id} is not an admin for organisation {org_id}") + log.error(f"User {user_data} is not an admin for organisation {org_id}") raise HTTPException( status_code=HTTPStatus.FORBIDDEN, detail="User is not organisation admin", diff --git a/src/backend/app/organisations/organisation_crud.py b/src/backend/app/organisations/organisation_crud.py index 8bc68eb080..87d7a2f5e8 100644 --- a/src/backend/app/organisations/organisation_crud.py +++ b/src/backend/app/organisations/organisation_crud.py @@ -34,7 +34,6 @@ ) from app.organisations.organisation_schemas import OrganisationEdit, OrganisationIn from app.s3 import add_obj_to_bucket -from app.users import user_crud async def get_organisations(db: Session, current_user: AuthUser, is_approved: bool): @@ -196,43 +195,37 @@ async def delete_organisation( async def add_organisation_admin( - db: Session, user_id: int, organisation: db_models.DbOrganisation + db: Session, user: db_models.DbUser, organisation: db_models.DbOrganisation ): """Adds a user as an admin to the specified organisation. Args: db (Session): The database session. - user_id (int): The ID of the user to be added as an admin. + user (DbUser): The user model instance. organisation (DbOrganisation): The organisation model instance. Returns: Response: The HTTP response with status code 200. """ - # get the user model instance - user_model_instance = await user_crud.get_user(db, user_id) - + log.info(f"Adding user ({user.id}) as org ({organisation.id}) admin") # add data to the managers field in organisation model - organisation.managers.append(user_model_instance) + organisation.managers.append(user) db.commit() return Response(status_code=HTTPStatus.OK) -async def approve_organisation(db, organisation_id): +async def approve_organisation(db, organisation): """Approves an oranisation request made by the user . Args: db: The database session. - organisation_id: The ID of the organisation to be approved. + organisation (DbOrganisation): The organisation model instance. Returns: Response: An HTTP response with the status code 200. """ - db_org = ( - db.query(db_models.DbOrganisation) - .filter(db_models.DbOrganisation.id == organisation_id) - .first() - ) - db_org.approved = True + log.info(f"Approving organisation ID {organisation.id}") + organisation.approved = True db.commit() return Response(status_code=HTTPStatus.OK) diff --git a/src/backend/app/organisations/organisation_deps.py b/src/backend/app/organisations/organisation_deps.py index 0b32e7ad88..4d26e504ed 100644 --- a/src/backend/app/organisations/organisation_deps.py +++ b/src/backend/app/organisations/organisation_deps.py @@ -31,40 +31,58 @@ from app.models.enums import HTTPStatus -async def get_organisation_by_name(db: Session, org_name: str) -> DbOrganisation: +async def get_organisation_by_name( + db: Session, org_name: str, check_approved: bool = True +) -> DbOrganisation: """Get an organisation from the db by name. Args: db (Session): database session org_name (int): id of the organisation + check_approved (bool): first check if the organisation is approved Returns: DbOrganisation: organisation with the given id """ - return ( + org_obj = ( db.query(DbOrganisation) - .filter_by(approved=True) .filter(func.lower(DbOrganisation.name).like(func.lower(f"%{org_name}%"))) .first() ) + if org_obj and check_approved and org_obj.approved is False: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail=f"Organisation ({org_obj.id}) is not approved yet", + ) + return org_obj -async def get_organisation_by_id(db: Session, org_id: int) -> DbOrganisation: +async def get_organisation_by_id( + db: Session, org_id: int, check_approved: bool = True +) -> DbOrganisation: """Get an organisation from the db by id. Args: db (Session): database session org_id (int): id of the organisation + check_approved (bool): first check if the organisation is approved Returns: DbOrganisation: organisation with the given id """ - return db.query(DbOrganisation).filter_by(id=org_id, approved=True).first() + org_obj = db.query(DbOrganisation).filter_by(id=org_id).first() + if org_obj and check_approved and org_obj.approved is False: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail=f"Organisation {org_id} is not approved yet", + ) + return org_obj -async def org_exists( +async def check_org_exists( + db: Session, org_id: Union[str, int], - db: Session = Depends(get_db), + check_approved: bool = True, ) -> DbOrganisation: """Check if organisation name exists, else error. @@ -77,11 +95,11 @@ async def org_exists( if isinstance(org_id, int): log.debug(f"Getting organisation by id: {org_id}") - db_organisation = await get_organisation_by_id(db, org_id) + db_organisation = await get_organisation_by_id(db, org_id, check_approved) if isinstance(org_id, str): log.debug(f"Getting organisation by name: {org_id}") - db_organisation = await get_organisation_by_name(db, org_id) + db_organisation = await get_organisation_by_name(db, org_id, check_approved) if not db_organisation: raise HTTPException( @@ -91,3 +109,14 @@ async def org_exists( log.debug(f"Organisation match: {db_organisation}") return db_organisation + + +async def org_exists( + org_id: Union[str, int], + db: Session = Depends(get_db), +) -> DbOrganisation: + """Wrapper for check_org_exists to be used as a route dependency. + + Requires Depends from a route. + """ + return await check_org_exists(db, org_id) diff --git a/src/backend/app/organisations/organisation_routes.py b/src/backend/app/organisations/organisation_routes.py index 2af7d4afa0..6b5000f004 100644 --- a/src/backend/app/organisations/organisation_routes.py +++ b/src/backend/app/organisations/organisation_routes.py @@ -28,10 +28,10 @@ from app.auth.osm import AuthUser, login_required from app.auth.roles import org_admin, super_admin from app.db import database -from app.db.db_models import DbOrganisation +from app.db.db_models import DbOrganisation, DbUser from app.organisations import organisation_crud, organisation_schemas -from app.organisations.organisation_deps import org_exists -from app.users.user_deps import user_exists +from app.organisations.organisation_deps import check_org_exists, org_exists +from app.users.user_deps import user_exists_in_db router = APIRouter( prefix="/organisation", @@ -93,31 +93,27 @@ async def delete_organisations( @router.post("/approve/") async def approve_organisation( - organisation_id: int, + org_id: int, db: Session = Depends(database.get_db), - current_user: AuthUser = Depends(login_required), + current_user: AuthUser = Depends(super_admin), ): """Approve the organisation request made by the user. The logged in user must be super admin to perform this action . """ - # check if the current_user is the super admin - super_admin(db, current_user) - return await organisation_crud.approve_organisation(db, organisation_id) + org_obj = await check_org_exists(db, org_id, check_approved=False) + return await organisation_crud.approve_organisation(db, org_obj) @router.post("/add_admin/") async def add_new_organisation_admin( db: Session = Depends(database.get_db), - current_user: AuthUser = Depends(login_required), - user: AuthUser = Depends(user_exists), organisation: DbOrganisation = Depends(org_exists), + user: DbUser = Depends(user_exists_in_db), + current_user: AuthUser = Depends(org_admin), ): """Add a new organisation admin. The logged in user must be either the owner of the organisation or a super admin. """ - # check if the current_user is the organisation admin - org_admin(db, organisation.id, current_user) - return await organisation_crud.add_organisation_admin(db, user, organisation) diff --git a/src/backend/app/users/user_deps.py b/src/backend/app/users/user_deps.py index c689e91b9b..3bf84e4363 100644 --- a/src/backend/app/users/user_deps.py +++ b/src/backend/app/users/user_deps.py @@ -32,7 +32,7 @@ from app.users.user_crud import get_user, get_user_by_username -async def user_exists( +async def user_exists_in_db( user_id: Union[str, int], db: Session = Depends(get_db), ) -> DbUser: