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 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 give_project_access(project_list, current_user, user): """Give specific user project access.""" # Loop through and check that the project(s) is(are) active fix_errors = {} for proj in project_list: try: if proj.is_active: project_keys_row = models.ProjectUserKeys.query.filter_by( project_id=proj.id, user_id=user.username ).one_or_none() if not project_keys_row: share_project_private_key( from_user=current_user, to_another=user, project=proj, from_user_token=dds_web.security.auth.obtain_current_encrypted_token(), ) except KeyNotFoundError as keyerr: fix_errors[ proj.public_id ] = "You do not have access to this project. Please contact the responsible unit." return fix_errors
def fill_basic_db(db): """ Fill the database with basic data. """ units, users, projects = add_data_to_db() db.session.add_all(units) db.session.add_all(users) db.session.commit() generate_project_key_pair(users[2], units[0].projects[0]) generate_project_key_pair(users[2], units[0].projects[2]) generate_project_key_pair(users[2], units[0].projects[4]) generate_project_key_pair(users[3], units[0].projects[1]) generate_project_key_pair(users[3], units[0].projects[3]) db.session.commit() user2_token = encrypted_jwt_token( username=users[2].username, sensitive_content="password", ) share_project_private_key( from_user=users[2], to_another=users[0], from_user_token=user2_token, project=projects[0], ) share_project_private_key( from_user=users[2], to_another=users[1], from_user_token=user2_token, project=projects[0], ) user3_token = encrypted_jwt_token( username=users[3].username, sensitive_content="password", ) share_project_private_key( from_user=users[3], to_another=users[6], from_user_token=user3_token, project=projects[3], ) db.session.commit()
def add_to_project(whom, project, role, send_email=True): """Add existing user or invite to a project""" allowed_roles = ["Project Owner", "Researcher"] if role not in allowed_roles: return { "status": ddserr.AccessDeniedError.code.value, "message": ("User Role should be either 'Project Owner' or " "'Researcher' to be added to a project"), } if whom.role not in allowed_roles: return { "status": ddserr.AccessDeniedError.code.value, "message": ("Users affiliated with units can not be added to projects individually." ), } is_owner = role == "Project Owner" ownership_change = False if isinstance(whom, models.ResearchUser): project_user_row = models.ProjectUsers.query.filter_by( project_id=project.id, user_id=whom.username).one_or_none() else: project_user_row = models.ProjectInviteKeys.query.filter_by( project_id=project.id, invite_id=whom.id).one_or_none() if project_user_row: send_email = False if project_user_row.owner == is_owner: return { "status": ddserr.RoleException.code.value, "message": f"{str(whom)} is already associated with the {str(project)} in this capacity. ", } ownership_change = True project_user_row.owner = is_owner if not ownership_change: if isinstance(whom, models.ResearchUser): project.researchusers.append( models.ProjectUsers( project_id=project.id, user_id=whom.username, owner=is_owner, )) try: share_project_private_key( from_user=auth.current_user(), to_another=whom, from_user_token=dds_web.security.auth. obtain_current_encrypted_token(), project=project, is_project_owner=is_owner, ) except ddserr.KeyNotFoundError as keyerr: return { "message": ("You do not have access to the current project. To get access, " "ask the a user within the responsible unit to grant you access." ), "status": ddserr.AccessDeniedError.code.value, } try: db.session.commit() except ( sqlalchemy.exc.SQLAlchemyError, sqlalchemy.exc.IntegrityError, sqlalchemy.exc.OperationalError, ) as err: flask.current_app.logger.exception(err) db.session.rollback() raise ddserr.DatabaseError( message=str(err), alt_message= f"Server Error: User was not associated with the project" + (": Database malfunction." if isinstance( err, sqlalchemy.exc.OperationalError) else "."), ) from err # If project is already released and not expired, send mail to user send_email = send_email and project.current_status == "Available" if send_email: AddUser.compose_and_send_email_to_user(whom, "project_release", project=project) flask.current_app.logger.debug( f"{str(whom)} was given access to the {str(project)} as a {'Project Owner' if is_owner else 'Researcher'}." ) return { "status": http.HTTPStatus.OK, "message": (f"{str(whom)} was given access to the " f"{str(project)} as a {'Project Owner' if is_owner else 'Researcher'}. An e-mail notification has{' not ' if not send_email else ' '}been sent." ), }
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
def fill_db(): """Fills the database with initial entries used for development.""" # Foreign key/relationship updates: # The model with the row db.relationship should append the row of the model with foreign key password = "******" # Super Admin superadmin = models.SuperAdmin(username="******", password=password, name="Super Admin") superadmin_email = models.Email(email="*****@*****.**", primary=True) superadmin_email.user = superadmin db.session.add(superadmin_email) # Create first unit user unituser_1 = models.UnitUser( username="******", password=password, name="First Unit User", ) # Create second unit user unituser_2 = models.UnitUser( username="******", password=password, name="Second Unit User", ) # create a few e-mail addresses email_unituser_1 = models.Email(email="*****@*****.**", primary=True) email_unituser_1b = models.Email(email="*****@*****.**", primary=False) email_unituser_2 = models.Email(email="*****@*****.**", primary=True) email_unituser_1.user = unituser_1 email_unituser_1b.user = unituser_1 email_unituser_2.user = unituser_2 unituser_1.active = True unituser_2.active = True # Create first unit unit_1 = models.Unit( public_id="unit_1", name="Unit 1", external_display_name="Unit 1 external", contact_email="*****@*****.**", internal_ref="someunit", safespring_endpoint=current_app.config.get("SAFESPRING_URL"), safespring_name=current_app.config.get("DDS_SAFESPRING_PROJECT"), safespring_access=current_app.config.get("DDS_SAFESPRING_ACCESS"), safespring_secret=current_app.config.get("DDS_SAFESPRING_SECRET"), ) unit_1.users.extend([unituser_1, unituser_2]) # Create first project - leave out foreign key project_1 = models.Project( public_id="project_1", title="First Project", description= "This is a test project. You will be able to upload to but NOT download " "from this project. Create a new project to test the entire system. ", pi="*****@*****.**", bucket="testbucket", ) project_1.project_statuses.append( models.ProjectStatuses(**{ "status": "In Progress", "date_created": dds_web.utils.current_time() })) unituser_1.created_projects.append(project_1) generate_project_key_pair(unituser_1, project_1) # Create second project - leave out foreign key project_2 = models.Project( public_id="project_2", title="Second Project", description= "This is a test project. You will be able to upload to but NOT download " "from this project. Create a new project to test the entire system. ", pi="*****@*****.**", bucket= f"secondproject-{str(dds_web.utils.timestamp(ts_format='%Y%m%d%H%M%S'))}-{str(uuid.uuid4())}", ) project_2.project_statuses.append( models.ProjectStatuses(**{ "status": "In Progress", "date_created": dds_web.utils.current_time() })) unituser_2.created_projects.append(project_2) generate_project_key_pair(unituser_2, project_2) # Connect project to unit. append (not =) due to many projects per unit unit_1.projects.extend([project_1, project_2]) # Create an email email_researchuser_1 = models.Email(email="*****@*****.**", primary=True) # Create first research user researchuser_1 = models.ResearchUser( username="******", password=password, name="First Research User", ) email_researchuser_1.user = researchuser_1 # Create association with user - not owner of project project_1_user_1_association = models.ProjectUsers(owner=False) # Connect research user to association row. = (not append) due to one user per ass. row project_1_user_1_association.researchuser = researchuser_1 # Connect project to association row. = (not append) due to one project per ass. row project_1_user_1_association.project = project_1 researchuser_1.active = True email_researchuser_2 = models.Email(email="*****@*****.**", primary=True) # Create second research user researchuser_2 = models.ResearchUser( username="******", password=password, name="Second Research User", ) email_researchuser_2.user = researchuser_2 # Create association with user - is owner of project project_1_user_2_association = models.ProjectUsers(owner=True) # Connect research user to association row. = (not append) due to one user per ass. row project_1_user_2_association.researchuser = researchuser_2 # Connect project to association row. = (not append) due to one project per ass. row project_1_user_2_association.project = project_1 researchuser_2.active = True # Add unit to database - relationship will add the rest because of foreign key constraints db.session.add(unit_1) db.session.commit() unituser_1_token = encrypted_jwt_token( username=unituser_1.username, sensitive_content=password, ) share_project_private_key( from_user=unituser_1, to_another=researchuser_1, from_user_token=unituser_1_token, project=project_1, ) share_project_private_key( from_user=unituser_1, to_another=researchuser_2, from_user_token=unituser_1_token, project=project_1, ) db.session.commit()