def verify_password_change_token(token, max_age=None): data, valid = verify_data_token(token, max_age=max_age) if valid == Validity.BAD_HASH: try: data = int(data) except: return None, Validity.INVALID_FORMAT user = User.get(data) if not user: return None, Validity.DATA_NOT_FOUND password = user.password.decode('iso-8859-1') if user.password else 'empty' data, valid = verify_data_token(token, password, max_age) return user, valid # Try decoding legacy try: id, hash = token.split('e', 1) id = int(id) user = User.get(id) if not user: return user, Validity.DATA_NOT_FOUND age = datetime.utcnow() - user.last_login if age > timedelta(days=3): return user, Validity.EXPIRED check = str(id) + user.last_login.isoformat()[:19] valid = verify_password( check, hash, HashEncoding.HEX) if not valid: return user, Validity.BAD_HASH return user, Validity.VALID except: return None, Validity.INVALID_FORMAT
def voting_widget_view(request): user_id = authenticated_userid(request) or Everyone ctx = request.context view = (request.matchdict or {}).get('view', None)\ or ctx.get_default_view() or 'default' widget = ctx._instance permissions = get_permissions( user_id, ctx.get_discussion_id()) json = widget.generic_json(view, user_id, permissions) #json['discussion'] = ... if user_id != Everyone: user = User.get(user_id) json['user'] = user.generic_json(view, user_id, permissions) json['user_permissions'] = get_permissions( user_id, widget.get_discussion_id()) user_state = widget.get_user_state(user_id) if user_state is not None: json['user_state'] = user_state target_id = request.GET.get('target', None) if target_id and Idea.get_database_id(target_id): json['user_votes_url'] = widget.get_user_votes_url(target_id) json['voting_urls'] = widget.get_voting_urls(target_id) json['criteria'] = [idea.generic_json(view, user_id, permissions) for idea in widget.criteria] return json
def discussion_list_view(request): user_id = authenticated_userid(request) or Everyone user = None if user_id != Everyone: user = User.get(user_id) roles = get_roles(user_id) context = get_default_context(request) context['discussions'] = [] #Show even anonymous users every discussion one has access to if #authenticated, so they can login and read them discussions = discussions_with_access(Authenticated if user_id == Everyone else user_id) for discussion in discussions: discussionFrontendUrls = FrontendUrls(discussion) discussion_context = { 'topic': discussion.topic, 'slug': discussion.slug, 'url': discussionFrontendUrls.get_discussion_url() } if user_has_permission(discussion.id, user_id, P_ADMIN_DISC): discussion_context['admin_url'] = discussionFrontendUrls.get_discussion_edition_url() discussion_context['permissions_url'] = request.route_url( 'discussion_permissions', discussion_id=discussion.id) context['discussions'].append(discussion_context) if R_SYSADMIN in roles: context['discussions_admin_url'] = request.route_url('discussion_admin') context['permissions_admin_url'] = request.route_url('general_permissions') context['user'] = user return context
def maybe_merge( backend, details, user=None, other_users=None, *args, **kwargs): # If we do not already have a user, see if we're in a situation # where we're adding an account to an existing user, and maybe # even merging request = backend.strategy.request adding_account = request.session.get("add_account", None) if adding_account is not None: del request.session["add_account"] # current discussion and next? logged_in = request.authenticated_userid if logged_in: logged_in = User.get(logged_in) if adding_account: if user and user != logged_in: # logged_in presumably newer? logged_in.merge(user) logged_in.db.delete(user) logged_in.db.flush() user = logged_in else: forget(request) if other_users: if not user: user = other_users.pop(0) # Merge other accounts with same verified email for profile in other_users: user.merge(profile) profile.delete() return {"user": user}
def widget_view(request): # IF_OWNED not applicable for widgets... so far ctx = request.context user_id = authenticated_userid(request) or Everyone permissions = get_permissions( user_id, ctx.get_discussion_id()) check_permissions(ctx, user_id, permissions, CrudPermissions.READ) view = (request.matchdict or {}).get('view', None)\ or ctx.get_default_view() or 'default' json = ctx._instance.generic_json(view, user_id, permissions) # json['discussion'] = ... if user_id != Everyone: user = User.get(user_id) user_state = ctx._instance.get_user_state(user_id) json['user'] = user.generic_json(view, user_id, permissions) json['user_permissions'] = get_permissions( user_id, ctx._instance.get_discussion_id()) if user_state is not None: json['user_state'] = user_state target_id = request.GET.get('target', None) if target_id: idea = Idea.get_instance(target_id) if idea: json['target'] = idea.generic_json(view, user_id, permissions) else: return HTTPNotFound("No idea "+target_id) return json
def finish_password_change(request): logged_in = authenticated_userid(request) if not logged_in: raise HTTPUnauthorized() user = User.get(logged_in) localizer = request.localizer discussion_slug = request.matchdict.get('discussion_slug', None) error = None p1, p2 = (request.params.get('password1', '').strip(), request.params.get('password2', '').strip()) if p1 != p2: error = localizer.translate(_('The passwords are not identical')) elif p1: user.password_p = p1 return HTTPFound(location=request.route_url( 'home' if discussion_slug else 'discussion_list', discussion_slug=discussion_slug, _query=dict( message=localizer.translate(_( "Password changed"))))) slug_prefix = "/" + discussion_slug if discussion_slug else "" return dict( get_default_context(request), slug_prefix=slug_prefix, error=error)
def do_password_change(request): localizer = request.localizer token = request.matchdict.get('ticket') (verified, user_id) = verify_password_change_token(token, 24) if not verified: if not user_id: raise HTTPBadRequest(localizer.translate(_( "Wrong password token."))) else: return HTTPFound(location=maybe_contextual_route( request, 'password_change_sent', profile_id=user_id, _query=dict( sent=True, error=localizer.translate(_( "This token is expired. " "Do you want us to send another?"))))) user = User.get(user_id) headers = remember(request, user_id) request.response.headerlist.extend(headers) user.last_login = datetime.utcnow() slug = request.matchdict.get('discussion_slug', None) slug_prefix = "/" + slug if slug else "" return dict( get_default_context(request), slug_prefix=slug_prefix, title=localizer.translate(_('Change your password')))
def request_password_change(request): localizer = request.localizer identifier = request.params.get('identifier') or '' user_id = request.params.get('user_id') or '' error = request.params.get('error') or '' user = None if user_id: try: user = User.get(int(user_id)) identifier = identifier or user.get_preferred_email() or '' except: error = error or localizer.translate(_("This user cannot be found")) elif identifier: user, account = from_identifier(identifier) if user: user_id = user.id else: error = error or localizer.translate(_("This user cannot be found")) if error or not user: context = get_default_context(request) get_route = context['get_route'] request.session.flash(error) return HTTPFound(location=get_route('react_request_password_change')) discussion_slug = request.matchdict.get('discussion_slug', None) route = 'password_change_sent' if discussion_slug: route = 'contextual_' + route return HTTPFound(location=maybe_contextual_route( request, 'password_change_sent', profile_id=user_id, _query=dict(email=identifier if '@' in identifier else '')))
def discussion_list_view(request): user_id = authenticated_userid(request) or Everyone user = None if user_id != Everyone: user = User.get(id=user_id) context = get_default_context() context['discussions'] = discussions_with_access(user_id) context['user'] = user return context
def add_local_role(request): # Do not use check_permissions, this is a special case ctx = request.context user_id = request.authenticated_userid if not user_id: raise HTTPUnauthorized() discussion_id = ctx.get_discussion_id() discussion = Discussion.get(discussion_id) user_uri = User.uri_generic(user_id) if discussion_id is None: raise HTTPBadRequest() permissions = get_permissions(user_id, discussion_id) json = request.json_body if "discussion" not in json: json["discussion"] = Discussion.uri_generic(discussion_id) requested_user = json.get('user', None) if not requested_user: json['user'] = requested_user = user_uri elif requested_user != user_uri and P_ADMIN_DISC not in permissions: raise HTTPUnauthorized() if P_ADMIN_DISC not in permissions: if P_SELF_REGISTER in permissions: json['requested'] = False json['role'] = R_PARTICIPANT req_user = User.get_instance(requested_user) if not discussion.check_authorized_email(req_user): raise HTTPForbidden() elif P_SELF_REGISTER_REQUEST in permissions: json['requested'] = True else: raise HTTPUnauthorized() try: instances = ctx.create_object("LocalUserRole", json, user_id) except HTTPClientError as e: raise e except Exception as e: raise HTTPBadRequest(e) if instances: first = instances[0] db = first.db for instance in instances: db.add(instance) db.flush() # Side effect: materialize subscriptions. if not first.requested: # relationship may not be initialized user = first.user or User.get(first.user_id) user.get_notification_subscriptions(discussion_id, True) # Update the user's AgentStatusInDiscussion user.update_agent_status_subscribe(discussion) view = request.GET.get('view', None) or 'default' permissions = get_permissions( user_id, ctx.get_discussion_id()) return CreationResponse(first, user_id, permissions, view)
def add_local_role(request): # Do not use check_permissions, this is a special case ctx = request.context user_id = authenticated_userid(request) if user_id == Everyone: raise HTTPUnauthorized() discussion_id = ctx.get_discussion_id() user_uri = User.uri_generic(user_id) if discussion_id is None: raise HTTPBadRequest() permissions = get_permissions(user_id, discussion_id) json = request.json_body if "discussion" not in json: json["discussion"] = Discussion.uri_generic(discussion_id) requested_user = json.get('user', None) if not requested_user: json['user'] = requested_user = user_uri elif requested_user != user_uri and P_ADMIN_DISC not in permissions: raise HTTPUnauthorized() if P_ADMIN_DISC not in permissions: if P_SELF_REGISTER in permissions: json['requested'] = False json['role'] = R_PARTICIPANT elif P_SELF_REGISTER_REQUEST in permissions: json['requested'] = True else: raise HTTPUnauthorized() try: instances = ctx.create_object("LocalUserRole", json, user_id) except HTTPClientError as e: raise e except Exception as e: raise HTTPBadRequest(e) if instances: first = instances[0] db = first.db() for instance in instances: db.add(instance) db.flush() # Side effect: materialize subscriptions. if not first.requested: # relationship may not be initialized user = first.user or User.get(first.user_id) user.get_notification_subscriptions(discussion_id, True) view = request.GET.get('view', None) or 'default' permissions = get_permissions( user_id, ctx.get_discussion_id()) return Response( dumps(first.generic_json(view, user_id, permissions)), location=first.uri_generic(first.id), status_code=201)
def delete_local_role(request): ctx = request.context instance = ctx._instance user_id = request.authenticated_userid if not user_id: raise HTTPUnauthorized() discussion_id = ctx.get_discussion_id() if discussion_id is None: raise HTTPBadRequest() permissions = get_permissions(user_id, discussion_id) requested_user = instance.user if requested_user.id != user_id and P_ADMIN_DISC not in permissions: raise HTTPUnauthorized() user = User.get(user_id) discussion = Discussion.get(discussion_id) instance.db.delete(instance) # Update the user's AgentStatusInDiscussion user.update_agent_status_unsubscribe(discussion) instance.db.flush() # maybe unnecessary return {}
def set_local_role(request): # Do not use check_permissions, this is a special case ctx = request.context instance = ctx._instance user_id = request.authenticated_userid if not user_id: raise HTTPUnauthorized() discussion_id = ctx.get_discussion_id() user_uri = User.uri_generic(user_id) if discussion_id is None: raise HTTPBadRequest() permissions = get_permissions(user_id, discussion_id) json = request.json_body requested_user = json.get('user', None) if not requested_user: json['user'] = requested_user = user_uri elif requested_user != user_uri and P_ADMIN_DISC not in permissions: raise HTTPUnauthorized() if P_ADMIN_DISC not in permissions: if P_SELF_REGISTER in permissions: json['requested'] = False json['role'] = R_PARTICIPANT elif P_SELF_REGISTER_REQUEST in permissions: json['requested'] = True else: raise HTTPUnauthorized() updated = instance.update_from_json(json, user_id, ctx) view = request.GET.get('view', None) or 'default' # Update the user's AgentStatusInDiscussion user = User.get(user_id) discussion = Discussion.get(discussion_id) user.update_agent_status_subscribe(discussion) if view == 'id_only': return [updated.uri()] else: return updated.generic_json(view, user_id, permissions)
def set_local_role(request): # Do not use check_permissions, this is a special case ctx = request.context instance = ctx._instance user_id = authenticated_userid(request) if not user_id: raise HTTPUnauthorized() discussion_id = ctx.get_discussion_id() user_uri = User.uri_generic(user_id) if discussion_id is None: raise HTTPBadRequest() permissions = ctx.get_permissions() json = request.json_body requested_user = json.get('user', None) if not requested_user: json['user'] = requested_user = user_uri elif requested_user != user_uri and P_ADMIN_DISC not in permissions: raise HTTPUnauthorized() if P_ADMIN_DISC not in permissions: if P_SELF_REGISTER in permissions: json['requested'] = False json['role'] = R_PARTICIPANT elif P_SELF_REGISTER_REQUEST in permissions: json['requested'] = True else: raise HTTPUnauthorized() updated = instance.update_from_json(json, user_id, ctx) view = request.GET.get('view', None) or 'default' # Update the user's AgentStatusInDiscussion user = User.get(user_id) discussion = Discussion.get(discussion_id) user.update_agent_status_subscribe(discussion) if view == 'id_only': return [updated.uri()] else: return updated.generic_json(view, user_id, permissions)
def maybe_social_logout(request): """If the user has a an account with the default social provider, and that account has a logout URL, redirect there. Maybe the next argument should be carried?""" discussion = discussion_from_request(request) if not discussion: return backend_name = discussion.preferences['authorization_server_backend'] if not backend_name: return user_id = request.authenticated_userid if not user_id: return user = User.get(user_id) for account in user.accounts: if getattr(account, 'provider_with_idp', None) == backend_name: break else: return # TODO: tell the account that the login has expired. # Also check if already expired? return config.get('SOCIAL_AUTH_%s_LOGOUT_URL' % (account.provider.upper(), ))
def maybe_social_logout(request): """If the user has a an account with the default social provider, and that account has a logout URL, redirect there. Maybe the next argument should be carried?""" discussion = discussion_from_request(request) if not discussion: return backend_name = discussion.preferences['authorization_server_backend'] if not backend_name: return user_id = request.authenticated_userid if not user_id: return user = User.get(user_id) for account in user.accounts: if getattr(account, 'provider_with_idp', None) == backend_name: break else: return # TODO: tell the account that the login has expired. # Also check if already expired? return config.get('SOCIAL_AUTH_%s_LOGOUT_URL' % ( account.provider.upper(),))
def finish_password_change(request): logged_in = authenticated_userid(request) if not logged_in: raise HTTPUnauthorized() user = User.get(logged_in) localizer = request.localizer discussion_slug = request.matchdict.get('discussion_slug', None) error = None p1, p2 = (request.params.get('password1', '').strip(), request.params.get('password2', '').strip()) if p1 != p2: error = localizer.translate(_('The passwords are not identical')) elif p1: user.password_p = p1 return HTTPFound(location=request.route_url( 'home' if discussion_slug else 'discussion_list', discussion_slug=discussion_slug, _query=dict(message=localizer.translate(_("Password changed"))))) slug_prefix = "/" + discussion_slug if discussion_slug else "" return dict(get_default_context(request), slug_prefix=slug_prefix, error=error)
def maybe_merge(backend, details, user=None, other_users=None, *args, **kwargs): # If we do not already have a user, see if we're in a situation # where we're adding an account to an existing user, and maybe # even merging request = backend.strategy.request adding_account = request.session.get("add_account", None) if adding_account is not None: del request.session["add_account"] # current discussion and next? logged_in = authenticated_userid(request) if logged_in: logged_in = User.get(logged_in) if adding_account: if user and user != logged_in: # logged_in presumably newer? logged_in.merge(user) logged_in.db.delete(user) logged_in.db.flush() user = logged_in else: forget(request) user = None logged_in = None if other_users: if not user: user = other_users.pop(0) # Merge other accounts with same verified email for profile in other_users: user.merge(profile) profile.delete() return {"user": user}
def request_password_change(request): localizer = request.localizer identifier = request.params.get('identifier') or '' user_id = request.params.get('user_id') or '' error = request.params.get('error') or '' user = None if user_id: try: user = User.get(int(user_id)) identifier = identifier or user.get_preferred_email() or '' except: error = error or localizer.translate(_("This user cannot be found")) elif identifier: user, account = from_identifier(identifier) if user: user_id = user.id else: error = error or localizer.translate(_("This user cannot be found")) if error or not user: slug = request.matchdict.get('discussion_slug', None) return dict( get_default_context(request), error=error, user_id=user_id, identifier=identifier, slug_prefix="/" + slug if slug else "", title=localizer.translate(_('I forgot my password'))) discussion_slug = request.matchdict.get('discussion_slug', None) route = 'password_change_sent' if discussion_slug: route = 'contextual_' + route return HTTPFound(location=maybe_contextual_route( request, 'password_change_sent', profile_id=user_id, _query=dict(email=identifier if '@' in identifier else '')))
def discussion_list_view(request): request.session.pop('discussion') user_id = request.authenticated_userid or Everyone user = None if user_id != Everyone: user = User.get(user_id) roles = get_roles(user_id) context = get_default_context(request) context['discussions'] = [] # Show even anonymous users every discussion one has access to if # authenticated, so they can login and read them discussions = discussions_with_access(Authenticated if user_id == Everyone else user_id) for discussion in discussions: discussionFrontendUrls = FrontendUrls(discussion) discussion_context = { 'topic': discussion.topic, 'slug': discussion.slug, 'url': discussionFrontendUrls.get_discussion_url() } if user_has_permission(discussion.id, user_id, P_ADMIN_DISC): discussion_context[ 'admin_url'] = discussionFrontendUrls.get_discussion_edition_url( ) discussion_context['permissions_url'] = request.route_url( 'discussion_permissions', discussion_id=discussion.id) context['discussions'].append(discussion_context) if R_SYSADMIN in roles: context['discussions_admin_url'] = request.route_url( 'discussion_admin') context['permissions_admin_url'] = request.route_url( 'general_permissions') context['preferences_admin_url'] = request.route_url( 'admin_global_preferences') context['user'] = user return context
def request_password_change(request): localizer = request.localizer identifier = request.params.get('identifier') or '' user_id = request.params.get('user_id') or '' error = request.params.get('error') or '' user = None if user_id: try: user = User.get(int(user_id)) identifier = identifier or user.get_preferred_email() or '' except: error = error or localizer.translate( _("This user cannot be found")) elif identifier: user, account = from_identifier(identifier) if user: user_id = user.id else: error = error or localizer.translate( _("This user cannot be found")) if error or not user: context = get_default_context(request) get_route = context['get_route'] request.session.flash(error) return HTTPFound(location=get_route('react_request_password_change')) discussion_slug = request.matchdict.get('discussion_slug', None) route = 'password_change_sent' if discussion_slug: route = 'contextual_' + route return HTTPFound(location=maybe_contextual_route( request, 'password_change_sent', profile_id=user_id, _query=dict(email=identifier if '@' in identifier else '')))
def request_password_change(request): localizer = request.localizer identifier = request.params.get('identifier') or '' user_id = request.params.get('user_id') or '' error = request.params.get('error') or '' user = None if user_id: try: user = User.get(int(user_id)) identifier = identifier or user.get_preferred_email() or '' except: error = error or localizer.translate(_("This user cannot be found")) elif identifier: user, account = from_identifier(identifier) if user: user_id = user.id else: error = error or localizer.translate(_("This user cannot be found")) if error or not user: slug = request.matchdict.get('discussion_slug', None) return dict( get_default_context(request), error=error, user_id=user_id, identifier=identifier, slug_prefix="/" + slug if slug else "", title=localizer.translate(_('I forgot my password'))) discussion_slug = request.matchdict.get('discussion_slug', None) route = 'password_change_sent' if discussion_slug: route = 'contextual_' + route return HTTPFound(location=maybe_contextual_route( request, 'password_change_sent', profile_id=user_id, _query=dict(email=identifier if '@' in identifier else '')))
def velruse_login_complete_view(request): # TODO: Write tests. Situations are a combinatorics of the folloing: # 1. User action: # A. Social login # B. Logged in, add social account # C. Logged in, add email account (does not happen here) # 2. Is there an existing account with that email? # A. No. # B. The social account already exists # C. A valid email account # D. An invalid email account, sole account of profile # E. An invalid email account, but profile has other accounts # F. A social account from a different provider # 3. When we're logged in (1B, 1C), is the existing account # on the logged in profile or another? # 4. If gmail account, is it an enterprise account? session = AgentProfile.default_db context = request.context now = datetime.utcnow() velruse_profile = context.profile discussion = None slug = request.session.get('discussion', None) if not slug: discussion = discussion_from_request(request) if discussion: slug = discussion.slug if slug and not discussion: discussion = session.query(Discussion).filter_by( slug=slug).first() next_view = handle_next_view(request, True) logged_in = authenticated_userid(request) if logged_in: logged_in = User.get(logged_in) base_profile = logged_in provider = get_identity_provider(request) # find or create IDP_Accounts idp_accounts = [] new_idp_account = None velruse_accounts = velruse_profile['accounts'] old_autoflush = session.autoflush # sqla mislikes creating accounts before profiles, so delay session.autoflush = False # Search for this social account for velruse_account in velruse_accounts: if 'userid' in velruse_account: idp_accounts.extend(session.query( IdentityProviderAccount).filter_by( provider=provider, domain=velruse_account['domain'], userid=velruse_account['userid']).all()) elif 'username' in velruse_account: idp_accounts.extend(session.query( IdentityProviderAccount).filter_by( provider=provider, domain=velruse_account['domain'], username=velruse_account['username']).all()) else: log.error("account needs username or email" + velruse_account) raise HTTPServerError("account needs username or userid") trusted_emails = set() if idp_accounts: for idp_account in idp_accounts: idp_account.profile_info_json = velruse_profile if len(idp_accounts) > 1: log.warn("multiple idp_accounts:" + ','.join((a.id for a in idp_accounts)) + " for " + velruse_accounts) # We will just the last one from the loop for now. trusted_emails.update([ a.email for a in idp_accounts if a.provider.trust_emails and a.email]) else: # Create it if not found idp_class = IdentityProviderAccount for cls in idp_class.get_subclasses(): if cls == idp_class: continue if cls.account_provider_name == provider.name: idp_class = cls break idp_account = idp_class( provider=provider, profile_info_json=velruse_profile, domain=velruse_account.get('domain'), userid=velruse_account.get('userid'), verified=True, username=velruse_account.get('username')) idp_accounts.append(idp_account) new_idp_account = base_account = idp_account session.add(idp_account) for account in idp_accounts: if account.provider.trust_emails: profile = (velruse_profile if account == new_idp_account else account.profile_info_json) email = profile.get('verifiedEmail', None) if email: if account == new_idp_account: account.email = email trusted_emails.add(email) for email in profile.get('emails', ()): if isinstance(email, dict): email = email.get('value', None) if isinstance(email, (str, unicode)) and email: trusted_emails.add(email) if account == new_idp_account and not account.email: account.email = email # else we have created an email-less account. Treat accordingly. conflicting_profiles = {a.profile for a in idp_accounts if a.profile} # Are there other accounts/profiles than the ones we know? conflicting_accounts = set() for email in trusted_emails: other_accounts = session.query(AbstractAgentAccount).filter_by( email=email) for account in other_accounts: conflicting_accounts.add(account) if account.verified or len(account.profile.accounts) == 1: conflicting_profiles.add(account.profile) # choose best known profile for base_account # prefer profiles with verified users, then users, then oldest profiles profile_list = list(conflicting_profiles) profile_list.sort(key=lambda p: ( not(isinstance(p, User) and p.verified), not isinstance(p, User), p.id)) if new_idp_account and profile_list: base_profile = profile_list[0] elif not new_idp_account: # Take first appropriate. Should be the first, somehow not. for profile in profile_list: accounts = [ a for a in idp_accounts if a.profile == profile] if accounts: base_account = accounts[0] break if not logged_in: base_profile = base_account.profile new_profile = None if new_idp_account: if not base_profile: # Create a new user base_profile = new_profile = User( name=velruse_profile.get('displayName', ''), verified=True, creation_date=now) session.add(new_profile) # Upgrade a AgentProfile if not isinstance(base_profile, User): base_profile = base_profile.change_class( User, None, verified=True) new_idp_account.profile = base_profile base_profile.last_login = now base_profile.verified = True if not base_profile.name: base_profile.name = velruse_profile.get('displayName', None) # TODO (needs parsing) # base_profile.timezone=velruse_profile['utcOffset'] # Now all accounts have a profile session.autoflush = old_autoflush session.flush() # Merge other profiles with the same (validated) email # TODO: Ask the user about doing this. conflicting_profiles.discard(base_profile) conflicting_accounts.discard(base_account) for conflicting_profile in conflicting_profiles: base_profile.merge(conflicting_profile) session.delete(conflicting_profile) # If base_profile is still an AgentProfile at this point # then it needs to be upgraded to a User if not isinstance(base_profile, User): base_profile = base_profile.change_class( User, None, verified=True, last_login=now) # Set username if not base_profile.username: username = None usernames = set((a['preferredUsername'] for a in velruse_accounts if 'preferredUsername' in a)) for u in usernames: if not session.query(Username).filter_by(username=u).count(): username = u break if username: session.add(Username(username=username, user=base_profile)) # Create AgentStatusInDiscussion if new_profile and discussion: agent_status = AgentStatusInDiscussion( agent_profile=base_profile, discussion=discussion, user_created_on_this_discussion=True) session.add(agent_status) if maybe_auto_subscribe(base_profile, discussion): next_view = "/%s/" % (slug,) # Delete other (email) accounts if base_account.provider.trust_emails: for account in conflicting_accounts: # Merge may have been confusing session.expire(account) account = AbstractAgentAccount.get(account.id) if account.profile == base_profile: if account.email == base_account.email: if isinstance(account, EmailAccount): account.delete() if account.verified and account.preferred: base_account.preferred = True elif isinstance(account, IdentityProviderAccount): if account.provider_id == base_account.provider_id: log.error("This should have been caught earlier") account.delete() else: log.warning("Two accounts with same email," + "different provider: %d, %d" % ( account.id, base_account.id)) else: # If they're verified, they should have been merged. if account.verified: log.error("account %d should not exist: " % (account.id,)) else: other_profile = account.profile account.delete() session.flush() session.expire(other_profile, ["accounts"]) if not len(other_profile.accounts): log.warning("deleting profile %d" % other_profile.id) other_profile.delete() session.expire(base_profile, ['accounts', 'email_accounts']) # create an email account for other emails. known_emails = {a.email for a in base_profile.accounts} for email in trusted_emails: if email not in known_emails: email = EmailAccount( email=email, profile=base_profile, verified=base_account.provider.trust_emails ) session.add(email) session.flush() base_profile.last_login = now headers = remember(request, base_profile.id) request.response.headerlist.extend(headers) if discussion: request.session['discussion'] = discussion.slug return HTTPFound(location=next_view)
def velruse_login_complete_view(request): # TODO: Write tests. Situations are a combinatorics of the folloing: # 1. User action: # A. Social login # B. Logged in, add social account # C. Logged in, add email account (does not happen here) # 2. Is there an existing account with that email? # A. No. # B. The social account already exists # C. A valid email account # D. An invalid email account, sole account of profile # E. An invalid email account, but profile has other accounts # F. A social account from a different provider # 3. When we're logged in (1B, 1C), is the existing account # on the logged in profile or another? # 4. If gmail account, is it an enterprise account? session = AgentProfile.default_db context = request.context now = datetime.utcnow() velruse_profile = context.profile discussion = None slug = request.session.get('discussion', None) if not slug: discussion = discussion_from_request(request) if discussion: slug = discussion.slug if slug and not discussion: discussion = session.query(Discussion).filter_by(slug=slug).first() next_view = handle_next_view(request, True) logged_in = authenticated_userid(request) if logged_in: logged_in = User.get(logged_in) base_profile = logged_in provider = get_identity_provider(request) # find or create IDP_Accounts idp_accounts = [] new_idp_account = None velruse_accounts = velruse_profile['accounts'] old_autoflush = session.autoflush # sqla mislikes creating accounts before profiles, so delay session.autoflush = False # Search for this social account for velruse_account in velruse_accounts: if 'userid' in velruse_account: idp_accounts.extend( session.query(IdentityProviderAccount).filter_by( provider=provider, domain=velruse_account['domain'], userid=velruse_account['userid']).all()) elif 'username' in velruse_account: idp_accounts.extend( session.query(IdentityProviderAccount).filter_by( provider=provider, domain=velruse_account['domain'], username=velruse_account['username']).all()) else: log.error("account needs username or email" + velruse_account) raise HTTPServerError("account needs username or userid") trusted_emails = set() if idp_accounts: for idp_account in idp_accounts: idp_account.profile_info_json = velruse_profile if len(idp_accounts) > 1: log.warn("multiple idp_accounts:" + ','.join((a.id for a in idp_accounts)) + " for " + velruse_accounts) # We will just the last one from the loop for now. trusted_emails.update([ a.email for a in idp_accounts if a.provider.trust_emails and a.email ]) else: # Create it if not found idp_class = IdentityProviderAccount for cls in idp_class.get_subclasses(): if cls == idp_class: continue if cls.account_provider_name == provider.name: idp_class = cls break idp_account = idp_class(provider=provider, profile_info_json=velruse_profile, domain=velruse_account.get('domain'), userid=velruse_account.get('userid'), verified=True, username=velruse_account.get('username')) idp_accounts.append(idp_account) new_idp_account = base_account = idp_account session.add(idp_account) for account in idp_accounts: if account.provider.trust_emails: profile = (velruse_profile if account == new_idp_account else account.profile_info_json) email = profile.get('verifiedEmail', None) if email: if account == new_idp_account: account.email = email trusted_emails.add(email) for email in profile.get('emails', ()): if isinstance(email, dict): email = email.get('value', None) if isinstance(email, (str, unicode)) and email: trusted_emails.add(email) if account == new_idp_account and not account.email: account.email = email # else we have created an email-less account. Treat accordingly. conflicting_profiles = {a.profile for a in idp_accounts if a.profile} # Are there other accounts/profiles than the ones we know? conflicting_accounts = set() for email in trusted_emails: other_accounts = session.query(AbstractAgentAccount).filter_by( email=email) for account in other_accounts: conflicting_accounts.add(account) if account.verified or len(account.profile.accounts) == 1: conflicting_profiles.add(account.profile) # choose best known profile for base_account # prefer profiles with verified users, then users, then oldest profiles profile_list = list(conflicting_profiles) profile_list.sort(key=lambda p: (not (isinstance(p, User) and p.verified), not isinstance(p, User), p.id)) if new_idp_account and profile_list: base_profile = profile_list[0] elif not new_idp_account: # Take first appropriate. Should be the first, somehow not. for profile in profile_list: accounts = [a for a in idp_accounts if a.profile == profile] if accounts: base_account = accounts[0] break if not logged_in: base_profile = base_account.profile new_profile = None if new_idp_account: if not base_profile: # Create a new user base_profile = new_profile = User(name=velruse_profile.get( 'displayName', ''), verified=True, creation_date=now) session.add(new_profile) # Upgrade a AgentProfile if not isinstance(base_profile, User): base_profile = base_profile.change_class(User, None, verified=True) new_idp_account.profile = base_profile base_profile.last_login = now base_profile.verified = True if not base_profile.name: base_profile.name = velruse_profile.get('displayName', None) # TODO (needs parsing) # base_profile.timezone=velruse_profile['utcOffset'] # Now all accounts have a profile session.autoflush = old_autoflush session.flush() # Merge other profiles with the same (validated) email # TODO: Ask the user about doing this. conflicting_profiles.discard(base_profile) conflicting_accounts.discard(base_account) for conflicting_profile in conflicting_profiles: base_profile.merge(conflicting_profile) session.delete(conflicting_profile) # If base_profile is still an AgentProfile at this point # then it needs to be upgraded to a User if not isinstance(base_profile, User): base_profile = base_profile.change_class(User, None, verified=True, last_login=now) # Set username if not base_profile.username: username = None usernames = set((a['preferredUsername'] for a in velruse_accounts if 'preferredUsername' in a)) for u in usernames: if not session.query(Username).filter_by(username=u).count(): username = u break if username: session.add(Username(username=username, user=base_profile)) # Create AgentStatusInDiscussion if new_profile and discussion: agent_status = AgentStatusInDiscussion( agent_profile=base_profile, discussion=discussion, user_created_on_this_discussion=True) session.add(agent_status) if maybe_auto_subscribe(base_profile, discussion): next_view = "/%s/" % (slug, ) # Delete other (email) accounts if base_account.provider.trust_emails: for account in conflicting_accounts: # Merge may have been confusing session.expire(account) account = AbstractAgentAccount.get(account.id) if account.profile == base_profile: if account.email == base_account.email: if isinstance(account, EmailAccount): account.delete() if account.verified and account.preferred: base_account.preferred = True elif isinstance(account, IdentityProviderAccount): if account.provider_id == base_account.provider_id: log.error("This should have been caught earlier") account.delete() else: log.warning("Two accounts with same email," + "different provider: %d, %d" % (account.id, base_account.id)) else: # If they're verified, they should have been merged. if account.verified: log.error("account %d should not exist: " % (account.id, )) else: other_profile = account.profile account.delete() session.flush() session.expire(other_profile, ["accounts"]) if not len(other_profile.accounts): log.warning("deleting profile %d" % other_profile.id) other_profile.delete() session.expire(base_profile, ['accounts', 'email_accounts']) # create an email account for other emails. known_emails = {a.email for a in base_profile.accounts} for email in trusted_emails: if email not in known_emails: email = EmailAccount(email=email, profile=base_profile, verified=base_account.provider.trust_emails) session.add(email) session.flush() base_profile.last_login = now headers = remember(request, base_profile.id) request.response.headerlist.extend(headers) if discussion: request.session['discussion'] = discussion.slug return HTTPFound(location=next_view)