diff --git a/ckan/config/routing.py b/ckan/config/routing.py index b048798caff..43fa8fd9e1f 100644 --- a/ckan/config/routing.py +++ b/ckan/config/routing.py @@ -216,6 +216,17 @@ def make_map(): register_package_plugins(map) register_group_plugins(map) + # authz group + map.redirect('/authorizationgroups', '/authorizationgroup') + map.redirect('/authorizationgroups/{url:.*}', '/authorizationgroup/{url}') + with SubMapper(map, controller='authorization_group') as m: + m.connect('/authorizationgroup', action='index') + m.connect('/authorizationgroup/list', action='list') + m.connect('/authorizationgroup/new', action='new') + m.connect('/authorizationgroup/edit/{id}', action='edit') + m.connect('/authorizationgroup/authz/{id}', action='authz') + m.connect('/authorizationgroup/{id}', action='read') + # tags map.redirect('/tags', '/tag') map.redirect('/tags/{url:.*}', '/tag/{url}') diff --git a/ckan/controllers/authorization_group.py b/ckan/controllers/authorization_group.py new file mode 100644 index 00000000000..fa1b91aafb6 --- /dev/null +++ b/ckan/controllers/authorization_group.py @@ -0,0 +1,201 @@ +import genshi + +from sqlalchemy.orm import eagerload_all +from ckan.lib.base import * +from pylons.i18n import get_lang, _ +import ckan.authz as authz +import ckan.forms +from ckan.lib.helpers import Page +from ckan.logic import NotAuthorized, check_access + +class AuthorizationGroupController(BaseController): + + def __init__(self): + BaseController.__init__(self) + + def index(self): + from ckan.lib.helpers import Page + try: + context = {'model':model,'user': c.user or c.author} + check_access('site_read',context) + except NotAuthorized: + abort(401, _('Not authorized to see this page')) + + query = ckan.authz.Authorizer().authorized_query(c.user, model.AuthorizationGroup) + query = query.options(eagerload_all('users')) + c.page = Page( + collection=query, + page=request.params.get('page', 1), + items_per_page=20 + ) + return render('authorization_group/index.html') + + def _get_authgroup_by_name_or_id(self, id): + return model.AuthorizationGroup.by_name(id) or\ + model.Session.query(model.AuthorizationGroup).get(id) + + def read(self, id): + c.authorization_group = self._get_authgroup_by_name_or_id(id) + if c.authorization_group is None: + abort(404) + auth_for_read = self.authorizer.am_authorized(c, model.Action.READ, + c.authorization_group) + if not auth_for_read: + abort(401, _('Not authorized to read %s') % id.encode('utf8')) + + import ckan.misc + c.authorization_group_admins = self.authorizer.get_admins(c.authorization_group) + + c.page = Page( + collection=c.authorization_group.users, + page=request.params.get('page', 1), + items_per_page=50 + ) + return render('authorization_group/read.html') + + def new(self): + record = model.AuthorizationGroup + c.error = '' + + auth_for_create = self.authorizer.am_authorized(c, model.Action.AUTHZ_GROUP_CREATE, model.System()) + if not auth_for_create: + abort(401, _('Unauthorized to create a group')) + + is_admin = self.authorizer.is_sysadmin(c.user) + + fs = ckan.forms.get_authorization_group_fieldset(is_admin=is_admin) + + if request.params.has_key('save'): + # needed because request is nested + # multidict which is read only + params = dict(request.params) + c.fs = fs.bind(record, data=params or None, session=model.Session) + try: + self._update(c.fs, id, record.id) + except ValidationException, error: + fs = error.args[0] + c.form = self._render_edit_form(fs) + return render('authorization_group/edit.html') + # do not use groupname from id as may have changed + c.authzgroupname = c.fs.name.value + authorization_group = model.AuthorizationGroup.by_name(c.authzgroupname) + assert authorization_group + user = model.User.by_name(c.user) + model.setup_default_user_roles(authorization_group, [user]) + users = [model.User.by_name(name) for name in \ + request.params.getall('AuthorizationGroup-users-current')] + authorization_group.users = list(set(users)) + usernames = request.params.getall('AuthorizationGroupUser--user_name') + for username in usernames: + if username: + usr = model.User.by_name(username) + if usr and usr not in authorization_group.users: + model.add_user_to_authorization_group(usr, authorization_group, model.Role.READER) + model.repo.commit_and_remove() + h.redirect_to(controller='authorization_group', action='read', id=c.authzgroupname) + + c.form = self._render_edit_form(fs) + return render('authorization_group/new.html') + + def edit(self, id=None): # allow id=None to allow posting + c.error = '' + authorization_group = self._get_authgroup_by_name_or_id(id) + if authorization_group is None: + abort(404, '404 Not Found') + am_authz = self.authorizer.am_authorized(c, model.Action.EDIT, authorization_group) + if not am_authz: + abort(401, _('User %r not authorized to edit %r') % (c.user, id)) + + is_admin = self.authorizer.is_sysadmin(c.user) + + if not 'save' in request.params: + c.authorization_group = authorization_group + c.authorization_group_name = authorization_group.name + + fs = ckan.forms.get_authorization_group_fieldset(is_admin=is_admin).bind(authorization_group) + c.form = self._render_edit_form(fs) + return render('authorization_group/edit.html') + else: + # id is the name (pre-edited state) + c.authorization_group_name = id + # needed because request is nested + # multidict which is read only + params = dict(request.params) + c.fs = ckan.forms.get_authorization_group_fieldset()\ + .bind(authorization_group, data=params or None) + try: + self._update(c.fs, id, authorization_group.id) + # do not use groupname from id as may have changed + c.authorization_group = authorization_group + c.authorization_group_name = authorization_group.name + except ValidationException, error: + fs = error.args[0] + c.form = self._render_edit_form(fs) + return render('authorization_group/edit.html') + user = model.User.by_name(c.user) + users = [model.User.by_name(name) for name in \ + request.params.getall('AuthorizationGroup-users-current')] + authorization_group.users = list(set(users)) + usernames = request.params.getall('AuthorizationGroupUser--user_name') + for username in usernames: + if username: + usr = model.User.by_name(username) + if usr and usr not in authorization_group.users: + model.add_user_to_authorization_group(usr, authorization_group, model.Role.READER) + model.repo.commit_and_remove() + h.redirect_to(controller='authorization_group', action='read', id=c.authorization_group_name) + + def authz(self, id): + authorization_group = self._get_authgroup_by_name_or_id(id) + if authorization_group is None: + abort(404, _('Group not found')) + + c.authorization_group_name = authorization_group.name + c.authorization_group = authorization_group + + c.authz_editable = self.authorizer.am_authorized(c, model.Action.EDIT_PERMISSIONS, + authorization_group) + if not c.authz_editable: + abort(401, gettext('User %r not authorized to edit %s authorizations') % (c.user, id)) + + roles = self._handle_update_of_authz(authorization_group) + self._prepare_authz_info_for_render(roles) + return render('authorization_group/authz.html') + + + def _render_edit_form(self, fs): + # errors arrive in c.error and fs.errors + c.fieldset = fs + c.fieldset2 = ckan.forms.get_authorization_group_user_fieldset() + return render('authorization_group/edit_form.html') + + def _update(self, fs, group_name, group_id): + ''' + Writes the POST data (associated with a group edit) to the database + @input c.error + ''' + validation = fs.validate() + if not validation: + c.form = self._render_edit_form(fs) + raise ValidationException(fs) + + try: + fs.sync() + except Exception, inst: + model.Session.rollback() + raise + else: + model.Session.commit() + + def _update_authz(self, fs): + validation = fs.validate() + if not validation: + c.form = self._render_edit_form(fs) + raise ValidationException(fs) + try: + fs.sync() + except Exception, inst: + model.Session.rollback() + raise + else: + model.Session.commit() diff --git a/ckan/forms/authz.py b/ckan/forms/authz.py index e0e96280765..ac7c512d4fa 100644 --- a/ckan/forms/authz.py +++ b/ckan/forms/authz.py @@ -30,6 +30,15 @@ def get_group_linker(action): action, action) +def get_authorization_group_linker(action): + return lambda item: '%s' % ( + ckan_h.url_for(controller='authorization_group', + action='authz', + id=item.authorization_group.name, + role_to_delete=item.id), + action, + action) + class RolesRenderer(formalchemy.fields.FieldRenderer): def render(self, **kwargs): selected = kwargs.get('selected', None) or unicode(self.value) @@ -49,14 +58,18 @@ def authz_fieldset_builder(role_class): fs.append( Field(u'delete', types.String, get_group_linker(u'delete')).readonly() ) - + elif role_class == model.AuthorizationGroupRole: + fs.append( + Field(u'delete', types.String, get_authorization_group_linker(u'delete')).readonly() + ) + fs.append( # use getattr because though we should always have a user name, # sometimes (due to error) we don't and want to avoid a 500 ... Field(u'username', types.String, lambda item: ckan_h.linked_user(getattr(item.user, 'name', ''))).readonly() ) - + fs.append( Field(u'authzgroupname', types.String, lambda item: getattr(item.authorized_group, 'name', '')).readonly() @@ -68,7 +81,8 @@ def authz_fieldset_builder(role_class): ], include=[fs.username, fs.authzgroupname, - fs.role] + fs.role, + fs.delete], ) return fs @@ -104,7 +118,7 @@ def get_new_role_fieldset(role_class): fieldsets = {} def get_authz_fieldset(name): - if not fieldsets: + if not fieldsets: fieldsets['package_authz_fs'] = authz_fieldset_builder(model.PackageRole) fieldsets['group_authz_fs'] = authz_fieldset_builder(model.GroupRole) fieldsets['authorization_group_authz_fs'] = authz_fieldset_builder(model.AuthorizationGroupRole) diff --git a/ckan/lib/helpers.py b/ckan/lib/helpers.py index bb5150285d8..ea8a4fecb7b 100644 --- a/ckan/lib/helpers.py +++ b/ckan/lib/helpers.py @@ -438,6 +438,20 @@ def linked_user(user, maxlength=0): return _icon + link_to(displayname, url_for(controller='user', action='read', id=_name)) +def linked_authorization_group(authgroup, maxlength=0): + from ckan import model + if not isinstance(authgroup, model.AuthorizationGroup): + authgroup_name = unicode(authgroup) + authgroup = model.AuthorizationGroup.get(authgroup_name) + if not authgroup: + return authgroup_name + if authgroup: + displayname = authgroup.name or authgroup.id + if maxlength and len(display_name) > maxlength: + displayname = displayname[:maxlength] + '...' + return link_to(displayname, + url_for(controller='authorization_group', action='read', id=displayname)) + def group_name_to_title(name): from ckan import model group = model.Group.by_name(name) @@ -757,6 +771,7 @@ def process_names(items): # am_authorized, # depreciated 'check_access', 'linked_user', + 'linked_authorization_group', 'group_name_to_title', 'markdown_extract', 'icon', diff --git a/ckan/templates/_util.html b/ckan/templates/_util.html index b76d73a83a8..5aa7c077c7e 100644 --- a/ckan/templates/_util.html +++ b/ckan/templates/_util.html @@ -89,6 +89,20 @@ + + + + + + + + + +
TitleNumber of members
+ ${authorization_group.name or authorization_group.id} + ${len(authorization_group.users)}
+ ${h.markdown_extract(related.title, + + + + + + + + + + + + + + + + + +
User Group ${role}
+ ${h.linked_authorization_group(user)} + + + + + + + + + + +
+ + diff --git a/ckan/templates/admin/authz.html b/ckan/templates/admin/authz.html index f409f7a98b6..7feaebde128 100644 --- a/ckan/templates/admin/authz.html +++ b/ckan/templates/admin/authz.html @@ -26,6 +26,25 @@

Add Roles for Any User


+

Existing Roles for Authorization Groups

+ + + ${authz_form_group_table('authzgroup_form', c.roles, c.authz_groups, c.authz_groups_role_dict)} + +
+ + +

Add Roles for Any Authorization Group

+ + + ${authz_add_group_table(c.roles)} + +
+ + + + + diff --git a/ckan/templates/authorization_group/__init__.py b/ckan/templates/authorization_group/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ckan/templates/authorization_group/authz.html b/ckan/templates/authorization_group/authz.html new file mode 100644 index 00000000000..12643d25792 --- /dev/null +++ b/ckan/templates/authorization_group/authz.html @@ -0,0 +1,46 @@ + + + ${c.authorization_group_name} - Authorization - AuthorizationGroups + Authorization: ${c.authorization_group_name} + +
+

Update Existing Roles

+ +
+ ${authz_form_table('theform', c.roles, c.users, c.user_role_dict)} + + + +

Add Roles for Any User

+ +
+ ${authz_add_table(c.roles)} + + + +
+ +

Existing Roles for Authorization Groups

+ +
+ ${authz_form_group_table('authzgroup_form', c.roles, c.authz_groups, c.authz_groups_role_dict)} + + + +

Add Roles for Any Authorization Group

+ +
+ ${authz_add_group_table(c.roles)} + + + +
+ + + diff --git a/ckan/templates/authorization_group/edit.html b/ckan/templates/authorization_group/edit.html new file mode 100644 index 00000000000..ade0f548c04 --- /dev/null +++ b/ckan/templates/authorization_group/edit.html @@ -0,0 +1,14 @@ + + + ${c.authorization_group_name} - Edit - Authorization Groups + Edit: ${c.authorization_group.name if c.authorization_group else ''} + +
+ ${Markup(c.form)} +
+ + + + diff --git a/ckan/templates/authorization_group/edit_form.html b/ckan/templates/authorization_group/edit_form.html new file mode 100644 index 00000000000..293428077ff --- /dev/null +++ b/ckan/templates/authorization_group/edit_form.html @@ -0,0 +1,30 @@ +
+ + ${h.literal(c.fieldset.render())} + +
+ Users +
+ +
+ +
+
+

There are no users currently in this group.

+
+ + ${h.literal(c.fieldset2.render())} + +
+ ${h.submit('save', _('Save'))} + diff --git a/ckan/templates/authorization_group/index.html b/ckan/templates/authorization_group/index.html new file mode 100644 index 00000000000..a135ac9fcaf --- /dev/null +++ b/ckan/templates/authorization_group/index.html @@ -0,0 +1,18 @@ + + + Authorization Groups + Authorization Groups + +
+

There are ${c.page.item_count} authorization groups.

+ + ${c.page.pager()} + ${authorization_group_list(c.page.items)} + ${c.page.pager()} +
+ + + diff --git a/ckan/templates/authorization_group/layout.html b/ckan/templates/authorization_group/layout.html new file mode 100644 index 00000000000..e575f8d8b98 --- /dev/null +++ b/ckan/templates/authorization_group/layout.html @@ -0,0 +1,43 @@ + + + + + + + +
  • +

    Authorization Groups

    +

    Instead of specifying the privileges of specific users on a dataset or group, + you can also specify a set of users that will share the same rights. To do that, an + authorization group can be set-up and users can be added to it.

    +

    + + + Create a new authorization group + +

    +
  • +
    + + + diff --git a/ckan/templates/authorization_group/new.html b/ckan/templates/authorization_group/new.html new file mode 100644 index 00000000000..0dbcc2a527a --- /dev/null +++ b/ckan/templates/authorization_group/new.html @@ -0,0 +1,14 @@ + + + New - Authorization Groups + New Authorization Group + +
    + ${Markup(c.form)} +
    + + + + diff --git a/ckan/templates/authorization_group/read.html b/ckan/templates/authorization_group/read.html new file mode 100644 index 00000000000..01ecec03843 --- /dev/null +++ b/ckan/templates/authorization_group/read.html @@ -0,0 +1,20 @@ + + + ${c.authorization_group.name} - Authorization Groups + ${c.authorization_group.name} + +
    +

    Members

    +

    There are ${c.page.item_count} users in this authorization group.

    + ${c.page.pager()} + ${user_list(c.page.items)} + ${c.page.pager()} +
    + + + + + diff --git a/ckan/templates/group/authz.html b/ckan/templates/group/authz.html index 1d9d9dc7b94..0b778b21188 100644 --- a/ckan/templates/group/authz.html +++ b/ckan/templates/group/authz.html @@ -1,10 +1,10 @@ - + Authorization: ${c.group.display_name} Authorization: ${c.group.display_name} - +

    Update Existing Roles

    @@ -23,6 +23,23 @@

    Add Roles for Any User

    +
    + +

    Update Existing Roles for Authorization Groups

    + +
    + ${authz_form_group_table('authzgroup_form', c.roles, c.authz_groups, c.authz_groups_role_dict)} + +
    + + +

    Add Roles for Any Authorization Group

    + +
    + ${authz_add_group_table(c.roles)} + +
    +
    diff --git a/ckan/templates/layout_base.html b/ckan/templates/layout_base.html index f2d51dd5816..8913814fab6 100644 --- a/ckan/templates/layout_base.html +++ b/ckan/templates/layout_base.html @@ -176,6 +176,11 @@

    Sections

    Revisions +
  • + + Authorization Groups + +
  • Site Admin diff --git a/ckan/templates/package/authz.html b/ckan/templates/package/authz.html index 5849265ec1a..aca614664b7 100644 --- a/ckan/templates/package/authz.html +++ b/ckan/templates/package/authz.html @@ -1,7 +1,7 @@ - + Authorization: ${c.pkgtitle or c.pkgname} Authorization: ${c.pkgtitle or c.pkgname} @@ -22,6 +22,25 @@

    Add Roles for Any User

    + +
    + +

    Update Existing Roles for Authorization Groups

    + +
    + ${authz_form_group_table('authzgroup_form', c.roles, c.authz_groups, c.authz_groups_role_dict)} + +
    + + +

    Add Roles for Any Authorization Group

    + +
    + ${authz_add_group_table(c.roles)} + +
    + + diff --git a/ckan/tests/functional/test_admin.py b/ckan/tests/functional/test_admin.py index 448dcdae00e..416f1b0b548 100644 --- a/ckan/tests/functional/test_admin.py +++ b/ckan/tests/functional/test_admin.py @@ -34,7 +34,7 @@ def setup_class(cls): # Creating a couple of authorization groups, which are enough to break # some things just by their existence for ag_name in [u'anauthzgroup', u'anotherauthzgroup']: - ag=model.AuthorizationGroup.by_name(ag_name) + ag=model.AuthorizationGroup.by_name(ag_name) if not ag: #may already exist, if not create ag=model.AuthorizationGroup(name=ag_name) model.Session.add(ag) @@ -72,6 +72,10 @@ def get_user_form(): response = get_response() return response.forms['theform'] + def get_authzgroup_form(): + response = get_response() + return response.forms['authzgroup_form'] + def check_and_set_checkbox(theform, user, role, should_be, set_to): user_role_string = '%s$%s' % (user, role) checkboxes = [x for x in theform.fields[user_role_string] \ @@ -94,7 +98,7 @@ def submit(form): def authz_submit(form): return form.submit('authz_save', extra_environ=as_testsysadmin) - + # get and store the starting state of the system roles original_user_roles = get_system_user_roles() original_authzgroup_roles = get_system_authzgroup_roles() @@ -121,12 +125,17 @@ def authz_submit(form): # and check that's the state in the database now assert get_system_user_roles() == expected_user_roles + assert get_system_authzgroup_roles() == expected_authzgroup_roles # try again, this time we expect the box to be ticked already submit(check_and_set_checkbox(get_user_form(), u'visitor', u'admin', True, True)) # performing the action twice shouldn't have changed anything assert get_system_user_roles() == expected_user_roles + assert get_system_authzgroup_roles() == expected_authzgroup_roles + + # now let's make the authzgroup which already has a system role an admin + authz_submit(check_and_set_checkbox(get_authzgroup_form(), u'anauthzgroup', u'admin', False, True)) # update expected state to reflect the change we should just have made expected_authzgroup_roles.append((u'anauthzgroup', u'admin')) @@ -134,12 +143,15 @@ def authz_submit(form): # check that's happened assert get_system_user_roles() == expected_user_roles + assert get_system_authzgroup_roles() == expected_authzgroup_roles # put it back how it was submit(check_and_set_checkbox(get_user_form(), u'visitor', u'admin', True, False)) + authz_submit(check_and_set_checkbox(get_authzgroup_form(), u'anauthzgroup', u'admin', True, False)) # should be back to our starting state assert original_user_roles == get_system_user_roles() + assert original_authzgroup_roles == get_system_authzgroup_roles() # now test making multiple changes @@ -150,7 +162,7 @@ def authz_submit(form): check_and_set_checkbox(form, u'visitor', u'editor', False, True) check_and_set_checkbox(form, u'visitor', u'reader', False, False) check_and_set_checkbox(form, u'logged_in', u'editor', True, False) - check_and_set_checkbox(form, u'logged_in', u'reader', False, True) + check_and_set_checkbox(form, u'logged_in', u'reader', False, True) submit(form) roles=get_system_user_roles() @@ -167,6 +179,8 @@ def get_roles_by_name(user=None, group=None): return [y for (x,y) in get_system_user_roles() if x==user] elif group: return [y for (x,y) in get_system_authzgroup_roles() if x==group] + else: + assert False, 'miscalled' # now we test the box for giving roles to an arbitrary user @@ -195,6 +209,20 @@ def get_roles_by_name(user=None, group=None): assert get_roles_by_name(group=u'anotherauthzgroup') == [], \ "should not have roles" + form = get_response().forms['authzgroup_addform'] + form.fields['new_user_name'][0].value='anotherauthzgroup' + checkbox = [x for x in form.fields['reader'] \ + if x.__class__.__name__ == 'Checkbox'][0] + assert checkbox.checked == False + checkbox.checked=True + + response = form.submit('authz_add', extra_environ=as_testsysadmin) + assert "Authorization Group Added" in response, "don't see flash message" + + + assert get_roles_by_name(group=u'anotherauthzgroup') == [u'reader'], \ + "should be a reader now" + class TestAdminTrashController(WsgiAppCase): def setup(cls): @@ -238,7 +266,7 @@ def test_purge_package(self): url = url_for('ckanadmin', action='trash') response = self.app.get(url, extra_environ=as_testsysadmin) assert 'dataset/warandpeace' in response, response - + # Check we get correct error message on attempted purge form = response.forms['form-purge-packages'] response = form.submit('purge-packages', status=[302], diff --git a/ckan/tests/functional/test_authorization_group.py b/ckan/tests/functional/test_authorization_group.py new file mode 100644 index 00000000000..3d223a52a33 --- /dev/null +++ b/ckan/tests/functional/test_authorization_group.py @@ -0,0 +1,440 @@ +from nose.plugins.skip import SkipTest +from nose.tools import assert_equal + +from ckan.tests import * +from ckan.authz import Authorizer +import ckan.model as model +from base import FunctionalTestCase +from ckan.tests import search_related + +class TestAuthorizationGroup(FunctionalTestCase): + + @classmethod + def setup_class(self): + model.Session.remove() + model.repo.init_db() + CreateTestData.create() + model.repo.new_revision() + treasury = model.AuthorizationGroup(name=u'treasury') + health = model.AuthorizationGroup(name=u'health') + model.Session.add(treasury) + model.Session.add(health) + model.add_user_to_authorization_group(model.User.by_name(u"russianfan"), + treasury, model.Role.ADMIN) + model.repo.commit_and_remove() + + @classmethod + def teardown_class(self): + model.Session.remove() + model.repo.rebuild_db() + model.Session.remove() + + def test_index(self): + offset = url_for(controller='authorization_group', action='index') + res = self.app.get(offset, extra_environ={'REMOTE_USER': 'russianfan'}) + assert '

    Authorization Groups

    ' in res, res + group_count = Authorizer.authorized_query(u'russianfan', model.AuthorizationGroup).count() + assert 'There are %s authorization groups.' % group_count in self.strip_tags(res), res + authz_groupname = u'treasury' + authz_group = model.AuthorizationGroup.by_name(unicode(authz_groupname)) + group_users_count = len(authz_group.users) + self.check_named_element(res, 'tr', authz_groupname, group_users_count) + #res = res.click(authz_groupname) + #assert authz_groupname in res, res + + def test_read(self): + name = u'treasury' + offset = url_for(controller='authorization_group', action='read', id=name) + res = self.app.get(offset, extra_environ={'REMOTE_USER': 'russianfan'}) + main_res = self.main_div(res) + assert '%s - Authorization Groups' % name in res, res + #assert 'edit' in main_res, main_res + assert name in res, res + + def test_new(self): + offset = url_for(controller='authorization_group', action='index') + res = self.app.get(offset, extra_environ={'REMOTE_USER': 'russianfan'}) + assert 'Create a new authorization group' in res, res + + +class TestEdit(TestController): + groupname = u'treasury' + + @classmethod + def setup_class(self): + model.Session.remove() + CreateTestData.create() + model.repo.new_revision() + treasury = model.AuthorizationGroup(name=u'treasury') + health = model.AuthorizationGroup(name=u'health') + model.Session.add(treasury) + model.Session.add(health) + model.add_user_to_authorization_group(model.User.by_name(u"russianfan"), + treasury, model.Role.ADMIN) + model.repo.commit_and_remove() + + self.username = u'testusr' + model.repo.new_revision() + model.Session.add(model.User(name=self.username)) + model.repo.commit_and_remove() + + @classmethod + def teardown_class(self): + model.Session.remove() + model.repo.rebuild_db() + model.Session.remove() + + def test_0_not_authz(self): + offset = url_for(controller='authorization_group', action='edit', id=self.groupname) + # 401 gets caught by repoze.who and turned into redirect + res = self.app.get(offset, status=[302, 401]) + res = res.follow() + assert res.request.url.startswith('/user/login') + + def test_1_read_allowed_for_admin(self): + raise SkipTest() + offset = url_for(controller='authorization_group', action='edit', id=self.groupname) + res = self.app.get(offset, status=200, extra_environ={'REMOTE_USER': 'russianfan'}) + assert 'Edit Authorization Group: %s' % self.groupname in res, res + + def test_2_edit(self): + raise SkipTest() + offset = url_for(controller='authorization_group', action='edit', id=self.groupname) + res = self.app.get(offset, status=200, extra_environ={'REMOTE_USER': 'russianfan'}) + assert 'Edit Authorization Group: %s' % self.groupname in res, res + + form = res.forms['group-edit'] + group = model.AuthorizationGroup.by_name(self.groupname) + usr = model.User.by_name(self.username) + form['AuthorizationGroupUser--user_name'] = usr.name + + res = form.submit('save', status=302, extra_environ={'REMOTE_USER': 'russianfan'}) + # should be read page + # assert 'Groups - %s' % self.groupname in res, res + + model.Session.remove() + group = model.AuthorizationGroup.by_name(self.groupname) + + # now look at packages + assert len(group.users) == 2 + + +class TestNew(FunctionalTestCase): + groupname = u'treasury' + + @classmethod + def setup_class(self): + CreateTestData.create_user('tester1') + CreateTestData.create_user('tester2') + CreateTestData.create_user('tester3') + + self.extra_environ = {'REMOTE_USER': 'tester1'} + + @classmethod + def teardown_class(self): + model.repo.rebuild_db() + + def test_0_new(self): + offset = url_for(controller='authorization_group', action='new', id=None) + res = self.app.get(offset, status=200, extra_environ=self.extra_environ) + assert 'New Authorization Group' in res, res + + form = res.forms['group-edit'] + form['AuthorizationGroup--name'] = 'testname' + + # can't test users - needs javascript + #form['AuthorizationGroupUser--user_name'] = 'tester2' + + res = form.submit('save', status=302, extra_environ=self.extra_environ) + res = res.follow() + + # should be read page + main_res = self.main_div(res) + assert 'testname' in main_res, main_res + + # test created object + auth_group = model.AuthorizationGroup.by_name('testname') + assert auth_group + assert_equal(auth_group.name, 'testname') + + def test_0_new_without_name(self): + offset = url_for(controller='authorization_group', action='new', id=None) + res = self.app.get(offset, status=200, extra_environ=self.extra_environ) + assert 'New Authorization Group' in res, res + + form = res.forms['group-edit'] + # don't set name + + res = form.submit('save', status=200, extra_environ=self.extra_environ) + assert 'Error' in res, res + assert 'Name: Please enter a value' in res, res + + +class TestAuthorizationGroupWalkthrough(FunctionalTestCase): + + @classmethod + def setup_class(self): + model.Session.remove() + model.repo.init_db() + CreateTestData.create() + model.repo.commit_and_remove() + + + @classmethod + def teardown_class(self): + model.Session.remove() + model.repo.rebuild_db() + model.Session.remove() + + + ## THIS WALKTHROUGH IS NOW COMPLETELY BROKEN BY THE CHANGES I MADE TO THE AUTHZ PAGE + + + # def test_authzgroups_walkthrough(self): + # # very long test sequence repeating the series of things I did to + # # convince myself that the authzgroups system worked as expected, + # # starting off with the default test data + + # # The first thing to notice is that the authzgroup page: + # auth_group_index_url = url_for(controller='/authorization_group', action='index') + # # displays differently for different users. + + # def get_page(url, expect_status, username, assert_text=None, error_text=None): + # res= self.app.get(url, + # status=expect_status, + # extra_environ={'REMOTE_USER': username}) + # if assert_text and assert_text not in res: + # errorstring = error_text + ' ( "' + assert_text + \ + # '" not found in result of getting "' + \ + # url + '" as user "' + username + '" )' + # assert False, errorstring + # return res + + # # testsysadmin sees the true picture, where the test data contains two groups + # get_page(auth_group_index_url, 200, 'testsysadmin', + # 'There are 2 authorization groups', + # 'Should be accurate for testsysadmin') + + # # But if we look at the same page as annafan, who does not have read + # # permissions on these groups, we should see neither + # get_page(auth_group_index_url, 200, 'annafan', + # 'There are 0 authorization groups', + # 'Should lie to annafan about number of groups') + + # # There is a page for each group + # anauthzgroup_url = url_for(controller='/authorization_group', + # action='read', + # id='anauthzgroup') + # # And an edit page + # anauthzgroup_edit_url = url_for(controller='/authorization_group', + # action='edit', + # id='anauthzgroup') + + # # testsysadmin should be able to see this, and check that there are no members + # get_page(anauthzgroup_url, 200, 'testsysadmin', + # 'There are 0 users in this', + # 'should be no users in anauthzgroup') + + # # now testsysadmin adds annafan to anauthzgroup via the edit page + # res = get_page(anauthzgroup_edit_url, 200, 'testsysadmin') + # group_edit_form = res.forms['group-edit'] + # group_edit_form['AuthorizationGroupUser--user_name'] = u'annafan' + # submit_res = group_edit_form.submit('save', + # extra_environ={'REMOTE_USER': 'testsysadmin'}) + + # # adding a user to a group should both make her a member, and give her + # # read permission on the group. We'll check those things have actually + # # happened by looking directly in the model. + # anauthzgroup = model.AuthorizationGroup.by_name('anauthzgroup') + # anauthzgroup_users = [x.name for x in anauthzgroup.users] + # anauthzgroup_user_roles = [(x.user.name, x.role) for x in anauthzgroup.roles if x.user] + # assert anauthzgroup_users == [u'annafan'], \ + # 'anauthzgroup should contain annafan (only)' + # assert anauthzgroup_user_roles == [(u'annafan', u'reader')],\ + # 'annafan should be a reader' + + # # Since annafan has been added to anauthzgroup, which is an admin on + # # anotherauthzgroup, she should now be able to see both the groups. + # get_page(auth_group_index_url, 200, 'annafan', + # 'There are 2 auth', + # "annafan should now be able to see both groups") + + # # When annafan looks at the page for anauthzgroup now + # # She should see that there's one user: + # get_page(anauthzgroup_url, 200,'annafan', + # 'There are 1 users in this', + # 'annafan should be able to see the list of members') + + # # Which is her, so her name should be in there somewhere: + # get_page(anauthzgroup_url, 200,'annafan', + # 'annafan', + # 'annafan should be listed as a member') + + # # But she shouldn't be able to see the edit page for that group. + + # # The behaviour of the test setup here is a bit weird, since in the + # # browser she gets redirected to the login page, but from these tests, + # # she just gets a 401, with no apparent redirect. Sources inform me + # # that this is normal, and to do with repoze being in the application + # # stack but not in the test stack. + # get_page(anauthzgroup_edit_url, 401, 'annafan', + # 'not authorized to edit', + # 'annafan should not be able to edit the list of members') + # # this behaviour also means that we get a flash message left over, which appears on + # # whatever the next page is. + + # # I'm going to assert that behaviour here, just to note it. It's most + # # definitely not required functionality! We'll do a dummy fetch of the + # # main page for anauthzgroup, which will have the errant flash message + # get_page(anauthzgroup_url, 200, 'annafan', + # 'not authorized to edit', + # 'flash message should carry over to next fetch') + + # # But if we do the dummy fetch twice, the flash message should have gone + # res = get_page(anauthzgroup_url, 200, 'annafan') + # assert 'not authorized to edit' not in res, 'flash message should have gone' + + # # Since annafan is now a member of anauthzgroup, she should have admin privileges + # # on anotherauthzgroup + # anotherauthzgroup_edit_url = url_for(controller='/authorization_group', + # action='edit', + # id='anotherauthzgroup') + + # # Which means that she can go to the edit page: + # res = get_page(anotherauthzgroup_edit_url, 200, 'annafan', + # 'There are no users', + # "There shouldn't be any users in anotherauthzgroup") + + # # And change the name of the group + # # The group name editing box has a name dependent on the id of the group, + # # so we find it by regex in the page. + # import re + # p = re.compile('AuthorizationGroup-.*-name') + # groupnamebox = [ v for k,v in res.forms['group-edit'].fields.items() if p.match(k)][0][0] + # groupnamebox.value = 'annasauthzgroup' + # res = res.forms['group-edit'].submit('save', extra_environ={'REMOTE_USER': 'annafan'}) + # res = res.follow() + + # ## POTENTIAL BUG: + # # note that she could change the name of the group to anauthzgroup, + # # which causes problems due to the name collision. This should be + # # guarded against. + + + # # annafan should still be able to see the admin and edit pages of the + # # newly renamed group by virtue of being a member of anauthzgroup + # annasauthzgroup_authz_url = url_for(controller='/authorization_group', + # action='authz', + # id='annasauthzgroup') + + # annasauthzgroup_edit_url = url_for(controller='/authorization_group', + # action='edit', + # id='annasauthzgroup') + + + # res = get_page(annasauthzgroup_authz_url, 200, 'annafan', + # 'Authorization for authorization group: annasauthzgroup', + # 'should be authz page') + + # # annafan has the power to remove anauthzgroup's admin role on her group + # # The button to remove that role is a link, rather than a submit. I + # # assume there is a better way to do this than searching by regex, but I + # # can't find it. + # import re + # delete_links = re.compile('
    ').findall(res.body) + # assert len(delete_links) == 1, "There should only be one delete link here" + # delete_link = delete_links[0] + + # # Paranoid check, try to follow link without credentials. Should be redirected. + # res = self.app.get(delete_link, status=302) + # res = res.follow() + # assert 'Not authorized to edit authorization for group' in res,\ + # "following link without credentials should result in redirection to login page" + + # # Now follow it as annafan, which should work. + # get_page(delete_link, 200,'annafan', + # "Deleted role 'admin' for authorization group 'anauthzgroup'", + # "Page should mention the deleted role") + + # # Trying it a second time should fail since she's now not an admin. + # get_page(delete_link, 401,'annafan') + + # # No one should now have any rights on annasauthzgroup, including + # # annafan herself. So this should fail too. Again, get a 401 error + # # here, but in the browser we get redirected if we try. + # get_page(annasauthzgroup_authz_url, 401,'annafan') + + # # testsysadmin can put her back. + # # It appears that the select boxes on this form need to be set by id + # anauthzgroupid = model.AuthorizationGroup.by_name(u'anauthzgroup').id + # annafanid = model.User.by_name(u'annafan').id + + # # first try to make both anauthzgroup and annafan editors. This should fail. + # res = get_page(annasauthzgroup_authz_url,200, 'testsysadmin') + # gaf= res.forms['group-authz'] + # gaf['AuthorizationGroupRole--authorized_group_id'] = anauthzgroupid + # gaf['AuthorizationGroupRole--role'] = 'editor' + # gaf['AuthorizationGroupRole--user_id'] = annafanid + # res = gaf.submit('save', status=200, extra_environ={'REMOTE_USER': 'testsysadmin'}) + # assert 'Please select either a user or an authorization group, not both.' in res,\ + # 'request should fail if you change both user and authz group' + + # # settle for just doing one at a time. make anauthzgroup an editor + # res = get_page(annasauthzgroup_authz_url, 200, 'testsysadmin') + # gaf= res.forms['group-authz'] + # gaf['AuthorizationGroupRole--authorized_group_id'] = anauthzgroupid + # gaf['AuthorizationGroupRole--role'] = 'editor' + # res = gaf.submit('save',status=200, extra_environ={'REMOTE_USER': 'testsysadmin'}) + # assert "Added role 'editor' for authorization group 'anauthzgroup'" in res, \ + # "no flash message" + + # # and make annafan a reader + # res = get_page(annasauthzgroup_authz_url, 200, 'testsysadmin') + # gaf= res.forms['group-authz'] + # gaf['AuthorizationGroupRole--user_id'] = annafanid + # gaf['AuthorizationGroupRole--role'] = 'reader' + # res = gaf.submit('save', status=200, extra_environ={'REMOTE_USER': 'testsysadmin'}) + # assert "Added role 'reader' for user 'annafan'" in res, "no flash message" + + # # annafan should now be able to add her friends to annasauthzgroup + # res = get_page(annasauthzgroup_edit_url, 200, 'annafan') + # res.forms['group-edit']['AuthorizationGroupUser--user_name']='tester' + # # this follows the post/redirect/get pattern + # res = res.forms['group-edit'].submit('save', status=302, + # extra_environ={'REMOTE_USER': 'annafan'}) + # res = res.follow(status=200, extra_environ={'REMOTE_USER': 'annafan'}) + # # and she gets redirected to the group view page + # assert 'tester' in res, 'tester not added?' + + # # she needs to do them one by one + # res = get_page(annasauthzgroup_edit_url, 200, 'annafan', + # 'tester', + # 'tester not in edit form') + # res.forms['group-edit']['AuthorizationGroupUser--user_name']='russianfan' + # res = res.forms['group-edit'].submit('save', status=302, extra_environ={'REMOTE_USER': 'annafan'}) + # res = res.follow(status=200, extra_environ={'REMOTE_USER': 'annafan'}) + + # # and finally adds herself + # res = self.app.get(annasauthzgroup_edit_url, status=200, extra_environ={'REMOTE_USER': 'annafan'}) + # assert 'russianfan' in res, 'russianfan not added?' + # res.forms['group-edit']['AuthorizationGroupUser--user_name']='annafan' + # res = res.forms['group-edit'].submit('save', status=302, extra_environ={'REMOTE_USER': 'annafan'}) + # res = res.follow(status=200, extra_environ={'REMOTE_USER': 'annafan'}) + # assert 'annafan' in res, 'annafan not added?' + + # # finally let's check that annafan can create a completely new authzgroup + # new_authzgroup_url = url_for(controller='/authorization_group', action='new') + # res = get_page(new_authzgroup_url, 200,'annafan', + # 'New Authorization Group', + # "wrong page?") + # gef = res.forms['group-edit'] + # gef['AuthorizationGroup--name']="newgroup" + # gef['AuthorizationGroupUser--user_name'] = "russianfan" + # res = gef.submit('save', status=302, extra_environ={'REMOTE_USER': 'annafan'}) + # #post/redirect/get + # res = res.follow(status=200, extra_environ={'REMOTE_USER': 'annafan'}) + + # assert 'newgroup' in res, "should have redirected to the newgroup page" + # assert 'russianfan' in res, "no russianfan" + # assert 'There are 1 users in this authorization group' in res, "missing text" + diff --git a/ckan/tests/functional/test_edit_authz.py b/ckan/tests/functional/test_edit_authz.py index 4573fbd3888..941332ddb9a 100644 --- a/ckan/tests/functional/test_edit_authz.py +++ b/ckan/tests/functional/test_edit_authz.py @@ -77,7 +77,6 @@ def teardown_class(self): model.repo.rebuild_db() def test_access_to_authz(self): - raise SkipTest() #for each of the three authz pages, check that the access permissions work correctly for (c,i) in [('package', self.pkg),('group', self.group),('authorization_group', self.authzgroup)]: offset = url_for(controller=c, action='authz', id=i) @@ -119,7 +118,8 @@ def authzgroup_roles(self): # check that the authz page for each object contains certain key strings def test_2_read_ok(self): for (c,i,m) in [('package', self.pkg, self.package_roles),\ - ('group', self.group, self.group_roles)]: + ('group', self.group, self.group_roles),\ + ('authorization_group', self.authzgroup, self.authzgroup_roles)]: offset = url_for(controller=c, action='authz', id=i) res = self.app.get(offset, extra_environ={'REMOTE_USER': self.admin}) assert i in res, res @@ -162,7 +162,8 @@ def change_roles(self, user): # loop variables here are the controller string, the name of the object we're changing, and three functions, # the first fn gets the roles which we'd like to change, and the other two get the roles which we'd like to stay the same. for (c,i,var,const1,const2) in [('package', self.pkg, self.package_roles, self.group_roles, self.authzgroup_roles),\ - ('group', self.group, self.group_roles, self.package_roles, self.authzgroup_roles)]: + ('group', self.group, self.group_roles, self.package_roles, self.authzgroup_roles),\ + ('authorization_group', self.authzgroup, self.authzgroup_roles, self.package_roles, self.group_roles)]: # load authz page offset = url_for(controller=c, action='authz', id=i) @@ -231,7 +232,8 @@ def delete_role_as(self,user): # loop variables here are the controller string, the name of the object we're changing, and three functions, # the first fn gets the roles which we'd like to change, and the other two get the roles which we'd like to stay the same. for (c,i,var,const1,const2) in [('package', self.pkg, self.package_roles, self.group_roles, self.authzgroup_roles),\ - ('group', self.group, self.group_roles, self.package_roles, self.authzgroup_roles)]: + ('group', self.group, self.group_roles, self.package_roles, self.authzgroup_roles),\ + ('authorization_group', self.authzgroup, self.authzgroup_roles, self.package_roles, self.group_roles)]: # get the authz page, check that visitor's in there # remove visitor's role on the package @@ -338,7 +340,8 @@ def add_change_delete_authzgroup_as(self, user): ('logged_in', 'editor')] for (c,i,var,const1,const2) in [('package', self.pkg, self.package_roles, self.group_roles, self.authzgroup_roles),\ - ('group', self.group, self.group_roles, self.package_roles, self.authzgroup_roles)]: + ('group', self.group, self.group_roles, self.package_roles, self.authzgroup_roles),\ + ('authorization_group', self.authzgroup, self.authzgroup_roles, self.package_roles, self.group_roles)]: # get the authz page, check that it contains the object name offset = url_for(controller=c, action='authz', id=i) @@ -412,9 +415,7 @@ def add_change_delete_authzgroup_as(self, user): def test_5_admin_changes_adds_deletes_authzgroup(self): - raise SkipTest() self.add_change_delete_authzgroup_as(self.admin) def test_5_sysadmin_changes_adds_deletes_authzgroup(self): - raise SkipTest() self.add_change_delete_authzgroup_as(self.sysadmin) diff --git a/ckan/tests/functional/test_package_edit_authz.py b/ckan/tests/functional/test_package_edit_authz.py index e3086437d90..8f80a2778a9 100644 --- a/ckan/tests/functional/test_package_edit_authz.py +++ b/ckan/tests/functional/test_package_edit_authz.py @@ -14,7 +14,7 @@ def setup_class(self): # two packages test6 and test6a, m-a is admin on both model.repo.init_db() model.repo.new_revision() - + self.sysadmin = 'madeup-sysadmin' sysadmin_user = model.User(name=unicode(self.sysadmin)) self.admin = 'madeup-administrator' @@ -51,7 +51,7 @@ def test_0_nonadmin_cannot_edit_authz(self): res = self.app.get(offset, status=[302, 401]) res = res.follow() assert res.request.url.startswith('/user/login') - + def test_1_admin_has_access(self): offset = url_for(controller='package', action='authz', id=self.pkgname) res = self.app.get(offset, extra_environ={'REMOTE_USER': @@ -61,7 +61,7 @@ def test_1_sysadmin_has_access(self): offset = url_for(controller='package', action='authz', id=self.pkgname) res = self.app.get(offset, extra_environ={'REMOTE_USER': self.sysadmin}) - + def test_2_read_ok(self): offset = url_for(controller='package', action='authz', id=self.pkgname) res = self.app.get(offset, extra_environ={'REMOTE_USER': @@ -215,7 +215,7 @@ def delete_role_as(self,user): check_and_set_checkbox(form, u'visitor', u'reader', True, False) check_and_set_checkbox(form, u'visitor', u'editor', False, True) res = form.submit('save', extra_environ={'REMOTE_USER': user}) - + # check that the page contains strings for everyone assert 'visitor' in res assert 'madeup-administrator' in res @@ -235,3 +235,79 @@ def test_4_sysadmin_deletes_role(self): self.delete_role_as(self.sysadmin) + def test_5_add_change_delete_authzgroup(self): + user=self.admin + + # get the authz page, check that authzgroup isn't in there + offset = url_for(controller='package', action='authz', id=self.pkgname) + res = self.app.get(offset, extra_environ={'REMOTE_USER':user}) + assert self.pkgname in res + + # check the state of the database + self.assert_package_roles_to_be([ + ('madeup-administrator', 'admin'), + ('visitor', 'editor'), + ('logged_in', 'editor')]) + + # and that corresponding user strings are in the authz page + assert 'visitor' in res + assert 'madeup-administrator' in res + assert 'logged_in' in res + assert 'madeup-authzgroup' not in res + + # add madeup-authzgroup as an admin + form = res.forms['authzgroup_addform'] + form.fields['new_user_name'][0].value='madeup-authzgroup' + checkbox = [x for x in form.fields['admin'] \ + if x.__class__.__name__ == 'Checkbox'][0] + # check the checkbox is currently unticked + assert checkbox.checked == False + # tick it and submit + checkbox.checked=True + res = form.submit('authz_add', extra_environ={'REMOTE_USER':user}) + assert "User role(s) added" in res, "don't see flash message" + + # examine the new page for user names/authzgroup names + assert 'visitor' in res + assert 'madeup-administrator' in res + assert 'logged_in' in res + assert 'madeup-authzgroup' in res + + # and ensure that the database has changed as expected + self.assert_package_roles_to_be([ + ('madeup-authzgroup', 'admin'), + ('madeup-administrator', 'admin'), + ('visitor', 'editor'), + ('logged_in', 'editor')]) + + # check that the checkbox states are what we think they should be + # and change madeup-authzgroup from admin to editor + form = res.forms['authzgroup_form'] + check_and_set_checkbox(form, u'madeup-authzgroup', u'editor', False, True) + check_and_set_checkbox(form, u'madeup-authzgroup', u'admin', True, False) + res = form.submit('authz_save', extra_environ={'REMOTE_USER': user}) + + #check database has changed. + self.assert_package_roles_to_be([ + ('madeup-authzgroup', 'editor'), + ('madeup-administrator', 'admin'), + ('visitor', 'editor'), + ('logged_in', 'editor')]) + + # now remove madeup-authzgroup entirely + form = res.forms['authzgroup_form'] + check_and_set_checkbox(form, u'madeup-authzgroup', u'editor', True, False) + check_and_set_checkbox(form, u'madeup-authzgroup', u'admin', False, False) + res = form.submit('authz_save', extra_environ={'REMOTE_USER': user}) + + #check database is back to normal + self.assert_package_roles_to_be([ + ('madeup-administrator', 'admin'), + ('visitor', 'editor'), + ('logged_in', 'editor')]) + + # and that page contains only the expected strings + assert 'visitor' in res + assert 'madeup-administrator' in res + assert 'logged_in' in res + assert 'madeup-authzgroup' not in res