Skip to content

Commit

Permalink
Enable RBAC support for public API endpoints (#5211)
Browse files Browse the repository at this point in the history
  • Loading branch information
matiasb authored Oct 30, 2024
1 parent 6254407 commit 91b67b9
Show file tree
Hide file tree
Showing 21 changed files with 272 additions and 26 deletions.
8 changes: 3 additions & 5 deletions engine/apps/auth_token/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from rest_framework.authentication import BaseAuthentication, get_authorization_header
from rest_framework.request import Request

from apps.api.permissions import GrafanaAPIPermissions, LegacyAccessControlRole, RBACPermission, user_is_authorized
from apps.api.permissions import GrafanaAPIPermissions, LegacyAccessControlRole
from apps.grafana_plugin.helpers.gcom import check_token
from apps.grafana_plugin.sync_data import SyncPermission, SyncUser
from apps.user_management.exceptions import OrganizationDeletedException, OrganizationMovedException
Expand Down Expand Up @@ -52,10 +52,8 @@ def authenticate(self, request):
auth = get_authorization_header(request).decode("utf-8")
user, auth_token = self.authenticate_credentials(auth)

if not user.is_active or not user_is_authorized(user, [RBACPermission.Permissions.API_KEYS_WRITE]):
raise exceptions.AuthenticationFailed(
"Only users with Admin permissions are allowed to perform this action."
)
if not user.is_active:
raise exceptions.AuthenticationFailed("Only active users are allowed to perform this action.")

return user, auth_token

Expand Down
98 changes: 98 additions & 0 deletions engine/apps/public_api/tests/test_rbac_permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
from unittest.mock import patch

import pytest
from django.urls import reverse
from rest_framework import status
from rest_framework.response import Response
from rest_framework.test import APIClient

from apps.api.permissions import GrafanaAPIPermission, LegacyAccessControlRole, get_most_authorized_role
from apps.public_api.urls import router


@pytest.mark.parametrize(
"rbac_enabled,role,give_perm",
[
# rbac disabled: we will check the role is enough based on get_most_authorized_role for the perm
(False, "admin", None),
(False, "editor", None),
(False, "viewer", None),
(False, None, None),
# rbac enabled: having role None, check the perm is required
(True, None, False),
(True, None, True),
],
)
@pytest.mark.django_db
def test_rbac_permissions(
make_organization_and_user_with_token,
rbac_enabled,
role,
give_perm,
):
# APIView default actions
# (name, http method, detail-based)
default_actions = {
"create": ("post", False),
"list": ("get", False),
"retrieve": ("get", True),
"update": ("put", True),
"partial_update": ("patch", True),
"destroy": ("delete", True),
}

organization, user, token = make_organization_and_user_with_token()
if organization.is_rbac_permissions_enabled != rbac_enabled:
# skip if the organization's rbac_enabled is not the expected by the test
return

client = APIClient()
# check all actions for all public API viewsets
for _, viewset, _basename in router.registry:
if viewset.__name__ == "ActionView":
# old actions (webhooks) are deprecated, no RBAC support
continue
for viewset_method_name, required_perms in viewset.rbac_permissions.items():
# setup user's role and permissions
if rbac_enabled:
# set the user's role to None and assign the permission or not based on the flag
user.role = LegacyAccessControlRole.NONE
user.permissions = []
expected = status.HTTP_403_FORBIDDEN
if give_perm:
# if permissions are given, expect a 200 response
user.permissions = [GrafanaAPIPermission(action=perm.value) for perm in required_perms]
expected = status.HTTP_200_OK
user.save()
else:
# set the user's role to the given role
user.role = LegacyAccessControlRole[role.upper()] if role else LegacyAccessControlRole.NONE
user.save()
# check what the minimum required role for the perms is
required_role = get_most_authorized_role(required_perms)
# set expected depending on the user's role
expected = status.HTTP_200_OK if user.role <= required_role else status.HTTP_403_FORBIDDEN

# iterate over all viewset actions, making an API request for each,
# using the user's token and confirming the response status code
if viewset_method_name in default_actions:
http_method, detail = default_actions[viewset_method_name]
else:
action_method = getattr(viewset, viewset_method_name)
http_method = list(action_method.mapping.keys())[0]
detail = action_method.detail

method_path = f"{viewset.__module__}.{viewset.__name__}.{viewset_method_name}"
success = Response(status=status.HTTP_200_OK)
kwargs = {"pk": "NONEXISTENT"} if detail else None
if viewset_method_name in default_actions and detail:
url = reverse(f"api-public:{_basename}-detail", kwargs=kwargs)
elif viewset_method_name in default_actions and not detail:
url = reverse(f"api-public:{_basename}-list", kwargs=kwargs)
else:
name = viewset_method_name.replace("_", "-")
url = reverse(f"api-public:{_basename}-{name}", kwargs=kwargs)

with patch(method_path, return_value=success):
response = client.generic(path=url, method=http_method, HTTP_AUTHORIZATION=token)
assert response.status_code == expected
15 changes: 14 additions & 1 deletion engine/apps/public_api/views/alert_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from apps.alerts.models import AlertGroup, AlertReceiveChannel
from apps.alerts.tasks import delete_alert_group, wipe
from apps.api.label_filtering import parse_label_query
from apps.api.permissions import RBACPermission
from apps.auth_token.auth import ApiTokenAuthentication
from apps.public_api.constants import VALID_DATE_FOR_DELETE_INCIDENT
from apps.public_api.helpers import is_valid_group_creation_date, team_has_slack_token_for_deleting
Expand Down Expand Up @@ -57,7 +58,19 @@ class AlertGroupView(
GenericViewSet,
):
authentication_classes = (ApiTokenAuthentication,)
permission_classes = (IsAuthenticated,)
permission_classes = (IsAuthenticated, RBACPermission)

rbac_permissions = {
"list": [RBACPermission.Permissions.ALERT_GROUPS_READ],
"retrieve": [RBACPermission.Permissions.ALERT_GROUPS_READ],
"destroy": [RBACPermission.Permissions.ALERT_GROUPS_WRITE],
"acknowledge": [RBACPermission.Permissions.ALERT_GROUPS_WRITE],
"unacknowledge": [RBACPermission.Permissions.ALERT_GROUPS_WRITE],
"resolve": [RBACPermission.Permissions.ALERT_GROUPS_WRITE],
"unresolve": [RBACPermission.Permissions.ALERT_GROUPS_WRITE],
"silence": [RBACPermission.Permissions.ALERT_GROUPS_WRITE],
"unsilence": [RBACPermission.Permissions.ALERT_GROUPS_WRITE],
}

throttle_classes = [UserThrottle]

Expand Down
7 changes: 6 additions & 1 deletion engine/apps/public_api/views/alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from rest_framework.viewsets import GenericViewSet

from apps.alerts.models import Alert
from apps.api.permissions import RBACPermission
from apps.auth_token.auth import ApiTokenAuthentication
from apps.public_api.serializers.alerts import AlertSerializer
from apps.public_api.throttlers.user_throttle import UserThrottle
Expand All @@ -19,7 +20,11 @@ class AlertFilter(filters.FilterSet):

class AlertView(RateLimitHeadersMixin, mixins.ListModelMixin, GenericViewSet):
authentication_classes = (ApiTokenAuthentication,)
permission_classes = (IsAuthenticated,)
permission_classes = (IsAuthenticated, RBACPermission)

rbac_permissions = {
"list": [RBACPermission.Permissions.ALERT_GROUPS_READ],
}

throttle_classes = [UserThrottle]

Expand Down
7 changes: 6 additions & 1 deletion engine/apps/public_api/views/escalation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from rest_framework.views import APIView

from apps.alerts.paging import DirectPagingAlertGroupResolvedError, DirectPagingUserTeamValidationError, direct_paging
from apps.api.permissions import RBACPermission
from apps.auth_token.auth import ApiTokenAuthentication
from apps.public_api.serializers import AlertGroupSerializer, EscalationSerializer
from apps.public_api.throttlers import UserThrottle
Expand All @@ -16,7 +17,11 @@ class EscalationView(APIView):
"""

authentication_classes = (ApiTokenAuthentication,)
permission_classes = (IsAuthenticated,)
permission_classes = (IsAuthenticated, RBACPermission)

rbac_permissions = {
"post": [RBACPermission.Permissions.ALERT_GROUPS_DIRECT_PAGING],
}

throttle_classes = [UserThrottle]

Expand Down
12 changes: 11 additions & 1 deletion engine/apps/public_api/views/escalation_chains.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from rest_framework.viewsets import ModelViewSet

from apps.alerts.models import EscalationChain
from apps.api.permissions import RBACPermission
from apps.auth_token.auth import ApiTokenAuthentication
from apps.public_api.serializers import EscalationChainSerializer
from apps.public_api.throttlers.user_throttle import UserThrottle
Expand All @@ -15,7 +16,16 @@

class EscalationChainView(RateLimitHeadersMixin, ModelViewSet):
authentication_classes = (ApiTokenAuthentication,)
permission_classes = (IsAuthenticated,)
permission_classes = (IsAuthenticated, RBACPermission)

rbac_permissions = {
"list": [RBACPermission.Permissions.ESCALATION_CHAINS_READ],
"retrieve": [RBACPermission.Permissions.ESCALATION_CHAINS_READ],
"create": [RBACPermission.Permissions.ESCALATION_CHAINS_WRITE],
"update": [RBACPermission.Permissions.ESCALATION_CHAINS_WRITE],
"partial_update": [RBACPermission.Permissions.ESCALATION_CHAINS_WRITE],
"destroy": [RBACPermission.Permissions.ESCALATION_CHAINS_WRITE],
}

throttle_classes = [UserThrottle]

Expand Down
12 changes: 11 additions & 1 deletion engine/apps/public_api/views/escalation_policies.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from rest_framework.viewsets import ModelViewSet

from apps.alerts.models import EscalationPolicy
from apps.api.permissions import RBACPermission
from apps.auth_token.auth import ApiTokenAuthentication
from apps.public_api.serializers import EscalationPolicySerializer, EscalationPolicyUpdateSerializer
from apps.public_api.throttlers.user_throttle import UserThrottle
Expand All @@ -14,7 +15,16 @@

class EscalationPolicyView(RateLimitHeadersMixin, UpdateSerializerMixin, ModelViewSet):
authentication_classes = (ApiTokenAuthentication,)
permission_classes = (IsAuthenticated,)
permission_classes = (IsAuthenticated, RBACPermission)

rbac_permissions = {
"list": [RBACPermission.Permissions.ESCALATION_CHAINS_READ],
"retrieve": [RBACPermission.Permissions.ESCALATION_CHAINS_READ],
"create": [RBACPermission.Permissions.ESCALATION_CHAINS_WRITE],
"update": [RBACPermission.Permissions.ESCALATION_CHAINS_WRITE],
"partial_update": [RBACPermission.Permissions.ESCALATION_CHAINS_WRITE],
"destroy": [RBACPermission.Permissions.ESCALATION_CHAINS_WRITE],
}

throttle_classes = [UserThrottle]

Expand Down
7 changes: 6 additions & 1 deletion engine/apps/public_api/views/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@
from rest_framework.response import Response
from rest_framework.views import APIView

from apps.api.permissions import RBACPermission
from apps.auth_token.auth import ApiTokenAuthentication
from apps.public_api.throttlers import InfoThrottler


class InfoView(APIView):
authentication_classes = (ApiTokenAuthentication,)
permission_classes = (IsAuthenticated,)
permission_classes = (IsAuthenticated, RBACPermission)

rbac_permissions = {
"get": [RBACPermission.Permissions.OTHER_SETTINGS_READ],
}

throttle_classes = [InfoThrottler]

Expand Down
14 changes: 13 additions & 1 deletion engine/apps/public_api/views/integrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from rest_framework.viewsets import ModelViewSet

from apps.alerts.models import AlertReceiveChannel
from apps.api.permissions import RBACPermission
from apps.auth_token.auth import ApiTokenAuthentication
from apps.public_api.serializers import IntegrationSerializer, IntegrationUpdateSerializer
from apps.public_api.throttlers.user_throttle import UserThrottle
Expand All @@ -24,7 +25,18 @@ class IntegrationView(
ModelViewSet,
):
authentication_classes = (ApiTokenAuthentication,)
permission_classes = (IsAuthenticated,)
permission_classes = (IsAuthenticated, RBACPermission)

rbac_permissions = {
"list": [RBACPermission.Permissions.INTEGRATIONS_READ],
"retrieve": [RBACPermission.Permissions.INTEGRATIONS_READ],
"create": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
"update": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
"partial_update": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
"destroy": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
"maintenance_start": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
"maintenance_stop": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
}

throttle_classes = [UserThrottle]

Expand Down
12 changes: 11 additions & 1 deletion engine/apps/public_api/views/on_call_shifts.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ModelViewSet

from apps.api.permissions import RBACPermission
from apps.auth_token.auth import ApiTokenAuthentication
from apps.public_api.serializers import CustomOnCallShiftSerializer, CustomOnCallShiftUpdateSerializer
from apps.public_api.throttlers.user_throttle import UserThrottle
Expand All @@ -16,7 +17,16 @@

class CustomOnCallShiftView(RateLimitHeadersMixin, UpdateSerializerMixin, ModelViewSet):
authentication_classes = (ApiTokenAuthentication,)
permission_classes = (IsAuthenticated,)
permission_classes = (IsAuthenticated, RBACPermission)

rbac_permissions = {
"list": [RBACPermission.Permissions.SCHEDULES_READ],
"retrieve": [RBACPermission.Permissions.SCHEDULES_READ],
"create": [RBACPermission.Permissions.SCHEDULES_WRITE],
"update": [RBACPermission.Permissions.SCHEDULES_WRITE],
"partial_update": [RBACPermission.Permissions.SCHEDULES_WRITE],
"destroy": [RBACPermission.Permissions.SCHEDULES_WRITE],
}

throttle_classes = [UserThrottle]

Expand Down
7 changes: 6 additions & 1 deletion engine/apps/public_api/views/organizations.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from rest_framework.settings import api_settings
from rest_framework.viewsets import ReadOnlyModelViewSet

from apps.api.permissions import RBACPermission
from apps.auth_token.auth import ApiTokenAuthentication
from apps.public_api.serializers import OrganizationSerializer
from apps.public_api.throttlers.user_throttle import UserThrottle
Expand All @@ -15,7 +16,11 @@ class OrganizationView(
ReadOnlyModelViewSet,
):
authentication_classes = (ApiTokenAuthentication,)
permission_classes = (IsAuthenticated,)
permission_classes = (IsAuthenticated, RBACPermission)

rbac_permissions = {
"retrieve": [RBACPermission.Permissions.OTHER_SETTINGS_READ],
}

throttle_classes = [UserThrottle]

Expand Down
12 changes: 11 additions & 1 deletion engine/apps/public_api/views/personal_notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet

from apps.api.permissions import RBACPermission
from apps.auth_token.auth import ApiTokenAuthentication
from apps.base.models import UserNotificationPolicy
from apps.public_api.serializers import PersonalNotificationRuleSerializer, PersonalNotificationRuleUpdateSerializer
Expand All @@ -17,7 +18,16 @@

class PersonalNotificationView(RateLimitHeadersMixin, UpdateSerializerMixin, ModelViewSet):
authentication_classes = (ApiTokenAuthentication,)
permission_classes = (IsAuthenticated,)
permission_classes = (IsAuthenticated, RBACPermission)

rbac_permissions = {
"list": [RBACPermission.Permissions.USER_SETTINGS_READ],
"retrieve": [RBACPermission.Permissions.USER_SETTINGS_READ],
"create": [RBACPermission.Permissions.USER_SETTINGS_WRITE],
"update": [RBACPermission.Permissions.USER_SETTINGS_WRITE],
"partial_update": [RBACPermission.Permissions.USER_SETTINGS_WRITE],
"destroy": [RBACPermission.Permissions.USER_SETTINGS_WRITE],
}

throttle_classes = [UserThrottle]

Expand Down
1 change: 0 additions & 1 deletion engine/apps/public_api/views/resolution_notes.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ class ResolutionNoteView(RateLimitHeadersMixin, UpdateSerializerMixin, ModelView
permission_classes = (IsAuthenticated, RBACPermission)

rbac_permissions = {
"metadata": [RBACPermission.Permissions.ALERT_GROUPS_READ],
"list": [RBACPermission.Permissions.ALERT_GROUPS_READ],
"retrieve": [RBACPermission.Permissions.ALERT_GROUPS_READ],
"create": [RBACPermission.Permissions.ALERT_GROUPS_WRITE],
Expand Down
12 changes: 11 additions & 1 deletion engine/apps/public_api/views/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from rest_framework.viewsets import ModelViewSet

from apps.alerts.models import ChannelFilter
from apps.api.permissions import RBACPermission
from apps.auth_token.auth import ApiTokenAuthentication
from apps.public_api.serializers import ChannelFilterSerializer, ChannelFilterUpdateSerializer
from apps.public_api.throttlers.user_throttle import UserThrottle
Expand All @@ -17,7 +18,16 @@

class ChannelFilterView(RateLimitHeadersMixin, UpdateSerializerMixin, ModelViewSet):
authentication_classes = (ApiTokenAuthentication,)
permission_classes = (IsAuthenticated,)
permission_classes = (IsAuthenticated, RBACPermission)

rbac_permissions = {
"list": [RBACPermission.Permissions.INTEGRATIONS_READ],
"retrieve": [RBACPermission.Permissions.INTEGRATIONS_READ],
"create": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
"update": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
"partial_update": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
"destroy": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
}

throttle_classes = [UserThrottle]

Expand Down
Loading

0 comments on commit 91b67b9

Please sign in to comment.