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:
Basraah 2017-02-10 21:25:09 +10:00 committed by GitHub
parent 489b9a601d
commit b636262e0c
19 changed files with 430 additions and 3 deletions

View File

@ -11,6 +11,7 @@ source =
hrapplications hrapplications
notifications notifications
optimer optimer
permissions_tool
services services
srp srp
timerboard timerboard

View File

@ -65,6 +65,7 @@ INSTALLED_APPS = [
'fleetactivitytracking', 'fleetactivitytracking',
'notifications', 'notifications',
'esi', 'esi',
'permissions_tool',
'geelweb.django.navhelper', 'geelweb.django.navhelper',
'bootstrap_pagination', 'bootstrap_pagination',

View File

@ -54,6 +54,7 @@ INSTALLED_APPS = [
'fleetactivitytracking', 'fleetactivitytracking',
'notifications', 'notifications',
'esi', 'esi',
'permissions_tool',
'geelweb.django.navhelper', 'geelweb.django.navhelper',
'bootstrap_pagination', 'bootstrap_pagination',
'services.modules.mumble', 'services.modules.mumble',
@ -141,7 +142,7 @@ SUPERUSER_STATE_BYPASS = 'True' == os.environ.get('AA_SUPERUSER_STATE_BYPASS', '
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/1.10/topics/i18n/ # 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') TIME_ZONE = os.environ.get('AA_TIME_ZONE', 'UTC')

View File

@ -17,6 +17,7 @@ import notifications.views
import hrapplications.views import hrapplications.views
import corputils.urls import corputils.urls
import esi.urls import esi.urls
import permissions_tool.urls
from alliance_auth import NAME from alliance_auth import NAME
admin.site.site_header = 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/$', fleetactivitytracking.views.fatlink_view, name='auth_click_fatlink_view'),
url(r'^fat/link/(?P<hash>[a-zA-Z0-9]+)/(?P<fatname>[a-z0-9_-]+)/$', url(r'^fat/link/(?P<hash>[a-zA-Z0-9]+)/(?P<fatname>[a-z0-9_-]+)/$',
fleetactivitytracking.views.click_fatlink_view), fleetactivitytracking.views.click_fatlink_view),
url(r'^permissions/', include(permissions_tool.urls))
) )
# Append hooked service urls # Append hooked service urls

View File

@ -3,8 +3,9 @@
```eval_rst ```eval_rst
.. toctree:: .. toctree::
:maxdepth: 1 :maxdepth: 1
:caption: Contents :caption: Features Contents
hrapplications hrapplications
corpstats corpstats
permissions_tool
``` ```

View 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.
![permissions overview](https://i.imgur.com/XALVFtc.png)
**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.
![permissions audit](https://i.imgur.com/XjnfC9Z.png)
Please note that users may appear multiple times if this permission is granted via multiple sources.

View File

8
permissions_tool/apps.py Normal file
View File

@ -0,0 +1,8 @@
from __future__ import unicode_literals
from django.apps import AppConfig
class ServicesConfig(AppConfig):
name = 'permissions_tool'

View 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()

View 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'),),
},
),
]

View File

View 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'),
)

View 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 %}

View File

@ -0,0 +1,10 @@
<tr>
<td>
{% if forloop.first %}
<b>{{ group }}</b>
{% endif %}
</td>
<td>
{{ user }}
</td>
</tr>

View 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
View 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
View 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
View 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)

View File

@ -13,7 +13,7 @@
<meta name="description" content=""> <meta name="description" content="">
<meta name="author" 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 --> <!-- Bootstrap Core CSS -->
<link href="{% static 'css/bootstrap.min.css' %}" rel="stylesheet"> <link href="{% static 'css/bootstrap.min.css' %}" rel="stylesheet">