-
Notifications
You must be signed in to change notification settings - Fork 2
/
approve.py
297 lines (244 loc) · 8.91 KB
/
approve.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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
"""Create group accounts."""
import ssl
import sys
import time
from argparse import ArgumentParser
from configparser import ConfigParser
from textwrap import dedent
import yaml
from celery import Celery
from Crypto.PublicKey import RSA
from ocflib.account.creation import CREATE_PUBLIC_KEY
from ocflib.account.creation import encrypt_password
from ocflib.account.creation import NewAccountRequest
from ocflib.account.submission import get_tasks
from ocflib.account.submission import NewAccountResponse
from ocflib.account.validators import validate_password
from ocflib.misc.mail import send_problem_report
from ocflib.misc.shell import bold
from ocflib.misc.shell import edit_file
from ocflib.misc.shell import green
from ocflib.misc.shell import prompt_for_new_password
from ocflib.misc.shell import red
from ocflib.misc.shell import yellow
from ocflib.ucb.groups import group_by_oid
TEMPLATE = dedent(
# "\n\" is to hack around linters complaining about trailing whitespace
"""\
user_name: \n\
group_name: {group_name}
callink_oid: {callink_oid}
email: {email}
# Please ensure that:
# * Person requesting account is signatory of group
# - Look up the signatory's CalNet UID on directory.berkeley.edu
# - Use `signat <uid>` to list groups they are a signatory for
# * Group does not have existing account (use checkacct)
# * Requested account name is based on group name
#
# vim: ft=yaml
"""
)
def wait_for_task(celery, task):
"""Wait for a validate_then_create_account task."""
print('Waiting...', end='')
task.wait() # this should be almost instant
if isinstance(task.result, NewAccountResponse):
print()
return task.result
task = celery.AsyncResult(task.result)
last_status_len = 0
while not task.ready():
time.sleep(0.25)
meta = task.info
if isinstance(meta, dict) and 'status' in meta:
status = meta['status']
if len(status) > last_status_len:
for line in status[last_status_len:]:
print()
print(line, end='')
last_status_len = len(status)
print('.', end='')
sys.stdout.flush()
print()
if isinstance(task.result, Exception):
raise task.result
else:
return task.result
def get_group_information(group_oid):
"""Return tuple (group name, group oid, group email)."""
if group_oid:
group = group_by_oid(group_oid)
if not group:
print(red('No group with OID {}').format(group_oid))
sys.exit(1)
if group['accounts']:
print(yellow(
'Warning: there is an existing group account with OID {}: {}'.format(
group_oid,
', '.join(group['accounts']),
),
))
input('Press enter to continue...')
return (group['name'], group_oid, group['email'])
else:
return ('', '', '')
def make_account_request(account, password):
"""Create an account request object for the new account."""
request = NewAccountRequest(
user_name=account['user_name'],
real_name=account['group_name'],
is_group=True,
calnet_uid=None,
callink_oid=account['callink_oid'],
email=account['email'],
encrypted_password=encrypt_password(
password,
RSA.importKey(CREATE_PUBLIC_KEY),
),
handle_warnings=NewAccountRequest.WARNINGS_WARN,
)
print()
print(bold('Pending account request:'))
print(dedent(
"""\
User Name: {request.user_name}
Group Name: {request.real_name}
CalLink OID: {request.callink_oid}
Email: {request.email}
"""
).format(request=request))
return request
def create_account(request):
"""Return tuple (tasks queue, celery connection, task reponse)."""
conf = ConfigParser()
conf.read('/etc/ocf-create/ocf-create.conf')
celery = Celery(
broker=conf.get('celery', 'broker'),
backend=conf.get('celery', 'backend'),
)
celery.conf.broker_use_ssl = {
'ssl_ca_certs': '/etc/ssl/certs/ca-certificates.crt',
'ssl_cert_reqs': ssl.CERT_REQUIRED,
}
celery.conf.redis_backend_use_ssl = {
'ssl_ca_certs': '/etc/ssl/certs/ca-certificates.crt',
'ssl_cert_reqs': ssl.CERT_REQUIRED,
}
# TODO: stop using pickle
celery.conf.task_serializer = 'pickle'
celery.conf.result_serializer = 'pickle'
celery.conf.accept_content = {'pickle'}
tasks = get_tasks(celery)
task = tasks.validate_then_create_account.delay(request)
response = wait_for_task(celery, task)
return (tasks, celery, response)
def error_report(request, new_request, response):
"""Create and send an error report through ocflib."""
print(bold(red('Error: Entered unexpected state.')))
print(bold('An email has been sent to OCF staff'))
error_report = dedent(
"""\
Error encountered running approve!
The request we submitted was:
{request}
The request we submitted after being flagged (if any) was:
{new_request}
The response we received was:
{response}
"""
).format(
request=request,
new_request=new_request,
reponse=response
)
send_problem_report(error_report)
def main():
"""Run approval script interactively."""
def pause_error_msg():
input('Press enter to edit group information (or Ctrl-C to exit)...')
parser = ArgumentParser(description='Create new OCF group accounts.')
parser.add_argument('oid', type=int, nargs='?', help='CalLink OID for the group.')
args = parser.parse_args()
group_name, callink_oid, email = get_group_information(args.oid)
content = TEMPLATE.format(
group_name=group_name,
callink_oid=callink_oid,
email=email
)
while True:
content = edit_file(content)
try:
account = yaml.safe_load(content)
except yaml.YAMLError as ex:
print('Error parsing your YAML:')
print(ex)
pause_error_msg()
continue
missing_key = False
for key in ['user_name', 'group_name', 'callink_oid', 'email']:
if account.get(key) is None:
print('Missing value for key: ' + key)
missing_key = True
if missing_key:
pause_error_msg()
continue
if account['user_name'].startswith('cal'):
print('Username cannot start with "cal" due to university trademarks')
pause_error_msg()
continue
try:
password = prompt_for_new_password(
validator=lambda pwd: validate_password(
account['user_name'], pwd,
),
)
except KeyboardInterrupt:
# we want to allow cancelling during the "enter password" stage
# without completely exiting approve
print()
pause_error_msg()
continue
request = make_account_request(account, password)
if input('Submit request? [yN] ') not in ('y', 'Y'):
pause_error_msg()
continue
tasks, celery, response = create_account(request)
new_request = None
if response.status == NewAccountResponse.REJECTED:
print(bold(red(
'Account requested was rejected for the following reasons:'
)))
for error in response.errors:
print(red(' - {}'.format(error)))
pause_error_msg()
continue
elif response.status == NewAccountResponse.FLAGGED:
print(bold(yellow(
'Account requested was flagged for the following reasons:'
)))
for error in response.errors:
print(yellow(' - {}'.format(error)))
print(bold(
'You can either create the account anyway, or go back and '
'modify the request.'
))
choice = input('Create the account anyway? [yN] ')
if choice in ('y', 'Y'):
new_request = request._replace(
handle_warnings=NewAccountRequest.WARNINGS_CREATE,
)
task = tasks.validate_then_create_account.delay(new_request)
response = wait_for_task(celery, task)
else:
pause_error_msg()
continue
if response.status == NewAccountResponse.CREATED:
print(bold(green('Account created!')))
print('Your account was created successfully.')
print('You\'ve been sent an email with more information.')
return
else:
# this shouldn't be possible; we must have entered some weird state
error_report(request, new_request, response)
pause_error_msg()