def push_empty_groups_to_cis(sender, instance, **kwargs): """Notify CIS about the profile deletion. Remove all the access groups and tags from the profile. """ from mozillians.users.tasks import send_userprofile_to_cis data = bundle_profile_data(instance.id, delete=True) send_userprofile_to_cis.delay(profile_results=data)
def push_empty_groups_to_cis(sender, instance, **kwargs): """Notify CIS about the profile deletion. Remove all the access groups and tags from the profile. """ from mozillians.users.tasks import send_userprofile_to_cis data = bundle_profile_data(instance.id, delete=True) send_userprofile_to_cis.delay(profile_results=data)
def add_member(self, userprofile, status=GroupMembership.MEMBER, inviter=None): """ Add a user to this group. Optionally specify status other than member. If user is already in the group with the given status, this is a no-op. If user is already in the group with a different status, their status will be updated if the change is a promotion. Otherwise, their status will not change. If the group in question is the NDA group, also add the user to the NDA newsletter. """ defaults = dict(status=status, date_joined=now()) membership, _ = GroupMembership.objects.get_or_create(userprofile=userprofile, group=self, defaults=defaults) send_userprofile_to_cis.delay(membership.userprofile.pk) # Remove the need_removal flag in any case # We have a renewal, let's save the object. if membership.needs_renewal: membership.needs_renewal = False membership.save() if membership.status != status: # Status changed # The only valid status change states are: # PENDING to MEMBER # PENDING to PENDING_TERMS # PENDING_TERMS to MEMBER old_status = membership.status membership.status = status statuses = [(GroupMembership.PENDING, GroupMembership.MEMBER), (GroupMembership.PENDING, GroupMembership.PENDING_TERMS), (GroupMembership.PENDING_TERMS, GroupMembership.MEMBER)] if (old_status, status) in statuses: # Status changed membership.save() if membership.status in [GroupMembership.PENDING, GroupMembership.MEMBER]: email_membership_change.delay(self.pk, userprofile.user.pk, old_status, status) # Since there is no demotion, we can check if the new status is MEMBER and # subscribe the user to the NDA newsletter if the group is NDA if self.name == settings.NDA_GROUP and status == GroupMembership.MEMBER: subscribe_user_to_basket.delay(userprofile.id, [settings.BASKET_NDA_NEWSLETTER]) if inviter: # Set the invite to the last person who renewed the membership invite = get_object_or_none(Invite, group=membership.group, redeemer=userprofile) if invite: invite.inviter = inviter invite.save()
def add_member(self, userprofile, status=GroupMembership.MEMBER, inviter=None): """ Add a user to this group. Optionally specify status other than member. If user is already in the group with the given status, this is a no-op. If user is already in the group with a different status, their status will be updated if the change is a promotion. Otherwise, their status will not change. If the group in question is the NDA group, also add the user to the NDA newsletter. """ defaults = dict(status=status, date_joined=now()) membership, _ = GroupMembership.objects.get_or_create(userprofile=userprofile, group=self, defaults=defaults) send_userprofile_to_cis.delay(membership.userprofile.pk) # Remove the need_removal flag in any case # We have a renewal, let's save the object. if membership.needs_renewal: membership.needs_renewal = False membership.save() if membership.status != status: # Status changed # The only valid status change states are: # PENDING to MEMBER # PENDING to PENDING_TERMS # PENDING_TERMS to MEMBER old_status = membership.status membership.status = status statuses = [(GroupMembership.PENDING, GroupMembership.MEMBER), (GroupMembership.PENDING, GroupMembership.PENDING_TERMS), (GroupMembership.PENDING_TERMS, GroupMembership.MEMBER)] if (old_status, status) in statuses: # Status changed membership.save() if membership.status in [GroupMembership.PENDING, GroupMembership.MEMBER]: email_membership_change.delay(self.pk, userprofile.user.pk, old_status, status) # Since there is no demotion, we can check if the new status is MEMBER and # subscribe the user to the NDA newsletter if the group is NDA if self.name == settings.NDA_GROUP and status == GroupMembership.MEMBER: subscribe_user_to_basket.delay(userprofile.id, [settings.BASKET_NDA_NEWSLETTER]) if inviter: # Set the invite to the last person who renewed the membership invite = get_object_or_none(Invite, group=membership.group, redeemer=userprofile) if invite: invite.inviter = inviter invite.save()
def check_authentication_method(self, user): """Check which Identity is used to login. This method, depending on the current status of the IdpProfile of a user, enforces MFA logins and creates the IdpProfiles. Returns the object (user) it was passed unchanged. """ if not user: return None profile = user.userprofile # Ensure compatibility with OIDC conformant mode auth0_user_id = self.claims.get('user_id') or self.claims.get('sub') email = self.claims.get('email') # Get current_idp current_idp = get_object_or_none(IdpProfile, profile=profile, primary=True) # Get or create new `user_id` obj, _ = IdpProfile.objects.get_or_create(profile=profile, email=email, auth0_user_id=auth0_user_id) # Update/Save the Github username if 'github|' in auth0_user_id: obj.username = self.claims.get('nickname', '') obj.save() # Do not allow downgrades. # Round the current type to the floor. # This way 30 and 39 will have always the same priority. if current_idp and obj.type < int( math.floor(current_idp.type / 10) * 10): msg = u'Please use one of the following authentication methods: {}' # convert the tuple to a dict to easily get the values provider_types = dict(IdpProfile.PROVIDER_TYPES) methods = ', '.join(provider_types[x] for x in ALLOWED_IDP_FLOWS[current_idp.type]) messages.error(self.request, msg.format(methods)) return None # Mark other `user_id` as `primary=False` idp_q = IdpProfile.objects.filter(profile=profile) with transaction.atomic(): idp_q.exclude(auth0_user_id=auth0_user_id, email=email).update(primary=False) # Mark current `user_id` as `primary=True` idp_q.filter(auth0_user_id=auth0_user_id, email=email).update(primary=True) # Update CIS send_userprofile_to_cis.delay(profile.pk) return user
def check_authentication_method(self, user): """Check which Identity is used to login. This method, depending on the current status of the IdpProfile of a user, enforces MFA logins and creates the IdpProfiles. Returns the object (user) it was passed unchanged. """ if not user: return None profile = user.userprofile # Ensure compatibility with OIDC conformant mode auth0_user_id = self.claims.get('user_id') or self.claims.get('sub') email = self.claims.get('email') # Grant an employee vouch if the user has the 'hris_is_staff' group groups = self.claims.get('https://sso.mozilla.com/claim/groups') if groups and 'hris_is_staff' in groups: profile.auto_vouch() # Get current_idp current_idp = get_object_or_none(IdpProfile, profile=profile, primary=True) # Get or create new `user_id` obj, _ = IdpProfile.objects.get_or_create(profile=profile, email=email, auth0_user_id=auth0_user_id) # Update/Save the Github username if 'github|' in auth0_user_id: obj.username = self.claims.get('nickname', '') obj.save() # Do not allow downgrades. if current_idp and obj.type < current_idp.type: msg = u'Please use {0} as the login method to authenticate' messages.error(self.request, msg.format(current_idp.get_type_display())) return None # Mark other `user_id` as `primary=False` idp_q = IdpProfile.objects.filter(profile=profile) with transaction.atomic(): idp_q.exclude(auth0_user_id=auth0_user_id, email=email).update(primary=False) # Mark current `user_id` as `primary=True` idp_q.filter(auth0_user_id=auth0_user_id, email=email).update(primary=True) # Update CIS send_userprofile_to_cis.delay(profile.pk) return user
def save(self, *args, **kwargs): self._privacy_level = None autovouch = kwargs.pop('autovouch', True) super(UserProfile, self).save(*args, **kwargs) # Auto_vouch follows the first save, because you can't # create foreign keys without a database id. if self.is_complete: send_userprofile_to_cis.delay(self.pk) if autovouch: self.auto_vouch()
def save(self, *args, **kwargs): self._privacy_level = None autovouch = kwargs.pop('autovouch', False) super(UserProfile, self).save(*args, **kwargs) # Auto_vouch follows the first save, because you can't # create foreign keys without a database id. if self.is_complete: send_userprofile_to_cis.delay(self.pk) if autovouch: self.auto_vouch()
def check_authentication_method(self, user): """Check which Identity is used to login. This method, depending on the current status of the IdpProfile of a user, enforces MFA logins and creates the IdpProfiles. Returns the object (user) it was passed unchanged. """ if not user: return None profile = user.userprofile # Ensure compatibility with OIDC conformant mode auth0_user_id = self.claims.get('user_id') or self.claims.get('sub') email = self.claims.get('email') aal_scope = self.claims.get(SSO_AAL_SCOPE) is_mfa = True if not aal_scope or aal_scope != 'MEDIUM': is_mfa = False # Grant an employee vouch if the user has the 'hris_is_staff' group groups = self.claims.get('https://sso.mozilla.com/claim/groups') if groups and 'hris_is_staff' in groups: profile.auto_vouch() # Get or create new `user_id` obj, _ = IdpProfile.objects.get_or_create( profile=profile, email=email, auth0_user_id=auth0_user_id) if profile.groups.filter(is_access_group=True).exists() and not is_mfa: msg = ('Members and Curators of Access Groups need to use a 2FA' ' authentication method to login.') messages.error(self.request, msg) return None # With account deracheting we will always get the same Auth0 user id. Mark it as primary if not obj.primary: obj.primary = True IdpProfile.objects.filter(profile=profile).exclude(id=obj.id).update(primary=False) # Update/Save the Github username if 'github|' in auth0_user_id: obj.username = self.claims.get('nickname', '') # Save once obj.save() # Update CIS send_userprofile_to_cis.delay(profile.pk) return user
def check_authentication_method(self, user): """Check which Identity is used to login. This method, depending on the current status of the IdpProfile of a user, enforces MFA logins and creates the IdpProfiles. Returns the object (user) it was passed unchanged. """ if not user: return None profile = user.userprofile # Ensure compatibility with OIDC conformant mode auth0_user_id = self.claims.get('user_id') or self.claims.get('sub') email = self.claims.get('email') aal_scope = self.claims.get(SSO_AAL_SCOPE) is_mfa = True if not aal_scope or aal_scope != ['2FA']: is_mfa = False # Grant an employee vouch if the user has the 'hris_is_staff' group groups = self.claims.get('https://sso.mozilla.com/claim/groups') if groups and 'hris_is_staff' in groups: profile.auto_vouch() # Get or create new `user_id` obj, _ = IdpProfile.objects.get_or_create(profile=profile, email=email, auth0_user_id=auth0_user_id) if profile.groups.filter(is_access_group=True).exists() and not is_mfa: msg = ('Members and Curators of Access Groups need to use a 2FA' ' authentication method to login.') messages.error(self.request, msg) return None # With account deracheting we will always get the same Auth0 user id. Mark it as primary if not obj.primary: obj.primary = True IdpProfile.objects.filter(profile=profile).exclude( id=obj.id).update(primary=False) # Update/Save the Github username if 'github|' in auth0_user_id: obj.username = self.claims.get('nickname', '') # Save once obj.save() # Update CIS send_userprofile_to_cis.delay(profile.pk) return user
def check_authentication_method(self, user): """Check which Identity is used to login. This method, depending on the current status of the IdpProfile of a user, enforces MFA logins and creates the IdpProfiles. Returns the object (user) it was passed unchanged. """ if not user: return None profile = user.userprofile # Ensure compatibility with OIDC conformant mode auth0_user_id = self.claims.get('user_id') or self.claims.get('sub') email = self.claims.get('email') # Grant an employee vouch if the user has the 'hris_is_staff' group groups = self.claims.get('https://sso.mozilla.com/claim/groups') if groups and 'hris_is_staff' in groups: profile.auto_vouch() # Get or create new `user_id` obj, _ = IdpProfile.objects.get_or_create( profile=profile, email=email, auth0_user_id=auth0_user_id) # Check if a user with passwordless login curates an access group and block it. if obj.type <= IdpProfile.PROVIDER_PASSWORDLESS: # LDAP is excluded since is checked at the Auth0 level. if not profile.idp_profiles.filter(type=IdpProfile.PROVIDER_LDAP).exists(): if profile.groups.filter(is_access_group=True, curators=profile).exists(): msg = 'Access group curators cannot use Passwordless as the login method.' messages.error(self.request, msg) return None # With account deracheting we will always get the same Auth0 user id. Mark it as primary if not obj.primary: obj.primary = True IdpProfile.objects.filter(profile=profile).exclude(id=obj.id).update(primary=False) # Update/Save the Github username if 'github|' in auth0_user_id: obj.username = self.claims.get('nickname', '') # Save once obj.save() # Update CIS send_userprofile_to_cis.delay(profile.pk) return user
def check_authentication_method(self, user): """Check which Identity is used to login. This method, depending on the current status of the IdpProfile of a user, enforces MFA logins and creates the IdpProfiles. Returns the object (user) it was passed unchanged. """ if not user: return None profile = user.userprofile auth0_user_id = self.claims.get('user_id') email = self.claims.get('email') # Get current_idp current_idp = get_object_or_none(IdpProfile, profile=profile, primary=True) # Get or create new `user_id` obj, _ = IdpProfile.objects.get_or_create(profile=profile, email=email, auth0_user_id=auth0_user_id) # Do not allow downgrades. if current_idp and obj.type < current_idp.type: msg = u'Please use one of the following authentication methods: {}' # convert the tuple to a dict to easily get the values provider_types = dict(IdpProfile.PROVIDER_TYPES) methods = ', '.join(provider_types[x] for x in ALLOWED_IDP_FLOWS[current_idp.type]) messages.error(self.request, msg.format(methods)) return None # Mark other `user_id` as `primary=False` idp_q = IdpProfile.objects.filter(profile=profile) with transaction.atomic(): idp_q.exclude(auth0_user_id=auth0_user_id, email=email).update(primary=False) # Mark current `user_id` as `primary=True` idp_q.filter(auth0_user_id=auth0_user_id, email=email).update(primary=True) # Update CIS send_userprofile_to_cis.delay(profile.pk) return user
def check_authentication_method(self, user): """Check which Identity is used to login. This method, depending on the current status of the IdpProfile of a user, enforces MFA logins and creates the IdpProfiles. Returns the object (user) it was passed unchanged. """ if not user: return None profile = user.userprofile # Ensure compatibility with OIDC conformant mode auth0_user_id = self.claims.get('user_id') or self.claims.get('sub') email = self.claims.get('email') # Get current_idp current_idp = get_object_or_none(IdpProfile, profile=profile, primary=True) # Get or create new `user_id` obj, _ = IdpProfile.objects.get_or_create( profile=profile, email=email, auth0_user_id=auth0_user_id) # Do not allow downgrades. # Round the current type to the floor. This way 30 and 39 will have always the same # priority. if current_idp and obj.type < int(math.floor(current_idp.type / 10) * 10): msg = u'Please use one of the following authentication methods: {}' # convert the tuple to a dict to easily get the values provider_types = dict(IdpProfile.PROVIDER_TYPES) methods = ', '.join(provider_types[x] for x in ALLOWED_IDP_FLOWS[current_idp.type]) messages.error(self.request, msg.format(methods)) return None # Mark other `user_id` as `primary=False` idp_q = IdpProfile.objects.filter(profile=profile) with transaction.atomic(): idp_q.exclude(auth0_user_id=auth0_user_id, email=email).update(primary=False) # Mark current `user_id` as `primary=True` idp_q.filter(auth0_user_id=auth0_user_id, email=email).update(primary=True) # Update CIS send_userprofile_to_cis.delay(profile.pk) return user
def check_authentication_method(self, user): """Check which Identity is used to login. This method, depending on the current status of the IdpProfile of a user, enforces MFA logins and creates the IdpProfiles. Returns the object (user) it was passed unchanged. """ if not user: return None profile = user.userprofile # Ensure compatibility with OIDC conformant mode auth0_user_id = self.claims.get('user_id') or self.claims.get('sub') email = self.claims.get('email') # Get current_idp current_idp = get_object_or_none(IdpProfile, profile=profile, primary=True) # Get or create new `user_id` obj, _ = IdpProfile.objects.get_or_create( profile=profile, email=email, auth0_user_id=auth0_user_id) # Update/Save the Github username if 'github|' in auth0_user_id: obj.username = self.claims.get('nickname', '') obj.save() # Do not allow downgrades. if current_idp and obj.type < current_idp.type: msg = u'Please use {0} as the login method to authenticate' messages.error(self.request, msg.format(current_idp.get_type_display())) return None # Mark other `user_id` as `primary=False` idp_q = IdpProfile.objects.filter(profile=profile) with transaction.atomic(): idp_q.exclude(auth0_user_id=auth0_user_id, email=email).update(primary=False) # Mark current `user_id` as `primary=True` idp_q.filter(auth0_user_id=auth0_user_id, email=email).update(primary=True) # Update CIS send_userprofile_to_cis.delay(profile.pk) return user
def remove_member(self, userprofile, status=None, send_email=False): """Change membership status for a group. If user is a member of an open group, then the user is removed. If a user is a member of a reviewed or closed group, then the membership is in a pending state. """ try: membership = GroupMembership.objects.get(group=self, userprofile=userprofile) except GroupMembership.DoesNotExist: return old_status = membership.status # If the group is of type Group.OPEN, delete membership # If no status is given, delete membership, # If the current membership is PENDING*, delete membership if (not status or self.accepting_new_members == Group.OPEN or old_status != GroupMembership.MEMBER): # We have either an open group or the request to join a reviewed group is denied # or the curator manually declined a user in a pending state. membership.delete() send_userprofile_to_cis.delay(membership.userprofile.pk) # delete the invitation to the group if exists Invite.objects.filter(group=self, redeemer=userprofile).delete() send_email = True # Group is either of Group.REVIEWED or Group.CLOSED, change membership to `status` else: # if we are here, there is a new status for our user membership.status = status membership.needs_renewal = False membership.save() send_email = True # If group is the NDA group, unsubscribe user from the newsletter. if self.name == settings.NDA_GROUP: unsubscribe_from_basket_task.delay( userprofile.email, [settings.BASKET_NDA_NEWSLETTER]) if send_email: email_membership_change.delay(self.pk, userprofile.user.pk, old_status, status)
def push_empty_groups_to_cis(sender, instance, **kwargs): """Notify CIS about the profile deletion. Remove all the access groups and tags from the profile. """ from mozillians.users.tasks import send_userprofile_to_cis data = bundle_profile_data(instance.id, delete=True) for d in data: log_name = 'CIS group deletion - {}'.format(d['user_id']) log_data = { 'level': logging.DEBUG, 'logger': 'mozillians.cis_transaction' } log_extra = { 'cis_transaction_data': json.dumps(d) } sentry_client.captureMessage(log_name, data=log_data, stack=True, extra=log_extra) send_userprofile_to_cis.delay(profile_results=data)
def push_empty_groups_to_cis(sender, instance, **kwargs): """Notify CIS about the profile deletion. Remove all the access groups and tags from the profile. """ from mozillians.users.tasks import send_userprofile_to_cis data = bundle_profile_data(instance.id, delete=True) for d in data: log_name = 'CIS group deletion - {}'.format(d['user_id']) log_data = { 'level': logging.DEBUG, 'logger': 'mozillians.cis_transaction' } log_extra = {'cis_transaction_data': json.dumps(d)} sentry_client.captureMessage(log_name, data=log_data, stack=True, extra=log_extra) send_userprofile_to_cis.delay(profile_results=data)
def check_authentication_method(self, user): """Check which Identity is used to login. This method, depending on the current status of the IdpProfile of a user, enforces MFA logins and creates the IdpProfiles. Returns the object (user) it was passed unchanged. """ if not user: return None profile = user.userprofile # Ensure compatibility with OIDC conformant mode auth0_user_id = self.claims.get('user_id') or self.claims.get('sub') email = self.claims.get('email') # Grant an employee vouch if the user has the 'hris_is_staff' group groups = self.claims.get('https://sso.mozilla.com/claim/groups') if groups and 'hris_is_staff' in groups: profile.auto_vouch() # Get or create new `user_id` obj, _ = IdpProfile.objects.get_or_create(profile=profile, email=email, auth0_user_id=auth0_user_id) # Update/Save the Github username if 'github|' in auth0_user_id: obj.username = self.claims.get('nickname', '') obj.save() idp_q = IdpProfile.objects.filter(profile=profile) # This is happening only the first time a user logs into mozillians.org if not idp_q.filter(primary=True).exists(): with transaction.atomic(): idp_q.filter(auth0_user_id=auth0_user_id, email=email).update(primary=True) # Update CIS send_userprofile_to_cis.delay(profile.pk) return user
def delete_identity(request, identity_pk): """Delete alternate email address.""" user = User.objects.get(pk=request.user.id) profile = user.userprofile # Only email owner can delete emails idp_query = IdpProfile.objects.filter(profile=profile, pk=identity_pk) if not idp_query.exists(): raise Http404() idp_query = idp_query.filter(primary=False, primary_contact_identity=False) if idp_query.exists(): idp_type = idp_query[0].get_type_display() idp_query.delete() send_userprofile_to_cis.delay(profile.pk) msg = _(u'Identity {0} successfully deleted.'.format(idp_type)) messages.success(request, msg) return redirect('phonebook:profile_edit') # We are trying to delete the primary identity, politely ignore the request msg = _(u'Sorry the requested Identity cannot be deleted.') messages.error(request, msg) return redirect('phonebook:profile_edit')
def delete_identity(request, identity_pk): """Delete alternate email address.""" user = User.objects.get(pk=request.user.id) profile = user.userprofile # Only email owner can delete emails idp_query = IdpProfile.objects.filter(profile=profile, pk=identity_pk) if not idp_query.exists(): raise Http404() idp_query = idp_query.filter(primary=False, primary_contact_identity=False) if idp_query.exists(): idp_type = idp_query[0].get_type_display() idp_query.delete() send_userprofile_to_cis.delay(profile.pk) msg = _(u'Identity {0} successfully deleted.'.format(idp_type)) messages.success(request, msg) return redirect('phonebook:profile_edit') # We are trying to delete the primary identity, politely ignore the request msg = _(u'Sorry the requested Identity cannot be deleted.') messages.error(request, msg) return redirect('phonebook:profile_edit')
def send_profile_to_cis_action(modeladmin, request, queryset): for obj in queryset: send_userprofile_to_cis.delay(obj.pk)
def remove_member(self, userprofile, status=None, send_email=False): """Change membership status for a group. If user is a member of an open group, then the user is removed. If a user is a member of a reviewed or closed group, then the membership is in a pending state. """ # Avoid circular dependencies from mozillians.users.models import UserProfile try: membership = GroupMembership.objects.get(group=self, userprofile=userprofile) except GroupMembership.DoesNotExist: return old_status = membership.status # If the group is of type Group.OPEN, delete membership # If no status is given, delete membership, # If the current membership is PENDING*, delete membership if (not status or self.accepting_new_members == Group.OPEN or old_status != GroupMembership.MEMBER): # We have either an open group or the request to join a reviewed group is denied # or the curator manually declined a user in a pending state. membership.delete() # delete the invitation to the group if exists Invite.objects.filter(group=self, redeemer=userprofile).delete() send_email = True # Remove all the access groups the user is a member of # if the group to remove is the NDA if self.name == settings.NDA_GROUP: group_memberships = GroupMembership.objects.none() # If the user is not staff, we need to delete the memberships to any access group if not userprofile.can_create_access_groups: group_memberships = GroupMembership.objects.filter(userprofile=userprofile, group__is_access_group=True) for access_membership in group_memberships: group = access_membership.group if not group.curator_can_leave(userprofile): # If the user is the only curator, let's add the superusers as curators # as a fallback option for super_user in UserProfile.objects.filter(user__is_superuser=True): group.curators.add(super_user) if not group.has_member(super_user): group.add_member(super_user) group.curators.remove(userprofile) access_membership.delete() # Notify CIS about this change send_userprofile_to_cis.delay(access_membership.userprofile.pk) # Notify CIS about this change send_userprofile_to_cis.delay(membership.userprofile.pk) # Group is either of Group.REVIEWED or Group.CLOSED, change membership to `status` else: # if we are here, there is a new status for our user membership.status = status membership.needs_renewal = False membership.save() send_email = True # If group is the NDA group, unsubscribe user from the newsletter. if self.name == settings.NDA_GROUP: unsubscribe_from_basket_task.delay(userprofile.email, [settings.BASKET_NDA_NEWSLETTER]) if send_email: email_membership_change.delay(self.pk, userprofile.user.pk, old_status, status)
def process_cis_profile(self, request, user_id, *args, **kwargs): messages.success(request, 'Profile with id {0} sent to CIS.'.format(user_id)) send_userprofile_to_cis.delay(user_id) return HttpResponseRedirect( reverse('admin:users_userprofile_changelist'))
def send_profile_to_cis_action(modeladmin, request, queryset): for obj in queryset: send_userprofile_to_cis.delay(obj.pk)
def get(self, request): """Callback handler for OIDC authorization code flow. This is based on the mozilla-django-oidc library. This callback is used to verify the identity added by the user. Users are already logged in and we do not care about authentication. The JWT token is used to prove the identity of the user. """ profile = request.user.userprofile # This is a difference nonce than the one used to login! nonce = request.session.get('oidc_verify_nonce') if nonce: # Make sure that nonce is not used twice del request.session['oidc_verify_nonce'] # Check for all possible errors and display a message to the user. errors = [ 'code' not in request.GET, 'state' not in request.GET, 'oidc_verify_state' not in request.session, request.GET['state'] != request.session['oidc_verify_state'] ] if any(errors): msg = 'Something went wrong, account verification failed.' messages.error(request, msg) return redirect('phonebook:profile_edit') token_payload = { 'client_id': self.OIDC_RP_VERIFICATION_CLIENT_ID, 'client_secret': self.OIDC_RP_VERIFICATION_CLIENT_SECRET, 'grant_type': 'authorization_code', 'code': request.GET['code'], 'redirect_uri': absolutify(self.request, nonprefixed_url('phonebook:verify_identity_callback')), } response = requests.post(self.OIDC_OP_TOKEN_ENDPOINT, data=token_payload, verify=import_from_settings( 'OIDC_VERIFY_SSL', True)) response.raise_for_status() token_response = response.json() id_token = token_response.get('id_token') # Verify JWT jws = JWS.from_compact(force_bytes(id_token)) jwk = JWK.load(smart_bytes(self.OIDC_RP_VERIFICATION_CLIENT_SECRET)) verified_token = None if jws.verify(jwk): verified_token = jws.payload # Create the new Identity Profile. if verified_token: user_info = json.loads(verified_token) email = user_info['email'] if not user_info.get('email_verified'): msg = 'Account verification failed: Email is not verified.' messages.error(request, msg) return redirect('phonebook:profile_edit') user_q = {'auth0_user_id': user_info['sub'], 'email': email} # If we are linking GitHub we need to save # the username too. if 'github|' in user_info['sub']: user_q['username'] = user_info['nickname'] # Check that the identity doesn't exist in another Identity profile # or in another mozillians profile error_msg = '' if IdpProfile.objects.filter(**user_q).exists(): error_msg = 'Account verification failed: Identity already exists.' elif User.objects.filter(email__iexact=email).exclude( pk=profile.user.pk).exists(): error_msg = 'The email in this identity is used by another user.' if error_msg: messages.error(request, error_msg) next_url = self.request.session.get('oidc_login_next', None) return HttpResponseRedirect( next_url or reverse('phonebook:profile_edit')) # Save the new identity to the IdpProfile user_q['profile'] = profile idp, created = IdpProfile.objects.get_or_create(**user_q) current_idp = get_object_or_none(IdpProfile, profile=profile, primary=True) # The new identity is stronger than the one currently used. Let's swap append_msg = '' # We need to check for equality too in the case a user updates the primary email in # the same identity (matching auth0_user_id). If there is an addition of the same type # we are not swapping login identities if ((current_idp and current_idp.type < idp.type) or (current_idp and current_idp.auth0_user_id == idp.auth0_user_id) or (not current_idp and created and idp.type >= IdpProfile.PROVIDER_GITHUB)): IdpProfile.objects.filter(profile=profile).exclude( pk=idp.pk).update(primary=False) idp.primary = True idp.save() # Also update the primary email of the user update_email_in_basket(profile.user.email, idp.email) User.objects.filter(pk=profile.user.id).update(email=idp.email) append_msg = ' You need to use this identity the next time you will login.' send_userprofile_to_cis.delay(profile.pk) if created: msg = 'Account successfully verified.' if append_msg: msg += append_msg messages.success(request, msg) else: msg = 'Account verification failed: Identity already exists.' messages.error(request, msg) next_url = self.request.session.get('oidc_login_next', None) return HttpResponseRedirect(next_url or reverse('phonebook:profile_edit'))
def process_cis_profile(self, request, user_id, *args, **kwargs): messages.success(request, 'Profile with id {0} sent to CIS.'.format(user_id)) send_userprofile_to_cis.delay(user_id) return HttpResponseRedirect(reverse('admin:users_userprofile_changelist'))
def delete_groupmembership(sender, instance, **kwargs): from mozillians.users.tasks import send_userprofile_to_cis send_userprofile_to_cis.delay(instance.userprofile.pk)
def remove_member(self, userprofile, status=None, send_email=False): """Change membership status for a group. If user is a member of an open group, then the user is removed. If a user is a member of a reviewed or closed group, then the membership is in a pending state. """ # Avoid circular dependencies from mozillians.users.models import UserProfile try: membership = GroupMembership.objects.get(group=self, userprofile=userprofile) except GroupMembership.DoesNotExist: return old_status = membership.status # If the group is of type Group.OPEN, delete membership # If no status is given, delete membership, # If the current membership is PENDING*, delete membership if (not status or self.accepting_new_members == Group.OPEN or old_status != GroupMembership.MEMBER): # We have either an open group or the request to join a reviewed group is denied # or the curator manually declined a user in a pending state. membership.delete() # delete the invitation to the group if exists Invite.objects.filter(group=self, redeemer=userprofile).delete() send_email = True # Remove all the access groups the user is a member of # if the group to remove is the NDA if self.name == settings.NDA_GROUP: group_memberships = GroupMembership.objects.none() # If the user is not staff, we need to delete the memberships to any access group if not userprofile.can_create_access_groups: group_memberships = GroupMembership.objects.filter( userprofile=userprofile, group__is_access_group=True) for access_membership in group_memberships: group = access_membership.group if not group.curator_can_leave(userprofile): # If the user is the only curator, let's add the superusers as curators # as a fallback option for super_user in UserProfile.objects.filter( user__is_superuser=True): group.curators.add(super_user) if not group.has_member(super_user): group.add_member(super_user) group.curators.remove(userprofile) access_membership.delete() # Notify CIS about this change send_userprofile_to_cis.delay( access_membership.userprofile.pk) # Notify CIS about this change send_userprofile_to_cis.delay(membership.userprofile.pk) # Group is either of Group.REVIEWED or Group.CLOSED, change membership to `status` else: # if we are here, there is a new status for our user membership.status = status membership.needs_renewal = False membership.save() send_email = True # If group is the NDA group, unsubscribe user from the newsletter. if self.name == settings.NDA_GROUP: unsubscribe_from_basket_task.delay( userprofile.email, [settings.BASKET_NDA_NEWSLETTER]) if send_email: email_membership_change.delay(self.pk, userprofile.user.pk, old_status, status)
def delete_groupmembership(sender, instance, **kwargs): from mozillians.users.tasks import send_userprofile_to_cis send_userprofile_to_cis.delay(instance.userprofile.pk)
def get(self, request): """Callback handler for OIDC authorization code flow. This is based on the mozilla-django-oidc library. This callback is used to verify the identity added by the user. Users are already logged in and we do not care about authentication. The JWT token is used to prove the identity of the user. """ profile = request.user.userprofile # This is a difference nonce than the one used to login! nonce = request.session.get('oidc_verify_nonce') if nonce: # Make sure that nonce is not used twice del request.session['oidc_verify_nonce'] # Check for all possible errors and display a message to the user. errors = [ 'code' not in request.GET, 'state' not in request.GET, 'oidc_verify_state' not in request.session, request.GET['state'] != request.session['oidc_verify_state'] ] if any(errors): msg = 'Something went wrong, account verification failed.' messages.error(request, msg) return redirect('phonebook:profile_edit') token_payload = { 'client_id': self.OIDC_RP_VERIFICATION_CLIENT_ID, 'client_secret': self.OIDC_RP_VERIFICATION_CLIENT_SECRET, 'grant_type': 'authorization_code', 'code': request.GET['code'], 'redirect_uri': absolutify( self.request, nonprefixed_url('phonebook:verify_identity_callback') ), } response = requests.post(self.OIDC_OP_TOKEN_ENDPOINT, data=token_payload, verify=import_from_settings('OIDC_VERIFY_SSL', True)) response.raise_for_status() token_response = response.json() id_token = token_response.get('id_token') # Verify JWT jws = JWS.from_compact(force_bytes(id_token)) jwk = JWK.load(smart_bytes(self.OIDC_RP_VERIFICATION_CLIENT_SECRET)) verified_token = None if jws.verify(jwk): verified_token = jws.payload # Create the new Identity Profile. if verified_token: user_info = json.loads(verified_token) email = user_info['email'] if not user_info.get('email_verified'): msg = 'Account verification failed: Email is not verified.' messages.error(request, msg) return redirect('phonebook:profile_edit') user_q = { 'auth0_user_id': user_info['sub'], 'email': email } # If we are linking GitHub we need to save # the username too. if 'github|' in user_info['sub']: user_q['username'] = user_info['nickname'] # Check that the identity doesn't exist in another Identity profile # or in another mozillians profile error_msg = '' if IdpProfile.objects.filter(**user_q).exists(): error_msg = 'Account verification failed: Identity already exists.' elif User.objects.filter(email__iexact=email).exclude(pk=profile.user.pk).exists(): error_msg = 'The email in this identity is used by another user.' if error_msg: messages.error(request, error_msg) next_url = self.request.session.get('oidc_login_next', None) return HttpResponseRedirect(next_url or reverse('phonebook:profile_edit')) # Save the new identity to the IdpProfile user_q['profile'] = profile idp, created = IdpProfile.objects.get_or_create(**user_q) current_idp = get_object_or_none(IdpProfile, profile=profile, primary=True) # The new identity is stronger than the one currently used. Let's swap append_msg = '' # We need to check for equality too in the case a user updates the primary email in # the same identity (matching auth0_user_id). If there is an addition of the same type # we are not swapping login identities if ((current_idp and current_idp.type < idp.type) or (current_idp and current_idp.auth0_user_id == idp.auth0_user_id) or (not current_idp and created and idp.type >= IdpProfile.PROVIDER_GITHUB)): IdpProfile.objects.filter(profile=profile).exclude(pk=idp.pk).update(primary=False) idp.primary = True idp.save() # Also update the primary email of the user update_email_in_basket(profile.user.email, idp.email) User.objects.filter(pk=profile.user.id).update(email=idp.email) append_msg = ' You need to use this identity the next time you will login.' send_userprofile_to_cis.delay(profile.pk) if created: msg = 'Account successfully verified.' if append_msg: msg += append_msg messages.success(request, msg) else: msg = 'Account verification failed: Identity already exists.' messages.error(request, msg) next_url = self.request.session.get('oidc_login_next', None) return HttpResponseRedirect(next_url or reverse('phonebook:profile_edit'))