From cab339e367b47b552142c6de5e7e6f469d9257ee Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 6 Feb 2017 13:37:42 -0600 Subject: [PATCH] Add support for Find Roles by Permission X feature --- .../templates/principal/find_by_perm.mako | 67 +++++++++++++ tailbone/templates/principal/index.mako | 11 +++ tailbone/templates/roles/find_by_perm.mako | 21 ++++ tailbone/templates/users/find_by_perm.mako | 70 ++----------- tailbone/templates/users/index.mako | 11 --- tailbone/views/principal.py | 97 +++++++++++++++++++ tailbone/views/roles.py | 19 +++- tailbone/views/users.py | 59 +---------- 8 files changed, 221 insertions(+), 134 deletions(-) create mode 100644 tailbone/templates/principal/find_by_perm.mako create mode 100644 tailbone/templates/principal/index.mako create mode 100644 tailbone/templates/roles/find_by_perm.mako delete mode 100644 tailbone/templates/users/index.mako create mode 100644 tailbone/views/principal.py diff --git a/tailbone/templates/principal/find_by_perm.mako b/tailbone/templates/principal/find_by_perm.mako new file mode 100644 index 00000000..621cac85 --- /dev/null +++ b/tailbone/templates/principal/find_by_perm.mako @@ -0,0 +1,67 @@ +## -*- coding: utf-8 -*- +<%inherit file="/base.mako" /> + +<%def name="title()">Find ${model_title_plural} by Permission + +<%def name="extra_javascript()"> + ${parent.extra_javascript()} + + + + +${h.form(request.current_route_url(), id='find-by-perm-form')} +${h.csrf_token(request)} + +
+ ${self.wtfield(form, 'permission_group')} + ${self.wtfield(form, 'permission')} +
+ ${h.submit('submit', "Find {}".format(model_title_plural))} +
+
+ +${h.end_form()} + +% if principals is not None: +
+
+

${model_title_plural} with that permission (${len(principals)} total):

+ ${self.principal_table()} +
+% endif diff --git a/tailbone/templates/principal/index.mako b/tailbone/templates/principal/index.mako new file mode 100644 index 00000000..97c48995 --- /dev/null +++ b/tailbone/templates/principal/index.mako @@ -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)): +
  • ${h.link_to("Find {} with Permission X".format(model_title_plural), url('{}.find_by_perm'.format(route_prefix)))}
  • + % endif + + +${parent.body()} diff --git a/tailbone/templates/roles/find_by_perm.mako b/tailbone/templates/roles/find_by_perm.mako new file mode 100644 index 00000000..8908d12e --- /dev/null +++ b/tailbone/templates/roles/find_by_perm.mako @@ -0,0 +1,21 @@ +## -*- coding: utf-8 -*- +<%inherit file="/principal/find_by_perm.mako" /> + +<%def name="principal_table()"> + + + + + + + + % for role in principals: + + + + % endfor + +
    Name
    ${h.link_to(role.name, url('roles.view', uuid=role.uuid))}
    + + +${parent.body()} diff --git a/tailbone/templates/users/find_by_perm.mako b/tailbone/templates/users/find_by_perm.mako index 5e1075c4..59fcf643 100644 --- a/tailbone/templates/users/find_by_perm.mako +++ b/tailbone/templates/users/find_by_perm.mako @@ -1,66 +1,7 @@ ## -*- coding: utf-8 -*- -<%inherit file="/base.mako" /> +<%inherit file="/principal/find_by_perm.mako" /> -<%def name="title()">Find Users by Permission - -<%def name="extra_javascript()"> - ${parent.extra_javascript()} - - - -${h.form(request.current_route_url(), id='find-by-perm-form')} -${h.csrf_token(request)} - -
    - ${self.wtfield(form, 'permission_group')} - ${self.wtfield(form, 'permission')} -
    - ${h.submit('submit', "Find Users")} -
    -
    - -${h.end_form()} - -% if users is not None: -
    -
    -

    Users with that permission (${len(users)} total):

    +<%def name="principal_table()"> @@ -69,7 +10,7 @@ ${h.end_form()} - % for user in users: + % for user in principals: @@ -77,5 +18,6 @@ ${h.end_form()} % endfor
    ${h.link_to(user.username, url('users.view', uuid=user.uuid))} ${user.person or ''}
    -
    -% endif + + +${parent.body()} diff --git a/tailbone/templates/users/index.mako b/tailbone/templates/users/index.mako deleted file mode 100644 index df7c291e..00000000 --- a/tailbone/templates/users/index.mako +++ /dev/null @@ -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'): -
  • ${h.link_to("Find Users with Permission X", url('users.find_by_perm'))}
  • - % endif - - -${parent.body()} diff --git a/tailbone/views/principal.py b/tailbone/views/principal.py new file mode 100644 index 00000000..39157219 --- /dev/null +++ b/tailbone/views/principal.py @@ -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 . +# +################################################################################ +""" +"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)) diff --git a/tailbone/views/roles.py b/tailbone/views/roles.py index ee666f14..fef86cad 100644 --- a/tailbone/views/roles.py +++ b/tailbone/views/roles.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2016 Lance Edgar +# Copyright © 2010-2017 Lance Edgar # # This file is part of Rattail. # @@ -26,6 +26,8 @@ Role Views from __future__ import unicode_literals, absolute_import +from sqlalchemy import orm + from rattail.db import model 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.db import Session -from tailbone.views import MasterView +from tailbone.views.principal import PrincipalMasterView from tailbone.views.continuum import VersionView, version_defaults from tailbone.newgrids import AlchemyGrid, GridAction @@ -50,7 +52,7 @@ class PermissionsField(formalchemy.Field): role.permissions = self.renderer.deserialize() -class RolesView(MasterView): +class RolesView(PrincipalMasterView): """ 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') 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): """ diff --git a/tailbone/views/users.py b/tailbone/views/users.py index dc46b498..76eb18a7 100644 --- a/tailbone/views/users.py +++ b/tailbone/views/users.py @@ -40,7 +40,7 @@ from webhelpers.html import HTML, tags from tailbone import forms from tailbone.db import Session -from tailbone.views import MasterView +from tailbone.views.principal import PrincipalMasterView 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] -class UsersView(MasterView): +class UsersView(PrincipalMasterView): """ Master view for the User model. """ @@ -188,41 +188,7 @@ class UsersView(MasterView): del fs.password del fs.confirm_password - 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) - - 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): + def find_principals_with_permission(self, session, permission): # TODO: this should search Permission table instead, and work backward to User? all_users = session.query(model.User)\ .filter(model.User.active == True)\ @@ -236,25 +202,6 @@ class UsersView(MasterView): users.append(user) 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): """