Add support for Find Roles by Permission X feature

This commit is contained in:
Lance Edgar 2017-02-06 13:37:42 -06:00
parent dd5162c151
commit cab339e367
8 changed files with 221 additions and 134 deletions

View file

@ -0,0 +1,67 @@
## -*- coding: utf-8 -*-
<%inherit file="/base.mako" />
<%def name="title()">Find ${model_title_plural} by Permission</%def>
<%def name="extra_javascript()">
${parent.extra_javascript()}
<script type="text/javascript">
<% gcount = len(permissions) %>
var permissions_by_group = {
% for g, (gkey, group) in enumerate(permissions, 1):
<% pcount = len(group['perms']) %>
'${gkey}': {
% for p, (pkey, perm) in enumerate(group['perms'], 1):
'${pkey}': "${perm['label']}"${',' if p < pcount else ''}
% endfor
}${',' if g < gcount else ''}
% endfor
};
$(function() {
$('#permission_group').selectmenu({
change: function(event, ui) {
var perms = $('#permission');
perms.find('option:first').siblings('option').remove();
$.each(permissions_by_group[ui.item.value], function(key, label) {
perms.append($('<option value="' + key + '">' + label + '</option>'));
});
perms.selectmenu('refresh');
}
});
$('#permission').selectmenu();
$('#find-by-perm-form').submit(function() {
$('.newgrid').remove();
$(this).find('#submit').button('disable').button('option', 'label', "Searching, please wait...");
});
});
</script>
</%def>
${h.form(request.current_route_url(), id='find-by-perm-form')}
${h.csrf_token(request)}
<div class="form">
${self.wtfield(form, 'permission_group')}
${self.wtfield(form, 'permission')}
<div class="buttons">
${h.submit('submit', "Find {}".format(model_title_plural))}
</div>
</div>
${h.end_form()}
% if principals is not None:
<div class="newgrid half">
<br />
<h2>${model_title_plural} with that permission (${len(principals)} total):</h2>
${self.principal_table()}
</div>
% endif

View file

@ -0,0 +1,11 @@
## -*- coding: utf-8 -*-
<%inherit file="/master/index.mako" />
<%def name="context_menu_items()">
${parent.context_menu_items()}
% if request.has_perm('{}.find_by_perm'.format(permission_prefix)):
<li>${h.link_to("Find {} with Permission X".format(model_title_plural), url('{}.find_by_perm'.format(route_prefix)))}</li>
% endif
</%def>
${parent.body()}

View file

@ -0,0 +1,21 @@
## -*- coding: utf-8 -*-
<%inherit file="/principal/find_by_perm.mako" />
<%def name="principal_table()">
<table>
<thead>
<tr>
<th>Name</th>
</tr>
</thead>
<tbody>
% for role in principals:
<tr>
<td>${h.link_to(role.name, url('roles.view', uuid=role.uuid))}</td>
</tr>
% endfor
</tbody>
</table>
</%def>
${parent.body()}

View file

@ -1,66 +1,7 @@
## -*- coding: utf-8 -*- ## -*- coding: utf-8 -*-
<%inherit file="/base.mako" /> <%inherit file="/principal/find_by_perm.mako" />
<%def name="title()">Find Users by Permission</%def> <%def name="principal_table()">
<%def name="extra_javascript()">
${parent.extra_javascript()}
<script type="text/javascript">
<% gcount = len(permissions) %>
var permissions_by_group = {
% for g, (gkey, group) in enumerate(permissions, 1):
<% pcount = len(group['perms']) %>
'${gkey}': {
% for p, (pkey, perm) in enumerate(group['perms'], 1):
'${pkey}': "${perm['label']}"${',' if p < pcount else ''}
% endfor
}${',' if g < gcount else ''}
% endfor
};
$(function() {
$('#permission_group').selectmenu({
change: function(event, ui) {
var perms = $('#permission');
perms.find('option:first').siblings('option').remove();
$.each(permissions_by_group[ui.item.value], function(key, label) {
perms.append($('<option value="' + key + '">' + label + '</option>'));
});
perms.selectmenu('refresh');
}
});
$('#permission').selectmenu();
$('#find-by-perm-form').submit(function() {
$('.newgrid').remove();
$(this).find('#submit').button('disable').button('option', 'label', "Searching, please wait...");
});
});
</script>
</%def>
${h.form(request.current_route_url(), id='find-by-perm-form')}
${h.csrf_token(request)}
<div class="form">
${self.wtfield(form, 'permission_group')}
${self.wtfield(form, 'permission')}
<div class="buttons">
${h.submit('submit', "Find Users")}
</div>
</div>
${h.end_form()}
% if users is not None:
<div class="newgrid half">
<br />
<h2>Users with that permission (${len(users)} total):</h2>
<table> <table>
<thead> <thead>
<tr> <tr>
@ -69,7 +10,7 @@ ${h.end_form()}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
% for user in users: % for user in principals:
<tr> <tr>
<td>${h.link_to(user.username, url('users.view', uuid=user.uuid))}</td> <td>${h.link_to(user.username, url('users.view', uuid=user.uuid))}</td>
<td>${user.person or ''}</td> <td>${user.person or ''}</td>
@ -77,5 +18,6 @@ ${h.end_form()}
% endfor % endfor
</tbody> </tbody>
</table> </table>
</div> </%def>
% endif
${parent.body()}

View file

@ -1,11 +0,0 @@
## -*- coding: utf-8 -*-
<%inherit file="/master/index.mako" />
<%def name="context_menu_items()">
${parent.context_menu_items()}
% if request.has_perm('users.find_by_perm'):
<li>${h.link_to("Find Users with Permission X", url('users.find_by_perm'))}</li>
% endif
</%def>
${parent.body()}

View file

@ -0,0 +1,97 @@
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2017 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
"Principal" master view
"""
from __future__ import unicode_literals, absolute_import
import copy
import wtforms
from tailbone.views import MasterView
class PrincipalMasterView(MasterView):
"""
Master view base class for security principal models, i.e. User and Role.
"""
def get_fallback_templates(self, template):
return [
'/principal/{}.mako'.format(template),
] + super(PrincipalMasterView, self).get_fallback_templates(template)
def find_by_perm(self):
"""
View for finding all users who have been granted a given permission
"""
permissions = copy.deepcopy(self.request.registry.settings.get('tailbone_permissions', {}))
# sort groups, and permissions for each group, for UI's sake
sorted_perms = sorted(permissions.items(), key=lambda (k, v): v['label'].lower())
for key, group in sorted_perms:
group['perms'] = sorted(group['perms'].items(), key=lambda (k, v): v['label'].lower())
# group options are stable, permission options may depend on submitted group
group_choices = [(gkey, group['label']) for gkey, group in sorted_perms]
permission_choices = [('_any_', "(any)")]
if self.request.method == 'POST':
if self.request.POST.get('permission_group') in permissions:
permission_choices.extend([
(pkey, perm['label'])
for pkey, perm in permissions[self.request.POST['permission_group']]['perms']
])
class PermissionForm(wtforms.Form):
permission_group = wtforms.SelectField(choices=group_choices)
permission = wtforms.SelectField(choices=permission_choices)
principals = None
form = PermissionForm(self.request.POST)
if self.request.method == 'POST' and form.validate():
permission = form.permission.data
principals = self.find_principals_with_permission(self.Session(), permission)
context = {'form': form, 'permissions': sorted_perms, 'principals': principals}
return self.render_to_response('find_by_perm', context)
@classmethod
def defaults(cls, config):
cls._principal_defaults(config)
cls._defaults(config)
@classmethod
def _principal_defaults(cls, config):
route_prefix = cls.get_route_prefix()
url_prefix = cls.get_url_prefix()
permission_prefix = cls.get_permission_prefix()
model_title_plural = cls.get_model_title_plural()
# find principal by permission
config.add_route('{}.find_by_perm'.format(route_prefix), '{}/find-by-perm'.format(url_prefix))
config.add_view(cls, attr='find_by_perm', route_name='{}.find_by_perm'.format(route_prefix),
permission='{}.find_by_perm'.format(permission_prefix))
config.add_tailbone_permission(permission_prefix, '{}.find_by_perm'.format(permission_prefix),
"Find all {} with permission X".format(model_title_plural))

View file

@ -2,7 +2,7 @@
################################################################################ ################################################################################
# #
# Rattail -- Retail Software Framework # Rattail -- Retail Software Framework
# Copyright © 2010-2016 Lance Edgar # Copyright © 2010-2017 Lance Edgar
# #
# This file is part of Rattail. # This file is part of Rattail.
# #
@ -26,6 +26,8 @@ Role Views
from __future__ import unicode_literals, absolute_import from __future__ import unicode_literals, absolute_import
from sqlalchemy import orm
from rattail.db import model from rattail.db import model
from rattail.db.auth import has_permission, administrator_role, guest_role, authenticated_role from rattail.db.auth import has_permission, administrator_role, guest_role, authenticated_role
@ -34,7 +36,7 @@ from webhelpers.html import HTML, tags
from tailbone import forms from tailbone import forms
from tailbone.db import Session from tailbone.db import Session
from tailbone.views import MasterView from tailbone.views.principal import PrincipalMasterView
from tailbone.views.continuum import VersionView, version_defaults from tailbone.views.continuum import VersionView, version_defaults
from tailbone.newgrids import AlchemyGrid, GridAction from tailbone.newgrids import AlchemyGrid, GridAction
@ -50,7 +52,7 @@ class PermissionsField(formalchemy.Field):
role.permissions = self.renderer.deserialize() role.permissions = self.renderer.deserialize()
class RolesView(MasterView): class RolesView(PrincipalMasterView):
""" """
Master view for the Role model. Master view for the Role model.
""" """
@ -108,6 +110,17 @@ class RolesView(MasterView):
self.request.session.flash("You may not delete the {} role.".format(role.name), 'error') self.request.session.flash("You may not delete the {} role.".format(role.name), 'error')
return self.redirect(self.request.get_referrer(default=self.request.route_url('roles'))) return self.redirect(self.request.get_referrer(default=self.request.route_url('roles')))
def find_principals_with_permission(self, session, permission):
# TODO: this should search Permission table instead, and work backward to Role?
all_roles = session.query(model.Role)\
.order_by(model.Role.name)\
.options(orm.joinedload(model.Role._permissions))
roles = []
for role in all_roles:
if has_permission(session, role, permission):
roles.append(role)
return roles
class RoleVersionView(VersionView): class RoleVersionView(VersionView):
""" """

View file

@ -40,7 +40,7 @@ from webhelpers.html import HTML, tags
from tailbone import forms from tailbone import forms
from tailbone.db import Session from tailbone.db import Session
from tailbone.views import MasterView from tailbone.views.principal import PrincipalMasterView
from tailbone.views.continuum import VersionView, version_defaults from tailbone.views.continuum import VersionView, version_defaults
@ -124,7 +124,7 @@ class RolesField(formalchemy.Field):
user.roles = [roles.get(x) for x in data] user.roles = [roles.get(x) for x in data]
class UsersView(MasterView): class UsersView(PrincipalMasterView):
""" """
Master view for the User model. Master view for the User model.
""" """
@ -188,41 +188,7 @@ class UsersView(MasterView):
del fs.password del fs.password
del fs.confirm_password del fs.confirm_password
def find_by_perm(self): def find_principals_with_permission(self, session, permission):
"""
View for finding all users who have been granted a given permission
"""
permissions = copy.deepcopy(self.request.registry.settings.get('tailbone_permissions', {}))
# sort groups, and permissions for each group, for UI's sake
sorted_perms = sorted(permissions.items(), key=lambda (k, v): v['label'].lower())
for key, group in sorted_perms:
group['perms'] = sorted(group['perms'].items(), key=lambda (k, v): v['label'].lower())
# group options are stable, permission options may depend on submitted group
group_choices = [(gkey, group['label']) for gkey, group in sorted_perms]
permission_choices = [('_any_', "(any)")]
if self.request.method == 'POST':
if self.request.POST.get('permission_group') in permissions:
permission_choices.extend([
(pkey, perm['label'])
for pkey, perm in permissions[self.request.POST['permission_group']]['perms']
])
class PermissionForm(wtforms.Form):
permission_group = wtforms.SelectField(choices=group_choices)
permission = wtforms.SelectField(choices=permission_choices)
users = None
form = PermissionForm(self.request.POST)
if self.request.method == 'POST' and form.validate():
permission = form.permission.data
users = self.find_users_by_permission(self.Session(), permission)
context = {'form': form, 'permissions': sorted_perms, 'users': users}
return self.render_to_response('find_by_perm', context)
def find_users_by_permission(self, session, permission):
# TODO: this should search Permission table instead, and work backward to User? # TODO: this should search Permission table instead, and work backward to User?
all_users = session.query(model.User)\ all_users = session.query(model.User)\
.filter(model.User.active == True)\ .filter(model.User.active == True)\
@ -236,25 +202,6 @@ class UsersView(MasterView):
users.append(user) users.append(user)
return users return users
@classmethod
def defaults(cls, config):
cls._user_defaults(config)
cls._defaults(config)
@classmethod
def _user_defaults(cls, config):
route_prefix = cls.get_route_prefix()
url_prefix = cls.get_url_prefix()
permission_prefix = cls.get_permission_prefix()
model_title_plural = cls.get_model_title_plural()
# find users by permission
config.add_route('{}.find_by_perm'.format(route_prefix), '{}/find-by-perm'.format(url_prefix))
config.add_view(cls, attr='find_by_perm', route_name='{}.find_by_perm'.format(route_prefix),
permission='{}.find_by_perm'.format(permission_prefix))
config.add_tailbone_permission(permission_prefix, '{}.find_by_perm'.format(permission_prefix),
"Find all {} with permission X".format(model_title_plural))
class UserVersionView(VersionView): class UserVersionView(VersionView):
""" """