From 75f319fb5d0ac650ae4057ba79612ff54e069aa4 Mon Sep 17 00:00:00 2001 From: Innokentii Konstantinov Date: Sat, 4 Jun 2022 16:49:10 +0400 Subject: [PATCH] Add CloudUsersView and CloudUserView --- engine/apps/api/views/features.py | 5 ++ engine/apps/base/utils.py | 17 ++++++ engine/apps/oss_installation/constants.py | 5 ++ .../apps/oss_installation/models/__init__.py | 2 +- .../models/cloud_organization_connector.py | 5 +- ...{cloud_users.py => cloud_user_identity.py} | 0 engine/apps/oss_installation/urls.py | 14 ++++- .../apps/oss_installation/views/cloud_user.py | 61 +++++++++++++++++++ .../oss_installation/views/cloud_users.py | 36 ++++++----- 9 files changed, 125 insertions(+), 20 deletions(-) rename engine/apps/oss_installation/models/{cloud_users.py => cloud_user_identity.py} (100%) create mode 100644 engine/apps/oss_installation/views/cloud_user.py diff --git a/engine/apps/api/views/features.py b/engine/apps/api/views/features.py index 6a4285de..79ed373b 100644 --- a/engine/apps/api/views/features.py +++ b/engine/apps/api/views/features.py @@ -4,11 +4,13 @@ from rest_framework.response import Response from rest_framework.views import APIView from apps.auth_token.auth import PluginAuthentication +from apps.base.utils import live_settings FEATURE_SLACK = "slack" FEATURE_TELEGRAM = "telegram" FEATURE_LIVE_SETTINGS = "live_settings" MOBILE_APP_PUSH_NOTIFICATIONS = "mobile_app" +FEATURE_GRAFANA_CLOUD_NOTIFICATIONS = "grafana_cloud_notifications" class FeaturesAPIView(APIView): @@ -34,6 +36,9 @@ class FeaturesAPIView(APIView): if settings.FEATURE_LIVE_SETTINGS_ENABLED: enabled_features.append(FEATURE_LIVE_SETTINGS) + if live_settings.GRAFANA_CLOUD_NOTIFICATIONS_ENABLED: + enabled_features.append(FEATURE_GRAFANA_CLOUD_NOTIFICATIONS) + if settings.MOBILE_APP_PUSH_NOTIFICATIONS_ENABLED: DynamicSetting = apps.get_model("base", "DynamicSetting") mobile_app_settings = DynamicSetting.objects.get_or_create( diff --git a/engine/apps/base/utils.py b/engine/apps/base/utils.py index 7342d00e..8ea5801e 100644 --- a/engine/apps/base/utils.py +++ b/engine/apps/base/utils.py @@ -1,7 +1,10 @@ import json import re +from urllib.parse import urljoin +import requests.exceptions from django.apps import apps +from django.conf import settings from python_http_client import UnauthorizedError from sendgrid import SendGridAPIClient from telegram import Bot @@ -94,6 +97,20 @@ class LiveSettingValidator: except Exception as e: return f"Telegram error: {str(e)}" + @classmethod + def _check_grafana_cloud_oncall_token(cls, grafan_oncall_token): + try: + info_url = urljoin(settings.GRAFANA_CLOUD_ONCALL_API_URL, "api/v1/info/") + r = requests.get(info_url, headers={"AUTHORIZATION": grafan_oncall_token}, timeout=5) + if r.status_code == 200: + return + elif r.status_code == 403: + return f"Invalid token." + else: + return f"Non-200 HTTP code. Got {r.status_code}" + except requests.exceptions.RequestException as e: + return f"Error {str(e)}" + @staticmethod def _is_email_valid(email): return re.match(r"^[^@]+@[^@]+\.[^@]+$", email) diff --git a/engine/apps/oss_installation/constants.py b/engine/apps/oss_installation/constants.py index c6c3b88b..db777bb3 100644 --- a/engine/apps/oss_installation/constants.py +++ b/engine/apps/oss_installation/constants.py @@ -1 +1,6 @@ CLOUD_URL = "https://a-prod-us-central-0.grafana.net/" + +CLOUD_NOT_SYNCED = 0 +CLOUD_SYNCED_USER_NOT_FOUND = 1 +CLOUD_SYNCED_PHONE_NOT_VERIFIED = 2 +CLOUD_SYNCED_PHONE_VERIFIED = 3 diff --git a/engine/apps/oss_installation/models/__init__.py b/engine/apps/oss_installation/models/__init__.py index 80721219..2ee74128 100644 --- a/engine/apps/oss_installation/models/__init__.py +++ b/engine/apps/oss_installation/models/__init__.py @@ -1,4 +1,4 @@ from .cloud_organization_connector import CloudOrganizationConnector # noqa: F401 -from .cloud_users import CloudUserIdentity # noqa: F401 +from .cloud_user_identity import CloudUserIdentity # noqa: F401 from .heartbeat import CloudHeartbeat # noqa: F401 from .oss_installation import OssInstallation # noqa: F401 diff --git a/engine/apps/oss_installation/models/cloud_organization_connector.py b/engine/apps/oss_installation/models/cloud_organization_connector.py index a142ddcb..732be38e 100644 --- a/engine/apps/oss_installation/models/cloud_organization_connector.py +++ b/engine/apps/oss_installation/models/cloud_organization_connector.py @@ -6,7 +6,7 @@ from django.db import models from apps.base.utils import live_settings from apps.oss_installation.constants import CLOUD_URL -from apps.oss_installation.models.cloud_users import CloudUserIdentity +from apps.oss_installation.models.cloud_user_identity import CloudUserIdentity from apps.user_management.models import User logger = logging.getLogger(__name__) @@ -59,7 +59,7 @@ class CloudOrganizationConnector(models.Model): users_url = urljoin(CLOUD_URL, "api/v1/users") existing_cloud_identities = list(CloudUserIdentity.objects.filter(organization=self.organization)) - existing_cloud_ids = list(map(lambda u: u.cloud_id, existing_cloud_identities)) + existing_cloud_ids = list(map(lambda identity: identity.cloud_id, existing_cloud_identities)) fetch_next_page = True page = 1 @@ -102,7 +102,6 @@ class CloudOrganizationConnector(models.Model): i.email = cloud_users_identities_to_update[i.cloud_id]["email"] i.phone_number_verified = cloud_users_identities_to_update[i.cloud_id]["is_phone_number_verified"] - # TODO: Grafana CN: check if data validation needed. CloudUserIdentity.objects.bulk_create(cloud_users_identities_to_create, batch_size=1000) CloudUserIdentity.objects.bulk_update( existing_cloud_identities, ["email", "phone_number_verified"], batch_size=1000 diff --git a/engine/apps/oss_installation/models/cloud_users.py b/engine/apps/oss_installation/models/cloud_user_identity.py similarity index 100% rename from engine/apps/oss_installation/models/cloud_users.py rename to engine/apps/oss_installation/models/cloud_user_identity.py diff --git a/engine/apps/oss_installation/urls.py b/engine/apps/oss_installation/urls.py index cfa876e2..86bb640b 100644 --- a/engine/apps/oss_installation/urls.py +++ b/engine/apps/oss_installation/urls.py @@ -1,8 +1,20 @@ +from django.urls import path + from common.api_helpers.optional_slash_router import optional_slash_path from .views import CloudHeartbeatStatusView, CloudUsersView +from .views.cloud_user import CloudUserView urlpatterns = [ optional_slash_path("cloud_heartbeat_status", CloudHeartbeatStatusView.as_view(), name="cloud_heartbeat_status"), - optional_slash_path("cloud_users", CloudUsersView.as_view(), name="cloud_users"), + optional_slash_path("cloud_users", CloudUsersView.as_view(), name="cloud-users-list"), + path( + "cloud_users/", + CloudUserView.as_view( + { + "get": "retrieve", + } + ), + name="cloud-user-detail", + ), ] diff --git a/engine/apps/oss_installation/views/cloud_user.py b/engine/apps/oss_installation/views/cloud_user.py new file mode 100644 index 00000000..5f5805cb --- /dev/null +++ b/engine/apps/oss_installation/views/cloud_user.py @@ -0,0 +1,61 @@ +from urllib.parse import urljoin + +from rest_framework import mixins, serializers, viewsets +from rest_framework.permissions import IsAuthenticated + +import apps.oss_installation.constants as cloud_constants +from apps.api.permissions import ActionPermission, IsOwnerOrAdmin +from apps.auth_token.auth import PluginAuthentication +from apps.oss_installation.models import CloudOrganizationConnector, CloudUserIdentity +from apps.user_management.models import User +from common.api_helpers.mixins import PublicPrimaryKeyMixin + + +class CloudUserSerializer(serializers.ModelSerializer): + cloud_data = serializers.SerializerMethodField() + + class Meta: + model = User + fields = ["sync_data"] + + def get_cloud_data(self, obj): + link = None + status = cloud_constants.CLOUD_NOT_SYNCED + connector = CloudOrganizationConnector.objects.filter( + organization=self.context["request"].auth.organization + ).first() + if connector is not None: + cloud_user_identity = CloudUserIdentity.objects.filter(email=obj.email).first() + if cloud_user_identity is None: + status = cloud_constants.CLOUD_SYNCED_USER_NOT_FOUND + link = connector.cloud_url + elif not cloud_user_identity.phone_number_verified: + status = cloud_constants.CLOUD_SYNCED_USER_NOT_FOUND + link = urljoin( + connector.cloud_url, f"a/grafana-oncall-app/?page=users&p=1&id={cloud_user_identity.cloud_id}" + ) + else: + status = cloud_constants.CLOUD_SYNCED_PHONE_VERIFIED + link = urljoin( + connector.cloud_url, f"a/grafana-oncall-app/?page=users&p=1&id={cloud_user_identity.cloud_id}" + ) + cloud_data = {"status": status, "link": link} + return cloud_data + + +class CloudUserView( + PublicPrimaryKeyMixin, + mixins.RetrieveModelMixin, + viewsets.GenericViewSet, +): + authentication_classes = (PluginAuthentication,) + permission_classes = (IsAuthenticated, ActionPermission) + + action_object_permissions = { + IsOwnerOrAdmin: ("retrieve",), + } + serializer_class = CloudUserSerializer + + def get_queryset(self): + queryset = User.objects.filter(organization=self.request.user.organization) + return queryset diff --git a/engine/apps/oss_installation/views/cloud_users.py b/engine/apps/oss_installation/views/cloud_users.py index a1f93343..af3a5cd8 100644 --- a/engine/apps/oss_installation/views/cloud_users.py +++ b/engine/apps/oss_installation/views/cloud_users.py @@ -3,6 +3,8 @@ from urllib.parse import urljoin from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView +import apps.oss_installation.constants as cloud_constants +from apps.api.permissions import IsAdmin from apps.auth_token.auth import PluginAuthentication from apps.oss_installation.models import CloudOrganizationConnector, CloudUserIdentity from apps.user_management.models import User @@ -11,8 +13,7 @@ from common.api_helpers.paginators import HundredPageSizePaginator class CloudUsersView(HundredPageSizePaginator, APIView): authentication_classes = (PluginAuthentication,) - # TODO: Grafana CN - permissions, ratelimit - permission_classes = (IsAuthenticated,) + permission_classes = (IsAuthenticated, IsAdmin) def get(self, request): queryset = User.objects.filter(organization=self.request.user.organization) @@ -31,23 +32,28 @@ class CloudUsersView(HundredPageSizePaginator, APIView): response = [] connector = CloudOrganizationConnector.objects.first() - for user in results: - cloud_identity = cloud_identities.get(user.email, None) link = None - status = 0 - if cloud_identity: - status = 1 - is_phone_verified = cloud_identity.phone_number_verified - if is_phone_verified: - status = 2 - link = urljoin( - connector.cloud_url, f"a/grafana-oncall-app/?page=users&p=1&id={cloud_identity.cloud_id}" - ) + status = cloud_constants.CLOUD_NOT_SYNCED + if connector is not None: + status = cloud_constants.CLOUD_SYNCED_USER_NOT_FOUND + cloud_identity = cloud_identities.get(user.email, None) + if cloud_identity: + status = cloud_constants.CLOUD_SYNCED_PHONE_NOT_VERIFIED + is_phone_verified = cloud_identity.phone_number_verified + if is_phone_verified: + status = cloud_constants.CLOUD_SYNCED_PHONE_VERIFIED + link = urljoin( + connector.cloud_url, f"a/grafana-oncall-app/?page=users&p=1&id={cloud_identity.cloud_id}" + ) - # TODO: Grafana CN - decide if emails is needed. If yes - don't forget to check that they mustn't be shown to users response.append( - {"id": user.public_primary_key, "username": user.username, "cloud_sync_status": status, "link": link} + { + "id": user.public_primary_key, + "email": user.email, + "username": user.username, + "cloud_data": {"status": status, "link": link}, + } ) return self.get_paginated_response(response)