def _build(cls, target_class, *args, **kwargs): inst = ProjectFactory._build(target_class) for inst_attr, node_attr in Institution.attribute_map.items(): default = cls.default_institution_attributes.get(inst_attr) if callable(default): default = default() setattr(inst, node_attr, kwargs.pop(inst_attr, default)) for key, val in kwargs.items(): setattr(inst, key, val) return Institution(inst)
def load_origins_whitelist(): global ORIGINS_WHITELIST from osf.models import Institution, PreprintProvider institution_origins = tuple(domain.lower() for domain in itertools.chain( *[institution.domains for institution in Institution.find()])) preprintprovider_origins = tuple( preprintprovider.domain.lower() for preprintprovider in PreprintProvider.objects.exclude(domain='')) ORIGINS_WHITELIST = institution_origins + preprintprovider_origins
def load_origins_whitelist(): global ORIGINS_WHITELIST from osf.models import Institution, PreprintProvider institution_origins = tuple(domain.lower() for domain in itertools.chain(*[ institution.domains for institution in Institution.find() ])) preprintprovider_origins = tuple(preprintprovider.domain.lower() for preprintprovider in PreprintProvider.objects.exclude(domain='')) ORIGINS_WHITELIST = institution_origins + preprintprovider_origins
def get_institutions_to_add_remove(self, institutions, new_institutions): diff = relationship_diff( current_items={inst._id: inst for inst in institutions.all()}, new_items={inst['_id']: inst for inst in new_institutions} ) insts_to_add = [] for inst_id in diff['add']: inst = Institution.load(inst_id) if not inst: raise exceptions.NotFound(detail='Institution with id "{}" was not found'.format(inst_id)) insts_to_add.append(inst) return insts_to_add, diff['remove'].values()
def load_origins_whitelist(): global ORIGINS_WHITELIST from osf.models import Institution, PreprintProvider institution_origins = tuple(domain.lower() for domain in itertools.chain( *[institution.domains for institution in Institution.find()])) preprintprovider_origins = tuple( preprintprovider.domain.lower() for preprintprovider in PreprintProvider.objects.exclude(domain='')) ORIGINS_WHITELIST = tuple( urlparse(url).geturl().lower().split('{}://'.format( urlparse(url).scheme))[-1] for url in institution_origins + preprintprovider_origins)
def get_institutions_to_add_remove(self, institutions, new_institutions): diff = relationship_diff( current_items={inst._id: inst for inst in institutions.all()}, new_items={inst['_id']: inst for inst in new_institutions}) insts_to_add = [] for inst_id in diff['add']: inst = Institution.load(inst_id) if not inst: raise exceptions.NotFound( detail='Institution with id "{}" was not found'.format( inst_id)) insts_to_add.append(inst) return insts_to_add, diff['remove'].values()
def migrate_institutions(index): for inst in Institution.find(Q('is_deleted', 'ne', True)): update_institution(inst, index)
def get_queryset(self): return Institution.find(self.get_query_from_request())
def get_institutions(self): institutions = Institution.find(Q('_id', 'ne', None)) return institutions
def test_querying_on_domains(): inst = InstitutionFactory(domains=['foo.test']) result = Institution.find(Q('domains', 'eq', 'foo.test')) assert inst in result
def authenticate(self, request): """ Handle CAS institution authentication request. The JWT `data` payload is expected in the following structure: { "provider": { "idp": "", "id": "", "user": { "username": "", "fullname": "", "familyName": "", "givenName": "", "middleNames": "", "suffix": "", } } } :param request: the POST request :return: user, None if authentication succeed :raises: AuthenticationFailed if authentication fails """ # Verify / decrypt / decode the payload try: payload = jwt.decode( jwe.decrypt(request.body, settings.JWE_SECRET), settings.JWT_SECRET, options={'verify_exp': False}, algorithm='HS256', ) except (jwt.InvalidTokenError, TypeError, jwe.exceptions.MalformedData): raise AuthenticationFailed # Load institution and user data data = json.loads(payload['data']) provider = data['provider'] institution = Institution.load(provider['id']) if not institution: raise AuthenticationFailed('Invalid institution id: "{}"'.format(provider['id'])) username = provider['user'].get('username') fullname = provider['user'].get('fullname') given_name = provider['user'].get('givenName') family_name = provider['user'].get('familyName') middle_names = provider['user'].get('middleNames') suffix = provider['user'].get('suffix') department = provider['user'].get('department') # Use given name and family name to build full name if it is not provided if given_name and family_name and not fullname: fullname = given_name + ' ' + family_name # Non-empty full name is required. Fail the auth and inform sentry if not provided. if not fullname: message = 'Institution login failed: fullname required for ' \ 'user "{}" from institution "{}"'.format(username, provider['id']) sentry.log_message(message) raise AuthenticationFailed(message) # Get an existing user or create a new one. If a new user is created, the user object is # confirmed but not registered,which is temporarily of an inactive status. If an existing # user is found, it is also possible that the user is inactive (e.g. unclaimed, disabled, # unconfirmed, etc.). user, created = get_or_create_user(fullname, username, reset_password=False) # Existing but inactive users need to be either "activated" or failed the auth activation_required = False new_password_required = False if not created: try: drf.check_user(user) logger.info('Institution SSO: active user "{}"'.format(username)) except exceptions.UnclaimedAccountError: # Unclaimed user (i.e. a user that has been added as an unregistered contributor) user.unclaimed_records = {} activation_required = True # Unclaimed users have an unusable password when being added as an unregistered # contributor. Thus a random usable password must be assigned during activation. new_password_required = True logger.info('Institution SSO: unclaimed contributor "{}"'.format(username)) except exceptions.UnconfirmedAccountError: if user.has_usable_password(): # Unconfirmed user from default username / password signup user.email_verifications = {} activation_required = True # Unconfirmed users already have a usable password set by the creator during # sign-up. However, it must be overwritten by a new random one so the creator # (if he is not the real person) can not access the account after activation. new_password_required = True logger.info('Institution SSO: unconfirmed user "{}"'.format(username)) else: # Login take-over has not been implemented for unconfirmed user created via # external IdP login (ORCiD). message = 'Institution SSO is not eligible for an unconfirmed account ' \ 'created via external IdP login: username = "******"'.format(username) sentry.log_message(message) logger.error(message) return None, None except exceptions.DeactivatedAccountError: # Deactivated user: login is not allowed for deactivated users message = 'Institution SSO is not eligible for a deactivated account: ' \ 'username = "******"'.format(username) sentry.log_message(message) logger.error(message) return None, None except exceptions.MergedAccountError: # Merged user: this shouldn't happen since merged users do not have an email message = 'Institution SSO is not eligible for a merged account: ' \ 'username = "******"'.format(username) sentry.log_message(message) logger.error(message) return None, None except exceptions.InvalidAccountError: # Other invalid status: this shouldn't happen unless the user happens to be in a # temporary state. Such state requires more updates before the user can be saved # to the database. (e.g. `get_or_create_user()` creates a temporary-state user.) message = 'Institution SSO is not eligible for an inactive account with ' \ 'an unknown or invalid status: username = "******"'.format(username) sentry.log_message(message) logger.error(message) return None, None else: logger.info('Institution SSO: new user "{}"'.format(username)) # The `department` field is updated each login when it was changed. if department and user.department != department: user.department = department user.save() # Both created and activated accounts need to be updated and registered if created or activation_required: if given_name: user.given_name = given_name if family_name: user.family_name = family_name if middle_names: user.middle_names = middle_names if suffix: user.suffix = suffix # Users claimed or confirmed via institution SSO should have their full name updated if activation_required: user.fullname = fullname user.update_date_last_login() # Relying on front-end validation until `accepted_tos` is added to the JWT payload user.accepted_terms_of_service = timezone.now() # Register and save user password = str(uuid.uuid4()) if new_password_required else None user.register(username, password=password) user.save() # Send confirmation email for all three: created, confirmed and claimed send_mail( to_addr=user.username, mail=WELCOME_OSF4I, mimetype='html', user=user, domain=DOMAIN, osf_support_email=OSF_SUPPORT_EMAIL, storage_flag_is_active=waffle.flag_is_active(request, features.STORAGE_I18N), ) # Affiliate the user if not previously affiliated if not user.is_affiliated_with_institution(institution): user.affiliated_institutions.add(institution) user.save() return user, None
def authenticate(self, request): """ Handle CAS institution authentication request. The JWT `data` payload is expected in the following structure: { "provider": { "idp": "", "id": "", "user": { "username": "", "fullname": "", "familyName": "", "givenName": "", "middleNames": "", "suffix": "", } } } :param request: the POST request :return: user, None if authentication succeed :raises: AuthenticationFailed if authentication fails """ try: payload = jwt.decode( jwe.decrypt(request.body, settings.JWE_SECRET), settings.JWT_SECRET, options={'verify_exp': False}, algorithm='HS256', ) except (jwt.InvalidTokenError, TypeError, jwe.exceptions.MalformedData): raise AuthenticationFailed data = json.loads(payload['data']) provider = data['provider'] institution = Institution.load(provider['id']) if not institution: raise AuthenticationFailed( 'Invalid institution id specified "{}"'.format(provider['id'])) username = provider['user'].get('username') fullname = provider['user'].get('fullname') given_name = provider['user'].get('givenName') family_name = provider['user'].get('familyName') middle_names = provider['user'].get('middleNames') suffix = provider['user'].get('suffix') # use given name and family name to build full name if not provided if given_name and family_name and not fullname: fullname = given_name + ' ' + family_name # institution must provide `fullname`, otherwise we fail the authentication and inform sentry if not fullname: message = 'Institution login failed: fullname required' \ ' for user {} from institution {}'.format(username, provider['id']) sentry.log_message(message) raise AuthenticationFailed(message) # `get_or_create_user()` guesses names from fullname # replace the guessed ones if the names are provided from the authentication user, created = get_or_create_user(fullname, username, reset_password=False) if created: if given_name: user.given_name = given_name if family_name: user.family_name = family_name if middle_names: user.middle_names = middle_names if suffix: user.suffix = suffix user.update_date_last_login() # Relying on front-end validation until `accepted_tos` is added to the JWT payload user.accepted_terms_of_service = timezone.now() # save and register user user.save() user.register(username) # send confirmation email send_mail( to_addr=user.username, mail=WELCOME_OSF4I, mimetype='html', user=user, domain=DOMAIN, osf_support_email=OSF_SUPPORT_EMAIL, storage_flag_is_active=waffle.flag_is_active( request, features.STORAGE_I18N), ) if not user.is_affiliated_with_institution(institution): user.affiliated_institutions.add(institution) user.save() return user, None
def authenticate(self, request): """ Handle CAS institution authentication request. The JWT `data` payload is expected in the following structure: { "provider": { "idp": "", "id": "", "user": { "username": "", "fullname": "", "familyName": "", "givenName": "", "middleNames": "", "suffix": "", "department": "", "isMemberOf": "", # Shared SSO "selectiveSsoFilter": "", # Selective SSO } } } Note that if authentication failed, HTTP 403 Forbidden is returned no matter what type of exception is raised. In this method, we use `AuthenticationFailed` when the payload is not correctly encrypted/encoded since it is the "authentication" between CAS and this endpoint. We use `PermissionDenied` for all other exceptions that happened afterwards. :param request: the POST request :return: user, None if authentication succeed :raises: AuthenticationFailed or PermissionDenied if authentication fails """ # Verify / decrypt / decode the payload try: payload = jwt.decode( jwe.decrypt(request.body, settings.JWE_SECRET), settings.JWT_SECRET, options={'verify_exp': False}, algorithm='HS256', ) except (jwt.InvalidTokenError, TypeError, jwe.exceptions.MalformedData): raise AuthenticationFailed( detail='InstitutionSsoRequestNotAuthorized') # Load institution and user data data = json.loads(payload['data']) provider = data['provider'] institution = Institution.load(provider['id']) if not institution: message = 'Institution SSO Error: invalid institution ID [{}]'.format( provider['id']) logger.error(message) sentry.log_message(message) raise PermissionDenied(detail='InstitutionSsoInvalidInstitution') username = provider['user'].get('username') fullname = provider['user'].get('fullname') given_name = provider['user'].get('givenName') family_name = provider['user'].get('familyName') middle_names = provider['user'].get('middleNames') suffix = provider['user'].get('suffix') department = provider['user'].get('department') selective_sso_filter = provider['user'].get('selectiveSsoFilter') # Check selective login first if provider['id'] in INSTITUTION_SELECTIVE_SSO_MAP: if selective_sso_filter != INSTITUTION_SELECTIVE_SSO_MAP[ provider['id']]: message = f'Institution SSO Error: user [email={username}] is not allowed for ' \ f'institution SSO [id={institution._id}] due to selective SSO rules' logger.error(message) sentry.log_message(message) raise PermissionDenied( detail='InstitutionSsoSelectiveNotAllowed') logger.info( f'Institution SSO: selective SSO verified for user [email={username}] ' f'at institution [id={institution._id}]', ) # Check secondary institutions which uses the SSO of primary ones secondary_institution = None if provider['id'] in INSTITUTION_SHARED_SSO_MAP: switch_map = INSTITUTION_SHARED_SSO_MAP[provider['id']] criteria_type = switch_map.get('criteria') if criteria_type == 'attribute': attribute_name = switch_map.get('attribute') attribute_value = provider['user'].get(attribute_name) if attribute_value: secondary_institution_id = switch_map.get( 'institutions', {}, ).get(attribute_value) logger.info( 'Institution SSO: primary=[{}], secondary=[{}], ' 'username=[{}]'.format(provider['id'], secondary_institution_id, username)) secondary_institution = Institution.load( secondary_institution_id) if not secondary_institution: # Log errors and inform Sentry but do not raise an exception if OSF fails # to load the secondary institution from database message = 'Institution SSO Error: invalid secondary institution [{}]; ' \ 'primary=[{}], username=[{}]'.format(attribute_value, provider['id'], username) logger.error(message) sentry.log_message(message) else: # SSO from primary institution only logger.info( 'Institution SSO: primary=[{}], secondary=[None], ' 'username=[{}]'.format(provider['id'], username)) else: message = 'Institution SSO Error: invalid criteria [{}]; ' \ 'primary=[{}], username=[{}]'.format(criteria_type, provider['id'], username) logger.error(message) sentry.log_message(message) # Use given name and family name to build full name if it is not provided if given_name and family_name and not fullname: fullname = given_name + ' ' + family_name # Non-empty full name is required. Fail the auth and inform sentry if not provided. if not fullname: message = 'Institution SSO Error: missing fullname ' \ 'for user [{}] from institution [{}]'.format(username, provider['id']) logger.error(message) sentry.log_message(message) raise PermissionDenied(detail='InstitutionSsoMissingUserNames') # Get an existing user or create a new one. If a new user is created, the user object is # confirmed but not registered,which is temporarily of an inactive status. If an existing # user is found, it is also possible that the user is inactive (e.g. unclaimed, disabled, # unconfirmed, etc.). user, created = get_or_create_user(fullname, username, reset_password=False) # Existing but inactive users need to be either "activated" or failed the auth activation_required = False new_password_required = False if not created: try: drf.check_user(user) logger.info( 'Institution SSO: active user [{}]'.format(username)) except exceptions.UnclaimedAccountError: # Unclaimed user (i.e. a user that has been added as an unregistered contributor) user.unclaimed_records = {} activation_required = True # Unclaimed users have an unusable password when being added as an unregistered # contributor. Thus a random usable password must be assigned during activation. new_password_required = True logger.warning( 'Institution SSO: unclaimed contributor [{}]'.format( username)) except exceptions.UnconfirmedAccountError: if user.has_usable_password(): # Unconfirmed user from default username / password signup user.email_verifications = {} activation_required = True # Unconfirmed users already have a usable password set by the creator during # sign-up. However, it must be overwritten by a new random one so the creator # (if he is not the real person) can not access the account after activation. new_password_required = True logger.warning( 'Institution SSO: unconfirmed user [{}]'.format( username)) else: # Login take-over has not been implemented for unconfirmed user created via # external IdP login (ORCiD). message = 'Institution SSO Error: SSO is not eligible for an unconfirmed account [{}] ' \ 'created via IdP login'.format(username) sentry.log_message(message) logger.error(message) raise PermissionDenied( detail='InstitutionSsoAccountNotConfirmed') except exceptions.DeactivatedAccountError: # Deactivated user: login is not allowed for deactivated users message = 'Institution SSO Error: SSO is not eligible for a deactivated account: [{}]'.format( username) sentry.log_message(message) logger.error(message) raise PermissionDenied(detail='InstitutionSsoAccountDisabled') except exceptions.MergedAccountError: # Merged user: this shouldn't happen since merged users do not have an email message = 'Institution SSO Error: SSO is not eligible for a merged account: [{}]'.format( username) sentry.log_message(message) logger.error(message) raise PermissionDenied(detail='InstitutionSsoAccountMerged') except exceptions.InvalidAccountError: # Other invalid status: this shouldn't happen unless the user happens to be in a # temporary state. Such state requires more updates before the user can be saved # to the database. (e.g. `get_or_create_user()` creates a temporary-state user.) message = 'Institution SSO Error: SSO is not eligible for an inactive account [{}] ' \ 'with an unknown or invalid status'.format(username) sentry.log_message(message) logger.error(message) raise PermissionDenied(detail='InstitutionSsoInvalidAccount') else: logger.info('Institution SSO: new user [{}]'.format(username)) # The `department` field is updated each login when it was changed. user_guid = user.guids.first()._id if department: if user.department != department: user.department = department user.save() logger.info( 'Institution SSO: user w/ dept: user=[{}], email=[{}], inst=[{}], ' 'dept=[{}]'.format(user_guid, username, institution._id, department)) else: logger.info( 'Institution SSO: user w/o dept: user=[{}], email=[{}], ' 'inst=[{}]'.format(user_guid, username, institution._id)) # Both created and activated accounts need to be updated and registered if created or activation_required: if given_name: user.given_name = given_name if family_name: user.family_name = family_name if middle_names: user.middle_names = middle_names if suffix: user.suffix = suffix # Users claimed or confirmed via institution SSO should have their full name updated if activation_required: user.fullname = fullname user.update_date_last_login() # Register and save user password = str(uuid.uuid4()) if new_password_required else None user.register(username, password=password) user.save() # Send confirmation email for all three: created, confirmed and claimed send_mail( to_addr=user.username, mail=WELCOME_OSF4I, user=user, domain=DOMAIN, osf_support_email=OSF_SUPPORT_EMAIL, storage_flag_is_active=waffle.flag_is_active( request, features.STORAGE_I18N), ) # Affiliate the user to the primary institution if not previously affiliated if not user.is_affiliated_with_institution(institution): user.affiliated_institutions.add(institution) user.save() # Affiliate the user to the secondary institution if not previously affiliated if secondary_institution and not user.is_affiliated_with_institution( secondary_institution): user.affiliated_institutions.add(secondary_institution) user.save() return user, None
def view_institution(inst_id, **kwargs): try: inst = Institution.find_one(Q('_id', 'eq', inst_id) & Q('is_deleted', 'ne', True)) except NoResultsFound: raise HTTPError(http.NOT_FOUND) return serialize_institution(inst)
def authenticate(self, request): """ Handle CAS institution authentication request. The JWT `data` payload is expected in the following structure: { "provider": { "idp": "", "id": "", "user": { "username": "", "fullname": "", "familyName": "", "givenName": "", "middleNames": "", "suffix": "", } } } :param request: the POST request :return: user, None if authentication succeed :raises: AuthenticationFailed if authentication fails """ try: payload = jwt.decode( jwe.decrypt(request.body, settings.JWE_SECRET), settings.JWT_SECRET, options={'verify_exp': False}, algorithm='HS256', ) except (jwt.InvalidTokenError, TypeError): raise AuthenticationFailed data = json.loads(payload['data']) provider = data['provider'] institution = Institution.load(provider['id']) if not institution: raise AuthenticationFailed('Invalid institution id specified "{}"'.format(provider['id'])) username = provider['user'].get('username') fullname = provider['user'].get('fullname') given_name = provider['user'].get('givenName') family_name = provider['user'].get('familyName') middle_names = provider['user'].get('middleNames') suffix = provider['user'].get('suffix') # use given name and family name to build full name if not provided if given_name and family_name and not fullname: fullname = given_name + ' ' + family_name # institution must provide `fullname`, otherwise we fail the authentication and inform sentry if not fullname: message = 'Institution login failed: fullname required' \ ' for user {} from institution {}'.format(username, provider['id']) sentry.log_message(message) raise AuthenticationFailed(message) # `get_or_create_user()` guesses names from fullname # replace the guessed ones if the names are provided from the authentication user, created = get_or_create_user(fullname, username, reset_password=False) if created: if given_name: user.given_name = given_name if family_name: user.family_name = family_name if middle_names: user.middle_names = middle_names if suffix: user.suffix = suffix user.update_date_last_login() # Relying on front-end validation until `accepted_tos` is added to the JWT payload user.accepted_terms_of_service = timezone.now() # save and register user user.save() user.register(username) # send confirmation email send_mail( to_addr=user.username, mail=WELCOME_OSF4I, mimetype='html', user=user, domain=DOMAIN, osf_support_email=OSF_SUPPORT_EMAIL, storage_flag_is_active=waffle.flag_is_active(request, features.STORAGE_I18N), ) if not user.is_affiliated_with_institution(institution): user.affiliated_institutions.add(institution) user.save() return user, None
def get_globals(): """Context variables that are available for every template rendered by OSFWebRenderer. """ user = _get_current_user() user_institutions = [{'id': inst._id, 'name': inst.name, 'logo_path': inst.logo_path_rounded_corners} for inst in user.affiliated_institutions.all()] if user else [] location = geolite2.lookup(request.remote_addr) if request.remote_addr else None if request.host_url != settings.DOMAIN: try: inst_id = (Institution.find_one(Q('domains', 'eq', request.host.lower())))._id request_login_url = '{}institutions/{}'.format(settings.DOMAIN, inst_id) except NoResultsFound: request_login_url = request.url.replace(request.host_url, settings.DOMAIN) else: request_login_url = request.url return { 'private_link_anonymous': is_private_link_anonymous_view(), 'user_name': user.username if user else '', 'user_full_name': user.fullname if user else '', 'user_id': user._id if user else '', 'user_locale': user.locale if user and user.locale else '', 'user_timezone': user.timezone if user and user.timezone else '', 'user_url': user.url if user else '', 'user_gravatar': get_gravatar(user=user, size=25) if user else '', 'user_email_verifications': user.unconfirmed_email_info if user else [], 'user_api_url': user.api_url if user else '', 'user_entry_point': metrics.get_entry_point(user) if user else '', 'user_institutions': user_institutions if user else None, 'display_name': get_display_name(user.fullname) if user else '', 'anon': { 'continent': getattr(location, 'continent', None), 'country': getattr(location, 'country', None), }, 'use_cdn': settings.USE_CDN_FOR_CLIENT_LIBS, 'sentry_dsn_js': settings.SENTRY_DSN_JS if sentry.enabled else None, 'dev_mode': settings.DEV_MODE, 'allow_login': settings.ALLOW_LOGIN, 'cookie_name': settings.COOKIE_NAME, 'status': status.pop_status_messages(), 'prev_status': status.pop_previous_status_messages(), 'domain': settings.DOMAIN, 'api_domain': settings.API_DOMAIN, 'disk_saving_mode': settings.DISK_SAVING_MODE, 'language': language, 'noteworthy_links_node': settings.NEW_AND_NOTEWORTHY_LINKS_NODE, 'popular_links_node': settings.POPULAR_LINKS_NODE, 'web_url_for': util.web_url_for, 'api_url_for': util.api_url_for, 'api_v2_url': util.api_v2_url, # URL function for templates 'api_v2_base': util.api_v2_url(''), # Base url used by JS api helper 'sanitize': sanitize, 'sjson': lambda s: sanitize.safe_json(s), 'webpack_asset': paths.webpack_asset, 'waterbutler_url': settings.WATERBUTLER_URL, 'login_url': cas.get_login_url(request_login_url), 'reauth_url': util.web_url_for('auth_logout', redirect_url=request.url, reauth=True), 'profile_url': cas.get_profile_url(), 'enable_institutions': settings.ENABLE_INSTITUTIONS, 'keen': { 'public': { 'project_id': settings.KEEN['public']['project_id'], 'write_key': settings.KEEN['public']['write_key'], }, 'private': { 'project_id': settings.KEEN['private']['project_id'], 'write_key': settings.KEEN['private']['write_key'], }, }, 'maintenance': maintenance.get_maintenance(), 'recaptcha_site_key': settings.RECAPTCHA_SITE_KEY, 'custom_citations': settings.CUSTOM_CITATIONS }