def test_user_find_by_jwt_token(session): """Assert that a User can be stored in the service. Start with a blank database. """ user = User(username='******', roles='{edit, uma_authorization, staff}', keycloak_guid='1b20db59-19a0-4727-affe-c6f64309fd04') session.add(user) session.commit() token = { 'preferred_username': '******', 'sub': '1b20db59-19a0-4727-affe-c6f64309fd04', 'realm_access': { 'roles': [ 'edit', 'uma_authorization', 'basic' ] } } u = User.find_by_jwt_token(token) assert u.id is not None
def test_user_save(session): # pylint: disable=unused-argument """Assert User record is saved.""" user = User(username='******', keycloak_guid='1b20db59-19a0-4727-affe-c6f64309fd04') user.save() assert user.id is not None
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 reset(token_info: Dict): """Cleanup all the data from all tables create by the provided user id.""" if Role.TESTER.value in token_info.get('realm_access').get('roles'): # pylint: disable=too-many-nested-blocks user = UserModel.find_by_jwt_token(token_info) if user: # TODO need to find a way to avoid using protected function for model_class in db.Model._decl_class_registry.values(): # pylint:disable=protected-access # skip version classes if not (hasattr(model_class, 'transaction_id') and hasattr(model_class, 'end_transaction_id')): if hasattr(model_class, 'created_by_id'): for model in model_class.query.filter_by( created_by_id=user.id).all(): model.reset() if hasattr(model_class, 'modified_by_id'): for model in model_class.query.filter_by( modified_by_id=user.id).all(): model.reset() # check the user is still exists or not user = UserModel.find_by_jwt_token(token_info) if user: user.modified_by = None user.modified_by_id = None user.reset() # Reset opt from keycloak if from BCEID login_source = token_info.get('loginSource', None) if login_source == LoginSource.BCEID.value: KeycloakService.reset_otp(token_info.get('sub'))
def test_update_from_jwt_token(session): # pylint: disable=unused-argument """Assert User is updated from a JWT and an existing User model.""" token = { 'preferred_username': '******', 'firstname': 'Bobby', 'lasname': 'Joe', 'realm_access': { 'roles': ['edit', 'uma_authorization', 'basic'] }, 'sub': '1b20db59-19a0-4727-affe-c6f64309fd04' } user = User.create_from_jwt_token(token, 'Bobby', 'Joe') updated_token = { 'preferred_username': '******', 'firstname': 'Bob', 'lastname': 'Joe', 'realm_access': { 'roles': ['edit', 'uma_authorization', 'basic'] }, 'sub': '1b20db59-19a0-4727-affe-c6f64309fd04' } user = User.update_from_jwt_token(user, updated_token, 'Bob', 'Joe') assert user.firstname == 'Bob'
def factory_user_model(username, roles, keycloak_guid): """Return a valid user object stamped with the supplied designation.""" from auth_api.models import User as UserModel user = UserModel(username=username, roles=roles, keycloak_guid=keycloak_guid) user.save() return user
def test_find_by_username(session): """Assert User can be found by the most current username.""" user = User(username='******', roles='{edit, uma_authorization, staff}') session.add(user) session.commit() u = User.find_by_username('CP1234567') assert u.id is not None
def test_find_by_username(session): """Assert User can be found by the most current username.""" user = User(username='******', keycloak_guid='1b20db59-19a0-4727-affe-c6f64309fd04') session.add(user) session.commit() u = User.find_by_username('CP1234567') assert u.id is not None
def delete_anonymous_user(user_name, token_info: Dict = None): """ Delete User Profile. 1) check if the token user is admin/owner of the current user 2) disable the user from kc 3) set user status as INACTIVE 4) set membership as inactive """ admin_user: UserModel = UserModel.find_by_jwt_token(token_info) if not admin_user: raise BusinessException(Error.DATA_NOT_FOUND, None) if admin_user.status == UserStatus.INACTIVE.value: raise BusinessException(Error.DELETE_FAILED_INACTIVE_USER, None) # handle validations. user = UserModel.find_by_username(user_name) membership = MembershipModel.find_membership_by_userid(user.id) org_id = membership.org_id is_valid_action = False # admin/owner deleteion admin_user_membership = MembershipModel.find_membership_by_user_and_org( admin_user.id, org_id) if admin_user_membership.membership_type_code in [ADMIN]: is_valid_action = True # staff admin deleteion is_staff_admin = token_info and Role.STAFF_CREATE_ACCOUNTS.value in token_info.get( 'realm_access').get('roles') if is_staff_admin: is_valid_action = True # self deletion if user.keycloak_guid == admin_user.keycloak_guid: is_valid_action = True # is the only owner getting deleted if is_valid_action and membership.membership_type_code == ADMIN: count_of_owners = MembershipModel.get_count_active_owner_org_id( org_id) if count_of_owners == 1: is_valid_action = False if not is_valid_action: raise BusinessException(Error.INVALID_USER_CREDENTIALS, None) user.is_terms_of_use_accepted = False user.status = UserStatus.INACTIVE.value user.save() membership.status = Status.INACTIVE.value membership.save() update_user_request = KeycloakUser() update_user_request.user_name = user_name.replace( IdpHint.BCROS.value + '/', '') update_user_request.enabled = False KeycloakService.update_user(update_user_request)
def factory_user_model(username, firstname=None, lastname=None, roles=None, keycloak_guid=None): """Return a valid user object stamped with the supplied designation.""" user = UserModel(username=username, firstname=firstname, lastname=lastname, roles=roles, keycloak_guid=keycloak_guid) user.save() return user
def _handle_bceid_status_and_notification(org, origin_url, token_info): org.status_code = OrgStatus.PENDING_STAFF_REVIEW.value user = UserModel.find_by_jwt_token(token=token_info) # Org.send_staff_review_account_reminder(user, org.id, origin_url) # create a staff review task for this account task_type = TaskTypePrefix.NEW_ACCOUNT_STAFF_REVIEW.value task_info = { 'name': org.name, 'relationshipId': org.id, 'relatedTo': user.id, 'dateSubmitted': datetime.today(), 'relationshipType': TaskRelationshipType.ORG.value, 'type': task_type, 'status': TaskStatus.OPEN.value, 'relationship_status': TaskRelationshipStatus.PENDING_STAFF_REVIEW.value } TaskService.create_task(task_info=task_info, user=user, origin_url=origin_url, do_commit=False)
def publish_activity(activity: Activity): # pylint:disable=unused-argument """Publish the activity asynchronously, using the given details.""" try: # find user_id if haven't passed in if not activity.actor_id and g and 'jwt_oidc_token_info' in g: user: UserModel = UserModel.find_by_jwt_token() activity.actor_id = user.id if user else None data = { 'actorId': activity.actor_id, 'action': activity.action, 'itemType': 'ACCOUNT', 'itemName': activity.name, 'itemId': activity.id, 'itemValue': activity.value, 'orgId': activity.org_id, 'remoteAddr': fetch_remote_addr(), 'createdAt': f'{datetime.now()}' } source = 'https://api.auth.bcregistry.gov.bc.ca/v1/accounts' payload = { 'specversion': '1.x-wip', 'type': 'bc.registry.auth.activity', 'source': source, 'id': str(uuid.uuid1()), 'time': f'{datetime.now()}', 'datacontenttype': 'application/json', 'data': data } publish_response(payload=payload, client_name=CONFIG.NATS_ACTIVITY_CLIENT_NAME, subject=CONFIG.NATS_ACTIVITY_SUBJECT) except Exception as err: # noqa: B902 # pylint: disable=broad-except capture_message('Activity Queue Publish Event Error:' + str(err), level='error') current_app.logger.error('Activity Queue Publish Event Error:', exc_info=True)
def test_delete_user_where_org_has_affiliations(session, auth_mock, keycloak_mock): # pylint:disable=unused-argument """Assert that a user can be deleted.""" user_model = factory_user_model(user_info=TestUserInfo.user_test) contact = factory_contact_model() contact_link = ContactLinkModel() contact_link.contact = contact contact_link.user = user_model contact_link = contact_link.flush() contact_link.commit() org = OrgService.create_org(TestOrgInfo.org1, user_id=user_model.id).as_dict() org_id = org['id'] entity = factory_entity_model(entity_info=TestEntityInfo.entity_lear_mock) affiliation = AffiliationModel(org_id=org_id, entity_id=entity.id) affiliation.save() with pytest.raises(BusinessException) as exception: UserService.delete_user(TestJwtClaims.user_test) assert exception.code == Error.DELETE_FAILED_ONLY_OWNER updated_user = UserModel.find_by_jwt_token(TestJwtClaims.user_test) contacts = UserService.get_contacts(TestJwtClaims.user_test) assert len(contacts) == 1 user_orgs = MembershipModel.find_orgs_for_user(updated_user.id) for org in user_orgs: assert org.status_code == 'ACTIVE'
def change_org_status(self, status_code, suspension_reason_code): """Update the status of the org. Used now for suspending/activate account. 1) check access .only staff can do it now 2) check org status/eligiblity 3) suspend it """ current_app.logger.debug('<change_org_status ') user: UserModel = UserModel.find_by_jwt_token() org_model = self._model org_model.status_code = status_code org_model.decision_made_by = user.username # not sure if a new field is needed for this. if status_code == OrgStatus.SUSPENDED.value: org_model.suspended_on = datetime.today() org_model.suspension_reason_code = suspension_reason_code org_model.save() if status_code == OrgStatus.SUSPENDED.value: suspension_reason_description = SuspensionReasonCode[suspension_reason_code].value \ if suspension_reason_code in [ item.name for item in SuspensionReasonCode] else '' ActivityLogPublisher.publish_activity( Activity(org_model.id, ActivityAction.ACCOUNT_SUSPENSION.value, name=org_model.name, value=suspension_reason_description)) current_app.logger.debug('change_org_status>') return Org(org_model)
def test_update_task(session, keycloak_mock, monkeypatch): # pylint:disable=unused-argument """Assert that a task can be updated.""" user_with_token = TestUserInfo.user_bceid_tester user_with_token['keycloak_guid'] = TestJwtClaims.public_bceid_user['sub'] user = factory_user_model_with_contact(user_with_token) patch_token_info(TestJwtClaims.public_bceid_user, monkeypatch) affidavit_info = TestAffidavit.get_test_affidavit_with_contact() AffidavitService.create_affidavit(affidavit_info=affidavit_info) org = OrgService.create_org(TestOrgInfo.org_with_mailing_address(), user_id=user.id) org_dict = org.as_dict() assert org_dict['org_status'] == OrgStatus.PENDING_STAFF_REVIEW.value token_info = TestJwtClaims.get_test_user(sub=user.keycloak_guid, source=LoginSource.STAFF.value) patch_token_info(token_info, monkeypatch) tasks = TaskService.fetch_tasks(task_status=[TaskStatus.OPEN.value], page=1, limit=10) fetched_tasks = tasks['tasks'] fetched_task = fetched_tasks[0] task_info = {'relationshipStatus': TaskRelationshipStatus.ACTIVE.value} task: TaskModel = TaskModel.find_by_task_id(fetched_task['id']) task = TaskService.update_task(TaskService(task), task_info=task_info) dictionary = task.as_dict() user = UserModel.find_by_id(user.id) assert dictionary['status'] == TaskStatus.COMPLETED.value assert dictionary[ 'relationship_status'] == TaskRelationshipStatus.ACTIVE.value assert user.verified
def test_update_from_jwt_token_no_token(session): # pylint:disable=unused-argument """Assert that a user is not updateable without a token (should return None).""" token = { 'preferred_username': '******', 'firstname': 'Bobby', 'lasname': 'Joe', 'realm_access': { 'roles': ['edit', 'uma_authorization', 'basic'] }, 'sub': '1b20db59-19a0-4727-affe-c6f64309fd04' } existing_user = User.create_from_jwt_token(token, 'Bobby', 'Joe') token = None user = User.update_from_jwt_token(existing_user, token, 'Bobby', 'Joe') assert user is None
def update_terms_of_use(token, is_terms_accepted, terms_of_use_version): """Update terms of use for an existing user.""" current_app.logger.debug('update_terms_of_use') if token is None: raise BusinessException(Error.DATA_NOT_FOUND, None) user = UserModel.update_terms_of_use(token, is_terms_accepted, terms_of_use_version) return User(user)
def find_by_jwt_token(cls, token: dict = None): """Find user from database by user token.""" if not token: return None user_model = UserModel.find_by_jwt_token(token) if not user_model: raise BusinessException(Error.DATA_NOT_FOUND, None) is_anonymous_user = token.get('accessType', None) == AccessType.ANONYMOUS.value # If terms accepted , double check if there is a new TOS in place. If so, update the flag to false. if user_model.is_terms_of_use_accepted: document_type = DocumentType.TERMS_OF_USE_DIRECTOR_SEARCH.value if is_anonymous_user \ else DocumentType.TERMS_OF_USE.value # get the digit version of the terms of service..ie d1 gives 1 ; d2 gives 2..for proper comparison latest_version = util.digitify( DocumentService.find_latest_version_by_type(document_type)) current_version = util.digitify( user_model.terms_of_use_accepted_version) if latest_version > current_version: user_model.is_terms_of_use_accepted = False return User(user_model)
def find_by_jwt_token(cls, **kwargs): """Find user from database by user token.""" user_from_context: UserContext = kwargs['user_context'] if not user_from_context.token_info: return None user_model = UserModel.find_by_jwt_token() if not user_model: if kwargs.get('silent_mode', False): return None raise BusinessException(Error.DATA_NOT_FOUND, None) is_anonymous_user = user_from_context.token_info.get('accessType', None) == AccessType.ANONYMOUS.value is_govm_user = user_from_context.login_source == LoginSource.STAFF.value # If terms accepted , double check if there is a new TOS in place. If so, update the flag to false. if user_model.is_terms_of_use_accepted: if is_anonymous_user: document_type = DocumentType.TERMS_OF_USE_DIRECTOR_SEARCH.value elif is_govm_user: document_type = DocumentType.TERMS_OF_USE_GOVM.value else: document_type = DocumentType.TERMS_OF_USE.value # get the digit version of the terms of service..ie d1 gives 1 ; d2 gives 2..for proper comparison latest_version = util.digitify(DocumentService.find_latest_version_by_type(document_type)) current_version = util.digitify(user_model.terms_of_use_accepted_version) if latest_version > current_version: user_model.is_terms_of_use_accepted = False return User(user_model)
def change_org_status(org_id: int, status_code, suspension_reason_code, token_info: Dict = None): """Update the status of the org. Used now for suspending/activate account. 1) check access .only staff can do it now 2) check org status/eligiblity 3) suspend it """ current_app.logger.debug('<change_org_status ') org_model: OrgModel = OrgModel.find_by_org_id(org_id) user: UserModel = UserModel.find_by_jwt_token(token=token_info) current_app.logger.debug('<setting org status to ') org_model.status_code = status_code org_model.decision_made_by = user.username # not sure if a new field is needed for this. if status_code == OrgStatus.SUSPENDED.value: org_model.suspended_on = datetime.today() org_model.suspension_reason_code = suspension_reason_code org_model.save() current_app.logger.debug('change_org_status>') return Org(org_model)
def get_admins_for_membership(membership_id, status=Status.ACTIVE.value): """Get admins for an org.""" membership = MembershipModel.find_membership_by_id(membership_id) org_id = membership.org_id return UserModel.find_users_by_org_id_by_status_by_roles( org_id, CLIENT_ADMIN_ROLES, status)
def get_member_emails(org_id, roles): """Get emails for the user role passed in.""" member_list = UserModel.find_users_by_org_id_by_status_by_roles( org_id, roles, Status.ACTIVE.value) member_emails = ','.join( [str(x.contacts[0].contact.email) for x in member_list if x.contacts]) return member_emails
def delete_org( org_id, token_info: Dict = None, ): """Soft-Deletes an Org. It should not be deletable if there are members or business associated with the org """ # Check authorization for the user current_app.logger.debug('<org Inactivated') check_auth(token_info, one_of_roles=OWNER, org_id=org_id) org: OrgModel = OrgModel.find_by_org_id(org_id) if not org: raise BusinessException(Error.DATA_NOT_FOUND, None) count_members = len([ member for member in org.members if member.status in VALID_STATUSES ]) if count_members > 1 or len(org.affiliated_entities) >= 1: raise BusinessException(Error.ORG_CANNOT_BE_DISSOLVED, None) org.delete() # Remove user from thr group if the user doesn't have any other orgs membership user = UserModel.find_by_jwt_token(token=token_info) if len(MembershipModel.find_orgs_for_user(user.id)) == 0: KeycloakService.remove_from_account_holders_group( user.keycloak_guid) current_app.logger.debug('org Inactivated>')
def _create_gov_account_task(org_model: OrgModel): # create a staff review task for this account task_type = TaskTypePrefix.GOVM_REVIEW.value user: UserModel = UserModel.find_by_jwt_token() task_info = { 'name': org_model.name, 'relationshipId': org_model.id, 'relatedTo': user.id, 'dateSubmitted': datetime.today(), 'relationshipType': TaskRelationshipType.ORG.value, 'type': task_type, 'action': TaskAction.ACCOUNT_REVIEW.value, 'status': TaskStatus.OPEN.value, 'relationship_status': TaskRelationshipStatus.PENDING_STAFF_REVIEW.value } TaskService.create_task(task_info=task_info, do_commit=False)
def approve_or_reject(org_id: int, is_approved: bool, token_info: Dict, origin_url: str = None): """Mark the affidavit as approved or rejected.""" current_app.logger.debug('<find_affidavit_by_org_id ') # Get the org and check what's the current status org: OrgModel = OrgModel.find_by_org_id(org_id) # Current User user: UserModel = UserModel.find_by_jwt_token(token=token_info) # If status is PENDING_AFFIDAVIT_REVIEW handle affidavit approve process, else raise error if org.status_code == OrgStatus.PENDING_AFFIDAVIT_REVIEW.value: AffidavitService.approve_or_reject(org_id, is_approved, user) else: raise BusinessException(Error.INVALID_INPUT, None) if is_approved: org.status_code = OrgStatus.ACTIVE.value else: org.status_code = OrgStatus.REJECTED.value org.decision_made_by = user.username org.decision_made_on = datetime.now() # TODO Publish to activity stream org.save() # Find admin email address admin_email = ContactLinkModel.find_by_user_id(org.members[0].user.id).contact.email Org.send_approved_rejected_notification(admin_email, org.name, org.status_code, origin_url) current_app.logger.debug('>find_affidavit_by_org_id ') return Org(org)
def send_staff_review_account_reminder( relationship_id, task_relationship_type=TaskRelationshipType.ORG.value): """Send staff review account reminder notification.""" current_app.logger.debug('<send_staff_review_account_reminder') user: UserModel = UserModel.find_by_jwt_token() recipient = current_app.config.get('STAFF_ADMIN_EMAIL') # Get task id that is related with the task. Task Relationship Type can be ORG, PRODUCT etc. task = TaskModel.find_by_task_relationship_id( task_relationship_type=task_relationship_type, relationship_id=relationship_id) context_path = f'review-account/{task.id}' app_url = f"{g.get('origin_url', '')}/{current_app.config.get('AUTH_WEB_TOKEN_CONFIRM_PATH')}" review_url = f'{app_url}/{context_path}' first_name = user.firstname last_name = user.lastname data = { 'emailAddresses': recipient, 'contextUrl': review_url, 'userFirstName': first_name, 'userLastName': last_name } try: publish_to_mailer('staffReviewAccount', org_id=relationship_id, data=data) current_app.logger.debug('<send_staff_review_account_reminder') except Exception as e: # noqa=B901 current_app.logger.error( '<send_staff_review_account_reminder failed') raise BusinessException(Error.FAILED_NOTIFICATION, None) from e
def reset_password_for_anon_user(user_info: dict, user_name, token_info: Dict = None): """Reset the password of the user.""" user = UserModel.find_by_username(user_name) membership = MembershipModel.find_membership_by_userid(user.id) org_id = membership.org_id org = OrgModel.find_by_org_id(org_id) if not org or org.access_type != AccessType.ANONYMOUS.value: raise BusinessException(Error.INVALID_INPUT, None) check_auth(org_id=org_id, token_info=token_info, one_of_roles=(ADMIN, STAFF)) update_user_request = KeycloakUser() update_user_request.user_name = user_name.replace( IdpHint.BCROS.value + '/', '') update_user_request.password = user_info['password'] update_user_request.update_password_on_login() try: kc_user = KeycloakService.update_user(update_user_request) except HTTPError as err: current_app.logger.error('update_user in keycloak failed {}', err) raise BusinessException(Error.UNDEFINED_ERROR, err) return kc_user
def save_from_jwt_token(cls, token: dict = None): """Save user to database (create/update).""" if not token: return None existing_user = UserModel.find_by_jwt_token(token) if existing_user is None: user_model = UserModel.create_from_jwt_token(token) else: user_model = UserModel.update_from_jwt_token(token, existing_user) if not user_model: return None user = User(user_model) return user
def add_contact(token, contact_info: dict, throw_error_for_duplicates: bool = True): """Add contact information for an existing user.""" current_app.logger.debug('add_contact') user = UserModel.find_by_jwt_token(token) if user is None: raise BusinessException(Error.DATA_NOT_FOUND, None) # check for existing contact (we only want one contact per user) contact_link = ContactLinkModel.find_by_user_id(user.id) if contact_link is not None: if not throw_error_for_duplicates: # TODO may be throw whole object return None raise BusinessException(Error.DATA_ALREADY_EXISTS, None) contact = ContactModel(**camelback2snake(contact_info)) contact = contact.flush() contact_link = ContactLinkModel() contact_link.user = user contact_link.contact = contact contact_link.save() return ContactService(contact)
def save_from_jwt_token(cls, token: dict, request_json: Dict = None): """Save user to database (create/update).""" current_app.logger.debug('save_from_jwt_token') if not token: return None request_json = {} if not request_json else request_json is_anonymous_user = token.get('accessType', None) == AccessType.ANONYMOUS.value if not is_anonymous_user: existing_user = UserModel.find_by_jwt_token(token) else: existing_user = UserModel.find_by_username( token.get('preferred_username')) first_name, last_name = User._get_names(existing_user, request_json, token) if existing_user is None: user_model = UserModel.create_from_jwt_token( token, first_name, last_name) else: user_model = UserModel.update_from_jwt_token( existing_user, token, first_name, last_name, is_login=request_json.get('isLogin', False)) if not user_model: return None # if accepted , double check if there is a new TOS in place .IF so , update the flag to false if user_model.is_terms_of_use_accepted: document_type = DocumentType.TERMS_OF_USE_DIRECTOR_SEARCH.value if is_anonymous_user \ else DocumentType.TERMS_OF_USE.value # get the digit version of the terms of service..ie d1 gives 1 ; d2 gives 2..for proper comparison latest_version = util.digitify( DocumentService.find_latest_version_by_type(document_type)) current_version = util.digitify( user_model.terms_of_use_accepted_version) if latest_version > current_version: user_model.is_terms_of_use_accepted = False user = User(user_model) return user