address Google OAuth2 issues where user didn't grant us the https://www.googleapis.com/auth/calendar.events.readonly scope (#4802)
# What this PR does Follow up PR to https://github.com/grafana/oncall/pull/4792 Basically if when communicating with Google Calendar's API we encounter an HTTP 403, or the Google client throws a `google.auth.exceptions.RefreshError` this means one of three things: 1. the refresh token we have persisted for the user is missing the `https://www.googleapis.com/auth/calendar.events.readonly` scope (HTTP 403) 2. the Google user has been deleted (`google.auth.exceptions.RefreshError`) 3. the refresh token has expired (`google.auth.exceptions.RefreshError`) To prevent scenario 1 above from happening in the future we now will check that the token has been granted the required scopes. If the user doesn't grant us all the necessary scopes, we will show them an error message in the UI: https://www.loom.com/share/0055ef03192b4154b894c2221cecbd5f For tokens that were granted prior to this PR and which are missing the required scope, we will show the user a dismissible warning banner in the UI letting them know that they will need to reconnect their account and grant us the missing permissions (see [this second demo video](https://www.loom.com/share/bf2ee8b840864a64893165370a892bcd) showing this). ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] Added the relevant release notes label (see labels prefixed w/ `release:`). These labels dictate how your PR will show up in the autogenerated release notes. --------- Co-authored-by: Dominik <dominik.broj@grafana.com>
This commit is contained in:
parent
29bd42c0b1
commit
eb777f5415
21 changed files with 670 additions and 142 deletions
|
|
@ -222,12 +222,14 @@ class UserSerializer(ListUserSerializer):
|
|||
class CurrentUserSerializer(UserSerializer):
|
||||
rbac_permissions = UserPermissionSerializer(read_only=True, many=True, source="permissions")
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
class Meta(UserSerializer.Meta):
|
||||
fields = UserSerializer.Meta.fields + [
|
||||
"rbac_permissions",
|
||||
"google_oauth2_token_is_missing_scopes",
|
||||
]
|
||||
read_only_fields = UserSerializer.Meta.read_only_fields + [
|
||||
"google_oauth2_token_is_missing_scopes",
|
||||
]
|
||||
read_only_fields = UserSerializer.Meta.read_only_fields
|
||||
|
||||
|
||||
class UserHiddenFieldsSerializer(ListUserSerializer):
|
||||
|
|
|
|||
|
|
@ -195,3 +195,30 @@ def test_google_complete_auth_redirect_ok(
|
|||
|
||||
assert response.status_code == status.HTTP_302_FOUND
|
||||
assert response.url == "/a/grafana-oncall-app/users/me"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_complete_google_auth_redirect_error(
|
||||
make_organization,
|
||||
make_user_for_organization,
|
||||
make_google_oauth2_token_for_user,
|
||||
):
|
||||
organization = make_organization()
|
||||
admin = make_user_for_organization(organization)
|
||||
_, google_oauth2_token = make_google_oauth2_token_for_user(admin)
|
||||
|
||||
client = APIClient()
|
||||
url = (
|
||||
reverse("api-internal:complete-social-auth", kwargs={"backend": "google-oauth2"})
|
||||
+ f"?state={google_oauth2_token}"
|
||||
)
|
||||
|
||||
def _custom_do_complete(backend, *args, **kwargs):
|
||||
backend.strategy.session[REDIRECT_FIELD_NAME] = "some-url"
|
||||
return HttpResponse(status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
with patch("apps.api.views.auth.do_complete", side_effect=_custom_do_complete):
|
||||
response = client.get(url)
|
||||
|
||||
assert response.status_code == status.HTTP_302_FOUND
|
||||
assert response.url == "some-url"
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ def test_current_user(
|
|||
"avatar_full": user.avatar_full_url,
|
||||
"has_google_oauth2_connected": False,
|
||||
"google_calendar_settings": None,
|
||||
"google_oauth2_token_is_missing_scopes": False,
|
||||
}
|
||||
|
||||
response = client.get(url, format="json", **make_user_auth_headers(user, token))
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ class CachedSchedulesContextMixin:
|
|||
return context
|
||||
|
||||
|
||||
@extend_schema(responses={status.HTTP_200_OK: CurrentUserSerializer})
|
||||
class CurrentUserView(APIView, CachedSchedulesContextMixin):
|
||||
authentication_classes = (MobileAppAuthTokenAuthentication, PluginAuthentication)
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
|
|
|||
|
|
@ -104,25 +104,44 @@ class GoogleCalendarAPIClient:
|
|||
)
|
||||
except HttpError as e:
|
||||
if e.status_code == 403:
|
||||
# this scenario can be encountered when, for some reason, the OAuth2 token that we have
|
||||
# this scenario can be encountered when, the OAuth2 token that we have
|
||||
# does not contain the https://www.googleapis.com/auth/calendar.events.readonly scope
|
||||
# example error:
|
||||
# <HttpError 403 when requesting https://www.googleapis.com/calendar/v3/calendars/primary/events?timeMin=2024-08-08T14%3A00%3A00%2B0000&timeMax=2024-09-07T14%3A00%3A00%2B0000&maxResults=250&singleEvents=true&orderBy=startTime&eventTypes=outOfOffice&alt=json returned "Request had insufficient authentication scopes.". Details: "[{'message': 'Insufficient Permission', 'domain': 'global', 'reason': 'insufficientPermissions'}]"> # noqa: E501
|
||||
#
|
||||
# this should really only occur for tokens granted prior to this commit (which wrote this comment).
|
||||
# Before then we didn't handle the scenario where the Google oauth consent screen could potentially
|
||||
# have checkboxes and users would have to actively check the checkbox to grant this scope. We now
|
||||
# handle this scenario.
|
||||
#
|
||||
# References
|
||||
# https://jpassing.com/2022/08/01/dealing-with-partial-consent-in-google-oauth-clients/
|
||||
# https://raintank-corp.slack.com/archives/C05AMEGMLCT/p1723556508149689
|
||||
# https://raintank-corp.slack.com/archives/C04JCU51NF8/p1723493330369349
|
||||
logger.error(f"GoogleCalendarAPIClient - HttpError 403 when fetching out of office events: {e}")
|
||||
raise GoogleCalendarUnauthorizedHTTPError(e)
|
||||
|
||||
logger.error(f"GoogleCalendarAPIClient - HttpError when fetching out of office events: {e}")
|
||||
raise GoogleCalendarGenericHTTPError(e)
|
||||
except RefreshError as e:
|
||||
# TODO: come back and solve this properly once we get better logging output
|
||||
# it seems like right now we are seeing RefreshError in two different scenarios:
|
||||
# we see RefreshError in two different scenarios:
|
||||
# 1. RefreshError('invalid_grant: Account has been deleted', {'error': 'invalid_grant', 'error_description': 'Account has been deleted'})
|
||||
# 2. RefreshError('invalid_grant: Token has been expired or revoked.', {'error': 'invalid_grant', 'error_description': 'Token has been expired or revoked.'})
|
||||
#
|
||||
# https://stackoverflow.com/a/49024030/3902555
|
||||
#
|
||||
# in both of these cases the granted token is no longer good and we should delete it
|
||||
|
||||
try:
|
||||
error_details = e.args[1] # should be a dict like in the comment above
|
||||
except IndexError:
|
||||
error_details = None # catch this just in case
|
||||
|
||||
error_description = error_details.get("error_description") if error_details else None
|
||||
|
||||
logger.error(
|
||||
f"GoogleCalendarAPIClient - RefreshError when fetching out of office events: {e} "
|
||||
# NOTE: remove e.args after debugging how to dig into the error details
|
||||
f"args={e.args}"
|
||||
f"error_description={error_description}"
|
||||
)
|
||||
raise GoogleCalendarRefreshError(e)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
from django.conf import settings
|
||||
|
||||
EVENT_SUMMARY_IGNORE_KEYWORD = "#grafana-oncall-ignore"
|
||||
|
||||
GOOGLE_CALENDAR_EVENT_DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S%z"
|
||||
|
|
@ -6,3 +8,4 @@ https://stackoverflow.com/a/17159470/3902555
|
|||
"""
|
||||
|
||||
DAYS_IN_FUTURE_TO_CONSIDER_OUT_OF_OFFICE_EVENTS = 30
|
||||
REQUIRED_OAUTH_SCOPES = settings.SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import logging
|
||||
|
||||
from celery.utils.log import get_task_logger
|
||||
from django.db.models import Q
|
||||
|
||||
from apps.google import constants
|
||||
from apps.google.client import (
|
||||
|
|
@ -39,15 +40,24 @@ def sync_out_of_office_calendar_events_for_user(google_oauth2_user_pk: int) -> N
|
|||
try:
|
||||
out_of_office_events = google_api_client.fetch_out_of_office_events()
|
||||
except GoogleCalendarUnauthorizedHTTPError:
|
||||
# this happens because the user's access token is (somehow) missing the
|
||||
# https://www.googleapis.com/auth/calendar.events.readonly scope
|
||||
# they will need to reconnect their Google account and grant us the necessary scopes, retrying will not help
|
||||
logger.exception(f"Failed to fetch out of office events for user {user_id} due to an unauthorized HTTP error")
|
||||
# TODO: come back and solve this properly once we get better logging output
|
||||
# user.reset_google_oauth2_settings()
|
||||
# this means the user's current token is missing the required scopes, don't delete their token for now
|
||||
# we'll notify them via the plugin UI and ask them to reauth and grant us the missing scope
|
||||
logger.warning(
|
||||
f"Failed to fetch out of office events for user {user_id} due to missing required scopes. "
|
||||
"Safe to skip for now"
|
||||
)
|
||||
return
|
||||
except (GoogleCalendarRefreshError, GoogleCalendarGenericHTTPError):
|
||||
logger.exception(f"Failed to fetch out of office events for user {user_id}")
|
||||
except GoogleCalendarRefreshError:
|
||||
# in this scenarios there's really not much we can do with the refresh/access token that we
|
||||
# have available. The user will need to re-connect with Google so lets delete their persisted token
|
||||
|
||||
logger.exception(
|
||||
f"Failed to fetch out of office events for user {user_id} due to an invalid access and/or refresh token"
|
||||
)
|
||||
user.reset_google_oauth2_settings()
|
||||
return
|
||||
except GoogleCalendarGenericHTTPError:
|
||||
logger.exception(f"Failed to fetch out of office events for user {user_id} due to a generic HTTP error")
|
||||
return
|
||||
|
||||
for out_of_office_event in out_of_office_events:
|
||||
|
|
@ -118,5 +128,16 @@ def sync_out_of_office_calendar_events_for_user(google_oauth2_user_pk: int) -> N
|
|||
|
||||
@shared_dedicated_queue_retry_task(autoretry_for=(Exception,), retry_backoff=True)
|
||||
def sync_out_of_office_calendar_events_for_all_users() -> None:
|
||||
for google_oauth2_user in GoogleOAuth2User.objects.filter(user__organization__deleted_at__isnull=True):
|
||||
# some existing tokens may not have all the required scopes, lets skip these
|
||||
tokens_containing_required_scopes = GoogleOAuth2User.objects.filter(
|
||||
*[Q(oauth_scope__contains=scope) for scope in constants.REQUIRED_OAUTH_SCOPES],
|
||||
user__organization__deleted_at__isnull=True,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Google OAuth2 tokens with the required scopes - "
|
||||
f"{tokens_containing_required_scopes.count()}/{GoogleOAuth2User.objects.count()}"
|
||||
)
|
||||
|
||||
for google_oauth2_user in tokens_containing_required_scopes:
|
||||
sync_out_of_office_calendar_events_for_user.apply_async(args=(google_oauth2_user.pk,))
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import datetime
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import call, patch
|
||||
|
||||
import pytest
|
||||
from django.utils import timezone
|
||||
from google.auth.exceptions import RefreshError
|
||||
from googleapiclient.errors import HttpError
|
||||
|
||||
from apps.google import constants, tasks
|
||||
from apps.google.models import GoogleOAuth2User
|
||||
from apps.schedules.models import CustomOnCallShift, OnCallScheduleWeb, ShiftSwapRequest
|
||||
|
||||
|
||||
|
|
@ -148,19 +150,61 @@ class MockResponse:
|
|||
|
||||
|
||||
@patch("apps.google.client.build")
|
||||
@pytest.mark.parametrize(
|
||||
"ErrorClass,http_status,should_reset_user_google_oauth2_settings,task_should_raise_exception",
|
||||
[
|
||||
(RefreshError, None, True, False),
|
||||
(HttpError, 401, False, False),
|
||||
(HttpError, 500, False, False),
|
||||
(HttpError, 403, False, False),
|
||||
(Exception, None, False, True),
|
||||
],
|
||||
)
|
||||
@pytest.mark.django_db
|
||||
def test_sync_out_of_office_calendar_events_for_user_httperror(mock_google_api_client_build, test_setup):
|
||||
mock_response = MockResponse(reason="forbidden", status=403)
|
||||
mock_google_api_client_build.return_value.events.return_value.list.return_value.execute.side_effect = HttpError(
|
||||
resp=mock_response, content=b"error"
|
||||
)
|
||||
def test_sync_out_of_office_calendar_events_for_user_error_scenarios(
|
||||
mock_google_api_client_build,
|
||||
ErrorClass,
|
||||
http_status,
|
||||
should_reset_user_google_oauth2_settings,
|
||||
task_should_raise_exception,
|
||||
test_setup,
|
||||
):
|
||||
if ErrorClass == HttpError:
|
||||
mock_response = MockResponse(reason="forbidden", status=http_status)
|
||||
exception = ErrorClass(resp=mock_response, content=b"error")
|
||||
elif ErrorClass == RefreshError:
|
||||
exception = ErrorClass(
|
||||
"invalid_grant: Token has been expired or revoked.",
|
||||
{"error": "invalid_grant", "error_description": "Token has been expired or revoked."},
|
||||
)
|
||||
else:
|
||||
exception = ErrorClass()
|
||||
|
||||
mock_google_api_client_build.return_value.events.return_value.list.return_value.execute.side_effect = exception
|
||||
|
||||
google_oauth2_user, schedule = test_setup([])
|
||||
user = google_oauth2_user.user
|
||||
|
||||
tasks.sync_out_of_office_calendar_events_for_user(google_oauth2_user.pk)
|
||||
assert user.google_calendar_settings is not None
|
||||
|
||||
assert ShiftSwapRequest.objects.filter(beneficiary=user, schedule=schedule).count() == 0
|
||||
if task_should_raise_exception:
|
||||
with pytest.raises(ErrorClass):
|
||||
tasks.sync_out_of_office_calendar_events_for_user(google_oauth2_user.pk)
|
||||
else:
|
||||
tasks.sync_out_of_office_calendar_events_for_user(google_oauth2_user.pk)
|
||||
|
||||
assert ShiftSwapRequest.objects.filter(beneficiary=user, schedule=schedule).count() == 0
|
||||
|
||||
user.refresh_from_db()
|
||||
|
||||
google_oauth2_user_count = GoogleOAuth2User.objects.filter(user=user).count()
|
||||
|
||||
if should_reset_user_google_oauth2_settings:
|
||||
assert user.google_calendar_settings is None
|
||||
assert google_oauth2_user_count == 0
|
||||
else:
|
||||
assert user.google_calendar_settings is not None
|
||||
assert google_oauth2_user_count == 1
|
||||
|
||||
|
||||
@patch("apps.google.client.build")
|
||||
|
|
@ -374,14 +418,50 @@ def test_sync_out_of_office_calendar_events_for_user_preexisting_shift_swap_requ
|
|||
assert _fetch_shift_swap_requests().count() == 1
|
||||
|
||||
|
||||
REQUIRED_SCOPE_1 = "https://www.googleapis.com/test/foo"
|
||||
REQUIRED_SCOPE_2 = "https://www.googleapis.com/test/bar"
|
||||
|
||||
|
||||
@patch("apps.google.tasks.constants.REQUIRED_OAUTH_SCOPES", [REQUIRED_SCOPE_1, REQUIRED_SCOPE_2])
|
||||
@patch("apps.google.tasks.sync_out_of_office_calendar_events_for_user.apply_async")
|
||||
@pytest.mark.django_db
|
||||
def test_sync_out_of_office_calendar_events_for_all_users(
|
||||
def test_sync_out_of_office_calendar_events_for_all_users_only_called_for_tokens_having_all_required_scopes(
|
||||
mock_sync_out_of_office_calendar_events_for_user,
|
||||
make_organization_and_user,
|
||||
make_user_for_organization,
|
||||
make_google_oauth2_user_for_user,
|
||||
):
|
||||
organization, user1 = make_organization_and_user()
|
||||
user2 = make_user_for_organization(organization)
|
||||
user3 = make_user_for_organization(organization)
|
||||
|
||||
missing_a_scope = f"{REQUIRED_SCOPE_1} foo_bar"
|
||||
has_all_scopes = f"{REQUIRED_SCOPE_1} {REQUIRED_SCOPE_2} foo_bar"
|
||||
|
||||
_ = make_google_oauth2_user_for_user(user1, oauth_scope=missing_a_scope)
|
||||
user2_google_oauth2_user = make_google_oauth2_user_for_user(user2, oauth_scope=has_all_scopes)
|
||||
user3_google_oauth2_user = make_google_oauth2_user_for_user(user3, oauth_scope=has_all_scopes)
|
||||
|
||||
tasks.sync_out_of_office_calendar_events_for_all_users()
|
||||
|
||||
assert len(mock_sync_out_of_office_calendar_events_for_user.mock_calls) == 2
|
||||
mock_sync_out_of_office_calendar_events_for_user.assert_has_calls(
|
||||
[
|
||||
call(args=(user2_google_oauth2_user.pk,)),
|
||||
call(args=(user3_google_oauth2_user.pk,)),
|
||||
],
|
||||
any_order=True,
|
||||
)
|
||||
|
||||
|
||||
@patch("apps.google.tasks.sync_out_of_office_calendar_events_for_user.apply_async")
|
||||
@pytest.mark.django_db
|
||||
def test_sync_out_of_office_calendar_events_for_all_users_filters_out_users_from_deleted_organizations(
|
||||
mock_sync_out_of_office_calendar_events_for_user,
|
||||
make_organization_and_user,
|
||||
make_google_oauth2_user_for_user,
|
||||
):
|
||||
organization, user = make_organization_and_user()
|
||||
_, user = make_organization_and_user()
|
||||
google_oauth2_user = make_google_oauth2_user_for_user(user)
|
||||
|
||||
deleted_organization, deleted_user = make_organization_and_user()
|
||||
|
|
|
|||
18
engine/apps/google/tests/test_utils.py
Normal file
18
engine/apps/google/tests/test_utils.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import pytest
|
||||
|
||||
from apps.google import utils
|
||||
|
||||
SCOPES_ALWAYS_GRANTED = (
|
||||
"openid https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"granted_scopes,expected",
|
||||
(
|
||||
(SCOPES_ALWAYS_GRANTED, False),
|
||||
(f"{SCOPES_ALWAYS_GRANTED} https://www.googleapis.com/auth/calendar.events.readonly", True),
|
||||
),
|
||||
)
|
||||
def test_user_granted_all_required_scopes(granted_scopes, expected):
|
||||
assert utils.user_granted_all_required_scopes(granted_scopes) == expected
|
||||
|
|
@ -3,6 +3,14 @@ import datetime
|
|||
from apps.google import constants
|
||||
|
||||
|
||||
def user_granted_all_required_scopes(user_granted_scopes: str) -> bool:
|
||||
"""
|
||||
`user_granted_scopes` should be a space-separated string of scopes
|
||||
"""
|
||||
granted_scopes = user_granted_scopes.split(" ")
|
||||
return all(scope in granted_scopes for scope in constants.REQUIRED_OAUTH_SCOPES)
|
||||
|
||||
|
||||
def datetime_strftime(dt: datetime.datetime) -> str:
|
||||
return dt.strftime(constants.GOOGLE_CALENDAR_EVENT_DATETIME_FORMAT)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,2 +1,5 @@
|
|||
class InstallMultiRegionSlackException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
GOOGLE_AUTH_MISSING_GRANTED_SCOPE_ERROR = "missing_granted_scope"
|
||||
|
|
|
|||
|
|
@ -1,24 +1,51 @@
|
|||
import logging
|
||||
import typing
|
||||
from urllib.parse import urljoin
|
||||
|
||||
import requests
|
||||
from rest_framework.response import Response
|
||||
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||
from django.http import HttpResponse
|
||||
from rest_framework import status
|
||||
from social_core.backends.base import BaseAuth
|
||||
|
||||
from apps.user_management.models import User
|
||||
from apps.google.utils import user_granted_all_required_scopes
|
||||
from apps.social_auth.exceptions import GOOGLE_AUTH_MISSING_GRANTED_SCOPE_ERROR
|
||||
from apps.social_auth.types import GoogleOauth2Response
|
||||
from apps.user_management.models import Organization, User
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def persist_access_and_refresh_tokens(backend: typing.Type[BaseAuth], response: Response, user: User, *args, **kwargs):
|
||||
"""
|
||||
TODO: what happens if `refresh_token` is not present? what are the scenarios that lead to this?
|
||||
def connect_user_to_google(
|
||||
strategy,
|
||||
response: GoogleOauth2Response,
|
||||
user: User,
|
||||
organization: Organization,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
granted_scopes = response.get("scope", "")
|
||||
|
||||
NOTE: I think it's only included when the user initially grants access to our Google OAuth2 app
|
||||
on subsequent logins, the refresh_token is not included in the response, only access_token.. what to do here?
|
||||
if not user_granted_all_required_scopes(granted_scopes):
|
||||
logger.warning(
|
||||
f"User {user.pk} did not grant all required scopes, redirecting w/ error message "
|
||||
f"granted_scopes={granted_scopes}"
|
||||
)
|
||||
|
||||
base_url_to_redirect = urljoin(organization.grafana_url, "/a/grafana-oncall-app/users/me")
|
||||
strategy.session[
|
||||
REDIRECT_FIELD_NAME
|
||||
] = f"{base_url_to_redirect}?google_error={GOOGLE_AUTH_MISSING_GRANTED_SCOPE_ERROR}"
|
||||
|
||||
return HttpResponse(status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# at this point everything is correct and we can persist the Google OAuth2 token + generate any other relevant
|
||||
# config for the user
|
||||
#
|
||||
# be sure to clear any pre-existing sessions, in case the user previously enecountered errors we want
|
||||
# to be sure to clear these so they do not see them again
|
||||
strategy.session.flush()
|
||||
|
||||
https://medium.com/starthinker/google-oauth-2-0-access-token-and-refresh-token-explained-cccf2fc0a6d9
|
||||
"""
|
||||
user.save_google_oauth2_settings(response)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ from apps.api.permissions import (
|
|||
RBACPermission,
|
||||
user_is_authorized,
|
||||
)
|
||||
from apps.google import utils as google_utils
|
||||
from apps.google.models import GoogleOAuth2User
|
||||
from apps.schedules.tasks import drop_cached_ical_for_custom_events_for_organization
|
||||
from apps.user_management.types import AlertGroupTableColumn, GoogleCalendarSettings
|
||||
|
|
@ -188,6 +189,12 @@ class User(models.Model):
|
|||
except ObjectDoesNotExist:
|
||||
return False
|
||||
|
||||
@property
|
||||
def google_oauth2_token_is_missing_scopes(self) -> bool:
|
||||
if not self.has_google_oauth2_connected:
|
||||
return False
|
||||
return not google_utils.user_granted_all_required_scopes(self.google_oauth2_user.oauth_scope)
|
||||
|
||||
@property
|
||||
def avatar_full_url(self):
|
||||
return urljoin(self.organization.grafana_url, self.avatar_url)
|
||||
|
|
@ -386,7 +393,7 @@ class User(models.Model):
|
|||
logger.info(
|
||||
f"Saving Google OAuth2 settings for user {self.pk} "
|
||||
f"sub={google_oauth2_response.get('sub')} "
|
||||
f"oauth_scope={google_oauth2_response.get('oauth_scope')}"
|
||||
f"oauth_scope={google_oauth2_response.get('scope')}"
|
||||
)
|
||||
|
||||
_, created = GoogleOAuth2User.objects.update_or_create(
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import pytest
|
|||
from django.utils import timezone
|
||||
|
||||
from apps.api.permissions import LegacyAccessControlRole
|
||||
from apps.google import constants as google_constants
|
||||
from apps.google.models import GoogleOAuth2User
|
||||
from apps.user_management.models import User
|
||||
|
||||
|
|
@ -120,6 +121,39 @@ def test_has_google_oauth2_connected(make_organization_and_user, make_google_oau
|
|||
assert user.has_google_oauth2_connected is True
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_google_oauth2_token_is_missing_scopes(make_organization_and_user, make_google_oauth2_user_for_user):
|
||||
initial_granted_scope = "foo bar baz"
|
||||
initial_oauth_response = {
|
||||
"access_token": "access",
|
||||
"refresh_token": "refresh",
|
||||
"sub": "google_user_id",
|
||||
"scope": initial_granted_scope,
|
||||
}
|
||||
|
||||
_, user = make_organization_and_user()
|
||||
|
||||
# false because the user hasn't yet connected their google account
|
||||
assert user.google_oauth2_token_is_missing_scopes is False
|
||||
|
||||
user.save_google_oauth2_settings(initial_oauth_response)
|
||||
user.refresh_from_db()
|
||||
|
||||
# true because we're missing a granted scope
|
||||
assert user.google_oauth2_token_is_missing_scopes is True
|
||||
|
||||
user.save_google_oauth2_settings(
|
||||
{
|
||||
**initial_oauth_response,
|
||||
"scope": f"{initial_granted_scope} {' '.join(google_constants.REQUIRED_OAUTH_SCOPES)}",
|
||||
}
|
||||
)
|
||||
user.refresh_from_db()
|
||||
|
||||
# False because we now have all the required scopes
|
||||
assert user.google_oauth2_token_is_missing_scopes is False
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_save_google_oauth2_settings(make_organization_and_user):
|
||||
oauth_response = {
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ from apps.base.tests.factories import (
|
|||
UserNotificationPolicyLogRecordFactory,
|
||||
)
|
||||
from apps.email.tests.factories import EmailMessageFactory
|
||||
from apps.google import constants as google_constants
|
||||
from apps.google.tests.factories import GoogleOAuth2UserFactory
|
||||
from apps.heartbeat.tests.factories import IntegrationHeartBeatFactory
|
||||
from apps.labels.tests.factories import (
|
||||
|
|
@ -1075,8 +1076,9 @@ def make_webhook_label_association(make_label_key_and_value):
|
|||
|
||||
@pytest.fixture
|
||||
def make_google_oauth2_user_for_user():
|
||||
def _make_google_oauth2_user_for_user(user):
|
||||
return GoogleOAuth2UserFactory(user=user)
|
||||
def _make_google_oauth2_user_for_user(user, **kwargs):
|
||||
oauth_scope = kwargs.pop("oauth_scope", " ".join(google_constants.REQUIRED_OAUTH_SCOPES))
|
||||
return GoogleOAuth2UserFactory(user=user, oauth_scope=oauth_scope, **kwargs)
|
||||
|
||||
return _make_google_oauth2_user_for_user
|
||||
|
||||
|
|
|
|||
|
|
@ -346,6 +346,8 @@ SPECTACULAR_INCLUDED_PATHS = [
|
|||
"/features",
|
||||
"/alertgroups",
|
||||
"/alert_receive_channels",
|
||||
# current user endpoint 👇, without trailing slash we pick-up /user_group endpoints, which we don't want for now
|
||||
"/user/",
|
||||
"/users",
|
||||
"/labels",
|
||||
# social auth routes
|
||||
|
|
@ -740,7 +742,7 @@ SOCIAL_AUTH_PIPELINE = (
|
|||
|
||||
SOCIAL_AUTH_GOOGLE_OAUTH2_PIPELINE = (
|
||||
"apps.social_auth.pipeline.common.set_user_and_organization_from_request",
|
||||
"apps.social_auth.pipeline.google.persist_access_and_refresh_tokens",
|
||||
"apps.social_auth.pipeline.google.connect_user_to_google",
|
||||
)
|
||||
|
||||
SOCIAL_AUTH_GOOGLE_OAUTH2_DISCONNECT_PIPELINE = (
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@ import React, { useCallback, useEffect, useState } from 'react';
|
|||
import { Alert } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { sanitize } from 'dompurify';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import { PluginLink } from 'components/PluginLink/PluginLink';
|
||||
import { getSlackMessage } from 'containers/DefaultPageLayout/DefaultPageLayout.helpers';
|
||||
import { SlackError } from 'containers/DefaultPageLayout/DefaultPageLayout.types';
|
||||
import { getIfChatOpsConnected } from 'containers/DefaultPageLayout/helper';
|
||||
import { UserHelper } from 'models/user/user.helpers';
|
||||
import { isTopNavbar } from 'plugin/GrafanaPluginRootPage.helpers';
|
||||
import { AppFeature } from 'state/features';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
|
@ -24,9 +26,10 @@ const cx = cn.bind(styles);
|
|||
|
||||
enum AlertID {
|
||||
CONNECTIVITY_WARNING = 'Connectivity Warning',
|
||||
USER_GOOGLE_OAUTH2_TOKEN_MISSING_SCOPES = 'User Google OAuth2 token is missing scopes',
|
||||
}
|
||||
|
||||
export const Alerts = function () {
|
||||
export const Alerts = observer(() => {
|
||||
const queryParams = useQueryParams();
|
||||
const [showSlackInstallAlert, setShowSlackInstallAlert] = useState<SlackError | undefined>();
|
||||
|
||||
|
|
@ -40,7 +43,7 @@ export const Alerts = function () {
|
|||
if (queryParams.get('slack_error')) {
|
||||
setShowSlackInstallAlert(queryParams.get('slack_error') as SlackError);
|
||||
|
||||
LocationHelper.update({ slack_error: undefined }, 'replace');
|
||||
LocationHelper.update({ slack_error: undefined }, 'partial');
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
|
@ -54,17 +57,24 @@ export const Alerts = function () {
|
|||
|
||||
const store = useStore();
|
||||
const {
|
||||
userStore: { currentUser },
|
||||
userStore: { currentUser, currentUserPk },
|
||||
organizationStore: { currentOrganization },
|
||||
} = store;
|
||||
|
||||
const versionMismatchLocalStorageId = `version_mismatch_${store.backendVersion}_${plugin?.version}`;
|
||||
const isChatOpsConnected = getIfChatOpsConnected(currentUser);
|
||||
const isPhoneVerified = currentUser?.cloud_connection_status === 3 || currentUser?.verified_phone_number;
|
||||
|
||||
const isDefaultNotificationsSet = currentUser?.notification_chain_verbal.default;
|
||||
const isImportantNotificationsSet = currentUser?.notification_chain_verbal.important;
|
||||
|
||||
if (!showSlackInstallAlert && !showBannerTeam() && !showMismatchWarning() && !showChannelWarnings()) {
|
||||
if (
|
||||
!showSlackInstallAlert &&
|
||||
!showCurrentUserGoogleOAuth2TokenIsMissingScopes() &&
|
||||
!showBannerTeam() &&
|
||||
!showMismatchWarning() &&
|
||||
!showChannelWarnings()
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
|
|
@ -79,6 +89,22 @@ export const Alerts = function () {
|
|||
{getSlackMessage(showSlackInstallAlert, currentOrganization, store.hasFeature(AppFeature.LiveSettings))}
|
||||
</Alert>
|
||||
)}
|
||||
{showCurrentUserGoogleOAuth2TokenIsMissingScopes() && (
|
||||
<Alert
|
||||
className={cx('alert')}
|
||||
severity="warning"
|
||||
title="User Google OAuth2 token is missing scopes"
|
||||
onRemove={getRemoveAlertHandler(AlertID.USER_GOOGLE_OAUTH2_TOKEN_MISSING_SCOPES)}
|
||||
>
|
||||
Your Google OAuth2 token is missing some required permissions (you may have forgotten to check the necessary
|
||||
checkboxes when connecting your Google account). To rectify this, please grant Grafana OnCall these
|
||||
permissions by clicking{' '}
|
||||
<a onClick={UserHelper.handleConnectGoogle} className={cx('instructions-link')}>
|
||||
here
|
||||
</a>{' '}
|
||||
and re-connecting your Google account.
|
||||
</Alert>
|
||||
)}
|
||||
{showBannerTeam() && (
|
||||
<Alert
|
||||
className={cx('alert')}
|
||||
|
|
@ -98,7 +124,7 @@ export const Alerts = function () {
|
|||
className={cx('alert')}
|
||||
severity="warning"
|
||||
title={'Version mismatch!'}
|
||||
onRemove={getRemoveAlertHandler(`version_mismatch_${store.backendVersion}_${plugin?.version}`)}
|
||||
onRemove={getRemoveAlertHandler(versionMismatchLocalStorageId)}
|
||||
>
|
||||
Please make sure you have the same versions of the Grafana OnCall plugin and the Grafana OnCall engine,
|
||||
otherwise there could be issues with your Grafana OnCall installation!
|
||||
|
|
@ -151,7 +177,7 @@ export const Alerts = function () {
|
|||
store.backendVersion &&
|
||||
plugin?.version &&
|
||||
store.backendVersion !== plugin?.version &&
|
||||
!getItem(`version_mismatch_${store.backendVersion}_${plugin?.version}`)
|
||||
!getItem(versionMismatchLocalStorageId)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -164,4 +190,24 @@ export const Alerts = function () {
|
|||
!getItem(AlertID.CONNECTIVITY_WARNING)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* tbh we don't really need the `currentUserPk` reference here...
|
||||
* the only reason why it's here is to appease mobx. Without this reference, the `@computed` property
|
||||
* on `UserStore.currentUser` doesn't recalculate and will just be stuck on returning `undefined`..
|
||||
*
|
||||
* If we dereference `currentUserPk` here, even if we don't use it.. things just seem to work
|
||||
* (what is this `mobx` wizardry?)
|
||||
*
|
||||
* Seems to be related to this https://stackoverflow.com/questions/77724466/mobx-computed-not-updating
|
||||
*/
|
||||
function showCurrentUserGoogleOAuth2TokenIsMissingScopes(): boolean {
|
||||
return Boolean(
|
||||
currentUserPk &&
|
||||
currentUser &&
|
||||
currentUser.has_google_oauth2_connected &&
|
||||
currentUser.google_oauth2_token_is_missing_scopes &&
|
||||
!getItem(AlertID.USER_GOOGLE_OAUTH2_TOKEN_MISSING_SCOPES)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,3 +8,18 @@
|
|||
width: calc(100% - var(--spacing) * 2); /* allow lateral spacing */
|
||||
max-width: 1100px;
|
||||
}
|
||||
|
||||
.alerts-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 10px;
|
||||
gap: 10px;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.alert {
|
||||
margin: 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
|
||||
import { HorizontalGroup, Modal } from '@grafana/ui';
|
||||
import { Alert, HorizontalGroup, Modal } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
import { useMediaQuery } from 'react-responsive';
|
||||
|
|
@ -10,7 +10,9 @@ import { Tabs, TabsContent } from 'containers/UserSettings/parts/UserSettingsPar
|
|||
import { ApiSchemas } from 'network/oncall-api/api.types';
|
||||
import { AppFeature } from 'state/features';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { LocationHelper } from 'utils/LocationHelper';
|
||||
import { BREAKPOINT_TABS } from 'utils/consts';
|
||||
import { useQueryParams } from 'utils/hooks';
|
||||
|
||||
import { UserSettingsTab } from './UserSettings.types';
|
||||
|
||||
|
|
@ -18,6 +20,10 @@ import styles from './UserSettings.module.css';
|
|||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
enum GoogleError {
|
||||
MISSING_GRANTED_SCOPE = 'missing_granted_scope',
|
||||
}
|
||||
|
||||
interface UserFormProps {
|
||||
onHide: () => void;
|
||||
id: ApiSchemas['User']['pk'] | 'new';
|
||||
|
|
@ -26,6 +32,53 @@ interface UserFormProps {
|
|||
tab?: UserSettingsTab;
|
||||
}
|
||||
|
||||
function getGoogleMessage(googleError: GoogleError) {
|
||||
if (googleError === GoogleError.MISSING_GRANTED_SCOPE) {
|
||||
return (
|
||||
<>
|
||||
Couldn't connect your Google account. You did not grant Grafana OnCall the necessary permissions. Please retry
|
||||
and be sure to check any checkboxes which grant Grafana OnCall read access to your calendar events.
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return <>Couldn't connect your Google account.</>;
|
||||
}
|
||||
|
||||
const UserAlerts: React.FC = () => {
|
||||
const queryParams = useQueryParams();
|
||||
const [showGoogleConnectAlert, setShowGoogleConnectAlert] = useState<GoogleError | undefined>();
|
||||
|
||||
const handleCloseGoogleAlert = useCallback(() => {
|
||||
setShowGoogleConnectAlert(undefined);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (queryParams.get('google_error')) {
|
||||
setShowGoogleConnectAlert(queryParams.get('google_error') as GoogleError);
|
||||
|
||||
LocationHelper.update({ google_error: undefined }, 'partial');
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!showGoogleConnectAlert) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cx('alerts-container')}>
|
||||
<Alert
|
||||
className={cx('alert')}
|
||||
onRemove={handleCloseGoogleAlert}
|
||||
severity="error"
|
||||
title="Google integration error"
|
||||
>
|
||||
{getGoogleMessage(showGoogleConnectAlert)}
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const UserSettings = observer(({ id, onHide, tab = UserSettingsTab.UserInfo }: UserFormProps) => {
|
||||
const store = useStore();
|
||||
const { userStore, organizationStore } = store;
|
||||
|
|
@ -74,6 +127,7 @@ export const UserSettings = observer(({ id, onHide, tab = UserSettingsTab.UserIn
|
|||
return (
|
||||
<>
|
||||
<Modal title={title} className={cx('modal', 'modal-wide')} isOpen closeOnEscape={false} onDismiss={onHide}>
|
||||
<UserAlerts />
|
||||
<div className={cx('root')}>
|
||||
<Tabs
|
||||
onTabChange={onTabChange}
|
||||
|
|
|
|||
|
|
@ -298,10 +298,10 @@ export class UserStore {
|
|||
}
|
||||
|
||||
@computed
|
||||
get currentUser() {
|
||||
get currentUser(): undefined | ApiSchemas['CurrentUser'] {
|
||||
if (!this.currentUserPk) {
|
||||
return undefined;
|
||||
}
|
||||
return this.items[this.currentUserPk];
|
||||
return this.items[this.currentUserPk] as ApiSchemas['CurrentUser'];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -994,6 +994,7 @@ export interface paths {
|
|||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** @description overridden_login_social_auth starts the installation of integration which uses OAuth flow. */
|
||||
get: operations['login_retrieve'];
|
||||
put?: never;
|
||||
post?: never;
|
||||
|
|
@ -1010,6 +1011,7 @@ export interface paths {
|
|||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** @description overridden_login_social_auth starts the installation of integration which uses OAuth flow. */
|
||||
get: operations['login_retrieve_2'];
|
||||
put?: never;
|
||||
post?: never;
|
||||
|
|
@ -1019,6 +1021,22 @@ export interface paths {
|
|||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
'/user/': {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations['user_retrieve'];
|
||||
put: operations['user_update'];
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
'/users/': {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
|
@ -1295,6 +1313,23 @@ export interface paths {
|
|||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
'/users/filters/': {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** @description Internal API endpoints for users. */
|
||||
get: operations['users_filters_list'];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
'/users/timezone_options/': {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
|
@ -1361,7 +1396,7 @@ export interface components {
|
|||
silenced_until?: string | null;
|
||||
/** Format: date-time */
|
||||
readonly started_at: string;
|
||||
readonly related_users: components['schemas']['UserShort'][];
|
||||
readonly related_users: Array<components['schemas']['UserShort']>;
|
||||
readonly render_for_web:
|
||||
| {
|
||||
title: string;
|
||||
|
|
@ -1370,22 +1405,22 @@ export interface components {
|
|||
source_link: string | null;
|
||||
}
|
||||
| Record<string, never>;
|
||||
dependent_alert_groups: components['schemas']['ShortAlertGroup'][];
|
||||
dependent_alert_groups: Array<components['schemas']['ShortAlertGroup']>;
|
||||
root_alert_group: components['schemas']['ShortAlertGroup'];
|
||||
readonly status: number;
|
||||
/** @description Generate a link for AlertGroup to declare Grafana Incident by click */
|
||||
readonly declare_incident_link: string;
|
||||
team: string | null;
|
||||
grafana_incident_id?: string | null;
|
||||
readonly labels: components['schemas']['AlertGroupLabel'][];
|
||||
readonly labels: Array<components['schemas']['AlertGroupLabel']>;
|
||||
readonly permalinks: {
|
||||
slack: string | null;
|
||||
slack_app: string | null;
|
||||
telegram: string | null;
|
||||
web: string;
|
||||
};
|
||||
readonly alerts: components['schemas']['Alert'][];
|
||||
readonly render_after_resolve_report_json: {
|
||||
readonly alerts: Array<components['schemas']['Alert']>;
|
||||
readonly render_after_resolve_report_json: Array<{
|
||||
time: string;
|
||||
action: string;
|
||||
/** @enum {string} */
|
||||
|
|
@ -1398,11 +1433,11 @@ export interface components {
|
|||
avatar: string;
|
||||
avatar_full: string;
|
||||
};
|
||||
}[];
|
||||
}>;
|
||||
readonly slack_permalink: string | null;
|
||||
/** Format: date-time */
|
||||
readonly last_alert_at: string;
|
||||
readonly paged_users: {
|
||||
readonly paged_users: Array<{
|
||||
id: number;
|
||||
username: string;
|
||||
name: string;
|
||||
|
|
@ -1410,13 +1445,13 @@ export interface components {
|
|||
avatar: string;
|
||||
avatar_full: string;
|
||||
important: boolean;
|
||||
}[];
|
||||
readonly external_urls: {
|
||||
}>;
|
||||
readonly external_urls: Array<{
|
||||
integration: string;
|
||||
integration_type: string;
|
||||
external_id: string;
|
||||
url: string;
|
||||
}[];
|
||||
}>;
|
||||
};
|
||||
AlertGroupAttach: {
|
||||
root_alert_group_pk: string;
|
||||
|
|
@ -1473,7 +1508,7 @@ export interface components {
|
|||
silenced_until?: string | null;
|
||||
/** Format: date-time */
|
||||
readonly started_at: string;
|
||||
readonly related_users: components['schemas']['UserShort'][];
|
||||
readonly related_users: Array<components['schemas']['UserShort']>;
|
||||
readonly render_for_web:
|
||||
| {
|
||||
title: string;
|
||||
|
|
@ -1482,14 +1517,14 @@ export interface components {
|
|||
source_link: string | null;
|
||||
}
|
||||
| Record<string, never>;
|
||||
dependent_alert_groups: components['schemas']['ShortAlertGroup'][];
|
||||
dependent_alert_groups: Array<components['schemas']['ShortAlertGroup']>;
|
||||
root_alert_group: components['schemas']['ShortAlertGroup'];
|
||||
readonly status: number;
|
||||
/** @description Generate a link for AlertGroup to declare Grafana Incident by click */
|
||||
readonly declare_incident_link: string;
|
||||
team: string | null;
|
||||
grafana_incident_id?: string | null;
|
||||
readonly labels: components['schemas']['AlertGroupLabel'][];
|
||||
readonly labels: Array<components['schemas']['AlertGroupLabel']>;
|
||||
readonly permalinks: {
|
||||
slack: string | null;
|
||||
slack_app: string | null;
|
||||
|
|
@ -1589,7 +1624,7 @@ export interface components {
|
|||
readonly is_based_on_alertmanager: boolean;
|
||||
readonly inbound_email: string;
|
||||
readonly is_legacy: boolean;
|
||||
labels?: components['schemas']['LabelPair'][];
|
||||
labels?: Array<components['schemas']['LabelPair']>;
|
||||
alert_group_labels?: components['schemas']['IntegrationAlertGroupLabels'];
|
||||
/** Format: date-time */
|
||||
readonly alertmanager_v2_migrated_at: string | null;
|
||||
|
|
@ -1606,15 +1641,15 @@ export interface components {
|
|||
AlertReceiveChannelConnectedContactPoints: {
|
||||
uid: string;
|
||||
name: string;
|
||||
contact_points: components['schemas']['AlertReceiveChannelConnectedContactPointsInner'][];
|
||||
contact_points: Array<components['schemas']['AlertReceiveChannelConnectedContactPointsInner']>;
|
||||
};
|
||||
AlertReceiveChannelConnectedContactPointsInner: {
|
||||
name: string;
|
||||
notification_connected: boolean;
|
||||
};
|
||||
AlertReceiveChannelConnection: {
|
||||
readonly source_alert_receive_channels: components['schemas']['AlertReceiveChannelSourceChannel'][];
|
||||
readonly connected_alert_receive_channels: components['schemas']['AlertReceiveChannelConnectedChannel'][];
|
||||
readonly source_alert_receive_channels: Array<components['schemas']['AlertReceiveChannelSourceChannel']>;
|
||||
readonly connected_alert_receive_channels: Array<components['schemas']['AlertReceiveChannelConnectedChannel']>;
|
||||
};
|
||||
AlertReceiveChannelContactPoints: {
|
||||
uid: string;
|
||||
|
|
@ -1656,7 +1691,7 @@ export interface components {
|
|||
readonly is_based_on_alertmanager: boolean;
|
||||
readonly inbound_email: string;
|
||||
readonly is_legacy: boolean;
|
||||
labels?: components['schemas']['LabelPair'][];
|
||||
labels?: Array<components['schemas']['LabelPair']>;
|
||||
alert_group_labels?: components['schemas']['IntegrationAlertGroupLabels'];
|
||||
/** Format: date-time */
|
||||
readonly alertmanager_v2_migrated_at: string | null;
|
||||
|
|
@ -1741,7 +1776,7 @@ export interface components {
|
|||
readonly is_based_on_alertmanager: boolean;
|
||||
readonly inbound_email: string;
|
||||
readonly is_legacy: boolean;
|
||||
labels?: components['schemas']['LabelPair'][];
|
||||
labels?: Array<components['schemas']['LabelPair']>;
|
||||
alert_group_labels?: components['schemas']['IntegrationAlertGroupLabels'];
|
||||
/** Format: date-time */
|
||||
readonly alertmanager_v2_migrated_at: string | null;
|
||||
|
|
@ -1749,6 +1784,45 @@ export interface components {
|
|||
};
|
||||
/** @enum {integer} */
|
||||
CloudConnectionStatusEnum: 0 | 1 | 2 | 3;
|
||||
CurrentUser: {
|
||||
readonly pk: string;
|
||||
readonly organization: components['schemas']['FastOrganization'];
|
||||
current_team?: string | null;
|
||||
/** Format: email */
|
||||
readonly email: string;
|
||||
readonly username: string;
|
||||
readonly name: string;
|
||||
readonly role: components['schemas']['RoleEnum'];
|
||||
/** Format: uri */
|
||||
readonly avatar: string;
|
||||
/** Format: uri */
|
||||
readonly avatar_full: string;
|
||||
timezone?: string | null;
|
||||
working_hours?: components['schemas']['WorkingHours'];
|
||||
unverified_phone_number?: string | null;
|
||||
/** @description Use property to highlight that _verified_phone_number should not be modified directly */
|
||||
readonly verified_phone_number: string | null;
|
||||
readonly slack_user_identity: components['schemas']['SlackUserIdentity'];
|
||||
readonly telegram_configuration: components['schemas']['TelegramToUserConnector'];
|
||||
readonly messaging_backends: {
|
||||
[key: string]:
|
||||
| {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
| undefined;
|
||||
};
|
||||
readonly notification_chain_verbal: {
|
||||
default: string;
|
||||
important: string;
|
||||
};
|
||||
readonly cloud_connection_status: components['schemas']['CloudConnectionStatusEnum'] | null;
|
||||
hide_phone_number?: boolean;
|
||||
readonly has_google_oauth2_connected: boolean;
|
||||
readonly is_currently_oncall: boolean;
|
||||
google_calendar_settings?: components['schemas']['GoogleCalendarSettings'];
|
||||
readonly rbac_permissions: Array<components['schemas']['UserPermission']>;
|
||||
readonly google_oauth2_token_is_missing_scopes: boolean;
|
||||
};
|
||||
/** @description This serializer is consistent with apps.api.serializers.labels.LabelPairSerializer, but allows null for value ID. */
|
||||
CustomLabel: {
|
||||
key: components['schemas']['CustomLabelKey'];
|
||||
|
|
@ -1813,17 +1887,14 @@ export interface components {
|
|||
inheritable: {
|
||||
[key: string]: boolean | undefined;
|
||||
};
|
||||
custom: components['schemas']['CustomLabel'][];
|
||||
custom: Array<components['schemas']['CustomLabel']>;
|
||||
template: string | null;
|
||||
};
|
||||
/**
|
||||
* @description * `alertmanager` - Alertmanager
|
||||
* * `legacy_alertmanager` - (Legacy) AlertManager
|
||||
* * `grafana` - Grafana
|
||||
* * `grafana_alerting` - Grafana Alerting
|
||||
* * `legacy_grafana_alerting` - (Legacy) Grafana Alerting
|
||||
* * `formatted_webhook` - Formatted webhook
|
||||
* @description * `grafana_alerting` - Grafana Alerting
|
||||
* * `webhook` - Webhook
|
||||
* * `alertmanager` - Alertmanager
|
||||
* * `formatted_webhook` - Formatted webhook
|
||||
* * `kapacitor` - Kapacitor
|
||||
* * `elastalert` - Elastalert
|
||||
* * `heartbeat` - Heartbeat
|
||||
|
|
@ -1833,6 +1904,9 @@ export interface components {
|
|||
* * `slack_channel` - Slack Channel
|
||||
* * `zabbix` - Zabbix
|
||||
* * `direct_paging` - Direct paging
|
||||
* * `grafana` - Grafana Legacy Alerting
|
||||
* * `legacy_alertmanager` - (Legacy) AlertManager
|
||||
* * `legacy_grafana_alerting` - (Deprecated) Grafana Alerting
|
||||
* * `servicenow` - ServiceNow
|
||||
* * `amazon_sns` - Amazon SNS
|
||||
* * `stackdriver` - Stackdriver
|
||||
|
|
@ -1852,13 +1926,10 @@ export interface components {
|
|||
* @enum {string}
|
||||
*/
|
||||
IntegrationEnum:
|
||||
| 'alertmanager'
|
||||
| 'legacy_alertmanager'
|
||||
| 'grafana'
|
||||
| 'grafana_alerting'
|
||||
| 'legacy_grafana_alerting'
|
||||
| 'formatted_webhook'
|
||||
| 'webhook'
|
||||
| 'alertmanager'
|
||||
| 'formatted_webhook'
|
||||
| 'kapacitor'
|
||||
| 'elastalert'
|
||||
| 'heartbeat'
|
||||
|
|
@ -1868,6 +1939,9 @@ export interface components {
|
|||
| 'slack_channel'
|
||||
| 'zabbix'
|
||||
| 'direct_paging'
|
||||
| 'grafana'
|
||||
| 'legacy_alertmanager'
|
||||
| 'legacy_grafana_alerting'
|
||||
| 'servicenow'
|
||||
| 'amazon_sns'
|
||||
| 'stackdriver'
|
||||
|
|
@ -1906,7 +1980,7 @@ export interface components {
|
|||
};
|
||||
LabelCreate: {
|
||||
key: components['schemas']['LabelRepr'];
|
||||
values: components['schemas']['LabelRepr'][];
|
||||
values: Array<components['schemas']['LabelRepr']>;
|
||||
};
|
||||
LabelKey: {
|
||||
id: string;
|
||||
|
|
@ -1916,7 +1990,7 @@ export interface components {
|
|||
};
|
||||
LabelOption: {
|
||||
key: components['schemas']['LabelKey'];
|
||||
values: components['schemas']['LabelValue'][];
|
||||
values: Array<components['schemas']['LabelValue']>;
|
||||
};
|
||||
LabelPair: {
|
||||
key: components['schemas']['LabelKey'];
|
||||
|
|
@ -1981,14 +2055,22 @@ export interface components {
|
|||
/** @enum {unknown} */
|
||||
NullEnum: null;
|
||||
PaginatedAlertGroupListList: {
|
||||
/**
|
||||
* Format: uri
|
||||
* @example http://api.example.org/accounts/?cursor=cD00ODY%3D"
|
||||
*/
|
||||
next?: string | null;
|
||||
/**
|
||||
* Format: uri
|
||||
* @example http://api.example.org/accounts/?cursor=cj0xJnA9NDg3
|
||||
*/
|
||||
previous?: string | null;
|
||||
results?: components['schemas']['AlertGroupList'][];
|
||||
results: Array<components['schemas']['AlertGroupList']>;
|
||||
page_size?: number;
|
||||
};
|
||||
PaginatedAlertReceiveChannelPolymorphicList: {
|
||||
/** @example 123 */
|
||||
count?: number;
|
||||
count: number;
|
||||
/**
|
||||
* Format: uri
|
||||
* @example http://api.example.org/accounts/?page=4
|
||||
|
|
@ -1999,14 +2081,14 @@ export interface components {
|
|||
* @example http://api.example.org/accounts/?page=2
|
||||
*/
|
||||
previous?: string | null;
|
||||
results?: components['schemas']['AlertReceiveChannelPolymorphic'][];
|
||||
results: Array<components['schemas']['AlertReceiveChannelPolymorphic']>;
|
||||
page_size?: number;
|
||||
current_page_number?: number;
|
||||
total_pages?: number;
|
||||
};
|
||||
PaginatedUserPolymorphicList: {
|
||||
/** @example 123 */
|
||||
count?: number;
|
||||
count: number;
|
||||
/**
|
||||
* Format: uri
|
||||
* @example http://api.example.org/accounts/?page=4
|
||||
|
|
@ -2017,7 +2099,7 @@ export interface components {
|
|||
* @example http://api.example.org/accounts/?page=2
|
||||
*/
|
||||
previous?: string | null;
|
||||
results?: components['schemas']['UserPolymorphic'][];
|
||||
results: Array<components['schemas']['UserPolymorphic']>;
|
||||
page_size?: number;
|
||||
current_page_number?: number;
|
||||
total_pages?: number;
|
||||
|
|
@ -2057,7 +2139,7 @@ export interface components {
|
|||
readonly is_based_on_alertmanager?: boolean;
|
||||
readonly inbound_email?: string;
|
||||
readonly is_legacy?: boolean;
|
||||
labels?: components['schemas']['LabelPair'][];
|
||||
labels?: Array<components['schemas']['LabelPair']>;
|
||||
alert_group_labels?: components['schemas']['IntegrationAlertGroupLabels'];
|
||||
/** Format: date-time */
|
||||
readonly alertmanager_v2_migrated_at?: string | null;
|
||||
|
|
@ -2240,6 +2322,21 @@ export interface components {
|
|||
created_at: string;
|
||||
export_url: string;
|
||||
};
|
||||
UserFilters: {
|
||||
name: string;
|
||||
type: string;
|
||||
href?: string;
|
||||
global?: boolean;
|
||||
default?: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
description?: string;
|
||||
options: components['schemas']['UserFiltersOptions'];
|
||||
};
|
||||
UserFiltersOptions: {
|
||||
value: string;
|
||||
display_name: number;
|
||||
};
|
||||
UserGetTelegramVerificationCode: {
|
||||
telegram_code: string;
|
||||
bot_link: string;
|
||||
|
|
@ -2251,9 +2348,12 @@ export interface components {
|
|||
avatar_full: string;
|
||||
name: string;
|
||||
readonly timezone: string | null;
|
||||
readonly teams: components['schemas']['FastTeam'][];
|
||||
readonly teams: Array<components['schemas']['FastTeam']>;
|
||||
readonly is_currently_oncall: boolean;
|
||||
};
|
||||
UserPermission: {
|
||||
readonly action: string;
|
||||
};
|
||||
UserPolymorphic:
|
||||
| components['schemas']['FilterUser']
|
||||
| components['schemas']['UserIsCurrentlyOnCall']
|
||||
|
|
@ -2288,16 +2388,16 @@ export interface components {
|
|||
readonly last_response_log: string;
|
||||
integration_filter?: string[];
|
||||
preset?: string | null;
|
||||
labels?: components['schemas']['LabelPair'][];
|
||||
labels?: Array<components['schemas']['LabelPair']>;
|
||||
};
|
||||
WorkingHours: {
|
||||
monday: components['schemas']['WorkingHoursPeriod'][];
|
||||
tuesday: components['schemas']['WorkingHoursPeriod'][];
|
||||
wednesday: components['schemas']['WorkingHoursPeriod'][];
|
||||
thursday: components['schemas']['WorkingHoursPeriod'][];
|
||||
friday: components['schemas']['WorkingHoursPeriod'][];
|
||||
saturday: components['schemas']['WorkingHoursPeriod'][];
|
||||
sunday: components['schemas']['WorkingHoursPeriod'][];
|
||||
monday: Array<components['schemas']['WorkingHoursPeriod']>;
|
||||
tuesday: Array<components['schemas']['WorkingHoursPeriod']>;
|
||||
wednesday: Array<components['schemas']['WorkingHoursPeriod']>;
|
||||
thursday: Array<components['schemas']['WorkingHoursPeriod']>;
|
||||
friday: Array<components['schemas']['WorkingHoursPeriod']>;
|
||||
saturday: Array<components['schemas']['WorkingHoursPeriod']>;
|
||||
sunday: Array<components['schemas']['WorkingHoursPeriod']>;
|
||||
};
|
||||
WorkingHoursPeriod: {
|
||||
start: string;
|
||||
|
|
@ -2316,13 +2416,10 @@ export interface operations {
|
|||
parameters: {
|
||||
query?: {
|
||||
id_ne?: string[];
|
||||
/** @description * `alertmanager` - Alertmanager
|
||||
* * `legacy_alertmanager` - (Legacy) AlertManager
|
||||
* * `grafana` - Grafana
|
||||
* * `grafana_alerting` - Grafana Alerting
|
||||
* * `legacy_grafana_alerting` - (Legacy) Grafana Alerting
|
||||
* * `formatted_webhook` - Formatted webhook
|
||||
/** @description * `grafana_alerting` - Grafana Alerting
|
||||
* * `webhook` - Webhook
|
||||
* * `alertmanager` - Alertmanager
|
||||
* * `formatted_webhook` - Formatted webhook
|
||||
* * `kapacitor` - Kapacitor
|
||||
* * `elastalert` - Elastalert
|
||||
* * `heartbeat` - Heartbeat
|
||||
|
|
@ -2332,6 +2429,9 @@ export interface operations {
|
|||
* * `slack_channel` - Slack Channel
|
||||
* * `zabbix` - Zabbix
|
||||
* * `direct_paging` - Direct paging
|
||||
* * `grafana` - Grafana Legacy Alerting
|
||||
* * `legacy_alertmanager` - (Legacy) AlertManager
|
||||
* * `legacy_grafana_alerting` - (Deprecated) Grafana Alerting
|
||||
* * `servicenow` - ServiceNow
|
||||
* * `amazon_sns` - Amazon SNS
|
||||
* * `stackdriver` - Stackdriver
|
||||
|
|
@ -2348,7 +2448,7 @@ export interface operations {
|
|||
* * `jira` - Jira
|
||||
* * `zendesk` - Zendesk
|
||||
* * `appdynamics` - AppDynamics */
|
||||
integration?: (
|
||||
integration?: Array<
|
||||
| 'alertmanager'
|
||||
| 'amazon_sns'
|
||||
| 'appdynamics'
|
||||
|
|
@ -2381,14 +2481,11 @@ export interface operations {
|
|||
| 'webhook'
|
||||
| 'zabbix'
|
||||
| 'zendesk'
|
||||
)[];
|
||||
/** @description * `alertmanager` - Alertmanager
|
||||
* * `legacy_alertmanager` - (Legacy) AlertManager
|
||||
* * `grafana` - Grafana
|
||||
* * `grafana_alerting` - Grafana Alerting
|
||||
* * `legacy_grafana_alerting` - (Legacy) Grafana Alerting
|
||||
* * `formatted_webhook` - Formatted webhook
|
||||
>;
|
||||
/** @description * `grafana_alerting` - Grafana Alerting
|
||||
* * `webhook` - Webhook
|
||||
* * `alertmanager` - Alertmanager
|
||||
* * `formatted_webhook` - Formatted webhook
|
||||
* * `kapacitor` - Kapacitor
|
||||
* * `elastalert` - Elastalert
|
||||
* * `heartbeat` - Heartbeat
|
||||
|
|
@ -2398,6 +2495,9 @@ export interface operations {
|
|||
* * `slack_channel` - Slack Channel
|
||||
* * `zabbix` - Zabbix
|
||||
* * `direct_paging` - Direct paging
|
||||
* * `grafana` - Grafana Legacy Alerting
|
||||
* * `legacy_alertmanager` - (Legacy) AlertManager
|
||||
* * `legacy_grafana_alerting` - (Deprecated) Grafana Alerting
|
||||
* * `servicenow` - ServiceNow
|
||||
* * `amazon_sns` - Amazon SNS
|
||||
* * `stackdriver` - Stackdriver
|
||||
|
|
@ -2414,7 +2514,7 @@ export interface operations {
|
|||
* * `jira` - Jira
|
||||
* * `zendesk` - Zendesk
|
||||
* * `appdynamics` - AppDynamics */
|
||||
integration_ne?: (
|
||||
integration_ne?: Array<
|
||||
| 'alertmanager'
|
||||
| 'amazon_sns'
|
||||
| 'appdynamics'
|
||||
|
|
@ -2447,10 +2547,10 @@ export interface operations {
|
|||
| 'webhook'
|
||||
| 'zabbix'
|
||||
| 'zendesk'
|
||||
)[];
|
||||
>;
|
||||
/** @description * `0` - Debug
|
||||
* * `1` - Maintenance */
|
||||
maintenance_mode?: (0 | 1)[];
|
||||
maintenance_mode?: Array<0 | 1>;
|
||||
/** @description A page number within the paginated result set. */
|
||||
page?: number;
|
||||
/** @description Number of results to return per page. */
|
||||
|
|
@ -2726,9 +2826,9 @@ export interface operations {
|
|||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': components['schemas']['AlertReceiveChannelNewConnection'][];
|
||||
'application/x-www-form-urlencoded': components['schemas']['AlertReceiveChannelNewConnection'][];
|
||||
'multipart/form-data': components['schemas']['AlertReceiveChannelNewConnection'][];
|
||||
'application/json': Array<components['schemas']['AlertReceiveChannelNewConnection']>;
|
||||
'application/x-www-form-urlencoded': Array<components['schemas']['AlertReceiveChannelNewConnection']>;
|
||||
'multipart/form-data': Array<components['schemas']['AlertReceiveChannelNewConnection']>;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
|
|
@ -2810,7 +2910,7 @@ export interface operations {
|
|||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
'application/json': components['schemas']['AlertReceiveChannelConnectedContactPoints'][];
|
||||
'application/json': Array<components['schemas']['AlertReceiveChannelConnectedContactPoints']>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
@ -3088,7 +3188,7 @@ export interface operations {
|
|||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
'application/json': components['schemas']['Webhook'][];
|
||||
'application/json': Array<components['schemas']['Webhook']>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
@ -3186,7 +3286,7 @@ export interface operations {
|
|||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
'application/json': components['schemas']['AlertReceiveChannelContactPoints'][];
|
||||
'application/json': Array<components['schemas']['AlertReceiveChannelContactPoints']>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
@ -3231,7 +3331,7 @@ export interface operations {
|
|||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
'application/json': components['schemas']['AlertReceiveChannelFilters'][];
|
||||
'application/json': Array<components['schemas']['AlertReceiveChannelFilters']>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
@ -3250,7 +3350,7 @@ export interface operations {
|
|||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
'application/json': components['schemas']['AlertReceiveChannelIntegrationOptions'][];
|
||||
'application/json': Array<components['schemas']['AlertReceiveChannelIntegrationOptions']>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
@ -3330,7 +3430,7 @@ export interface operations {
|
|||
* * `1` - Acknowledged
|
||||
* * `2` - Resolved
|
||||
* * `3` - Silenced */
|
||||
status?: (0 | 1 | 2 | 3)[];
|
||||
status?: Array<0 | 1 | 2 | 3>;
|
||||
with_resolution_note?: boolean;
|
||||
};
|
||||
header?: never;
|
||||
|
|
@ -3701,7 +3801,7 @@ export interface operations {
|
|||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
'application/json': components['schemas']['AlertGroupBulkActionOptions'][];
|
||||
'application/json': Array<components['schemas']['AlertGroupBulkActionOptions']>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
@ -3720,7 +3820,7 @@ export interface operations {
|
|||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
'application/json': components['schemas']['AlertGroupFilters'][];
|
||||
'application/json': Array<components['schemas']['AlertGroupFilters']>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
@ -3760,7 +3860,7 @@ export interface operations {
|
|||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
'application/json': components['schemas']['LabelKey'][];
|
||||
'application/json': Array<components['schemas']['LabelKey']>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
@ -3779,7 +3879,7 @@ export interface operations {
|
|||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
'application/json': components['schemas']['AlertGroupSilenceOptions'][];
|
||||
'application/json': Array<components['schemas']['AlertGroupSilenceOptions']>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
@ -3804,7 +3904,7 @@ export interface operations {
|
|||
* * `1` - Acknowledged
|
||||
* * `2` - Resolved
|
||||
* * `3` - Silenced */
|
||||
status?: (0 | 1 | 2 | 3)[];
|
||||
status?: Array<0 | 1 | 2 | 3>;
|
||||
with_resolution_note?: boolean;
|
||||
};
|
||||
header?: never;
|
||||
|
|
@ -3877,9 +3977,10 @@ export interface operations {
|
|||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
'application/json': (
|
||||
'application/json': Array<
|
||||
| 'msteams'
|
||||
| 'slack'
|
||||
| 'unified_slack'
|
||||
| 'telegram'
|
||||
| 'live_settings'
|
||||
| 'grafana_cloud_notifications'
|
||||
|
|
@ -3887,7 +3988,7 @@ export interface operations {
|
|||
| 'grafana_alerting_v2'
|
||||
| 'labels'
|
||||
| 'google_oauth2'
|
||||
)[];
|
||||
>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
@ -3901,9 +4002,9 @@ export interface operations {
|
|||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': components['schemas']['LabelCreate'][];
|
||||
'application/x-www-form-urlencoded': components['schemas']['LabelCreate'][];
|
||||
'multipart/form-data': components['schemas']['LabelCreate'][];
|
||||
'application/json': Array<components['schemas']['LabelCreate']>;
|
||||
'application/x-www-form-urlencoded': Array<components['schemas']['LabelCreate']>;
|
||||
'multipart/form-data': Array<components['schemas']['LabelCreate']>;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
|
|
@ -4056,7 +4157,7 @@ export interface operations {
|
|||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
'application/json': components['schemas']['LabelKey'][];
|
||||
'application/json': Array<components['schemas']['LabelKey']>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
@ -4101,6 +4202,44 @@ export interface operations {
|
|||
};
|
||||
};
|
||||
};
|
||||
user_retrieve: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
'application/json': components['schemas']['CurrentUser'];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
user_update: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
'application/json': components['schemas']['CurrentUser'];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
users_list: {
|
||||
parameters: {
|
||||
query?: {
|
||||
|
|
@ -4170,7 +4309,7 @@ export interface operations {
|
|||
* * `1` - EDITOR
|
||||
* * `2` - VIEWER
|
||||
* * `3` - NONE */
|
||||
roles?: (0 | 1 | 2 | 3)[];
|
||||
roles?: Array<0 | 1 | 2 | 3>;
|
||||
/** @description A search term. */
|
||||
search?: string;
|
||||
team?: string[];
|
||||
|
|
@ -4591,7 +4730,7 @@ export interface operations {
|
|||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
'application/json': {
|
||||
'application/json': Array<{
|
||||
schedule_id: string;
|
||||
schedule_name: string;
|
||||
is_oncall: boolean;
|
||||
|
|
@ -4601,7 +4740,7 @@ export interface operations {
|
|||
start: string;
|
||||
/** Format: date-time */
|
||||
end: string;
|
||||
users: {
|
||||
users: Array<{
|
||||
display_name: string;
|
||||
pk: string;
|
||||
email: string;
|
||||
|
|
@ -4615,7 +4754,7 @@ export interface operations {
|
|||
avatar_full: string;
|
||||
} | null;
|
||||
} | null;
|
||||
}[];
|
||||
}>;
|
||||
missing_users: string[];
|
||||
priority_level: number | null;
|
||||
source: string | null;
|
||||
|
|
@ -4633,7 +4772,7 @@ export interface operations {
|
|||
start: string;
|
||||
/** Format: date-time */
|
||||
end: string;
|
||||
users: {
|
||||
users: Array<{
|
||||
display_name: string;
|
||||
pk: string;
|
||||
email: string;
|
||||
|
|
@ -4647,7 +4786,7 @@ export interface operations {
|
|||
avatar_full: string;
|
||||
} | null;
|
||||
} | null;
|
||||
}[];
|
||||
}>;
|
||||
missing_users: string[];
|
||||
priority_level: number | null;
|
||||
source: string | null;
|
||||
|
|
@ -4659,7 +4798,7 @@ export interface operations {
|
|||
pk: string;
|
||||
};
|
||||
} | null;
|
||||
}[];
|
||||
}>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
@ -4687,6 +4826,25 @@ export interface operations {
|
|||
};
|
||||
};
|
||||
};
|
||||
users_filters_list: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
'application/json': Array<components['schemas']['UserFilters']>;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
users_timezone_options_retrieve: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue