Skip to content

Commit 0184a9f

Browse files
authored
feat: adds functionality to view all users access lists (#54)
Users with specific permission can view all users access list.
1 parent 76b6f2a commit 0184a9f

File tree

9 files changed

+599
-23
lines changed

9 files changed

+599
-23
lines changed

Access/helpers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,4 @@ def getPossibleApproverPermissions():
8282
approver_permissions = each_module.fetch_approver_permissions()
8383
all_approver_permissions.extend(approver_permissions.values())
8484
return list(set(all_approver_permissions))
85+

Access/models.py

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,23 @@ def get_access_history(self, all_access_modules):
233233
)
234234

235235
return access_history
236+
237+
@staticmethod
238+
def get_user_from_username(username):
239+
try:
240+
return User.objects.get(user__username=username)
241+
except User.DoesNotExist:
242+
return None
243+
244+
def get_accesses_by_access_tag_and_status(self, access_tag, status):
245+
try:
246+
user_identities = self.module_identity.filter(access_tag=access_tag)
247+
except UserIdentity.DoesNotExist:
248+
return None
249+
return UserAccessMapping.objects.filter(
250+
user_identity__in=user_identities,
251+
access__access_tag=access_tag,
252+
status__in=status)
236253

237254
def __str__(self):
238255
return "%s" % (self.user)
@@ -636,19 +653,30 @@ def getAccessRequestDetails(self, access_module):
636653
# code metadata
637654
access_request_data["access_tag"] = access_tag
638655
# ui metadata
639-
access_request_data["userEmail"] = self.user.email
656+
access_request_data["user"] = self.user_identity.user.name
657+
access_request_data["userEmail"] = self.user_identity.user.email
640658
access_request_data["requestId"] = self.request_id
641659
access_request_data["accessReason"] = self.request_reason
642-
access_request_data["requested_on"] = self.requested_on
660+
access_request_data["requested_on"] = str(self.requested_on)[:19] + "UTC" if self.updated_on else ""
643661

644-
access_request_data["accessType"] = access_module.access_desc()
662+
access_request_data["access_desc"] = access_module.access_desc()
645663
access_request_data["accessCategory"] = access_module.combine_labels_desc(
646664
access_labels
647665
)
648666
access_request_data["accessMeta"] = access_module.combine_labels_meta(
649667
access_labels
650668
)
669+
access_request_data["access_label"] = [key + "-" + str(val).strip("[]")
670+
for key,val in list(self.access.access_label.items())
671+
if key != "keySecret"]
672+
access_request_data["access_type"] = self.access_type
673+
access_request_data["approver_1"] = self.approver_1.user.username
674+
access_request_data["approver_2"] = self.approver_2.user.username
675+
access_request_data["approved_on"] = self.approved_on
676+
access_request_data["updated_on"] = str(self.updated_on)[:19] + "UTC" if self.updated_on else ""
651677
access_request_data["status"] = self.status
678+
access_request_data["revoker"] = self.revoker.user.username
679+
access_request_data["offboarding_date"] = str(self.user_identity.user.offbaord_date)[:19] + "UTC" if self.user_identity.user.offbaord_date else ""
652680
access_request_data["revokeOwner"] = ",".join(access_module.revoke_owner())
653681
access_request_data["grantOwner"] = ",".join(access_module.grant_owner())
654682

@@ -663,6 +691,19 @@ def updateMetaData(self, key, data):
663691
mapping.save()
664692
return True
665693

694+
def revoke(self, revoker):
695+
self.status = "Revoked"
696+
self.revoker = revoker
697+
self.save()
698+
699+
@staticmethod
700+
def get_accesses_not_declined():
701+
return UserAccessMapping.objects.exclude(status='Declined')
702+
703+
@staticmethod
704+
def get_unrevoked_accesses_by_request_id(request_id):
705+
return UserAccessMapping.objects.filter(request_id=request_id).exclude(status='Revoked')
706+
666707
def is_approved(self):
667708
return self.status == "Approved"
668709

Access/notifications.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,11 @@ def send_new_group_approved_notification(group, group_id, initial_member_names):
5656

5757
def send_membership_accepted_notification(user, group, membership):
5858
subject = MEMBERSHIP_ACCEPTED_SUBJECT.format(user.name, group.name)
59-
body = MEMBERSHIP_ACCEPTED_BODY.format(
60-
user.name, group.name, membership.approver.name
59+
body = helpers.generateStringFromTemplate(
60+
filename="membershipAcceptedEmailBody.html",
61+
user_name=user.name,
62+
group_name=group.name,
63+
approver=membership.approver.name,
6164
)
6265
destination = []
6366
destination.append(membership.requested_by.email)

Access/views.py

Lines changed: 129 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
from django.contrib.auth.decorators import login_required
22
from django.core.exceptions import ValidationError
3+
from django.core.paginator import Paginator
4+
from django.contrib.auth.models import User as djangoUser
5+
from .models import UserAccessMapping
6+
from Access import views_helper
37
from django.http import JsonResponse
48
from django.shortcuts import render
59
from rest_framework.authentication import TokenAuthentication, BasicAuthentication
@@ -22,7 +26,7 @@
2226
from Access.views_helper import render_error_message
2327
from BrowserStackAutomation.settings import PERMISSION_CONSTANTS
2428

25-
INVALID_REQUEST_MESSAGE = "Error in request not found OR Invalid request type - "
29+
INVALID_REQUEST_MESSAGE = "Error in request not found OR Invalid request type"
2630

2731
logger = logging.getLogger(__name__)
2832

@@ -131,14 +135,6 @@ def createNewGroup(request):
131135
return render(request, "BSOps/createNewGroup.html", {})
132136

133137

134-
@api_view(["GET"])
135-
@login_required
136-
@user_with_permission(["VIEW_USER_ACCESS_LIST"])
137-
@authentication_classes((TokenAuthentication, BasicAuthentication))
138-
def allUserAccessList(request, load_ui=True):
139-
return False
140-
141-
142138
@login_required
143139
def allUsersList(request):
144140
context = getallUserList(request)
@@ -260,9 +256,9 @@ def accept_bulk(request, selector):
260256
context["returnIds"] = returnIds
261257
return JsonResponse(context, status=200)
262258
except Exception as e:
263-
logger.debug(INVALID_REQUEST_MESSAGE + str(str(e)))
259+
logger.debug(INVALID_REQUEST_MESSAGE + " - " + str(str(e)))
264260
json_response = {}
265-
json_response["error"] = INVALID_REQUEST_MESSAGE + str(str(e))
261+
json_response['error'] = INVALID_REQUEST_MESSAGE + " - " + str(str(e))
266262
json_response["success"] = False
267263
json_response["status_code"] = 401
268264
return JsonResponse(json_response, status=json_response["status_code"])
@@ -277,3 +273,125 @@ def remove_group_member(request):
277273
except Exception as e:
278274
logger.exception(str(e))
279275
return JsonResponse({"error": "Failed to remove the user"}, status=400)
276+
277+
278+
@api_view(['GET'])
279+
@login_required
280+
@user_with_permission(["VIEW_USER_ACCESS_LIST"])
281+
@authentication_classes((TokenAuthentication, BasicAuthentication))
282+
def all_user_access_list(request, load_ui=True):
283+
user = None
284+
page = 1
285+
try:
286+
if request.GET.get('username'):
287+
username = request.GET.get('username')
288+
user = djangoUser.objects.get(username=username)
289+
except Exception as e:
290+
# show all
291+
logger.exception(e)
292+
293+
try:
294+
data_list = []
295+
last_page = 1
296+
show_tabs = False
297+
username = ""
298+
generic_accesses = UserAccessMapping.get_accesses_not_declined()
299+
response_type = request.GET.get('responseType', "ui")
300+
load_ui = request.GET.get('load_ui', "true").lower() == "true"
301+
record_date = request.GET.get('recordDate', None)
302+
303+
if user:
304+
generic_accesses = generic_accesses.filter(
305+
user_identity__user=user.user).order_by("-requested_on")
306+
show_tabs = True
307+
username = user.username
308+
elif "usersearch" in request.GET:
309+
generic_accesses = generic_accesses.filter(
310+
user_identity__user__user__username__icontains=request.GET.get('usersearch')) \
311+
.order_by("user_identity__user__user__username")
312+
else:
313+
generic_accesses = generic_accesses.order_by("user_identity__user__user__username")
314+
315+
filters = views_helper.get_filters_for_access_list(request)
316+
generic_accesses = generic_accesses.filter(**filters)
317+
318+
page = int(request.GET.get('page', 1))
319+
320+
if load_ui and response_type != "csv":
321+
paginator_obj = Paginator(generic_accesses, 10)
322+
last_page = paginator_obj.num_pages
323+
page = min(page, last_page) if page > last_page else page
324+
paginator = paginator_obj.page(page)
325+
else:
326+
paginator = generic_accesses
327+
328+
access_types = list(set(generic_accesses.values_list("access__access_tag", flat=True)))
329+
330+
data_list = views_helper.prepare_datalist(paginator=paginator, record_date=record_date)
331+
332+
context = {}
333+
logger.debug(data_list)
334+
335+
data_dict = {
336+
'dataList': data_list,
337+
'last_page': last_page,
338+
'current_page': page,
339+
'access_types': sorted(access_types, key=str.casefold),
340+
'show_tabs': show_tabs,
341+
'username': username
342+
}
343+
344+
context.update(data_dict)
345+
346+
if response_type == "json":
347+
return JsonResponse(context, status=200)
348+
elif response_type == "csv":
349+
return views_helper.gen_all_user_access_list_csv(data_list=data_list)
350+
if load_ui:
351+
return render(request, 'BSOps/allUserAccessList.html', context)
352+
else:
353+
return JsonResponse(context)
354+
355+
except Exception as e:
356+
logger.debug("Error in request not found OR Invalid request type")
357+
logger.exception(e)
358+
json_response = {}
359+
json_response['error'] = {'error_msg': str(e), 'msg': INVALID_REQUEST_MESSAGE}
360+
return render(request, 'BSOps/accessStatus.html', json_response)
361+
362+
363+
@login_required
364+
@user_with_permission(["VIEW_USER_ACCESS_LIST"])
365+
def mark_revoked(request):
366+
json_response = {}
367+
status = 200
368+
request_id = None
369+
try:
370+
request_id = request.GET.get("requestId")
371+
if request_id.startswith("module-"):
372+
username = request.GET.get("username")
373+
if not username:
374+
json_response["error"] = "Username is invalid!"
375+
status = 403
376+
return JsonResponse(json_response, status=status)
377+
access_tag = request_id.split("-", 1)[1]
378+
user = User.get_user_from_username(username=username)
379+
if user:
380+
requests = user.get_accesses_by_access_tag_and_status(access_tag=access_tag, status=["Approved", "Offboarding"])
381+
else:
382+
raise User.DoesNotExist(f"User with username '{username}' does not exist")
383+
else:
384+
requests = UserAccessMapping.get_unrevoked_accesses_by_request_id(request_id=request_id)
385+
success_list = []
386+
for mapping_object in requests:
387+
logger.info("Marking access revoke - %s by user %s "
388+
% (mapping_object.request_id, request.user.user))
389+
mapping_object.revoke(revoker=request.user.user)
390+
success_list.append(mapping_object.request_id)
391+
json_response["msg"] = "Success"
392+
json_response["request_ids"] = success_list
393+
except Exception as e:
394+
logger.exception(str(e))
395+
json_response["error"] = str(e)
396+
status = 403
397+
return JsonResponse(json_response, status=status)

Access/views_helper.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from django.shortcuts import render
2+
from django.http import HttpResponse
23
import datetime
34
import logging
45
import traceback
56

7+
import csv
68
from . import helpers as helper
79
from .models import UserAccessMapping, GroupAccessMapping
810
from bootprocess import general
@@ -70,7 +72,10 @@ def executeGroupAccess(userMappingsList):
7072
if "other" in mappingObj.request_id:
7173
decline_group_other_access(mappingObj)
7274
else:
73-
background_task("run_access_grant", (mappingObj.request_id, mappingObj, accessType, user, approver))
75+
background_task("run_access_grant",
76+
(mappingObj.request_id,
77+
mappingObj, accessType,
78+
user, approver))
7479
logger.debug(
7580
"Successful group access grant for " + mappingObj.request_id
7681
)
@@ -109,3 +114,56 @@ def render_error_message(request, log_message, user_message, user_message_descri
109114
"msg": user_message_description,
110115
}
111116
})
117+
118+
119+
def get_filters_for_access_list(request):
120+
filters = {}
121+
if "accessTag" in request.GET:
122+
filters['access__access_tag__icontains'] = request.GET.get('accessTag')
123+
if "accessTagExact" in request.GET:
124+
filters['access__access_tag'] = request.GET.get('accessTagExact')
125+
if "status" in request.GET:
126+
filters['status__icontains'] = request.GET.get('status')
127+
if "type" in request.GET:
128+
filters['access_type__icontains'] = request.GET.get('type')
129+
return filters
130+
131+
132+
def prepare_datalist(paginator, record_date):
133+
data_list = []
134+
for each_access_request in paginator:
135+
if record_date is not None and record_date != str(each_access_request.updated_on)[:10]:
136+
continue
137+
access_details = get_generic_user_access_mapping(each_access_request)
138+
data_list.append(access_details)
139+
return data_list
140+
141+
142+
def gen_all_user_access_list_csv(data_list):
143+
logger.debug("Processing CSV response")
144+
response = HttpResponse(content_type='text/csv')
145+
filename = "AccessList-" + str(datetime.datetime.now().strftime('%Y-%m-%d_%H:%M:%S')) + ".csv"
146+
response['Content-Disposition'] = 'attachment; filename="' + filename + '"'
147+
148+
writer = csv.writer(response)
149+
writer.writerow(['User', 'AccessType', 'Access', 'AccessStatus',
150+
'RequestDate', 'Approver', 'GrantOwner',
151+
'RevokeOwner', 'Type'])
152+
for data in data_list:
153+
access_status = data["status"]
154+
if len(data["revoker"]) > 0:
155+
access_status += " by - " + data["revoker"]
156+
writer.writerow([data['user'], data['access_desc'],
157+
(", ".join(data["access_label"])),
158+
access_status, data["requested_on"],
159+
data["approver_1"], data["grantOwner"],
160+
data["revokeOwner"], data["access_type"]])
161+
return response
162+
163+
def get_generic_user_access_mapping(user_access_mapping):
164+
access_module = helper.get_available_access_module_from_tag(
165+
user_access_mapping.access.access_tag)
166+
if access_module:
167+
access_details = user_access_mapping.getAccessRequestDetails(access_module)
168+
logger.debug("Generic access generated: " + str(access_details))
169+
return access_details

BrowserStackAutomation/urls.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
updateUserInfo,
2626
saveIdentity,
2727
createNewGroup,
28-
allUserAccessList,
28+
all_user_access_list,
29+
mark_revoked,
2930
allUsersList,
3031
requestAccess,
3132
group_access,
@@ -44,6 +45,7 @@
4445
re_path(r"^$", dashboard, name="dashboard"),
4546
re_path(r"^login/$", auth_views.LoginView.as_view(), name="login"),
4647
re_path(r"^logout/$", logout_view, name="logout"),
48+
re_path(r"^access/markRevoked", mark_revoked, name="markRevoked"),
4749
re_path(r"^oauth/", include("social_django.urls", namespace="social")),
4850
re_path(r"^access/showAccessHistory$", showAccessHistory, name="showAccessHistory"),
4951
re_path(r"^access/pendingRequests$", pendingRequests, name="pendingRequests"),
@@ -53,7 +55,7 @@
5355
re_path(r"^user/saveIdentity/", saveIdentity, name="saveIdentity"),
5456
re_path(r"^group/create$", createNewGroup, name="createNewGroup"),
5557
re_path(r"^group/dashboard/$", groupDashboard, name="groupDashboard"),
56-
re_path(r"^access/userAccesses$", allUserAccessList, name="allUserAccessList"),
58+
re_path(r"^access/userAccesses$", all_user_access_list, name="allUserAccessList"),
5759
re_path(r"^access/usersList$", allUsersList, name="allUsersList"),
5860
re_path(r"^access/requestAccess$", requestAccess, name="requestAccess"),
5961
re_path(r"^group/requestAccess$", group_access, name="groupRequestAccess"),

0 commit comments

Comments
 (0)