GCal autogenerated shift swap requests - don't recreate if one was previously created and deleted (#4281)

# What this PR does

Addresses two issues:
- addresses an internal feature request ([in
Slack](https://raintank-corp.slack.com/archives/C03KS498VGV/p1713550543916289?thread_ts=1713546008.831749&cid=C03KS498VGV))
to not have a new shift swap request auto-generated if one previously
was, but than the user decided to delete that SSR
- when disconnecting the GCal integration from your user, I've seen
cases where Google will return HTTP 400 as such ([example
logs](https://ops.grafana-ops.net/goto/8vX76pBSg?orgId=1)):
  ```
{'error': 'invalid_token', 'error_description': 'Token expired or
revoked'}
  ```
I can't seem to find detailed documentation on the revoke endpoint (`GET
https://accounts.google.com/o/oauth2/revoke?token=<token>`) to try and
better understand the possible `error` values.. but I think our best bet
here is to just continue forward w/
`user.finish_google_oauth2_disconnection_flow()` (which deletes the
`GoogleOAuth2User` associated with the user and sets
`user.google_calendar_settings = None`)

## 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.
This commit is contained in:
Joey Orlando 2024-04-25 14:16:42 -04:00 committed by GitHub
parent 9432e9ee44
commit 3fd9a73a52
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 49 additions and 6 deletions

View file

@ -114,6 +114,9 @@ are involved in).
If you would like to have Grafana OnCall ignore a specific Out of Office event from being considered for
Shift Swap Request generation, simply add `#grafana-oncall-ignore` to the Out of Office event's title.
Additionally, if we generate a shift swap request for you from a Google Calendar event, and you delete the shift swap
request, we will not attempt to regenerate a new shift swap request.
### Configuring for open source
1. Follow the instructions [here](https://developers.google.com/identity/protocols/oauth2) to setup your Google OAuth2

View file

@ -64,7 +64,10 @@ def sync_out_of_office_calendar_events_for_user(google_oauth2_user_pk: int) -> N
f"for user {user_id} during the out of office event {event_id}"
)
shift_swap_request_exists = ShiftSwapRequest.objects.filter(
# also consider deleted shift swap requests.. this can be useful in the event
# that we autogenerated a shift swap request for the user but the user decided to delete it
# in this case, we shouldn't recreate a new one
shift_swap_request_exists = ShiftSwapRequest.objects_with_deleted.filter(
beneficiary=user,
schedule=schedule,
swap_start=start_time_utc,

View file

@ -325,6 +325,7 @@ def test_sync_out_of_office_calendar_events_for_user_preexisting_shift_swap_requ
}
google_oauth2_user, schedule = test_setup(out_of_office_events)
google_oauth2_user_pk = google_oauth2_user.pk
user = google_oauth2_user.user
make_shift_swap_request(
@ -334,7 +335,17 @@ def test_sync_out_of_office_calendar_events_for_user_preexisting_shift_swap_requ
swap_end=end_time,
)
tasks.sync_out_of_office_calendar_events_for_user(google_oauth2_user.pk)
def _fetch_shift_swap_requests():
return ShiftSwapRequest.objects_with_deleted.filter(beneficiary=user, schedule=schedule)
tasks.sync_out_of_office_calendar_events_for_user(google_oauth2_user_pk)
# should be 1 because we just created a shift swap request above via the fixture
assert ShiftSwapRequest.objects.filter(beneficiary=user, schedule=schedule).count() == 1
ssrs = _fetch_shift_swap_requests()
assert ssrs.count() == 1
# lets delete the shift swap request and run the task again, it should recognize that there was already
# a shift swap request and shouldn't recreate a new one
ssrs.first().delete()
tasks.sync_out_of_office_calendar_events_for_user(google_oauth2_user_pk)
assert _fetch_shift_swap_requests().count() == 1

View file

@ -1,6 +1,7 @@
import logging
import typing
import requests
from rest_framework.response import Response
from social_core.backends.base import BaseAuth
@ -28,10 +29,35 @@ def disconnect_user_google_oauth2_settings(backend: typing.Type[BaseAuth], user:
https://stackoverflow.com/a/18578660/3902555
"""
logger.info(f"Disconnecting user {user.pk} from Google OAuth2")
user_pk = user.pk
google_oauth2_user = user.google_oauth2_user
logger.info(f"Disconnecting user {user_pk} from Google OAuth2")
try:
backend.revoke_token(google_oauth2_user.refresh_token, google_oauth2_user.google_user_id)
except requests.exceptions.HTTPError as e:
response = e.response
logger.error(f"There was an HTTP error when trying to revoke Google OAuth2 token for user={user_pk}")
if response.status_code == 400:
error_details = response.json()
error_code = error_details["error"]
error_description = error_details["error_description"]
logger.error(
f"There was an HTTP 400 error when trying to revoke Google OAuth2 token for user={user_pk} "
f"error_code={error_code} error_description={error_description}"
)
error_codes_to_ignore = ["invalid_token"]
if error_code not in error_codes_to_ignore:
raise e
else:
logger.info(f"Google OAuth2 token for user {user_pk} is already invalid or revoked, ignoring error")
# 2nd argument, uid, is not needed for GoogleOauth2 backend
backend.revoke_token(user.google_oauth2_user.refresh_token, "")
user.finish_google_oauth2_disconnection_flow()
logger.info(f"Successfully disconnected user {user.pk} from Google OAuth2")