diff --git a/alembic/versions/61978d612820_add_user_create_update_permission.py b/alembic/versions/61978d612820_add_user_create_update_permission.py index 5ecf7068..cb57e61c 100644 --- a/alembic/versions/61978d612820_add_user_create_update_permission.py +++ b/alembic/versions/61978d612820_add_user_create_update_permission.py @@ -18,8 +18,25 @@ branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None +foreign_key_name = "user_permissions_permission_id_fkey" + def upgrade() -> None: + op.drop_constraint( + constraint_name=foreign_key_name, + table_name="user_permissions", + type_="foreignkey", + ) + + op.create_foreign_key( + constraint_name=foreign_key_name, + source_table="user_permissions", + referent_table="permissions", + local_cols=["permission_id"], + remote_cols=["permission_id"], + ondelete="CASCADE", + ) + op.execute( """ INSERT INTO PERMISSIONS( @@ -33,9 +50,25 @@ def upgrade() -> None: def downgrade() -> None: + op.execute( """ - DELETE FROM PERMISSIONS + DELETE FROM PERMISSIONS WHERE permission_name = 'user_create_update'; """ ) + + op.drop_constraint( + constraint_name=foreign_key_name, + table_name="user_permissions", + type_="foreignkey", + ) + + op.create_foreign_key( + constraint_name=foreign_key_name, + source_table="user_permissions", + referent_table="permissions", + local_cols=["permission_id"], + remote_cols=["permission_id"], + ondelete="NO ACTION", + ) diff --git a/app.py b/app.py index 55e7bab5..b5d06157 100644 --- a/app.py +++ b/app.py @@ -6,6 +6,7 @@ from flask_cors import CORS from middleware.util import get_env_variable +from resources.Admin import namespace_admin from resources.Batch import namespace_bulk from resources.Callback import namespace_auth from resources.DataRequests import namespace_data_requests @@ -72,6 +73,7 @@ namespace_match, namespace_locations, namespace_metrics, + namespace_admin, ] MY_PREFIX = "/api" @@ -147,7 +149,6 @@ def get_api_with_namespaces(): description="The following is the API documentation for the PDAP Data Sources API." "\n\nFor API help, consult [our getting started guide.](https://docs.pdap.io/api/introduction)" "\n\nTo search the database, go to [pdap.io](https://pdap.io).", - ) for namespace in NAMESPACES: api.add_namespace(namespace) diff --git a/database_client/DTOs.py b/database_client/DTOs.py index 235f2da2..2bdca9d3 100644 --- a/database_client/DTOs.py +++ b/database_client/DTOs.py @@ -6,7 +6,7 @@ class UserInfoNonSensitive(BaseModel): - id: int + user_id: int email: str created_at: datetime updated_at: datetime diff --git a/database_client/database_client.py b/database_client/database_client.py index df4bfacc..ed69780d 100644 --- a/database_client/database_client.py +++ b/database_client/database_client.py @@ -12,7 +12,7 @@ from psycopg import sql, Cursor from psycopg.rows import dict_row, tuple_row from sqlalchemy import select, MetaData, delete, update, insert, Select, func -from sqlalchemy.orm import aliased, defaultload, load_only, selectinload +from sqlalchemy.orm import aliased, defaultload, load_only, selectinload, joinedload from database_client.DTOs import UserInfoNonSensitive, UsersWithPermissions from database_client.constants import METADATA_METHOD_NAMES, PAGE_SIZE @@ -193,7 +193,7 @@ def get_user_id(self, email: str) -> Optional[int]: return None return int(results[0]["id"]) - def set_user_password_digest(self, user_id: int, password_digest: str): + def update_user_password_digest(self, user_id: int, password_digest: str): """ Updates the password digest for a user in the database. :param user_id: @@ -590,7 +590,7 @@ def link_external_account( ) @cursor_manager() - def add_user_permission(self, user_id: str, permission: PermissionsEnum): + def add_user_permission(self, user_id: str or int, permission: PermissionsEnum): """ Adds a permission to a user. @@ -1455,7 +1455,7 @@ def get_user_info_by_id(self, user_id: int) -> UserInfoNonSensitive: where_mappings={"id": user_id}, ) return UserInfoNonSensitive( - id=user_id, + user_id=user_id, email=result["email"], created_at=result["created_at"], updated_at=result["updated_at"], @@ -1464,23 +1464,34 @@ def get_user_info_by_id(self, user_id: int) -> UserInfoNonSensitive: @session_manager def get_users(self, page: int) -> List[UsersWithPermissions]: raw_results = self.session.execute( - select(User.id, User.email, User.created_at, User.updated_at) + select(User) .options(selectinload(User.permissions)) .order_by(User.created_at.desc()) .limit(100) .offset((page - 1) * 100) ).all() - return [ - UsersWithPermissions( - id=user_id, - email=email, - created_at=created_at, - updated_at=updated_at, - permissions=permissions, + final_results = [] + + for raw_result in raw_results: + user = raw_result[0] + permissions_db = user.permissions + permissions_str = [ + permission.permission_name for permission in permissions_db + ] + permissions_enum = [ + PermissionsEnum(permission) for permission in permissions_str + ] + uwp = UsersWithPermissions( + user_id=user.id, + email=user.email, + created_at=user.created_at, + updated_at=user.updated_at, + permissions=permissions_enum, ) - for user_id, email, created_at, updated_at, permissions in raw_results - ] + final_results.append(uwp) + + return final_results def get_user_email(self, user_id: int) -> str: return self._select_single_entry_from_relation( diff --git a/database_client/models.py b/database_client/models.py index d34eb645..ebeb3785 100644 --- a/database_client/models.py +++ b/database_client/models.py @@ -632,18 +632,18 @@ class User(Base): role: Mapped[Optional[text]] # Relationships - permissions: Mapped[list[PermissionsEnum]] = relationship( - argument="PermissionsEnum", + permissions = relationship( + argument="Permission", secondary="public.user_permissions", primaryjoin="User.id == UserPermission.user_id", - secondaryjoin="UserPermission.permission_id == Permission.id", + secondaryjoin="UserPermission.permission_id == Permission.permission_id", ) class Permission(Base): __tablename__ = Relations.PERMISSIONS.value - id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + permission_id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) permission_name: Mapped[str_255] description: Mapped[Optional[text]] @@ -653,7 +653,9 @@ class UserPermission(Base): id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) user_id: Mapped[int] = mapped_column(ForeignKey("public.users.id")) - permission_id: Mapped[int] = mapped_column(ForeignKey("public.permissions.id")) + permission_id: Mapped[int] = mapped_column( + ForeignKey("public.permissions.permission_id") + ) class PendingUser(Base): diff --git a/middleware/enums.py b/middleware/enums.py index d21eb392..9fcd7c47 100644 --- a/middleware/enums.py +++ b/middleware/enums.py @@ -86,6 +86,8 @@ class Relations(Enum): RECENT_SEARCHES = "recent_searches" RECENT_SEARCHES_EXPANDED = "recent_searches_expanded" LINK_RECENT_SEARCH_RECORD_CATEGORIES = "link_recent_search_record_categories" + PERMISSIONS = "permissions" + USER_PERMISSIONS = "user_permissions" class JurisdictionType(Enum): @@ -119,7 +121,6 @@ class AgencyType(Enum): A list of valid agency types """ - NONE = None AGGREGATED = "aggregated" COURT = "court" POLICE = "police" diff --git a/middleware/primary_resource_logic/admin.py b/middleware/primary_resource_logic/admin.py index c6163632..1b7682d6 100644 --- a/middleware/primary_resource_logic/admin.py +++ b/middleware/primary_resource_logic/admin.py @@ -2,81 +2,42 @@ from flask import Response from werkzeug.security import generate_password_hash -from database_client.DTOs import UserInfoNonSensitive +from database_client.DTOs import UserInfoNonSensitive, UsersWithPermissions +from database_client.database_client import DatabaseClient from middleware.access_logic import AccessInfoPrimary -from middleware.db_client import DatabaseClient from middleware.common_response_formatting import created_id_response -from middleware.dynamic_request_logic.supporting_classes import MiddlewareParameters -from middleware.enums import Relations -from middleware.primary_resource_logic.util import ( - create_entry, - delete_entry, - get_entries, - get_entry_by_id, - update_entry, -) from middleware.flask_response_manager import FlaskResponseManager +from middleware.schema_and_dto_logic.common_schemas_and_dtos import ( + GetByIDBaseDTO, + GetManyBaseDTO, +) from middleware.schema_and_dto_logic.primary_resource_dtos.admin_dtos import ( AdminUserPostDTO, AdminUserPutDTO, ) -from middleware.schema_and_dto_logic.primary_resource_schemas.admin_schemas import AdminUserBaseSchema - -ADMIN_POST_MIDDLEWARE_PARAMETERS = MiddlewareParameters( - entry_name="admin_user", - relation=Relations.USERS.value, - db_client_method=DatabaseClient.create_admin_user, -) - - -ADMIN_PUT_MIDDLEWARE_PARAMETERS = MiddlewareParameters( - entry_name="admin_user", - relation=Relations.USERS.value, - db_client_method=DatabaseClient.update_admin_user, -) -def get_users_admin( - db_client: DatabaseClient, - access_info: AccessInfoPrimary -) -> list[dict]: +def get_users_admin(db_client: DatabaseClient, dto: GetManyBaseDTO) -> Response: # Return database client method - pass - - -def get_user_by_id_admin( - db_client: DatabaseClient, - access_info: AccessInfoPrimary, - user_id: str -) -> Response: - # Return database client method - user_info: UserInfoNonSensitive = db_client.get_user_info_by_id(user_id) - - user_permissions = db_client.get_user_permissions(user_id) - - return FlaskResponseManager.make_response({ - "id": user_info.id, - "created_at": user_info.created_at, - "updated_at": user_info.updated_at, - "email": user_info.email, - "permissions": [permission.value for permission in user_permissions] - }) + results: list[UsersWithPermissions] = db_client.get_users(page=dto.page) + return FlaskResponseManager.make_response( + { + "message": "Returning users", + "data": [user.model_dump(mode="json") for user in results], + "metadata": {"count": len(results)}, + } + ) -def create_admin_user( - db_client: DatabaseClient, - access_info: AccessInfoPrimary, - dto: AdminUserPostDTO -) -> Response: +def create_admin_user(db_client: DatabaseClient, dto: AdminUserPostDTO) -> Response: # Hash password password_digest = generate_password_hash(dto.password) # Apply database client method for user and get id user_id = db_client.create_new_user( - email=dto.email, - password_digest=password_digest + email=dto.email, password_digest=password_digest ) # Apply database client method for permissions @@ -84,15 +45,19 @@ def create_admin_user( db_client.add_user_permission(user_id, permission) # Return response with id - return created_id_response( - new_id=str(user_id), message="User created." - ) + return created_id_response(new_id=str(user_id), message="User created.") -def update_admin_user(db_client: DatabaseClient, access_info: AccessInfoPrimary, admin_user_id: str, dto: AdminUserPutDTO) -> dict: +def update_user_password( + db_client: DatabaseClient, user_id: int, dto: AdminUserPutDTO +) -> Response: # Hash password - pass + password_digest = generate_password_hash(dto.password) # Apply database client method + db_client.update_user_password_digest( + user_id=user_id, password_digest=password_digest + ) - # Return response \ No newline at end of file + # Return response + return FlaskResponseManager.make_response({"message": "User updated."}) diff --git a/middleware/primary_resource_logic/reset_token_queries.py b/middleware/primary_resource_logic/reset_token_queries.py index 620314b0..9b3f79b3 100644 --- a/middleware/primary_resource_logic/reset_token_queries.py +++ b/middleware/primary_resource_logic/reset_token_queries.py @@ -114,7 +114,9 @@ def change_password_wrapper( def set_user_password(db_client: DatabaseClient, user_id: int, password: str): password_digest = generate_password_hash(password) - db_client.set_user_password_digest(user_id=user_id, password_digest=password_digest) + db_client.update_user_password_digest( + user_id=user_id, password_digest=password_digest + ) def invalid_token_response(): diff --git a/middleware/primary_resource_logic/user_profile.py b/middleware/primary_resource_logic/user_profile.py index aa9fa29b..d375756e 100644 --- a/middleware/primary_resource_logic/user_profile.py +++ b/middleware/primary_resource_logic/user_profile.py @@ -50,7 +50,7 @@ def get_user_by_id_wrapper( # Check that user is either owner or admin if ( user_id != access_info.get_user_id() - and PermissionsEnum.DB_WRITE not in access_info.permissions + and PermissionsEnum.READ_ALL_USER_INFO not in access_info.permissions ): return FlaskResponseManager.make_response( data={"message": "Forbidden."}, status_code=HTTPStatus.FORBIDDEN diff --git a/middleware/schema_and_dto_logic/dynamic_logic/dynamic_schema_documentation_construction.py b/middleware/schema_and_dto_logic/dynamic_logic/dynamic_schema_documentation_construction.py index 886bcffc..3fc11722 100644 --- a/middleware/schema_and_dto_logic/dynamic_logic/dynamic_schema_documentation_construction.py +++ b/middleware/schema_and_dto_logic/dynamic_logic/dynamic_schema_documentation_construction.py @@ -314,8 +314,11 @@ def _build_nested_field_as_model(self, fi: FieldInfo): def _raise_if_nonzero_parser_fields(self, fi, sorter): if len(sorter.parser_fields) != 0: + parser_field_names = [x.field_name for x in sorter.parser_fields] raise ValueError( - f"Nested Model `{fi.field_name}` in model `{self.model_name}` has fields that are identified as parser fields." + f"Nested Model `{fi.field_name}` in model `{self.model_name}` " + f"has fields that are identified as parser fields." + f"({parser_field_names})" ) def _raise_if_no_model_fields(self, fi, fields): diff --git a/middleware/schema_and_dto_logic/primary_resource_dtos/admin_dtos.py b/middleware/schema_and_dto_logic/primary_resource_dtos/admin_dtos.py index 1aa71c07..24b0fb37 100644 --- a/middleware/schema_and_dto_logic/primary_resource_dtos/admin_dtos.py +++ b/middleware/schema_and_dto_logic/primary_resource_dtos/admin_dtos.py @@ -1,15 +1,16 @@ from dataclasses import dataclass from typing import Optional +from middleware.enums import PermissionsEnum + @dataclass class AdminUserPostDTO: email: str password: str - permissions: list[str] + permissions: list[PermissionsEnum] @dataclass class AdminUserPutDTO: password: Optional[str] = None - permissions: Optional[list[str]] = None diff --git a/middleware/schema_and_dto_logic/primary_resource_dtos/agencies_dtos.py b/middleware/schema_and_dto_logic/primary_resource_dtos/agencies_dtos.py index 3742614d..9ac92731 100644 --- a/middleware/schema_and_dto_logic/primary_resource_dtos/agencies_dtos.py +++ b/middleware/schema_and_dto_logic/primary_resource_dtos/agencies_dtos.py @@ -11,7 +11,7 @@ class AgencyInfoPutDTO(BaseModel): name: str = None jurisdiction_type: JurisdictionType = None - agency_type: AgencyType = AgencyType.NONE + agency_type: AgencyType = None multi_agency: bool = False no_web_presence: bool = False approved: bool = False diff --git a/middleware/schema_and_dto_logic/primary_resource_schemas/admin_schemas.py b/middleware/schema_and_dto_logic/primary_resource_schemas/admin_schemas.py index 90074ae2..396770e5 100644 --- a/middleware/schema_and_dto_logic/primary_resource_schemas/admin_schemas.py +++ b/middleware/schema_and_dto_logic/primary_resource_schemas/admin_schemas.py @@ -5,24 +5,6 @@ from middleware.enums import PermissionsEnum -class AdminUserBaseSchema(GetManyRequestsBaseSchema): - user_id = fields.Integer( - required=True, metadata={"description": "The ID of the admin user"} - ) - email = fields.Email( - required=True, metadata={"description": "The email of the admin user"} - ) - permissions = fields.List( - fields.Enum(PermissionsEnum, by_value=True), - required=True, - metadata={"description": "The permissions of the admin user"}, - ) - created_at = fields.DateTime( - required=True, - metadata={"description": "The date and time the admin user was created"}, - ) - - from marshmallow import Schema, fields from middleware.schema_and_dto_logic.common_response_schemas import ( MessageSchema, @@ -35,32 +17,68 @@ class AdminUserBaseSchema(GetManyRequestsBaseSchema): from middleware.schema_and_dto_logic.util import get_json_metadata +class AdminUserBaseSchema(Schema): + user_id = fields.Integer( + required=True, metadata=get_json_metadata("The ID of the user") + ) + email = fields.Email( + required=True, metadata=get_json_metadata("The email of the user") + ) + permissions = fields.List( + fields.Enum( + PermissionsEnum, + by_value=True, + metadata=get_json_metadata("The permissions of the user"), + ), + required=True, + metadata=get_json_metadata("The permissions of the user"), + ) + created_at = fields.DateTime( + required=True, + metadata=get_json_metadata("The date and time the user was created"), + ) + updated_at = fields.DateTime( + required=True, + metadata=get_json_metadata("The date and time the user was updated"), + ) + + class AdminUsersGetManyResponseSchema(GetManyResponseSchemaBase): data = fields.List( - fields.Nested(AdminUserBaseSchema(), required=True), + fields.Nested( + AdminUserBaseSchema(), + required=True, + metadata=get_json_metadata(description="The list of admin users"), + ), required=True, metadata=get_json_metadata(description="The list of admin users"), ) -class AdminUsersPostSchema(AdminUserBaseSchema): +class AdminUsersPostSchema(Schema): email = fields.Email( - required=True, metadata={"description": "The email of the admin user"} + required=True, + metadata=get_json_metadata(description="The email of the admin user"), ) password = fields.String( - required=True, metadata={"description": "The password of the admin user"} + required=True, + metadata=get_json_metadata(description="The password of the admin user"), ) permissions = fields.List( - fields.Enum(PermissionsEnum, by_value=True), + fields.Enum( + PermissionsEnum, + by_value=True, + metadata=get_json_metadata(description="The permissions of the admin user"), + ), required=True, - metadata={"description": "The permissions of the admin user"}, + metadata=get_json_metadata(description="The permissions of the admin user"), ) -class AdminUsersPutSchema(AdminUserBaseSchema): +class AdminUsersPutSchema(Schema): password = fields.String( required=False, - metadata={"description": "The new password of the admin user"}, + metadata=get_json_metadata(description="The new password of the admin user"), ) diff --git a/middleware/schema_and_dto_logic/primary_resource_schemas/agencies_base_schemas.py b/middleware/schema_and_dto_logic/primary_resource_schemas/agencies_base_schemas.py index e31236b4..bb4bcb8c 100644 --- a/middleware/schema_and_dto_logic/primary_resource_schemas/agencies_base_schemas.py +++ b/middleware/schema_and_dto_logic/primary_resource_schemas/agencies_base_schemas.py @@ -71,11 +71,10 @@ class AgencyInfoBaseSchema(Schema): }, ) agency_type = fields.Enum( - required=False, + required=True, enum=AgencyType, by_value=fields.Str, allow_none=True, - load_default=AgencyType.NONE, metadata={ "description": "The type of agency.", "source": SourceMappingEnum.JSON, diff --git a/middleware/schema_and_dto_logic/util.py b/middleware/schema_and_dto_logic/util.py index 687451ed..3f26933a 100644 --- a/middleware/schema_and_dto_logic/util.py +++ b/middleware/schema_and_dto_logic/util.py @@ -16,7 +16,7 @@ def _get_required_argument( try: return metadata[argument_name] except KeyError: - name = field_name if field_name else schema_class.__class__.__name__ + name = field_name if field_name else schema_class.__name__ raise MissingArgumentError( f"The argument {argument_name} must be specified as a metadata argument in class {name} (as in `Fields.Str(metadata={argument_name}:value`)" ) diff --git a/resources/Admin.py b/resources/Admin.py index d8b6f984..297956f0 100644 --- a/resources/Admin.py +++ b/resources/Admin.py @@ -11,9 +11,8 @@ ) from middleware.primary_resource_logic.admin import ( get_users_admin, - get_user_by_id_admin, create_admin_user, - update_admin_user, + update_user_password, ) from middleware.schema_and_dto_logic.common_schemas_and_dtos import ( GET_MANY_SCHEMA_POPULATE_PARAMETERS, @@ -51,7 +50,6 @@ def get(self, access_info: AccessInfoPrimary) -> Response: return self.run_endpoint( wrapper_function=get_users_admin, schema_populate_parameters=GET_MANY_SCHEMA_POPULATE_PARAMETERS, - access_info=access_info, ) @endpoint_info( @@ -71,21 +69,6 @@ def post(self, access_info: AccessInfoPrimary): @namespace_admin.route("/users/", methods=["GET", "PUT", "DELETE"]) class AdminUsersByID(PsycopgResource): - @endpoint_info( - namespace=namespace_admin, - auth_info=READ_USER_AUTH_INFO, - schema_config=SchemaConfigs.ADMIN_USERS_BY_ID_GET, - response_info=ResponseInfo( - success_message="Returns information on the specific admin user." - ), - description="Get an admin user by id", - ) - def get(self, resource_id: str, access_info: AccessInfoPrimary) -> Response: - return self.run_endpoint( - wrapper_function=get_user_by_id_admin, - schema_populate_parameters=SchemaConfigs.ADMIN_USERS_BY_ID_GET.value.get_schema_populate_parameters(), - access_info=access_info, - ) @endpoint_info( namespace=namespace_admin, @@ -96,7 +79,7 @@ def get(self, resource_id: str, access_info: AccessInfoPrimary) -> Response: ) def put(self, resource_id: str, access_info: AccessInfoPrimary) -> Response: return self.run_endpoint( - update_admin_user, - access_info=access_info, - admin_user_id=resource_id, + update_user_password, + schema_populate_parameters=SchemaConfigs.ADMIN_USERS_BY_ID_PUT.value.get_schema_populate_parameters(), + user_id=int(resource_id), ) diff --git a/resources/endpoint_schema_config.py b/resources/endpoint_schema_config.py index 6f0f4376..58865d2d 100644 --- a/resources/endpoint_schema_config.py +++ b/resources/endpoint_schema_config.py @@ -590,9 +590,10 @@ class SchemaConfigs(Enum): primary_output_schema=AdminUsersGetByIDResponseSchema(), ) - ADMIN_USERS_BY_ID_PUT = get_post_resource_endpoint_schema_config( + ADMIN_USERS_BY_ID_PUT = EndpointSchemaConfig( input_schema=AdminUsersPutSchema(), input_dto_class=AdminUserPutDTO, + primary_output_schema=MessageSchema(), ) ADMIN_USERS_POST = get_post_resource_endpoint_schema_config( diff --git a/tests/helper_scripts/complex_test_data_creation_functions.py b/tests/helper_scripts/complex_test_data_creation_functions.py index 80e13784..92f40380 100644 --- a/tests/helper_scripts/complex_test_data_creation_functions.py +++ b/tests/helper_scripts/complex_test_data_creation_functions.py @@ -6,7 +6,7 @@ from database_client.database_client import DatabaseClient from database_client.enums import RequestUrgency -from middleware.enums import JurisdictionType +from middleware.enums import JurisdictionType, AgencyType from middleware.schema_and_dto_logic.primary_resource_schemas.agencies_advanced_schemas import ( AgencyInfoPostSchema, ) @@ -150,6 +150,7 @@ def get_sample_agency_post_parameters( override={ "name": name, "jurisdiction_type": JurisdictionType.LOCAL.value, + "agency_type": AgencyType.POLICE.value, }, ), "location_info": location_info, diff --git a/tests/helper_scripts/helper_classes/RequestValidator.py b/tests/helper_scripts/helper_classes/RequestValidator.py index 89fb4481..c300d2b1 100644 --- a/tests/helper_scripts/helper_classes/RequestValidator.py +++ b/tests/helper_scripts/helper_classes/RequestValidator.py @@ -5,13 +5,13 @@ from dataclasses import dataclass from http import HTTPStatus from io import BytesIO -from typing import Optional, Type, Union +from typing import Optional, Type, Union, List from flask.testing import FlaskClient from marshmallow import Schema from database_client.enums import SortOrder, RequestStatus, ApprovalStatus -from middleware.enums import OutputFormatEnum +from middleware.enums import OutputFormatEnum, PermissionsEnum from middleware.util import update_if_not_none from resources.endpoint_schema_config import SchemaConfigs from tests.helper_scripts.common_test_data import get_test_name @@ -681,4 +681,44 @@ def get_metrics( expected_schema=SchemaConfigs.METRICS_GET.value.primary_output_schema, ) + def get_user_by_id_admin(self, headers: dict, user_id: str): + return self.get( + endpoint=f"/api/admin/users/{user_id}", + headers=headers, + expected_schema=SchemaConfigs.ADMIN_USERS_BY_ID_GET.value.primary_output_schema, + ) + + def get_users(self, headers: dict, page: int = 1): + return self.get( + endpoint=f"/api/admin/users?page={page}", + headers=headers, + expected_schema=SchemaConfigs.ADMIN_USERS_GET_MANY.value.primary_output_schema, + ) + + def create_user( + self, + headers: dict, + email: str, + password: str, + permissions: List[str], + ): + return self.post( + endpoint="/api/admin/users", + headers=headers, + json={ + "email": email, + "password": password, + "permissions": permissions, + }, + expected_schema=SchemaConfigs.ADMIN_USERS_POST.value.primary_output_schema, + ) + + def update_admin_user(self, headers: dict, resource_id: str, password: str): + return self.put( + endpoint=f"/api/admin/users/{resource_id}", + headers=headers, + json={"password": password}, + expected_schema=SchemaConfigs.ADMIN_USERS_BY_ID_PUT.value.primary_output_schema, + ) + # endregion diff --git a/tests/helper_scripts/helper_classes/TestDataCreatorDBClient.py b/tests/helper_scripts/helper_classes/TestDataCreatorDBClient.py index dd0a6e95..130c040f 100644 --- a/tests/helper_scripts/helper_classes/TestDataCreatorDBClient.py +++ b/tests/helper_scripts/helper_classes/TestDataCreatorDBClient.py @@ -11,7 +11,7 @@ ExternalAccountTypeEnum, ) from database_client.models import SQL_ALCHEMY_TABLE_REFERENCE -from middleware.enums import JurisdictionType, Relations +from middleware.enums import JurisdictionType, Relations, AgencyType from tests.helper_scripts.common_endpoint_calls import CreatedDataSource from tests.helper_scripts.common_test_data import ( get_random_number_for_testing, @@ -239,6 +239,7 @@ def agency( column_value_mappings = { "name": agency_name, "jurisdiction_type": JurisdictionType.FEDERAL.value, + "agency_type": AgencyType.POLICE.value, } column_value_mappings.update(additional_column_value_mappings) diff --git a/tests/helper_scripts/helper_functions_complex.py b/tests/helper_scripts/helper_functions_complex.py index 31f0aa72..488e95dc 100644 --- a/tests/helper_scripts/helper_functions_complex.py +++ b/tests/helper_scripts/helper_functions_complex.py @@ -15,6 +15,7 @@ PermissionsEnum, Relations, JurisdictionType, + AgencyType, ) from resources.ApiKeyResource import API_KEY_ROUTE from tests.helper_scripts.common_test_data import get_test_name, get_test_email @@ -132,6 +133,7 @@ def setup_get_typeahead_suggestion_test_data(cursor: Optional[psycopg.Cursor] = column_value_mappings={ "name": "Xylodammerung Police Agency", "jurisdiction_type": JurisdictionType.STATE, + "agency_type": AgencyType.POLICE.value, "location_id": location_id, }, column_to_return="id", @@ -175,6 +177,10 @@ def create_test_user_setup( def create_admin_test_user_setup(flask_client: FlaskClient) -> TestUserSetup: tus_admin = create_test_user_setup( flask_client, - permissions=[PermissionsEnum.READ_ALL_USER_INFO, PermissionsEnum.DB_WRITE], + permissions=[ + PermissionsEnum.READ_ALL_USER_INFO, + PermissionsEnum.DB_WRITE, + PermissionsEnum.USER_CREATE_UPDATE, + ], ) return tus_admin diff --git a/tests/integration/test_admin.py b/tests/integration/test_admin.py new file mode 100644 index 00000000..dd1f31ed --- /dev/null +++ b/tests/integration/test_admin.py @@ -0,0 +1,80 @@ +from tests.helper_scripts.helper_classes.TestDataCreatorFlask import ( + TestDataCreatorFlask, +) + + +def test_admin_user_create(test_data_creator_flask: TestDataCreatorFlask): + tdc = test_data_creator_flask + admin_tus = tdc.get_admin_tus() + + # Create a new admin user + new_user_data = { + "email": "newuser@example.com", + "password": "password123", + "permissions": ["read_all_user_info", "db_write"], + } + response = tdc.request_validator.create_user( + headers=admin_tus.jwt_authorization_header, + email=new_user_data["email"], + password=new_user_data["password"], + permissions=new_user_data["permissions"], + ) + + # Successfully log in as the new admin user + tdc.request_validator.login( + email=new_user_data["email"], + password=new_user_data["password"], + ) + + +def test_admin_user_get_all(test_data_creator_flask: TestDataCreatorFlask): + tdc = test_data_creator_flask + admin_tus = tdc.get_admin_tus() + + # Create a few new admin users + for i in range(3): + tdc.request_validator.create_user( + headers=admin_tus.jwt_authorization_header, + email=f"newuser{i}@example.com", + password="password123", + permissions=["read_all_user_info", "db_write"], + ) + + # Get all admin users + response = tdc.request_validator.get_users( + headers=admin_tus.jwt_authorization_header, + ) + assert len(response) >= 3 + + data = response["data"] + + for i in range(3): + assert "newuser" in data[i]["email"] + assert data[i]["permissions"] == ["read_all_user_info", "db_write"] + + +def test_admin_user_update(test_data_creator_flask: TestDataCreatorFlask): + tdc = test_data_creator_flask + admin_tus = tdc.get_admin_tus() + + # Create a new admin user + response = tdc.request_validator.create_user( + headers=admin_tus.jwt_authorization_header, + email="newuserput@example.com", + password="password123", + permissions=["read_all_user_info", "db_write"], + ) + new_user_id = response["id"] + + # Update the new admin user + tdc.request_validator.update_admin_user( + headers=admin_tus.jwt_authorization_header, + resource_id=new_user_id, + password="newpassword123", + ) + + # Login as the updated admin user + tdc.request_validator.login( + email="newuserput@example.com", + password="newpassword123", + ) diff --git a/tests/integration/test_agencies.py b/tests/integration/test_agencies.py index 878060d7..810c0528 100644 --- a/tests/integration/test_agencies.py +++ b/tests/integration/test_agencies.py @@ -5,7 +5,7 @@ from database_client.db_client_dataclasses import WhereMapping from database_client.enums import SortOrder -from middleware.enums import JurisdictionType +from middleware.enums import JurisdictionType, AgencyType from middleware.schema_and_dto_logic.primary_resource_schemas.agencies_advanced_schemas import ( AgencyInfoPutSchema, ) @@ -270,6 +270,7 @@ def test_agencies_delete(test_data_creator_flask: TestDataCreatorFlask): "agency_info": { "name": get_test_name(), "jurisdiction_type": JurisdictionType.FEDERAL.value, + "agency_type": AgencyType.COURT.value, } }, ) diff --git a/tests/integration/test_bulk.py b/tests/integration/test_bulk.py index f385d1c6..e68d89a1 100644 --- a/tests/integration/test_bulk.py +++ b/tests/integration/test_bulk.py @@ -8,6 +8,7 @@ from conftest import test_data_creator_flask, monkeysession from database_client.enums import LocationType +from middleware.enums import AgencyType from middleware.primary_resource_logic.bulk_logic import listify_strings from middleware.schema_and_dto_logic.common_response_schemas import MessageSchema from middleware.schema_and_dto_logic.dynamic_logic.dynamic_csv_to_schema_conversion_logic import ( @@ -210,7 +211,13 @@ def test_batch_agencies_update_happy_path( agencies = [runner.tdc.agency() for _ in range(3)] locality_info = generate_agencies_locality_data() rows = [ - runner.generate_test_data(override={**locality_info, "id": agencies[i].id}) + runner.generate_test_data( + override={ + **locality_info, + "id": agencies[i].id, + "agency_type": AgencyType.POLICE.value, + } + ) for i in range(3) ] data = create_csv_and_run( diff --git a/tests/integration/test_search.py b/tests/integration/test_search.py index 70f9b6a9..11b7be1d 100644 --- a/tests/integration/test_search.py +++ b/tests/integration/test_search.py @@ -7,7 +7,12 @@ from marshmallow import Schema from database_client.enums import LocationType, ApprovalStatus -from middleware.enums import OutputFormatEnum, JurisdictionSimplified, JurisdictionType +from middleware.enums import ( + OutputFormatEnum, + JurisdictionSimplified, + JurisdictionType, + AgencyType, +) from middleware.schema_and_dto_logic.primary_resource_schemas.agencies_advanced_schemas import ( AgencyInfoPostSchema, ) @@ -333,6 +338,7 @@ def test_search_federal(test_data_creator_flask: TestDataCreatorFlask): override={ "jurisdiction_type": JurisdictionType.FEDERAL.value, "approved": True, + "agency_type": AgencyType.POLICE.value, }, ), }, diff --git a/tests/test_schema_validation.py b/tests/test_schema_validation.py index 1f1593b0..32f2fd26 100644 --- a/tests/test_schema_validation.py +++ b/tests/test_schema_validation.py @@ -3,7 +3,7 @@ import pytest from marshmallow import ValidationError -from middleware.enums import JurisdictionType +from middleware.enums import JurisdictionType, AgencyType from middleware.schema_and_dto_logic.primary_resource_schemas.agencies_advanced_schemas import ( AgenciesPostSchema, ) @@ -80,6 +80,7 @@ def produce_data( "agency_info": { "name": "test", "jurisdiction_type": jurisdiction_type.value, + "agency_type": AgencyType.POLICE.value, } } if include_location_info: