Merge branch 'dev' into rares/grafana-faro
This commit is contained in:
commit
acfa730903
35 changed files with 173 additions and 177 deletions
31
CHANGELOG.md
31
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
|
||||
|
||||
|
|
|
|||
8
Makefile
8
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` && \
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
26
dev/add_env_var.sh
Executable file
26
dev/add_env_var.sh
Executable file
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)):
|
||||
|
|
|
|||
21
engine/engine/management/commands/enable_mobile_app.py
Normal file
21
engine/engine/management/commands/enable_mobile_app.py
Normal file
|
|
@ -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."))
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ const NewScheduleSelector: FC<NewScheduleSelectorProps> = (props) => {
|
|||
</VerticalGroup>
|
||||
</HorizontalGroup>
|
||||
<WithPermissionControl userAction={UserActions.SchedulesWrite}>
|
||||
<Button variant="primary" icon="plus" onClick={getCreateScheduleClickHandler(ScheduleType.Calendar)}>
|
||||
<Button variant="primary" icon="plus" onClick={getCreateScheduleClickHandler(ScheduleType.API)}>
|
||||
Create
|
||||
</Button>
|
||||
</WithPermissionControl>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,11 @@
|
|||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.qr-code {
|
||||
background-color: #fff;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.qr-loader {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ const MobileAppVerification = observer(({ userPk }: Props) => {
|
|||
</Text>
|
||||
<Text type="primary">Open Grafana IRM mobile application and scan this code to sync it with your account.</Text>
|
||||
<div className={cx('u-width-100', 'u-flex', 'u-flex-center', 'u-position-relative')}>
|
||||
<QRCode className={cx({ blurry: isQRBlurry })} value={QRCodeValue} />
|
||||
<QRCode className={cx({ 'qr-code': true, blurry: isQRBlurry })} value={QRCodeValue} />
|
||||
{isQRBlurry && <QRLoading />}
|
||||
</div>
|
||||
</VerticalGroup>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
<div
|
||||
class="root root_bordered"
|
||||
class="root qr-code root_bordered"
|
||||
>
|
||||
<svg
|
||||
height="256"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ import { useMediaQuery } from 'react-responsive';
|
|||
|
||||
import { Tabs, TabsContent } from 'containers/UserSettings/parts';
|
||||
import { User as UserType } from 'models/user/user.types';
|
||||
import { AppFeature } from 'state/features';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { isUserActionAllowed, UserActions } from 'utils/authorization';
|
||||
import { BREAKPOINT_TABS } from 'utils/consts';
|
||||
|
||||
import { UserSettingsTab } from './UserSettings.types';
|
||||
|
|
@ -19,13 +21,12 @@ const cx = cn.bind(styles);
|
|||
interface UserFormProps {
|
||||
onHide: () => 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 (
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -276,7 +276,7 @@ class IncidentPage extends React.Component<IncidentPageProps, IncidentPageState>
|
|||
</a>
|
||||
{showLinkTo && (
|
||||
<Button variant="primary" size="sm" icon="link" onClick={this.showAttachIncidentForm}>
|
||||
Attach to another incident
|
||||
Attach to another alert group
|
||||
</Button>
|
||||
)}
|
||||
<PluginLink query={{ page: 'integrations', id: incident.alert_receive_channel.id }}>
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
|
|||
};
|
||||
|
||||
handleCreateSchedule = (data: Schedule) => {
|
||||
if (data.type === ScheduleType.Calendar) {
|
||||
if (data.type === ScheduleType.API) {
|
||||
LocationHelper.update({ page: 'schedule', id: data.id }, 'partial');
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<UsersProps, UsersState> {
|
|||
|
||||
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<UsersProps, UsersState> {
|
|||
});
|
||||
|
||||
const { count, results } = userStore.getSearchResult();
|
||||
const showMobileAppScreen: boolean =
|
||||
id !== undefined && id !== 'me' && id === userStore.currentUserPk && store.hasFeature(AppFeature.MobileApp);
|
||||
|
||||
return (
|
||||
<PluginPage pageNav={pages['users'].getPageNav()}>
|
||||
|
|
@ -239,13 +232,7 @@ class Users extends React.Component<UsersProps, UsersState> {
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
{userPkToEdit && (
|
||||
<UserSettings
|
||||
id={userPkToEdit}
|
||||
onHide={this.handleHideUserSettings}
|
||||
showMobileAppScreen={showMobileAppScreen}
|
||||
/>
|
||||
)}
|
||||
{userPkToEdit && <UserSettings id={userPkToEdit} onHide={this.handleHideUserSettings} />}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue