# -*- 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 copy import json import logging from datetime import datetime, timedelta import colander from pyramid.httpexceptions import HTTPFound, HTTPUnprocessableEntity from pyramid.view import view_config from webob.multidict import MultiDict from zope.sqlalchemy import mark_changed from ziggurat_foundations.permissions import ANY_PERMISSION import appenlight.forms as forms from appenlight.models import DBSession from appenlight.models.resource import Resource from appenlight.models.application import Application from appenlight.models.application_postprocess_conf import ApplicationPostprocessConf from ziggurat_foundations.models.services.user import UserService from ziggurat_foundations.models.services.resource import ResourceService from ziggurat_foundations.models.services.user_resource_permission import ( UserResourcePermissionService, ) from appenlight.models.user_resource_permission import UserResourcePermission from appenlight.models.group_resource_permission import GroupResourcePermission from appenlight.models.services.application import ApplicationService from appenlight.models.services.application_postprocess_conf import ( ApplicationPostprocessConfService, ) from appenlight.models.services.group import GroupService from appenlight.models.services.group_resource_permission import ( GroupResourcePermissionService, ) from appenlight.models.services.request_metric import RequestMetricService from appenlight.models.services.report_group import ReportGroupService from appenlight.models.services.slow_call import SlowCallService from appenlight.lib import helpers as h from appenlight.lib.utils import build_filter_settings_from_query_dict from appenlight.security import RootFactory from appenlight.models.report import REPORT_TYPE_MATRIX from appenlight.validators import build_rule_schema _ = str log = logging.getLogger(__name__) def app_not_found(request, id): """ Redirects on non found and sets a flash message """ request.session.flash(_("Application not found"), "warning") return HTTPFound(location=request.route_url("applications", action="index")) @view_config( route_name="applications_no_id", renderer="json", request_method="GET", permission="authenticated", ) def applications_list(request): """ Applications list if query params contain ?type=foo, it will list applications with one of those permissions for user, otherwise only list of owned applications will be returned appending ?root_list while being administration will allow to list all applications in the system """ is_root = request.has_permission("root_administration", RootFactory(request)) if is_root and request.GET.get("root_list"): resources = Resource.all().order_by(Resource.resource_name) resource_type = request.GET.get("resource_type", "application") if resource_type: resources = resources.filter(Resource.resource_type == resource_type) else: permissions = request.params.getall("permission") if permissions: resources = UserService.resources_with_perms( request.user, permissions, resource_types=[request.GET.get("resource_type", "application")], ) else: resources = request.user.resources.filter( Application.resource_type == request.GET.get("resource_type", "application") ) return [ r.get_dict( include_keys=[ "resource_id", "resource_name", "domains", "owner_user_name", "owner_group_name", ] ) for r in resources ] @view_config( route_name="applications", renderer="json", request_method="GET", permission="view" ) def application_GET(request): resource = request.context.resource include_sensitive_info = False if request.has_permission("edit"): include_sensitive_info = True resource_dict = resource.get_dict( include_perms=include_sensitive_info, include_processing_rules=include_sensitive_info, ) return resource_dict @view_config( route_name="applications_no_id", request_method="POST", renderer="json", permission="create_resources", ) def application_create(request): """ Creates new application instances """ user = request.user form = forms.ApplicationCreateForm( MultiDict(request.unsafe_json_body), csrf_context=request ) if form.validate(): session = DBSession() resource = Application() DBSession.add(resource) form.populate_obj(resource) resource.api_key = resource.generate_api_key() user.resources.append(resource) request.session.flash(_("Application created")) DBSession.flush() mark_changed(session) else: return HTTPUnprocessableEntity(body=form.errors_json) return resource.get_dict() @view_config( route_name="applications", request_method="PATCH", renderer="json", permission="edit", ) def application_update(request): """ Updates main application configuration """ resource = request.context.resource if not resource: return app_not_found() # disallow setting permanent storage by non-admins # use default/non-resource based context for this check req_dict = copy.copy(request.unsafe_json_body) if not request.has_permission("root_administration", RootFactory(request)): req_dict["allow_permanent_storage"] = "" if not req_dict.get("uptime_url"): # needed cause validator is still triggered by default req_dict.pop("uptime_url", "") application_form = forms.ApplicationUpdateForm( MultiDict(req_dict), csrf_context=request ) if application_form.validate(): application_form.populate_obj(resource) request.session.flash(_("Application updated")) else: return HTTPUnprocessableEntity(body=application_form.errors_json) include_sensitive_info = False if request.has_permission("edit"): include_sensitive_info = True resource_dict = resource.get_dict( include_perms=include_sensitive_info, include_processing_rules=include_sensitive_info, ) return resource_dict @view_config( route_name="applications_property", match_param="key=api_key", request_method="POST", renderer="json", permission="delete", ) def application_regenerate_key(request): """ Regenerates API keys for application """ resource = request.context.resource form = forms.CheckPasswordForm( MultiDict(request.unsafe_json_body), csrf_context=request ) form.password.user = request.user if form.validate(): resource.api_key = resource.generate_api_key() resource.public_key = resource.generate_api_key() msg = "API keys regenerated - please update your application config." request.session.flash(_(msg)) else: return HTTPUnprocessableEntity(body=form.errors_json) if request.has_permission("edit"): include_sensitive_info = True resource_dict = resource.get_dict( include_perms=include_sensitive_info, include_processing_rules=include_sensitive_info, ) return resource_dict @view_config( route_name="applications_property", match_param="key=delete_resource", request_method="PATCH", renderer="json", permission="delete", ) def application_remove(request): """ Removes application resources """ resource = request.context.resource # we need polymorphic object here, to properly launch sqlalchemy events resource = ApplicationService.by_id(resource.resource_id) form = forms.CheckPasswordForm( MultiDict(request.safe_json_body or {}), csrf_context=request ) form.password.user = request.user if form.validate(): DBSession.delete(resource) request.session.flash(_("Application removed")) else: return HTTPUnprocessableEntity(body=form.errors_json) return True @view_config( route_name="applications_property", match_param="key=owner", request_method="PATCH", renderer="json", permission="delete", ) def application_ownership_transfer(request): """ Allows application owner to transfer application ownership to other user """ resource = request.context.resource form = forms.ChangeApplicationOwnerForm( MultiDict(request.safe_json_body or {}), csrf_context=request ) form.password.user = request.user if form.validate(): user = UserService.by_user_name(form.user_name.data) user.resources.append(resource) # remove integrations to not leak security data of external applications for integration in resource.integrations[:]: resource.integrations.remove(integration) request.session.flash(_("Application transfered")) else: return HTTPUnprocessableEntity(body=form.errors_json) return True @view_config( route_name="applications_property", match_param="key=postprocessing_rules", renderer="json", request_method="POST", permission="edit", ) def applications_postprocess_POST(request): """ Creates new postprocessing rules for applications """ resource = request.context.resource conf = ApplicationPostprocessConf() conf.do = "postprocess" conf.new_value = "1" resource.postprocess_conf.append(conf) DBSession.flush() return conf.get_dict() @view_config( route_name="applications_property", match_param="key=postprocessing_rules", renderer="json", request_method="PATCH", permission="edit", ) def applications_postprocess_PATCH(request): """ Creates new postprocessing rules for applications """ 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())) resource = request.context.resource conf = ApplicationPostprocessConfService.by_pkey_and_resource_id( json_body["pkey"], resource.resource_id ) conf.rule = request.unsafe_json_body["rule"] # for now hardcode int since we dont support anything else so far conf.new_value = int(request.unsafe_json_body["new_value"]) return conf.get_dict() @view_config( route_name="applications_property", match_param="key=postprocessing_rules", renderer="json", request_method="DELETE", permission="edit", ) def applications_postprocess_DELETE(request): """ Removes application postprocessing rules """ form = forms.ReactorForm(request.POST, csrf_context=request) resource = request.context.resource if form.validate(): for postprocess_conf in resource.postprocess_conf: if postprocess_conf.pkey == int(request.GET["pkey"]): # remove rule DBSession.delete(postprocess_conf) return True @view_config( route_name="applications_property", match_param="key=report_graphs", renderer="json", permission="view", ) @view_config( route_name="applications_property", match_param="key=slow_report_graphs", renderer="json", permission="view", ) def get_application_report_stats(request): query_params = request.GET.mixed() query_params["resource"] = (request.context.resource.resource_id,) filter_settings = build_filter_settings_from_query_dict(request, query_params) if not filter_settings.get("end_date"): end_date = datetime.utcnow().replace(microsecond=0, second=0) filter_settings["end_date"] = end_date if not filter_settings.get("start_date"): delta = timedelta(hours=1) filter_settings["start_date"] = filter_settings["end_date"] - delta result = ReportGroupService.get_report_stats(request, filter_settings) return result @view_config( route_name="applications_property", match_param="key=metrics_graphs", renderer="json", permission="view", ) def metrics_graphs(request): """ Handles metric dashboard graphs Returns information for time/tier breakdown """ query_params = request.GET.mixed() query_params["resource"] = (request.context.resource.resource_id,) filter_settings = build_filter_settings_from_query_dict(request, query_params) if not filter_settings.get("end_date"): end_date = datetime.utcnow().replace(microsecond=0, second=0) filter_settings["end_date"] = end_date delta = timedelta(hours=1) if not filter_settings.get("start_date"): filter_settings["start_date"] = filter_settings["end_date"] - delta if filter_settings["end_date"] <= filter_settings["start_date"]: filter_settings["end_date"] = filter_settings["start_date"] delta = filter_settings["end_date"] - filter_settings["start_date"] if delta < h.time_deltas.get("12h")["delta"]: divide_by_min = 1 elif delta <= h.time_deltas.get("3d")["delta"]: divide_by_min = 5.0 elif delta >= h.time_deltas.get("2w")["delta"]: divide_by_min = 60.0 * 24 else: divide_by_min = 60.0 results = RequestMetricService.get_metrics_stats(request, filter_settings) # because requests are PER SECOND / we divide 1 min stats by 60 # requests are normalized to 1 min average # results are average seconds time spent per request in specific area for point in results: if point["requests"]: point["main"] = ( point["main"] - point["sql"] - point["nosql"] - point["remote"] - point["tmpl"] - point["custom"] ) / point["requests"] point["sql"] = point["sql"] / point["requests"] point["nosql"] = point["nosql"] / point["requests"] point["remote"] = point["remote"] / point["requests"] point["tmpl"] = point["tmpl"] / point["requests"] point["custom"] = point["custom"] / point["requests"] point["requests_2"] = point["requests"] / 60.0 / divide_by_min selected_types = ["main", "sql", "nosql", "remote", "tmpl", "custom"] for point in results: for stat_type in selected_types: point[stat_type] = round(point.get(stat_type, 0), 3) return results @view_config( route_name="applications_property", match_param="key=response_graphs", renderer="json", permission="view", ) def response_graphs(request): """ Handles dashboard infomation for avg. response time split by today, 2 days ago and week ago """ query_params = request.GET.mixed() query_params["resource"] = (request.context.resource.resource_id,) filter_settings = build_filter_settings_from_query_dict(request, query_params) if not filter_settings.get("end_date"): end_date = datetime.utcnow().replace(microsecond=0, second=0) filter_settings["end_date"] = end_date delta = timedelta(hours=1) if not filter_settings.get("start_date"): filter_settings["start_date"] = filter_settings["end_date"] - delta result_now = RequestMetricService.get_metrics_stats(request, filter_settings) filter_settings_2d = filter_settings.copy() filter_settings_2d["start_date"] = filter_settings["start_date"] - timedelta(days=2) filter_settings_2d["end_date"] = filter_settings["end_date"] - timedelta(days=2) result_2d = RequestMetricService.get_metrics_stats(request, filter_settings_2d) filter_settings_7d = filter_settings.copy() filter_settings_7d["start_date"] = filter_settings["start_date"] - timedelta(days=7) filter_settings_7d["end_date"] = filter_settings["end_date"] - timedelta(days=7) result_7d = RequestMetricService.get_metrics_stats(request, filter_settings_7d) plot_data = [] for item in result_now: point = {"x": item["x"], "today": 0, "days_ago_2": 0, "days_ago_7": 0} if item["requests"]: point["today"] = round(item["main"] / item["requests"], 3) plot_data.append(point) for i, item in enumerate(result_2d[: len(plot_data)]): plot_data[i]["days_ago_2"] = 0 point = result_2d[i] if point["requests"]: plot_data[i]["days_ago_2"] = round(point["main"] / point["requests"], 3) for i, item in enumerate(result_7d[: len(plot_data)]): plot_data[i]["days_ago_7"] = 0 point = result_7d[i] if point["requests"]: plot_data[i]["days_ago_7"] = round(point["main"] / point["requests"], 3) return plot_data @view_config( route_name="applications_property", match_param="key=requests_graphs", renderer="json", permission="view", ) def requests_graphs(request): """ Handles dashboard infomation for avg. response time split by today, 2 days ago and week ago """ query_params = request.GET.mixed() query_params["resource"] = (request.context.resource.resource_id,) filter_settings = build_filter_settings_from_query_dict(request, query_params) if not filter_settings.get("end_date"): end_date = datetime.utcnow().replace(microsecond=0, second=0) filter_settings["end_date"] = end_date delta = timedelta(hours=1) if not filter_settings.get("start_date"): filter_settings["start_date"] = filter_settings["end_date"] - delta result_now = RequestMetricService.get_metrics_stats(request, filter_settings) delta = filter_settings["end_date"] - filter_settings["start_date"] if delta < h.time_deltas.get("12h")["delta"]: seconds = h.time_deltas["1m"]["minutes"] * 60.0 elif delta <= h.time_deltas.get("3d")["delta"]: seconds = h.time_deltas["5m"]["minutes"] * 60.0 elif delta >= h.time_deltas.get("2w")["delta"]: seconds = h.time_deltas["24h"]["minutes"] * 60.0 else: seconds = h.time_deltas["1h"]["minutes"] * 60.0 for item in result_now: if item["requests"]: item["requests"] = round(item["requests"] / seconds, 3) return result_now @view_config( route_name="applications_property", match_param="key=apdex_stats", renderer="json", permission="view", ) def get_apdex_stats(request): """ Returns information and calculates APDEX score per server for dashboard server information (upper right stats boxes) """ query_params = request.GET.mixed() query_params["resource"] = (request.context.resource.resource_id,) filter_settings = build_filter_settings_from_query_dict(request, query_params) # make sure we have only one resource here to don't produce # weird results when we have wrong app in app selector filter_settings["resource"] = [filter_settings["resource"][0]] if not filter_settings.get("end_date"): end_date = datetime.utcnow().replace(microsecond=0, second=0) filter_settings["end_date"] = end_date delta = timedelta(hours=1) if not filter_settings.get("start_date"): filter_settings["start_date"] = filter_settings["end_date"] - delta return RequestMetricService.get_apdex_stats(request, filter_settings) @view_config( route_name="applications_property", match_param="key=slow_calls", renderer="json", permission="view", ) def get_slow_calls(request): """ Returns information for time consuming calls in specific time interval """ query_params = request.GET.mixed() query_params["resource"] = (request.context.resource.resource_id,) filter_settings = build_filter_settings_from_query_dict(request, query_params) if not filter_settings.get("end_date"): end_date = datetime.utcnow().replace(microsecond=0, second=0) filter_settings["end_date"] = end_date delta = timedelta(hours=1) if not filter_settings.get("start_date"): filter_settings["start_date"] = filter_settings["end_date"] - delta return SlowCallService.get_time_consuming_calls(request, filter_settings) @view_config( route_name="applications_property", match_param="key=requests_breakdown", renderer="json", permission="view", ) def get_requests_breakdown(request): """ Used on dashboard to get information which views are most used in a time interval """ query_params = request.GET.mixed() query_params["resource"] = (request.context.resource.resource_id,) filter_settings = build_filter_settings_from_query_dict(request, query_params) if not filter_settings.get("end_date"): end_date = datetime.utcnow().replace(microsecond=0, second=0) filter_settings["end_date"] = end_date if not filter_settings.get("start_date"): delta = timedelta(hours=1) filter_settings["start_date"] = filter_settings["end_date"] - delta series = RequestMetricService.get_requests_breakdown(request, filter_settings) results = [] for row in series: d_row = { "avg_response": round(row["main"] / row["requests"], 3), "requests": row["requests"], "main": row["main"], "view_name": row["key"], "latest_details": row["latest_details"], "percentage": round(row["percentage"] * 100, 1), } results.append(d_row) return results @view_config( route_name="applications_property", match_param="key=trending_reports", renderer="json", permission="view", ) def trending_reports(request): """ Returns exception/slow reports trending for specific time interval """ query_params = request.GET.mixed().copy() # pop report type to rewrite it to tag later report_type = query_params.pop("report_type", None) if report_type: query_params["type"] = report_type query_params["resource"] = (request.context.resource.resource_id,) filter_settings = build_filter_settings_from_query_dict(request, query_params) if not filter_settings.get("end_date"): end_date = datetime.utcnow().replace(microsecond=0, second=0) filter_settings["end_date"] = end_date if not filter_settings.get("start_date"): delta = timedelta(hours=1) filter_settings["start_date"] = filter_settings["end_date"] - delta results = ReportGroupService.get_trending(request, filter_settings) trending = [] for occurences, group in results: report_group = group.get_dict(request) # show the occurences in time range instead of global ones report_group["occurences"] = occurences trending.append(report_group) return trending @view_config( route_name="applications_property", match_param="key=integrations", renderer="json", permission="view", ) def integrations(request): """ Integration list for given application """ application = request.context.resource return {"resource": application} @view_config( route_name="applications_property", match_param="key=user_permissions", renderer="json", permission="owner", request_method="POST", ) def user_resource_permission_create(request): """ Set new permissions for user for a resource """ resource = request.context.resource user_name = request.unsafe_json_body.get("user_name") user = UserService.by_user_name(user_name) if not user: user = UserService.by_email(user_name) if not user: return False for perm_name in request.unsafe_json_body.get("permissions", []): permission = UserResourcePermissionService.by_resource_user_and_perm( user.id, perm_name, resource.resource_id ) if not permission: permission = UserResourcePermission(perm_name=perm_name, user_id=user.id) resource.user_permissions.append(permission) DBSession.flush() perms = [ p.perm_name for p in ResourceService.perms_for_user(resource, user) if p.type == "user" ] result = {"user_name": user.user_name, "permissions": list(set(perms))} return result @view_config( route_name="applications_property", match_param="key=user_permissions", renderer="json", permission="owner", request_method="DELETE", ) def user_resource_permission_delete(request): """ Removes user permission from specific resource """ resource = request.context.resource user = UserService.by_user_name(request.GET.get("user_name")) if not user: return False for perm_name in request.GET.getall("permissions"): permission = UserResourcePermissionService.by_resource_user_and_perm( user.id, perm_name, resource.resource_id ) resource.user_permissions.remove(permission) DBSession.flush() perms = [ p.perm_name for p in ResourceService.perms_for_user(resource, user) if p.type == "user" ] result = {"user_name": user.user_name, "permissions": list(set(perms))} return result @view_config( route_name="applications_property", match_param="key=group_permissions", renderer="json", permission="owner", request_method="POST", ) def group_resource_permission_create(request): """ Set new permissions for group for a resource """ resource = request.context.resource group = GroupService.by_id(request.unsafe_json_body.get("group_id")) if not group: return False for perm_name in request.unsafe_json_body.get("permissions", []): permission = GroupResourcePermissionService.by_resource_group_and_perm( group.id, perm_name, resource.resource_id ) if not permission: permission = GroupResourcePermission(perm_name=perm_name, group_id=group.id) resource.group_permissions.append(permission) DBSession.flush() perm_tuples = ResourceService.groups_for_perm( resource, ANY_PERMISSION, limit_group_permissions=True, group_ids=[group.id] ) perms = [p.perm_name for p in perm_tuples if p.type == "group"] result = {"group": group.get_dict(), "permissions": list(set(perms))} return result @view_config( route_name="applications_property", match_param="key=group_permissions", renderer="json", permission="owner", request_method="DELETE", ) def group_resource_permission_delete(request): """ Removes group permission from specific resource """ form = forms.ReactorForm(request.POST, csrf_context=request) form.validate() resource = request.context.resource group = GroupService.by_id(request.GET.get("group_id")) if not group: return False for perm_name in request.GET.getall("permissions"): permission = GroupResourcePermissionService.by_resource_group_and_perm( group.id, perm_name, resource.resource_id ) resource.group_permissions.remove(permission) DBSession.flush() perm_tuples = ResourceService.groups_for_perm( resource, ANY_PERMISSION, limit_group_permissions=True, group_ids=[group.id] ) perms = [p.perm_name for p in perm_tuples if p.type == "group"] result = {"group": group.get_dict(), "permissions": list(set(perms))} return result