v1.8.13
This commit is contained in:
commit
ce9c7dbcd1
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