def test_user_key_not_found_error_for_project(client): project_without_keys = models.Project( public_id="random_project_id", title="random project_title", description="This is a random project. ", pi="PI", bucket= f"publicproj-{str(timestamp(ts_format='%Y%m%d%H%M%S'))}-{str(uuid.uuid4())}", ) # Somehow the key pair for the project is not created or persisted to the database invite1 = models.Invite(email="*****@*****.**", role="Unit Personnel") unituser = models.User.query.filter_by(username="******").first() unituser.unit.projects.append(project_without_keys) dds_web.db.session.commit() unituser_token = encrypted_jwt_token( username=unituser.username, sensitive_content="password", ) with pytest.raises(KeyNotFoundError) as error: share_project_private_key( from_user=unituser, to_another=invite1, from_user_token=unituser_token, project=project_without_keys, ) assert "Unrecoverable key error. Aborting." in str(error.value)
def __setup_invite(unit_name, invite_email): unit = models.Unit.query.filter_by(name=unit_name).first() invite = models.Invite(email=invite_email, role="Researcher") unit.invites.append(invite) db.session.add(invite) db.session.commit() invite = models.Invite.query.filter_by(email=invite_email).first() assert invite is not None assert invite.unit is not None return invite
def test_user_key_operation_error_with_decrypt_user_private_key(client): invite1 = models.Invite(email="*****@*****.**", role="Unit Personnel") unituser = models.User.query.filter_by(username="******").first() # Somehow a wrong password has ended up in the encrypted token unituser_token = encrypted_jwt_token( username=unituser.username, sensitive_content="passwor", ) with pytest.raises(KeyOperationError) as error: share_project_private_key( from_user=unituser, to_another=invite1, from_user_token=unituser_token, project=unituser.unit.projects[0], ) assert "User private key could not be decrypted!" in str(error.value)
def test_user_key_setup_error_with_public_key(client): invite1 = models.Invite(email="*****@*****.**", role="Unit Personnel") # Somehow the key pair for the invite has not taken place or disappeared unituser = models.User.query.filter_by(username="******").first() unituser_token = encrypted_jwt_token( username=unituser.username, sensitive_content="password", ) with pytest.raises(KeySetupError) as error: share_project_private_key( from_user=unituser, to_another=invite1, from_user_token=unituser_token, project=unituser.unit.projects[0], ) assert "User keys are not properly setup!" in str(error.value)
def test_sensitive_content_missing_error(client): invite1 = models.Invite(email="*****@*****.**", role="Unit Personnel") unituser = models.User.query.filter_by(username="******").first() # Somehow the password is missing in the encrypted token unituser_token = encrypted_jwt_token( username=unituser.username, sensitive_content=None, ) with pytest.raises(SensitiveContentMissingError) as error: share_project_private_key( from_user=unituser, to_another=invite1, from_user_token=unituser_token, project=unituser.unit.projects[0], ) assert "Sensitive content is missing in the encrypted token!" in str( error.value)
def test_user_key_operation_error_with_load_user_public_key(client): invite1 = models.Invite(email="*****@*****.**", role="Unit Personnel") generate_invite_key_pair(invite1) unituser = models.User.query.filter_by(username="******").first() unituser_token = encrypted_jwt_token( username=unituser.username, sensitive_content="password", ) # Somehow the public key of the invite is not the expected public key invite1.public_key = b"useless_bytes" with pytest.raises(KeyOperationError) as error: share_project_private_key( from_user=unituser, to_another=invite1, from_user_token=unituser_token, project=unituser.unit.projects[0], ) assert "User public key could not be loaded!" in str(error.value)
def invite_user(email, new_user_role, project=None, unit=None): """Invite a new user""" current_user_role = get_user_roles_common(user=auth.current_user()) if not project: if current_user_role == "Project Owner": return { "status": ddserr.InviteError.code.value, "message": "Project ID required to invite users to projects.", } if new_user_role == "Project Owner": return { "status": ddserr.InviteError.code.value, "message": "Project ID required to invite a 'Project Owner'.", } # Verify role or current and new user if current_user_role == "Super Admin" and project: return { "status": ddserr.InviteError.code.value, "message": ("Super Admins do not have project data access and can therefore " "not invite users to specific projects."), } elif current_user_role == "Unit Admin" and new_user_role == "Super Admin": return { "status": ddserr.AccessDeniedError.code.value, "message": ddserr.AccessDeniedError.description, } elif current_user_role == "Unit Personnel" and new_user_role in [ "Super Admin", "Unit Admin", ]: return { "status": ddserr.AccessDeniedError.code.value, "message": ddserr.AccessDeniedError.description, } elif current_user_role == "Project Owner" and new_user_role in [ "Super Admin", "Unit Admin", "Unit Personnel", ]: return { "status": ddserr.AccessDeniedError.code.value, "message": ddserr.AccessDeniedError.description, } elif current_user_role == "Researcher": return { "status": ddserr.AccessDeniedError.code.value, "message": ddserr.AccessDeniedError.description, } # Create invite row new_invite = models.Invite( email=email, role=("Researcher" if new_user_role == "Project Owner" else new_user_role), ) # Create URL safe token for invitation link token = encrypted_jwt_token( username="", sensitive_content=generate_invite_key_pair( invite=new_invite).hex(), expires_in=datetime.timedelta( hours=flask.current_app.config["INVITATION_EXPIRES_IN_HOURS"]), additional_claims={"inv": new_invite.email}, ) # Create link for invitation email link = flask.url_for("auth_blueprint.confirm_invite", token=token, _external=True) # Quick search gave this as the URL length limit. if len(link) >= 2048: flask.current_app.logger.error( "Invitation link was not possible to create due to length.") return { "message": "Invite failed due to server error", "status": http.HTTPStatus.INTERNAL_SERVER_ERROR, } projects_not_shared = {} goahead = False # Append invite to unit if applicable if new_invite.role in ["Unit Admin", "Unit Personnel"]: # TODO Change / move this later. This is just so that we can add an initial Unit Admin. if auth.current_user().role == "Super Admin": if unit: unit_row = models.Unit.query.filter_by( public_id=unit).one_or_none() if not unit_row: raise ddserr.DDSArgumentError( message="Invalid unit publid id.") unit_row.invites.append(new_invite) goahead = True else: raise ddserr.DDSArgumentError( message= "You need to specify a unit to invite a Unit Personnel or Unit Admin." ) if "Unit" in auth.current_user().role: # Give new unit user access to all projects of the unit auth.current_user().unit.invites.append(new_invite) if auth.current_user().unit.projects: for unit_project in auth.current_user().unit.projects: if unit_project.is_active: try: share_project_private_key( from_user=auth.current_user(), to_another=new_invite, from_user_token=dds_web.security.auth. obtain_current_encrypted_token(), project=unit_project, ) except ddserr.KeyNotFoundError as keyerr: projects_not_shared[ unit_project. public_id] = "You do not have access to the project(s)" else: goahead = True else: goahead = True if not project: # specified project is disregarded for unituser invites msg = f"{str(new_invite)} was successful." else: msg = f"{str(new_invite)} was successful, but specification for {str(project)} dropped. Unit Users have automatic access to projects of their unit." else: db.session.add(new_invite) if project: try: share_project_private_key( from_user=auth.current_user(), to_another=new_invite, project=project, from_user_token=dds_web.security.auth. obtain_current_encrypted_token(), is_project_owner=new_user_role == "Project Owner", ) except ddserr.KeyNotFoundError as keyerr: projects_not_shared[ project. public_id] = "You do not have access to the specified project." else: goahead = True else: goahead = True # Compose and send email status_code = http.HTTPStatus.OK if goahead: try: db.session.commit() except (sqlalchemy.exc.SQLAlchemyError, sqlalchemy.exc.OperationalError) as sqlerr: db.session.rollback() raise ddserr.DatabaseError( message=str(sqlerr), alt_message=f"Invitation failed" + (": Database malfunction." if isinstance( sqlerr, sqlalchemy.exc.OperationalError) else "."), ) from sqlerr AddUser.compose_and_send_email_to_user(userobj=new_invite, mail_type="invite", link=link) msg = f"{str(new_invite)} was successful." else: msg = (f"The user could not be added to the project(s)." if projects_not_shared else "Unknown error!") + " The invite did not succeed." status_code = ddserr.InviteError.code.value return { "email": new_invite.email, "message": msg, "status": status_code, "errors": projects_not_shared, }
def test_share_project_keys_via_two_invites(client): # this test focuses only on the secure parts related to the following scenario # unituser invites a new Unit Personnel invite1 = models.Invite(email="*****@*****.**", role="Unit Personnel") temporary_key = generate_invite_key_pair(invite1) invite_token1 = encrypted_jwt_token( username="", sensitive_content=temporary_key.hex(), additional_claims={"inv": invite1.email}, ) unituser = models.User.query.filter_by(username="******").first() unituser.unit.invites.append(invite1) unituser_token = encrypted_jwt_token( username=unituser.username, sensitive_content="password", ) for project in unituser.unit.projects: share_project_private_key( from_user=unituser, to_another=invite1, from_user_token=unituser_token, project=project, ) dds_web.db.session.commit() # ************************************ # invited Unit Personnel follows the link and registers itself common_user_fields = { "username": "******", "password": "******", "name": "Test User", } new_user = models.UnitUser(**common_user_fields) invite1.unit.users.append(new_user) new_email = models.Email(email=invite1.email, primary=True) new_user.emails.append(new_email) new_user.active = True dds_web.db.session.add(new_user) verify_and_transfer_invite_to_user(invite_token1, new_user, common_user_fields["password"]) for project_invite_key in invite1.project_invite_keys: project_user_key = models.ProjectUserKeys( project_id=project_invite_key.project_id, user_id=new_user.username, key=project_invite_key.key, ) dds_web.db.session.add(project_user_key) dds_web.db.session.delete(project_invite_key) assert invite1.nonce != new_user.nonce assert invite1.public_key == new_user.public_key assert invite1.private_key != new_user.private_key dds_web.db.session.delete(invite1) dds_web.db.session.commit() # ************************************ # new Unit Personnel invites another new Unit Personnel invite2 = models.Invite(email="*****@*****.**", role="Unit Personnel") invite_token2 = encrypted_jwt_token( username="", sensitive_content=generate_invite_key_pair(invite2).hex(), additional_claims={"inv": invite2.email}, ) unituser = models.User.query.filter_by( username="******").first() unituser.unit.invites.append(invite2) unituser_token = encrypted_jwt_token( username=unituser.username, sensitive_content=common_user_fields["password"], ) for project in unituser.unit.projects: share_project_private_key( from_user=unituser, to_another=invite2, from_user_token=unituser_token, project=project, ) dds_web.db.session.commit() project_invite_keys = invite2.project_invite_keys number_of_asserted_projects = 0 for project_invite_key in project_invite_keys: if (project_invite_key.project.public_id == "public_project_id" or project_invite_key.project.public_id == "unused_project_id" or project_invite_key.project.public_id == "restricted_project_id" or project_invite_key.project.public_id == "second_public_project_id" or project_invite_key.project.public_id == "file_testing_project"): number_of_asserted_projects += 1 assert len(project_invite_keys) == number_of_asserted_projects assert len(project_invite_keys) == 5