summaryrefslogtreecommitdiffstats
path: root/ipaserver/plugins/aci.py
diff options
context:
space:
mode:
authorJan Cholasta <jcholast@redhat.com>2016-04-28 10:30:05 +0200
committerJan Cholasta <jcholast@redhat.com>2016-06-03 09:00:34 +0200
commit6e44557b601f769d23ee74555a72e8b5cc62c0c9 (patch)
treeeedd3e054b0709341b9f58c190ea54f999f7d13a /ipaserver/plugins/aci.py
parentec841e5d7ab29d08de294b3fa863a631cd50e30a (diff)
downloadfreeipa-6e44557b601f769d23ee74555a72e8b5cc62c0c9.tar.gz
freeipa-6e44557b601f769d23ee74555a72e8b5cc62c0c9.tar.xz
freeipa-6e44557b601f769d23ee74555a72e8b5cc62c0c9.zip
ipalib: move server-side plugins to ipaserver
Move the remaining plugin code from ipalib.plugins to ipaserver.plugins. Remove the now unused ipalib.plugins package. https://fedorahosted.org/freeipa/ticket/4739 Reviewed-By: David Kupka <dkupka@redhat.com>
Diffstat (limited to 'ipaserver/plugins/aci.py')
-rw-r--r--ipaserver/plugins/aci.py986
1 files changed, 986 insertions, 0 deletions
diff --git a/ipaserver/plugins/aci.py b/ipaserver/plugins/aci.py
new file mode 100644
index 000000000..01c929230
--- /dev/null
+++ b/ipaserver/plugins/aci.py
@@ -0,0 +1,986 @@
+# Authors:
+# Rob Crittenden <rcritten@redhat.com>
+# Pavel Zuna <pzuna@redhat.com>
+#
+# Copyright (C) 2009 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+Directory Server Access Control Instructions (ACIs)
+
+ACIs are used to allow or deny access to information. This module is
+currently designed to allow, not deny, access.
+
+The aci commands are designed to grant permissions that allow updating
+existing entries or adding or deleting new ones. The goal of the ACIs
+that ship with IPA is to provide a set of low-level permissions that
+grant access to special groups called taskgroups. These low-level
+permissions can be combined into roles that grant broader access. These
+roles are another type of group, roles.
+
+For example, if you have taskgroups that allow adding and modifying users you
+could create a role, useradmin. You would assign users to the useradmin
+role to allow them to do the operations defined by the taskgroups.
+
+You can create ACIs that delegate permission so users in group A can write
+attributes on group B.
+
+The type option is a map that applies to all entries in the users, groups or
+host location. It is primarily designed to be used when granting add
+permissions (to write new entries).
+
+An ACI consists of three parts:
+1. target
+2. permissions
+3. bind rules
+
+The target is a set of rules that define which LDAP objects are being
+targeted. This can include a list of attributes, an area of that LDAP
+tree or an LDAP filter.
+
+The targets include:
+- attrs: list of attributes affected
+- type: an object type (user, group, host, service, etc)
+- memberof: members of a group
+- targetgroup: grant access to modify a specific group. This is primarily
+ designed to enable users to add or remove members of a specific group.
+- filter: A legal LDAP filter used to narrow the scope of the target.
+- subtree: Used to apply a rule across an entire set of objects. For example,
+ to allow adding users you need to grant "add" permission to the subtree
+ ldap://uid=*,cn=users,cn=accounts,dc=example,dc=com. The subtree option
+ is a fail-safe for objects that may not be covered by the type option.
+
+The permissions define what the ACI is allowed to do, and are one or
+more of:
+1. write - write one or more attributes
+2. read - read one or more attributes
+3. add - add a new entry to the tree
+4. delete - delete an existing entry
+5. all - all permissions are granted
+
+Note the distinction between attributes and entries. The permissions are
+independent, so being able to add a user does not mean that the user will
+be editable.
+
+The bind rule defines who this ACI grants permissions to. The LDAP server
+allows this to be any valid LDAP entry but we encourage the use of
+taskgroups so that the rights can be easily shared through roles.
+
+For a more thorough description of access controls see
+http://www.redhat.com/docs/manuals/dir-server/ag/8.0/Managing_Access_Control.html
+
+EXAMPLES:
+
+NOTE: ACIs are now added via the permission plugin. These examples are to
+demonstrate how the various options work but this is done via the permission
+command-line now (see last example).
+
+ Add an ACI so that the group "secretaries" can update the address on any user:
+ ipa group-add --desc="Office secretaries" secretaries
+ ipa aci-add --attrs=streetAddress --memberof=ipausers --group=secretaries --permissions=write --prefix=none "Secretaries write addresses"
+
+ Show the new ACI:
+ ipa aci-show --prefix=none "Secretaries write addresses"
+
+ Add an ACI that allows members of the "addusers" permission to add new users:
+ ipa aci-add --type=user --permission=addusers --permissions=add --prefix=none "Add new users"
+
+ Add an ACI that allows members of the editors manage members of the admins group:
+ ipa aci-add --permissions=write --attrs=member --targetgroup=admins --group=editors --prefix=none "Editors manage admins"
+
+ Add an ACI that allows members of the admins group to manage the street and zip code of those in the editors group:
+ ipa aci-add --permissions=write --memberof=editors --group=admins --attrs=street --attrs=postalcode --prefix=none "admins edit the address of editors"
+
+ Add an ACI that allows the admins group manage the street and zipcode of those who work for the boss:
+ ipa aci-add --permissions=write --group=admins --attrs=street --attrs=postalcode --filter="(manager=uid=boss,cn=users,cn=accounts,dc=example,dc=com)" --prefix=none "Edit the address of those who work for the boss"
+
+ Add an entirely new kind of record to IPA that isn't covered by any of the --type options, creating a permission:
+ ipa permission-add --permissions=add --subtree="cn=*,cn=orange,cn=accounts,dc=example,dc=com" --desc="Add Orange Entries" add_orange
+
+
+The show command shows the raw 389-ds ACI.
+
+IMPORTANT: When modifying the target attributes of an existing ACI you
+must include all existing attributes as well. When doing an aci-mod the
+targetattr REPLACES the current attributes, it does not add to them.
+
+"""
+from copy import deepcopy
+
+import six
+
+from ipalib import api, crud, errors
+from ipalib import Object
+from ipalib import Flag, Str, StrEnum, DNParam
+from ipalib.aci import ACI
+from ipalib import output
+from ipalib import _, ngettext
+from ipalib.plugable import Registry
+from .baseldap import gen_pkey_only_option, pkey_to_value
+from ipapython.ipa_log_manager import root_logger
+from ipapython.dn import DN
+
+if six.PY3:
+ unicode = str
+
+register = Registry()
+
+ACI_NAME_PREFIX_SEP = ":"
+
+_type_map = {
+ 'user': 'ldap:///' + str(DN(('uid', '*'), api.env.container_user, api.env.basedn)),
+ 'group': 'ldap:///' + str(DN(('cn', '*'), api.env.container_group, api.env.basedn)),
+ 'host': 'ldap:///' + str(DN(('fqdn', '*'), api.env.container_host, api.env.basedn)),
+ 'hostgroup': 'ldap:///' + str(DN(('cn', '*'), api.env.container_hostgroup, api.env.basedn)),
+ 'service': 'ldap:///' + str(DN(('krbprincipalname', '*'), api.env.container_service, api.env.basedn)),
+ 'netgroup': 'ldap:///' + str(DN(('ipauniqueid', '*'), api.env.container_netgroup, api.env.basedn)),
+ 'dnsrecord': 'ldap:///' + str(DN(('idnsname', '*'), api.env.container_dns, api.env.basedn)),
+}
+
+_valid_permissions_values = [
+ u'read', u'write', u'add', u'delete', u'all'
+]
+
+_valid_prefix_values = (
+ u'permission', u'delegation', u'selfservice', u'none'
+)
+
+class ListOfACI(output.Output):
+ type = (list, tuple)
+ doc = _('A list of ACI values')
+
+ def validate(self, cmd, entries):
+ assert isinstance(entries, self.type)
+ for (i, entry) in enumerate(entries):
+ if not isinstance(entry, unicode):
+ raise TypeError(output.emsg %
+ (cmd.name, self.__class__.__name__,
+ self.name, i, unicode, type(entry), entry)
+ )
+
+aci_output = (
+ output.Output('result', unicode, 'A string representing the ACI'),
+ output.value,
+ output.summary,
+)
+
+
+def _make_aci_name(aciprefix, aciname):
+ """
+ Given a name and a prefix construct an ACI name.
+ """
+ if aciprefix == u"none":
+ return aciname
+
+ return aciprefix + ACI_NAME_PREFIX_SEP + aciname
+
+def _parse_aci_name(aciname):
+ """
+ Parse the raw ACI name and return a tuple containing the ACI prefix
+ and the actual ACI name.
+ """
+ aciparts = aciname.partition(ACI_NAME_PREFIX_SEP)
+
+ if not aciparts[2]: # no prefix/name separator found
+ return (u"none",aciparts[0])
+
+ return (aciparts[0], aciparts[2])
+
+def _group_from_memberof(memberof):
+ """
+ Pull the group name out of a memberOf filter
+ """
+ st = memberof.find('memberOf=')
+ if st == -1:
+ # We have a raw group name, use that
+ return api.Object['group'].get_dn(memberof)
+ en = memberof.find(')', st)
+ return memberof[st+9:en]
+
+def _make_aci(ldap, current, aciname, kw):
+ """
+ Given a name and a set of keywords construct an ACI.
+ """
+ # Do some quick and dirty validation.
+ checked_args=['type','filter','subtree','targetgroup','attrs','memberof']
+ valid={}
+ for arg in checked_args:
+ if arg in kw:
+ valid[arg]=kw[arg] is not None
+ else:
+ valid[arg]=False
+
+ if valid['type'] + valid['filter'] + valid['subtree'] + valid['targetgroup'] > 1:
+ raise errors.ValidationError(name='target', error=_('type, filter, subtree and targetgroup are mutually exclusive'))
+
+ if 'aciprefix' not in kw:
+ raise errors.ValidationError(name='aciprefix', error=_('ACI prefix is required'))
+
+ if sum(valid.values()) == 0:
+ raise errors.ValidationError(name='target', error=_('at least one of: type, filter, subtree, targetgroup, attrs or memberof are required'))
+
+ if valid['filter'] + valid['memberof'] > 1:
+ raise errors.ValidationError(name='target', error=_('filter and memberof are mutually exclusive'))
+
+ group = 'group' in kw
+ permission = 'permission' in kw
+ selfaci = 'selfaci' in kw and kw['selfaci'] == True
+ if group + permission + selfaci > 1:
+ raise errors.ValidationError(name='target', error=_('group, permission and self are mutually exclusive'))
+ elif group + permission + selfaci == 0:
+ raise errors.ValidationError(name='target', error=_('One of group, permission or self is required'))
+
+ # Grab the dn of the group we're granting access to. This group may be a
+ # permission or a user group.
+ entry_attrs = []
+ if permission:
+ # This will raise NotFound if the permission doesn't exist
+ try:
+ entry_attrs = api.Command['permission_show'](kw['permission'])['result']
+ except errors.NotFound as e:
+ if 'test' in kw and not kw.get('test'):
+ raise e
+ else:
+ entry_attrs = {
+ 'dn': DN(('cn', kw['permission']),
+ api.env.container_permission, api.env.basedn),
+ }
+ elif group:
+ # Not so friendly with groups. This will raise
+ try:
+ group_dn = api.Object['group'].get_dn_if_exists(kw['group'])
+ entry_attrs = {'dn': group_dn}
+ except errors.NotFound:
+ raise errors.NotFound(reason=_("Group '%s' does not exist") % kw['group'])
+
+ try:
+ a = ACI(current)
+ a.name = _make_aci_name(kw['aciprefix'], aciname)
+ a.permissions = kw['permissions']
+ if 'selfaci' in kw and kw['selfaci']:
+ a.set_bindrule('userdn = "ldap:///self"')
+ else:
+ dn = entry_attrs['dn']
+ a.set_bindrule('groupdn = "ldap:///%s"' % dn)
+ if valid['attrs']:
+ a.set_target_attr(kw['attrs'])
+ if valid['memberof']:
+ try:
+ api.Object['group'].get_dn_if_exists(kw['memberof'])
+ except errors.NotFound:
+ api.Object['group'].handle_not_found(kw['memberof'])
+ groupdn = _group_from_memberof(kw['memberof'])
+ a.set_target_filter('memberOf=%s' % groupdn)
+ if valid['filter']:
+ # Test the filter by performing a simple search on it. The
+ # filter is considered valid if either it returns some entries
+ # or it returns no entries, otherwise we let whatever exception
+ # happened be raised.
+ if kw['filter'] in ('', None, u''):
+ raise errors.BadSearchFilter(info=_('empty filter'))
+ try:
+ entries = ldap.find_entries(filter=kw['filter'])
+ except errors.NotFound:
+ pass
+ a.set_target_filter(kw['filter'])
+ if valid['type']:
+ target = _type_map[kw['type']]
+ a.set_target(target)
+ if valid['targetgroup']:
+ # Purposely no try here so we'll raise a NotFound
+ group_dn = api.Object['group'].get_dn_if_exists(kw['targetgroup'])
+ target = 'ldap:///%s' % group_dn
+ a.set_target(target)
+ if valid['subtree']:
+ # See if the subtree is a full URI
+ target = kw['subtree']
+ if not target.startswith('ldap:///'):
+ target = 'ldap:///%s' % target
+ a.set_target(target)
+ except SyntaxError as e:
+ raise errors.ValidationError(name='target', error=_('Syntax Error: %(error)s') % dict(error=str(e)))
+
+ return a
+
+def _aci_to_kw(ldap, a, test=False, pkey_only=False):
+ """Convert an ACI into its equivalent keywords.
+
+ This is used for the modify operation so we can merge the
+ incoming kw and existing ACI and pass the result to
+ _make_aci().
+ """
+ kw = {}
+ kw['aciprefix'], kw['aciname'] = _parse_aci_name(a.name)
+ if pkey_only:
+ return kw
+ kw['permissions'] = tuple(a.permissions)
+ if 'targetattr' in a.target:
+ kw['attrs'] = tuple(unicode(e)
+ for e in a.target['targetattr']['expression'])
+ if 'targetfilter' in a.target:
+ target = a.target['targetfilter']['expression']
+ if target.startswith('(memberOf=') or target.startswith('memberOf='):
+ (junk, memberof) = target.split('memberOf=', 1)
+ memberof = DN(memberof)
+ kw['memberof'] = memberof['cn']
+ else:
+ kw['filter'] = unicode(target)
+ if 'target' in a.target:
+ target = a.target['target']['expression']
+ found = False
+ for k in _type_map.keys():
+ if _type_map[k] == target:
+ kw['type'] = unicode(k)
+ found = True
+ break
+ if not found:
+ if target.startswith('('):
+ kw['filter'] = unicode(target)
+ else:
+ # See if the target is a group. If so we set the
+ # targetgroup attr, otherwise we consider it a subtree
+ try:
+ targetdn = DN(target.replace('ldap:///',''))
+ except ValueError as e:
+ raise errors.ValidationError(name='subtree', error=_("invalid DN (%s)") % e.message)
+ if targetdn.endswith(DN(api.env.container_group, api.env.basedn)):
+ kw['targetgroup'] = targetdn[0]['cn']
+ else:
+ kw['subtree'] = unicode(target)
+
+ groupdn = a.bindrule['expression']
+ groupdn = groupdn.replace('ldap:///','')
+ if groupdn == 'self':
+ kw['selfaci'] = True
+ elif groupdn == 'anyone':
+ pass
+ else:
+ groupdn = DN(groupdn)
+ if len(groupdn) and groupdn[0].attr == 'cn':
+ dn = DN()
+ entry = ldap.make_entry(dn)
+ try:
+ entry = ldap.get_entry(groupdn, ['cn'])
+ except errors.NotFound as e:
+ # FIXME, use real name here
+ if test:
+ dn = DN(('cn', 'test'), api.env.container_permission,
+ api.env.basedn)
+ entry = ldap.make_entry(dn, {'cn': [u'test']})
+ if api.env.container_permission in entry.dn:
+ kw['permission'] = entry['cn'][0]
+ else:
+ if 'cn' in entry:
+ kw['group'] = entry['cn'][0]
+
+ return kw
+
+def _convert_strings_to_acis(acistrs):
+ acis = []
+ for a in acistrs:
+ try:
+ acis.append(ACI(a))
+ except SyntaxError as e:
+ root_logger.warning("Failed to parse: %s" % a)
+ return acis
+
+def _find_aci_by_name(acis, aciprefix, aciname):
+ name = _make_aci_name(aciprefix, aciname).lower()
+ for a in acis:
+ if a.name.lower() == name:
+ return a
+ raise errors.NotFound(reason=_('ACI with name "%s" not found') % aciname)
+
+
+def validate_permissions(ugettext, perm):
+ perm = perm.strip().lower()
+ if perm not in _valid_permissions_values:
+ return '"%s" is not a valid permission' % perm
+
+
+def _normalize_permissions(perm):
+ valid_permissions = []
+ perm = perm.strip().lower()
+ if perm not in valid_permissions:
+ valid_permissions.append(perm)
+ return ','.join(valid_permissions)
+
+_prefix_option = StrEnum('aciprefix',
+ cli_name='prefix',
+ label=_('ACI prefix'),
+ doc=_('Prefix used to distinguish ACI types ' \
+ '(permission, delegation, selfservice, none)'),
+ values=_valid_prefix_values,
+ )
+
+
+@register()
+class aci(Object):
+ """
+ ACI object.
+ """
+ NO_CLI = True
+
+ label = _('ACIs')
+
+ takes_params = (
+ Str('aciname',
+ cli_name='name',
+ label=_('ACI name'),
+ primary_key=True,
+ flags=('virtual_attribute',),
+ ),
+ Str('permission?',
+ cli_name='permission',
+ label=_('Permission'),
+ doc=_('Permission ACI grants access to'),
+ flags=('virtual_attribute',),
+ ),
+ Str('group?',
+ cli_name='group',
+ label=_('User group'),
+ doc=_('User group ACI grants access to'),
+ flags=('virtual_attribute',),
+ ),
+ Str('permissions+', validate_permissions,
+ cli_name='permissions',
+ label=_('Permissions'),
+ doc=_('Permissions to grant' \
+ '(read, write, add, delete, all)'),
+ normalizer=_normalize_permissions,
+ flags=('virtual_attribute',),
+ ),
+ Str('attrs*',
+ cli_name='attrs',
+ label=_('Attributes to which the permission applies'),
+ doc=_('Attributes'),
+ flags=('virtual_attribute',),
+ ),
+ StrEnum('type?',
+ cli_name='type',
+ label=_('Type'),
+ doc=_('type of IPA object (user, group, host, hostgroup, service, netgroup)'),
+ values=(u'user', u'group', u'host', u'service', u'hostgroup', u'netgroup', u'dnsrecord'),
+ flags=('virtual_attribute',),
+ ),
+ Str('memberof?',
+ cli_name='memberof',
+ label=_('Member of'), # FIXME: Does this label make sense?
+ doc=_('Member of a group'),
+ flags=('virtual_attribute',),
+ ),
+ Str('filter?',
+ cli_name='filter',
+ label=_('Filter'),
+ doc=_('Legal LDAP filter (e.g. ou=Engineering)'),
+ flags=('virtual_attribute',),
+ ),
+ Str('subtree?',
+ cli_name='subtree',
+ label=_('Subtree'),
+ doc=_('Subtree to apply ACI to'),
+ flags=('virtual_attribute',),
+ ),
+ Str('targetgroup?',
+ cli_name='targetgroup',
+ label=_('Target group'),
+ doc=_('Group to apply ACI to'),
+ flags=('virtual_attribute',),
+ ),
+ Flag('selfaci?',
+ cli_name='self',
+ label=_('Target your own entry (self)'),
+ doc=_('Apply ACI to your own entry (self)'),
+ flags=('virtual_attribute',),
+ ),
+ )
+
+
+@register()
+class aci_add(crud.Create):
+ """
+ Create new ACI.
+ """
+ NO_CLI = True
+ msg_summary = _('Created ACI "%(value)s"')
+
+ takes_options = (
+ _prefix_option,
+ Flag('test?',
+ doc=_('Test the ACI syntax but don\'t write anything'),
+ default=False,
+ ),
+ )
+
+ def execute(self, aciname, **kw):
+ """
+ Execute the aci-create operation.
+
+ Returns the entry as it will be created in LDAP.
+
+ :param aciname: The name of the ACI being added.
+ :param kw: Keyword arguments for the other LDAP attributes.
+ """
+ assert 'aciname' not in kw
+ ldap = self.api.Backend.ldap2
+
+ newaci = _make_aci(ldap, None, aciname, kw)
+
+ entry = ldap.get_entry(self.api.env.basedn, ['aci'])
+
+ acis = _convert_strings_to_acis(entry.get('aci', []))
+ for a in acis:
+ # FIXME: add check for permission_group = permission_group
+ if a.isequal(newaci) or newaci.name == a.name:
+ raise errors.DuplicateEntry()
+
+ newaci_str = unicode(newaci)
+ entry.setdefault('aci', []).append(newaci_str)
+
+ if not kw.get('test', False):
+ ldap.update_entry(entry)
+
+ if kw.get('raw', False):
+ result = dict(aci=unicode(newaci_str))
+ else:
+ result = _aci_to_kw(ldap, newaci, kw.get('test', False))
+ return dict(
+ result=result,
+ value=pkey_to_value(aciname, kw),
+ )
+
+
+@register()
+class aci_del(crud.Delete):
+ """
+ Delete ACI.
+ """
+ NO_CLI = True
+ has_output = output.standard_boolean
+ msg_summary = _('Deleted ACI "%(value)s"')
+
+ takes_options = (_prefix_option,)
+
+ def execute(self, aciname, aciprefix, **options):
+ """
+ Execute the aci-delete operation.
+
+ :param aciname: The name of the ACI being deleted.
+ :param aciprefix: The ACI prefix.
+ """
+ ldap = self.api.Backend.ldap2
+
+ entry = ldap.get_entry(self.api.env.basedn, ['aci'])
+
+ acistrs = entry.get('aci', [])
+ acis = _convert_strings_to_acis(acistrs)
+ aci = _find_aci_by_name(acis, aciprefix, aciname)
+ for a in acistrs:
+ candidate = ACI(a)
+ if aci.isequal(candidate):
+ acistrs.remove(a)
+ break
+
+ entry['aci'] = acistrs
+
+ ldap.update_entry(entry)
+
+ return dict(
+ result=True,
+ value=pkey_to_value(aciname, options),
+ )
+
+
+@register()
+class aci_mod(crud.Update):
+ """
+ Modify ACI.
+ """
+ NO_CLI = True
+ has_output_params = (
+ Str('aci',
+ label=_('ACI'),
+ ),
+ )
+
+ takes_options = (_prefix_option,)
+
+ internal_options = ['rename']
+
+ msg_summary = _('Modified ACI "%(value)s"')
+
+ def execute(self, aciname, **kw):
+ aciprefix = kw['aciprefix']
+ ldap = self.api.Backend.ldap2
+
+ entry = ldap.get_entry(self.api.env.basedn, ['aci'])
+
+ acis = _convert_strings_to_acis(entry.get('aci', []))
+ aci = _find_aci_by_name(acis, aciprefix, aciname)
+
+ # The strategy here is to convert the ACI we're updating back into
+ # a series of keywords. Then we replace any keywords that have been
+ # updated and convert that back into an ACI and write it out.
+ oldkw = _aci_to_kw(ldap, aci)
+ newkw = deepcopy(oldkw)
+ if newkw.get('selfaci', False):
+ # selfaci is set in aci_to_kw to True only if the target is self
+ kw['selfaci'] = True
+ newkw.update(kw)
+ for acikw in (oldkw, newkw):
+ acikw.pop('aciname', None)
+
+ # _make_aci is what is run in aci_add and validates the input.
+ # Do this before we delete the existing ACI.
+ newaci = _make_aci(ldap, None, aciname, newkw)
+ if aci.isequal(newaci):
+ raise errors.EmptyModlist()
+
+ self.api.Command['aci_del'](aciname, aciprefix=aciprefix)
+
+ try:
+ result = self.api.Command['aci_add'](aciname, **newkw)['result']
+ except Exception as e:
+ # ACI could not be added, try to restore the old deleted ACI and
+ # report the ADD error back to user
+ try:
+ self.api.Command['aci_add'](aciname, **oldkw)
+ except Exception:
+ pass
+ raise e
+
+ if kw.get('raw', False):
+ result = dict(aci=unicode(newaci))
+ else:
+ result = _aci_to_kw(ldap, newaci)
+ return dict(
+ result=result,
+ value=pkey_to_value(aciname, kw),
+ )
+
+
+@register()
+class aci_find(crud.Search):
+ """
+ Search for ACIs.
+
+ Returns a list of ACIs
+
+ EXAMPLES:
+
+ To find all ACIs that apply directly to members of the group ipausers:
+ ipa aci-find --memberof=ipausers
+
+ To find all ACIs that grant add access:
+ ipa aci-find --permissions=add
+
+ Note that the find command only looks for the given text in the set of
+ ACIs, it does not evaluate the ACIs to see if something would apply.
+ For example, searching on memberof=ipausers will find all ACIs that
+ have ipausers as a memberof. There may be other ACIs that apply to
+ members of that group indirectly.
+ """
+ NO_CLI = True
+ msg_summary = ngettext('%(count)d ACI matched', '%(count)d ACIs matched', 0)
+
+ takes_options = (_prefix_option.clone_rename("aciprefix?", required=False),
+ gen_pkey_only_option("name"),)
+
+ def execute(self, term=None, **kw):
+ ldap = self.api.Backend.ldap2
+
+ entry = ldap.get_entry(self.api.env.basedn, ['aci'])
+
+ acis = _convert_strings_to_acis(entry.get('aci', []))
+ results = []
+
+ if term:
+ term = term.lower()
+ for a in acis:
+ if a.name.lower().find(term) != -1 and a not in results:
+ results.append(a)
+ acis = list(results)
+ else:
+ results = list(acis)
+
+ if kw.get('aciname'):
+ for a in acis:
+ prefix, name = _parse_aci_name(a.name)
+ if name != kw['aciname']:
+ results.remove(a)
+ acis = list(results)
+
+ if kw.get('aciprefix'):
+ for a in acis:
+ prefix, name = _parse_aci_name(a.name)
+ if prefix != kw['aciprefix']:
+ results.remove(a)
+ acis = list(results)
+
+ if kw.get('attrs'):
+ for a in acis:
+ if not 'targetattr' in a.target:
+ results.remove(a)
+ continue
+ alist1 = sorted(
+ [t.lower() for t in a.target['targetattr']['expression']]
+ )
+ alist2 = sorted([t.lower() for t in kw['attrs']])
+ if len(set(alist1) & set(alist2)) != len(alist2):
+ results.remove(a)
+ acis = list(results)
+
+ if kw.get('permission'):
+ try:
+ self.api.Command['permission_show'](
+ kw['permission']
+ )
+ except errors.NotFound:
+ pass
+ else:
+ for a in acis:
+ uri = 'ldap:///%s' % entry.dn
+ if a.bindrule['expression'] != uri:
+ results.remove(a)
+ acis = list(results)
+
+ if kw.get('permissions'):
+ for a in acis:
+ alist1 = sorted(a.permissions)
+ alist2 = sorted(kw['permissions'])
+ if len(set(alist1) & set(alist2)) != len(alist2):
+ results.remove(a)
+ acis = list(results)
+
+ if kw.get('memberof'):
+ try:
+ dn = _group_from_memberof(kw['memberof'])
+ except errors.NotFound:
+ pass
+ else:
+ memberof_filter = '(memberOf=%s)' % dn
+ for a in acis:
+ if 'targetfilter' in a.target:
+ targetfilter = a.target['targetfilter']['expression']
+ if targetfilter != memberof_filter:
+ results.remove(a)
+ else:
+ results.remove(a)
+
+ if kw.get('type'):
+ for a in acis:
+ if 'target' in a.target:
+ target = a.target['target']['expression']
+ else:
+ results.remove(a)
+ continue
+ found = False
+ for k in _type_map.keys():
+ if _type_map[k] == target and kw['type'] == k:
+ found = True
+ break
+ if not found:
+ try:
+ results.remove(a)
+ except ValueError:
+ pass
+
+ if kw.get('selfaci', False) is True:
+ for a in acis:
+ if a.bindrule['expression'] != u'ldap:///self':
+ try:
+ results.remove(a)
+ except ValueError:
+ pass
+
+ if kw.get('group'):
+ for a in acis:
+ groupdn = a.bindrule['expression']
+ groupdn = DN(groupdn.replace('ldap:///',''))
+ try:
+ cn = groupdn[0]['cn']
+ except (IndexError, KeyError):
+ cn = None
+ if cn is None or cn != kw['group']:
+ try:
+ results.remove(a)
+ except ValueError:
+ pass
+
+ if kw.get('targetgroup'):
+ for a in acis:
+ found = False
+ if 'target' in a.target:
+ target = a.target['target']['expression']
+ targetdn = DN(target.replace('ldap:///',''))
+ group_container_dn = DN(api.env.container_group, api.env.basedn)
+ if targetdn.endswith(group_container_dn):
+ try:
+ cn = targetdn[0]['cn']
+ except (IndexError, KeyError):
+ cn = None
+ if cn == kw['targetgroup']:
+ found = True
+ if not found:
+ try:
+ results.remove(a)
+ except ValueError:
+ pass
+
+ if kw.get('filter'):
+ if not kw['filter'].startswith('('):
+ kw['filter'] = unicode('('+kw['filter']+')')
+ for a in acis:
+ if 'targetfilter' not in a.target or\
+ not a.target['targetfilter']['expression'] or\
+ a.target['targetfilter']['expression'] != kw['filter']:
+ results.remove(a)
+
+ if kw.get('subtree'):
+ for a in acis:
+ if 'target' in a.target:
+ target = a.target['target']['expression']
+ else:
+ results.remove(a)
+ continue
+ if kw['subtree'].lower() != target.lower():
+ try:
+ results.remove(a)
+ except ValueError:
+ pass
+
+ acis = []
+ for result in results:
+ if kw.get('raw', False):
+ aci = dict(aci=unicode(result))
+ else:
+ aci = _aci_to_kw(ldap, result,
+ pkey_only=kw.get('pkey_only', False))
+ acis.append(aci)
+
+ return dict(
+ result=acis,
+ count=len(acis),
+ truncated=False,
+ )
+
+
+@register()
+class aci_show(crud.Retrieve):
+ """
+ Display a single ACI given an ACI name.
+ """
+ NO_CLI = True
+
+ has_output_params = (
+ Str('aci',
+ label=_('ACI'),
+ ),
+ )
+
+ takes_options = (
+ _prefix_option,
+ DNParam('location?',
+ label=_('Location of the ACI'),
+ )
+ )
+
+ def execute(self, aciname, **kw):
+ """
+ Execute the aci-show operation.
+
+ Returns the entry
+
+ :param uid: The login name of the user to retrieve.
+ :param kw: unused
+ """
+ ldap = self.api.Backend.ldap2
+
+ dn = kw.get('location', self.api.env.basedn)
+ entry = ldap.get_entry(dn, ['aci'])
+
+ acis = _convert_strings_to_acis(entry.get('aci', []))
+
+ aci = _find_aci_by_name(acis, kw['aciprefix'], aciname)
+ if kw.get('raw', False):
+ result = dict(aci=unicode(aci))
+ else:
+ result = _aci_to_kw(ldap, aci)
+ return dict(
+ result=result,
+ value=pkey_to_value(aciname, kw),
+ )
+
+
+@register()
+class aci_rename(crud.Update):
+ """
+ Rename an ACI.
+ """
+ NO_CLI = True
+ has_output_params = (
+ Str('aci',
+ label=_('ACI'),
+ ),
+ )
+
+ takes_options = (
+ _prefix_option,
+ Str('newname',
+ doc=_('New ACI name'),
+ ),
+ )
+
+ msg_summary = _('Renamed ACI to "%(value)s"')
+
+ def execute(self, aciname, **kw):
+ ldap = self.api.Backend.ldap2
+
+ entry = ldap.get_entry(self.api.env.basedn, ['aci'])
+
+ acis = _convert_strings_to_acis(entry.get('aci', []))
+ aci = _find_aci_by_name(acis, kw['aciprefix'], aciname)
+
+ for a in acis:
+ prefix, name = _parse_aci_name(a.name)
+ if _make_aci_name(prefix, kw['newname']) == a.name:
+ raise errors.DuplicateEntry()
+
+ # The strategy here is to convert the ACI we're updating back into
+ # a series of keywords. Then we replace any keywords that have been
+ # updated and convert that back into an ACI and write it out.
+ newkw = _aci_to_kw(ldap, aci)
+ if 'selfaci' in newkw and newkw['selfaci'] == True:
+ # selfaci is set in aci_to_kw to True only if the target is self
+ kw['selfaci'] = True
+ if 'aciname' in newkw:
+ del newkw['aciname']
+
+ # _make_aci is what is run in aci_add and validates the input.
+ # Do this before we delete the existing ACI.
+ newaci = _make_aci(ldap, None, kw['newname'], newkw)
+
+ self.api.Command['aci_del'](aciname, aciprefix=kw['aciprefix'])
+
+ result = self.api.Command['aci_add'](kw['newname'], **newkw)['result']
+
+ if kw.get('raw', False):
+ result = dict(aci=unicode(newaci))
+ else:
+ result = _aci_to_kw(ldap, newaci)
+ return dict(
+ result=result,
+ value=pkey_to_value(kw['newname'], kw),
+ )