import operator
import re
from django.forms import model_to_dict
from django.shortcuts import render, get_object_or_404, redirect
from django.http import HttpResponse
from django.contrib.auth.decorators import login_required
from .models import (
RBACGroup,
RBACUserGroup,
RBACGroupRole,
RBACAdminUserGroup,
RBACAdminGroup,
RBACAdminGroupRole,
RBACAdminTree,
RBACAppModelAction,
RBACModelDefault,
)
from accounts.models import User
from .core import (
rbac_add_user_to_group,
rbac_user_is_group_admin,
rbac_access_in_english,
rbac_admin_all_rights,
rbac_get_admins_for_group,
rbac_user_role_list,
rbac_user_has_role,
rbac_user_is_role_admin,
rbac_user_has_role_explain,
rbac_get_groups_for_role,
rbac_user_blocked_for_model,
rbac_user_allowed_for_model,
role_to_parts,
rbac_get_users_with_role,
rbac_admin_tree_access,
rbac_user_has_any_model,
rbac_user_is_admin_for_admin_group,
)
from cobalt.settings import TIME_ZONE, COBALT_HOSTNAME
from .forms import AddGroup
from django.contrib import messages
from django.utils import timezone
from organisations.models import Organisation
import os
import datetime
import pytz
[docs]
@login_required
def rbac_forbidden(request, role, htmx=False):
"""RBAC screen for forbidden access - gives the user more info than a
normal error screen.
If htmx value is set, then we return a simpler template view
"""
groups = rbac_get_groups_for_role(role)
template = "rbac/forbidden_htmx.html" if htmx else "rbac/forbidden.html"
return render(request, template, {"role": role, "groups": groups})
[docs]
@login_required
def view_screen(request):
"""Shows the user what roles they have in RBAC"""
groups = RBACGroup.objects.filter(rbacusergroup__member=request.user)
# split by type
# data = {}
# for user_group in user_groups:
# if user_group.group.name_qualifier in data:
# data[user_group.group.name_qualifier].append(user_group.group)
# else:
# data[user_group.group.name_qualifier] = [user_group.group]
#
# group_list = user_groups.values_list("group")
roles = RBACGroupRole.objects.filter(group__in=groups)
english = rbac_access_in_english(request.user)
return render(
request,
"rbac/view-screen.html",
{"groups": groups, "english": english, "roles": roles},
)
[docs]
def generic_tree_builder(groups, detail_link=None, html_type="href"):
"""function to build an rbac tree for use with the tree viewer.
Args:
groups - queryset of RBACGroup or RBACAdminGroup
detail_link - str of link to follow
html_type - str to specify whether to generate <a> or <button>
Returns:
str - HTML string to insert into page
build a list of the tree. We want to turn:
abf.people.fred (id=34)
abf.people.john (id=45)
abf.animals.dogs.rover (id=2)
abf.animals.cats.felix (id=21)
into:
items["abf"]=["people", "animals"]
items["abf.people"]=["fred", "john"]
items["abf.people.fred"]=34
items["abf.people.john"]=45
items["abf.animals"]=["dogs", "cats"]
items["abf.animals.dogs"]=["rover"]
items["abf.animals.cats"]=["felix"]
items["abf.animals.dogs.rover"]=2
items["abf.animals.cats.felix"]=21
"""
# Tree is hard to navigate for the parts that have numbers instead of club names
# We get the club numbers(pks) and names and substitute to make more readable
all_orgs = Organisation.objects.all()
# turn to dictionary
all_orgs_dict = {org.pk: org.name for org in all_orgs}
items = {}
items_description = {}
for group in groups:
line = f"{group.name_qualifier}.{group.name_item}"
parts = line.split(".")
for i in range(1, len(parts) + 1):
string = ".".join(parts[:i])
if i == len(parts): # end of line
child = group.id
items_description[group.id] = group.description
else: # more left
child = parts[i]
if string in items:
if child not in items[string]:
items[string].append(child)
else:
items[string] = [child]
# sort the dictionary on keys
sorted_items = dict(sorted(items.items()))
""" generate html - loop through the sorted dictionary and do 3 things:
1) if there is more below create a new ul
2) if we are the end of the tree (contents is an integer) print the link to the details
3) manage the stack so we close the right number of uls
"""
html_tree = ""
depth = []
for key, value in sorted_items.items():
this_level = ".".join(key.split(".")[:-1]) # eg we are a.b.c level=a.b
# are we at the top?
if not depth:
depth.append(this_level)
elif this_level == depth[-1]:
pass
elif this_level.find(depth[-1]) == 0:
depth.append(this_level)
else:
while depth:
if depth[-1] == this_level:
break
depth = depth[:-1]
html_tree += "</ul>\n"
# Make tree display more readable for generated branches
# Change key from "something.generated.<state>.<number>" to "something.generated.<state>.<club name>"
match = re.findall("generated\\.(\\w+)\\.(\\d+)", key)
if match:
this_state = match[0][0]
this_org = int(match[0][1])
key = key.replace(
f"generated.{this_state}.{this_org}",
f"generated.{this_state}.{all_orgs_dict[this_org]}",
)
# now process line
last_part = key.split(".")[-1]
if isinstance(value[0], int):
# This is the end of the line
if html_type == "button":
html_tree += (
"<li>%s (%s) <button value='%s' class='tree-btn cobalt-rbac-tree btn btn-sm btn-primary'>Use</button></li>\n"
% (last_part, items_description[value[0]], key)
)
elif html_type == "href":
html_tree += (
"<li><a href='%s%s/' target='_blank'>%s (%s)</a></li>\n"
% (
detail_link,
value[0],
last_part,
items_description[value[0]],
)
)
else:
html_tree += (
"<li><span class='caret'>%s</span><ul class='nested'>\n" % last_part
)
return html_tree
[docs]
def generic_tree_screen(request, groups, detail_link, title):
"""Show full RBAC Tree for RBAC or Admin"""
html_tree = generic_tree_builder(groups, detail_link)
return render(
request, "rbac/tree-screen.html", {"html_tree": html_tree, "title": title}
)
[docs]
@login_required
def tree_screen(request):
"""Show full RBAC Tree"""
# Get groups
groups = RBACGroup.objects.all().order_by("name_qualifier")
return generic_tree_screen(request, groups, "/rbac/group/view/", "Tree Viewer")
[docs]
@login_required
def list_screen(request):
"""Show full RBAC Tree as a table"""
# Get groups
groups = (
RBACGroup.objects.prefetch_related("rbacgrouprole_set", "rbacusergroup_set")
.all()
.order_by("name_qualifier")
)
for group in groups:
roles = RBACGroupRole.objects.filter(group=group)
group.roles = roles
members = RBACUserGroup.objects.filter(group=group)
group.members = members
return render(request, "rbac/list_screen.html", {"groups": groups})
[docs]
@login_required
def list_roles_screen(request):
"""Show full RBAC Tree as a table - order by roles"""
# Get roles
roles = (
RBACGroupRole.objects.prefetch_related("group", "group__rbacusergroup_set")
.all()
.order_by("app", "model", "model_id")
)
for role in roles:
groups = (
RBACGroup.objects.filter(rbacgrouprole__app=role.app)
.filter(rbacgrouprole__model=role.model)
.filter(rbacgrouprole__model_id=role.model_id)
.filter(rbacgrouprole__action=role.action)
.distinct()
)
role.groups = groups
members = (
RBACUserGroup.objects.filter(group__rbacgrouprole__app=role.app)
.filter(group__rbacgrouprole__model=role.model)
.filter(group__rbacgrouprole__model_id=role.model_id)
.filter(group__rbacgrouprole__action=role.action)
.distinct()
)
role.members = members
return render(request, "rbac/list_roles_screen.html", {"roles": roles})
[docs]
@login_required
def list_members_screen(request):
"""Show full RBAC Tree as a table order by members"""
# Get members
members = (
RBACUserGroup.objects.prefetch_related("group", "group__rbacgrouprole_set")
.all()
.order_by("member", "member__first_name")
.distinct("member")
)
for member in members:
roles = RBACGroupRole.objects.filter(group__rbacusergroup__member=member.member)
member.roles = roles
groups = RBACGroup.objects.filter(rbacusergroup__member=member.member)
member.groups = groups
return render(request, "rbac/list_members_screen.html", {"members": members})
[docs]
@login_required
def admin_tree_screen(request):
"""Show full RBAC Admin Tree"""
# Get groups
groups = RBACAdminGroup.objects.all().order_by("name_qualifier")
return generic_tree_screen(
request, groups, "/rbac/admin/group/view/", "Admin Tree Viewer"
)
[docs]
@login_required
def role_view_screen(request):
"""Show Roles"""
# Get groups
roles = RBACAppModelAction.objects.all().order_by("app", "model", "valid_action")
defaults = RBACModelDefault.objects.all().order_by("app", "model")
return render(
request, "rbac/role_view.html", {"roles": roles, "defaults": defaults}
)
[docs]
@login_required
def group_view(request, group_id):
"""view to show details of a group"""
group = get_object_or_404(RBACGroup, pk=group_id)
users = RBACUserGroup.objects.filter(group=group)
roles = RBACGroupRole.objects.filter(group=group)
is_admin = rbac_user_is_group_admin(request.user, group)
admins = rbac_get_admins_for_group(group)
warning = None
if group.name_qualifier.find(".generated.") > 0:
warning = "This is a generated group."
return render(
request,
"rbac/group_view.html",
{
"users": users,
"roles": roles,
"group": group,
"is_admin": is_admin,
"admins": admins,
"warning": warning,
},
)
[docs]
@login_required
def admin_group_view(request, group_id):
"""view to show details of an admin group"""
group = get_object_or_404(RBACAdminGroup, pk=group_id)
users = RBACAdminUserGroup.objects.filter(group=group)
roles = RBACAdminGroupRole.objects.filter(group=group)
trees = RBACAdminTree.objects.filter(group=group)
user_list = users.values_list("member", flat=True)
is_admin = request.user.id in user_list
return render(
request,
"rbac/admin_group_view.html",
{
"users": users,
"roles": roles,
"group": group,
"trees": trees,
"is_admin": is_admin,
},
)
[docs]
@login_required
def group_delete(request, group_id):
"""view to delete a group"""
group = get_object_or_404(RBACGroup, pk=group_id)
if not rbac_user_is_group_admin(request.user, group):
return HttpResponse("You are not an admin for this group")
if request.method == "POST":
group.delete()
messages.success(
request,
"Group successfully deleted.",
extra_tags="cobalt-message-success",
)
return redirect("rbac:tree_screen")
return render(request, "rbac/group_delete.html", {"group": group})
[docs]
@login_required
def admin_group_delete(request, group_id):
"""view to delete an admin group"""
group = get_object_or_404(RBACAdminGroup, pk=group_id)
if not rbac_user_is_admin_for_admin_group(request.user, group):
return HttpResponse("You are not an admin for this group")
if request.method == "POST":
group.delete()
messages.success(
request,
"Admin Group successfully deleted.",
extra_tags="cobalt-message-success",
)
return redirect("rbac:admin_tree_screen")
return render(request, "rbac/admin_group_delete.html", {"group": group})
[docs]
@login_required
def group_create(request):
"""view to create a new group"""
if request.method == "POST":
form = AddGroup(request.POST, user=request.user, environment="rbac")
if form.is_valid():
group = RBACGroup(
name_item=form.cleaned_data["name_item"],
name_qualifier=form.cleaned_data["name_qualifier"],
description=form.cleaned_data["description"],
created_by=request.user,
)
group.save()
messages.success(
request,
"Group successfully created.",
extra_tags="cobalt-message-success",
)
if form.cleaned_data["add_self"]:
rbac_add_user_to_group(request.user, group)
messages.success(
request,
"Added you to new group.",
extra_tags="cobalt-message-success",
)
return redirect("rbac:group_edit", group_id=group.id)
else:
form = AddGroup(user=request.user, environment="rbac")
return render(request, "rbac/group_create.html", {"form": form})
[docs]
@login_required
def admin_group_create(request):
"""view to create a new admin group"""
if request.method == "POST":
form = AddGroup(request.POST, user=request.user, environment="admin")
if form.is_valid():
group = RBACAdminGroup(
name_item=form.cleaned_data["name_item"],
name_qualifier=form.cleaned_data["name_qualifier"],
description=form.cleaned_data["description"],
created_by=request.user,
)
group.save()
messages.success(
request,
"Admin Group successfully created.",
extra_tags="cobalt-message-success",
)
if form.cleaned_data["add_self"]:
mapping = RBACAdminUserGroup(group=group, member=request.user)
mapping.save()
messages.success(
request,
"Added you to group %s." % group,
extra_tags="cobalt-message-success",
)
return redirect("rbac:admin_group_view", group_id=group.id)
else:
form = AddGroup(user=request.user, environment="admin")
return render(request, "rbac/admin_group_create.html", {"form": form})
[docs]
@login_required
def group_edit(request, group_id):
"""view to edit a group"""
group = get_object_or_404(RBACGroup, pk=group_id)
if not rbac_user_is_group_admin(request.user, group):
return HttpResponse("You are not an admin for this group")
if request.method == "POST":
form = AddGroup(request.POST, user=request.user, environment="rbac")
if form.is_valid():
group.name_item = form.cleaned_data["name_item"]
group.description = form.cleaned_data["description"]
group.save()
messages.success(
request,
"Group successfully updated.",
extra_tags="cobalt-message-success",
)
else:
print(form.errors)
else:
form = AddGroup(user=request.user, environment="rbac")
form.fields["name_item"].initial = group.name_item
form.fields["description"].initial = group.description
users = RBACUserGroup.objects.filter(group=group)
admin_roles = rbac_admin_all_rights(request.user)
roles = RBACGroupRole.objects.filter(group=group)
warning = None
if group.name_qualifier.find(".generated.") > 0:
warning = "This is a generated group. It is strongly advised that you do not edit this here."
return render(
request,
"rbac/group_edit.html",
{
"form": form,
"group": group,
"users": users,
"roles": roles,
"admin_roles": admin_roles,
"warning": warning,
},
)
[docs]
@login_required
def admin_group_edit(request, group_id):
"""view to edit an admin group"""
group = get_object_or_404(RBACAdminGroup, pk=group_id)
# could use rbac_user_is_admin_for_admin_group but we need the
# users anyway so more efficient to do it here
users = RBACAdminUserGroup.objects.filter(group=group)
user_list = users.values_list("member", flat=True)
if request.user.id not in user_list:
return HttpResponse("You are not an admin for this admin group")
if request.method == "POST":
form = AddGroup(request.POST, user=request.user, environment="admin")
if form.is_valid():
group.name_item = form.cleaned_data["name_item"]
group.description = form.cleaned_data["description"]
group.save()
messages.success(
request,
"Admin Group successfully updated.",
extra_tags="cobalt-message-success",
)
else:
print(form.errors)
else:
form = AddGroup(user=request.user, environment="admin")
form.fields["name_item"].initial = group.name_item
form.fields["description"].initial = group.description
roles = RBACAdminGroupRole.objects.filter(group=group).distinct()
trees = RBACAdminTree.objects.filter(group=group)
admin_roles = rbac_admin_all_rights(request.user)
return render(
request,
"rbac/admin_group_edit.html",
{
"form": form,
"group": group,
"users": users,
"roles": roles,
"trees": trees,
"admin_roles": admin_roles,
},
)
[docs]
@login_required
def rbac_admin(request):
"""shows the admin groups a user is in"""
group_list = RBACAdminUserGroup.objects.filter(member=request.user).values_list(
"group"
)
groups = RBACAdminGroup.objects.filter(id__in=group_list)
roles = RBACAdminGroupRole.objects.filter(group__in=group_list)
group_list = RBACAdminUserGroup.objects.filter(member=request.user).values_list(
"group"
)
trees = RBACAdminTree.objects.filter(group__in=group_list)
return render(
request,
"rbac/admin-screen.html",
{"groups": groups, "roles": roles, "trees": trees},
)
[docs]
@login_required
def rbac_tests(request):
"""easy way to underlying functions. Test only"""
ans = None
userid = None
user = None
text = ""
group = None
role = None
model = None
last_query = None
if request.method == "POST":
userid = request.POST.get("id_user")
if userid:
user = get_object_or_404(User, pk=userid)
text = request.POST.get("id_text", "")
if "user_has_role" in request.POST:
ans = rbac_user_has_role(user, text)
if not ans:
ans = "False" # bool -> string so it shows on screen
last_query = "User Has Role"
role = text
if "user_has_role_explain" in request.POST:
ans = rbac_user_has_role_explain(user, text)
last_query = "User Has Role Explain"
role = text
if "user_access_in_english" in request.POST:
ans = ""
for row in rbac_access_in_english(user):
role, description = row
ans = f"{ans}\n{role.role} - {description}"
last_query = "User Access in English"
if "user_blocked_for_model" in request.POST:
(app, model, model_instance, action) = role_to_parts(text)
ans = rbac_user_blocked_for_model(user, app, model, action)
if not ans:
ans = "Nothing found."
last_query = "User Blocked for Model"
role = text
if "user_allowed_for_model" in request.POST:
(app, model, model_instance, action) = role_to_parts(text)
(everything, ans) = rbac_user_allowed_for_model(user, app, model, action)
if everything:
ans = "Allowed for all models"
elif not ans:
ans = "Nothing found."
last_query = "User Allowed for Model"
model = text
if "get_users_with_role" in request.POST:
users = rbac_get_users_with_role(text)
if users:
ans = "".join("%s\n" % user_inst for user_inst in users)
else:
ans = "Nothing found"
last_query = "Get Users With Role"
role = text
user = None
user = None
if "get_admins_for_group" in request.POST:
last_query = "Get Admins for Group"
parts = text.split(".")
name_qualifier = ".".join(parts[:-1])
name_item = parts[-1]
group = RBACGroup.objects.filter(
name_qualifier=name_qualifier, name_item=name_item
).first()
if group:
admins = rbac_get_admins_for_group(group)
ans = "".join("%s\n" % admin.member for admin in admins)
else:
ans = "Group not found"
group = text
if "user_is_group_admin" in request.POST:
last_query = "User Is Group Admin"
parts = text.split(".")
name_qualifier = ".".join(parts[:-1])
name_item = parts[-1]
group = RBACGroup.objects.filter(
name_qualifier=name_qualifier, name_item=name_item
).first()
if group:
ans = rbac_user_is_group_admin(user, group)
if not ans:
ans = "False" # bool -> string so it shows on screen
else:
ans = "Group not found"
group = text
if "user_is_role_admin" in request.POST:
ans = rbac_user_is_role_admin(user, text)
if not ans:
ans = "False"
last_query = "User Is Role Admin"
role = text
if "admin" in request.POST:
last_query = "Admin"
rights = rbac_admin_all_rights(user)
ans = "\n".join(rights)
if not ans:
ans = "Nothing found."
if "admin-tree" in request.POST:
last_query = "Admin Tree"
rights = rbac_admin_tree_access(user)
ans = "\n".join(rights)
if not ans:
ans = "Nothing found."
# Get trees
groups = RBACGroup.objects.all().order_by("name_qualifier")
tree = generic_tree_builder(groups, html_type="button")
admin_groups = RBACAdminGroup.objects.all().order_by("name_qualifier")
admin_tree = generic_tree_builder(admin_groups, html_type="button")
# get models
models = RBACModelDefault.objects.all().order_by("app", "model")
# Get roles
roles = RBACAppModelAction.objects.all().order_by("app", "model", "valid_action")
# format ans for the Sweet Alert popup
# if ans.type() == 'str':
# ans_swal = ans.replace("'", "")
# ans_swal = ans_swal.replace("\n", "\\\n")
# else:
ans_swal = ans
return render(
request,
"rbac/tests.html",
{
"ans": ans_swal,
"ans_swal": ans_swal,
"member_id": userid,
"text": text,
"tree": tree,
"admin_tree": admin_tree,
"models": models,
"roles": roles,
"group": group,
"role": role,
"model": model,
"user": user,
"last_query": last_query,
},
)
[docs]
def get_rbac_statistics():
"""get basic stats on RBAC, called by utils statistics"""
rbac_groups = RBACGroup.objects.count()
rbac_roles = RBACGroupRole.objects.count()
rbac_users = RBACUserGroup.objects.count()
rbac_users_distinct = RBACUserGroup.objects.distinct("member").count()
rbac_admin_users_distinct = RBACAdminUserGroup.objects.distinct("member").count()
return {
"rbac_users_distinct": rbac_users_distinct,
"rbac_users": rbac_users,
"rbac_admin_users_distinct": rbac_admin_users_distinct,
"rbac_roles": rbac_roles,
"rbac_groups": rbac_groups,
}