diff --git a/CHANGELOG.md b/CHANGELOG.md index d7c8ead8..3120bf88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,36 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## v1.2.0 (TBD) +## v1.1.8 (2022-12-13) + +### Added + +- Added a `make` command, `enable-mobile-app-feature-flags`, which sets the backend feature flag in `./dev/.env.dev`, + and updates a record in the `base_dynamicsetting` database table, which are needed to enable the mobile + app backend features. + +### Changed + +- removed APNS support +- changed the `django-push-notification` library from the `iskhakov` fork to the [`grafana` fork](https://github.com/grafana/django-push-notifications). + This new fork basically patches an issue which affected the database migrations of this django app (previously the + library would not respect the `USER_MODEL` setting when creating its tables and would instead reference the + `auth_user` table.. which we don't want) +- add `--no-cache` flag to the `make build` command + +### Fixed + +- fix schedule UI types and permissions + +## v1.1.7 (2022-12-09) + +### Fixed + +- Update fallback role for schedule write RBAC permission +- Mobile App Verification tab in the user settings modal is now hidden for users that do not have proper + permissions to use it + +## v1.1.6 (2022-12-09) ### Added diff --git a/Makefile b/Makefile index 52efd02e..4ada9540 100644 --- a/Makefile +++ b/Makefile @@ -86,7 +86,7 @@ restart: $(call run_docker_compose_command,restart) build: - $(call run_docker_compose_command,build) + $(call run_docker_compose_command,build --no-cache) cleanup: stop docker system prune --filter label="$(DOCKER_COMPOSE_DEV_LABEL)" --all --volumes @@ -126,6 +126,12 @@ engine-manage: exec-engine: docker exec -it oncall_engine bash +_enable-mobile-app-feature-flags: + $(shell ./dev/add_env_var.sh FEATURE_MOBILE_APP_INTEGRATION_ENABLED True $(DEV_ENV_FILE)) + $(call run_engine_docker_command,python manage.py enable_mobile_app) + +enable-mobile-app-feature-flags: _enable-mobile-app-feature-flags stop start + # The below commands are useful for running backend services outside of docker define backend_command export `grep -v '^#' $(DEV_ENV_FILE) | xargs -0` && \ diff --git a/dev/README.md b/dev/README.md index 5fd5dbea..4ffbfebc 100644 --- a/dev/README.md +++ b/dev/README.md @@ -120,6 +120,10 @@ make build # rebuild images (e.g. when changing requirements.txt) # run Django's `manage.py` script, inside of a docker container, passing `$CMD` as arguments. # e.g. `make engine-manage CMD="makemigrations"` - https://docs.djangoproject.com/en/4.1/ref/django-admin/#django-admin-makemigrations make engine-manage CMD="..." +# sets a feature flag, related to mobile app backend functionality, in your ./dev/.env.dev +# and sets the necessary database values +# NOTE: you need to enable, and configure, the plugin before running this command +make enable-mobile-app-feature-flags # this will remove all of the images, containers, volumes, and networks # associated with your local OnCall developer setup diff --git a/dev/add_env_var.sh b/dev/add_env_var.sh new file mode 100755 index 00000000..ab3e7c6c --- /dev/null +++ b/dev/add_env_var.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# https://gist.github.com/maxpoletaev/4ed25183427a2cd7e57a + +case "$OSTYPE" in + darwin*) PLATFORM="OSX" ;; + linux*) PLATFORM="LINUX" ;; + bsd*) PLATFORM="BSD" ;; + *) PLATFORM="UNKNOWN" ;; +esac + +replace() { + if [[ "$PLATFORM" == "OSX" || "$PLATFORM" == "BSD" ]]; then + sed -i "" "$1" "$2" + elif [ "$PLATFORM" == "LINUX" ]; then + sed -i "$1" "$2" + fi +} + +if grep -q $1 $3; then + # file contains string, lets replace it + # https://stackoverflow.com/a/42667816 - why we need the -i '' + replace "s~$1=.*~$1=$2~g" $3 +else + # file doesn't contain string, lets append it + echo "$1=$2" >> $3 +fi diff --git a/engine/apps/alerts/models/alert_group.py b/engine/apps/alerts/models/alert_group.py index 2b7659c5..8a00fb83 100644 --- a/engine/apps/alerts/models/alert_group.py +++ b/engine/apps/alerts/models/alert_group.py @@ -74,7 +74,7 @@ class AlertGroupQuerySet(models.QuerySet): # Try to return the last open group # Note that (channel, channel_filter, distinction, is_open_for_grouping) is in unique_together try: - return self.get(**search_params, is_open_for_grouping=True), False + return self.get(**search_params, is_open_for_grouping__isnull=False), False except self.model.DoesNotExist: pass diff --git a/engine/apps/api/permissions/__init__.py b/engine/apps/api/permissions/__init__.py index 1b9409c5..00311d75 100644 --- a/engine/apps/api/permissions/__init__.py +++ b/engine/apps/api/permissions/__init__.py @@ -109,7 +109,7 @@ class RBACPermission(permissions.BasePermission): Resources.SCHEDULES, Actions.READ, LegacyAccessControlRole.VIEWER ) SCHEDULES_WRITE = LegacyAccessControlCompatiblePermission( - Resources.SCHEDULES, Actions.WRITE, LegacyAccessControlRole.ADMIN + Resources.SCHEDULES, Actions.WRITE, LegacyAccessControlRole.EDITOR ) SCHEDULES_EXPORT = LegacyAccessControlCompatiblePermission( Resources.SCHEDULES, Actions.EXPORT, LegacyAccessControlRole.EDITOR diff --git a/engine/apps/api/tests/test_oncall_shift.py b/engine/apps/api/tests/test_oncall_shift.py index 0235775d..23e23079 100644 --- a/engine/apps/api/tests/test_oncall_shift.py +++ b/engine/apps/api/tests/test_oncall_shift.py @@ -926,7 +926,7 @@ def test_create_on_call_shift_override_invalid_data(on_call_shift_internal_api_s "role,expected_status", [ (LegacyAccessControlRole.ADMIN, status.HTTP_201_CREATED), - (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.EDITOR, status.HTTP_201_CREATED), (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) @@ -958,7 +958,7 @@ def test_on_call_shift_create_permissions( "role,expected_status", [ (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), - (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) @@ -1080,7 +1080,7 @@ def test_on_call_shift_retrieve_permissions( "role,expected_status", [ (LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT), - (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.EDITOR, status.HTTP_204_NO_CONTENT), (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) @@ -1185,7 +1185,7 @@ def test_on_call_shift_days_options_permissions( "role,expected_status", [ (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), - (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) diff --git a/engine/apps/api/tests/test_schedules.py b/engine/apps/api/tests/test_schedules.py index 64185118..e0f73002 100644 --- a/engine/apps/api/tests/test_schedules.py +++ b/engine/apps/api/tests/test_schedules.py @@ -1204,7 +1204,7 @@ def test_filter_events_invalid_type( "role,expected_status", [ (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), - (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) @@ -1242,7 +1242,7 @@ def test_schedule_create_permissions( "role,expected_status", [ (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), - (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) @@ -1360,7 +1360,7 @@ def test_schedule_retrieve_permissions( "role,expected_status", [ (LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT), - (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.EDITOR, status.HTTP_204_NO_CONTENT), (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) @@ -1436,7 +1436,7 @@ def test_events_permissions( "role,expected_status", [ (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), - (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) diff --git a/engine/apps/api/urls.py b/engine/apps/api/urls.py index 05d87b75..96f05f71 100644 --- a/engine/apps/api/urls.py +++ b/engine/apps/api/urls.py @@ -1,7 +1,5 @@ -from django.conf import settings from django.urls import include, path, re_path -from apps.mobile_app.views import APNSDeviceAuthorizedViewSet from common.api_helpers.optional_slash_router import OptionalSlashRouter, optional_slash_path from .views import UserNotificationPolicyView, auth @@ -68,10 +66,6 @@ router.register(r"tokens", PublicApiTokenView, basename="api_token") router.register(r"live_settings", LiveSettingViewSet, basename="live_settings") router.register(r"oncall_shifts", OnCallShiftView, basename="oncall_shifts") -# TODO: remove this when the hackathon app is deprecated (APNSDeviceAuthorizedViewSet is registered in mobile_app) -if settings.FEATURE_MOBILE_APP_INTEGRATION_ENABLED: - router.register(r"device/apns", APNSDeviceAuthorizedViewSet) - urlpatterns = [ path("", include(router.urls)), optional_slash_path("user", CurrentUserView.as_view(), name="api-user"), diff --git a/engine/apps/api/views/user.py b/engine/apps/api/views/user.py index f54d58be..34155272 100644 --- a/engine/apps/api/views/user.py +++ b/engine/apps/api/views/user.py @@ -28,8 +28,7 @@ from apps.auth_token.constants import SCHEDULE_EXPORT_TOKEN_NAME from apps.auth_token.models import UserScheduleExportAuthToken from apps.base.messaging import get_messaging_backend_from_id from apps.base.utils import live_settings -from apps.mobile_app.auth import MobileAppAuthTokenAuthentication, MobileAppVerificationTokenAuthentication -from apps.mobile_app.models import MobileAppAuthToken +from apps.mobile_app.auth import MobileAppAuthTokenAuthentication from apps.telegram.client import TelegramClient from apps.telegram.models import TelegramVerificationCode from apps.twilioapp.phone_manager import PhoneManager @@ -128,7 +127,6 @@ class UserView( "unlink_backend": [RBACPermission.Permissions.USER_SETTINGS_WRITE], "make_test_call": [RBACPermission.Permissions.USER_SETTINGS_WRITE], "export_token": [RBACPermission.Permissions.USER_SETTINGS_WRITE], - "mobile_app_auth_token": [RBACPermission.Permissions.USER_SETTINGS_WRITE], } rbac_object_permissions = { @@ -149,7 +147,6 @@ class UserView( "unlink_backend", "make_test_call", "export_token", - "mobile_app_auth_token", ], } @@ -471,64 +468,3 @@ class UserView( except UserScheduleExportAuthToken.DoesNotExist: raise NotFound return Response(status=status.HTTP_204_NO_CONTENT) - - @action( - methods=["get", "post", "delete"], - detail=False, - authentication_classes=(MobileAppVerificationTokenAuthentication,), - ) - def mobile_app_auth_token(self, request): - """ - TODO: remove after hackathon app is deprecated (see apps.mobile_app.views.MobileAppAuthTokenAPIView) - """ - DynamicSetting = apps.get_model("base", "DynamicSetting") - - if not settings.FEATURE_MOBILE_APP_INTEGRATION_ENABLED: - return Response(status=status.HTTP_404_NOT_FOUND) - - mobile_app_settings = DynamicSetting.objects.get_or_create( - name="mobile_app_settings", - defaults={ - "json_value": { - "org_ids": [], - } - }, - )[0] - if self.request.auth.organization.pk not in mobile_app_settings.json_value["org_ids"]: - return Response(status=status.HTTP_404_NOT_FOUND) - - if self.request.method == "GET": - try: - token = MobileAppAuthToken.objects.get(user=self.request.user) - except MobileAppAuthToken.DoesNotExist: - raise NotFound - - response = { - "token_id": token.id, - "user_id": token.user_id, - "organization_id": token.organization_id, - "created_at": token.created_at, - "revoked_at": token.revoked_at, - } - return Response(response, status=status.HTTP_200_OK) - - if self.request.method == "POST": - # If token already exists revoke it - try: - token = MobileAppAuthToken.objects.get(user=self.request.user) - token.delete() - except MobileAppAuthToken.DoesNotExist: - pass - - instance, token = MobileAppAuthToken.create_auth_token(self.request.user, self.request.user.organization) - data = {"id": instance.pk, "token": token, "created_at": instance.created_at} - return Response(data, status=status.HTTP_201_CREATED) - - if self.request.method == "DELETE": - try: - token = MobileAppAuthToken.objects.get(user=self.request.user) - token.delete() - except MobileAppAuthToken.DoesNotExist: - raise NotFound - - return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/engine/apps/grafana_plugin/helpers/client.py b/engine/apps/grafana_plugin/helpers/client.py index 9244a1ce..44316e22 100644 --- a/engine/apps/grafana_plugin/helpers/client.py +++ b/engine/apps/grafana_plugin/helpers/client.py @@ -191,7 +191,7 @@ class GcomAPIClient(APIClient): super().__init__(settings.GRAFANA_COM_API_URL, api_token) def get_instance_info(self, stack_id: str) -> Optional[GCOMInstanceInfo]: - data, _ = self.api_get(f"instances/{stack_id}?config=true") + data, _ = self.api_get(f"instances/{stack_id}") return data def get_instances(self, query: str): diff --git a/engine/apps/grafana_plugin/helpers/gcom.py b/engine/apps/grafana_plugin/helpers/gcom.py index f4b5b47b..3a595a44 100644 --- a/engine/apps/grafana_plugin/helpers/gcom.py +++ b/engine/apps/grafana_plugin/helpers/gcom.py @@ -44,8 +44,6 @@ def check_gcom_permission(token_string: str, context) -> Optional["GcomToken"]: if not instance_info or str(instance_info["orgId"]) != org_id: raise InvalidToken - rbac_is_enabled = client.is_rbac_enabled_for_organization() - if not organization: DynamicSetting = apps.get_model("base", "DynamicSetting") allow_signup = DynamicSetting.objects.get_or_create( @@ -62,7 +60,6 @@ def check_gcom_permission(token_string: str, context) -> Optional["GcomToken"]: region_slug=instance_info["regionSlug"], gcom_token=token_string, gcom_token_org_last_time_synced=timezone.now(), - is_rbac_permissions_enabled=rbac_is_enabled, ) else: organization.stack_slug = instance_info["slug"] @@ -72,7 +69,6 @@ def check_gcom_permission(token_string: str, context) -> Optional["GcomToken"]: organization.grafana_url = instance_info["url"] organization.gcom_token = token_string organization.gcom_token_org_last_time_synced = timezone.now() - organization.is_rbac_permissions_enabled = rbac_is_enabled organization.save( update_fields=[ "stack_slug", @@ -82,7 +78,6 @@ def check_gcom_permission(token_string: str, context) -> Optional["GcomToken"]: "grafana_url", "gcom_token", "gcom_token_org_last_time_synced", - "is_rbac_permissions_enabled", ] ) logger.debug(f"Finish authenticate by making request to gcom api for org={org_id}, stack_id={stack_id}") diff --git a/engine/apps/mobile_app/backend.py b/engine/apps/mobile_app/backend.py index 40663216..db518de0 100644 --- a/engine/apps/mobile_app/backend.py +++ b/engine/apps/mobile_app/backend.py @@ -1,7 +1,7 @@ import json from django.conf import settings -from push_notifications.models import APNSDevice, GCMDevice +from push_notifications.models import GCMDevice from apps.base.messaging import BaseMessagingBackend from apps.mobile_app.tasks import notify_user_async @@ -35,7 +35,6 @@ class MobileAppBackend(BaseMessagingBackend): token.delete() # delete push notification related info for user - APNSDevice.objects.filter(user=user).delete() GCMDevice.objects.filter(user=user).delete() def serialize_user(self, user): diff --git a/engine/apps/mobile_app/tasks.py b/engine/apps/mobile_app/tasks.py index e6a6a4bf..fc9341d9 100644 --- a/engine/apps/mobile_app/tasks.py +++ b/engine/apps/mobile_app/tasks.py @@ -1,6 +1,6 @@ from celery.utils.log import get_task_logger from django.conf import settings -from push_notifications.models import APNSDevice, GCMDevice +from push_notifications.models import GCMDevice from apps.alerts.models import AlertGroup from apps.mobile_app.alert_rendering import get_push_notification_message @@ -34,12 +34,10 @@ def notify_user_async(user_pk, alert_group_pk, notification_policy_pk, critical) logger.warning(f"User notification policy {notification_policy_pk} does not exist") return - # APNS is for notifying iOS devices, GCM for Android - apns_devices_to_notify = APNSDevice.objects.filter(user=user) gcm_devices_to_notify = GCMDevice.objects.filter(user=user) # create an error log in case user has no devices set up - if not apns_devices_to_notify.exists() and not gcm_devices_to_notify.exists(): + if not gcm_devices_to_notify.exists(): UserNotificationPolicyLogRecord.objects.create( author=user, type=UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED, @@ -67,29 +65,21 @@ def notify_user_async(user_pk, alert_group_pk, notification_policy_pk, critical) "sound": "bingbong.aiff", } - apns_devices_to_notify.send_message( - message, - thread_id=thread_id, - category="USER_NEW_INCIDENT", # TODO: rename to USER_NEW_ALERT_GROUP - extra={ - "orgId": alert_group.channel.organization.public_primary_key, - "orgName": alert_group.channel.organization.stack_slug, - "alertGroupId": alert_group.public_primary_key, - "incidentId": alert_group.public_primary_key, # TODO: remove after hackathon app is deprecated - "status": alert_group.status, - "aps": aps, - }, + extra = { + "orgId": alert_group.channel.organization.public_primary_key, + "orgName": alert_group.channel.organization.stack_slug, + "alertGroupId": alert_group.public_primary_key, + "status": alert_group.status, + "aps": aps, + } + + logger.info(f"Sending push notification with message: {message}; thread-id: {thread_id}; extra: {extra}") + + # TODO: rename category to USER_NEW_ALERT_GROUP + fcm_response = gcm_devices_to_notify.send_message( + message, thread_id=thread_id, category="USER_NEW_INCIDENT", extra=extra ) - gcm_devices_to_notify.send_message( - message, - thread_id=thread_id, - category="USER_NEW_INCIDENT", # TODO: rename to USER_NEW_ALERT_GROUP - extra={ - "orgId": alert_group.channel.organization.public_primary_key, - "orgName": alert_group.channel.organization.stack_slug, - "alertGroupId": alert_group.public_primary_key, - "status": alert_group.status, - "aps": aps, - }, - ) + # NOTE: we may want to further handle the response from FCM, but for now lets simply log it out + # https://firebase.google.com/docs/cloud-messaging/http-server-ref#interpret-downstream + logger.info(f"FCM response was: {fcm_response}") diff --git a/engine/apps/mobile_app/urls.py b/engine/apps/mobile_app/urls.py index 5af825a9..8b14dc7c 100644 --- a/engine/apps/mobile_app/urls.py +++ b/engine/apps/mobile_app/urls.py @@ -1,13 +1,12 @@ from django.conf import settings from apps.mobile_app.fcm_relay import FCMRelayView -from apps.mobile_app.views import APNSDeviceAuthorizedViewSet, FCMDeviceAuthorizedViewSet, MobileAppAuthTokenAPIView +from apps.mobile_app.views import FCMDeviceAuthorizedViewSet, MobileAppAuthTokenAPIView from common.api_helpers.optional_slash_router import OptionalSlashRouter, optional_slash_path app_name = "mobile_app" router = OptionalSlashRouter() -router.register("apns", APNSDeviceAuthorizedViewSet, basename="apns") router.register("fcm", FCMDeviceAuthorizedViewSet, basename="fcm") urlpatterns = [ diff --git a/engine/apps/mobile_app/views.py b/engine/apps/mobile_app/views.py index a9290f85..b0188b3e 100644 --- a/engine/apps/mobile_app/views.py +++ b/engine/apps/mobile_app/views.py @@ -1,4 +1,3 @@ -from push_notifications.api.rest_framework import APNSDeviceAuthorizedViewSet as BaseAPNSDeviceAuthorizedViewSet from push_notifications.api.rest_framework import GCMDeviceAuthorizedViewSet, GCMDeviceSerializer from rest_framework import status from rest_framework.exceptions import NotFound @@ -10,10 +9,6 @@ from apps.mobile_app.auth import MobileAppAuthTokenAuthentication, MobileAppVeri from apps.mobile_app.models import MobileAppAuthToken -class APNSDeviceAuthorizedViewSet(BaseAPNSDeviceAuthorizedViewSet): - authentication_classes = (MobileAppAuthTokenAuthentication,) - - class FCMDeviceAuthorizedViewSet(GCMDeviceAuthorizedViewSet): class FCMDeviceSerializer(GCMDeviceSerializer): """ diff --git a/engine/apps/slack/views.py b/engine/apps/slack/views.py index 439a8f64..d7df72e9 100644 --- a/engine/apps/slack/views.py +++ b/engine/apps/slack/views.py @@ -143,6 +143,15 @@ class SlackEventApiEndpointView(APIView): if isinstance(payload, str): payload = json.JSONDecoder().decode(payload) + logger.info( + "team_id: %s channel_id: %s user_id: %s command: %s event: %s", + payload.get("team_id"), + payload.get("channel_id"), + payload.get("user_id"), + payload.get("command"), + payload.get("event", {}).get("type"), + ) + # Checking if it's repeated Slack request if "HTTP_X_SLACK_RETRY_NUM" in request.META and int(request.META["HTTP_X_SLACK_RETRY_NUM"]) > 1: logger.critical( diff --git a/engine/apps/user_management/sync.py b/engine/apps/user_management/sync.py index 740b3332..2334c5c5 100644 --- a/engine/apps/user_management/sync.py +++ b/engine/apps/user_management/sync.py @@ -17,18 +17,7 @@ def sync_organization(organization): rbac_is_enabled = client.is_rbac_enabled_for_organization() organization.is_rbac_permissions_enabled = rbac_is_enabled - if organization.gcom_token: - gcom_client = GcomAPIClient(organization.gcom_token) - instance_info = gcom_client.get_instance_info(organization.stack_id) - if not instance_info or str(instance_info["orgId"]) != organization.org_id: - return - - organization.stack_slug = instance_info["slug"] - organization.org_slug = instance_info["orgSlug"] - organization.org_title = instance_info["orgName"] - organization.region_slug = instance_info["regionSlug"] - organization.grafana_url = instance_info["url"] - organization.gcom_token_org_last_time_synced = timezone.now() + _sync_instance_info(organization) api_users = client.get_users(rbac_is_enabled) @@ -53,6 +42,22 @@ def sync_organization(organization): ) +def _sync_instance_info(organization): + if organization.gcom_token: + gcom_client = GcomAPIClient(organization.gcom_token) + instance_info = gcom_client.get_instance_info(organization.stack_id) + + if not instance_info or instance_info["orgId"] != organization.org_id: + return + + organization.stack_slug = instance_info["slug"] + organization.org_slug = instance_info["orgSlug"] + organization.org_title = instance_info["orgName"] + organization.region_slug = instance_info["regionSlug"] + organization.grafana_url = instance_info["url"] + organization.gcom_token_org_last_time_synced = timezone.now() + + def sync_users_and_teams(client, api_users, organization): # check if api_users are shaped correctly. e.g. for paused instance, the response is not a list. if not api_users or not isinstance(api_users, (tuple, list)): diff --git a/engine/engine/management/commands/enable_mobile_app.py b/engine/engine/management/commands/enable_mobile_app.py new file mode 100644 index 00000000..01208147 --- /dev/null +++ b/engine/engine/management/commands/enable_mobile_app.py @@ -0,0 +1,21 @@ +from django.core.management.base import BaseCommand, CommandError + +from apps.base.models.dynamic_setting import DynamicSetting +from apps.user_management.models.organization import Organization + + +class Command(BaseCommand): + note = "Note: you will also need to set the appropriate environment variables in your ./dev/.env.dev file." + help = f"Handles the database portion of enabling the mobile app related features. {note}" + + def handle(self, *args, **options): + org = Organization.objects.first() + + if not org: + raise CommandError("No organization exists. Have you enabled, and configured, the plugin?") + + DynamicSetting.objects.update_or_create( + name="mobile_app_settings", defaults={"json_value": {"org_ids": [org.pk]}} + ) + + self.stdout.write(self.style.SUCCESS(f"Mobile app successfully enabled.")) diff --git a/engine/requirements.txt b/engine/requirements.txt index 5e41b01d..3e45d9a4 100644 --- a/engine/requirements.txt +++ b/engine/requirements.txt @@ -33,12 +33,11 @@ django-log-request-id==1.6.0 django-polymorphic==3.0.0 django-rest-polymorphic==0.1.9 pre-commit==2.15.0 -https://github.com/iskhakov/django-push-notifications/archive/refs/tags/3.0.0-fix-migration.tar.gz +https://github.com/grafana/django-push-notifications/archive/refs/tags/3.0.0-fix-migration.tar.gz django-mirage-field==1.3.0 django-mysql==4.6.0 PyMySQL==1.0.2 psycopg2-binary==2.9.3 emoji==1.7.0 -apns2==0.7.2 regex==2021.11.2 psutil==5.9.4 diff --git a/engine/settings/base.py b/engine/settings/base.py index 680b8249..e5aac426 100644 --- a/engine/settings/base.py +++ b/engine/settings/base.py @@ -556,12 +556,6 @@ PUSH_NOTIFICATIONS_SETTINGS = { "FCM_POST_URL": os.getenv("FCM_POST_URL", default="https://fcm.googleapis.com/fcm/send"), "USER_MODEL": "user_management.User", "UPDATE_ON_DUPLICATE_REG_ID": True, - # TODO: remove APNS related endpoints after the hackathon app is deprecated - "APNS_AUTH_KEY_PATH": os.environ.get("APNS_AUTH_KEY_PATH", None), - "APNS_TOPIC": os.environ.get("APNS_TOPIC", None), - "APNS_AUTH_KEY_ID": os.environ.get("APNS_AUTH_KEY_ID", None), - "APNS_TEAM_ID": os.environ.get("APNS_TEAM_ID", None), - "APNS_USE_SANDBOX": getenv_boolean("APNS_USE_SANDBOX", True), } FCM_RELAY_ENABLED = getenv_boolean("FCM_RELAY_ENABLED", default=False) diff --git a/grafana-plugin/src/components/NewScheduleSelector/NewScheduleSelector.tsx b/grafana-plugin/src/components/NewScheduleSelector/NewScheduleSelector.tsx index fdbce3d4..b9beecb3 100644 --- a/grafana-plugin/src/components/NewScheduleSelector/NewScheduleSelector.tsx +++ b/grafana-plugin/src/components/NewScheduleSelector/NewScheduleSelector.tsx @@ -50,7 +50,7 @@ const NewScheduleSelector: FC = (props) => { - diff --git a/grafana-plugin/src/components/SchedulesFilters_NEW/SchedulesFilters.tsx b/grafana-plugin/src/components/SchedulesFilters_NEW/SchedulesFilters.tsx index 2b7f6a0b..cc6ee209 100644 --- a/grafana-plugin/src/components/SchedulesFilters_NEW/SchedulesFilters.tsx +++ b/grafana-plugin/src/components/SchedulesFilters_NEW/SchedulesFilters.tsx @@ -72,7 +72,7 @@ const SchedulesFilters = (props: SchedulesFiltersProps) => { { label: 'All', value: undefined }, { label: 'Web', - value: ScheduleType.Calendar, + value: ScheduleType.API, }, { label: 'ICal', @@ -80,7 +80,7 @@ const SchedulesFilters = (props: SchedulesFiltersProps) => { }, { label: 'API', - value: ScheduleType.API, + value: ScheduleType.Calendar, }, ]} value={value?.type} diff --git a/grafana-plugin/src/containers/MobileAppVerification/MobileAppVerification.module.scss b/grafana-plugin/src/containers/MobileAppVerification/MobileAppVerification.module.scss index cd4af8f4..94dbd47f 100644 --- a/grafana-plugin/src/containers/MobileAppVerification/MobileAppVerification.module.scss +++ b/grafana-plugin/src/containers/MobileAppVerification/MobileAppVerification.module.scss @@ -39,6 +39,11 @@ opacity: 0.2; } +.qr-code { + background-color: #fff; + margin-bottom: 12px; +} + .qr-loader { position: absolute; z-index: 10; diff --git a/grafana-plugin/src/containers/MobileAppVerification/MobileAppVerification.tsx b/grafana-plugin/src/containers/MobileAppVerification/MobileAppVerification.tsx index ed634a9f..ed8baee3 100644 --- a/grafana-plugin/src/containers/MobileAppVerification/MobileAppVerification.tsx +++ b/grafana-plugin/src/containers/MobileAppVerification/MobileAppVerification.tsx @@ -133,7 +133,7 @@ const MobileAppVerification = observer(({ userPk }: Props) => { Open Grafana IRM mobile application and scan this code to sync it with your account.
- + {isQRBlurry && }
diff --git a/grafana-plugin/src/containers/MobileAppVerification/__snapshots__/MobileAppVerification.test.tsx.snap b/grafana-plugin/src/containers/MobileAppVerification/__snapshots__/MobileAppVerification.test.tsx.snap index e471869c..ac791478 100644 --- a/grafana-plugin/src/containers/MobileAppVerification/__snapshots__/MobileAppVerification.test.tsx.snap +++ b/grafana-plugin/src/containers/MobileAppVerification/__snapshots__/MobileAppVerification.test.tsx.snap @@ -109,7 +109,7 @@ exports[`MobileAppVerification if we disconnect the app, it disconnects and fetc class="u-width-100 u-flex u-flex-center u-position-relative" >
void; id: UserType['pk'] | 'new'; - showMobileAppScreen: boolean; onCreate?: (data: UserType) => void; onUpdate?: () => void; tab?: UserSettingsTab; } -const UserSettings = observer(({ id, showMobileAppScreen, onHide, tab = UserSettingsTab.UserInfo }: UserFormProps) => { +const UserSettings = observer(({ id, onHide, tab = UserSettingsTab.UserInfo }: UserFormProps) => { const store = useStore(); const { userStore, teamStore } = store; @@ -59,7 +60,7 @@ const UserSettings = observer(({ id, showMobileAppScreen, onHide, tab = UserSett !isDesktopOrLaptop, isCurrent && teamStore.currentTeam?.slack_team_identity && !storeUser.slack_user_identity, isCurrent && !storeUser.telegram_configuration, - showMobileAppScreen, + isCurrent && store.hasFeature(AppFeature.MobileApp) && isUserActionAllowed(UserActions.UserSettingsWrite), ]; return ( diff --git a/grafana-plugin/src/models/schedule/schedule.types.ts b/grafana-plugin/src/models/schedule/schedule.types.ts index c38a69eb..8b703377 100644 --- a/grafana-plugin/src/models/schedule/schedule.types.ts +++ b/grafana-plugin/src/models/schedule/schedule.types.ts @@ -6,9 +6,9 @@ import { User } from 'models/user/user.types'; import { UserGroup } from 'models/user_group/user_group.types'; export enum ScheduleType { - 'API', - 'Ical', 'Calendar', + 'Ical', + 'API', } export interface RotationFormLiveParams { diff --git a/grafana-plugin/src/pages/incident/Incident.tsx b/grafana-plugin/src/pages/incident/Incident.tsx index 4be78be1..b155f4c4 100644 --- a/grafana-plugin/src/pages/incident/Incident.tsx +++ b/grafana-plugin/src/pages/incident/Incident.tsx @@ -276,7 +276,7 @@ class IncidentPage extends React.Component {showLinkTo && ( )} diff --git a/grafana-plugin/src/pages/schedules/Schedules.tsx b/grafana-plugin/src/pages/schedules/Schedules.tsx index 699108ae..e0750a76 100644 --- a/grafana-plugin/src/pages/schedules/Schedules.tsx +++ b/grafana-plugin/src/pages/schedules/Schedules.tsx @@ -209,7 +209,7 @@ class SchedulesPage extends React.Component { - if (data.type === ScheduleType.Calendar) { + if (data.type === ScheduleType.API) { LocationHelper.update({ page: 'schedule', id: data.id }, 'partial'); } }; diff --git a/grafana-plugin/src/pages/users/Users.tsx b/grafana-plugin/src/pages/users/Users.tsx index d61fee49..924ad5a0 100644 --- a/grafana-plugin/src/pages/users/Users.tsx +++ b/grafana-plugin/src/pages/users/Users.tsx @@ -21,7 +21,6 @@ import UserSettings from 'containers/UserSettings/UserSettings'; import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl'; import { User as UserType } from 'models/user/user.types'; import { pages } from 'pages'; -import { AppFeature } from 'state/features'; import { PageProps, WithStoreProps } from 'state/types'; import { withMobXProviderContext } from 'state/withStore'; import LocationHelper from 'utils/LocationHelper'; @@ -117,11 +116,7 @@ class Users extends React.Component { render() { const { usersFilters, userPkToEdit, page, errorData } = this.state; - const { - store, - query, - query: { id }, - } = this.props; + const { store, query } = this.props; const { userStore } = store; const columns = [ @@ -162,8 +157,6 @@ class Users extends React.Component { }); const { count, results } = userStore.getSearchResult(); - const showMobileAppScreen: boolean = - id !== undefined && id !== 'me' && id === userStore.currentUserPk && store.hasFeature(AppFeature.MobileApp); return ( @@ -239,13 +232,7 @@ class Users extends React.Component { /> )}
- {userPkToEdit && ( - - )} + {userPkToEdit && } )} diff --git a/grafana-plugin/src/plugin.json b/grafana-plugin/src/plugin.json index 776b809c..22d3f50b 100644 --- a/grafana-plugin/src/plugin.json +++ b/grafana-plugin/src/plugin.json @@ -250,6 +250,7 @@ { "action": "grafana-oncall-app.escalation-chains:read" }, { "action": "grafana-oncall-app.schedules:read" }, + { "action": "grafana-oncall-app.schedules:write" }, { "action": "grafana-oncall-app.schedules:export" }, { "action": "grafana-oncall-app.chatops:read" }, diff --git a/grafana-plugin/src/utils/authorization/index.ts b/grafana-plugin/src/utils/authorization/index.ts index a5709e7e..215f58b2 100644 --- a/grafana-plugin/src/utils/authorization/index.ts +++ b/grafana-plugin/src/utils/authorization/index.ts @@ -122,7 +122,7 @@ export const UserActions: { [action in Actions]: UserAction } = { EscalationChainsWrite: constructAction(Resource.ESCALATION_CHAINS, Action.WRITE, OrgRole.Admin), SchedulesRead: constructAction(Resource.SCHEDULES, Action.READ, OrgRole.Viewer), - SchedulesWrite: constructAction(Resource.SCHEDULES, Action.WRITE, OrgRole.Admin), + SchedulesWrite: constructAction(Resource.SCHEDULES, Action.WRITE, OrgRole.Editor), SchedulesExport: constructAction(Resource.SCHEDULES, Action.WRITE, OrgRole.Editor), ChatOpsRead: constructAction(Resource.CHATOPS, Action.READ, OrgRole.Viewer), diff --git a/grafana-plugin/yarn.lock b/grafana-plugin/yarn.lock index 757e45e4..894b36d8 100644 --- a/grafana-plugin/yarn.lock +++ b/grafana-plugin/yarn.lock @@ -5809,9 +5809,9 @@ decimal.js@^10.2.1: integrity sha512-ic1yEvwT6GuvaYwBLLY6/aFFgjZdySKTE8en/fkU3QICTmRtgtSlFn0u0BXN06InZwtfCelR7j8LRiDI/02iGA== decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" - integrity sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og== + version "0.2.2" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" + integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== dedent@^0.7.0: version "0.7.0" diff --git a/tools/pagerduty-migrator/migrator/__main__.py b/tools/pagerduty-migrator/migrator/__main__.py index c71fc34d..14f73691 100644 --- a/tools/pagerduty-migrator/migrator/__main__.py +++ b/tools/pagerduty-migrator/migrator/__main__.py @@ -34,6 +34,7 @@ from migrator.resources.users import ( def main() -> None: session = APISession(PAGERDUTY_API_TOKEN) + session.timeout = 20 print("▶ Fetching users...") users = session.list_all("users", params={"include[]": "notification_rules"})