-
Notifications
You must be signed in to change notification settings - Fork 5
/
gcal.py
170 lines (139 loc) · 7.83 KB
/
gcal.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
"""A module with APIs to create Performance Triage reminders on Google Calendar. Expected usage:
creds = auth_as_user()
service = get_calendar_service(creds)
send_triage_reminder(service, ...)
To use this API, you must:
1. Create or reuse a Google Cloud Project with access to the Google Calendar API: we created the
perf-triage-reminder project.
2. Download the credentials for that project to PATH_GCLOUD_PROJECT_SECRETS. Note: this is only used
to fetch credentials locally and is not used in automation.
If you are using this API in automation, you must also:
3. Run these APIs manually, locally before running it automation. You will be prompted to sign in to
a Google account via a browser: preferably, you should use an account independent of a specific
person (i.e. a service account): we created perf.triage.bot.
4. Add the cached secrets at PATH_CACHED_USER_SECRETS or ENV_CACHED_USER_SECRETS ^ to your
automation environment.
## Resources shared across the performance team
We have set up shared projects and accounts - this is a summary of them:
- Performance Team calendar
- perf.triage.bot Google Account: this is our service account that we use to run in GitHub Actions.
It is a Google Account external to the Mozilla organization that has edit permissions to the
Performance Team Google Calendar. Access is granted through 1Password Vault.
- perf-triage-reminder Google Cloud Project: this project gives access to the Google Calendar APIs
and enables sign in to get credentials. Access is granted through the performance team mailing lists
- perf.triage.bot 1Password Vault: stores the credentials for ^. Access is granted to individuals
because that's the only way it seems to work.
WARNING: as folks leave Mozilla, we must make sure folks continue to have access to the 1Password
Vault so we don't lose access to the Google Account. If we do, we'll have to create a new one and
take time to set it up.
## Alternative implementations
Google has a newer, safer form of authorization for server-to-server applications like this one:
https://cloud.google.com/iam/docs/workload-identity-federation However, I didn't know if it'd work
for this use case and I wanted to keep the implementation quick so I did what was familiar.
Instead of creating a service account external to the Mozilla google organization, we could create a
service account inside of it. However, it requires IT to grant the account the
"Domain-Wide Delegation Authority" permission, which grants service accounts access to user data
which is necessary to invite users to calendar events. However, they chose not to grant the
permission. As we understand it, this would give the account slightly more access than the current
set up (it can access internal-only Mozilla organization calendars) but it would be easier to keep
track of and safely share the account.
Instead of creating a service account, a Mozilla employee could use their credentials (e.g. on
cron). However, this has a failure point when they leave and the credentials need to be updated.
Instead of Google Calendar reminders, we could send Matrix reminders or emails but we didn't think
this was as useful.
"""
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
import json
import os
import os.path
import sys
# List of scopes: https://developers.google.com/identity/protocols/oauth2/scopes#calendar
# If these scopes are modified, users (including you) will need to delete the
# PATH_CACHED_USER_SECRETS file.
SCOPES = ["https://www.googleapis.com/auth/calendar.events"]
# 'Performance Team' calendar ID.
ID_CALENDAR = "mozilla.com_9bk5f2rqdeuip38jbeld84kpqc@group.calendar.google.com"
# This file is downloaded from the Google Cloud API console for the project perf-triage-reminder. It
# is used to enable the OAuth flow and track/rate-limit the requests made to the GCal API.
PATH_GCLOUD_PROJECT_SECRETS = ".google-cloud-project-secrets.json"
# This file is generated by the OAuth flow. The file is specific to the user account used to sign
# in. The
PATH_CACHED_USER_SECRETS = ".google-user-token.json"
ENV_CACHED_USER_SECRETS = 'PERF_TRIAGE_BOT_CACHED_USER_SECRETS'
IN_AUTOMATION = True if os.environ.get('CI') else False # CI is always true on GitHub Actions.
DESCRIPTION = """{lead_sheriff} as Triage Sheriff #1, can you please take the lead to coordinate a date/time this week?
For the latest guidelines, please see https://wiki.mozilla.org/Performance/Triage.
-- Sent by the friendly scripts at https://github.com/mozilla/perf-triage/"""
class CredentialException(Exception):
pass
def _fetch_cached_user_credentials():
log_prefix = 'Fetching cached user credentials from'
env_secrets = os.environ.get(ENV_CACHED_USER_SECRETS)
if env_secrets:
print(f'{log_prefix} environment variable')
creds = Credentials.from_authorized_user_info(json.loads(env_secrets), SCOPES)
elif os.path.exists(PATH_CACHED_USER_SECRETS):
print(f'{log_prefix} local file')
creds = Credentials.from_authorized_user_file(PATH_CACHED_USER_SECRETS, SCOPES)
else:
print('Cached user credentials not found')
creds = None
return creds
# via https://developers.google.com/calendar/api/quickstart/python
def auth_as_user():
"""Prompt the user to authorize access to their calendars via a web browser and returns
credentials on success. If this is called a second time, the user will not be prompted because a
cached version will be used.
To call this API locally, you must get a PATH_GCLOUD_PROJECT_SECRETS: see the top-of-file
comment.
"""
creds = _fetch_cached_user_credentials()
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
elif IN_AUTOMATION:
raise CredentialException(('Credentials unavailable for refresh. Credentials must be '
'refetched manually and updated in secrets.'))
else:
flow = InstalledAppFlow.from_client_secrets_file(PATH_GCLOUD_PROJECT_SECRETS, SCOPES)
creds = flow.run_local_server(port=0)
if not IN_AUTOMATION: # Don't save secrets to disk.
with open(PATH_CACHED_USER_SECRETS, "w") as token:
token.write(creds.to_json())
return creds
# via https://developers.google.com/calendar/api/quickstart/python
def get_calendar_service(creds):
"""Returns the Google Calendar API object."""
try:
return build("calendar", "v3", credentials=creds)
except HttpError as error:
print("Unable to fetch calendar service: {}".format(error), file=sys.stderr)
raise error
def send_triage_reminder(service, date, emails):
"""Adds a triage reminder event to the Performance Team Google Calendar."""
details = {
"summary": "Reminder: perf triage rotation",
"description": DESCRIPTION.format(lead_sheriff=emails[0]),
"start": {
"dateTime": "{}T17:00:00Z".format(date), # utc.
},
"end": {
"dateTime": "{}T17:30:00Z".format(date), # utc.
},
"attendees": [{"email": email} for email in emails],
# Not sure if this key actually works.
"sendNotifications": True, # send an "Invitation: ..." email to attendees.
}
try:
event = service.events().insert(calendarId=ID_CALENDAR, body=details).execute()
print("Event created: {}".format(event.get("htmlLink")))
except HttpError as error:
print("Error when creating event: {}".format(error), file=sys.stderr)
raise error