diff --git a/auth-api/migrations/versions/9c58b78727c8_users_type.py b/auth-api/migrations/versions/9c58b78727c8_users_type.py index 4b857264e..ec20d144d 100644 --- a/auth-api/migrations/versions/9c58b78727c8_users_type.py +++ b/auth-api/migrations/versions/9c58b78727c8_users_type.py @@ -31,9 +31,6 @@ def upgrade(): login_source = user.login_source if user.login_source in [LoginSource.BCEID.value, LoginSource.BCSC.value]: user_type = Role.PUBLIC_USER.name - elif user.login_source == LoginSource.BCROS.value or user.username.startswith("bcros/"): - user_type = Role.ANONYMOUS_USER.name - login_source = LoginSource.BCSC.value elif user.login_source == LoginSource.STAFF.value: user_type = Role.STAFF.name diff --git a/auth-api/src/auth_api/config.py b/auth-api/src/auth_api/config.py index 013a7e86a..6df301ecf 100644 --- a/auth-api/src/auth_api/config.py +++ b/auth-api/src/auth_api/config.py @@ -119,12 +119,6 @@ class _Config: # pylint: disable=too-few-public-methods ENTITY_SVC_CLIENT_ID = os.getenv("ENTITY_SVC_CLIENT_ID") ENTITY_SVC_CLIENT_SECRET = os.getenv("ENTITY_SVC_CLIENT_SECRET") - # Upstream Keycloak setting - should be removed - KEYCLOAK_BCROS_BASE_URL = os.getenv("KEYCLOAK_BCROS_BASE_URL") - KEYCLOAK_BCROS_REALMNAME = os.getenv("KEYCLOAK_BCROS_REALMNAME") - KEYCLOAK_BCROS_ADMIN_CLIENTID = os.getenv("KEYCLOAK_BCROS_ADMIN_CLIENTID") - KEYCLOAK_BCROS_ADMIN_SECRET = os.getenv("KEYCLOAK_BCROS_ADMIN_SECRET") - # API Endpoints BCOL_API_URL = os.getenv("BCOL_API_URL", "") + os.getenv("BCOL_API_VERSION", "") NAMEX_API_URL = os.getenv("NAMEX_API_URL", "") + os.getenv("NAMEX_API_VERSION", "") @@ -277,10 +271,10 @@ class TestConfig(_Config): # pylint: disable=too-few-public-methods 4H8UZcVFN95vEKxJiLRjAmj6g273pu9kK4ymXNEjWWJn -----END RSA PRIVATE KEY-----""" - KEYCLOAK_ADMIN_USERNAME = KEYCLOAK_BCROS_ADMIN_CLIENTID = os.getenv("KEYCLOAK_TEST_ADMIN_CLIENTID") - KEYCLOAK_ADMIN_SECRET = KEYCLOAK_BCROS_ADMIN_SECRET = os.getenv("KEYCLOAK_TEST_ADMIN_SECRET") - KEYCLOAK_BASE_URL = KEYCLOAK_BCROS_BASE_URL = os.getenv("KEYCLOAK_TEST_BASE_URL") - KEYCLOAK_REALMNAME = KEYCLOAK_BCROS_REALMNAME = os.getenv("KEYCLOAK_TEST_REALMNAME") + KEYCLOAK_ADMIN_USERNAME = os.getenv("KEYCLOAK_TEST_ADMIN_CLIENTID") + KEYCLOAK_ADMIN_SECRET = os.getenv("KEYCLOAK_TEST_ADMIN_SECRET") + KEYCLOAK_BASE_URL = os.getenv("KEYCLOAK_TEST_BASE_URL") + KEYCLOAK_REALMNAME = os.getenv("KEYCLOAK_TEST_REALMNAME") JWT_OIDC_AUDIENCE = os.getenv("JWT_OIDC_TEST_AUDIENCE") JWT_OIDC_CLIENT_SECRET = os.getenv("JWT_OIDC_TEST_CLIENT_SECRET") JWT_OIDC_ISSUER = os.getenv("JWT_OIDC_TEST_ISSUER") diff --git a/auth-api/src/auth_api/exceptions/errors.py b/auth-api/src/auth_api/exceptions/errors.py index 34ca2bc38..30170a81e 100644 --- a/auth-api/src/auth_api/exceptions/errors.py +++ b/auth-api/src/auth_api/exceptions/errors.py @@ -82,8 +82,7 @@ class Error(Enum): ORG_CANNOT_BE_DISSOLVED = "Organization cannot be dissolved", HTTPStatus.NOT_ACCEPTABLE FAILED_ADDING_USER_IN_KEYCLOAK = "Error adding user to keycloak", HTTPStatus.INTERNAL_SERVER_ERROR ACCCESS_TYPE_MANDATORY = "staff created orgs needs access type", HTTPStatus.BAD_REQUEST - USER_CANT_CREATE_ANONYMOUS_ORG = "Only staff can create anonymous org", HTTPStatus.UNAUTHORIZED - USER_CANT_CREATE_GOVM_ORG = "Only staff can create govt ministy org", HTTPStatus.UNAUTHORIZED + USER_CANT_CREATE_GOVM_ORG = "Only staff can create govt ministry org", HTTPStatus.UNAUTHORIZED USER_CANT_CREATE_EXTRA_PROVINCIAL_ORG = ( "Only out of province users can create extra provincial org", diff --git a/auth-api/src/auth_api/exceptions/exceptions.py b/auth-api/src/auth_api/exceptions/exceptions.py index 624db3930..70cf7dd5c 100644 --- a/auth-api/src/auth_api/exceptions/exceptions.py +++ b/auth-api/src/auth_api/exceptions/exceptions.py @@ -7,7 +7,7 @@ status_code - where possible use HTTP Error Codes """ -from auth_api.exceptions.errors import Error # noqa: I001, I003 +from auth_api.exceptions.errors import Error # noqa: I001 class BusinessException(Exception): # noqa: N818 diff --git a/auth-api/src/auth_api/models/org.py b/auth-api/src/auth_api/models/org.py index 4ad8c85a2..7cfeb31d5 100644 --- a/auth-api/src/auth_api/models/org.py +++ b/auth-api/src/auth_api/models/org.py @@ -53,7 +53,7 @@ class Org(Versioned, BaseModel): # pylint: disable=too-few-public-methods,too-m status_code = Column(ForeignKey("org_statuses.code"), nullable=False) name = Column(String(250), index=True) branch_name = Column(String(100), nullable=True, default="") # used for any additional info as branch name - access_type = Column(String(250), index=True, nullable=True) # for ANONYMOUS ACCESS + access_type = Column(String(250), index=True, nullable=True) decision_made_by = Column(String(250)) decision_made_on = Column(DateTime, nullable=True) bcol_user_id = Column(String(20)) @@ -259,16 +259,9 @@ def _search_for_statuses(cls, query, statuses): .outerjoin(Invitation, Invitation.id == InvitationMembership.invitation_id) .filter(Invitation.invitation_status_code == InvitationStatus.PENDING.value) .filter( - ( - (Invitation.type == InvitationType.DIRECTOR_SEARCH.value) - & (Org.status_code == OrgStatusEnum.ACTIVE.value) - & (Org.access_type == AccessType.ANONYMOUS.value) - ) - | ( - (Invitation.type == InvitationType.GOVM.value) - & (Org.status_code == OrgStatusEnum.PENDING_INVITE_ACCEPT.value) - & (Org.access_type == AccessType.GOVM.value) - ) + (Invitation.type == InvitationType.GOVM.value) + & (Org.status_code == OrgStatusEnum.PENDING_INVITE_ACCEPT.value) + & (Org.access_type == AccessType.GOVM.value) ) ) query = query.filter(Org.id.notin_(pending_inv_subquery)) @@ -284,16 +277,9 @@ def search_pending_activation_orgs(cls, name: str): .options(contains_eager(Org.invitations).load_only(InvitationMembership.invitation_id)) .filter(Invitation.invitation_status_code == InvitationStatus.PENDING.value) .filter( - ( - (Invitation.type == InvitationType.DIRECTOR_SEARCH.value) - & (Org.status_code == OrgStatusEnum.ACTIVE.value) - & (Org.access_type == AccessType.ANONYMOUS.value) - ) - | ( - (Invitation.type == InvitationType.GOVM.value) - & (Org.status_code == OrgStatusEnum.PENDING_INVITE_ACCEPT.value) - & (Org.access_type == AccessType.GOVM.value) - ) + (Invitation.type == InvitationType.GOVM.value) + & (Org.status_code == OrgStatusEnum.PENDING_INVITE_ACCEPT.value) + & (Org.access_type == AccessType.GOVM.value) ) ) if name: diff --git a/auth-api/src/auth_api/models/user.py b/auth-api/src/auth_api/models/user.py index d13164337..0c24dd3f7 100644 --- a/auth-api/src/auth_api/models/user.py +++ b/auth-api/src/auth_api/models/user.py @@ -52,13 +52,13 @@ class User(Versioned, BaseModel): "keycloak_guid", UUID(as_uuid=True), unique=True, - nullable=True, # bcros users comes with no guid + nullable=True, ) is_terms_of_use_accepted = Column(Boolean(), default=False, nullable=True) terms_of_use_accepted_version = Column(ForeignKey("documents.version_id"), nullable=True) - # a type for the user to identify what kind of user it is..ie anonymous , bcsc etc ..similar to login source + # a type for the user to identify what kind of user it is..bcsc etc ..similar to login source type = Column("type", String(200), nullable=True) status = Column(ForeignKey("user_status_codes.id")) idp_userid = Column("idp_userid", String(256), index=True) @@ -272,12 +272,7 @@ def _get_type(cls, user_from_context: UserContext) -> str: """Return type of the user from the token info.""" user_type: str = None if user_from_context.roles: - if ( - Role.ANONYMOUS_USER.value in user_from_context.roles - or user_from_context.login_source == LoginSource.BCROS.value - ): - user_type = Role.ANONYMOUS_USER.name - elif user_from_context.is_staff(): + if user_from_context.is_staff(): user_type = Role.STAFF.name elif Role.GOV_ACCOUNT_USER.value in user_from_context.roles: user_type = Role.GOV_ACCOUNT_USER.name diff --git a/auth-api/src/auth_api/resources/v1/__init__.py b/auth-api/src/auth_api/resources/v1/__init__.py index cebe6d3ad..2f55bb1ab 100644 --- a/auth-api/src/auth_api/resources/v1/__init__.py +++ b/auth-api/src/auth_api/resources/v1/__init__.py @@ -24,7 +24,6 @@ from .activity_log import bp as activity_log_bp from .affiliation_invitation import bp as affiliation_invitation_bp from .bcol_profiles import bp as bcol_profiles_bp -from .bulk_user import bp as bulk_user_bp from .codes import bp as codes_bp from .documents import bp as documents_bp from .documents_affidavit import bp as documents_affidavit_bp @@ -60,7 +59,6 @@ def init_app(self, app): self.app.register_blueprint(activity_log_bp) self.app.register_blueprint(affiliation_invitation_bp) self.app.register_blueprint(bcol_profiles_bp) - self.app.register_blueprint(bulk_user_bp) self.app.register_blueprint(codes_bp) self.app.register_blueprint(documents_bp) self.app.register_blueprint(documents_affidavit_bp) diff --git a/auth-api/src/auth_api/resources/v1/bulk_user.py b/auth-api/src/auth_api/resources/v1/bulk_user.py deleted file mode 100644 index 4cbb059e9..000000000 --- a/auth-api/src/auth_api/resources/v1/bulk_user.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""API endpoints for managing a User resource.""" - -from http import HTTPStatus - -from flask import Blueprint, request -from flask_cors import cross_origin - -from auth_api.exceptions import BusinessException -from auth_api.schemas import utils as schema_utils -from auth_api.services.user import User as UserService -from auth_api.utils.auth import jwt as _jwt -from auth_api.utils.endpoints_enums import EndpointEnum - -bp = Blueprint("BULK_USERS", __name__, url_prefix=f"{EndpointEnum.API_V1.value}/bulk/users") - - -@bp.route("", methods=["POST", "OPTIONS"]) -@cross_origin(origins="*", methods=["POST"]) -@_jwt.requires_auth -def post_bulk_users(): - """Admin users can post multiple users to his org.Use it for anonymous purpose only.""" - try: - request_json = request.get_json() - valid_format, errors = schema_utils.validate(request_json, "bulk_user") - if not valid_format: - return {"message": schema_utils.serialize(errors)}, HTTPStatus.BAD_REQUEST - - users = UserService.create_user_and_add_membership(request_json["users"], request_json["orgId"]) - is_any_error = any(user["http_status"] != 201 for user in users["users"]) - - response, status = users, HTTPStatus.MULTI_STATUS if is_any_error else HTTPStatus.OK - except BusinessException as exception: - response, status = {"code": exception.code, "message": exception.message}, exception.status_code - return response, status diff --git a/auth-api/src/auth_api/resources/v1/documents.py b/auth-api/src/auth_api/resources/v1/documents.py index 276dba5bf..20c267e37 100644 --- a/auth-api/src/auth_api/resources/v1/documents.py +++ b/auth-api/src/auth_api/resources/v1/documents.py @@ -24,7 +24,7 @@ from auth_api.services.google_store import GoogleStoreService from auth_api.utils.auth import jwt as _jwt from auth_api.utils.endpoints_enums import EndpointEnum -from auth_api.utils.enums import AccessType, DocumentType, LoginSource +from auth_api.utils.enums import DocumentType, LoginSource bp = Blueprint("DOCUMENTS", __name__, url_prefix=f"{EndpointEnum.API_V1.value}/documents") @@ -37,11 +37,7 @@ def get_document_by_type(document_type): try: if document_type == DocumentType.TERMS_OF_USE.value: token = g.jwt_oidc_token_info - if token.get("accessType", None) == AccessType.ANONYMOUS.value: - document_type = DocumentType.TERMS_OF_USE_DIRECTOR_SEARCH.value - elif ( - token.get("loginSource", None) == LoginSource.STAFF.value - ): # ideally for govm user who logs in with IDIR + if token.get("loginSource", None) == LoginSource.STAFF.value: # ideally for govm user who logs in with IDIR document_type = DocumentType.TERMS_OF_USE_GOVM.value doc = DocumentService.fetch_latest_document(document_type) diff --git a/auth-api/src/auth_api/resources/v1/invitation.py b/auth-api/src/auth_api/resources/v1/invitation.py index 3fdcc6330..cf0117fdd 100644 --- a/auth-api/src/auth_api/resources/v1/invitation.py +++ b/auth-api/src/auth_api/resources/v1/invitation.py @@ -126,7 +126,7 @@ def accept_invitation_token(invitation_token): response, status = ( InvitationService.accept_invitation(invitation_id, user, origin).as_dict(), HTTPStatus.OK, - ) # noqa:E127 + ) except BusinessException as exception: response, status = {"code": exception.code, "message": exception.message}, exception.status_code diff --git a/auth-api/src/auth_api/resources/v1/org.py b/auth-api/src/auth_api/resources/v1/org.py index e7d541a45..d02189ee2 100644 --- a/auth-api/src/auth_api/resources/v1/org.py +++ b/auth-api/src/auth_api/resources/v1/org.py @@ -42,7 +42,7 @@ from auth_api.services.flags import flags from auth_api.utils.auth import jwt as _jwt from auth_api.utils.endpoints_enums import EndpointEnum -from auth_api.utils.enums import AccessType, NotificationType, OrgStatus, OrgType, PatchActions, Status +from auth_api.utils.enums import NotificationType, OrgStatus, OrgType, PatchActions, Status from auth_api.utils.role_validator import validate_roles from auth_api.utils.roles import ( # noqa: I001 AFFILIATION_ALLOWED_ROLES, @@ -148,10 +148,7 @@ def search_simple_orgs(): @bp.route("", methods=["POST"]) @cross_origin(origins="*") -@validate_roles( - allowed_roles=[Role.PUBLIC_USER.value, Role.STAFF_CREATE_ACCOUNTS.value, Role.SYSTEM.value], - not_allowed_roles=[Role.ANONYMOUS_USER.value], -) +@validate_roles(allowed_roles=[Role.PUBLIC_USER.value, Role.STAFF_CREATE_ACCOUNTS.value, Role.SYSTEM.value]) @_jwt.has_one_of_roles([Role.PUBLIC_USER.value, Role.STAFF_CREATE_ACCOUNTS.value, Role.SYSTEM.value]) def post_organization(): """Post a new org using the request body. @@ -194,17 +191,10 @@ def put_organization(org_id): """Update the org specified by the provided id with the request body.""" request_json = request.get_json() valid_format, errors = schema_utils.validate(request_json, "org") - token_info = g.jwt_oidc_token_info if not valid_format: return {"message": schema_utils.serialize(errors)}, HTTPStatus.BAD_REQUEST try: org = OrgService.find_by_org_id(org_id, allowed_roles=(*CLIENT_ADMIN_ROLES, STAFF)) - if ( - org - and org.as_dict().get("accessType", None) == AccessType.ANONYMOUS.value - and Role.STAFF_CREATE_ACCOUNTS.value not in token_info.get("realm_access").get("roles") - ): - return {"message": "The organisation can only be updated by a staff admin."}, HTTPStatus.UNAUTHORIZED if org: response, status = org.update_org(org_info=request_json).as_dict(), HTTPStatus.OK else: diff --git a/auth-api/src/auth_api/resources/v1/user.py b/auth-api/src/auth_api/resources/v1/user.py index 501f6573e..eb8f88664 100644 --- a/auth-api/src/auth_api/resources/v1/user.py +++ b/auth-api/src/auth_api/resources/v1/user.py @@ -22,7 +22,6 @@ from auth_api.schemas import MembershipSchema, OrgSchema from auth_api.schemas import utils as schema_utils from auth_api.services import Affidavit as AffidavitService -from auth_api.services import Invitation as InvitationService from auth_api.services.authorization import Authorization as AuthorizationService from auth_api.services.keycloak import KeycloakService from auth_api.services.membership import Membership as MembershipService @@ -31,49 +30,12 @@ from auth_api.utils.auth import jwt as _jwt from auth_api.utils.constants import GROUP_GOV_ACCOUNT_USERS from auth_api.utils.endpoints_enums import EndpointEnum -from auth_api.utils.enums import LoginSource, Status +from auth_api.utils.enums import LoginSource from auth_api.utils.roles import Role bp = Blueprint("USERS", __name__, url_prefix=f"{EndpointEnum.API_V1.value}/users") -@bp.route("/bcros", methods=["POST", "OPTIONS"]) -@cross_origin(origins="*", methods=["POST"]) -def post_anonymous_user(): - """Post a new user using the request body who has a proper invitation.""" - try: - request_json = request.get_json() - invitation_token = request.headers.get("invitation_token", None) - invitation = InvitationService.validate_token(invitation_token).as_dict() - - valid_format, errors = schema_utils.validate(request_json, "anonymous_user") - if not valid_format: - return {"message": schema_utils.serialize(errors)}, HTTPStatus.BAD_REQUEST - - membership_details = { - "email": invitation["recipient_email"], - "membershipType": invitation["membership"][0]["membership_type"], - "update_password_on_login": False, - } - membership_details.update(request_json) - user = UserService.create_user_and_add_membership( - [membership_details], invitation["membership"][0]["org"]["id"], single_mode=True - ) - user_dict = user["users"][0] - if user_dict["http_status"] != HTTPStatus.CREATED: - response, status = ( - {"code": user_dict["http_status"], "message": user_dict["error"]}, - user_dict["http_status"], - ) - else: - InvitationService.accept_invitation(invitation["id"], None, None, False) - response, status = user, HTTPStatus.CREATED - - except BusinessException as exception: - response, status = {"code": exception.code, "message": exception.message}, exception.status_code - return response, status - - @bp.route("", methods=["GET", "OPTIONS"]) @cross_origin(origins="*", methods=["GET", "POST"]) @_jwt.has_one_of_roles([Role.STAFF_VIEW_ACCOUNTS.value]) @@ -121,11 +83,6 @@ def post_user(): # Add the user to public_users group if the user doesn't have public_user group if token.get("loginSource", "") != LoginSource.STAFF.value: KeycloakService.join_users_group() - # For anonymous users, there are no invitation process for members, - # so whenever they login perform this check and add them to corresponding groups - if token.get("loginSource", "") == LoginSource.BCROS.value: - if len(OrgService.get_orgs(user.identifier, [Status.ACTIVE.value])) > 0: - KeycloakService.join_account_holders_group() if user.type == Role.STAFF.name: MembershipService.add_staff_membership(user.identifier) else: @@ -176,52 +133,6 @@ def get_by_username(username): return response, status -@bp.route("/", methods=["DELETE"]) -@cross_origin(origins="*") -@_jwt.requires_auth -def delete_by_username(username): - """Delete the user profile associated with the provided username.""" - try: - user = UserService.find_by_username(username) - if user is None: - response, status = jsonify({"message": f"User {username} does not exist."}), HTTPStatus.NOT_FOUND - elif user.as_dict().get("type", None) != Role.ANONYMOUS_USER.name: - response, status = {"Normal users cant be deleted", HTTPStatus.NOT_IMPLEMENTED} - else: - UserService.delete_anonymous_user(username) - response, status = "", HTTPStatus.NO_CONTENT - except BusinessException as exception: - response, status = {"code": exception.code, "message": exception.message}, exception.status_code - return response, status - - -@bp.route("/", methods=["PATCH"]) -@cross_origin(origins="*") -@_jwt.requires_auth -def patch_by_username(username): - """Patch the user profile associated with the provided username. - - User only for patching the password. - """ - try: - request_json = request.get_json() - valid_format, errors = schema_utils.validate(request_json, "anonymous_user") - if not valid_format: - return {"message": schema_utils.serialize(errors)}, HTTPStatus.BAD_REQUEST - user = UserService.find_by_username(username) - - if user is None: - response, status = jsonify({"message": f"User {username} does not exist."}), HTTPStatus.NOT_FOUND - elif user.as_dict().get("type", None) != Role.ANONYMOUS_USER.name: - response, status = {"Normal users cant be patched", HTTPStatus.NOT_IMPLEMENTED} - else: - UserService.reset_password_for_anon_user(request_json, username) - response, status = "", HTTPStatus.NO_CONTENT - except BusinessException as exception: - response, status = {"code": exception.code, "message": exception.message}, exception.status_code - return response, status - - @bp.route("/@me", methods=["GET", "OPTIONS"]) @cross_origin(origins="*", methods=["GET", "PATCH", "DELETE"]) @_jwt.requires_auth diff --git a/auth-api/src/auth_api/resources/v2/org.py b/auth-api/src/auth_api/resources/v2/org.py index f2a3986e3..af76f3c9c 100644 --- a/auth-api/src/auth_api/resources/v2/org.py +++ b/auth-api/src/auth_api/resources/v2/org.py @@ -29,10 +29,7 @@ @bp.route("", methods=["POST"]) @cross_origin(origins="*") -@validate_roles( - allowed_roles=[Role.PUBLIC_USER.value, Role.STAFF_CREATE_ACCOUNTS.value, Role.SYSTEM.value], - not_allowed_roles=[Role.ANONYMOUS_USER.value], -) +@validate_roles(allowed_roles=[Role.PUBLIC_USER.value, Role.STAFF_CREATE_ACCOUNTS.value, Role.SYSTEM.value]) @_jwt.has_one_of_roles([Role.PUBLIC_USER.value, Role.STAFF_CREATE_ACCOUNTS.value, Role.SYSTEM.value]) def post_organization(): """Post a new org with contact using the request body. diff --git a/auth-api/src/auth_api/schemas/schemas/anonymous_user.json b/auth-api/src/auth_api/schemas/schemas/anonymous_user.json deleted file mode 100644 index 6f23a82a5..000000000 --- a/auth-api/src/auth_api/schemas/schemas/anonymous_user.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "definitions": {}, - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://bcrs.gov.bc.ca/.well_known/schemas/anonymous_user", - "type": "object", - "title": "Anonymous user", - "additionalProperties": false, - "required": [ - "username", - "password" - ], - "properties": { - "username": { - "$id": "#/properties/username", - "type": "string", - "title": "User Name", - "default": "", - "examples": [ - "Foobar" - ], - "pattern": "^[^\\s]+(\\s+[^\\s]+)*$" - }, - "password": { - "$id": "#/properties/password", - "type": "string", - "title": "password for the user", - "examples": [ - "Mysecretcode@1234" - ], - "pattern": "^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,})" - } - } - } - \ No newline at end of file diff --git a/auth-api/src/auth_api/schemas/schemas/anonymous_user_response.json b/auth-api/src/auth_api/schemas/schemas/anonymous_user_response.json deleted file mode 100644 index 6299ce9ed..000000000 --- a/auth-api/src/auth_api/schemas/schemas/anonymous_user_response.json +++ /dev/null @@ -1,185 +0,0 @@ -{ - "definitions": {}, - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://bcrs.gov.bc.ca/.well_known/schemas/anonymous_user_response", - "type": "object", - "title": "Response for bulk user", - "additionalProperties": true, - "required": [ - "users" - ], - "properties": { - "users": { - "$id": "#/properties/users", - "type": "array", - "title": "The Users Schema", - "description": "Contains a list of users", - "default": [], - "items": { - "$id": "#/properties/users/items", - "type": "object", - "title": "The users Schema", - "description": "A user item", - "default": {}, - "examples": [ - { - "httpStatus": 201, - "firstname": "firsst2usssaser2ss2dsd38", - "contacts": [], - "created": "2020-03-28T17:59:30.208643+00:00", - "modified": "2020-03-28T17:59:30.208666+00:00", - "type": "ANONYMOUS", - "username": "bcros/testuser", - "error": "", - "userTerms": { - "isTermsOfUseAccepted": false, - "termsOfUseAcceptedVersion": null - }, - "userStatus": 1.0 - }, - { - "created": "2020-03-28T17:59:31.528573+00:00", - "modified": "2020-03-28T17:59:31.528591+00:00", - "type": "ANONYMOUS", - "username": "bcros/testuser", - "userTerms": { - "isTermsOfUseAccepted": false, - "termsOfUseAcceptedVersion": null - }, - "error": "", - "userStatus": 1.0, - "httpStatus": 201, - "firstname": "testuser", - "contacts": [] - }, - { - "error": "The username is already taken", - "httpStatus": 409, - "username": "testuser" - } - ], - "required": [ - "error", - "httpStatus", - "username" - ], - "properties": { - "contacts": { - "$id": "#/properties/users/items/properties/contacts", - "type": "array", - "title": "The Contacts Schema", - "description": "list of contacts.", - "default": [] - }, - "created": { - "$id": "#/properties/users/items/properties/created", - "type": "string", - "title": "The Created Schema", - "default": "", - "examples": [ - "2020-03-28T17:59:30.208643+00:00" - ] - }, - "error": { - "$id": "#/properties/users/items/properties/error", - "type": "string", - "title": "The Error Schema", - "default": "", - "examples": [ - "" - ] - }, - "firstname": { - "$id": "#/properties/users/items/properties/firstname", - "type": "string", - "title": "The Firstname Schema", - "default": "", - "examples": [ - "firsst2usssaser2ss2dsd38" - ] - }, - "modified": { - "$id": "#/properties/users/items/properties/modified", - "type": "string", - "title": "The Modified Schema", - "default": "", - "examples": [ - "2020-03-28T17:59:30.208666+00:00" - ] - }, - "httpStatus": { - "$id": "#/properties/users/items/properties/httpStatus", - "type": "integer", - "title": "The Status Schema", - "default": 0, - "examples": [ - 201,409 - ] - }, - "type": { - "$id": "#/properties/users/items/properties/type", - "type": "string", - "title": "The Type Schema", - "default": "", - "examples": [ - "ANONYMOUS" - ] - }, - "userStatus": { - "$id": "#/properties/users/items/properties/userStatus", - "type": "integer", - "title": "The Userstatus Schema", - "default": 0, - "examples": [ - 1 - ] - }, - "userTerms": { - "$id": "#/properties/users/items/properties/userTerms", - "type": "object", - "title": "The Userterms Schema", - "default": {}, - "examples": [ - { - "isTermsOfUseAccepted": true, - "termsOfUseAcceptedVersion": 1 - } - ], - "required": [ - "isTermsOfUseAccepted", - "termsOfUseAcceptedVersion" - ], - "properties": { - "isTermsOfUseAccepted": { - "$id": "#/properties/users/items/properties/userTerms/properties/isTermsOfUseAccepted", - "type": "boolean", - "title": "The Istermsofuseaccepted Schema", - "default": false, - "examples": [ - true - ] - }, - "termsOfUseAcceptedVersion": { - "$id": "#/properties/users/items/properties/userTerms/properties/termsOfUseAcceptedVersion", - "title": "The Termsofuseacceptedversion Schema", - "default": null, - "examples": [ - 1 - ] - } - } - }, - "username": { - "$id": "#/properties/users/items/properties/username", - "type": "string", - "title": "The Username", - "default": "", - "examples": [ - "bcros/testuser" - ] - } - } - } - } - } -} \ No newline at end of file diff --git a/auth-api/src/auth_api/schemas/schemas/invitation.json b/auth-api/src/auth_api/schemas/schemas/invitation.json index a243fa9d0..7963c99fd 100644 --- a/auth-api/src/auth_api/schemas/schemas/invitation.json +++ b/auth-api/src/auth_api/schemas/schemas/invitation.json @@ -29,7 +29,8 @@ "type": "string", "title": "Type of Invitation.", "examples": [ - "DIRECTOR_SEARCH" + "STANDARD", + "GOVM" ] }, "sentDate": { @@ -48,4 +49,4 @@ "recipientEmail", "membership" ] -} \ No newline at end of file +} diff --git a/auth-api/src/auth_api/schemas/schemas/org.json b/auth-api/src/auth_api/schemas/schemas/org.json index 285649a61..3c384f2f0 100644 --- a/auth-api/src/auth_api/schemas/schemas/org.json +++ b/auth-api/src/auth_api/schemas/schemas/org.json @@ -52,7 +52,6 @@ "type": "string", "title": "Access Type", "examples": [ - "ANONYMOUS", "EXTRA_PROVINCIAL" ], "pattern": "^(.*)$" diff --git a/auth-api/src/auth_api/schemas/schemas/paged_response.json b/auth-api/src/auth_api/schemas/schemas/paged_response.json index 251b800e2..5822df254 100644 --- a/auth-api/src/auth_api/schemas/schemas/paged_response.json +++ b/auth-api/src/auth_api/schemas/schemas/paged_response.json @@ -23,7 +23,7 @@ "sentDate": "2020-11-23T11:49:44.148559+00:00", "status": "PENDING", "token": "eyJpZCI6MiwidHlwZSI6IkRJUkVDVE9SX1NFQVJDSCJ9.X7wSWA.8_WvWRuAUSnk-Jsj9it869UvL-M", - "type": "DIRECTOR_SEARCH" + "type": "STANDARD" } ], "loginOptions": [], @@ -51,7 +51,7 @@ "sentDate": "2020-11-23T11:49:43.152821+00:00", "status": "PENDING", "token": "eyJpZCI6MSwidHlwZSI6IkRJUkVDVE9SX1NFQVJDSCJ9.X7wSVw.pVZrsBz7EyKxaF3GtoZbbf7fLEY", - "type": "DIRECTOR_SEARCH" + "type": "STANDARD" } ], "loginOptions": [], @@ -116,4 +116,4 @@ } }, "additionalProperties": true -} \ No newline at end of file +} diff --git a/auth-api/src/auth_api/services/affiliation_invitation.py b/auth-api/src/auth_api/services/affiliation_invitation.py index cdb141c1d..f52880b3d 100644 --- a/auth-api/src/auth_api/services/affiliation_invitation.py +++ b/auth-api/src/auth_api/services/affiliation_invitation.py @@ -329,9 +329,7 @@ def create_affiliation_invitation( affiliation_invitation_info["entityId"] = entity.identifier - if from_org.access_type == AccessType.ANONYMOUS.value: # anonymous account never get bceid or bcsc choices - mandatory_login_source = LoginSource.BCROS.value - elif from_org.access_type == AccessType.GOVM.value: + if from_org.access_type == AccessType.GOVM.value: mandatory_login_source = LoginSource.STAFF.value else: default_login_option_based_on_accesstype = ( diff --git a/auth-api/src/auth_api/services/contact.py b/auth-api/src/auth_api/services/contact.py index 78b3a8941..c6523bff2 100644 --- a/auth-api/src/auth_api/services/contact.py +++ b/auth-api/src/auth_api/services/contact.py @@ -16,7 +16,7 @@ This module manages the Contact information for a user or entity. """ -from auth_api.schemas import ContactSchema, ContactSchemaPublic # noqa: I001, I003, I004 +from auth_api.schemas import ContactSchema, ContactSchemaPublic # noqa: I001 class Contact: diff --git a/auth-api/src/auth_api/services/flags.py b/auth-api/src/auth_api/services/flags.py index 23b647bf0..f40ffc358 100644 --- a/auth-api/src/auth_api/services/flags.py +++ b/auth-api/src/auth_api/services/flags.py @@ -73,8 +73,7 @@ def _get_client(self): return client - @staticmethod - def _get_anonymous_user(): + def _get_anonymous_user(self): return Context.create("anonymous") @staticmethod diff --git a/auth-api/src/auth_api/services/invitation.py b/auth-api/src/auth_api/services/invitation.py index 3ba6e3b4e..d9e532eec 100644 --- a/auth-api/src/auth_api/services/invitation.py +++ b/auth-api/src/auth_api/services/invitation.py @@ -99,9 +99,7 @@ def create_invitation(invitation_info: dict, user, invitation_origin, **kwargs): org_name = org.name invitation_type = Invitation._get_inv_type(org) - if org.access_type == AccessType.ANONYMOUS.value: # anonymous account never get bceid or bcsc choices - mandatory_login_source = LoginSource.BCROS.value - elif org.access_type == AccessType.GOVM.value: + if org.access_type == AccessType.GOVM.value: mandatory_login_source = LoginSource.STAFF.value else: default_login_option_based_on_accesstype = ( @@ -156,7 +154,6 @@ def _get_inv_type(org): """Return the correct invitation type.""" inv_types = { AccessType.GOVM.value: InvitationType.GOVM.value, - AccessType.ANONYMOUS.value: InvitationType.DIRECTOR_SEARCH.value, AccessType.REGULAR.value: InvitationType.STANDARD.value, } return inv_types.get(org.access_type, InvitationType.STANDARD.value) @@ -328,10 +325,6 @@ def _get_invitation_configs(org_name, login_source, org_status=None): "token_confirm_path": token_confirm_path, "notification_type": QueueMessageTypes.GOVM_MEMBER_INVITATION.value, } - director_search_configs = { - "token_confirm_path": token_confirm_path, - "notification_type": QueueMessageTypes.DIRSEARCH_BUSINESS_INVITATION.value, - } bceid_configs = { "token_confirm_path": token_confirm_path, "notification_type": QueueMessageTypes.BUSINESS_INVITATION_FOR_BCEID.value, @@ -341,7 +334,6 @@ def _get_invitation_configs(org_name, login_source, org_status=None): "notification_type": QueueMessageTypes.BUSINESS_INVITATION.value, } mail_configs = { - "BCROS": director_search_configs, "BCEID": bceid_configs, "IDIR": govm_member_configs, "IDIR/ACCOUNTSETUP": govm_setup_configs, diff --git a/auth-api/src/auth_api/services/keycloak.py b/auth-api/src/auth_api/services/keycloak.py index 5d3079706..7ceb8a0e7 100644 --- a/auth-api/src/auth_api/services/keycloak.py +++ b/auth-api/src/auth_api/services/keycloak.py @@ -16,7 +16,6 @@ import asyncio import json from dataclasses import dataclass -from string import Template import aiohttp import requests @@ -28,7 +27,6 @@ from auth_api.models.dataclass import KeycloakGroupSubscription from auth_api.utils.constants import ( GROUP_ACCOUNT_HOLDERS, - GROUP_ANONYMOUS_USERS, GROUP_GOV_ACCOUNT_USERS, GROUP_PUBLIC_USERS, ) @@ -36,8 +34,6 @@ from auth_api.utils.roles import Role from auth_api.utils.user_context import UserContext, user_context -from .keycloak_user import KeycloakUser - @dataclass class KCRequestConfig: @@ -52,91 +48,12 @@ class KeycloakService: """For Keycloak services.""" @staticmethod - def add_user(user: KeycloakUser, return_if_exists: bool = False, throw_error_if_exists: bool = False): - """Add user to Keycloak.""" - config = current_app.config - # Add user and set password - admin_token = KeycloakService._get_admin_token(upstream=True) - - base_url = config.get("KEYCLOAK_BCROS_BASE_URL") - realm = config.get("KEYCLOAK_BCROS_REALMNAME") - timeout = config.get("CONNECT_TIMEOUT", 60) - - # Check if the user exists - if return_if_exists or throw_error_if_exists: - existing_user = KeycloakService.get_user_by_username(user.user_name, admin_token=admin_token) - if existing_user: - if not throw_error_if_exists: - return existing_user - raise BusinessException(Error.USER_ALREADY_EXISTS_IN_KEYCLOAK, None) - # Add user to the keycloak group '$group_name' - headers = {"Content-Type": ContentType.JSON.value, "Authorization": f"Bearer {admin_token}"} - - add_user_url = f"{base_url}/auth/admin/realms/{realm}/users" - response = requests.post(add_user_url, data=user.value(), headers=headers, timeout=timeout) - response.raise_for_status() - - return KeycloakService.get_user_by_username(user.user_name, admin_token) - - @staticmethod - def update_user(user: KeycloakUser): - """Add user to Keycloak.""" - config = current_app.config - # Add user and set password - admin_token = KeycloakService._get_admin_token(upstream=True) - - base_url = config.get("KEYCLOAK_BCROS_BASE_URL") - realm = config.get("KEYCLOAK_BCROS_REALMNAME") - timeout = current_app.config.get("CONNECT_TIMEOUT", 60) - - existing_user = KeycloakService.get_user_by_username(user.user_name, admin_token=admin_token) - if not existing_user: - raise BusinessException(Error.DATA_NOT_FOUND, None) - headers = {"Content-Type": ContentType.JSON.value, "Authorization": f"Bearer {admin_token}"} - - update_user_url = f"{base_url}/auth/admin/realms/{realm}/users/{existing_user.id}" - response = requests.put(update_user_url, data=user.value(), headers=headers, timeout=timeout) - response.raise_for_status() - - return KeycloakService.get_user_by_username(user.user_name, admin_token) - - @staticmethod - def get_user_by_username(username: str, admin_token=None) -> KeycloakUser: + def get_user_groups(user_id): """Get user from Keycloak by username.""" - user = None - base_url = current_app.config.get("KEYCLOAK_BCROS_BASE_URL") - realm = current_app.config.get("KEYCLOAK_BCROS_REALMNAME") + base_url = current_app.config.get("KEYCLOAK_BASE_URL") + realm = current_app.config.get("KEYCLOAK_REALMNAME") timeout = current_app.config.get("CONNECT_TIMEOUT", 60) - if not admin_token: - admin_token = KeycloakService._get_admin_token(upstream=True) - - headers = {"Content-Type": ContentType.JSON.value, "Authorization": f"Bearer {admin_token}"} - - # Get the user and return - query_user_url = Template(f"{base_url}/auth/admin/realms/{realm}/users?username=$username").substitute( - username=username - ) - response = requests.get(query_user_url, headers=headers, timeout=timeout) - response.raise_for_status() - if len(response.json()) == 1: - user = KeycloakUser(response.json()[0]) - return user - - @staticmethod - def get_user_groups(user_id, upstream: bool = False) -> KeycloakUser: - """Get user from Keycloak by username.""" - base_url = ( - current_app.config.get("KEYCLOAK_BCROS_BASE_URL") - if upstream - else current_app.config.get("KEYCLOAK_BASE_URL") - ) - realm = ( - current_app.config.get("KEYCLOAK_BCROS_REALMNAME") - if upstream - else current_app.config.get("KEYCLOAK_REALMNAME") - ) - timeout = current_app.config.get("CONNECT_TIMEOUT", 60) - admin_token = KeycloakService._get_admin_token(upstream=upstream) + admin_token = KeycloakService._get_admin_token() headers = {"Content-Type": ContentType.JSON.value, "Authorization": f"Bearer {admin_token}"} # Get the user and return @@ -145,25 +62,6 @@ def get_user_groups(user_id, upstream: bool = False) -> KeycloakUser: response.raise_for_status() return response.json() - @staticmethod - def delete_user_by_username(username): - """Delete user from Keycloak by username.""" - admin_token = KeycloakService._get_admin_token(upstream=True) - headers = {"Content-Type": ContentType.JSON.value, "Authorization": f"Bearer {admin_token}"} - - base_url = current_app.config.get("KEYCLOAK_BCROS_BASE_URL") - realm = current_app.config.get("KEYCLOAK_BCROS_REALMNAME") - timeout = current_app.config.get("CONNECT_TIMEOUT", 60) - user = KeycloakService.get_user_by_username(username) - - if not user: - raise BusinessException(Error.DATA_NOT_FOUND, None) - - # Delete the user - delete_user_url = f"{base_url}/auth/admin/realms/{realm}/users/{user.id}" - response = requests.delete(delete_user_url, headers=headers, timeout=timeout) - response.raise_for_status() - @staticmethod def get_token(username, password): """Get user access token by username and password.""" @@ -189,7 +87,7 @@ def get_token(username, password): @staticmethod @user_context def join_users_group(**kwargs) -> str: - """Add user to the group (public_users or anonymous_users) if the user is public.""" + """Add user to the group (public_users if the user is public).""" user_from_context: UserContext = kwargs["user_context"] group_name: str = None login_source = user_from_context.login_source @@ -204,8 +102,6 @@ def join_users_group(**kwargs) -> str: and Role.STAFF.value not in roles ): group_name = GROUP_GOV_ACCOUNT_USERS - elif login_source == LoginSource.BCROS.value and Role.ANONYMOUS_USER.value not in roles: - group_name = GROUP_ANONYMOUS_USERS if group_name: KeycloakService.add_user_to_group(user_from_context.sub, group_name) @@ -291,7 +187,7 @@ async def add_or_remove_users_from_group(kgs: list[KeycloakGroupSubscription]): attempts=3, start_timeout=1, statuses={429, 500, 502, 503, 504}, - exceptions={TimeoutError, aiohttp.ClientConnectionError} + exceptions={TimeoutError, aiohttp.ClientConnectionError}, ) async with RetryClient(connector=connector, retry_options=retry_options) as session: tasks = [ @@ -299,7 +195,7 @@ async def add_or_remove_users_from_group(kgs: list[KeycloakGroupSubscription]): method, f"{base_url}/auth/admin/realms/{realm}/users/{kg.user_guid}/groups/{group_ids[kg.group_name]}", headers=headers, - timeout=timeout + timeout=timeout, ) for kg in kgs ] @@ -429,15 +325,13 @@ def remove_user_from_group(user_id: str, group_name: str): response.raise_for_status() @staticmethod - def _get_admin_token(upstream: bool = False): + def _get_admin_token(): """Create an admin token.""" config = current_app.config - base_url = config.get("KEYCLOAK_BCROS_BASE_URL") if upstream else config.get("KEYCLOAK_BASE_URL") - realm = config.get("KEYCLOAK_BCROS_REALMNAME") if upstream else config.get("KEYCLOAK_REALMNAME") - admin_client_id = ( - config.get("KEYCLOAK_BCROS_ADMIN_CLIENTID") if upstream else config.get("KEYCLOAK_ADMIN_USERNAME") - ) - admin_secret = config.get("KEYCLOAK_BCROS_ADMIN_SECRET") if upstream else config.get("KEYCLOAK_ADMIN_SECRET") + base_url = config.get("KEYCLOAK_BASE_URL") + realm = config.get("KEYCLOAK_REALMNAME") + admin_client_id = config.get("KEYCLOAK_ADMIN_USERNAME") + admin_secret = config.get("KEYCLOAK_ADMIN_SECRET") timeout = config.get("CONNECT_TIMEOUT", 60) headers = {"Content-Type": "application/x-www-form-urlencoded"} token_url = f"{base_url}/auth/realms/{realm}/protocol/openid-connect/token" diff --git a/auth-api/src/auth_api/services/membership.py b/auth-api/src/auth_api/services/membership.py index a9c57170e..f03576cc1 100644 --- a/auth-api/src/auth_api/services/membership.py +++ b/auth-api/src/auth_api/services/membership.py @@ -284,14 +284,13 @@ def update_membership(self, updated_fields, **kwargs): # Add to account_holders group in keycloak Membership._add_or_remove_group(self._model) - is_bcros_user = self._model.user.login_source == LoginSource.BCROS.value - # send mail if staff modifies , not applicable for bcros , only if anything is getting updated - if user_from_context.is_staff() and not is_bcros_user and len(updated_fields) != 0: + # send mail if staff modifies , only if anything is getting updated + if user_from_context.is_staff() and len(updated_fields) != 0: data = {"accountId": self._model.org.id} publish_to_mailer(notification_type=QueueMessageTypes.TEAM_MODIFIED.value, data=data) # send mail to the person itself who is getting removed by staff ;if he is admin and has an email on record - if user_from_context.is_staff() and not is_bcros_user and admin_getting_removed: + if user_from_context.is_staff() and admin_getting_removed: contact_link = ContactLinkModel.find_by_user_id(self._model.user.id) if contact_link and contact_link.contact.email: data = {"accountId": self._model.org.id, "recipientEmail": contact_link.contact.email} diff --git a/auth-api/src/auth_api/services/org.py b/auth-api/src/auth_api/services/org.py index 75c210cb5..a775fa391 100644 --- a/auth-api/src/auth_api/services/org.py +++ b/auth-api/src/auth_api/services/org.py @@ -139,7 +139,7 @@ def create_org(org_info: dict, user_id): Org.add_contact_to_org(mailing_address, org) # create the membership record for this user if its not created by staff and access_type is anonymous - Org.create_membership(access_type, org, user_id) + Org.create_membership(org, user_id) if product_subscriptions is not None: ProductService.create_product_subscription( @@ -202,10 +202,10 @@ def _create_staff_review_task(org: OrgModel, user: UserModel): @staticmethod @user_context - def create_membership(access_type, org, user_id, **kwargs): + def create_membership(org, user_id, **kwargs): """Create membership account.""" user: UserContext = kwargs["user_context"] - if not user.is_staff_admin() and access_type != AccessType.ANONYMOUS.value: + if not user.is_staff_admin(): membership = MembershipModel( org_id=org.id, user_id=user_id, membership_type_code="ADMIN", membership_type_status=Status.ACTIVE.value ) @@ -903,10 +903,7 @@ def refine_access_type(access_types, **kwargs): is_staff_admin = Role.STAFF_CREATE_ACCOUNTS.value in roles or Role.STAFF_MANAGE_ACCOUNTS.value in roles if not is_staff_admin: if len(access_types) < 1: - # pass everything except DIRECTOR SEARCH - access_types = [item.value for item in AccessType if item != AccessType.ANONYMOUS] - else: - access_types.remove(AccessType.ANONYMOUS.value) + access_types = [item.value for item in AccessType] return access_types, is_staff_admin @staticmethod diff --git a/auth-api/src/auth_api/services/products.py b/auth-api/src/auth_api/services/products.py index de970160d..c25528b56 100644 --- a/auth-api/src/auth_api/services/products.py +++ b/auth-api/src/auth_api/services/products.py @@ -254,7 +254,10 @@ def create_product_subscription( ) Product._send_product_subscription_confirmation( ProductNotificationInfo( - product_model=product_model, product_sub_model=product_subscription, is_confirmation=True, org_id=org.id + product_model=product_model, + product_sub_model=product_subscription, + is_confirmation=True, + org_id=org.id, ), org.id, ) @@ -511,7 +514,7 @@ def update_product_subscription(product_sub_info: ProductSubscriptionInfo, is_ne is_reapproved=is_reapproved, remarks=product_sub_info.task_remarks, org_id=org_id, - org_name=org_name + org_name=org_name, ) ) @@ -575,7 +578,7 @@ def approve_reject_parent_subscription( product_model=product_model, product_sub_model=product_subscription, is_reapproved=is_reapproved, - org_id=org_id + org_id=org_id, ) ) else: diff --git a/auth-api/src/auth_api/services/user.py b/auth-api/src/auth_api/services/user.py index de2f1510e..4d668e49a 100644 --- a/auth-api/src/auth_api/services/user.py +++ b/auth-api/src/auth_api/services/user.py @@ -16,9 +16,6 @@ This module manages the User Information. """ -import json -from http import HTTPStatus - from flask import current_app from jinja2 import Environment, FileSystemLoader from requests import HTTPError @@ -31,18 +28,14 @@ from auth_api.models import Membership as MembershipModel from auth_api.models import Org as OrgModel from auth_api.models import User as UserModel -from auth_api.models import db from auth_api.models.dataclass import Activity from auth_api.schemas import UserSchema from auth_api.services.authorization import check_auth -from auth_api.services.keycloak_user import KeycloakUser from auth_api.utils import util from auth_api.utils.account_mailer import publish_to_mailer from auth_api.utils.enums import ( - AccessType, ActivityAction, DocumentType, - IdpHint, LoginSource, OrgStatus, Status, @@ -101,139 +94,6 @@ def as_dict(self): obj = user_schema.dump(self._model, many=False) return obj - @staticmethod - def create_user_and_add_membership( - memberships: list[dict], - org_id, - # pylint: disable=too-many-locals, too-many-statements, too-many-branches - single_mode: bool = False, - ): - """Create user(s) in the DB and upstream keycloak. - - accepts a list of memberships ie.a list of objects with username,password and membershipTpe - single_mode can be used if called method already perfomed the authenticaiton - single_mode= true is used now incase of invitation for admin users scenarion - other cases should be invoked with single_mode=false - """ - User._validate_and_throw_exception(memberships, org_id, single_mode) - - current_app.logger.debug("create_user") - users = [] - for membership in memberships: - username = membership["username"] - current_app.logger.debug(f"create user username: {username}") - create_user_request = User._create_kc_user(membership) - db_username = IdpHint.BCROS.value + "/" + username - user_model = UserModel.find_by_username(db_username) - re_enable_user = False - existing_kc_user = KeycloakService.get_user_by_username(username) - enabled_in_kc = getattr(existing_kc_user, "enabled", True) - if getattr(user_model, "status", None) == Status.INACTIVE.value and not enabled_in_kc: - membership_model = MembershipModel.find_membership_by_userid(user_model.id) - re_enable_user = membership_model.org_id == int(org_id or -1) - if user_model and not re_enable_user: - current_app.logger.debug("Existing users found in DB") - users.append(User._get_error_dict(username, Error.USER_ALREADY_EXISTS)) - continue - - if membership.get("update_password_on_login", True): # by default , reset needed - create_user_request.update_password_on_login() - try: - if re_enable_user: - kc_user = KeycloakService.update_user(create_user_request) - else: - kc_user = KeycloakService.add_user(create_user_request, throw_error_if_exists=True) - except BusinessException as err: - error_msg = f"create_user in keycloak failed :duplicate user {err}" - current_app.logger.error(error_msg) - users.append(User._get_error_dict(username, Error.USER_ALREADY_EXISTS)) - continue - except HTTPError as err: - error_msg = f"create_user in keycloak failed {err}" - current_app.logger.error(error_msg) - users.append(User._get_error_dict(username, Error.FAILED_ADDING_USER_ERROR)) - continue - try: - if re_enable_user: - user_model.status = Status.ACTIVE.value - user_model.type = Role.ANONYMOUS_USER.name - user_model.login_source = LoginSource.BCROS.value - user_model.flush() - membership_model.status = Status.ACTIVE.value - membership_model.membership_type_code = membership["membershipType"] - membership_model.flush() - else: - user_model = User._create_new_user_and_membership(db_username, kc_user, membership, org_id) - - db.session.commit() # commit is for session ;need not to invoke for every object - user_dict = User(user_model).as_dict() - user_dict.update({"http_status": HTTPStatus.CREATED, "error": ""}) - users.append(user_dict) - except Exception as e: # NOQA # pylint: disable=broad-except - error_msg = f"Error on create_user_and_add_membership {e}" - current_app.logger.error(error_msg) - db.session.rollback() - if re_enable_user: - User._update_user_in_kc(create_user_request) - else: - KeycloakService.delete_user_by_username(create_user_request.user_name) - users.append(User._get_error_dict(username, Error.FAILED_ADDING_USER_ERROR)) - continue - - return {"users": users} - - @staticmethod - def _update_user_in_kc(create_user_request): - update_user_request = KeycloakUser() - update_user_request.user_name = create_user_request.user_name - update_user_request.enabled = False - KeycloakService.update_user(update_user_request) - - @staticmethod - def _validate_and_throw_exception(memberships, org_id, single_mode): - if single_mode: # make sure no bulk operation and only owner is created using if no auth - if len(memberships) > 1 or memberships[0].get("membershipType") not in [ADMIN, COORDINATOR]: - raise BusinessException(Error.INVALID_USER_CREDENTIALS, None) - else: - check_auth(org_id=org_id, one_of_roles=(COORDINATOR, ADMIN, STAFF)) - # check if anonymous org ;these actions cannot be performed on normal orgs - org = OrgModel.find_by_org_id(org_id) - if not org or org.access_type != AccessType.ANONYMOUS.value: - raise BusinessException(Error.INVALID_INPUT, None) - - @staticmethod - def _create_new_user_and_membership(db_username, kc_user, membership, org_id): - user_model: UserModel = UserModel( - username=db_username, - is_terms_of_use_accepted=False, - status=Status.ACTIVE.value, - type=Role.ANONYMOUS_USER.name, - email=membership.get("email", None), - firstname=kc_user.first_name, - lastname=kc_user.last_name, - login_source=LoginSource.BCROS.value, - ) - user_model.flush() - membership_model = MembershipModel( - org_id=org_id, - user_id=user_model.id, - membership_type_code=membership["membershipType"], - membership_type_status=Status.ACTIVE.value, - ) - - membership_model.flush() - name = {"first_name": user_model.firstname, "last_name": user_model.lastname} - ActivityLogPublisher.publish_activity( - Activity( - org_id, - ActivityAction.APPROVE_TEAM_MEMBER.value, - name=json.dumps(name), - value=membership["membershipType"], - id=user_model.id, - ) - ) - return user_model - @staticmethod @user_context def delete_otp_for_user(user_name, org_id, origin_url: str = None, **kwargs): @@ -268,92 +128,6 @@ def send_otp_authenticator_reset_notification(recipient_email, origin_url, org_i current_app.logger.error(" ValidatorResponse: error = None validator_response = ValidatorResponse() if access_type: - if not user.is_staff_admin() and access_type in AccessType.ANONYMOUS.value: - error = Error.USER_CANT_CREATE_ANONYMOUS_ORG if not user.is_staff_admin() and access_type in AccessType.GOVM.value: error = Error.USER_CANT_CREATE_GOVM_ORG if not user.is_bceid_user() and access_type in ( diff --git a/auth-api/src/auth_api/utils/constants.py b/auth-api/src/auth_api/utils/constants.py index 51968c5a0..b0da06986 100644 --- a/auth-api/src/auth_api/utils/constants.py +++ b/auth-api/src/auth_api/utils/constants.py @@ -16,7 +16,6 @@ # Group names GROUP_PUBLIC_USERS = "public_users" GROUP_ACCOUNT_HOLDERS = "account_holders" -GROUP_ANONYMOUS_USERS = "anonymous_users" GROUP_GOV_ACCOUNT_USERS = "gov_account_users" GROUP_API_GW_USERS = "api_gateway_users" GROUP_API_GW_SANDBOX_USERS = "api_gateway_sandbox_users" diff --git a/auth-api/src/auth_api/utils/enums.py b/auth-api/src/auth_api/utils/enums.py index 019e9389b..5fbdb70d6 100644 --- a/auth-api/src/auth_api/utils/enums.py +++ b/auth-api/src/auth_api/utils/enums.py @@ -111,7 +111,6 @@ class DocumentType(Enum): """Document types.""" TERMS_OF_USE = "termsofuse" - TERMS_OF_USE_DIRECTOR_SEARCH = "termsofuse_directorsearch" TERMS_OF_USE_GOVM = "termsofuse_govm" AFFIDAVIT = "affidavit" TERMS_OF_USE_PAD = "termsofuse_pad" @@ -149,7 +148,6 @@ class AccessType(Enum): REGULAR = "REGULAR" REGULAR_BCEID = "REGULAR_BCEID" EXTRA_PROVINCIAL = "EXTRA_PROVINCIAL" - ANONYMOUS = "ANONYMOUS" GOVM = "GOVM" # for govt ministry GOVN = "GOVN" # for govt non-ministry @@ -208,8 +206,7 @@ class SuspensionReasonCode(Enum): class InvitationType(Enum): """Invitation type.""" - GOVM = "GOVM" # Used to indicate an anonymous account invitation - DIRECTOR_SEARCH = "DIRECTOR_SEARCH" # Used to indicate an anonymous account invitation + GOVM = "GOVM" STANDARD = "STANDARD" # Used to indicate the standard email invite with admin approval @@ -238,7 +235,6 @@ def from_value(cls, value): class IdpHint(Enum): """IdpHint for user login.""" - BCROS = "bcros" BCEID = "bceid" @@ -258,7 +254,6 @@ class LoginSource(Enum): BCSC = "BCSC" BCEID = "BCEID" STAFF = "IDIR" - BCROS = "BCROS" API_GW = "API_GW" IDIR = "IDIR" diff --git a/auth-api/src/auth_api/utils/notifications.py b/auth-api/src/auth_api/utils/notifications.py index b46143d75..c8852ef53 100644 --- a/auth-api/src/auth_api/utils/notifications.py +++ b/auth-api/src/auth_api/utils/notifications.py @@ -132,7 +132,9 @@ def get_product_notification_data(product_notification_info: ProductNotification return None -def get_default_product_notification_data(product_model: ProductCodeModel, recipient_emails: str, org_id: int = None, org_name: str = None): +def get_default_product_notification_data( + product_model: ProductCodeModel, recipient_emails: str, org_id: int = None, org_name: str = None +): """Get the default product notification data.""" data = { "productName": product_model.description, @@ -143,7 +145,9 @@ def get_default_product_notification_data(product_model: ProductCodeModel, recip return data -def get_mhr_qs_approval_data(product_model: ProductCodeModel, recipient_emails: str, org_id: int, is_reapproved: bool = False): +def get_mhr_qs_approval_data( + product_model: ProductCodeModel, recipient_emails: str, org_id: int, is_reapproved: bool = False +): """Get the mhr qualified supplier product approval notification data.""" data = { "subjectDescriptor": ProductSubjectDescriptor.MHR_QUALIFIED_SUPPLIER.value, @@ -152,12 +156,14 @@ def get_mhr_qs_approval_data(product_model: ProductCodeModel, recipient_emails: "isReapproved": is_reapproved, "productName": product_model.description, "emailAddresses": recipient_emails, - "accountId": org_id + "accountId": org_id, } return data -def get_mhr_qs_rejected_data(product_model: ProductCodeModel, recipient_emails: str, org_id: int, reject_reason: str = None): +def get_mhr_qs_rejected_data( + product_model: ProductCodeModel, recipient_emails: str, org_id: int, reject_reason: str = None +): """Get the mhr qualified supplier product rejected notification data.""" data = { "subjectDescriptor": ProductSubjectDescriptor.MHR_QUALIFIED_SUPPLIER.value, @@ -168,7 +174,7 @@ def get_mhr_qs_rejected_data(product_model: ProductCodeModel, recipient_emails: "emailAddresses": recipient_emails, "remarks": reject_reason, "contactType": get_notification_contact_type(product_model.code), - "accountId": org_id + "accountId": org_id, } return data @@ -184,7 +190,7 @@ def get_mhr_qs_confirmation_data(product_model: ProductCodeModel, recipient_emai "contactType": get_notification_contact_type(product_model.code), "hasAgreementAttachment": True, "attachmentType": NotificationAttachmentType.MHR_QS.value, - "accountId": org_id + "accountId": org_id, } return data diff --git a/auth-api/src/auth_api/utils/roles.py b/auth-api/src/auth_api/utils/roles.py index 13d0e760f..01771ce12 100644 --- a/auth-api/src/auth_api/utils/roles.py +++ b/auth-api/src/auth_api/utils/roles.py @@ -26,7 +26,6 @@ class Role(Enum): PUBLIC_USER = "public_user" ACCOUNT_HOLDER = "account_holder" GOV_ACCOUNT_USER = "gov_account_user" - ANONYMOUS_USER = "anonymous_user" ACCOUNT_IDENTITY = "account_identity" MANAGE_EFT = "manage_eft" CHANGE_ADDRESS = "change_address" diff --git a/auth-api/src/auth_api/utils/user_context.py b/auth-api/src/auth_api/utils/user_context.py index 3134cdc59..e3c5800a4 100644 --- a/auth-api/src/auth_api/utils/user_context.py +++ b/auth-api/src/auth_api/utils/user_context.py @@ -122,10 +122,7 @@ def account_id(self) -> dict: if not account_id: account_id = request.headers["Account-Id"] if request and "Account-Id" in request.headers else None if account_id == "undefined": - abort( - HTTPStatus.BAD_REQUEST, - description="Account-Id header contains invalid value 'undefined'" - ) + abort(HTTPStatus.BAD_REQUEST, description="Account-Id header contains invalid value 'undefined'") return account_id @property diff --git a/auth-api/tests/docker/setup/demo-realm.json b/auth-api/tests/docker/setup/demo-realm.json index 3aabb6c91..b874ce2a2 100755 --- a/auth-api/tests/docker/setup/demo-realm.json +++ b/auth-api/tests/docker/setup/demo-realm.json @@ -72,12 +72,6 @@ "clientRole": false, "containerId": "demo" }, - { - "name": "anonymous_user", - "composite": false, - "clientRole": false, - "containerId": "demo" - }, { "name": "account_holder", "composite": false, @@ -358,17 +352,6 @@ "clientRoles": {}, "subGroups": [] }, - { - "name": "anonymous_users", - "path": "/anonymous_users", - "attributes": {}, - "realmRoles": [ - "edit", - "anonymous_user" - ], - "clientRoles": {}, - "subGroups": [] - }, { "name": "ppr", "path": "/ppr", diff --git a/auth-api/tests/unit/api/test_bulk_user.py b/auth-api/tests/unit/api/test_bulk_user.py deleted file mode 100644 index 37a69f2e0..000000000 --- a/auth-api/tests/unit/api/test_bulk_user.py +++ /dev/null @@ -1,120 +0,0 @@ -# Copyright © 2019 Province of British Columbia -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Tests to assure the users API end-point. - -Test-Suite to ensure that the /users endpoint is working as expected. -""" - -import json -import uuid -from http import HTTPStatus -from random import randint - -from auth_api.config import get_named_config -from auth_api.schemas import utils as schema_utils -from auth_api.services.keycloak import KeycloakService -from auth_api.utils.enums import IdpHint, ProductCode -from tests.utilities.factory_scenarios import BulkUserTestScenario, TestJwtClaims, TestOrgInfo -from tests.utilities.factory_utils import factory_auth_header, factory_invitation_anonymous - -KEYCLOAK_SERVICE = KeycloakService() - -CONFIG = get_named_config("testing") - - -def test_add_user(client, jwt, session): # pylint:disable=unused-argument - """Assert that a user can be POSTed.""" - headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.public_user_role) - rv = client.post("/api/v1/users", headers=headers, content_type="application/json") - assert rv.status_code == HTTPStatus.CREATED - assert schema_utils.validate(rv.json, "user_response")[0] - - -def test_add_user_admin_valid_bcros(client, jwt, session, keycloak_mock): # pylint:disable=unused-argument - """Assert that an org admin can create members.""" - headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_admin_dir_search_role) - rv = client.post("/api/v1/users", headers=headers, content_type="application/json") - - rv = client.post( - "/api/v1/orgs", data=json.dumps(TestOrgInfo.org_anonymous), headers=headers, content_type="application/json" - ) - - dictionary = json.loads(rv.data) - org_id = dictionary["id"] - rv = client.post( - "/api/v1/invitations", - data=json.dumps(factory_invitation_anonymous(org_id=org_id)), - headers=headers, - content_type="application/json", - ) - - dictionary = json.loads(rv.data) - assert dictionary.get("token") is not None - assert rv.status_code == HTTPStatus.CREATED - - user = { - "username": f"testuser{randint(0, 1000)}", - "password": "Password@1234", - } - rv = client.post( - "/api/v1/users/bcros", - data=json.dumps(user), - headers={"invitation_token": dictionary.get("token")}, - content_type="application/json", - ) - - # Login as this user - invited_user_token = { - "iss": CONFIG.JWT_OIDC_TEST_ISSUER, - "sub": str(uuid.uuid4()), - "firstname": "Test", - "lastname": "User", - "preferred_username": "bcros/{}".format(user.get("username")), - "realm_access": {"roles": []}, - "roles": [], - "accessType": "ANONYMOUS", - "product_code": ProductCode.DIR_SEARCH.value, - } - headers = factory_auth_header(jwt=jwt, claims=invited_user_token) - - rv = client.post("/api/v1/users", headers=headers, content_type="application/json") - assert rv.status_code == HTTPStatus.CREATED - - # headers = factory_auth_header(jwt=jwt, - # claims=TestJwtClaims.anonymous_bcros_role) - user_input = BulkUserTestScenario.get_bulk_user1_for_org(org_id) - rv = client.post( - "/api/v1/bulk/users", headers=headers, data=json.dumps(user_input), content_type="application/json" - ) - assert len(rv.json["users"]) == 2 - assert schema_utils.validate(rv.json, "anonymous_user_response")[0] - - assert rv.json["users"][0]["httpStatus"] == 201 - assert rv.json["users"][0]["httpStatus"] == 201 - assert rv.json["users"][0]["error"] == "" - assert rv.json["users"][1]["error"] == "" - assert rv.json["users"][0]["username"] == IdpHint.BCROS.value + "/" + user_input["users"][0]["username"] - assert rv.json["users"][1]["username"] == IdpHint.BCROS.value + "/" + user_input["users"][1]["username"] - - rv = client.post( - "/api/v1/bulk/users", headers=headers, data=json.dumps(user_input), content_type="application/json" - ) - - assert len(rv.json["users"]) == 2 - assert schema_utils.validate(rv.json, "anonymous_user_response")[0] - assert rv.json["users"][0]["httpStatus"] == 409 - assert rv.json["users"][1]["httpStatus"] == 409 - assert rv.json["users"][0]["error"] == "The username is already taken" - assert rv.json["users"][1]["error"] == "The username is already taken" diff --git a/auth-api/tests/unit/api/test_cors_preflight.py b/auth-api/tests/unit/api/test_cors_preflight.py index 97b0db359..b992f144a 100644 --- a/auth-api/tests/unit/api/test_cors_preflight.py +++ b/auth-api/tests/unit/api/test_cors_preflight.py @@ -64,13 +64,6 @@ def test_preflight_bcol_profiles(app, client, jwt, session): assert_access_control_headers(rv, "*", "POST") -def test_preflight_bulk_users(app, client, jwt, session): - """Assert preflight responses for bcol profiles are correct.""" - rv = client.options("/api/v1/bulk/users", headers={"Access-Control-Request-Method": "POST"}) - assert rv.status_code == HTTPStatus.OK - assert_access_control_headers(rv, "*", "POST") - - def test_preflight_codes(app, client, jwt, session): """Assert preflight responses for codes are correct.""" rv = client.options("/api/v1/codes/CODETYPE", headers={"Access-Control-Request-Method": "GET"}) @@ -255,10 +248,6 @@ def test_preflight_user(app, client, jwt, session): assert rv.status_code == HTTPStatus.OK assert_access_control_headers(rv, "*", "GET, POST") - rv = client.options("/api/v1/users/bcros", headers={"Access-Control-Request-Method": "POST"}) - assert rv.status_code == HTTPStatus.OK - assert_access_control_headers(rv, "*", "POST") - rv = client.options("/api/v1/users/USERNAME/otp/1", headers={"Access-Control-Request-Method": "DELETE"}) assert rv.status_code == HTTPStatus.OK assert_access_control_headers(rv, "*", "DELETE") diff --git a/auth-api/tests/unit/api/test_documents.py b/auth-api/tests/unit/api/test_documents.py index 1ee12fd53..833d7cdce 100644 --- a/auth-api/tests/unit/api/test_documents.py +++ b/auth-api/tests/unit/api/test_documents.py @@ -42,18 +42,6 @@ def test_documents_returns_200(client, jwt, session): # pylint:disable=unused-a assert rv.status_code == HTTPStatus.OK assert rv.json.get("versionId") == get_tos_pad_latest_version() - headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.anonymous_bcros_role) - rv = client.get("/api/v1/documents/termsofuse", headers=headers, content_type="application/json") - - assert rv.status_code == HTTPStatus.OK - assert schema_utils.validate(rv.json, "document")[0] - assert rv.json.get("versionId") == "d1" - - rv = client.get("/api/v1/documents/termsofuse_pad", headers=headers, content_type="application/json") - - assert rv.status_code == HTTPStatus.OK - assert rv.json.get("versionId") == get_tos_pad_latest_version() - def test_invalid_documents_returns_404(client, jwt, session): # pylint:disable=unused-argument """Assert get documents endpoint returns 404.""" @@ -99,20 +87,6 @@ def test_documents_returns_latest_always(client, jwt, session): # pylint:disabl assert rv.json.get("content") == html_content_2 assert rv.json.get("versionId") == version_id_2 - version_id_3 = "d30" # putting higher numbers so that version number doesnt collide with existing in db - factory_document_model(version_id_3, "termsofuse_directorsearch", html_content_1) - - version_id_4 = "d31" # putting higher numbers so that version number doesnt collide with existing in db - factory_document_model(version_id_4, "termsofuse_directorsearch", html_content_2) - - headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.anonymous_bcros_role) - rv = client.get("/api/v1/documents/termsofuse", headers=headers, content_type="application/json") - - assert rv.status_code == HTTPStatus.OK - assert schema_utils.validate(rv.json, "document")[0] - assert rv.json.get("content") == html_content_2 - assert rv.json.get("versionId") == version_id_4 - def test_document_signature_get_returns_200(client, jwt, session, gcs_mock): # pylint:disable=unused-argument """Assert get documents/filename/signatures endpoint returns 200.""" diff --git a/auth-api/tests/unit/api/test_invitation.py b/auth-api/tests/unit/api/test_invitation.py index c49126cd4..4a75557c4 100644 --- a/auth-api/tests/unit/api/test_invitation.py +++ b/auth-api/tests/unit/api/test_invitation.py @@ -28,7 +28,12 @@ from auth_api.utils.constants import GROUP_GOV_ACCOUNT_USERS, GROUP_PUBLIC_USERS from auth_api.utils.enums import LoginSource, Status from tests.utilities.factory_scenarios import KeycloakScenario, TestJwtClaims, TestOrgInfo -from tests.utilities.factory_utils import factory_auth_header, factory_invitation +from tests.utilities.factory_utils import ( + factory_auth_header, + factory_invitation, + keycloak_add_user, + keycloak_get_user_by_username, +) KEYCLOAK_SERVICE = KeycloakService() @@ -251,8 +256,8 @@ def test_accept_public_users_invitation( invitation_id_token = InvitationService.generate_confirmation_token(invitation_id) request = KeycloakScenario.create_user_request() - KEYCLOAK_SERVICE.add_user(request, return_if_exists=True) - user = KEYCLOAK_SERVICE.get_user_by_username(request.user_name) + keycloak_add_user(request, return_if_exists=True) + user = keycloak_get_user_by_username(request.user_name) user_id = user.id headers_invitee = factory_auth_header(jwt=jwt, claims=TestJwtClaims.get_test_user(user_id, source=source)) client.post("/api/v1/users", headers=headers_invitee, content_type="application/json") @@ -297,8 +302,8 @@ def test_accept_gov_account_invitation(client, jwt, session): # pylint:disable= invitation_id_token = InvitationService.generate_confirmation_token(invitation_id) request = KeycloakScenario.create_user_request() - KEYCLOAK_SERVICE.add_user(request, return_if_exists=True) - user = KEYCLOAK_SERVICE.get_user_by_username(request.user_name) + keycloak_add_user(request, return_if_exists=True) + user = keycloak_get_user_by_username(request.user_name) user_id = user.id headers_invitee = factory_auth_header(jwt=jwt, claims=TestJwtClaims.get_test_user(user_id, source="IDIR", roles=[])) client.post("/api/v1/users", headers=headers_invitee, content_type="application/json") diff --git a/auth-api/tests/unit/api/test_org.py b/auth-api/tests/unit/api/test_org.py index d2c4999b0..ec032db85 100644 --- a/auth-api/tests/unit/api/test_org.py +++ b/auth-api/tests/unit/api/test_org.py @@ -72,7 +72,6 @@ convert_org_to_staff_org, factory_auth_header, factory_invitation, - factory_invitation_anonymous, factory_membership_model, factory_org_model, factory_user_model, @@ -152,24 +151,6 @@ def test_add_org_v2_with_contact_fields_too_long(client, jwt, session, keycloak_ assert "message" in rv.json -@pytest.mark.parametrize( - "org_info", - [ - TestOrgInfo.org1, - TestOrgInfo.org_onlinebanking, - TestOrgInfo.org_with_products, - TestOrgInfo.org_regular, - TestOrgInfo.org_with_all_info, - ], -) -def test_add_org_by_anon_user(client, jwt, session, keycloak_mock, org_info): # pylint:disable=unused-argument - """Assert that an org can be POSTed.""" - headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.anonymous_bcros_role) - client.post("/api/v1/users", headers=headers, content_type="application/json") - rv = client.post("/api/v1/orgs", data=json.dumps(org_info), headers=headers, content_type="application/json") - assert rv.status_code == HTTPStatus.UNAUTHORIZED - - def test_add_basic_org_with_pad_throws_error(client, jwt, session, keycloak_mock): # pylint:disable=unused-argument """Assert that an org can be POSTed.""" headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.public_user_role) @@ -375,7 +356,7 @@ def test_search_org_by_client_multiple_status(client, jwt, session, keycloak_moc assert orgs.get("total") == 1 -def test_search_org_for_dir_search(client, jwt, session, keycloak_mock): # pylint:disable=unused-argument +def test_search_org(client, jwt, session, keycloak_mock): # pylint:disable=unused-argument """Assert that an org can be searched.""" headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.public_user_role) rv = client.post("/api/v1/users", headers=headers, content_type="application/json") @@ -383,27 +364,23 @@ def test_search_org_for_dir_search(client, jwt, session, keycloak_mock): # pyli "/api/v1/orgs", data=json.dumps(TestOrgInfo.org1), headers=headers, content_type="application/json" ) assert rv.status_code == HTTPStatus.CREATED - headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_admin_dir_search_role) - - rv = client.post( - "/api/v1/orgs", data=json.dumps(TestOrgInfo.org_anonymous), headers=headers, content_type="application/json" - ) + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_admin_role) - # staff search with manage account role gets both ORG + # staff search with manage account role gets 1 org headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_manage_accounts_role) rv = client.get("/api/v1/orgs", headers=headers, content_type="application/json") assert rv.status_code == HTTPStatus.OK assert schema_utils.validate(rv.json, "paged_response")[0] orgs = json.loads(rv.data) - assert len(orgs.get("orgs")) == 2 + assert len(orgs.get("orgs")) == 1 - # staff search with staff_admin_role gets both ORG + # staff search with staff_admin_role gets 1 org headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_admin_role) rv = client.get("/api/v1/orgs", headers=headers, content_type="application/json") assert rv.status_code == HTTPStatus.OK assert schema_utils.validate(rv.json, "paged_response")[0] orgs = json.loads(rv.data) - assert len(orgs.get("orgs")) == 2 + assert len(orgs.get("orgs")) == 1 # staff search with out manage account role gets only normal org headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_view_accounts_role) @@ -508,19 +485,6 @@ def test_add_govm_full_flow(client, jwt, session, keycloak_mock): # pylint:disa assert vs_product.get("subscriptionStatus") == "ACTIVE" -def test_add_anonymous_org_staff_admin(client, jwt, session, keycloak_mock): # pylint:disable=unused-argument - """Assert that an org can be POSTed.""" - headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_admin_role) - rv = client.post("/api/v1/users", headers=headers, content_type="application/json") - rv = client.post( - "/api/v1/orgs", data=json.dumps(TestOrgInfo.org_anonymous), headers=headers, content_type="application/json" - ) - assert rv.status_code == HTTPStatus.CREATED - dictionary = json.loads(rv.data) - assert dictionary["accessType"] == "ANONYMOUS" - assert schema_utils.validate(rv.json, "org_response")[0] - - def test_add_govm_org_by_user_exception(client, jwt, session, keycloak_mock): # pylint:disable=unused-argument """Assert that an org can be POSTed.""" headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.public_user_role) @@ -531,31 +495,6 @@ def test_add_govm_org_by_user_exception(client, jwt, session, keycloak_mock): # assert rv.status_code == HTTPStatus.UNAUTHORIZED -def test_add_anonymous_org_by_user_exception(client, jwt, session, keycloak_mock): # pylint:disable=unused-argument - """Assert that an org can be POSTed.""" - headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.public_user_role) - rv = client.post("/api/v1/users", headers=headers, content_type="application/json") - rv = client.post( - "/api/v1/orgs", data=json.dumps(TestOrgInfo.org_anonymous), headers=headers, content_type="application/json" - ) - assert rv.status_code == HTTPStatus.UNAUTHORIZED - - -def test_add_org_staff_admin_anonymous_not_passed(client, jwt, session, keycloak_mock): # pylint:disable=unused-argument - """Assert that an org can be POSTed.""" - headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_admin_role) - rv = client.post("/api/v1/users", headers=headers, content_type="application/json") - rv = client.post( - "/api/v1/orgs", - data=json.dumps({"name": "My Test Org", "accessType": AccessType.ANONYMOUS.value}), - headers=headers, - content_type="application/json", - ) - assert rv.status_code == HTTPStatus.CREATED - dictionary = json.loads(rv.data) - assert dictionary["accessType"] == "ANONYMOUS" - - def test_add_org_staff_admin_any_number_of_orgs(client, jwt, session, keycloak_mock): # pylint:disable=unused-argument """Assert that an org can be POSTed.""" headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_admin_role) @@ -879,35 +818,6 @@ def test_update_org_payment_method_for_org(client, jwt, session, keycloak_mock): assert rv.status_code == HTTPStatus.OK -def test_upgrade_anon_org_fail(client, jwt, session, keycloak_mock): # pylint:disable=unused-argument - """Assert that an org can be updated via PUT.""" - headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_admin_role) - rv = client.post("/api/v1/users", headers=headers, content_type="application/json") - rv = client.post( - "/api/v1/orgs", data=json.dumps(TestOrgInfo.org_anonymous), headers=headers, content_type="application/json" - ) - - dictionary = json.loads(rv.data) - assert rv.status_code == HTTPStatus.CREATED - assert rv.json.get("orgType") == OrgType.PREMIUM.value - assert rv.json.get("name") == TestOrgInfo.org_anonymous.get("name") - - org_id = dictionary["id"] - # upgrade with same data - - premium_info = TestOrgInfo.bcol_linked() - premium_info["typeCode"] = OrgType.STAFF.value - - rv = client.put( - f"/api/v1/orgs/{org_id}?action=UPGRADE", - data=json.dumps(premium_info), - headers=headers, - content_type="application/json", - ) - # FRCR review change.Staff cant change org details - assert rv.status_code == HTTPStatus.UNAUTHORIZED - - def test_update_premium_org(client, jwt, session, keycloak_mock): # pylint:disable=unused-argument """Assert that an org can be updated via PUT.""" headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.public_user_role) @@ -1436,37 +1346,6 @@ def test_get_invitations(client, jwt, session, keycloak_mock): # pylint:disable assert dictionary["invitations"][1]["recipientEmail"] == "xyz456@email.com" -def test_update_anon_org(client, jwt, session, keycloak_mock): # pylint:disable=unused-argument - """Assert that an org can be updated via PUT.""" - headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_admin_role) - rv = client.post("/api/v1/users", headers=headers, content_type="application/json") - rv = client.post( - "/api/v1/orgs", data=json.dumps(TestOrgInfo.org_anonymous), headers=headers, content_type="application/json" - ) - assert rv.status_code == HTTPStatus.CREATED - dictionary = json.loads(rv.data) - assert dictionary["accessType"] == "ANONYMOUS" - org_id = dictionary["id"] - rv = client.put( - f"/api/v1/orgs/{org_id}", - data=json.dumps({"name": "helo2"}), - headers=headers, - content_type="application/json", - ) - # FRCR review changes..staff cant change org details - assert rv.status_code == HTTPStatus.UNAUTHORIZED - - public_headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_role) - rv = client.put( - f"/api/v1/orgs/{org_id}", - data=json.dumps({"name": "helo2"}), - headers=public_headers, - content_type="application/json", - ) - # not an admin/owner..so unauthorized will be thrown when trying to access it - assert rv.status_code == HTTPStatus.UNAUTHORIZED - - def test_update_member(client, jwt, session, auth_mock, keycloak_mock): # pylint:disable=unused-argument """Assert that a member of an org can have their role updated.""" # Set up: create/login user, create org @@ -1695,7 +1574,7 @@ def test_get_affiliation(client, jwt, session, keycloak_mock, entity_mapping_moc assert dictionary["business"]["businessIdentifier"] == business_identifier -def test_get_affiliation_without_authrized(client, jwt, session, keycloak_mock, entity_mapping_mock): # pylint:disable=unused-argument +def test_get_affiliation_without_authorized(client, jwt, session, keycloak_mock, entity_mapping_mock): # pylint:disable=unused-argument """Assert that a list of affiliation for an org can be retrieved.""" headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.passcode) rv = client.post( @@ -1723,7 +1602,9 @@ def test_get_affiliation_without_authrized(client, jwt, session, keycloak_mock, business_identifier = TestAffliationInfo.nr_affiliation["businessIdentifier"] - headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.anonymous_bcros_role) + headers = factory_auth_header( + jwt=jwt, claims=TestJwtClaims.get_test_real_user(sub="a8098c1a-f86e-11da-bd1a-00112444be1e") + ) rv = client.get(f"/api/v1/orgs/{org_id}/affiliations/{business_identifier}", headers=headers) assert rv.status_code == HTTPStatus.UNAUTHORIZED @@ -2382,44 +2263,50 @@ def test_search_org_pagination(client, jwt, session, keycloak_mock): # pylint:d def test_search_org_invitations(client, jwt, session, keycloak_mock): # pylint:disable=unused-argument - """Assert that pagination works.""" - # Create 2 anonymous org invitations - headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_admin_dir_search_role) + """Assert that org search includes invitations for PENDING_ACTIVATION GOVM orgs.""" + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_admin_role) client.post("/api/v1/users", headers=headers, content_type="application/json") + # Create two GOVM orgs (created with PENDING_INVITE_ACCEPT status) rv = client.post( - "/api/v1/orgs", data=json.dumps(TestOrgInfo.org_anonymous), headers=headers, content_type="application/json" + "/api/v1/orgs", data=json.dumps(TestOrgInfo.org_govm), headers=headers, content_type="application/json" ) - + assert rv.status_code == HTTPStatus.CREATED dictionary = json.loads(rv.data) - org_id = dictionary["id"] - client.post( + org_id_1 = dictionary["id"] + rv = client.post( "/api/v1/invitations", - data=json.dumps(factory_invitation_anonymous(org_id=org_id)), + data=json.dumps(factory_invitation(org_id=org_id_1)), headers=headers, content_type="application/json", ) + assert rv.status_code == HTTPStatus.CREATED rv = client.post( - "/api/v1/orgs", data=json.dumps(TestOrgInfo.org_anonymous_2), headers=headers, content_type="application/json" + "/api/v1/orgs", + data=json.dumps({**TestOrgInfo.org_govm, "name": "Second GOVM Org"}), + headers=headers, + content_type="application/json", ) + assert rv.status_code == HTTPStatus.CREATED dictionary = json.loads(rv.data) - org_id = dictionary["id"] - client.post( + org_id_2 = dictionary["id"] + rv = client.post( "/api/v1/invitations", - data=json.dumps(factory_invitation_anonymous(org_id=org_id)), + data=json.dumps(factory_invitation(org_id=org_id_2)), headers=headers, content_type="application/json", ) + assert rv.status_code == HTTPStatus.CREATED - # staff search + # Staff search with PENDING_ACTIVATION status to include invitations + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_manage_accounts_role) rv = client.get( - f"/api/v1/orgs?status={OrgStatus.PENDING_ACTIVATION.value}", + "/api/v1/orgs?status=PENDING_ACTIVATION", headers=headers, content_type="application/json", ) assert rv.status_code == HTTPStatus.OK - assert schema_utils.validate(rv.json, "paged_response")[0] orgs = json.loads(rv.data) assert len(orgs.get("orgs")) == 2 diff --git a/auth-api/tests/unit/api/test_org_login_options.py b/auth-api/tests/unit/api/test_org_login_options.py index a81dce31e..c75f2d336 100644 --- a/auth-api/tests/unit/api/test_org_login_options.py +++ b/auth-api/tests/unit/api/test_org_login_options.py @@ -23,10 +23,9 @@ import pytest from faker import Faker -from auth_api.services.keycloak import KeycloakService from auth_api.utils.enums import LoginSource from tests.utilities.factory_scenarios import CONFIG, KeycloakScenario, TestJwtClaims, TestOrgInfo -from tests.utilities.factory_utils import factory_auth_header +from tests.utilities.factory_utils import factory_auth_header, keycloak_add_user, keycloak_get_user_by_username fake = Faker() @@ -47,8 +46,8 @@ def generate_user_headers(jwt, claim, login_source): """Create KC User and generate JWT for headers.""" claims = generate_claims_payload(claim["realm_access"], login_source) request = KeycloakScenario.create_user_by_user_info(claims) - KeycloakService.add_user(request, return_if_exists=True) - kc_user = KeycloakService.get_user_by_username(request.user_name) + keycloak_add_user(request, return_if_exists=True) + kc_user = keycloak_get_user_by_username(request.user_name) claims["id"] = kc_user.id claims["sub"] = kc_user.id return kc_user, factory_auth_header(jwt=jwt, claims=claims) diff --git a/auth-api/tests/unit/api/test_org_products.py b/auth-api/tests/unit/api/test_org_products.py index dd2bd38e7..7b1a82268 100644 --- a/auth-api/tests/unit/api/test_org_products.py +++ b/auth-api/tests/unit/api/test_org_products.py @@ -93,26 +93,6 @@ def test_add_single_org_product_vs(client, jwt, session, keycloak_mock): # pyli assert vs_product.get("subscriptionStatus") == "PENDING_STAFF_REVIEW" -def test_dir_search_doesnt_get_any_product(client, jwt, session, keycloak_mock): # pylint:disable=unused-argument - """Assert dir search doesnt get any active product subscriptions.""" - headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_admin_role) - client.post("/api/v1/users", headers=headers, content_type="application/json") - rv = client.post( - "/api/v1/orgs", data=json.dumps(TestOrgInfo.org_anonymous), headers=headers, content_type="application/json" - ) - assert rv.status_code == HTTPStatus.CREATED - dictionary = json.loads(rv.data) - assert dictionary["accessType"] == "ANONYMOUS" - assert schema_utils.validate(rv.json, "org_response")[0] - - rv_products = client.get( - f"/api/v1/orgs/{dictionary.get('id')}/products", headers=headers, content_type="application/json" - ) - - list_products = json.loads(rv_products.data) - assert len([x for x in list_products if x.get("subscriptionStatus") != "NOT_SUBSCRIBED"]) == 0 - - def test_new_dir_search_can_be_returned(client, jwt, session, keycloak_mock): # pylint:disable=unused-argument """Assert new dir search product subscriptions can be subscribed to via system admin / returned via org user.""" headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.public_user_role) diff --git a/auth-api/tests/unit/api/test_orgs_by_type.py b/auth-api/tests/unit/api/test_orgs_by_type.py index e4595853a..5138ea63e 100644 --- a/auth-api/tests/unit/api/test_orgs_by_type.py +++ b/auth-api/tests/unit/api/test_orgs_by_type.py @@ -30,7 +30,12 @@ from auth_api.utils.enums import LoginSource, OrgType from auth_api.utils.roles import ADMIN from tests.utilities.factory_scenarios import CONFIG, KeycloakScenario, TestJwtClaims, TestOrgInfo -from tests.utilities.factory_utils import factory_auth_header, factory_invitation +from tests.utilities.factory_utils import ( + factory_auth_header, + factory_invitation, + keycloak_add_user, + keycloak_get_user_by_username, +) fake = Faker() @@ -51,8 +56,8 @@ def generate_gov_user_headers(jwt): """Create KC User and generate JWT for headers.""" claims = generate_claims_gov_user_payload() request = KeycloakScenario.create_user_by_user_info(claims) - KeycloakService.add_user(request, return_if_exists=True) - kc_user = KeycloakService.get_user_by_username(request.user_name) + keycloak_add_user(request, return_if_exists=True) + kc_user = keycloak_get_user_by_username(request.user_name) claims["id"] = kc_user.id claims["sub"] = kc_user.id return kc_user, factory_auth_header(jwt=jwt, claims=claims) diff --git a/auth-api/tests/unit/api/test_user.py b/auth-api/tests/unit/api/test_user.py index c385dacb6..a7c67cfd9 100644 --- a/auth-api/tests/unit/api/test_user.py +++ b/auth-api/tests/unit/api/test_user.py @@ -35,16 +35,13 @@ from auth_api.models.org import receive_before_insert, receive_before_update from auth_api.schemas import utils as schema_utils from auth_api.services import Org as OrgService -from auth_api.services import User as UserService -from auth_api.services.keycloak import KeycloakService -from auth_api.utils.enums import AccessType, AffidavitStatus, IdpHint, ProductCode, Status, UserStatus -from auth_api.utils.roles import ADMIN, COORDINATOR, USER, Role +from auth_api.utils.enums import AffidavitStatus, Status +from auth_api.utils.roles import COORDINATOR, USER from tests import skip_in_pod from tests.conftest import mock_token from tests.utilities.factory_scenarios import ( KeycloakScenario, TestAffidavit, - TestAnonymousMembership, TestContactInfo, TestEntityInfo, TestJwtClaims, @@ -57,17 +54,15 @@ factory_auth_header, factory_contact_model, factory_entity_model, - factory_invitation_anonymous, factory_membership_model, factory_org_model, - factory_product_model, factory_user_model, + keycloak_add_user, + keycloak_get_user_by_username, patch_token_info, ) from tests.utilities.sqlalchemy import clear_event_listeners -KEYCLOAK_SERVICE = KeycloakService() - def test_add_user(client, jwt, session): # pylint:disable=unused-argument """Assert that a user can be POSTed.""" @@ -108,232 +103,6 @@ def test_add_user_staff_org(client, jwt, session, keycloak_mock, monkeypatch): assert len(staff_memberships) == 0 # 0, because our row was set to INACTIVE. -def test_delete_bcros_valdiations(client, jwt, session, keycloak_mock, monkeypatch): - """Assert different conditions of user deletion.""" - admin_user = dict(TestUserInfo.user_bcros_active) - org = factory_org_model(org_info=TestOrgInfo.org_anonymous) - user = factory_user_model(user_info=TestUserInfo.user_bcros_active) - factory_membership_model(user.id, org.id) - factory_product_model(org.id, product_code=ProductCode.DIR_SEARCH.value) - owner_claims = TestJwtClaims.get_test_real_user(user.keycloak_guid, idp_userid=user.idp_userid) - - patch_token_info(owner_claims, monkeypatch) - member = TestAnonymousMembership.generate_random_user(USER) - admin = TestAnonymousMembership.generate_random_user(COORDINATOR) - membership = [member, admin] - UserService.create_user_and_add_membership(membership, org.id) - owner_headers = factory_auth_header(jwt=jwt, claims=owner_claims) - member_username = IdpHint.BCROS.value + "/" + member["username"] - admin_username = IdpHint.BCROS.value + "/" + admin["username"] - admin_claims = TestJwtClaims.get_test_real_user( - uuid.uuid4(), admin_username, access_ype=AccessType.ANONYMOUS.value, roles=[Role.ANONYMOUS_USER.value] - ) - admin_headers = factory_auth_header(jwt=jwt, claims=admin_claims) - member_claims = TestJwtClaims.get_test_real_user( - uuid.uuid4(), member_username, access_ype=AccessType.ANONYMOUS.value, roles=[Role.ANONYMOUS_USER.value] - ) - - member_headers = factory_auth_header(jwt=jwt, claims=member_claims) - - # set up JWTS for member and admin - patch_token_info(admin_claims, monkeypatch) - client.post( - "/api/v1/users", headers=admin_headers, content_type="application/json", data=json.dumps({"isLogin": True}) - ) - - patch_token_info(member_claims, monkeypatch) - client.post( - "/api/v1/users", headers=member_headers, content_type="application/json", data=json.dumps({"isLogin": True}) - ) - - patch_token_info(owner_claims, monkeypatch) - # delete only owner ;failure - rv = client.delete( - f"/api/v1/users/{admin_user['username']}", headers=owner_headers, content_type="application/json" - ) - assert rv.status_code == HTTPStatus.UNAUTHORIZED - - # admin trying to delete member: Failure - patch_token_info(admin_claims, monkeypatch) - rv = client.delete(f"/api/v1/users/{member_username}", headers=admin_headers, content_type="application/json") - assert rv.status_code == HTTPStatus.UNAUTHORIZED - - # member delete admin: failure - patch_token_info(member_claims, monkeypatch) - rv = client.delete(f"/api/v1/users/{admin_username}", headers=member_headers, content_type="application/json") - assert rv.status_code == HTTPStatus.UNAUTHORIZED - - # a self delete ;should work ;mimics leave team for anonymous user - patch_token_info(member_claims, monkeypatch) - rv = client.delete(f"/api/v1/users/{member_username}", headers=member_headers, content_type="application/json") - assert rv.status_code == HTTPStatus.NO_CONTENT - - patch_token_info(admin_claims, monkeypatch) - rv = client.delete(f"/api/v1/users/{admin_username}", headers=admin_headers, content_type="application/json") - assert rv.status_code == HTTPStatus.NO_CONTENT - - # add one more admin - patch_token_info(owner_claims, monkeypatch) - new_owner = TestAnonymousMembership.generate_random_user(ADMIN) - membership = [new_owner] - UserService.create_user_and_add_membership(membership, org.id) - patch_token_info(owner_claims, monkeypatch) - rv = client.delete( - f"/api/v1/users/{IdpHint.BCROS.value + '/' + new_owner['username']}", - headers=owner_headers, - content_type="application/json", - ) - assert rv.status_code == HTTPStatus.NO_CONTENT - - -def test_add_back_a_delete_bcros(client, jwt, session, keycloak_mock, monkeypatch): - """Assert different conditions of user deletion.""" - org = factory_org_model(org_info=TestOrgInfo.org_anonymous) - user = factory_user_model(user_info=TestUserInfo.user_bcros_active) - factory_membership_model(user.id, org.id) - factory_product_model(org.id, product_code=ProductCode.DIR_SEARCH.value) - owner_claims = TestJwtClaims.get_test_real_user(user.keycloak_guid, idp_userid=user.idp_userid) - member = TestAnonymousMembership.generate_random_user(USER) - membership = [member, TestAnonymousMembership.generate_random_user(COORDINATOR)] - patch_token_info(owner_claims, monkeypatch) - UserService.create_user_and_add_membership(membership, org.id) - headers = factory_auth_header(jwt=jwt, claims=owner_claims) - member_user_id = IdpHint.BCROS.value + "/" + member.get("username") - rv = client.delete(f"/api/v1/users/{member_user_id}", headers=headers, content_type="application/json") - assert rv.status_code == HTTPStatus.NO_CONTENT - kc_user = KeycloakService.get_user_by_username(member.get("username")) - assert kc_user.enabled is False - user_model = UserService.find_by_username(member_user_id) - assert user_model.as_dict().get("user_status") == UserStatus.INACTIVE.value - membership = MembershipModel.find_membership_by_userid(user_model.identifier) - assert membership.status == Status.INACTIVE.value - - -def test_reset_password(client, jwt, session, keycloak_mock, monkeypatch): # pylint:disable=unused-argument - """Assert that an anonymous admin can be Patched.""" - org = factory_org_model(org_info=TestOrgInfo.org_anonymous) - user = factory_user_model(user_info=TestUserInfo.user_bcros_active) - factory_membership_model(user.id, org.id) - factory_product_model(org.id, product_code=ProductCode.DIR_SEARCH.value) - owner_claims = TestJwtClaims.get_test_real_user(user.keycloak_guid) - member = TestAnonymousMembership.generate_random_user(USER) - admin = TestAnonymousMembership.generate_random_user(COORDINATOR) - membership = [member, admin] - - patch_token_info(owner_claims, monkeypatch) - UserService.create_user_and_add_membership(membership, org.id) - owner_headers = factory_auth_header(jwt=jwt, claims=owner_claims) - member_username = IdpHint.BCROS.value + "/" + member["username"] - admin_username = IdpHint.BCROS.value + "/" + admin["username"] - admin_claims = TestJwtClaims.get_test_real_user( - uuid.uuid4(), admin_username, access_ype=AccessType.ANONYMOUS.value, roles=[Role.ANONYMOUS_USER.value] - ) - admin_headers = factory_auth_header(jwt=jwt, claims=admin_claims) - member_claims = TestJwtClaims.get_test_real_user( - uuid.uuid4(), member_username, access_ype=AccessType.ANONYMOUS.value, roles=[Role.ANONYMOUS_USER.value] - ) - member_headers = factory_auth_header(jwt=jwt, claims=member_claims) - # set up JWTS for member and admin - patch_token_info(admin_claims, monkeypatch) - client.post( - "/api/v1/users", headers=admin_headers, content_type="application/json", data=json.dumps({"isLogin": True}) - ) - patch_token_info(member_claims, monkeypatch) - client.post( - "/api/v1/users", headers=member_headers, content_type="application/json", data=json.dumps({"isLogin": True}) - ) - - # reset password of admin by owner - input_data = json.dumps({"username": admin_username, "password": "Mysecretcode@1234"}) - - patch_token_info(owner_claims, monkeypatch) - rv = client.patch( - f"/api/v1/users/{admin_username}", headers=owner_headers, data=input_data, content_type="application/json" - ) - assert rv.status_code == HTTPStatus.NO_CONTENT - - # member cant reset password - patch_token_info(member_claims, monkeypatch) - rv = client.patch( - f"/api/v1/users/{admin_username}", headers=member_headers, data=input_data, content_type="application/json" - ) - assert rv.status_code == HTTPStatus.FORBIDDEN - - # admin cant reset password - patch_token_info(admin_claims, monkeypatch) - rv = client.patch( - f"/api/v1/users/{admin_username}", headers=admin_headers, data=input_data, content_type="application/json" - ) - assert rv.status_code == HTTPStatus.FORBIDDEN - - -def test_add_user_admin_valid_bcros(client, jwt, session, keycloak_mock): # pylint:disable=unused-argument - """Assert that an anonymous admin can be POSTed.""" - headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_admin_dir_search_role) - rv = client.post("/api/v1/users", headers=headers, content_type="application/json") - rv = client.post( - "/api/v1/orgs", data=json.dumps(TestOrgInfo.org_anonymous), headers=headers, content_type="application/json" - ) - dictionary = json.loads(rv.data) - org_id = dictionary["id"] - rv = client.post( - "/api/v1/invitations", - data=json.dumps(factory_invitation_anonymous(org_id=org_id)), - headers=headers, - content_type="application/json", - ) - dictionary = json.loads(rv.data) - assert dictionary.get("token") is not None - assert rv.status_code == HTTPStatus.CREATED - rv = client.post( - "/api/v1/users/bcros", - data=json.dumps(TestUserInfo.user_anonymous_1), - headers={"invitation_token": dictionary.get("token")}, - content_type="application/json", - ) - dictionary = json.loads(rv.data) - - assert rv.status_code == HTTPStatus.CREATED - assert ( - dictionary["users"][0].get("username") == IdpHint.BCROS.value + "/" + TestUserInfo.user_anonymous_1["username"] - ) - assert dictionary["users"][0].get("password") is None - assert dictionary["users"][0].get("type") == Role.ANONYMOUS_USER.name - assert schema_utils.validate(rv.json, "anonymous_user_response")[0] - - # different error scenarios - - # check expired invitation - rv = client.post( - "/api/v1/users/bcros", - data=json.dumps(TestUserInfo.user_anonymous_1), - headers={"invitation_token": dictionary.get("token")}, - content_type="application/json", - ) - dictionary = json.loads(rv.data) - assert dictionary["code"] == "EXPIRED_INVITATION" - - rv = client.post( - "/api/v1/invitations", - data=json.dumps(factory_invitation_anonymous(org_id=org_id)), - headers=headers, - content_type="application/json", - ) - dictionary = json.loads(rv.data) - - # check duplicate user - rv = client.post( - "/api/v1/users/bcros", - data=json.dumps(TestUserInfo.user_anonymous_1), - headers={"invitation_token": dictionary.get("token")}, - content_type="application/json", - ) - dictionary = json.loads(rv.data) - - assert dictionary["code"] == 409 - assert dictionary["message"] == "The username is already taken" - - def test_add_user_no_token_returns_401(client, session): # pylint:disable=unused-argument """Assert that POSTing a user with no token returns a 401.""" rv = client.post("/api/v1/users", headers=None, content_type="application/json") @@ -881,8 +650,8 @@ def test_delete_otp_for_user(client, jwt, session): # pylint:disable=unused-arg user_headers = factory_auth_header(jwt=jwt, claims=user_claims) request = KeycloakScenario.create_user_by_user_info(user_info=TestJwtClaims.tester_bceid_role) - KEYCLOAK_SERVICE.add_user(request, return_if_exists=True) - user = KEYCLOAK_SERVICE.get_user_by_username(request.user_name) + keycloak_add_user(request, return_if_exists=True) + user = keycloak_get_user_by_username(request.user_name) assert "CONFIGURE_TOTP" not in json.loads(user.value()).get("requiredActions", None) user_id = user.id # Create user, org and membserhip in DB @@ -894,7 +663,7 @@ def test_delete_otp_for_user(client, jwt, session): # pylint:disable=unused-arg rv = client.delete(f"api/v1/users/{user.username}/otp/{org.id}", headers=headers) assert rv.status_code == HTTPStatus.NO_CONTENT - user1 = KEYCLOAK_SERVICE.get_user_by_username(request.user_name) + user1 = keycloak_get_user_by_username(request.user_name) assert "CONFIGURE_TOTP" in json.loads(user1.value()).get("requiredActions") @@ -929,8 +698,8 @@ def test_add_bceid_user(client, jwt, session): # pylint:disable=unused-argument """Assert that a user can be POSTed.""" # Create a user in keycloak request = KeycloakScenario.create_user_request() - KEYCLOAK_SERVICE.add_user(request, return_if_exists=True) - user = KEYCLOAK_SERVICE.get_user_by_username(request.user_name) + keycloak_add_user(request, return_if_exists=True) + user = keycloak_get_user_by_username(request.user_name) user_id = user.id headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.get_test_user(user_id, source="BCEID")) diff --git a/auth-api/tests/unit/services/test_affiliation.py b/auth-api/tests/unit/services/test_affiliation.py index 1fe71b9be..9e235feb0 100644 --- a/auth-api/tests/unit/services/test_affiliation.py +++ b/auth-api/tests/unit/services/test_affiliation.py @@ -270,7 +270,8 @@ def raise_for_status(self): pass monkeypatch.setattr( - "auth_api.services.rest_service.RestService.get", lambda *args, **kwargs: MockPartiesResponse() # noqa: ARG005 + "auth_api.services.rest_service.RestService.get", + lambda *args, **kwargs: MockPartiesResponse(), # noqa: ARG005 ) else: patch_get_firms_parties(monkeypatch) diff --git a/auth-api/tests/unit/services/test_affiliation_invitation.py b/auth-api/tests/unit/services/test_affiliation_invitation.py index cef1513f8..43acc0261 100644 --- a/auth-api/tests/unit/services/test_affiliation_invitation.py +++ b/auth-api/tests/unit/services/test_affiliation_invitation.py @@ -1000,7 +1000,6 @@ def test_get_all_invitations_with_details_related_to_org( ("accept", LoginSource.BCSC.value, None), ("accept", LoginSource.BCEID.value, Error.INVALID_USER_CREDENTIALS), ("accept", LoginSource.STAFF.value, Error.INVALID_USER_CREDENTIALS), - ("accept", LoginSource.BCROS.value, Error.INVALID_USER_CREDENTIALS), ], ) def test_unaffiliated_email_invitation_auth( diff --git a/auth-api/tests/unit/services/test_documents.py b/auth-api/tests/unit/services/test_documents.py index b8cf00dea..640dc3158 100644 --- a/auth-api/tests/unit/services/test_documents.py +++ b/auth-api/tests/unit/services/test_documents.py @@ -52,9 +52,3 @@ def test_find_latest_version_by_type(session): # pylint: disable=unused-argumen """Assert that a document is rendered correctly as a dictionary.""" terms_of_use = DocumentService.find_latest_version_by_type("termsofuse") assert terms_of_use == get_tos_latest_version() - - -def test_find_latest_version_for_director_search(session): # pylint: disable=unused-argument - """Assert that a document is rendered correctly as a dictionary.""" - terms_of_use = DocumentService.find_latest_version_by_type("termsofuse_directorsearch") - assert terms_of_use == "d1" diff --git a/auth-api/tests/unit/services/test_invitation_auth.py b/auth-api/tests/unit/services/test_invitation_auth.py index 91a08b67b..620c5aa71 100644 --- a/auth-api/tests/unit/services/test_invitation_auth.py +++ b/auth-api/tests/unit/services/test_invitation_auth.py @@ -356,7 +356,7 @@ def test_invitation_govm(session, auth_mock, keycloak_mock, monkeypatch): @mock.patch("auth_api.services.affiliation_invitation.RestService.get_service_account_token", mock_token) -def test_invitation_anonymous(session, auth_mock, keycloak_mock, monkeypatch): +def test_invitation_regular_org(session, auth_mock, keycloak_mock, monkeypatch): """Assert that non government ministry organization invites can be accepted by different login sources.""" # inviter/invitee user setup inviter_user = factory_user_model(TestUserInfo.user_tester) diff --git a/auth-api/tests/unit/services/test_keycloak.py b/auth-api/tests/unit/services/test_keycloak.py index e7606a5f0..e43a03580 100644 --- a/auth-api/tests/unit/services/test_keycloak.py +++ b/auth-api/tests/unit/services/test_keycloak.py @@ -25,11 +25,16 @@ from auth_api.exceptions.errors import Error from auth_api.models.dataclass import KeycloakGroupSubscription from auth_api.services.keycloak import KeycloakService -from auth_api.utils.constants import GROUP_ACCOUNT_HOLDERS, GROUP_ANONYMOUS_USERS, GROUP_PUBLIC_USERS +from auth_api.utils.constants import GROUP_ACCOUNT_HOLDERS, GROUP_PUBLIC_USERS from auth_api.utils.enums import KeycloakGroupActions, LoginSource from auth_api.utils.roles import Role from tests.utilities.factory_scenarios import KeycloakScenario, TestJwtClaims -from tests.utilities.factory_utils import patch_token_info +from tests.utilities.factory_utils import ( + keycloak_add_user, + keycloak_delete_user_by_username, + keycloak_get_user_by_username, + patch_token_info, +) KEYCLOAK_SERVICE = KeycloakService() @@ -38,7 +43,7 @@ def test_keycloak_add_user(session): """Add user to Keycloak. Assert return a user with the same username as the username in request.""" # with app.app_context(): request = KeycloakScenario.create_user_request() - user = KEYCLOAK_SERVICE.add_user(request, return_if_exists=True) + user = keycloak_add_user(request, return_if_exists=True) assert user.user_name == request.user_name @@ -46,8 +51,8 @@ def test_keycloak_get_user_by_username(session): """Get user by username. Assert get a user with the same username as the username in request.""" request = KeycloakScenario.create_user_request() # with app.app_context(): - KEYCLOAK_SERVICE.add_user(request, return_if_exists=True) - user = KEYCLOAK_SERVICE.get_user_by_username(request.user_name) + keycloak_add_user(request, return_if_exists=True) + user = keycloak_get_user_by_username(request.user_name) assert user.user_name == request.user_name @@ -57,7 +62,7 @@ def test_keycloak_get_user_by_username_not_exist(session): request = KeycloakScenario.create_user_request() # with app.app_context(): try: - user = KEYCLOAK_SERVICE.get_user_by_username(request.user_name) + user = keycloak_get_user_by_username(request.user_name) except BusinessException as err: assert err.code == Error.DATA_NOT_FOUND.name assert user is None @@ -67,11 +72,11 @@ def test_keycloak_get_token(session): """Get token by username and password. Assert access_token is included in response.""" request = KeycloakScenario.create_user_request() # with app.app_context(): - KEYCLOAK_SERVICE.add_user(request, return_if_exists=True) + keycloak_add_user(request, return_if_exists=True) response = KEYCLOAK_SERVICE.get_token(request.user_name, request.password) assert response.get("access_token") is not None - KEYCLOAK_SERVICE.delete_user_by_username(request.user_name) + keycloak_delete_user_by_username(request.user_name) def test_keycloak_get_token_user_not_exist(session): @@ -89,8 +94,8 @@ def test_keycloak_delete_user_by_username(session): """Delete user by username.Assert response is not None.""" # with app.app_context(): request = KeycloakScenario.create_user_request() - KEYCLOAK_SERVICE.add_user(request, return_if_exists=True) - KEYCLOAK_SERVICE.delete_user_by_username(request.user_name) + keycloak_add_user(request, return_if_exists=True) + keycloak_delete_user_by_username(request.user_name) assert True @@ -100,7 +105,7 @@ def test_keycloak_delete_user_by_username_user_not_exist(session): # First delete the user if it exists response = None try: - response = KEYCLOAK_SERVICE.delete_user_by_username(KeycloakScenario.create_user_request().user_name) + response = keycloak_delete_user_by_username(KeycloakScenario.create_user_request().user_name) except BusinessException as err: assert err.code == Error.DATA_NOT_FOUND.name assert response is None @@ -110,8 +115,8 @@ def test_join_users_group(app, session, monkeypatch): """Test the public_users group membership for public users.""" # with app.app_context(): request = KeycloakScenario.create_user_request() - KEYCLOAK_SERVICE.add_user(request, return_if_exists=True) - user = KEYCLOAK_SERVICE.get_user_by_username(request.user_name) + keycloak_add_user(request, return_if_exists=True) + user = keycloak_get_user_by_username(request.user_name) user_id = user.id patch_token_info( @@ -125,25 +130,13 @@ def test_join_users_group(app, session, monkeypatch): groups.append(group.get("name")) assert GROUP_PUBLIC_USERS in groups - # BCROS - patch_token_info( - {"sub": user_id, "loginSource": LoginSource.BCROS.value, "realm_access": {"roles": []}}, monkeypatch - ) - KEYCLOAK_SERVICE.join_users_group() - # Get the user groups and verify the public_users group is in the list - user_groups = KEYCLOAK_SERVICE.get_user_groups(user_id=user_id) - groups = [] - for group in user_groups: - groups.append(group.get("name")) - assert GROUP_ANONYMOUS_USERS in groups - def test_join_users_group_for_staff_users(session, app, monkeypatch): """Test the staff user account creation, and assert the public_users group is not added.""" # with app.app_context(): request = KeycloakScenario.create_user_request() - KEYCLOAK_SERVICE.add_user(request, return_if_exists=True) - user = KEYCLOAK_SERVICE.get_user_by_username(request.user_name) + keycloak_add_user(request, return_if_exists=True) + user = keycloak_get_user_by_username(request.user_name) user_id = user.id patch_token_info( {"sub": user_id, "loginSource": LoginSource.STAFF.value, "realm_access": {"roles": []}}, monkeypatch @@ -160,8 +153,8 @@ def test_join_users_group_for_staff_users(session, app, monkeypatch): def test_join_users_group_for_existing_users(session, monkeypatch): """Test the existing user account, and assert the public_users group is not added.""" request = KeycloakScenario.create_user_request() - KEYCLOAK_SERVICE.add_user(request, return_if_exists=True) - user = KEYCLOAK_SERVICE.get_user_by_username(request.user_name) + keycloak_add_user(request, return_if_exists=True) + user = keycloak_get_user_by_username(request.user_name) user_id = user.id patch_token_info( @@ -180,8 +173,8 @@ def test_join_users_group_for_existing_users(session, monkeypatch): def test_join_account_holders_group(session): """Assert that the account_holders group is getting added to the user.""" request = KeycloakScenario.create_user_request() - KEYCLOAK_SERVICE.add_user(request, return_if_exists=True) - user = KEYCLOAK_SERVICE.get_user_by_username(request.user_name) + keycloak_add_user(request, return_if_exists=True) + user = keycloak_get_user_by_username(request.user_name) user_id = user.id KEYCLOAK_SERVICE.join_account_holders_group(keycloak_guid=user_id) # Get the user groups and verify the public_users group is in the list @@ -195,8 +188,8 @@ def test_join_account_holders_group(session): def test_join_account_holders_group_from_token(session, monkeypatch): """Assert that the account_holders group is getting added to the user.""" request = KeycloakScenario.create_user_request() - KEYCLOAK_SERVICE.add_user(request, return_if_exists=True) - user = KEYCLOAK_SERVICE.get_user_by_username(request.user_name) + keycloak_add_user(request, return_if_exists=True) + user = keycloak_get_user_by_username(request.user_name) user_id = user.id # Patch token info @@ -214,8 +207,8 @@ def test_join_account_holders_group_from_token(session, monkeypatch): def test_remove_from_account_holders_group(session, monkeypatch): """Assert that the account_holders group is removed from the user.""" request = KeycloakScenario.create_user_request() - KEYCLOAK_SERVICE.add_user(request, return_if_exists=True) - user = KEYCLOAK_SERVICE.get_user_by_username(request.user_name) + keycloak_add_user(request, return_if_exists=True) + user = keycloak_get_user_by_username(request.user_name) user_id = user.id KEYCLOAK_SERVICE.join_account_holders_group(keycloak_guid=user_id) # Get the user groups and verify the public_users group is in the list @@ -236,8 +229,8 @@ def test_remove_from_account_holders_group(session, monkeypatch): def test_reset_otp(session): """Assert that the user otp configuration get reset in keycloak.""" request = KeycloakScenario.create_user_by_user_info(user_info=TestJwtClaims.tester_bceid_role) - KEYCLOAK_SERVICE.add_user(request, return_if_exists=True) - user = KEYCLOAK_SERVICE.get_user_by_username(request.user_name) + keycloak_add_user(request, return_if_exists=True) + user = keycloak_get_user_by_username(request.user_name) user_id = user.id KEYCLOAK_SERVICE.reset_otp(user_id) assert True @@ -246,8 +239,8 @@ def test_reset_otp(session): @pytest.mark.asyncio def test_add_remove_group_bulk(session): """Assert that the users' groups can be updated in bulk.""" - user1 = KEYCLOAK_SERVICE.add_user(KeycloakScenario.create_user_request(), return_if_exists=True) - user2 = KEYCLOAK_SERVICE.add_user(KeycloakScenario.create_user_request(), return_if_exists=True) + user1 = keycloak_add_user(KeycloakScenario.create_user_request(), return_if_exists=True) + user2 = keycloak_add_user(KeycloakScenario.create_user_request(), return_if_exists=True) kgs = [ KeycloakGroupSubscription( user_guid=user1.id, diff --git a/auth-api/tests/unit/services/test_membership.py b/auth-api/tests/unit/services/test_membership.py index 0418cd557..d91574c1b 100644 --- a/auth-api/tests/unit/services/test_membership.py +++ b/auth-api/tests/unit/services/test_membership.py @@ -39,6 +39,8 @@ factory_org_model, factory_product_model, factory_user_model, + keycloak_add_user, + keycloak_get_user_by_username, ) @@ -48,8 +50,8 @@ def test_accept_invite_adds_group_to_the_user(session, monkeypatch): # pylint:d # Create a user in keycloak keycloak_service = KeycloakService() request = KeycloakScenario.create_user_request() - keycloak_service.add_user(request, return_if_exists=True) - kc_user = keycloak_service.get_user_by_username(request.user_name) + keycloak_add_user(request, return_if_exists=True) + kc_user = keycloak_get_user_by_username(request.user_name) user = factory_user_model(TestUserInfo.get_user_with_kc_guid(kc_guid=kc_user.id)) # Patch token info @@ -66,8 +68,8 @@ def token_info(): # pylint: disable=unused-argument; mocks of library methods org = OrgService.create_org(TestOrgInfo.org1, user_id=user.id) # Create another user request = KeycloakScenario.create_user_request() - keycloak_service.add_user(request, return_if_exists=True) - kc_user2 = keycloak_service.get_user_by_username(request.user_name) + keycloak_add_user(request, return_if_exists=True) + kc_user2 = keycloak_get_user_by_username(request.user_name) user2 = factory_user_model(TestUserInfo.get_user_with_kc_guid(kc_guid=kc_user2.id)) # Add a membership to the user for the org created @@ -96,8 +98,8 @@ def test_remove_member_removes_group_to_the_user(publish_mock, session, monkeypa # Create a user in keycloak keycloak_service = KeycloakService() request = KeycloakScenario.create_user_request() - keycloak_service.add_user(request, return_if_exists=True) - kc_user = keycloak_service.get_user_by_username(request.user_name) + keycloak_add_user(request, return_if_exists=True) + kc_user = keycloak_get_user_by_username(request.user_name) user = factory_user_model(TestUserInfo.get_user_with_kc_guid(kc_guid=kc_user.id)) # Patch token info @@ -114,8 +116,8 @@ def token_info(): # pylint: disable=unused-argument; mocks of library methods org = OrgService.create_org(TestOrgInfo.org1, user_id=user.id) # Create another user request = KeycloakScenario.create_user_request() - keycloak_service.add_user(request, return_if_exists=True) - kc_user2 = keycloak_service.get_user_by_username(request.user_name) + keycloak_add_user(request, return_if_exists=True) + kc_user2 = keycloak_get_user_by_username(request.user_name) user2 = factory_user_model(TestUserInfo.get_user_with_kc_guid(kc_guid=kc_user2.id)) # Add a membership to the user for the org created diff --git a/auth-api/tests/unit/services/test_org.py b/auth-api/tests/unit/services/test_org.py index 7d4110e0d..aec1b9855 100644 --- a/auth-api/tests/unit/services/test_org.py +++ b/auth-api/tests/unit/services/test_org.py @@ -86,6 +86,8 @@ factory_org_service, factory_user_model, factory_user_model_with_contact, + keycloak_add_user, + keycloak_get_user_by_username, patch_pay_account_delete, patch_pay_account_post, patch_pay_account_put, @@ -963,8 +965,8 @@ def test_create_org_adds_user_to_account_holders_group(session, monkeypatch): # # Create a user in keycloak keycloak_service = KeycloakService() request = KeycloakScenario.create_user_request() - keycloak_service.add_user(request, return_if_exists=True) - kc_user = keycloak_service.get_user_by_username(request.user_name) + keycloak_add_user(request, return_if_exists=True) + kc_user = keycloak_get_user_by_username(request.user_name) user = factory_user_model(TestUserInfo.get_user_with_kc_guid(kc_guid=kc_user.id)) patch_token_info({"sub": user.keycloak_guid, "idp_userid": user.idp_userid}, monkeypatch) @@ -983,8 +985,8 @@ def test_delete_org_removes_user_from_account_holders_group(session, auth_mock, # Create a user in keycloak keycloak_service = KeycloakService() request = KeycloakScenario.create_user_request() - keycloak_service.add_user(request, return_if_exists=True) - kc_user = keycloak_service.get_user_by_username(request.user_name) + keycloak_add_user(request, return_if_exists=True) + kc_user = keycloak_get_user_by_username(request.user_name) user = factory_user_model(TestUserInfo.get_user_with_kc_guid(kc_guid=kc_user.id)) patch_token_info({"sub": user.keycloak_guid, "idp_userid": user.idp_userid}, monkeypatch) @@ -1006,8 +1008,8 @@ def test_delete_does_not_remove_user_from_account_holder_group(session, monkeypa # Create a user in keycloak keycloak_service = KeycloakService() request = KeycloakScenario.create_user_request() - keycloak_service.add_user(request, return_if_exists=True) - kc_user = keycloak_service.get_user_by_username(request.user_name) + keycloak_add_user(request, return_if_exists=True) + kc_user = keycloak_get_user_by_username(request.user_name) user = factory_user_model(TestUserInfo.get_user_with_kc_guid(kc_guid=kc_user.id)) patch_token_info({"sub": user.keycloak_guid, "idp_userid": user.idp_userid}, monkeypatch) @@ -1300,8 +1302,8 @@ def test_create_product_single_subscription_qs(session, monkeypatch): # Create a user in keycloak keycloak_service = KeycloakService() request = KeycloakScenario.create_user_by_user_info(TestJwtClaims.public_bceid_user) - keycloak_service.add_user(request, return_if_exists=True) - kc_user = keycloak_service.get_user_by_username(request.user_name) + keycloak_add_user(request, return_if_exists=True) + kc_user = keycloak_get_user_by_username(request.user_name) user = factory_user_model(TestUserInfo.get_bceid_user_with_kc_guid(kc_guid=kc_user.id)) patch_token_info({"sub": user.keycloak_guid, "idp_userid": user.idp_userid}, monkeypatch) diff --git a/auth-api/tests/unit/services/test_product.py b/auth-api/tests/unit/services/test_product.py index 8b65e9638..5f55c4450 100644 --- a/auth-api/tests/unit/services/test_product.py +++ b/auth-api/tests/unit/services/test_product.py @@ -30,7 +30,6 @@ from auth_api.services import Product as ProductService from auth_api.services import User as UserService from auth_api.services.activity_log_publisher import ActivityLogPublisher -from auth_api.services.keycloak import KeycloakService from auth_api.utils.enums import ActivityAction, KeycloakGroupActions, ProductCode, ProductSubscriptionStatus, Status from auth_api.utils.notifications import ProductSubscriptionInfo from tests.conftest import mock_token @@ -39,6 +38,7 @@ factory_membership_model, factory_product_model, factory_user_model, + keycloak_add_user, patch_token_info, ) @@ -109,7 +109,7 @@ def test_get_users_product_subscriptions_kc_groups(session, keycloak_mock, monke """Assert that our keycloak groups are returned correctly.""" # Used these to test without the keycloak_mock. request = KeycloakScenario.create_user_request() - user = KeycloakService.add_user(request, return_if_exists=True) + user = keycloak_add_user(request, return_if_exists=True) # Set keycloak groups, because empty gets filtered out. bca_code = ProductCodeModel.find_by_code("BCA") @@ -274,7 +274,7 @@ def test_get_users_sub_product_subscriptions_kc_groups(session, keycloak_mock, m """Assert that our keycloak groups are returned correctly for sub products.""" # Used these to test without the keycloak_mock. request = KeycloakScenario.create_user_request() - user = KeycloakService.add_user(request, return_if_exists=True) + user = keycloak_add_user(request, return_if_exists=True) # Filter out types that are not parents or children bca_code = ProductCodeModel.find_by_code("BCA") diff --git a/auth-api/tests/unit/services/test_user.py b/auth-api/tests/unit/services/test_user.py index 2440bf0bb..e837e8d23 100644 --- a/auth-api/tests/unit/services/test_user.py +++ b/auth-api/tests/unit/services/test_user.py @@ -22,7 +22,6 @@ from unittest.mock import patch import pytest -from werkzeug.exceptions import HTTPException from auth_api.exceptions import BusinessException from auth_api.exceptions.errors import Error @@ -32,14 +31,10 @@ from auth_api.models import User as UserModel from auth_api.services import Org as OrgService from auth_api.services import User as UserService -from auth_api.services.keycloak import KeycloakService -from auth_api.services.keycloak_user import KeycloakUser -from auth_api.utils.enums import IdpHint, ProductCode, Status -from auth_api.utils.roles import ADMIN, COORDINATOR, USER, Role +from auth_api.utils.enums import Status from tests.conftest import mock_token from tests.utilities.factory_scenarios import ( KeycloakScenario, - TestAnonymousMembership, TestContactInfo, TestEntityInfo, TestJwtClaims, @@ -51,9 +46,10 @@ factory_entity_model, factory_membership_model, factory_org_model, - factory_product_model, factory_user_model, get_tos_latest_version, + keycloak_add_user, + keycloak_get_user_by_username, patch_token_info, ) @@ -77,29 +73,18 @@ def test_user_save_by_token(session, monkeypatch): # pylint: disable=unused-arg assert dictionary["keycloak_guid"] == TestJwtClaims.user_test["sub"] -def test_bcros_user_save_by_token(session, monkeypatch): # pylint: disable=unused-argument - """Assert that a user can be created by token.""" - patch_token_info(TestJwtClaims.anonymous_bcros_role, monkeypatch) +def test_user_update_by_token(session, monkeypatch): # pylint: disable=unused-argument + """Assert that an existing user can be updated by token.""" + patch_token_info(TestJwtClaims.user_test, monkeypatch) user = UserService.save_from_jwt_token() assert user is not None - dictionary = user.as_dict() - assert dictionary["username"] == TestJwtClaims.anonymous_bcros_role["preferred_username"] - assert dictionary["keycloak_guid"] == TestJwtClaims.anonymous_bcros_role["sub"] - - -def test_bcros_user_update_by_token(session, monkeypatch): # pylint: disable=unused-argument - """Assert that a user can be created by token.""" - user_model = factory_user_model(TestUserInfo.user_bcros) - user = UserService(user_model) - dictionary = user.as_dict() - assert dictionary.get("keycloak_guid", None) is None - patch_token_info(TestJwtClaims.anonymous_bcros_role, monkeypatch) + # Save again to exercise the update path user = UserService.save_from_jwt_token() assert user is not None dictionary = user.as_dict() - assert dictionary["username"] == TestJwtClaims.anonymous_bcros_role["preferred_username"] - assert dictionary["keycloak_guid"] == TestJwtClaims.anonymous_bcros_role["sub"] + assert dictionary["username"] == TestJwtClaims.user_test["preferred_username"] + assert dictionary["keycloak_guid"] == TestJwtClaims.user_test["sub"] def test_user_save_by_token_no_token(session): # pylint: disable=unused-argument @@ -108,309 +93,25 @@ def test_user_save_by_token_no_token(session): # pylint: disable=unused-argumen assert user is None -def test_create_user_and_add_membership_owner_skip_auth_mode(session, auth_mock, keycloak_mock): # pylint:disable=unused-argument - """Assert that an owner can be added as anonymous.""" - org = factory_org_model(org_info=TestOrgInfo.org_anonymous) - membership = [TestAnonymousMembership.generate_random_user(ADMIN)] - users = UserService.create_user_and_add_membership(membership, org.id, single_mode=True) - assert len(users["users"]) == 1 - assert users["users"][0]["username"] == IdpHint.BCROS.value + "/" + membership[0]["username"] - assert users["users"][0]["type"] == Role.ANONYMOUS_USER.name - - members = MembershipModel.find_members_by_org_id(org.id) - - # only one member should be there since its a STAFF created org - assert len(members) == 1 - assert members[0].membership_type_code == ADMIN - - -def test_reset_password(session, auth_mock, keycloak_mock, monkeypatch): # pylint:disable=unused-argument - """Assert that the password can be changed.""" - org = factory_org_model(org_info=TestOrgInfo.org_anonymous) - user = factory_user_model() - factory_membership_model(user.id, org.id) - factory_product_model(org.id, product_code=ProductCode.DIR_SEARCH.value) - claims = TestJwtClaims.get_test_real_user(user.keycloak_guid) - - patch_token_info(claims, monkeypatch) - - membership = [TestAnonymousMembership.generate_random_user(USER)] - users = UserService.create_user_and_add_membership(membership, org.id) - user_name = users["users"][0]["username"] - user_info = {"username": user_name, "password": "password"} - kc_user = UserService.reset_password_for_anon_user(user_info, user_name) - # cant assert anything else since password wont be gotten back - assert kc_user.user_name == user_name.replace(f"{IdpHint.BCROS.value}/", "").lower() - - -def test_reset_password_by_member(session, auth_mock, keycloak_mock, monkeypatch): # pylint:disable=unused-argument - """Assert that the password cant be changed by member.""" - org = factory_org_model(org_info=TestOrgInfo.org_anonymous) - user = factory_user_model() - factory_membership_model(user.id, org.id) - factory_product_model(org.id, product_code=ProductCode.DIR_SEARCH.value) - admin_claims = TestJwtClaims.get_test_real_user(user.keycloak_guid) - membership = [TestAnonymousMembership.generate_random_user(USER)] - - patch_token_info(admin_claims, monkeypatch) - users = UserService.create_user_and_add_membership(membership, org.id) - user_name = users["users"][0]["username"] - user_info = {"username": user_name, "password": "password"} - with pytest.raises(HTTPException) as excinfo: - patch_token_info(TestJwtClaims.public_user_role, monkeypatch) - UserService.reset_password_for_anon_user(user_info, user_name) - assert excinfo.value.code == 403 - - def test_delete_otp_for_user(session, auth_mock, keycloak_mock, monkeypatch): - """Assert that the otp cant be reset.""" - kc_service = KeycloakService() - org = factory_org_model(org_info=TestOrgInfo.org_anonymous) + """Assert that the otp can be reset.""" + org = factory_org_model(org_info=TestOrgInfo.org_regular_bceid) admin_user = factory_user_model() factory_membership_model(admin_user.id, org.id) admin_claims = TestJwtClaims.get_test_real_user(admin_user.keycloak_guid) - membership = [TestAnonymousMembership.generate_random_user(USER)] - keycloak_service = KeycloakService() + request = KeycloakScenario.create_user_request() - request.user_name = membership[0]["username"] - keycloak_service.add_user(request) - user = kc_service.get_user_by_username(request.user_name) + keycloak_add_user(request) + user = keycloak_get_user_by_username(request.user_name) user = factory_user_model(TestUserInfo.get_bceid_user_with_kc_guid(user.id)) factory_membership_model(user.id, org.id) patch_token_info(admin_claims, monkeypatch) UserService.delete_otp_for_user(user.username, org.id) - user1 = kc_service.get_user_by_username(request.user_name) + user1 = keycloak_get_user_by_username(request.user_name) assert "CONFIGURE_TOTP" in json.loads(user1.value()).get("requiredActions") -def test_create_user_and_add_same_user_name_error_in_kc(session, auth_mock, keycloak_mock): # pylint:disable=unused-argument - """Assert that same user name cannot be added twice.""" - org = factory_org_model(org_info=TestOrgInfo.org_anonymous) - membership = [TestAnonymousMembership.generate_random_user(ADMIN)] - keycloak_service = KeycloakService() - request = KeycloakScenario.create_user_request() - request.user_name = membership[0]["username"] - keycloak_service.add_user(request) - users = UserService.create_user_and_add_membership(membership, org.id, single_mode=True) - assert users["users"][0]["http_status"] == 409 - assert users["users"][0]["error"] == "The username is already taken" - - -def test_create_user_and_add_same_user_name_error_in_db(session, auth_mock, keycloak_mock): # pylint:disable=unused-argument - """Assert that same user name cannot be added twice.""" - org = factory_org_model(org_info=TestOrgInfo.org_anonymous) - user = factory_user_model(TestUserInfo.user_bcros) - factory_membership_model(user.id, org.id) - new_members = TestAnonymousMembership.generate_random_user(ADMIN) - new_members["username"] = user.username.replace(f"{IdpHint.BCROS.value}/", "") - membership = [new_members] - users = UserService.create_user_and_add_membership(membership, org.id, single_mode=True) - assert users["users"][0]["http_status"] == 409 - assert users["users"][0]["error"] == "The username is already taken" - - -def test_create_user_and_add_transaction_membership(session, auth_mock, keycloak_mock): # pylint:disable=unused-argument - """Assert transactions works fine.""" - org = factory_org_model(org_info=TestOrgInfo.org_anonymous) - membership = [TestAnonymousMembership.generate_random_user(ADMIN)] - with patch("auth_api.models.Membership.flush", side_effect=Exception("mocked error")): - users = UserService.create_user_and_add_membership(membership, org.id, single_mode=True) - - user_name = IdpHint.BCROS.value + "/" + membership[0]["username"] - assert len(users["users"]) == 1 - assert users["users"][0]["username"] == membership[0]["username"] - assert users["users"][0]["http_status"] == 500 - assert users["users"][0]["error"] == "Adding User Failed" - - # make sure no records are created - user = UserModel.find_by_username(user_name) - assert user is None - user = UserModel.find_by_username(membership[0]["username"]) - assert user is None - members = MembershipModel.find_members_by_org_id(org.id) - # only one member should be there since its a STAFF created org - assert len(members) == 0 - - -def test_create_user_and_add_transaction_membership_1(session, auth_mock, keycloak_mock): # pylint:disable=unused-argument - """Assert transactions works fine.""" - org = factory_org_model(org_info=TestOrgInfo.org_anonymous) - membership = [TestAnonymousMembership.generate_random_user(ADMIN)] - with patch("auth_api.models.User.flush", side_effect=Exception("mocked error")): - users = UserService.create_user_and_add_membership(membership, org.id, single_mode=True) - - user_name = IdpHint.BCROS.value + "/" + membership[0]["username"] - assert len(users["users"]) == 1 - assert users["users"][0]["username"] == membership[0]["username"] - assert users["users"][0]["http_status"] == 500 - assert users["users"][0]["error"] == "Adding User Failed" - - # make sure no records are created - user = UserModel.find_by_username(user_name) - assert user is None - user = UserModel.find_by_username(membership[0]["username"]) - assert user is None - members = MembershipModel.find_members_by_org_id(org.id) - # only one member should be there since its a STAFF created org - assert len(members) == 0 - - -def test_create_user_and_add_membership_admin_skip_auth_mode(session, auth_mock, keycloak_mock): # pylint:disable=unused-argument - """Assert that an admin can be added as anonymous.""" - org = factory_org_model(org_info=TestOrgInfo.org_anonymous) - membership = [TestAnonymousMembership.generate_random_user(COORDINATOR)] - users = UserService.create_user_and_add_membership(membership, org.id, single_mode=True) - assert len(users["users"]) == 1 - assert users["users"][0]["username"] == IdpHint.BCROS.value + "/" + membership[0]["username"] - assert users["users"][0]["type"] == Role.ANONYMOUS_USER.name - - members = MembershipModel.find_members_by_org_id(org.id) - - # only one member should be there since its a STAFF created org - assert len(members) == 1 - assert members[0].membership_type_code == COORDINATOR - - -def test_create_user_and_add_membership_admin_bulk_mode(session, auth_mock, keycloak_mock, monkeypatch): # pylint:disable=unused-argument - """Assert that an admin can add a member.""" - org = factory_org_model(org_info=TestOrgInfo.org_anonymous) - user = factory_user_model() - factory_membership_model(user.id, org.id) - factory_product_model(org.id, product_code=ProductCode.DIR_SEARCH.value) - claims = TestJwtClaims.get_test_real_user(user.keycloak_guid) - - patch_token_info(claims, monkeypatch) - membership = [TestAnonymousMembership.generate_random_user(USER)] - users = UserService.create_user_and_add_membership(membership, org.id) - - assert len(users["users"]) == 1 - assert users["users"][0]["username"] == IdpHint.BCROS.value + "/" + membership[0]["username"] - assert users["users"][0]["type"] == Role.ANONYMOUS_USER.name - - members = MembershipModel.find_members_by_org_id(org.id) - - # staff didnt create members..so count is count of owner+other 1 member - assert len(members) == 2 - - -def test_create_user_add_membership_reenable(session, auth_mock, keycloak_mock, monkeypatch): # pylint:disable=unused-argument - """Assert that an admin can add a member.""" - org = factory_org_model(org_info=TestOrgInfo.org_anonymous) - user = factory_user_model() - factory_membership_model(user.id, org.id) - factory_product_model(org.id, product_code=ProductCode.DIR_SEARCH.value) - claims = TestJwtClaims.get_test_real_user(user.keycloak_guid) - - patch_token_info(claims, monkeypatch) - anon_member = TestAnonymousMembership.generate_random_user(USER) - membership = [anon_member] - users = UserService.create_user_and_add_membership(membership, org.id) - user_name = IdpHint.BCROS.value + "/" + membership[0]["username"] - assert len(users["users"]) == 1 - assert users["users"][0]["username"] == user_name - assert users["users"][0]["type"] == Role.ANONYMOUS_USER.name - - members = MembershipModel.find_members_by_org_id(org.id) - - # staff didnt create members..so count is count of owner+other 1 member - assert len(members) == 2 - - # assert cant be readded - users = UserService.create_user_and_add_membership(membership, org.id) - assert users["users"][0]["http_status"] == 409 - assert users["users"][0]["error"] == "The username is already taken" - - # deactivate everything and try again - - anon_user = UserModel.find_by_username(user_name) - anon_user.status = Status.INACTIVE.value - anon_user.save() - membership_model = MembershipModel.find_membership_by_userid(anon_user.id) - membership_model.status = Status.INACTIVE.value - - update_user_request = KeycloakUser() - update_user_request.user_name = membership[0]["username"] - update_user_request.enabled = False - KeycloakService.update_user(update_user_request) - - org2 = factory_org_model(org_info=TestOrgInfo.org_anonymous_2, org_type_info={"code": "BASIC"}) - - factory_membership_model(user.id, org2.id) - factory_product_model(org2.id, product_code=ProductCode.DIR_SEARCH.value) - users = UserService.create_user_and_add_membership(membership, org2.id) - assert users["users"][0]["http_status"] == 409 - assert users["users"][0]["error"] == "The username is already taken" - - # add to same org.Should work - users = UserService.create_user_and_add_membership(membership, org.id) - assert len(users["users"]) == 1 - assert users["users"][0]["username"] == IdpHint.BCROS.value + "/" + membership[0]["username"] - assert users["users"][0]["type"] == Role.ANONYMOUS_USER.name - - -def test_create_user_and_add_membership_admin_bulk_mode_unauthorised(session, auth_mock, keycloak_mock, monkeypatch): # pylint:disable=unused-argument - """Assert that bulk operation cannot be performed by unauthorised users.""" - org = factory_org_model(org_info=TestOrgInfo.org_anonymous) - user = factory_user_model() - factory_membership_model(user.id, org.id) - membership = [TestAnonymousMembership.generate_random_user(USER)] - - with pytest.raises(HTTPException) as excinfo: - patch_token_info(TestJwtClaims.public_user_role, monkeypatch) - UserService.create_user_and_add_membership(membership, org.id) - assert excinfo.value.code == 403 - - -def test_create_user_and_add_membership_admin_bulk_mode_multiple(session, auth_mock, keycloak_mock, monkeypatch): # pylint:disable=unused-argument - """Assert that an admin can add a group of members.""" - org = factory_org_model(org_info=TestOrgInfo.org_anonymous) - user = factory_user_model() - factory_membership_model(user.id, org.id) - factory_product_model(org.id, product_code=ProductCode.DIR_SEARCH.value) - claims = TestJwtClaims.get_test_real_user(user.keycloak_guid) - membership = [ - TestAnonymousMembership.generate_random_user(USER), - TestAnonymousMembership.generate_random_user(COORDINATOR), - ] - - patch_token_info(claims, monkeypatch) - users = UserService.create_user_and_add_membership(membership, org.id) - - assert len(users["users"]) == 2 - assert users["users"][0]["username"] == IdpHint.BCROS.value + "/" + membership[0]["username"] - assert users["users"][0]["type"] == Role.ANONYMOUS_USER.name - assert users["users"][1]["username"] == IdpHint.BCROS.value + "/" + membership[1]["username"] - assert users["users"][1]["type"] == Role.ANONYMOUS_USER.name - - members = MembershipModel.find_members_by_org_id(org.id) - - # staff didnt create members..so count is count of owner+other 2 members - assert len(members) == 3 - - -def test_create_user_and_add_membership_member_error_skip_auth_mode(session, auth_mock, keycloak_mock): # pylint:disable=unused-argument - """Assert that an member cannot be added as anonymous in single_mode mode.""" - org = factory_org_model(org_info=TestOrgInfo.org_anonymous) - membership = [TestAnonymousMembership.generate_random_user(USER)] - with pytest.raises(BusinessException) as exception: - UserService.create_user_and_add_membership(membership, org.id, single_mode=True) - assert exception.value.code == Error.INVALID_USER_CREDENTIALS.name - - -def test_create_user_and_add_membership_multiple_error_skip_auth_mode(session, auth_mock, keycloak_mock, monkeypatch): # pylint:disable=unused-argument - """Assert that multiple user cannot be created in single_mode mode.""" - org = factory_org_model(org_info=TestOrgInfo.org_anonymous) - membership = [ - TestAnonymousMembership.generate_random_user(USER), - TestAnonymousMembership.generate_random_user(COORDINATOR), - ] - with pytest.raises(BusinessException) as exception: - patch_token_info(TestJwtClaims.public_user_role, monkeypatch) - UserService.create_user_and_add_membership(membership, org.id, single_mode=True) - assert exception.value.code == Error.INVALID_USER_CREDENTIALS.name - - def test_user_save_by_token_fail(session, monkeypatch): # pylint: disable=unused-argument """Assert that a user cannot not be created.""" with patch.object(UserModel, "create_from_jwt_token", return_value=None): diff --git a/auth-api/tests/utilities/factory_scenarios.py b/auth-api/tests/utilities/factory_scenarios.py index 58cc8156d..f18237ec7 100644 --- a/auth-api/tests/utilities/factory_scenarios.py +++ b/auth-api/tests/utilities/factory_scenarios.py @@ -209,18 +209,6 @@ class TestJwtClaims(dict, Enum): "loginSource": LoginSource.STAFF.value, } - staff_admin_dir_search_role = { - "iss": CONFIG.JWT_OIDC_TEST_ISSUER, - "sub": "f7a4a1d3-73a8-4cbc-a40f-bb1145302064", - "idp_userid": "f7a4a1d3-73a8-4cbc-a40f-bb1145302064", - "firstname": fake.first_name(), - "lastname": fake.last_name(), - "preferred_username": fake.user_name(), - "realm_access": {"roles": ["staff", "create_accounts", "view_accounts", "edit"]}, - "roles": ["staff", "create_accounts"], - "loginSource": LoginSource.STAFF.value, - } - bcol_admin_role = { "iss": CONFIG.JWT_OIDC_TEST_ISSUER, "sub": "f7a4a1d3-73a8-4cbc-a40f-bb1145302064", @@ -313,19 +301,6 @@ class TestJwtClaims(dict, Enum): "realm_access": {"roles": ["tester"]}, } - anonymous_bcros_role = { - "iss": CONFIG.JWT_OIDC_TEST_ISSUER, - "sub": "f7a4a1d3-73a8-4cbc-a40f-bb1145302069", - "idp_userid": "f7a4a1d3-73a8-4cbc-a40f-bb1145302069", - "firstname": fake.first_name(), - "lastname": fake.last_name(), - "preferred_username": f"{IdpHint.BCROS.value}/{fake.user_name()}", - "accessType": "ANONYMOUS", - "loginSource": "BCROS", - "realm_access": {"roles": ["edit", "anonymous_user", "public_user"]}, - "product_code": "DIR_SEARCH", - } - tester_bceid_role = { "iss": CONFIG.JWT_OIDC_TEST_ISSUER, "sub": "f7a4a1d3-73a8-4cbc-a40f-bb1145302064", @@ -410,21 +385,6 @@ def get_payment_method_input_with_revenue(payment_method: PaymentMethod = Paymen return {"paymentInfo": {"paymentMethod": payment_method.value, "revenueAccount": revenue_account_details}} -class TestAnonymousMembership(dict, Enum): - """Test scenarios of org status.""" - - __test__ = False - - @staticmethod - def generate_random_user(membership: str): - """Return user with keycloak guid.""" - return { - "username": "".join(choice(ascii_uppercase) for i in range(5)), - "password": "firstuser", - "membershipType": membership, - } - - class TestOrgStatusInfo(dict, Enum): """Test scenarios of org status.""" @@ -452,9 +412,7 @@ class TestOrgInfo(dict, Enum): org3 = {"name": "Third Orgs"} org4 = {"name": "fourth Orgs"} org5 = {"name": "fifth Orgs"} - org_anonymous = {"name": "My Test Anon Org", "accessType": "ANONYMOUS"} org_govm = {"name": "My Test Anon Org", "branchName": "Bar", "accessType": AccessType.GOVM.value} - org_anonymous_2 = {"name": "Another test org", "accessType": "ANONYMOUS"} org_premium = {"name": "Another test org", "typeCode": OrgType.PREMIUM.value} invalid = {"foo": "bar"} invalid_name_space = {"name": ""} @@ -777,27 +735,6 @@ class TestUserInfo(dict, Enum): "keycloak_guid": "1b20db59-19a0-4727-affe-c6f64309fd04", "idp_userid": "1b20db59-19a0-4727-affe-c6f64309fd04", } - user_anonymous_1 = { - "username": fake.user_name(), - "password": "Password@1234", - } - user_bcros = { - "username": f"{IdpHint.BCROS.value}/{fake.user_name()}", - "firstname": fake.first_name(), - "lastname": fake.last_name(), - "roles": "{edit, uma_authorization, staff}", - # dont add a kc_guid - } - - user_bcros_active = { - "username": f"{IdpHint.BCROS.value}/{fake.user_name()}", - "firstname": fake.first_name(), - "lastname": fake.last_name(), - "roles": "{edit, uma_authorization, staff}", - "keycloak_guid": uuid.uuid4(), - "idp_userid": uuid.uuid4(), - "access_type": "ANONYMOUS", - } user_bceid_tester = { "username": f"{fake.user_name()}@{IdpHint.BCEID.value}", "firstname": fake.first_name(), diff --git a/auth-api/tests/utilities/factory_utils.py b/auth-api/tests/utilities/factory_utils.py index 5b3ed64da..cff10bee0 100644 --- a/auth-api/tests/utilities/factory_utils.py +++ b/auth-api/tests/utilities/factory_utils.py @@ -17,11 +17,16 @@ """ import datetime +from string import Template +import requests +from flask import current_app from requests.exceptions import HTTPError from sqlalchemy import event from auth_api.config import get_named_config +from auth_api.exceptions import BusinessException +from auth_api.exceptions.errors import Error from auth_api.models import ActivityLog as ActivityLogModel from auth_api.models import Affiliation as AffiliationModel from auth_api.models import Contact as ContactModel @@ -40,9 +45,10 @@ from auth_api.services import Entity as EntityService from auth_api.services import Org as OrgService from auth_api.services import Task as TaskService +from auth_api.services.keycloak import KeycloakService +from auth_api.services.keycloak_user import KeycloakUser from auth_api.utils.enums import ( - AccessType, - InvitationType, + ContentType, OrgType, ProductSubscriptionStatus, TaskRelationshipStatus, @@ -89,9 +95,7 @@ def factory_entity_service(entity_info: dict = TestEntityInfo.entity1): def factory_user_model(user_info: dict = dict(TestUserInfo.user1)): """Produce a user model.""" roles = user_info.get("roles", None) - if user_info.get("access_type", None) == AccessType.ANONYMOUS.value: - user_type = Role.ANONYMOUS_USER.name - elif Role.STAFF.value in roles: + if Role.STAFF.value in roles: user_type = Role.STAFF.name else: user_type = None @@ -113,7 +117,7 @@ def factory_user_model(user_info: dict = dict(TestUserInfo.user1)): def factory_user_model_with_contact(user_info: dict = dict(TestUserInfo.user1), keycloak_guid=None): """Produce a user model.""" - user_type = Role.ANONYMOUS_USER.name if user_info.get("access_type", None) == AccessType.ANONYMOUS.value else None + user_type = None user = UserModel( username=user_info.get("username", user_info.get("preferred_username")), firstname=user_info["firstname"], @@ -257,21 +261,6 @@ def factory_invitation( } -def factory_invitation_anonymous( - org_id, - email="abc123@email.com", - sent_date=datetime.datetime.now().strftime("Y-%m-%d %H:%M:%S"), - membership_type="ADMIN", -): - """Produce an invite for the given org and email.""" - return { - "recipientEmail": email, - "sentDate": sent_date, - "type": InvitationType.DIRECTOR_SEARCH.value, - "membership": [{"membershipType": membership_type, "orgId": org_id}], - } - - def factory_document_model(version_id, doc_type, content, content_type="text/html"): """Produce a Document model.""" document = DocumentsModel(version_id=version_id, type=doc_type, content=content, content_type=content_type) @@ -481,3 +470,67 @@ def convert_org_to_staff_org(org_id: int, type_code: OrgType): org_db.save() event.listen(OrgModel, "before_update", receive_before_update, raw=True) event.listen(OrgModel, "before_insert", receive_before_insert) + + +def keycloak_get_user_by_username(username: str, admin_token=None) -> KeycloakUser: + """Get user from Keycloak by username. Test utility only.""" + user = None + base_url = current_app.config.get("KEYCLOAK_BASE_URL") + realm = current_app.config.get("KEYCLOAK_REALMNAME") + timeout = current_app.config.get("CONNECT_TIMEOUT", 60) + if not admin_token: + admin_token = KeycloakService._get_admin_token() + + headers = {"Content-Type": ContentType.JSON.value, "Authorization": f"Bearer {admin_token}"} + + query_user_url = Template(f"{base_url}/auth/admin/realms/{realm}/users?username=$username").substitute( + username=username + ) + response = requests.get(query_user_url, headers=headers, timeout=timeout) + response.raise_for_status() + if len(response.json()) == 1: + user = KeycloakUser(response.json()[0]) + return user + + +def keycloak_add_user(user: KeycloakUser, return_if_exists: bool = False, throw_error_if_exists: bool = False): + """Add user to Keycloak. Test utility only.""" + config = current_app.config + admin_token = KeycloakService._get_admin_token() + + base_url = config.get("KEYCLOAK_BASE_URL") + realm = config.get("KEYCLOAK_REALMNAME") + timeout = config.get("CONNECT_TIMEOUT", 60) + + if return_if_exists or throw_error_if_exists: + existing_user = keycloak_get_user_by_username(user.user_name, admin_token=admin_token) + if existing_user: + if not throw_error_if_exists: + return existing_user + raise BusinessException(Error.USER_ALREADY_EXISTS_IN_KEYCLOAK, None) + + headers = {"Content-Type": ContentType.JSON.value, "Authorization": f"Bearer {admin_token}"} + + add_user_url = f"{base_url}/auth/admin/realms/{realm}/users" + response = requests.post(add_user_url, data=user.value(), headers=headers, timeout=timeout) + response.raise_for_status() + + return keycloak_get_user_by_username(user.user_name, admin_token) + + +def keycloak_delete_user_by_username(username): + """Delete user from Keycloak by username. Test utility only.""" + admin_token = KeycloakService._get_admin_token() + headers = {"Content-Type": ContentType.JSON.value, "Authorization": f"Bearer {admin_token}"} + + base_url = current_app.config.get("KEYCLOAK_BASE_URL") + realm = current_app.config.get("KEYCLOAK_REALMNAME") + timeout = current_app.config.get("CONNECT_TIMEOUT", 60) + user = keycloak_get_user_by_username(username) + + if not user: + raise BusinessException(Error.DATA_NOT_FOUND, None) + + delete_user_url = f"{base_url}/auth/admin/realms/{realm}/users/{user.id}" + response = requests.delete(delete_user_url, headers=headers, timeout=timeout) + response.raise_for_status() diff --git a/queue_services/account-mailer/poetry.lock b/queue_services/account-mailer/poetry.lock index 98d124c27..f16578da9 100644 --- a/queue_services/account-mailer/poetry.lock +++ b/queue_services/account-mailer/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. [[package]] name = "aiofiles" @@ -166,6 +166,21 @@ yarl = ">=1.17.0,<2.0" [package.extras] speedups = ["Brotli (>=1.2) ; platform_python_implementation == \"CPython\"", "aiodns (>=3.3.0)", "backports.zstd ; platform_python_implementation == \"CPython\" and python_version < \"3.14\"", "brotlicffi (>=1.2) ; platform_python_implementation != \"CPython\""] +[[package]] +name = "aiohttp-retry" +version = "2.9.1" +description = "Simple retry client for aiohttp" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "aiohttp_retry-2.9.1-py3-none-any.whl", hash = "sha256:66d2759d1921838256a05a3f80ad7e724936f083e35be5abb5e16eed6be6dc54"}, + {file = "aiohttp_retry-2.9.1.tar.gz", hash = "sha256:8eb75e904ed4ee5c2ec242fefe85bf04240f685391c4879d8f541d6028ff01f1"}, +] + +[package.dependencies] +aiohttp = "*" + [[package]] name = "aiosignal" version = "1.4.0" @@ -258,6 +273,7 @@ develop = false [package.dependencies] aiohttp = "^3.13.3" +aiohttp-retry = "^2.9.1" attrs = "24.2.0" bcrypt = "^4.2.0" CacheControl = "0.14.0" @@ -302,8 +318,8 @@ Werkzeug = "^3.1.5" [package.source] type = "git" url = "https://github.com/seeker25/sbc-auth.git" -reference = "fix_urllib_dep" -resolved_reference = "a6d5008fff90f5f16a7a28a43a86a91b0edb64c5" +reference = "32356" +resolved_reference = "aa17019fa3384b2cfc9433b4cb2100f19e5d43b5" subdirectory = "auth-api" [[package]] @@ -3446,4 +3462,4 @@ test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-it [metadata] lock-version = "2.1" python-versions = ">=3.12,<3.13" -content-hash = "1396a85e660465428226e9c6774d3dd5d370fe8a0f8884955bf9b7c4b81d5a08" +content-hash = "2e38bbfadfea919e29bafb6eb8d9014a687bfa594d963fc1e8272d31025b0660" diff --git a/queue_services/account-mailer/pyproject.toml b/queue_services/account-mailer/pyproject.toml index 41f79d642..6d0df2289 100644 --- a/queue_services/account-mailer/pyproject.toml +++ b/queue_services/account-mailer/pyproject.toml @@ -38,7 +38,7 @@ urllib3 = "2.6.3" zipp = "3.19.1" # VCS dependencies -auth-api = { git = "https://github.com/seeker25/sbc-auth.git", branch = "fix_urllib_dep", subdirectory = "auth-api" } +auth-api = { git = "https://github.com/seeker25/sbc-auth.git", branch = "32356", subdirectory = "auth-api" } simple-cloudevent = { git = "https://github.com/daxiom/simple-cloudevent.py.git" } cloud-sql-python-connector = "^1.13.0" pkginfo = "^1.12.1.2" diff --git a/queue_services/auth-queue/poetry.lock b/queue_services/auth-queue/poetry.lock index da90f8977..facb3ba4f 100644 --- a/queue_services/auth-queue/poetry.lock +++ b/queue_services/auth-queue/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. [[package]] name = "aiofiles" @@ -166,6 +166,21 @@ yarl = ">=1.17.0,<2.0" [package.extras] speedups = ["Brotli (>=1.2) ; platform_python_implementation == \"CPython\"", "aiodns (>=3.3.0)", "backports.zstd ; platform_python_implementation == \"CPython\" and python_version < \"3.14\"", "brotlicffi (>=1.2) ; platform_python_implementation != \"CPython\""] +[[package]] +name = "aiohttp-retry" +version = "2.9.1" +description = "Simple retry client for aiohttp" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "aiohttp_retry-2.9.1-py3-none-any.whl", hash = "sha256:66d2759d1921838256a05a3f80ad7e724936f083e35be5abb5e16eed6be6dc54"}, + {file = "aiohttp_retry-2.9.1.tar.gz", hash = "sha256:8eb75e904ed4ee5c2ec242fefe85bf04240f685391c4879d8f541d6028ff01f1"}, +] + +[package.dependencies] +aiohttp = "*" + [[package]] name = "aiosignal" version = "1.4.0" @@ -258,6 +273,7 @@ develop = false [package.dependencies] aiohttp = "^3.13.3" +aiohttp-retry = "^2.9.1" attrs = "24.2.0" bcrypt = "^4.2.0" CacheControl = "0.14.0" @@ -302,8 +318,8 @@ Werkzeug = "^3.1.5" [package.source] type = "git" url = "https://github.com/seeker25/sbc-auth.git" -reference = "fix_urllib_dep" -resolved_reference = "a6d5008fff90f5f16a7a28a43a86a91b0edb64c5" +reference = "32356" +resolved_reference = "aa17019fa3384b2cfc9433b4cb2100f19e5d43b5" subdirectory = "auth-api" [[package]] @@ -3629,4 +3645,4 @@ test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-it [metadata] lock-version = "2.1" python-versions = ">=3.12,<3.13" -content-hash = "70db0007c3178bc624587e18227184563c98f20f9155aff3fc71cb4bffc6ab4e" +content-hash = "6f768ff77fedd2fb28f2a70f84b59dc76ab60c18e230d2ccd375cb5b8ab6180d" diff --git a/queue_services/auth-queue/pyproject.toml b/queue_services/auth-queue/pyproject.toml index 976baddda..f2e6f842d 100644 --- a/queue_services/auth-queue/pyproject.toml +++ b/queue_services/auth-queue/pyproject.toml @@ -39,7 +39,7 @@ zipp = "3.19.1" # VCS dependencies -auth-api = { git = "https://github.com/seeker25/sbc-auth.git", branch = "fix_urllib_dep", subdirectory = "auth-api" } +auth-api = { git = "https://github.com/seeker25/sbc-auth.git", branch = "32356", subdirectory = "auth-api" } simple-cloudevent = { git = "https://github.com/daxiom/simple-cloudevent.py.git" } cloud-sql-python-connector = "^1.13.0"