def retrieve_v2_profile(request, username=None, from_db=False): """Helper method to retrieve a profile either from the v2 schema or from the database. """ if not username and not request.user.is_authenticated(): return None username_q = username or request.user.username if from_db: # This is a db query, let's return the user profile = get_object_or_none(User, username=username_q) return profile # We need to fetch data from ES orgchart_related_data = None if not username: # This is a query to get a minified version of a profile # for the user menu profile_data = search_get_profile(request, username_q, UserAccessLevel.PRIVATE) else: profile_data = search_get_profile(request, username_q) orgchart_related_data = orgchart_get_by_username( request, 'related', username_q) profile = json2obj(profile_data.content) if orgchart_related_data: profile.update(json2obj(orgchart_related_data.content)) return profile
def handle(self, *args, **options): path = options.get('file', None) dry_run = options.get('dry-run') if not path: raise CommandError('Option --file must be specified') try: f = open(path) except IOError: raise CommandError('Invalid file path.') now = timezone.now() former_employee_descr = 'An automatic vouch for being a former Mozilla employee.' employee_descr = 'An automatic vouch for being a Mozilla employee.' count = 0 for email in f: u = get_object_or_none(UserProfile, user__email=email.strip()) if u: vouches = u.vouches_received.all() already_vouched = vouches.filter( Q(description=employee_descr) | Q(description=former_employee_descr), autovouch=True) if not already_vouched.exists(): if not dry_run: Vouch.objects.create(voucher=None, vouchee=u, autovouch=True, date=now, description=former_employee_descr) count = count + 1 print "%d former staff members vouched." % count
def subscribe_user_task(self, result, email='', newsletters=[], sync='N', optin='Y'): """Subscribes a user to basket newsletters. The email to subscribe is provided either from the result of the lookup task or from the profile of the user. """ if not result and not email: return None from mozillians.users.models import UserProfile profile = get_object_or_none(UserProfile, user__email=email) newsletters_to_subscribe = [] if result.get('status') == 'ok': # This is used when we want to subscribe a different email # than the one in the lookup (eg when a user changes emails) if not email: email = result.get('email') if newsletters: newsletters_to_subscribe = list(set(newsletters) - set(result['newsletters'])) else: # This case is used when a user changes email. # The lookup task will provide the newsletters that the user was registered. # We need to find the common with the mozillians newsletters and # subscribe the email provided as an argument. newsletters_to_subscribe = list(set(MOZILLIANS_NEWSLETTERS) .intersection(result['newsletters'])) # The lookup failed because the user does not exist. We have a new user! if (result.get('status') == 'error' and result.get('desc') == u'User not found' and newsletters): newsletters_to_subscribe = newsletters if newsletters_to_subscribe: try: kwargs = { 'sync': sync, 'source_url': MOZILLIANS_URL, 'optin': optin, 'api_key': BASKET_API_KEY } if profile and profile.country: kwargs['country'] = profile.country.code2 subscribe_result = basket.subscribe(email, newsletters_to_subscribe, **kwargs) except MaxRetriesExceededError as exc: raise exc except basket.BasketException as exc: raise self.retry(exc=exc) return subscribe_result return None
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 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) # 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 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 bundle_profile_data(profile_id, delete=False): """Packs all the Identity Profiles of a user into a dictionary.""" from mozillians.common.templatetags.helpers import get_object_or_none from mozillians.users.models import IdpProfile, UserProfile try: profile = UserProfile.objects.get(pk=profile_id) except UserProfile.DoesNotExist: return [] human_name = HumanName(profile.full_name) primary_idp = get_object_or_none(IdpProfile, profile=profile, primary=True) primary_login_email = profile.email if primary_idp: primary_login_email = primary_idp.email results = [] for idp in profile.idp_profiles.all(): data = { 'user_id': idp.auth0_user_id, 'timezone': profile.timezone, 'active': profile.user.is_active, 'lastModified': profile.last_updated.isoformat(), 'created': profile.user.date_joined.isoformat(), 'userName': profile.user.username, 'displayName': profile.display_name, 'primaryEmail': primary_login_email, 'emails': profile.get_cis_emails(), 'uris': profile.get_cis_uris(), 'picture': profile.get_photo_url(), 'shirtSize': profile.get_tshirt_display() or '', 'groups': [] if delete else profile.get_cis_groups(idp), 'tags': [] if delete else profile.get_cis_tags(), # Derived fields 'firstName': human_name.first, 'lastName': human_name.last, # Hardcoded fields 'preferredLanguage': 'en_US', 'phoneNumbers': [], 'nicknames': [], 'SSHFingerprints': [], 'PGPFingerprints': [], 'authoritativeGroups': [] } results.append(data) return results
def check_spam_account(instance_id, **kwargs): """Task to check if profile is spam according to akismet""" # Avoid circular dependencies from mozillians.users.models import AbuseReport, UserProfile spam = akismet_spam_check(**kwargs) profile = get_object_or_none(UserProfile, id=instance_id) if spam and profile: kwargs = { 'type': AbuseReport.TYPE_SPAM, 'profile': profile, 'reporter': None, 'is_akismet': True, } AbuseReport.objects.get_or_create(**kwargs)
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) # 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 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 get_results(self, context): """Modify the text in the results of the group invitation form.""" results = [] for result in context['object_list']: pk = self.get_result_value(result) if not pk: continue profile = UserProfile.objects.get(pk=pk) idp = get_object_or_none(IdpProfile, profile=profile, primary=True) text = self.get_result_label(result) # Append the email used for login in the autocomplete text if idp: text += ' ({0})'.format(idp.email) item = {'id': pk, 'text': text} results.append(item) return results
def handle(self, *args, **options): path = options.get('file', None) dry_run = options.get('dry-run') if not path: raise CommandError('Option --file must be specified') try: f = open(path) except IOError: raise CommandError('Invalid file path.') now = timezone.now() former_employee_descr = 'An automatic vouch for being a former Mozilla employee.' employee_descr = 'An automatic vouch for being a Mozilla employee.' count = 0 for email in f: u = get_object_or_none(UserProfile, user__email=email.strip()) if u: vouches = u.vouches_received.all() already_vouched = vouches.filter( Q(description=employee_descr) | Q(description=former_employee_descr), autovouch=True ) if not already_vouched.exists(): if not dry_run: Vouch.objects.create( voucher=None, vouchee=u, autovouch=True, date=now, description=former_employee_descr ) count = count + 1 print "%d former staff members vouched." % count
def retrieve_v2_profile(request, username, from_db=False): """Helper method to retrieve a profile either from the v2 schema or from the database. """ profile_username = None if request.user.is_authenticated(): profile_username = request.user.username username_q = username or profile_username if not username_q: return None if from_db: profile = get_object_or_none(User, username=username_q) else: # We need to fetch data from ES profile_data = search_get_profile(request, username_q) orgchart_related_data = orgchart_get_by_username( request, 'related', username_q) profile = json2obj(profile_data.content) profile.update(json2obj(orgchart_related_data.content)) return profile
def get_results(self, context): """Modify the text in the results of the group invitation form.""" results = [] for result in context['object_list']: pk = self.get_result_value(result) if not pk: continue profile = UserProfile.objects.get(pk=pk) idp = get_object_or_none(IdpProfile, profile=profile, primary=True) text = self.get_result_label(result) # Append the email used for login in the autocomplete text if idp: text += ' ({0})'.format(idp.email) item = { 'id': pk, 'text': text } results.append(item) return results
def edit_profile(request): """Edit user profile view.""" # Don't use request.user user = User.objects.get(pk=request.user.id) profile = user.userprofile user_groups = profile.groups.all().order_by('name') idp_profiles = IdpProfile.objects.filter(profile=profile) idp_primary_profile = get_object_or_none(IdpProfile, profile=profile, primary=True) # The accounts that a user can select as the primary login identity accounts_qs = ExternalAccount.objects.exclude(type=ExternalAccount.TYPE_EMAIL) sections = { 'registration_section': ['user_form', 'registration_form'], 'basic_section': ['user_form', 'basic_information_form'], 'groups_section': ['groups_privacy_form'], 'skills_section': ['skills_form'], 'idp_section': ['idp_profile_formset'], 'languages_section': ['language_privacy_form', 'language_formset'], 'accounts_section': ['accounts_formset'], 'location_section': ['location_form'], 'irc_section': ['irc_form'], 'contribution_section': ['contribution_form'], 'tshirt_section': ['tshirt_form'], } curr_sect = next((s for s in sections.keys() if s in request.POST), None) def get_request_data(form): if curr_sect and form in sections[curr_sect]: return request.POST return None ctx = {} ctx['user_form'] = forms.UserForm(get_request_data('user_form'), instance=user) ctx['registration_form'] = forms.RegisterForm(get_request_data('registration_form'), request.FILES or None, instance=profile) basic_information_data = get_request_data('basic_information_form') ctx['basic_information_form'] = forms.BasicInformationForm(basic_information_data, request.FILES or None, instance=profile) ctx['accounts_formset'] = forms.AccountsFormset(get_request_data('accounts_formset'), instance=profile, queryset=accounts_qs) ctx['location_form'] = forms.LocationForm(get_request_data('location_form'), instance=profile) ctx['language_formset'] = forms.LanguagesFormset(get_request_data('language_formset'), instance=profile, locale=request.locale) language_privacy_data = get_request_data('language_privacy_form') ctx['language_privacy_form'] = forms.LanguagesPrivacyForm(language_privacy_data, instance=profile) ctx['skills_form'] = forms.SkillsForm(get_request_data('skills_form'), instance=profile) ctx['contribution_form'] = forms.ContributionForm(get_request_data('contribution_form'), instance=profile) ctx['tshirt_form'] = forms.TshirtForm(get_request_data('tshirt_form'), instance=profile) ctx['groups_privacy_form'] = forms.GroupsPrivacyForm(get_request_data('groups_privacy_form'), instance=profile) ctx['irc_form'] = forms.IRCForm(get_request_data('irc_form'), instance=profile) ctx['idp_profile_formset'] = forms.IdpProfileFormset(get_request_data('idp_profile_formset'), instance=profile, queryset=idp_profiles) ctx['idp_primary_profile'] = idp_primary_profile ctx['autocomplete_form_media'] = ctx['registration_form'].media + ctx['skills_form'].media forms_valid = True if request.POST: if not curr_sect: raise Http404 curr_forms = map(lambda x: ctx[x], sections[curr_sect]) forms_valid = all(map(lambda x: x.is_valid(), curr_forms)) if forms_valid: old_username = request.user.username for f in curr_forms: f.save() # Spawn task to check for spam if not profile.is_vouched: x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') if x_forwarded_for: user_ip = x_forwarded_for.split(',')[0] else: user_ip = request.META.get('REMOTE_ADDR') params = { 'instance_id': profile.id, 'user_ip': user_ip, 'user_agent': request.META.get('HTTP_USER_AGENT'), 'referrer': request.META.get('HTTP_REFERER'), 'comment_author': profile.full_name, 'comment_author_email': profile.email, 'comment_content': profile.bio } check_spam_account.delay(**params) next_section = request.GET.get('next') next_url = urlparams(reverse('phonebook:profile_edit'), next_section) if curr_sect == 'registration_section': settings_url = reverse('phonebook:profile_edit') settings_link = '<a href="{0}">settings</a>'.format(settings_url) msg = _(u'Your registration is complete. ' u'Feel free to visit the {0} page to add ' u'additional information to your profile.'.format(settings_link)) messages.info(request, mark_safe(msg)) redeem_invite(profile, request.session.get('invite-code')) next_url = reverse('phonebook:profile_view', args=[user.username]) elif user.username != old_username: msg = _(u'You changed your username; ' u'please note your profile URL has also changed.') messages.info(request, _(msg)) return HttpResponseRedirect(next_url) ctx.update({ 'user_groups': user_groups, 'profile': request.user.userprofile, 'vouch_threshold': settings.CAN_VOUCH_THRESHOLD, 'appsv2': profile.apps.filter(enabled=True), 'forms_valid': forms_valid }) return render(request, 'phonebook/edit_profile.html', ctx)
def view_profile(request, username): """View a profile by username.""" data = {} privacy_mappings = {'anonymous': PUBLIC, 'mozillian': MOZILLIANS, 'employee': EMPLOYEES, 'privileged': PRIVILEGED, 'myself': None} privacy_level = None abuse_form = None if (request.user.is_authenticated() and request.user.username == username): # own profile view_as = request.GET.get('view_as', 'myself') privacy_level = privacy_mappings.get(view_as, None) profile = UserProfile.objects.privacy_level(privacy_level).get(user__username=username) data['privacy_mode'] = view_as else: userprofile_query = UserProfile.objects.filter(user__username=username) public_profile_exists = userprofile_query.public().exists() profile_exists = userprofile_query.exists() profile_complete = userprofile_query.exclude(full_name='').exists() if not public_profile_exists: if not request.user.is_authenticated(): # you have to be authenticated to continue messages.warning(request, LOGIN_MESSAGE) return (login_required(view_profile, login_url=reverse('phonebook:home')) (request, username)) if not request.user.userprofile.is_vouched: # you have to be vouched to continue messages.error(request, GET_VOUCHED_MESSAGE) return redirect('phonebook:home') if not profile_exists or not profile_complete: raise Http404 profile = UserProfile.objects.get(user__username=username) profile.set_instance_privacy_level(PUBLIC) if request.user.is_authenticated(): profile.set_instance_privacy_level( request.user.userprofile.privacy_level) if (request.user.is_authenticated() and request.user.userprofile.is_vouched and not profile.can_vouch): abuse_report = get_object_or_none(AbuseReport, reporter=request.user.userprofile, profile=profile) if not abuse_report: abuse_report = AbuseReport(reporter=request.user.userprofile, profile=profile) abuse_form = forms.AbuseReportForm(request.POST or None, instance=abuse_report) if abuse_form.is_valid(): abuse_form.save() msg = _(u'Thanks for helping us improve mozillians.org!') messages.info(request, msg) return redirect('phonebook:profile_view', profile.user.username) if (request.user.is_authenticated() and profile.is_vouchable(request.user.userprofile)): vouch_form = forms.VouchForm(request.POST or None) data['vouch_form'] = vouch_form if vouch_form.is_valid(): # We need to re-fetch profile from database. profile = UserProfile.objects.get(user__username=username) profile.vouch(request.user.userprofile, vouch_form.cleaned_data['description']) # Notify the current user that they vouched successfully. msg = _(u'Thanks for vouching for a fellow Mozillian! This user is now vouched!') messages.info(request, msg) return redirect('phonebook:profile_view', profile.user.username) data['shown_user'] = profile.user data['profile'] = profile data['groups'] = profile.get_annotated_groups() data['abuse_form'] = abuse_form # Only show pending groups if user is looking at their own profile, # or current user is a superuser if not (request.user.is_authenticated() and (request.user.username == username or request.user.is_superuser)): data['groups'] = [grp for grp in data['groups'] if not (grp.pending or grp.pending_terms)] return render(request, 'phonebook/profile.html', data)
def edit_profile(request): """Edit user profile view.""" # Don't use request.user user = User.objects.get(pk=request.user.id) profile = user.userprofile idp_profiles = IdpProfile.objects.filter(profile=profile) idp_primary_profile = get_object_or_none(IdpProfile, profile=profile, primary=True) # The accounts that a user can select as the primary login identity accounts_qs = ExternalAccount.objects.exclude( type=ExternalAccount.TYPE_EMAIL) sections = { 'basic_section': ['user_form', 'basic_information_form'], 'idp_section': ['idp_profile_formset'], 'accounts_section': ['accounts_formset'], 'location_section': ['location_form'], 'contribution_section': ['contribution_form'], } curr_sect = next((s for s in sections.keys() if s in request.POST), None) def get_request_data(form): if curr_sect and form in sections[curr_sect]: return request.POST return None ctx = {} ctx['user_form'] = forms.UserForm(get_request_data('user_form'), instance=user) basic_information_data = get_request_data('basic_information_form') ctx['basic_information_form'] = forms.BasicInformationForm( basic_information_data, request.FILES or None, instance=profile) ctx['accounts_formset'] = forms.AccountsFormset( get_request_data('accounts_formset'), instance=profile, queryset=accounts_qs) ctx['location_form'] = forms.LocationForm( get_request_data('location_form'), instance=profile) ctx['contribution_form'] = forms.ContributionForm( get_request_data('contribution_form'), instance=profile) ctx['idp_profile_formset'] = forms.IdpProfileFormset( get_request_data('idp_profile_formset'), instance=profile, queryset=idp_profiles) ctx['idp_primary_profile'] = idp_primary_profile forms_valid = True if request.POST: if not curr_sect: raise Http404 curr_forms = map(lambda x: ctx[x], sections[curr_sect]) forms_valid = all(map(lambda x: x.is_valid(), curr_forms)) if forms_valid: old_username = request.user.username for f in curr_forms: f.save() next_section = request.GET.get('next') next_url = urlparams(reverse('phonebook:profile_edit'), next_section) if user.username != old_username: msg = _(u'You changed your username; ' u'please note your profile URL has also changed.') messages.info(request, _(msg)) return HttpResponseRedirect(next_url) ctx.update({ 'profile': request.user.userprofile, 'vouch_threshold': settings.CAN_VOUCH_THRESHOLD, 'appsv2': profile.apps.filter(enabled=True), 'forms_valid': forms_valid }) return render(request, 'phonebook/edit_profile.html', ctx)
def show(request, url, alias_model, template): """List all members in this group.""" group_alias = get_object_or_404(alias_model, url=url) if group_alias.alias.url != url: return redirect('groups:show_group', url=group_alias.alias.url) is_curator = False is_manager = request.user.userprofile.is_manager is_pending = False show_delete_group_button = False membership_filter_form = forms.MembershipFilterForm(request.GET) group = group_alias.alias profile = request.user.userprofile in_group = group.has_member(profile) memberships = group.members.all() data = {} if isinstance(group, Group): # Has the user accepted the group terms if group.terms: membership = get_object_or_none(GroupMembership, group=group, userprofile=profile, status=GroupMembership.PENDING_TERMS) if membership: return redirect(reverse('groups:review_terms', args=[group.url])) # Is this user's membership pending? is_pending = group.has_pending_member(profile) is_curator = is_manager or (request.user.userprofile in group.curators.all()) # initialize the form only when the group is moderated and user is curator of the group if is_curator and group.accepting_new_members == 'by_request': membership_filter_form = forms.MembershipFilterForm(request.GET) else: membership_filter_form = None if is_curator: statuses = [GroupMembership.MEMBER, GroupMembership.PENDING] if membership_filter_form and membership_filter_form.is_valid(): filtr = membership_filter_form.cleaned_data['filtr'] if filtr == 'members': statuses = [GroupMembership.MEMBER] elif filtr == 'pending_members': statuses = [GroupMembership.PENDING] memberships = group.groupmembership_set.filter(status__in=statuses) # Curators can delete their group if there are no other members. show_delete_group_button = is_curator and group.members.all().count() == 1 else: # only show full members, or this user memberships = group.groupmembership_set.filter( Q(status=GroupMembership.MEMBER) | Q(userprofile=profile)) # Order by UserProfile.Meta.ordering memberships = memberships.order_by('userprofile') # Find the most common skills of the group members. # Order by popularity in the group. shared_skill_ids = (group.members.filter(groupmembership__status=GroupMembership.MEMBER) .values_list('skills', flat=True)) count_skills = defaultdict(int) for skill_id in shared_skill_ids: count_skills[skill_id] += 1 common_skills_ids = [k for k, v in sorted(count_skills.items(), key=lambda x: x[1], reverse=True) if count_skills[k] > 1] # Translate ids to Skills preserving order. skills = [Skill.objects.get(id=skill_id) for skill_id in common_skills_ids if skill_id] data.update(skills=skills, membership_filter_form=membership_filter_form) page = request.GET.get('page', 1) paginator = Paginator(memberships, settings.ITEMS_PER_PAGE) try: people = paginator.page(page) except PageNotAnInteger: people = paginator.page(1) except EmptyPage: people = paginator.page(paginator.num_pages) show_pagination = paginator.count > settings.ITEMS_PER_PAGE extra_data = dict( people=people, group=group, in_group=in_group, is_curator=is_curator, is_pending=is_pending, show_pagination=show_pagination, show_delete_group_button=show_delete_group_button, show_join_button=group.user_can_join(request.user.userprofile), show_leave_button=group.user_can_leave(request.user.userprofile), members=group.member_count, ) data.update(extra_data) return render(request, template, data)
def show(request, url, alias_model, template): """List all members in this group.""" group_alias = get_object_or_404(alias_model, url=url) if group_alias.alias.url != url: return redirect('groups:show_group', url=group_alias.alias.url) is_curator = False is_manager = request.user.userprofile.is_manager is_pending = False show_delete_group_button = False membership_filter_form = forms.MembershipFilterForm(request.GET) group = group_alias.alias profile = request.user.userprofile in_group = group.has_member(profile) memberships = group.members.all() data = {} if isinstance(group, Group): # Has the user accepted the group terms if group.terms: membership = get_object_or_none( GroupMembership, group=group, userprofile=profile, status=GroupMembership.PENDING_TERMS) if membership: return redirect( reverse('groups:review_terms', args=[group.url])) # Is this user's membership pending? is_pending = group.has_pending_member(profile) is_curator = is_manager or (request.user.userprofile in group.curators.all()) # initialize the form only when the group is moderated and user is curator of the group if (is_curator and (group.accepting_new_members == Group.REVIEWED or group.accepting_new_members == Group.CLOSED)): membership_filter_form = forms.MembershipFilterForm(request.GET) else: membership_filter_form = None if is_curator: statuses = [ GroupMembership.MEMBER, GroupMembership.PENDING, GroupMembership.PENDING_TERMS ] q_args = {'status__in': statuses} if membership_filter_form and membership_filter_form.is_valid(): filtr = membership_filter_form.cleaned_data['filtr'] if filtr == 'members': statuses = [GroupMembership.MEMBER] elif filtr == 'pending_members': statuses = [GroupMembership.PENDING] elif filtr == 'pending_terms': statuses = [GroupMembership.PENDING_TERMS] q_args.update({'status__in': statuses}) if filtr == 'needs_renewal': q_args = {'needs_renewal': True} memberships = group.groupmembership_set.filter(**q_args) # Curators can delete their group if there are no other members. show_delete_group_button = is_curator and group.members.all( ).count() == 1 else: # only show full members, or this user memberships = group.groupmembership_set.filter( Q(status=GroupMembership.MEMBER) | Q(userprofile=profile)) invitation = get_object_or_none(Invite, redeemer=profile, group=group, accepted=False) data.update(invitation=invitation) # Order by UserProfile.Meta.ordering memberships = memberships.order_by('userprofile') # Find the most common skills of the group members. # Order by popularity in the group. shared_skill_ids = (group.members.filter( groupmembership__status=GroupMembership.MEMBER).values_list( 'skills', flat=True)) count_skills = defaultdict(int) for skill_id in shared_skill_ids: count_skills[skill_id] += 1 common_skills_ids = [ k for k, _ in sorted( count_skills.items(), key=lambda x: x[1], reverse=True) if count_skills[k] > 1 ] # Translate ids to Skills preserving order. skills = [ Skill.objects.get(id=skill_id) for skill_id in common_skills_ids if skill_id ] data.update(skills=skills, membership_filter_form=membership_filter_form) page = request.GET.get('page', 1) paginator = Paginator(memberships, settings.ITEMS_PER_PAGE) try: people = paginator.page(page) except PageNotAnInteger: people = paginator.page(1) except EmptyPage: people = paginator.page(paginator.num_pages) show_pagination = paginator.count > settings.ITEMS_PER_PAGE extra_data = dict( people=people, group=group, in_group=in_group, is_curator=is_curator, is_pending=is_pending, show_pagination=show_pagination, show_delete_group_button=show_delete_group_button, show_join_button=group.user_can_join(request.user.userprofile), show_leave_button=group.user_can_leave(request.user.userprofile), members=group.member_count, ) data.update(extra_data) return render(request, template, data)
def view_profile(request, username): """View a profile by username.""" data = {} privacy_mappings = {'anonymous': PUBLIC, 'mozillian': MOZILLIANS, 'employee': EMPLOYEES, 'privileged': PRIVILEGED, 'myself': None} privacy_level = None abuse_form = None if (request.user.is_authenticated() and request.user.username == username): # own profile view_as = request.GET.get('view_as', 'myself') privacy_level = privacy_mappings.get(view_as, None) profile = UserProfile.objects.privacy_level(privacy_level).get(user__username=username) data['privacy_mode'] = view_as else: userprofile_query = UserProfile.objects.filter(user__username=username) public_profile_exists = userprofile_query.public().exists() profile_exists = userprofile_query.exists() profile_complete = userprofile_query.exclude(full_name='').exists() if not public_profile_exists: if not request.user.is_authenticated(): # you have to be authenticated to continue messages.warning(request, LOGIN_MESSAGE) return (login_required(view_profile, login_url=reverse('phonebook:home')) (request, username)) if not request.user.userprofile.is_vouched: # you have to be vouched to continue messages.error(request, GET_VOUCHED_MESSAGE) return redirect('phonebook:home') if not profile_exists or not profile_complete: raise Http404 profile = UserProfile.objects.get(user__username=username) profile.set_instance_privacy_level(PUBLIC) if request.user.is_authenticated(): profile.set_instance_privacy_level( request.user.userprofile.privacy_level) if (request.user.is_authenticated() and request.user.userprofile.is_vouched and not profile.can_vouch): abuse_report = get_object_or_none(AbuseReport, reporter=request.user.userprofile, profile=profile) if not abuse_report: abuse_report = AbuseReport(reporter=request.user.userprofile, profile=profile) abuse_form = forms.AbuseReportForm(request.POST or None, instance=abuse_report) if abuse_form.is_valid(): abuse_form.save() msg = _(u'Thanks for helping us improve mozillians.org!') messages.info(request, msg) return redirect('phonebook:profile_view', profile.user.username) if (request.user.is_authenticated() and profile.is_vouchable(request.user.userprofile)): vouch_form = forms.VouchForm(request.POST or None) data['vouch_form'] = vouch_form if vouch_form.is_valid(): # We need to re-fetch profile from database. profile = UserProfile.objects.get(user__username=username) profile.vouch(request.user.userprofile, vouch_form.cleaned_data['description']) # Notify the current user that they vouched successfully. msg = _(u'Thanks for vouching for a fellow Mozillian! This user is now vouched!') messages.info(request, msg) return redirect('phonebook:profile_view', profile.user.username) data['shown_user'] = profile.user data['profile'] = profile data['groups'] = profile.get_annotated_groups() data['abuse_form'] = abuse_form # Only show pending groups if user is looking at their own profile, # or current user is a superuser if not (request.user.is_authenticated() and (request.user.username == username or request.user.is_superuser)): data['groups'] = [grp for grp in data['groups'] if not grp.pending] return render(request, 'phonebook/profile.html', data)
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 edit_profile(request): """Edit user profile view.""" # Don't use request.user user = User.objects.get(pk=request.user.id) profile = user.userprofile user_groups = profile.groups.all().order_by('name') idp_profiles = IdpProfile.objects.filter(profile=profile) idp_primary_profile = get_object_or_none(IdpProfile, profile=profile, primary=True) # The accounts that a user can select as the primary login identity accounts_qs = ExternalAccount.objects.exclude( type=ExternalAccount.TYPE_EMAIL) sections = { 'registration_section': ['user_form', 'registration_form'], 'basic_section': ['user_form', 'basic_information_form'], 'groups_section': ['groups_privacy_form'], 'skills_section': ['skills_form'], 'idp_section': ['idp_profile_formset'], 'languages_section': ['language_privacy_form', 'language_formset'], 'accounts_section': ['accounts_formset'], 'location_section': ['location_form'], 'irc_section': ['irc_form'], 'contribution_section': ['contribution_form'], 'tshirt_section': ['tshirt_form'], } curr_sect = next((s for s in sections.keys() if s in request.POST), None) def get_request_data(form): if curr_sect and form in sections[curr_sect]: return request.POST return None ctx = {} ctx['user_form'] = forms.UserForm(get_request_data('user_form'), instance=user) ctx['registration_form'] = forms.RegisterForm( get_request_data('registration_form'), request.FILES or None, instance=profile) basic_information_data = get_request_data('basic_information_form') ctx['basic_information_form'] = forms.BasicInformationForm( basic_information_data, request.FILES or None, instance=profile) ctx['accounts_formset'] = forms.AccountsFormset( get_request_data('accounts_formset'), instance=profile, queryset=accounts_qs) ctx['location_form'] = forms.LocationForm( get_request_data('location_form'), instance=profile) ctx['language_formset'] = forms.LanguagesFormset( get_request_data('language_formset'), instance=profile, locale=request.locale) language_privacy_data = get_request_data('language_privacy_form') ctx['language_privacy_form'] = forms.LanguagesPrivacyForm( language_privacy_data, instance=profile) ctx['skills_form'] = forms.SkillsForm(get_request_data('skills_form'), instance=profile) ctx['contribution_form'] = forms.ContributionForm( get_request_data('contribution_form'), instance=profile) ctx['tshirt_form'] = forms.TshirtForm(get_request_data('tshirt_form'), instance=profile) ctx['groups_privacy_form'] = forms.GroupsPrivacyForm( get_request_data('groups_privacy_form'), instance=profile) ctx['irc_form'] = forms.IRCForm(get_request_data('irc_form'), instance=profile) ctx['idp_profile_formset'] = forms.IdpProfileFormset( get_request_data('idp_profile_formset'), instance=profile, queryset=idp_profiles) ctx['idp_primary_profile'] = idp_primary_profile ctx['autocomplete_form_media'] = ctx['registration_form'].media + ctx[ 'skills_form'].media forms_valid = True if request.POST: if not curr_sect: raise Http404 curr_forms = map(lambda x: ctx[x], sections[curr_sect]) forms_valid = all(map(lambda x: x.is_valid(), curr_forms)) if forms_valid: old_username = request.user.username for f in curr_forms: f.save() # Spawn task to check for spam if not profile.is_vouched: x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') if x_forwarded_for: user_ip = x_forwarded_for.split(',')[0] else: user_ip = request.META.get('REMOTE_ADDR') params = { 'instance_id': profile.id, 'user_ip': user_ip, 'user_agent': request.META.get('HTTP_USER_AGENT'), 'referrer': request.META.get('HTTP_REFERER'), 'comment_author': profile.full_name, 'comment_author_email': profile.email, 'comment_content': profile.bio } check_spam_account.delay(**params) next_section = request.GET.get('next') next_url = urlparams(reverse('phonebook:profile_edit'), next_section) if curr_sect == 'registration_section': settings_url = reverse('phonebook:profile_edit') settings_link = '<a href="{0}">settings</a>'.format( settings_url) msg = _(u'Your registration is complete. ' u'Feel free to visit the {0} page to add ' u'additional information to your profile.'.format( settings_link)) messages.info(request, mark_safe(msg)) redeem_invite(profile, request.session.get('invite-code')) next_url = reverse('phonebook:profile_view', args=[user.username]) elif user.username != old_username: msg = _(u'You changed your username; ' u'please note your profile URL has also changed.') messages.info(request, _(msg)) return HttpResponseRedirect(next_url) ctx.update({ 'user_groups': user_groups, 'profile': request.user.userprofile, 'vouch_threshold': settings.CAN_VOUCH_THRESHOLD, 'appsv2': profile.apps.filter(enabled=True), 'forms_valid': forms_valid }) return render(request, 'phonebook/edit_profile.html', ctx)
def notify_membership_renewal(): """ For groups with defined `invalidation_days` we need to notify users 2 weeks prior invalidation that the membership is expiring. """ from mozillians.groups.models import Group, GroupMembership, Invite groups = (Group.objects.filter(invalidation_days__isnull=False, invalidation_days__gte=DAYS_BEFORE_INVALIDATION) .exclude(accepting_new_members=Group.OPEN).distinct()) for group in groups: curator_ids = group.curators.all().values_list('id', flat=True) memberships = (group.groupmembership_set.filter(status=GroupMembership.MEMBER) .exclude(userprofile__id__in=curator_ids)) # Filter memberships to be notified # Switch is being used only for testing mail notifications # It disables membership filtering based on date if not switch_is_active('test_membership_renewal_notification'): last_update_days = group.invalidation_days - DAYS_BEFORE_INVALIDATION last_update = now() - timedelta(days=last_update_days) query_start = datetime.combine(last_update.date(), datetime.min.time()) query_end = datetime.combine(last_update.date(), datetime.max.time()) query = { 'updated_on__range': [query_start, query_end], 'needs_renewal': False, } memberships = memberships.filter(**query) member_template = get_template('groups/email/notify_member_renewal.txt') curator_template = get_template('groups/email/notify_curator_renewal.txt') for membership in memberships: ctx = { 'member_full_name': membership.userprofile.full_name, 'group_name': membership.group.name, 'group_url': membership.group.get_absolute_url(), 'member_profile_url': membership.userprofile.get_absolute_url(), 'inviter': None } invite = get_object_or_none(Invite, group=group, redeemer=membership.userprofile) if invite: ctx['inviter'] = invite.inviter subject_msg = unicode('[Mozillians] Your membership to Mozilla group "{0}" ' 'is about to expire') subject = _(subject_msg.format(membership.group.name)) message = member_template.render(ctx) send_mail(subject, message, settings.FROM_NOREPLY, [membership.userprofile.email]) subject_msg = unicode('[Mozillians][{0}] Membership of "{1}" is about to expire') format_args = [membership.group.name, membership.userprofile.full_name] subject = _(subject_msg.format(*format_args)) # In case the membership was created after an invitation we notify inviters only # Else we fallback to all group curators curators = group.curators.all() inviter = ctx['inviter'] if inviter and curators.filter(pk=inviter.id).exists(): curators = [inviter] for curator in curators: ctx['curator_full_name'] = curator.full_name message = curator_template.render(ctx) send_mail(subject, message, settings.FROM_NOREPLY, [curator.email]) # Mark these memberships ready for an early renewal memberships.update(needs_renewal=True)
def notify_membership_renewal(): """ For groups with defined `invalidation_days` we need to notify users 2 weeks prior invalidation that the membership is expiring. """ from mozillians.groups.models import Group, GroupMembership, Invite groups = (Group.objects.filter( invalidation_days__isnull=False, invalidation_days__gte=DAYS_BEFORE_INVALIDATION).exclude( accepting_new_members=Group.OPEN).distinct()) for group in groups: curator_ids = group.curators.all().values_list('id', flat=True) memberships = (group.groupmembership_set.filter( status=GroupMembership.MEMBER).exclude( userprofile__id__in=curator_ids)) # Filter memberships to be notified # Switch is being used only for testing mail notifications # It disables membership filtering based on date if not switch_is_active('test_membership_renewal_notification'): last_update_days = group.invalidation_days - DAYS_BEFORE_INVALIDATION last_update = now() - timedelta(days=last_update_days) query_start = datetime.combine(last_update.date(), datetime.min.time()) query_end = datetime.combine(last_update.date(), datetime.max.time()) query = { 'updated_on__range': [query_start, query_end], 'needs_renewal': False, } memberships = memberships.filter(**query) member_template = get_template( 'groups/email/notify_member_renewal.txt') curator_template = get_template( 'groups/email/notify_curator_renewal.txt') for membership in memberships: ctx = { 'member_full_name': membership.userprofile.full_name, 'group_name': membership.group.name, 'group_url': membership.group.get_absolute_url(), 'member_profile_url': membership.userprofile.get_absolute_url(), 'inviter': None } invite = get_object_or_none(Invite, group=group, redeemer=membership.userprofile) if invite: ctx['inviter'] = invite.inviter subject_msg = unicode( '[Mozillians] Your membership to Mozilla group "{0}" ' 'is about to expire') subject = _(subject_msg.format(membership.group.name)) message = member_template.render(ctx) send_mail(subject, message, settings.FROM_NOREPLY, [membership.userprofile.email]) subject_msg = unicode( '[Mozillians][{0}] Membership of "{1}" is about to expire') format_args = [ membership.group.name, membership.userprofile.full_name ] subject = _(subject_msg.format(*format_args)) # In case the membership was created after an invitation we notify inviters only # Else we fallback to all group curators curators = group.curators.all() inviter = ctx['inviter'] if inviter and curators.filter(pk=inviter.id).exists(): curators = [inviter] for curator in curators: ctx['curator_full_name'] = curator.full_name message = curator_template.render(ctx) send_mail(subject, message, settings.FROM_NOREPLY, [curator.email]) # Mark these memberships ready for an early renewal memberships.update(needs_renewal=True)
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'))