-
Notifications
You must be signed in to change notification settings - Fork 0
/
views.py
794 lines (722 loc) · 37.6 KB
/
views.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
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
from typing import Any, Callable, Dict, TypeVar, cast
from tokenize import group
from urllib.request import HTTPRedirectHandler
from .apps import HopskotchAuthConfig
from django.shortcuts import render, redirect
from django.http import HttpRequest, HttpResponse, JsonResponse, HttpResponseRedirect
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
from django.db import connection, transaction
from django.template import Library
from django.views.decorators.http import require_POST, require_GET
from django.urls import reverse
from wsgiref.util import FileWrapper
from io import StringIO
from django.views.decorators.csrf import csrf_exempt
from mozilla_django_oidc.auth import get_user_model
from .forms import *
from .directinterface import DirectInterface, Error
from .models import *
import logging
logger = logging.getLogger(__name__)
engine = DirectInterface()
MESSAGE_TAGS = {
messages.DEBUG: 'alert-secondary',
messages.INFO: 'alert-info',
messages.SUCCESS: 'alert-success',
messages.WARNING: 'alert-warning',
messages.ERROR: 'alert-danger',
}
class AuthenticatedHttpRequest(HttpRequest):
user: User
WrappedFunc = TypeVar('WrappedFunc', bound=Callable[..., Any])
def admin_required(func: WrappedFunc) -> WrappedFunc:
def admin_check(request: AuthenticatedHttpRequest, *args: Any, **kwargs: Dict[str,Any]) -> Any:
is_admin = request.user.is_staff
if is_admin:
return func(request, *args, **kwargs)
return render(request, 'hopskotch_auth/admin_required.html')
return cast(WrappedFunc, admin_check)
def client_ip(request: HttpRequest) -> str:
"""Determine the original client IP address, taking into account headers set
by the load balancer, if they exist.
"""
if "X-Forwarded-For" in request.headers:
header = request.headers["X-Forwarded-For"]
if ',' in header:
trusted_addr = header.split(',')[-1]
return trusted_addr+f" (full X-Forwarded-For header: {header})"
return header
return request.META["REMOTE_ADDR"]
def download(request: AuthenticatedHttpRequest) -> HttpResponse:
myfile = StringIO()
myfile.write("username,password\n")
myfile.write(f"{request.POST['username']},{request.POST['password']}")
myfile.flush()
myfile.seek(0) # move the pointer to the beginning of the buffer
response = HttpResponse(FileWrapper(myfile), content_type='text/plain')
response['Content-Disposition'] = 'attachment; filename=hop-credentials.csv'
logger.info(f"Sent data for credential {request.POST['username']} to user "
f"{request.user.username} ({request.user.email}) at {client_ip(request)}")
return response
def log_request(request: AuthenticatedHttpRequest, description: str):
logger.info(f"User {request.user.username} ({request.user.email}) requested "
f"to {description} from {client_ip(request)}")
def redirect_with_error(request: AuthenticatedHttpRequest, operation: str, err: Error,
redirect_to: str, *redir_args, **redir_kwargs) -> HttpResponse:
logger.info(f"Request by user {request.user.username} ({request.user.email}) failed. "
f"Operation={operation}, Reason={err.desc}")
messages.error(request, err.desc)
return redirect(redirect_to, permanent=False, *redir_args, **redir_kwargs)
def json_with_error(request: AuthenticatedHttpRequest, operation: str, err: Error) -> JsonResponse:
logger.info(f"Request by user {request.user.username} ({request.user.email}) failed. "
f"Operation={operation}, Reason={err.desc}")
return JsonResponse(status=err.status, data={'error': err.desc})
@login_required
def index(request: AuthenticatedHttpRequest) -> HttpResponse:
clean_credentials = []
clean_memberships = []
clean_topics = []
creds_result = engine.get_user_credentials(request.user, request.user)
if not creds_result:
# we don't use redirect_with_error for these, because the place we would usually redirect would be
# the index, and we don't want an infinite redirect loop if something goes wrong.
messages.error(request, creds_result.err().desc)
else:
for cred in creds_result.ok():
clean_credentials.append({
'username': cred.username,
'created_at': cred.created_at.strftime("%Y/%m/%d %H:%M"),
'description': cred.description
})
memberships_result = engine.get_user_group_memberships(request.user, request.user)
if not memberships_result:
messages.error(request, memberships_result.err().desc)
else:
for membership in memberships_result.ok():
clean_memberships.append({
'group_id': membership.group.id,
'group_name': membership.group.name,
'status': membership.status.name,
'member_count': membership.group.members.count(),
})
topics_result = engine.get_user_accessible_topics(request.user, request.user)
if not topics_result:
messages.error(request, topics_result.err().desc)
else:
for topic, means in topics_result.ok():
clean_topics.append({
'topic': topic.name,
'topic_description': topic.description,
'accessible_by': means
})
return render(
request, 'hopskotch_auth/index.html',
{'credentials': clean_credentials, 'memberships': clean_memberships,
'accessible_topics': clean_topics})
def login(request: AuthenticatedHttpRequest) -> HttpResponse:
if request.user.is_authenticated:
return redirect(settings.LOGIN_REDIRECT_URL)
return render(request, 'hopskotch_auth/login.html',)
def logout(request: AuthenticatedHttpRequest) -> HttpResponse:
return render(request, 'hopskotch_auth/logged_out.html',)
def login_failure(request: AuthenticatedHttpRequest) -> HttpResponse:
return render(request, 'hopskotch_auth/login_failure.html')
@login_required
def services(request: AuthenticatedHttpRequest) -> HttpResponse:
if not request.user.is_authenticated:
return render(request, 'hopskotch_auth/services_not_logged_in.html')
mlist = settings.OPENMMA_MAILINGLIST
openmma_subscription = engine.user_is_member_of_mailinglist(request.user, mlist)
return render(request, 'hopskotch_auth/services.html',
{"openmma_subscription": openmma_subscription.ok()})
@login_required
def create_credential(request: AuthenticatedHttpRequest) -> HttpResponse:
if request.method == 'POST':
log_request(request, "create a new credential")
form = CreateCredentialForm(request.POST)
description = request.POST['desc_field']
cred_result = engine.new_credential(request.user, description)
if not cred_result:
return redirect_with_error(request, "create_credential", cred_result.err(), 'index')
username = cred_result.ok()['username']
password = cred_result.ok()['password']
messages.warning(request, 'PLEASE READ. This information will only be displayed once. '
'Please copy this information down and/or download it as a CSV file via the button below')
return render(request, 'hopskotch_auth/finished_credential.html',
{
'cred_username': username,
'cred_password': password,
'cred_description': description,
})
return render(request, 'hopskotch_auth/create_credential.html')
@login_required
def suspend_credential(request: AuthenticatedHttpRequest, credname: str='', redirect_to: str='index') -> HttpResponse:
log_request(request, f"suspend credential {credname}")
cred_result = engine.get_credential(request.user, credname)
if not cred_result:
messages.error(request, cred_result.err().desc)
else:
cred = cred_result.ok()
if cred.suspended:
result = engine.unsuspend_credential(request.user, cred)
else:
result = engine.suspend_credential(request.user, cred)
if not result:
messages.error(request, result.err().desc)
if redirect_to == 'index':
return redirect('index')
elif redirect_to == 'admin':
return redirect('admin_credential')
return redirect('index')
@login_required
def manage_credential(request: AuthenticatedHttpRequest, credname: str) -> HttpResponse:
if request.method == 'POST':
log_request(request, f"update credential {credname} description")
# TODO: data pulled from request.POST must be sanitized
result = engine.update_credential(request.user, credname, request.POST['desc_field'])
if not result:
return redirect_with_error(request, "manage_credential", result.err(), 'index')
return HttpResponseRedirect(request.path_info)
log_request(request, f"manage credential {credname}")
cred_result = engine.get_credential(request.user, credname)
if not cred_result:
return redirect_with_error(request, "manage_credential", cred_result.err(), 'index')
cred = cred_result.ok()
# Get all currently added permissions
perms_result = engine.get_credential_permissions(request.user, cred.owner, cred.username)
if not perms_result:
return redirect_with_error(request, "manage_credential", perms_result.err(), 'index')
# TODO: this code makes no sense; a credential has permissions to topics, and may have several for a
# given topic, it does not have topics themselves
cred_topic_perms = {}
for perm in perms_result.ok():
if perm.topic.name not in cred_topic_perms:
cred_topic_perms[perm.topic.name] = {
'topic': perm.topic.name,
'description': perm.topic.description,
'access_via': perm.parent.principal.name,
'permissions': []
}
cred_topic_perms[perm.topic.name]['permissions'].append(perm.operation._name_)
added_topics = []
easy_lookup = []
for perm in perms_result.ok():
if perm.topic.name not in easy_lookup:
easy_lookup.append(perm.topic.name)
added_topics.append({
'topic': perm.topic.name,
'description': perm.topic.description,
'access_via': perm.parent.principal.name,
})
avail_perms = engine.get_available_credential_permissions(request.user, cred.owner)
if not avail_perms:
return redirect_with_error(request, "manage_credential", avail_perms.err(), 'index')
avail_topics = {}
for perm in avail_perms.ok():
if perm[1]!="Read" and perm[1]!="Write":
# To keep things simple, expose only read and write permissions in the GUI.
# Other permissions can be added via the API, but needing to do so is rare.
continue
if perm[0].topic.name in cred_topic_perms \
and (perm[1] in cred_topic_perms[perm[0].topic.name]["permissions"] or \
"All" in cred_topic_perms[perm[0].topic.name]["permissions"]):
continue;
if perm[0].topic.name not in avail_topics:
avail_topics[perm[0].topic.name]={
'topic': perm[0].topic.name,
'topic_description': perm[0].topic.description,
'accessible_by': perm[0].principal.name,
'permissions': [perm[1]]
}
else:
avail_topics[perm[0].topic.name]["permissions"].append(perm[1])
return render(request,
'hopskotch_auth/manage_credential.html',
{
'accessible_topics': list(avail_topics.values()),
'added_topics': list(cred_topic_perms.values()),
'cur_username': cred.username,
'cur_desc': cred.description})
@login_required
def create_group(request: AuthenticatedHttpRequest) -> HttpResponse:
if request.method == 'POST':
log_request(request, f"create a group with name {request.POST.get('name_field','<unset>')}")
groupname = request.POST['name_field']
descfield = request.POST['desc_field']
create_result = engine.create_group(request.user, groupname, descfield)
if not create_result:
return redirect_with_error(request, "create_group", create_result.err(), 'index')
return redirect('manage_group_members', groupname)
users_result = engine.get_all_users()
if not users_result:
return redirect_with_error(request, "create_group", users_result.err(), 'index')
form = CreateGroupForm()
return render(request, 'hopskotch_auth/create_group.html', {'form': form, 'accessible_members': users_result.ok()})
# TODO: does this serve any purpose?
@login_required
def finished_group(request: AuthenticatedHttpRequest) -> HttpResponse:
# Capture all objects in post, then submit to database
return render(request, 'hopskotch_auth/index.html')
@login_required
def create_topic(request: AuthenticatedHttpRequest) -> HttpResponse:
groups_result = engine.get_user_group_memberships(request.user, request.user)
if not groups_result:
return redirect_with_error(request, "create_topic", groups_result.err(), 'index')
all_groups = groups_result.ok()
owned_groups = []
available_groups = []
for membership in all_groups:
if membership == MembershipStatus.Owner:
owned_groups.append(membership.group)
available_groups.append(membership.group)
if request.method == 'POST':
if request.POST['submit'].lower() == 'select':
owner = request.POST['submit_owner']
form = CreateTopicForm(owning_group=owner)
# TODO: Why is this a list since it appears it should contain only a single group?
available_groups = [group for group in available_groups if group['group_name'] != owner]
return render(request, 'hopskotch_auth/create_topic.html',
{'form': form, 'all_groups': available_groups, 'owning_group': owner})
elif request.POST['submit'].lower() == 'create':
log_request(request, f"create a topic with name {request.POST.get('name_field','<unset>')}"
f" owned by group {request.POST.get('owning_group_field','<unset>')}")
owning_group_name = request.POST['owning_group_field']
topic_name = request.POST['name_field']
create_result = engine.create_topic(
request.user,
owning_group_name,
topic_name,
request.POST['desc_field'],
True if 'visibility_field' in request.POST else False
)
if not create_result:
return redirect_with_error(request, "create_topic", Error('Topic creation failed, please try again. '
'Reason: '+groups_result.err().desc, 400), 'index')
topic_name = owning_group_name+'.'+topic_name
for x in request.POST:
# TODO: among other issues, this encoding does not cover the full range of possible permissions
if x.startswith('group_name['):
idx = x[len('group_name['):-1]
group_name = request.POST[f'group_name[{idx}]']
read_perm = True if f'read_[{idx}]' in request.POST else False
write_perm = True if f'write_[{idx}]' in request.POST else False
if read_perm:
perm_result = engine.add_group_topic_permission(request.user, group_name,
topic_name, KafkaOperation.Read)
if not perm_result:
messages.error(request=request, message=f'Failed to add read permission to {group_name}')
if write_perm:
perm_result = engine.add_group_topic_permission(request.user, group_name,
topic_name, KafkaOperation.Write)
if not perm_result:
messages.error(request=request, message=f'Failed to add write permission to {group_name}')
messages.success(request=request, message='Topic created successfully')
return redirect('index') # TODO: should redirect to the management page for the topic
else:
messages.warning(request=request, message='Something went wrong')
return redirect('index')
form = CreateTopicForm()
owner_form = SelectOwnerForm()
return render(request, 'hopskotch_auth/create_topic.html', {'owner_form': owner_form, 'form': form, 'owned_groups': owned_groups, 'all_groups': available_groups})
@login_required
def manage_topic(request, topicname) -> HttpResponse:
if request.method == 'POST':
log_request(request, "modify the topic with name "
f"{request.POST.get('owning_group_field','<unset>')}."
f"{request.POST.get('name_field','<unset>')}")
# TODO: splitting the topic name up and putting it back together like this is not necessary and will probably confuse users
full_topic_name = '{}.{}'.format(
request.POST['owning_group_field'],
request.POST['name_field']
)
topic_result = engine.get_topic(full_topic_name)
if not topic_result:
return redirect_with_error(request, "manage_topic", topic_result.err(), 'index')
topic = topic_result.ok()
# TODO: multiplexing different types of requests through the same function makes precise logging of requests difficult; this needs to be cleaned up
if 'desc_field' in request.POST:
update_result = engine.update_topic_description(request.user, topic, request.POST['desc_field'])
if not update_result:
return redirect_with_error(request, "manage_topic", update_result.err(), request.path_info)
# The state of the visibility_field checkbox is signalled by whether it is included in the
# POST data at all. If this signal differs from the current state of the topic's public
# visibility, then we must invert it
if topic.publicly_readable != ('visibility_field' in request.POST):
update_result = engine.update_topic_public_readability(request.user, topic, ('visibility_field' in request.POST))
if not update_result:
return redirect_with_error(request, "manage_topic", update_result.err(), request.path_info)
if topic.archivable != ('archive_field' in request.POST):
update_result = engine.update_topic_archiving(request.user, topic, ('archive_field' in request.POST))
if not update_result:
return redirect_with_error(request, "manage_topic", update_result.err(), request.path_info)
return HttpResponseRedirect(request.path_info)
topic_result = engine.get_topic(topicname)
if not topic_result:
return redirect_with_error(request, "manage_topic", topic_result.err(), 'index')
topic = topic_result.ok()
access_result = engine.get_groups_with_access_to_topic(request.user, topic)
if not access_result:
return redirect_with_error(request, "manage_topic", access_result.err(), 'index')
existing_perms = access_result.ok()
groups_result = engine.get_all_groups()
if not groups_result:
return redirect_with_error(request, "manage_topic", groups_result.err(), 'index')
groups_available = groups_result.ok()
groups_with_access = {}
for perm in existing_perms:
if perm.principal not in groups_with_access:
groups_with_access[perm.principal] = {
"name": perm.principal.name,
"permissions": [],
}
groups_with_access[perm.principal]["permissions"].append(perm.operation._name_)
other_groups = {}
for group in groups_available:
for op in [KafkaOperation.Read, KafkaOperation.Write]:
if group not in groups_with_access or op.name not in groups_with_access[group]["permissions"]:
if group not in other_groups:
other_groups[group] = {
"name": group.name,
"permissions": []
}
other_groups[group]["permissions"].append(op.name)
short_name = topic.name[len(topic.owning_group.name)+1:] if topic.name.startswith(topic.owning_group.name+'.') else topic.name
return render(request,
'hopskotch_auth/manage_topic.html',
{'topic_owner': topic.owning_group.name,
'topic_name': short_name,
'topic_desc': topic.description,
'is_visible': topic.publicly_readable,
'is_archivable': topic.archivable,
'all_groups': list(other_groups.values()),
'group_list': list(groups_with_access.values())}
)
@login_required
def manage_group_members(request, groupname) -> HttpResponse:
if request.method == 'POST':
log_request(request, f"modify the description of the group with name {groupname}")
description = request.POST['desc_field']
modify_result = engine.modify_group_description(request.user, groupname, description)
if not modify_result:
return redirect_with_error(request, "modify_group_description", modify_result.err(),
'manage_group_members', groupname=groupname)
else:
messages.success(request, 'Successfully modified description')
return redirect('manage_group_members', groupname=groupname)
users_result = engine.get_all_users()
if not users_result:
return redirect_with_error(request, "modify_group_description", users_result.err(), 'index')
users = users_result.ok()
group_result = engine.get_group(groupname)
if not group_result:
return redirect_with_error(request, "modify_group_description", group_result.err(), 'index')
group = group_result.ok()
members_result = engine.get_group_members(request.user, group)
if not members_result:
return redirect_with_error(request, "modify_group_description", members_result.err(), 'index')
members = members_result.ok()
# TODO: Quadratic-ish complexity needs fixing
# NOTE: Still quadratic (O(n*m)) but it looks better :)
cleaned_users = []
for member in members:
if member.user in users:
users.remove(member.user)
clean_members = [{'username': m.user.username, 'name': (m.user.last_name + ', ' + m.user.first_name), 'email': m.user.email, 'status': m.status.name} for m in members]
return render(request, 'hopskotch_auth/manage_group_members.html',
{'members': clean_members, 'accessible_members': users,
'groupname': groupname, 'cur_name': group.name, 'cur_description': group.description})
@login_required
def manage_group_topics(request, groupname) -> HttpResponse:
if request.method == 'POST':
log_request(request, f"modify the description of the group with name {groupname}")
description = request.POST['desc_field']
modify_result = engine.modify_group_description(request.user, groupname, description)
if not modify_result:
return redirect_with_error(request, "manage_group_topics", modify_result.err(),
'manage_group_topics', groupname=groupname)
else:
messages.success(request, 'Successfully modified description')
return redirect('manage_group_topics', groupname=groupname)
group_result = engine.get_group(groupname)
if not group_result:
return redirect_with_error(request, "manage_group_topics", group_result.err(), 'index')
group = group_result.ok()
topics_result = engine.get_group_topics(request.user, groupname)
if not topics_result:
return redirect_with_error(request, "manage_group_topics", topics_result.err(), 'index')
topics = topics_result.ok()
# TODO: Is this supposed to be the topics the group owns (from get_group_topics) or the topics
# to which the group has some access (from get_group_accessible_topics)
clean_topics_added = {}
for topic in topics:
if topic.name not in clean_topics_added:
clean_topics_added[topic.name] = {
'topicname': topic.name,
'description': topic.description,
#'accessible_by': topic['accessible_by'], # not valid for a group
}
return render(request, 'hopskotch_auth/manage_group_topics.html',
{'topics': clean_topics_added.values(), 'groupname': group.name,
'cur_name': group.name, 'cur_description': group.description })
@admin_required
@login_required
def admin_credential(request: AuthenticatedHttpRequest) -> HttpResponse:
log_request(request, "manage all credentials")
creds_result = engine.get_all_credentials(request.user)
if not creds_result:
return redirect_with_error(request, "admin_credential", creds_result.err(), 'index')
clean_creds = [{
'username': credential.owner.username,
'credname': credential.username,
'created_at': credential.created_at,
'suspended': credential.suspended,
'description': credential.description,
} for credential in creds_result.ok()]
clean_creds.sort(key=lambda item: item["credname"])
return render(request, 'hopskotch_auth/admin_credential.html', {'all_credentials': clean_creds})
@admin_required
@login_required
def admin_topic(request: AuthenticatedHttpRequest) -> HttpResponse:
log_request(request, "manage all topics")
topics_result = engine.get_all_topics(request.user)
if not topics_result:
return redirect_with_error(request, "admin_topic", topics_result.err(), 'index')
clean_topics = [{
'owning_group': topic.owning_group.name,
'name': topic.name,
'description': topic.description,
'public': "public" if topic.publicly_readable else "",
} for topic in topics_result.ok()]
clean_topics.sort(key=lambda item: item["name"])
return render(request, 'hopskotch_auth/admin_topic.html', {'all_topics': clean_topics})
@admin_required
@login_required
def admin_group(request: AuthenticatedHttpRequest) -> HttpResponse:
log_request(request, "manage all groups")
groups_result = engine.get_all_groups()
if not groups_result:
return redirect_with_error(request, "admin_group", groups_result.err(), 'index')
clean_groups = [{
'name': group.name,
'description': group.description,
'members': [member.user.username
for member in engine.get_group_members(request.user, group).ok()]
} for group in groups_result.ok()]
for group in clean_groups:
group['mem_count'] = len(group['members'])
clean_groups.sort(key=lambda item: item["name"])
return render(request, 'hopskotch_auth/admin_group.html', {'all_groups': clean_groups})
def add_credential_permission(request: AuthenticatedHttpRequest) -> JsonResponse:
log_request(request, f"add a/an {request.POST.get('perm_perm','<unset>')} permission "
f"for topic {request.POST.get('perm_name','<unset>')} to credential "
f"{request.POST.get('credname','<unset>')}")
if request.method != 'POST' or \
'credname' not in request.POST or \
'perm_name' not in request.POST or \
'perm_perm' not in request.POST:
return JsonResponse(status=400, data={
'error': 'Bad request'
})
credname = request.POST['credname']
topic_name = request.POST['perm_name']
perm_perm = request.POST['perm_perm']
operation = KafkaOperation[perm_perm]
add_result = engine.add_credential_permission(request.user, credname, topic_name, operation)
if not add_result:
return json_with_error(request, "add_credential_permission", add_result.err())
return JsonResponse(data={}, status=200)
def remove_credential_permission(request: AuthenticatedHttpRequest) -> JsonResponse:
log_request(request, f"remove a/an {request.POST.get('perm_perm','<unset>')} permission "
f"for topic {request.POST.get('perm_name','<unset>')} to credential "
f"{request.POST.get('credname','<unset>')}")
if request.method != 'POST' or \
'credname' not in request.POST or \
'perm_name' not in request.POST or \
'perm_perm' not in request.POST:
return json_with_error(request, "remove_credential_permission", Error("Invalid request", 400))
credname = request.POST['credname']
topic_name = request.POST['perm_name']
perm_perm = request.POST['perm_perm']
operation = KafkaOperation[perm_perm]
remove_result = engine.remove_credential_permission(request.user, credname, topic_name, operation)
if not remove_result:
return json_with_error(request, "remove_credential_permission", remove_result.err())
return JsonResponse(data={}, status=200)
# TODO: Purpose of these functions completely impossible to determine from names.
# They appear to never be used?
'''
def add_topic_group(request: AuthenticatedHttpRequest) -> HttpResponse:
topic_name = request.POST['topic_name']
owning_group = request.POST['owning_group']
new_group = request.POST['group_name']
operation = request.POST['group_perm']
full_topic_name = f'{owning_group}.{topic_name}'
status_code, _ = engine.add_group_to_topic(full_topic_name, new_group, operation)
if status_code is not None:
messages.error(request, status_code)
return redirect('manage_topic', full_topic_name)
def remove_topic_group(request: AuthenticatedHttpRequest) -> HttpResponse:
topic_name = request.POST['topic_name']
owning_group = request.POST['owning_group']
new_group = request.POST['group_name']
operation = request.POST['group_perm']
full_topic_name = f'{owning_group}.{topic_name}'
status_code, _ = engine.remove_group_from_topic(full_topic_name, new_group, operation)
if status_code is not None:
messages.error(request, status_code)
return redirect('manage_topic', full_topic_name)
@login_required
def add_group_topic(request: AuthenticatedHttpRequest) -> HttpResponse:
print(request.POST)
topicname = request.POST['topicname']
groupname = topicname.split('.')[0]
permission = request.POST['op_perm']
status_code, _ = engine.add_topic_to_group(groupname, topicname, permission)
if status_code is not None:
messages.error(request, status_code)
return redirect('index')
return redirect('manage_group_topic', groupname)
@login_required
def remove_group_topic(request: AuthenticatedHttpRequest) -> HttpResponse:
topicname = request.POST['topicname']
perm = request.POST['topic_pub']
groupname = request.POST['groupname']
status_code, _ = engine.remove_topic_from_group(groupname, topicname, perm)
if status_code is not None:
messages.error(request, status_code)
return redirect('index')
return redirect('manage_group_topics', groupname)
'''
@login_required
def get_topic_permissions(request: AuthenticatedHttpRequest) -> JsonResponse:
log_request(request, f"fetch group {request.POST.get('groupname','<unset>')}'s"
f" permissions for topic {request.POST.get('topicname','<unset>')}")
groupname = request.POST['groupname']
topicname = request.POST['topicname']
perms_result = engine.get_group_permissions_for_topic(request.user, groupname, topicname)
if not perms_result:
return json_with_error(request, "get_topic_permissions", perms_result.err())
data = [str(p.operation) for p in perms_result.ok()]
return JsonResponse(data={'data': data}, status=200)
# TODO: Why is this called '_in_group'?
@login_required
def create_topic_in_group(request: AuthenticatedHttpRequest) -> JsonResponse:
log_request(request, f"create a topic named {request.POST.get('topicname','<unset>')},"
f" owned by group {request.POST.get('groupname','<unset>')}")
groupname = request.POST['groupname']
topicname = request.POST['topicname']
create_result = engine.create_topic(request.user, groupname, topicname, '', False)
if not create_result:
return json_with_error(request, "create_topic", create_result.err())
full_topic_name = '{}.{}'.format(groupname, topicname)
editpath = reverse('manage_topic', args=(full_topic_name,))
return JsonResponse(data={'editpath': editpath}, status=200)
# TODO: Function name makes no sense; unclear what this should do. Appears to be unused?
'''
@login_required
def add_topic_to_group(request: AuthenticatedHttpRequest) -> JsonResponse:
topicname = request.POST['topicname']
groupname = request.POST['groupname']
status_code, _ = engine.add_topic_to_group(groupname, topicname)
if status_code is not None:
return JsonResponse(data={'error': status_code}, status=404)
return JsonResponse(data={}, status=200)
'''
# TODO: Function name makes no sense; unclear what this should do. Appears to be unused?
'''
@login_required
def remove_topic_from_group(request: AuthenticatedHttpRequest) -> JsonResponse:
topicname = request.POST['topicname']
groupname = request.POST['groupname']
status_code, _ = engine.delete_topic(request.user.username, groupname, topicname)
if status_code is not None:
return JsonResponse(data={'error': status_code}, status=404)
return JsonResponse(data={}, status=200)
'''
# TODO: Appears to be unused
'''
@login_required
def get_available_credential_topics(request: AuthenticatedHttpRequest) -> JsonResponse:
credname = request.POST['credname']
topicname = request.POST['topicname']
perms_result = engine.get_available_credential_permissions(request.user.username)
if not perms_result:
return json_with_error(request, "get_available_credential_topics", perms_result.err(), 400)
# TODO: Why is this filtering by tpic name?
# Computing all possible permissions is fairly expensive, we should do it as few times as possible,
# not repeat for each topic. If the UI wants to display split by topic it should do that itself.
possible_perms = []
for perm in avail_perms:
if perm['topic'] == topicname:
possible_perms.append(perm)
existing_result = engine.get_credential_permissions_for_topic(credname, topicname)
if not existing_result:
return json_with_error(request, "get_available_credential_topics", existing_result.err(), 400)
cred_perms = [str(p.operation) for p in existing_result.ok()]
return JsonResponse(data={'data': possible_perms, 'cred_data': cred_perms}, status=200)
'''
# TODO: When is this operation useful?
@login_required
def add_all_credential_permission(request: AuthenticatedHttpRequest) -> JsonResponse:
log_request(request, f"add all permission to topic {request.POST.get('topicname','<unset>')}"
f" to credential {request.POST.get('credname','<unset>')}")
credname = request.POST['credname']
topicname = request.POST['topicname']
existing_result = engine.get_credential_permissions_for_topic(request.user, credname, topicname)
if not existing_result:
return json_with_error(request, "add_all_credential_permission", existing_result.err())
existing = existing_result.ok()
if any(p.operation == KafkaOperation.All for p in existing):
# Nothing to do
return JsonResponse(data={}, status=200)
# Remove all individual permissions
for existing_perm in existing:
remove_result = engine.remove_credential_permission(request.user, credname, topicname, existing_perm.operation)
if not remove_result:
return json_with_error(request, "add_all_credential_permission", remove_result.err())
# add the All permission
add_result = engine.add_credential_permission(request.user, credname, topicname, KafkaOperation.All)
if not add_result:
return json_with_error(request, "add_all_credential_permission", add_result.err())
return JsonResponse(data={}, status=200)
'''
def get_user_available_permissions(user):
possible_permissions = {}
for membership in user.groupmembership_set.all():
group = membership.group
group_permissions = GroupKafkaPermission.objects.filter(principal=group).select_related('topic')
for permission in group_permissions:
if permission.topic.name not in possible_permissions:
possible_permissions[permission.topic.name] = {
'topic': permission.topic.name,
'description': permission.topic.description,
'access_via': permission.principal.name,
'operation': permission.operation.name
}
possible_permissions = list(possible_permissions.values())
# sort and eliminate duplicates
# sort on operation
#possible_permissions.sort(key=lambda p: p[1])
possible_permissions.sort(key=lambda p: p['operation'])
# sort on topic names, because that looks nice for users, but since there is a bijection
# between topic names and IDs this will place all matching topic IDs together in blocks
# in some order
#possible_permissions.sort(key=lambda p: p[0])
possible_permissions.sort(key=lambda p: p['topic'])
def equivalent(p1, p2):
return p1['topic'] == p2['topic']
#return p1['topic_id'] == p2['topic_id'] and p1['operation'] == p2['operation']
#return p1[0] == p2[0] and p1[-1] == p2[-1]
# remove adjacent (practical) duplicates which have different permission IDs
dedup = []
last = None
for p in possible_permissions:
if last is None or not equivalent(last,p):
dedup.append(p)
last=p
return dedup
'''