oncall-engine/engine/apps/api/views/escalation_chain.py

248 lines
9.1 KiB
Python
Raw Permalink Normal View History

from django.db.models import Count, Q
from django_filters import rest_framework as filters
from drf_spectacular.utils import PolymorphicProxySerializer, extend_schema, extend_schema_view, inline_serializer
from emoji import emojize
from rest_framework import serializers, status, viewsets
from rest_framework.decorators import action
from rest_framework.filters import SearchFilter
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from apps.alerts.models import EscalationChain
Add RBAC Support (#777) * Modify plugin.json to support RBAC role registration * defines 26 new custom roles in plugin.json. The main roles are: - Admin: read/write access to everything in OnCall - Reader: read access to everything in OnCall - OnCaller : read access to everything in OnCall + edit access to Alert Groups and Schedules - <object-type> Editor: read/write access to everything related to <object-type> - <object-type> Reader: read access for <object-type> - User Settings Admin: read/write access to all user's settings, not just own settings. This is in comparison to User Settings Editor which can only read/write own settings * update changelog and documentation (#686) * implement RBAC for OnCall backend This commit refactors backend authorization. It trys to use RBAC authorization if the org's grafana instance supports it, otherwise it falls back to basic role authorization. * update RBAC backend tests * add tests for RBAC changes - run backend tests as matrix where RBAC is enabled/disabled. When RBAC is enabled, the permissions granted are read from the role grants in the frontend's plugin.json file (instead of relying what we specify in RBACPermission.Permissions) - remove --reuse-db --nomigrations flags from engine/tox.ini - minor autoformatting changes to docker-compose-developer.yml * remove --ds=settings.ci-test from pytest CI command DJANGO_SETTINGS_MODULE is already specified as an env var so this is just unecessary duplication * update gitignore * update github action job name for "test" * RBAC frontend changes * refactors the use of basic roles (ex. Viewer, Editor, Admin) use RBAC permissions (when supported), or falling back to basic roles when RBAC is not supported. - updates the UserAction enum in grafana-plugin/src/state/userAction.ts. Previously this was hardcoded to a list of strings that were being returned by the OnCall API. Now the values here correspond to the permissions in plugin.json (plus a fallback role) * changes per Gabriel's comments: - get rid of group attribute in rbac roles - remove displayName role attribute - remove hidden role attribute - add back role to includes section * don't try to update user timezone if they don't have permission
2022-11-29 09:41:56 +01:00
from apps.api.permissions import RBACPermission
from apps.api.serializers.escalation_chain import (
EscalationChainListSerializer,
EscalationChainSerializer,
FilterEscalationChainSerializer,
)
from apps.auth_token.auth import PluginAuthentication
from apps.mobile_app.auth import MobileAppAuthTokenAuthentication
from apps.user_management.models import Team
from common.api_helpers.exceptions import BadRequest
from common.api_helpers.filters import (
NO_TEAM_VALUE,
ByTeamModelFieldFilterMixin,
ModelFieldFilterMixin,
TeamModelMultipleChoiceFilter,
)
from common.api_helpers.mixins import (
FilterSerializerMixin,
ListSerializerMixin,
PublicPrimaryKeyMixin,
TeamFilteringMixin,
)
from common.insight_log import EntityEvent, write_resource_insight_log
class EscalationChainFilter(ByTeamModelFieldFilterMixin, ModelFieldFilterMixin, filters.FilterSet):
team = TeamModelMultipleChoiceFilter()
@extend_schema_view(
list=extend_schema(
responses=PolymorphicProxySerializer(
component_name="EscalationChainPolymorphic",
serializers=[EscalationChainListSerializer, FilterEscalationChainSerializer],
resource_type_field_name=None,
)
),
update=extend_schema(responses=EscalationChainSerializer),
partial_update=extend_schema(responses=EscalationChainSerializer),
)
class EscalationChainViewSet(
TeamFilteringMixin,
Support alert routing based on labels (#3778) # What this PR does This PR adds support for routing alerts based on labels. https://www.loom.com/share/4401de6e3c4945d5b8961fe43ee373c9 Additionally: - improve the typing around the `get_object` method that is inherited by [`PublicPrimaryKeyMixin.get_object`](https://github.com/grafana/oncall/blob/dev/engine/common/api_helpers/mixins.py#L153) in most of our models. `PublicPrimaryKeyMixin` is generic, so it can be more strongly typed when it is being subclassed, which results in better typing of the `get_object` method in child classes - I decided to do this because I started looking into this task via the [`AlertReceiveChannelView.send_demo_alert` method/endpoint](https://github.com/grafana/oncall/blob/dev/engine/apps/api/views/alert_receive_channel.py#L242). Within that method, `instance` is not typed because the inherited `get_object` method is not typed.. I digress 😄 - improve typing around `Alert.create` and `apps.integrations.tasks.create_alert` functions - make `Alert.render_group_data` more DRY by extracting some logic out into `Alert._apply_jinja_template_to_alert_payload_and_labels` - deduplicate the logic of `value.strip().lower() in ["1", "true", "ok"]` into a shared function, `common.jinja_templater.apply_jinja_template.templated_value_is_truthy` Closes https://github.com/grafana/oncall-private/issues/2490 ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required) - [x] Documentation added (or `pr:no public docs` PR label added if not required) (will be done in #3762)
2024-01-30 13:07:19 -05:00
PublicPrimaryKeyMixin[EscalationChain],
FilterSerializerMixin,
ListSerializerMixin,
viewsets.ModelViewSet,
):
"""
Internal API endpoints for escalation chains.
"""
authentication_classes = (
MobileAppAuthTokenAuthentication,
PluginAuthentication,
)
Add RBAC Support (#777) * Modify plugin.json to support RBAC role registration * defines 26 new custom roles in plugin.json. The main roles are: - Admin: read/write access to everything in OnCall - Reader: read access to everything in OnCall - OnCaller : read access to everything in OnCall + edit access to Alert Groups and Schedules - <object-type> Editor: read/write access to everything related to <object-type> - <object-type> Reader: read access for <object-type> - User Settings Admin: read/write access to all user's settings, not just own settings. This is in comparison to User Settings Editor which can only read/write own settings * update changelog and documentation (#686) * implement RBAC for OnCall backend This commit refactors backend authorization. It trys to use RBAC authorization if the org's grafana instance supports it, otherwise it falls back to basic role authorization. * update RBAC backend tests * add tests for RBAC changes - run backend tests as matrix where RBAC is enabled/disabled. When RBAC is enabled, the permissions granted are read from the role grants in the frontend's plugin.json file (instead of relying what we specify in RBACPermission.Permissions) - remove --reuse-db --nomigrations flags from engine/tox.ini - minor autoformatting changes to docker-compose-developer.yml * remove --ds=settings.ci-test from pytest CI command DJANGO_SETTINGS_MODULE is already specified as an env var so this is just unecessary duplication * update gitignore * update github action job name for "test" * RBAC frontend changes * refactors the use of basic roles (ex. Viewer, Editor, Admin) use RBAC permissions (when supported), or falling back to basic roles when RBAC is not supported. - updates the UserAction enum in grafana-plugin/src/state/userAction.ts. Previously this was hardcoded to a list of strings that were being returned by the OnCall API. Now the values here correspond to the permissions in plugin.json (plus a fallback role) * changes per Gabriel's comments: - get rid of group attribute in rbac roles - remove displayName role attribute - remove hidden role attribute - add back role to includes section * don't try to update user timezone if they don't have permission
2022-11-29 09:41:56 +01:00
permission_classes = (IsAuthenticated, RBACPermission)
Add RBAC Support (#777) * Modify plugin.json to support RBAC role registration * defines 26 new custom roles in plugin.json. The main roles are: - Admin: read/write access to everything in OnCall - Reader: read access to everything in OnCall - OnCaller : read access to everything in OnCall + edit access to Alert Groups and Schedules - <object-type> Editor: read/write access to everything related to <object-type> - <object-type> Reader: read access for <object-type> - User Settings Admin: read/write access to all user's settings, not just own settings. This is in comparison to User Settings Editor which can only read/write own settings * update changelog and documentation (#686) * implement RBAC for OnCall backend This commit refactors backend authorization. It trys to use RBAC authorization if the org's grafana instance supports it, otherwise it falls back to basic role authorization. * update RBAC backend tests * add tests for RBAC changes - run backend tests as matrix where RBAC is enabled/disabled. When RBAC is enabled, the permissions granted are read from the role grants in the frontend's plugin.json file (instead of relying what we specify in RBACPermission.Permissions) - remove --reuse-db --nomigrations flags from engine/tox.ini - minor autoformatting changes to docker-compose-developer.yml * remove --ds=settings.ci-test from pytest CI command DJANGO_SETTINGS_MODULE is already specified as an env var so this is just unecessary duplication * update gitignore * update github action job name for "test" * RBAC frontend changes * refactors the use of basic roles (ex. Viewer, Editor, Admin) use RBAC permissions (when supported), or falling back to basic roles when RBAC is not supported. - updates the UserAction enum in grafana-plugin/src/state/userAction.ts. Previously this was hardcoded to a list of strings that were being returned by the OnCall API. Now the values here correspond to the permissions in plugin.json (plus a fallback role) * changes per Gabriel's comments: - get rid of group attribute in rbac roles - remove displayName role attribute - remove hidden role attribute - add back role to includes section * don't try to update user timezone if they don't have permission
2022-11-29 09:41:56 +01:00
rbac_permissions = {
"metadata": [RBACPermission.Permissions.ESCALATION_CHAINS_READ],
"list": [RBACPermission.Permissions.ESCALATION_CHAINS_READ],
"retrieve": [RBACPermission.Permissions.ESCALATION_CHAINS_READ],
"details": [RBACPermission.Permissions.ESCALATION_CHAINS_READ],
"create": [RBACPermission.Permissions.ESCALATION_CHAINS_WRITE],
"update": [RBACPermission.Permissions.ESCALATION_CHAINS_WRITE],
"destroy": [RBACPermission.Permissions.ESCALATION_CHAINS_WRITE],
"copy": [RBACPermission.Permissions.ESCALATION_CHAINS_WRITE],
"filters": [RBACPermission.Permissions.ESCALATION_CHAINS_READ],
}
queryset = EscalationChain.objects.none() # needed for drf-spectacular introspection
filter_backends = [SearchFilter, filters.DjangoFilterBackend]
search_fields = ("name",)
filterset_class = EscalationChainFilter
serializer_class = EscalationChainSerializer
list_serializer_class = EscalationChainListSerializer
filter_serializer_class = FilterEscalationChainSerializer
def get_queryset(self, ignore_filtering_by_available_teams=False):
is_filters_request = self.request.query_params.get("filters", "false") == "true"
queryset = EscalationChain.objects.filter(
organization=self.request.auth.organization,
)
if not ignore_filtering_by_available_teams:
queryset = queryset.filter(*self.available_teams_lookup_args).distinct()
if is_filters_request:
# Do not annotate num_integrations and num_routes for filters request,
# only fetch public_primary_key and name fields needed by FilterEscalationChainSerializer
return queryset.only("public_primary_key", "name")
queryset = queryset.annotate(
num_integrations=Count(
"channel_filters__alert_receive_channel",
distinct=True,
filter=Q(channel_filters__alert_receive_channel__deleted_at__isnull=True),
)
).annotate(
num_routes=Count(
"channel_filters",
distinct=True,
filter=Q(channel_filters__alert_receive_channel__deleted_at__isnull=True),
)
)
return queryset
def perform_create(self, serializer):
serializer.save()
write_resource_insight_log(instance=serializer.instance, author=self.request.user, event=EntityEvent.CREATED)
def perform_destroy(self, instance):
write_resource_insight_log(
instance=instance,
author=self.request.user,
event=EntityEvent.DELETED,
)
instance.delete()
def perform_update(self, serializer):
prev_state = serializer.instance.insight_logs_serialized
serializer.save()
new_state = serializer.instance.insight_logs_serialized
write_resource_insight_log(
instance=serializer.instance,
author=self.request.user,
event=EntityEvent.UPDATED,
prev_state=prev_state,
new_state=new_state,
)
@extend_schema(responses=EscalationChainSerializer)
@action(methods=["post"], detail=True)
def copy(self, request, pk):
obj = self.get_object()
name = request.data.get("name")
team_id = request.data.get("team")
if team_id == NO_TEAM_VALUE:
team_id = None
if not name:
raise BadRequest(detail={"name": ["This field may not be null."]})
else:
if EscalationChain.objects.filter(organization=request.auth.organization, name=name).exists():
raise BadRequest(detail={"name": ["Escalation chain with this name already exists."]})
try:
team = request.user.available_teams.get(public_primary_key=team_id) if team_id else None
except Team.DoesNotExist:
return Response(data={"error_code": "wrong_team"}, status=status.HTTP_403_FORBIDDEN)
copy = obj.make_copy(name, team)
serializer = self.get_serializer(copy)
write_resource_insight_log(
instance=copy,
author=self.request.user,
event=EntityEvent.CREATED,
)
return Response(serializer.data)
@extend_schema(
responses=inline_serializer(
name="EscalationChainDetails",
fields={
"id": serializers.CharField(),
"display_name": serializers.CharField(),
"channel_filters": inline_serializer(
name="EscalationChainDetailsChannelFilter",
fields={
"id": serializers.CharField(),
"display_name": serializers.CharField(),
},
many=True,
),
},
many=True,
)
)
@action(methods=["get"], detail=True)
def details(self, request, pk):
obj = self.get_object()
channel_filters = obj.channel_filters.filter(alert_receive_channel__deleted_at__isnull=True).values(
"public_primary_key",
"filtering_term",
"is_default",
"alert_receive_channel__public_primary_key",
"alert_receive_channel__verbal_name",
)
data = {}
for channel_filter in channel_filters:
channel_filter_data = {
"display_name": "Default Route" if channel_filter["is_default"] else channel_filter["filtering_term"],
"id": channel_filter["public_primary_key"],
}
data.setdefault(
channel_filter["alert_receive_channel__public_primary_key"],
{
"id": channel_filter["alert_receive_channel__public_primary_key"],
Fix warnings when running backend tests (#2079) # What this PR does - update `make test` to always use `settings.ci-test`. Right now it will use whatever the value of `DJANGO_SETTINGS_MODULE` is in `./dev/.env.dev`, which causes ~45 tests to fail - Fix several Python warnings that we see when running the tests ```bash RemovedInDjango40Warning: The providing_args argument is deprecated. As it is purely documentational, it has no replacement. If you rely on this argument as documentation, you can move the text to a code comment or docstring. alert_create_signal = django.dispatch.Signal( ``` ```bash PytestCollectionWarning: cannot collect test class 'TestOnlyBackend' because it has a __init__ constructor (from: apps/api/tests/test_alert_receive_channel_template.py) class TestOnlyBackend(BaseMessagingBackend): ``` ```bash DeprecationWarning: The parameter 'use_aliases' in emoji.emojize() is deprecated and will be removed in version 2.0.0. Use language='alias' instead. To hide this warning, pin/downgrade the package to 'emoji~=1.6.3' return emoji.emojize(self.verbal_name, use_aliases=True) ``` ```bash DateTimeField CustomOnCallShift.start received a naive datetime (2023-06-01 12:53:12) while time zone support is active. warnings.warn("DateTimeField %s received a naive datetime (%s)" ``` ```bash apps/twilioapp/tests/test_phone_calls.py::test_resolve_by_phone /etc/app/apps/twilioapp/tests/test_phone_calls.py:173: DeprecationWarning: The 'text' argument to find()-type methods is deprecated. Use 'string' instead. content = BeautifulSoup(content, features="html.parser").findAll(text=True) ``` ```bash apps/twilioapp/tests/test_phone_calls.py::test_resolve_by_phone apps/twilioapp/tests/test_phone_calls.py::test_wrong_pressed_digit /usr/local/lib/python3.11/site-packages/bs4/builder/__init__.py:545: XMLParsedAsHTMLWarning: It looks like you're parsing an XML document using an HTML parser. If this really is an HTML document (maybe it's XHTML?), you can ignore or filter this warning. If it's XML, you should know that using an XML parser will be more reliable. To parse this document as XML, make sure you have the lxml package installed, and pass the keyword argument `features="xml"` into the BeautifulSoup constructor. ``` ```bash apps/twilioapp/tests/test_phone_calls.py::test_forbidden_requests /usr/local/lib/python3.11/site-packages/social_django/urls.py:15: RemovedInDjango40Warning: django.conf.urls.url() is deprecated in favor of django.urls.re_path(). url(r'^login/(?P<backend>[^/]+){0}$'.format(extra), views.auth, ``` ```bash apps/twilioapp/tests/test_phone_calls.py: 66 warnings /usr/local/lib/python3.11/site-packages/debug_toolbar/utils.py:255: DeprecationWarning: currentThread() is deprecated, use current_thread() instead thread = threading.currentThread() ``` ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [ ] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required)
2023-06-06 20:38:00 +02:00
"display_name": emojize(channel_filter["alert_receive_channel__verbal_name"], language="alias"),
"channel_filters": [],
},
)["channel_filters"].append(channel_filter_data)
return Response(data.values())
@extend_schema(
responses=inline_serializer(
name="EscalationChainFilters",
fields={
"name": serializers.CharField(),
"type": serializers.CharField(),
"href": serializers.CharField(required=False),
"global": serializers.BooleanField(required=False),
},
many=True,
)
)
@action(methods=["get"], detail=False)
def filters(self, request):
api_root = "/api/internal/v1/"
filter_options = [
{"name": "search", "type": "search"},
{
"name": "team",
"type": "team_select",
"href": api_root + "teams/",
"global": True,
},
]
return Response(filter_options)