# -*- coding: utf-8 -*- # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import colander import datetime import json import logging import uuid import pyramid.security as security import appenlight.lib.helpers as h from authomatic.adapters import WebObAdapter from pyramid.view import view_config from pyramid.httpexceptions import HTTPFound, HTTPUnprocessableEntity from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest from pyramid.security import NO_PERMISSION_REQUIRED from ziggurat_foundations.models.services.external_identity import ( ExternalIdentityService, ) from ziggurat_foundations.models.services.user import UserService from appenlight.lib import generate_random_string from appenlight.lib.social import handle_social_data from appenlight.lib.utils import ( channelstream_request, add_cors_headers, permission_tuple_to_dict, ) from appenlight.models import DBSession from appenlight.models.alert_channels.email import EmailAlertChannel from appenlight.models.alert_channel_action import AlertChannelAction from appenlight.models.services.alert_channel import AlertChannelService from appenlight.models.services.alert_channel_action import AlertChannelActionService from appenlight.models.auth_token import AuthToken from appenlight.models.report import REPORT_TYPE_MATRIX from appenlight.models.user import User from appenlight.models.services.user import UserService from appenlight.subscribers import _ from appenlight.validators import build_rule_schema from appenlight import forms from webob.multidict import MultiDict log = logging.getLogger(__name__) @view_config( route_name="users_no_id", renderer="json", request_method="GET", permission="root_administration", ) def users_list(request): """ Returns users list """ props = [ "user_name", "id", "first_name", "last_name", "email", "last_login_date", "status", ] users = UserService.all() users_dicts = [] for user in users: u_dict = user.get_dict(include_keys=props) u_dict["gravatar_url"] = UserService.gravatar_url(user, s=20) users_dicts.append(u_dict) return users_dicts @view_config( route_name="users_no_id", renderer="json", request_method="POST", permission="root_administration", ) def users_create(request): """ Returns users list """ form = forms.UserCreateForm( MultiDict(request.safe_json_body or {}), csrf_context=request ) if form.validate(): log.info("registering user") # probably not needed in the future since this requires root anyways # lets keep this here in case we lower view permission in the future # if request.registry.settings['appenlight.disable_registration']: # return HTTPUnprocessableEntity(body={'error': 'Registration is currently disabled.'}) user = User() # insert new user here DBSession.add(user) form.populate_obj(user) UserService.regenerate_security_code(user) UserService.set_password(user, user.user_password) user.status = 1 if form.status.data else 0 request.session.flash(_("User created")) DBSession.flush() return user.get_dict( exclude_keys=[ "security_code_date", "notes", "security_code", "user_password", ] ) else: return HTTPUnprocessableEntity(body=form.errors_json) @view_config( route_name="users", renderer="json", request_method="GET", permission="root_administration", ) @view_config( route_name="users", renderer="json", request_method="PATCH", permission="root_administration", ) def users_update(request): """ Updates user object """ user = UserService.by_id(request.matchdict.get("user_id")) if not user: return HTTPNotFound() post_data = request.safe_json_body or {} if request.method == "PATCH": form = forms.UserUpdateForm(MultiDict(post_data), csrf_context=request) if form.validate(): form.populate_obj(user, ignore_none=True) if form.user_password.data: UserService.set_password(user, user.user_password) if form.status.data: user.status = 1 else: user.status = 0 else: return HTTPUnprocessableEntity(body=form.errors_json) return user.get_dict( exclude_keys=["security_code_date", "notes", "security_code", "user_password"] ) @view_config( route_name="users_property", match_param="key=resource_permissions", renderer="json", permission="authenticated", ) def users_resource_permissions_list(request): """ Get list of permissions assigned to specific resources """ user = UserService.by_id(request.matchdict.get("user_id")) if not user: return HTTPNotFound() return [ permission_tuple_to_dict(perm) for perm in UserService.resources_with_possible_perms(user) ] @view_config( route_name="users", renderer="json", request_method="DELETE", permission="root_administration", ) def users_DELETE(request): """ Removes a user permanently from db - makes a check to see if after the operation there will be at least one admin left """ msg = _("There needs to be at least one administrator in the system") user = UserService.by_id(request.matchdict.get("user_id")) if user: users = UserService.users_for_perms(["root_administration"]).all() if len(users) < 2 and user.id == users[0].id: request.session.flash(msg, "warning") else: DBSession.delete(user) request.session.flash(_("User removed")) return True request.response.status = 422 return False @view_config( route_name="users_self", renderer="json", request_method="GET", permission="authenticated", ) @view_config( route_name="users_self", renderer="json", request_method="PATCH", permission="authenticated", ) def users_self(request): """ Updates user personal information """ if request.method == "PATCH": form = forms.gen_user_profile_form()( MultiDict(request.unsafe_json_body), csrf_context=request ) if form.validate(): form.populate_obj(request.user) request.session.flash(_("Your profile got updated.")) else: return HTTPUnprocessableEntity(body=form.errors_json) return request.user.get_dict( exclude_keys=["security_code_date", "notes", "security_code", "user_password"], extended_info=True, ) @view_config( route_name="users_self_property", match_param="key=external_identities", renderer="json", request_method="GET", permission="authenticated", ) def users_external_identies(request): user = request.user identities = [ {"provider": ident.provider_name, "id": ident.external_user_name} for ident in user.external_identities.all() ] return identities @view_config( route_name="users_self_property", match_param="key=external_identities", renderer="json", request_method="DELETE", permission="authenticated", ) def users_external_identies_DELETE(request): """ Unbinds external identities(google,twitter etc.) from user account """ user = request.user for identity in user.external_identities.all(): log.info("found identity %s" % identity) if identity.provider_name == request.params.get( "provider" ) and identity.external_user_name == request.params.get("id"): log.info("remove identity %s" % identity) DBSession.delete(identity) return True return False @view_config( route_name="users_self_property", match_param="key=password", renderer="json", request_method="PATCH", permission="authenticated", ) def users_password(request): """ Sets new password for user account """ user = request.user form = forms.ChangePasswordForm( MultiDict(request.unsafe_json_body), csrf_context=request ) form.old_password.user = user if form.validate(): UserService.regenerate_security_code(user) UserService.set_password(user, form.new_password.data) msg = ( "Your password got updated. " "Next time log in with your new credentials." ) request.session.flash(_(msg)) return True else: return HTTPUnprocessableEntity(body=form.errors_json) return False @view_config( route_name="users_self_property", match_param="key=websocket", renderer="json", permission="authenticated", ) def users_websocket(request): """ Handle authorization of users trying to connect """ # handle preflight request user = request.user if request.method == "OPTIONS": res = request.response.body("OK") add_cors_headers(res) return res applications = UserService.resources_with_perms( user, ["view"], resource_types=["application"] ) channels = ["app_%s" % app.resource_id for app in applications] payload = { "username": user.user_name, "conn_id": str(uuid.uuid4()), "channels": channels, } settings = request.registry.settings response = channelstream_request( settings["cometd.secret"], "/connect", payload, servers=[request.registry.settings["cometd_servers"]], throw_exceptions=True, ) return payload @view_config( route_name="users_self_property", request_method="GET", match_param="key=alert_channels", renderer="json", permission="authenticated", ) def alert_channels(request): """ Lists all available alert channels """ user = request.user return [c.get_dict(extended_info=True) for c in user.alert_channels] @view_config( route_name="users_self_property", match_param="key=alert_actions", request_method="GET", renderer="json", permission="authenticated", ) def alert_actions(request): """ Lists all available alert channels """ user = request.user return [r.get_dict(extended_info=True) for r in user.alert_actions] @view_config( route_name="users_self_property", renderer="json", match_param="key=alert_channels_rules", request_method="POST", permission="authenticated", ) def alert_channels_rule_POST(request): """ Creates new notification rule for specific alert channel """ user = request.user alert_action = AlertChannelAction(owner_id=request.user.id, type="report") DBSession.add(alert_action) DBSession.flush() return alert_action.get_dict() @view_config( route_name="users_self_property", permission="authenticated", match_param="key=alert_channels_rules", renderer="json", request_method="DELETE", ) def alert_channels_rule_DELETE(request): """ Removes specific alert channel rule """ user = request.user rule_action = AlertChannelActionService.by_owner_id_and_pkey( user.id, request.GET.get("pkey") ) if rule_action: DBSession.delete(rule_action) return True return HTTPNotFound() @view_config( route_name="users_self_property", permission="authenticated", match_param="key=alert_channels_rules", renderer="json", request_method="PATCH", ) def alert_channels_rule_PATCH(request): """ Removes specific alert channel rule """ user = request.user json_body = request.unsafe_json_body schema = build_rule_schema(json_body["rule"], REPORT_TYPE_MATRIX) try: schema.deserialize(json_body["rule"]) except colander.Invalid as exc: return HTTPUnprocessableEntity(body=json.dumps(exc.asdict())) rule_action = AlertChannelActionService.by_owner_id_and_pkey( user.id, request.GET.get("pkey") ) if rule_action: rule_action.rule = json_body["rule"] rule_action.resource_id = json_body["resource_id"] rule_action.action = json_body["action"] return rule_action.get_dict() return HTTPNotFound() @view_config( route_name="users_self_property", permission="authenticated", match_param="key=alert_channels", renderer="json", request_method="PATCH", ) def alert_channels_PATCH(request): user = request.user channel_name = request.GET.get("channel_name") channel_value = request.GET.get("channel_value") # iterate over channels channel = None for channel in user.alert_channels: if ( channel.channel_name == channel_name and channel.channel_value == channel_value ): break if not channel: return HTTPNotFound() allowed_keys = ["daily_digest", "send_alerts"] for k, v in request.unsafe_json_body.items(): if k in allowed_keys: setattr(channel, k, v) else: return HTTPBadRequest() return channel.get_dict() @view_config( route_name="users_self_property", permission="authenticated", match_param="key=alert_channels", request_method="POST", renderer="json", ) def alert_channels_POST(request): """ Creates a new email alert channel for user, sends a validation email """ user = request.user form = forms.EmailChannelCreateForm( MultiDict(request.unsafe_json_body), csrf_context=request ) if not form.validate(): return HTTPUnprocessableEntity(body=form.errors_json) email = form.email.data.strip() channel = EmailAlertChannel() channel.channel_name = "email" channel.channel_value = email security_code = generate_random_string(10) channel.channel_json_conf = {"security_code": security_code} user.alert_channels.append(channel) email_vars = { "user": user, "email": email, "request": request, "security_code": security_code, "email_title": "AppEnlight :: " "Please authorize your email", } UserService.send_email( request, recipients=[email], variables=email_vars, template="/email_templates/authorize_email.jinja2", ) request.session.flash(_("Your alert channel was " "added to the system.")) request.session.flash( _( "You need to authorize your email channel, a message was " "sent containing necessary information." ), "warning", ) DBSession.flush() channel.get_dict() @view_config( route_name="section_view", match_param=["section=user_section", "view=alert_channels_authorize"], renderer="string", permission="authenticated", ) def alert_channels_authorize(request): """ Performs alert channel authorization based on auth code sent in email """ user = request.user for channel in user.alert_channels: security_code = request.params.get("security_code", "") if channel.channel_json_conf["security_code"] == security_code: channel.channel_validated = True request.session.flash(_("Your email was authorized.")) return HTTPFound(location=request.route_url("/")) @view_config( route_name="users_self_property", request_method="DELETE", match_param="key=alert_channels", renderer="json", permission="authenticated", ) def alert_channel_DELETE(request): """ Removes alert channel from users channel """ user = request.user channel = None for chan in user.alert_channels: if chan.channel_name == request.params.get( "channel_name" ) and chan.channel_value == request.params.get("channel_value"): channel = chan break if channel: user.alert_channels.remove(channel) request.session.flash(_("Your channel was removed.")) return True return False @view_config( route_name="users_self_property", permission="authenticated", match_param="key=alert_channels_actions_binds", renderer="json", request_method="POST", ) def alert_channels_actions_binds_POST(request): """ Adds alert action to users channels """ user = request.user json_body = request.unsafe_json_body channel = AlertChannelService.by_owner_id_and_pkey( user.id, json_body.get("channel_pkey") ) rule_action = AlertChannelActionService.by_owner_id_and_pkey( user.id, json_body.get("action_pkey") ) if channel and rule_action: if channel.pkey not in [c.pkey for c in rule_action.channels]: rule_action.channels.append(channel) return rule_action.get_dict(extended_info=True) return HTTPUnprocessableEntity() @view_config( route_name="users_self_property", request_method="DELETE", match_param="key=alert_channels_actions_binds", renderer="json", permission="authenticated", ) def alert_channels_actions_binds_DELETE(request): """ Removes alert action from users channels """ user = request.user channel = AlertChannelService.by_owner_id_and_pkey( user.id, request.GET.get("channel_pkey") ) rule_action = AlertChannelActionService.by_owner_id_and_pkey( user.id, request.GET.get("action_pkey") ) if channel and rule_action: if channel.pkey in [c.pkey for c in rule_action.channels]: rule_action.channels.remove(channel) return rule_action.get_dict(extended_info=True) return HTTPUnprocessableEntity() @view_config( route_name="social_auth_abort", renderer="string", permission=NO_PERMISSION_REQUIRED ) def oauth_abort(request): """ Handles problems with authorization via velruse """ @view_config(route_name="social_auth", permission=NO_PERMISSION_REQUIRED) def social_auth(request): # Get the internal provider name URL variable. provider_name = request.matchdict.get("provider") # Start the login procedure. adapter = WebObAdapter(request, request.response) result = request.authomatic.login(adapter, provider_name) if result: if result.error: return handle_auth_error(request, result) elif result.user: return handle_auth_success(request, result) return request.response def handle_auth_error(request, result): # Login procedure finished with an error. request.session.pop("zigg.social_auth", None) request.session.flash( _( "Something went wrong when we tried to " "authorize you via external provider. " "Please try again." ), "warning", ) return HTTPFound(location=request.route_url("/")) def handle_auth_success(request, result): # Hooray, we have the user! # OAuth 2.0 and OAuth 1.0a provide only limited user data on login, # We need to update the user to get more info. if result.user: result.user.update() social_data = { "user": {"data": result.user.data}, "credentials": result.user.credentials, } # normalize data social_data["user"]["id"] = result.user.id user_name = result.user.username or "" # use email name as username for google if social_data["credentials"].provider_name == "google" and result.user.email: user_name = result.user.email social_data["user"]["user_name"] = user_name social_data["user"]["email"] = result.user.email or "" request.session["zigg.social_auth"] = social_data # user is logged so bind his external identity with account if request.user: handle_social_data(request, request.user, social_data) request.session.pop("zigg.social_auth", None) return HTTPFound(location=request.route_url("/")) else: user = ExternalIdentityService.user_by_external_id_and_provider( social_data["user"]["id"], social_data["credentials"].provider_name ) # fix legacy accounts with wrong google ID if not user and social_data["credentials"].provider_name == "google": user = ExternalIdentityService.user_by_external_id_and_provider( social_data["user"]["email"], social_data["credentials"].provider_name ) # user tokens are already found in our db if user: handle_social_data(request, user, social_data) headers = security.remember(request, user.id) request.session.pop("zigg.social_auth", None) return HTTPFound(location=request.route_url("/"), headers=headers) else: msg = ( "You need to finish registration " "process to bind your external identity to your account " "or sign in to existing account" ) request.session.flash(msg) return HTTPFound(location=request.route_url("register")) @view_config( route_name="section_view", permission="authenticated", match_param=["section=users_section", "view=search_users"], renderer="json", ) def search_users(request): """ Returns a list of users for autocomplete """ user = request.user items_returned = [] like_condition = request.params.get("user_name", "") + "%" # first append used if email is passed found_user = UserService.by_email(request.params.get("user_name", "")) if found_user: name = "{} {}".format(found_user.first_name, found_user.last_name) items_returned.append({"user": found_user.user_name, "name": name}) for found_user in UserService.user_names_like(like_condition).limit(20): name = "{} {}".format(found_user.first_name, found_user.last_name) items_returned.append({"user": found_user.user_name, "name": name}) return items_returned @view_config( route_name="users_self_property", match_param="key=auth_tokens", request_method="GET", renderer="json", permission="authenticated", ) @view_config( route_name="users_property", match_param="key=auth_tokens", request_method="GET", renderer="json", permission="authenticated", ) def auth_tokens_list(request): """ Lists all available alert channels """ if request.matched_route.name == "users_self_property": user = request.user else: user = UserService.by_id(request.matchdict.get("user_id")) if not user: return HTTPNotFound() return [c.get_dict() for c in user.auth_tokens] @view_config( route_name="users_self_property", match_param="key=auth_tokens", request_method="POST", renderer="json", permission="authenticated", ) @view_config( route_name="users_property", match_param="key=auth_tokens", request_method="POST", renderer="json", permission="authenticated", ) def auth_tokens_POST(request): """ Lists all available alert channels """ if request.matched_route.name == "users_self_property": user = request.user else: user = UserService.by_id(request.matchdict.get("user_id")) if not user: return HTTPNotFound() req_data = request.safe_json_body or {} if not req_data.get("expires"): req_data.pop("expires", None) form = forms.AuthTokenCreateForm(MultiDict(req_data), csrf_context=request) if not form.validate(): return HTTPUnprocessableEntity(body=form.errors_json) token = AuthToken() form.populate_obj(token) if token.expires: interval = h.time_deltas.get(token.expires)["delta"] token.expires = datetime.datetime.utcnow() + interval user.auth_tokens.append(token) DBSession.flush() return token.get_dict() @view_config( route_name="users_self_property", match_param="key=auth_tokens", request_method="DELETE", renderer="json", permission="authenticated", ) @view_config( route_name="users_property", match_param="key=auth_tokens", request_method="DELETE", renderer="json", permission="authenticated", ) def auth_tokens_DELETE(request): """ Lists all available alert channels """ if request.matched_route.name == "users_self_property": user = request.user else: user = UserService.by_id(request.matchdict.get("user_id")) if not user: return HTTPNotFound() for token in user.auth_tokens: if token.token == request.params.get("token"): user.auth_tokens.remove(token) return True return False