mirror of
https://gitlab.com/allianceauth/allianceauth.git
synced 2025-07-09 04:20:17 +02:00
Permissions Auditing Tool (#698)
* Added block for page_title as title fragment * Add permissions auditing tool * Added tests for permissions audit tool * Added documentation for permissions tool * Add permissions tool to coverage
This commit is contained in:
parent
489b9a601d
commit
b636262e0c
@ -11,6 +11,7 @@ source =
|
||||
hrapplications
|
||||
notifications
|
||||
optimer
|
||||
permissions_tool
|
||||
services
|
||||
srp
|
||||
timerboard
|
||||
|
@ -65,6 +65,7 @@ INSTALLED_APPS = [
|
||||
'fleetactivitytracking',
|
||||
'notifications',
|
||||
'esi',
|
||||
'permissions_tool',
|
||||
'geelweb.django.navhelper',
|
||||
'bootstrap_pagination',
|
||||
|
||||
|
@ -54,6 +54,7 @@ INSTALLED_APPS = [
|
||||
'fleetactivitytracking',
|
||||
'notifications',
|
||||
'esi',
|
||||
'permissions_tool',
|
||||
'geelweb.django.navhelper',
|
||||
'bootstrap_pagination',
|
||||
'services.modules.mumble',
|
||||
@ -141,7 +142,7 @@ SUPERUSER_STATE_BYPASS = 'True' == os.environ.get('AA_SUPERUSER_STATE_BYPASS', '
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/1.10/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = os.environ.get('AA_LANGUAGE_CODE', 'en-us')
|
||||
LANGUAGE_CODE = os.environ.get('AA_LANGUAGE_CODE', 'en')
|
||||
|
||||
TIME_ZONE = os.environ.get('AA_TIME_ZONE', 'UTC')
|
||||
|
||||
|
@ -17,6 +17,7 @@ import notifications.views
|
||||
import hrapplications.views
|
||||
import corputils.urls
|
||||
import esi.urls
|
||||
import permissions_tool.urls
|
||||
from alliance_auth import NAME
|
||||
|
||||
admin.site.site_header = NAME
|
||||
@ -211,6 +212,8 @@ urlpatterns += i18n_patterns(
|
||||
url(r'^fat/link/$', fleetactivitytracking.views.fatlink_view, name='auth_click_fatlink_view'),
|
||||
url(r'^fat/link/(?P<hash>[a-zA-Z0-9]+)/(?P<fatname>[a-z0-9_-]+)/$',
|
||||
fleetactivitytracking.views.click_fatlink_view),
|
||||
|
||||
url(r'^permissions/', include(permissions_tool.urls))
|
||||
)
|
||||
|
||||
# Append hooked service urls
|
||||
|
@ -3,8 +3,9 @@
|
||||
```eval_rst
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:caption: Contents
|
||||
:caption: Features Contents
|
||||
|
||||
hrapplications
|
||||
corpstats
|
||||
permissions_tool
|
||||
```
|
||||
|
39
docs/features/permissions_tool.md
Normal file
39
docs/features/permissions_tool.md
Normal file
@ -0,0 +1,39 @@
|
||||
# Permissions Auditing
|
||||
|
||||
```eval_rst
|
||||
.. note::
|
||||
New in 1.15
|
||||
```
|
||||
|
||||
Access to most of Alliance Auth's features are controlled by Django's permissions system. In order to help you secure your services, Alliance Auth provides a permissions auditing tool.
|
||||
|
||||
### Access
|
||||
|
||||
In order to grant users access to the permissions auditing tool they will need to be granted the `permissions_tool.audit_permissions` permission or be a superuser.
|
||||
|
||||
When a user has access to the tool they will see the "Permissions Audit" menu item under the "Util" sub menu.
|
||||
|
||||
|
||||
### Permissions Overview
|
||||
|
||||
The first page gives you a general overview of permissions and how many users have access to each permission.
|
||||
|
||||

|
||||
|
||||
**App**, **Model** and **Code Name** contain the internal details of the permission while **Name** contains the name/description you'll see in the admin panel.
|
||||
|
||||
**Users** is the number of users explicitly granted this permission on their account.
|
||||
|
||||
**Groups** is the number of groups with this permission assigned.
|
||||
|
||||
**Groups Users** is the total number of users in all of the groups with this permission assigned.
|
||||
|
||||
Clicking on the **Code Name** link will take you to the [Permissions Audit Page](#permissions-audit-page)
|
||||
|
||||
### Permissions Audit Page
|
||||
|
||||
The permissions audit page will give you an overview of all the users who have access to this permission either directly or granted via group membership.
|
||||
|
||||

|
||||
|
||||
Please note that users may appear multiple times if this permission is granted via multiple sources.
|
0
permissions_tool/__init__.py
Normal file
0
permissions_tool/__init__.py
Normal file
8
permissions_tool/apps.py
Normal file
8
permissions_tool/apps.py
Normal file
@ -0,0 +1,8 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ServicesConfig(AppConfig):
|
||||
name = 'permissions_tool'
|
||||
|
20
permissions_tool/auth_hooks.py
Normal file
20
permissions_tool/auth_hooks.py
Normal file
@ -0,0 +1,20 @@
|
||||
from services.hooks import MenuItemHook
|
||||
from alliance_auth import hooks
|
||||
|
||||
|
||||
class PermissionsTool(MenuItemHook):
|
||||
def __init__(self):
|
||||
MenuItemHook.__init__(self,
|
||||
'Permissions Audit',
|
||||
'fa fa-key fa-id-card grayiconecolor',
|
||||
'permissions_overview', 400)
|
||||
|
||||
def render(self, request):
|
||||
if request.user.has_perm('permissions_tool.audit_permissions'):
|
||||
return MenuItemHook.render(self, request)
|
||||
return ''
|
||||
|
||||
|
||||
@hooks.register('menu_util_hook')
|
||||
def register_menu():
|
||||
return PermissionsTool()
|
26
permissions_tool/migrations/0001_initial.py
Normal file
26
permissions_tool/migrations/0001_initial.py
Normal file
@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.5 on 2017-02-06 08:58
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PermissionsTool',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
],
|
||||
options={
|
||||
'managed': False,
|
||||
'permissions': (('audit_permissions', 'Can audit permissions'),),
|
||||
},
|
||||
),
|
||||
]
|
0
permissions_tool/migrations/__init__.py
Normal file
0
permissions_tool/migrations/__init__.py
Normal file
12
permissions_tool/models.py
Normal file
12
permissions_tool/models.py
Normal file
@ -0,0 +1,12 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class PermissionsTool(models.Model):
|
||||
"""
|
||||
Dummy model for holding permissions
|
||||
"""
|
||||
class Meta:
|
||||
managed = False
|
||||
permissions = (
|
||||
('audit_permissions', 'Can audit permissions'),
|
||||
)
|
39
permissions_tool/templates/permissions_tool/audit.html
Normal file
39
permissions_tool/templates/permissions_tool/audit.html
Normal file
@ -0,0 +1,39 @@
|
||||
{% extends "public/base.html" %}
|
||||
{% load bootstrap %}
|
||||
{% load staticfiles %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_title %}{{ permission.permission.codename }} - {% trans "Permissions Audit" %}{% endblock page_title %}
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
<h1 class="page-header">{% trans "Permissions Audit" %}: {{ permission.permission.codename }}</h1>
|
||||
<a href="{% url 'permissions_overview' %}" class="btn btn-default">
|
||||
<i class="glyphicon glyphicon-chevron-left"></i> {% trans "Back" %}
|
||||
</a>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-md-3">
|
||||
{% trans "Group" %}
|
||||
</th>
|
||||
<th class="col-md-3">
|
||||
{% trans "User" %}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in permission.users %}
|
||||
<tr>
|
||||
{% include 'permissions_tool/audit_row.html' with group="Permission Granted Directly (No Group)" %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% for group in permission.groups %}
|
||||
{% for user in group.user_set.all %}
|
||||
{% include 'permissions_tool/audit_row.html' %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock content %}
|
10
permissions_tool/templates/permissions_tool/audit_row.html
Normal file
10
permissions_tool/templates/permissions_tool/audit_row.html
Normal file
@ -0,0 +1,10 @@
|
||||
<tr>
|
||||
<td>
|
||||
{% if forloop.first %}
|
||||
<b>{{ group }}</b>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ user }}
|
||||
</td>
|
||||
</tr>
|
79
permissions_tool/templates/permissions_tool/overview.html
Normal file
79
permissions_tool/templates/permissions_tool/overview.html
Normal file
@ -0,0 +1,79 @@
|
||||
{% extends "public/base.html" %}
|
||||
{% load bootstrap %}
|
||||
{% load staticfiles %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_title %}{% trans "Permissions Overview" %}{% endblock page_title %}
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
<h1 class="page-header">{% trans "Permissions Overview" %}</h1>
|
||||
{% if request.GET.all != 'yes' %}
|
||||
<span class="pull-right">
|
||||
{% blocktrans %}Showing only applied permissions{% endblocktrans %}
|
||||
<a href="{% url 'permissions_overview' %}?all=yes" class="btn btn-primary">{% trans "Show All" %}</a>
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="pull-right">
|
||||
{% blocktrans %}Showing all permissions{% endblocktrans %}
|
||||
<a href="{% url 'permissions_overview' %}?all=no" class="btn btn-primary">{% trans "Show Applied" %}</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
{% trans "App" %}
|
||||
</th>
|
||||
<th>
|
||||
{% trans "Model" %}
|
||||
</th>
|
||||
<th>
|
||||
{% trans "Code Name" %}
|
||||
</th>
|
||||
<th>
|
||||
{% trans "Name" %}
|
||||
</th>
|
||||
<th class="col-md-1">
|
||||
{% trans "Users" %}
|
||||
</th>
|
||||
<th class="col-md-1">
|
||||
{% trans "Groups" %}
|
||||
</th>
|
||||
<th class="col-md-1">
|
||||
{% trans "Groups Users" %}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for perm in permissions %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ perm.permission.content_type.app_label }}
|
||||
</td>
|
||||
<td>
|
||||
{{ perm.permission.content_type.model }}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url "permissions_audit" app_label=perm.permission.content_type.app_label model=perm.permission.content_type.model codename=perm.permission.codename %}">
|
||||
{{ perm.permission.codename }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{{ perm.permission.name }}
|
||||
</td>
|
||||
<td class="{% if perm.users > 0 %}info {% endif %}text-right">
|
||||
{{ perm.users }}
|
||||
</td>
|
||||
<td class="{% if perm.groups > 0 %}info {% endif %}text-right">
|
||||
{{ perm.groups }}
|
||||
</td>
|
||||
<td class="{% if perm.group_users > 0 %}info {% endif %}text-right">
|
||||
{{ perm.group_users }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock content %}
|
122
permissions_tool/tests.py
Normal file
122
permissions_tool/tests.py
Normal file
@ -0,0 +1,122 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
try:
|
||||
# Py3
|
||||
from unittest import mock
|
||||
except ImportError:
|
||||
# Py2
|
||||
import mock
|
||||
|
||||
from django.test import TestCase
|
||||
from django import urls
|
||||
from django.contrib.auth.models import User, Group, Permission
|
||||
|
||||
from alliance_auth.tests.auth_utils import AuthUtils
|
||||
|
||||
import six
|
||||
|
||||
|
||||
class PermissionsToolViewsTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.member = AuthUtils.create_member('auth_member')
|
||||
self.member.set_password('password')
|
||||
self.member.email = 'auth_member@example.com'
|
||||
self.member.save()
|
||||
self.none_user = AuthUtils.create_user('none_user', disconnect_signals=True)
|
||||
self.none_user2 = AuthUtils.create_user('none_user2', disconnect_signals=True)
|
||||
self.none_user3 = AuthUtils.create_user('none_user3', disconnect_signals=True)
|
||||
|
||||
self.no_perm_user = AuthUtils.create_user('no_perm_user', disconnect_signals=True)
|
||||
self.no_perm_user.set_password('password')
|
||||
|
||||
AuthUtils.disconnect_signals()
|
||||
self.no_perm_group = Group.objects.create(name="No Permission Group")
|
||||
|
||||
self.test_group = Group.objects.create(name="Test group")
|
||||
|
||||
self.test_group.user_set.add(self.none_user)
|
||||
self.test_group.user_set.add(self.none_user2)
|
||||
self.test_group.user_set.add(self.none_user3)
|
||||
|
||||
self.permission = Permission.objects.get_by_natural_key(codename='audit_permissions',
|
||||
app_label='permissions_tool',
|
||||
model='permissionstool')
|
||||
|
||||
self.test_group.permissions.add(self.permission)
|
||||
self.member.user_permissions.add(self.permission)
|
||||
AuthUtils.connect_signals()
|
||||
|
||||
def test_menu_item(self):
|
||||
self.client.login(username=self.member.username, password='password')
|
||||
|
||||
response = self.client.get(urls.reverse('permissions_overview'))
|
||||
|
||||
response_content = response.content
|
||||
if six.PY3:
|
||||
response_content = str(response_content, encoding='utf8')
|
||||
|
||||
self.assertInHTML('<li><a class="active" href="/en/permissions/overview/"><i class="fa fa-key fa-id-card '
|
||||
'grayiconecolor"></i> Permissions Audit</a></li>', response_content)
|
||||
|
||||
def test_permissions_overview(self):
|
||||
self.client.login(username=self.member.username, password='password')
|
||||
|
||||
response = self.client.get(urls.reverse('permissions_overview'))
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed('permissions_tool/overview.html')
|
||||
self.assertContains(response, self.permission.codename)
|
||||
self.assertContains(response, self.permission.content_type.app_label)
|
||||
self.assertContains(response, self.permission.content_type.model)
|
||||
|
||||
tested_context = False
|
||||
# Test the context
|
||||
for perm in response.context['permissions']:
|
||||
if perm['permission'] == self.permission:
|
||||
tested_context = True
|
||||
self.assertDictContainsSubset({'users': 1}, perm)
|
||||
self.assertDictContainsSubset({'groups': 1}, perm)
|
||||
self.assertDictContainsSubset({'group_users': 3}, perm)
|
||||
break
|
||||
self.assertTrue(tested_context)
|
||||
|
||||
def test_permissions_overview_perms(self):
|
||||
# Ensure permission effectively denys access
|
||||
self.client.login(username=self.no_perm_user.username, password='password')
|
||||
|
||||
response = self.client.get(urls.reverse('permissions_overview'))
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
def test_permissions_audit(self):
|
||||
self.client.login(username=self.member.username, password='password')
|
||||
|
||||
response = self.client.get(urls.reverse('permissions_audit',
|
||||
kwargs={
|
||||
'app_label': self.permission.content_type.app_label,
|
||||
'model': self.permission.content_type.model,
|
||||
'codename': self.permission.codename,
|
||||
}))
|
||||
|
||||
self.assertTemplateUsed('permissions_tool/audit.html')
|
||||
self.assertTemplateUsed('permissions_tool/audit_row.html')
|
||||
|
||||
self.assertContains(response, self.permission.codename)
|
||||
self.assertContains(response, self.none_user)
|
||||
self.assertContains(response, self.none_user3)
|
||||
self.assertContains(response, self.test_group)
|
||||
|
||||
self.assertNotContains(response, self.no_perm_user)
|
||||
|
||||
def test_permissions_audit_perms(self):
|
||||
# Ensure permission effectively denys access
|
||||
self.client.login(username=self.no_perm_user.username, password='password')
|
||||
|
||||
response = self.client.get(urls.reverse('permissions_audit',
|
||||
kwargs={
|
||||
'app_label': self.permission.content_type.app_label,
|
||||
'model': self.permission.content_type.model,
|
||||
'codename': self.permission.codename,
|
||||
}))
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
10
permissions_tool/urls.py
Normal file
10
permissions_tool/urls.py
Normal file
@ -0,0 +1,10 @@
|
||||
from __future__ import unicode_literals
|
||||
from django.conf.urls import url
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^overview/$', views.permissions_overview, name='permissions_overview'),
|
||||
url(r'^audit/(?P<app_label>[\w\-_]+)/(?P<model>[\w\-_]+)/(?P<codename>[\w\-_]+)/$', views.permissions_audit,
|
||||
name='permissions_audit'),
|
||||
]
|
55
permissions_tool/views.py
Normal file
55
permissions_tool/views.py
Normal file
@ -0,0 +1,55 @@
|
||||
from __future__ import unicode_literals
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.db.models import Count
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('permissions_tool.audit_permissions')
|
||||
def permissions_overview(request):
|
||||
logger.debug("permissions_overview called by user %s" % request.user)
|
||||
perms = Permission.objects.all()
|
||||
|
||||
get_all = True if request.GET.get('all', 'no') == 'yes' else False
|
||||
|
||||
context = {'permissions': []}
|
||||
for perm in perms:
|
||||
this_perm = {
|
||||
'users': perm.user_set.all().count(),
|
||||
'groups': perm.group_set.all().count(),
|
||||
'permission': perm
|
||||
}
|
||||
|
||||
if get_all or this_perm['users'] > 0 or this_perm['groups'] > 0:
|
||||
# Only add if we're getting everything or one of the objects has this permission
|
||||
# Add group_users separately to improve performance
|
||||
this_perm['group_users'] = sum(group.user_count for group in
|
||||
perm.group_set.annotate(user_count=Count('user')))
|
||||
context['permissions'].append(this_perm)
|
||||
|
||||
return render(request, 'permissions_tool/overview.html', context=context)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('permissions_tool.audit_permissions')
|
||||
def permissions_audit(request, app_label, model, codename):
|
||||
logger.debug("permissions_audit called by user {} on {}:{}:{}".format(request.user, app_label, model, codename))
|
||||
perm = get_object_or_404(Permission,
|
||||
content_type__app_label=app_label,
|
||||
content_type__model=model,
|
||||
codename=codename)
|
||||
|
||||
context = {'permission': {
|
||||
'permission': perm,
|
||||
'users': perm.user_set.all(),
|
||||
'groups': perm.group_set.all(),
|
||||
'group_users': [group.user_set.all() for group in perm.group_set.all()]
|
||||
}
|
||||
}
|
||||
|
||||
return render(request, 'permissions_tool/audit.html', context=context)
|
@ -13,7 +13,7 @@
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
|
||||
<title>{% block title %}Alliance Auth{% endblock title %}</title>
|
||||
<title>{% block title %}{% block page_title %}{% endblock page_title %} - Alliance Auth{% endblock title %}</title>
|
||||
|
||||
<!-- Bootstrap Core CSS -->
|
||||
<link href="{% static 'css/bootstrap.min.css' %}" rel="stylesheet">
|
||||
|
Loading…
x
Reference in New Issue
Block a user