summaryrefslogtreecommitdiffstats
path: root/ipalib
diff options
context:
space:
mode:
authorPetr Viktorin <pviktori@redhat.com>2013-11-13 16:31:58 +0100
committerMartin Kosek <mkosek@redhat.com>2013-12-13 15:08:52 +0100
commitd7ee87cfa1e288fe18dc2dbeb2d691753048f4db (patch)
tree10f41a5a3a82011428f170fe725bafdce77845d7 /ipalib
parent445634d6ac39669cc007871861e19e15ae22c12d (diff)
downloadfreeipa.git-d7ee87cfa1e288fe18dc2dbeb2d691753048f4db.tar.gz
freeipa.git-d7ee87cfa1e288fe18dc2dbeb2d691753048f4db.tar.xz
freeipa.git-d7ee87cfa1e288fe18dc2dbeb2d691753048f4db.zip
Rewrite the Permission plugin
Ticket: https://fedorahosted.org/freeipa/ticket/3566 Design: http://www.freeipa.org/page/V3/Permissions_V2
Diffstat (limited to 'ipalib')
-rw-r--r--ipalib/capabilities.py6
-rw-r--r--ipalib/plugins/dns.py2
-rw-r--r--ipalib/plugins/permission.py1038
3 files changed, 709 insertions, 337 deletions
diff --git a/ipalib/capabilities.py b/ipalib/capabilities.py
index 6fcc436d..b60a570c 100644
--- a/ipalib/capabilities.py
+++ b/ipalib/capabilities.py
@@ -40,7 +40,11 @@ capabilities = dict(
# a user with UID=999. With the capability, these parameters are optional
# and 999 really means 999.
# https://fedorahosted.org/freeipa/ticket/2886
- optional_uid_params=u'2.54'
+ optional_uid_params=u'2.54',
+
+ # permissions2: Reworked permission system
+ # http://www.freeipa.org/page/V3/Permissions_V2
+ permissions2=u'2.69',
)
diff --git a/ipalib/plugins/dns.py b/ipalib/plugins/dns.py
index 07523dc7..19811d7f 100644
--- a/ipalib/plugins/dns.py
+++ b/ipalib/plugins/dns.py
@@ -2030,7 +2030,7 @@ class dnszone_add_permission(LDAPQuery):
permission_name = self.obj.permission_name(keys[-1])
permission = api.Command['permission_add_noaci'](permission_name,
- permissiontype=u'SYSTEM'
+ ipapermissiontype=u'SYSTEM'
)['result']
update = {}
diff --git a/ipalib/plugins/permission.py b/ipalib/plugins/permission.py
index 0f1fe667..ff5335d1 100644
--- a/ipalib/plugins/permission.py
+++ b/ipalib/plugins/permission.py
@@ -1,7 +1,7 @@
# Authors:
-# Rob Crittenden <rcritten@redhat.com>
+# Petr Viktorin <pviktori@redhat.com>
#
-# Copyright (C) 2010 Red Hat
+# Copyright (C) 2013 Red Hat
# see file 'COPYING' for use and warranty information
#
# This program is free software; you can redistribute it and/or modify
@@ -17,106 +17,130 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-from ipalib.plugins.baseldap import *
+import re
+
+from ipalib.plugins import baseldap
+from ipalib import errors
+from ipalib.parameters import Str, StrEnum, DNParam, Flag
from ipalib import api, _, ngettext
-from ipalib import Flag, Str, StrEnum
from ipalib.plugable import Registry
+from ipalib.capabilities import client_has_capability
+from ipalib.aci import ACI
+from ipapython.dn import DN
from ipalib.request import context
-from ipalib import errors
-from ipapython.dn import DN, EditableDN
__doc__ = _("""
Permissions
-
+""" + """
A permission enables fine-grained delegation of rights. A permission is
-a human-readable form of a 389-ds Access Control Rule, or instruction (ACI).
+a human-readable wrapper around a 389-ds Access Control Rule,
+or instruction (ACI).
A permission grants the right to perform a specific task such as adding a
user, modifying a group, etc.
-
+""" + """
A permission may not contain other permissions.
-
-* A permission grants access to read, write, add or delete.
+""" + """
+* A permission grants access to read, write, add, delete, read, search,
+ or compare.
* A privilege combines similar permissions (for example all the permissions
needed to add a user).
* A role grants a set of privileges to users, groups, hosts or hostgroups.
-
+""" + """
A permission is made up of a number of different parts:
1. The name of the permission.
2. The target of the permission.
3. The rights granted by the permission.
-
+""" + """
Rights define what operations are allowed, and may be one or more
of the following:
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
-
-Read permission is granted for most attributes by default so the read
-permission is not expected to be used very often.
-
+3. search - search on one or more attributes
+4. compare - compare one or more attributes
+5. add - add a new entry to the tree
+6. delete - delete an existing entry
+7. 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.
-
+""" + """
There are a number of allowed targets:
-1. type: a type of object (user, group, etc).
-2. memberof: a member of a group or hostgroup
-3. filter: an LDAP filter
-4. subtree: an LDAP filter specifying part of the LDAP DIT. This is a
- super-set of the "type" target.
-5. targetgroup: grant access to modify a specific group (such as granting
- the rights to manage group membership)
-
+1. subtree: a DN; the permission applies to the subtree under this DN
+2. target filter: an LDAP filter
+3. target: DN with possible wildcards, specifies entries permission applies to
+""" + """
+Additionally, there are the following convenience options.
+Setting one of these options will set the corresponding attribute(s).
+1. type: a type of object (user, group, etc); sets subtree and target filter.
+2. memberof: apply to members of a group; sets target filter
+3. targetgroup: grant access to modify a specific group (such as granting
+ the rights to manage group membership); sets target.
+""" + """
EXAMPLES:
-
+""" + """
Add a permission that grants the creation of users:
ipa permission-add --type=user --permissions=add "Add Users"
-
+""" + """
Add a permission that grants the ability to manage group membership:
ipa permission-add --attrs=member --permissions=write --type=group "Manage Group Members"
""")
-ACI_PREFIX=u"permission"
-
register = Registry()
+VALID_OBJECT_TYPES = (u'user', u'group', u'host', u'service', u'hostgroup',
+ u'netgroup', u'dnsrecord',)
+
+_DEPRECATED_OPTION_ALIASES = {
+ 'permissions': 'ipapermright',
+ 'attrs': 'ipapermallowedattr',
+ 'filter': 'ipapermtargetfilter',
+ 'subtree': 'ipapermlocation',
+}
+
+KNOWN_FLAGS = {'SYSTEM', 'V2'}
+
output_params = (
- Str('ipapermissiontype',
- label=_('Permission Type'),
- ),
Str('aci',
label=_('ACI'),
),
)
-def filter_options(options, keys):
- """Return a dict that includes entries from `options` that are in `keys`
- example:
- >>> filtered = filter_options({'a': 1, 'b': 2, 'c': 3}, ['a', 'c'])
- >>> filtered == {'a': 1, 'c': 3}
- True
+def strip_ldap_prefix(uri):
+ prefix = 'ldap:///'
+ if not uri.startswith(prefix):
+ raise ValueError('%r does not start with %r' % (uri, prefix))
+ return uri[len(prefix):]
+
+
+class DNOrURL(DNParam):
+ """DN parameter that allows, and strips, a "ldap:///" prefix on input
+
+ Used for ``subtree`` to maintain backward compatibility.
"""
- return dict((k, options[k]) for k in keys if k in options)
+
+ def _convert_scalar(self, value, index=None):
+ if isinstance(value, basestring) and value.startswith('ldap:///'):
+ value = strip_ldap_prefix(value)
+ return super(DNOrURL, self)._convert_scalar(value, index=index)
@register()
-class permission(LDAPObject):
+class permission(baseldap.LDAPObject):
"""
Permission object.
"""
container_dn = api.env.container_permission
object_name = _('permission')
object_name_plural = _('permissions')
- object_class = ['groupofnames', 'ipapermission']
+ object_class = ['groupofnames', 'ipapermission', 'ipapermissionv2']
default_attributes = ['cn', 'member', 'memberof',
- 'memberindirect', 'ipapermissiontype',
- ]
- aci_attributes = ['aci', 'group', 'permissions', 'attrs', 'type',
- 'filter', 'subtree', 'targetgroup', 'memberof',
+ 'memberindirect', 'ipapermissiontype', 'objectclass',
+ 'ipapermdefaultattr', 'ipapermallowedattr', 'ipapermexcludedattr',
+ 'ipapermbindruletype', 'ipapermlocation', 'ipapermright',
+ 'ipapermtargetfilter', 'ipapermtarget'
]
attribute_members = {
'member': ['privilege'],
@@ -132,396 +156,740 @@ class permission(LDAPObject):
cli_name='name',
label=_('Permission name'),
primary_key=True,
- pattern='^[-_ a-zA-Z0-9]+$',
- pattern_errmsg="May only contain letters, numbers, -, _, and space",
+ pattern='^[-_ a-zA-Z0-9.]+$',
+ pattern_errmsg="May only contain letters, numbers, -, _, ., and space",
),
- Str('permissions+',
+ StrEnum(
+ 'ipapermright*',
cli_name='permissions',
label=_('Permissions'),
- doc=_('Permissions to grant ' \
- '(read, write, add, delete, all)'),
- csv=True,
+ doc=_('Rights to grant '
+ '(read, search, compare, write, add, delete, all)'),
+ values=(u'read', u'search', u'compare',
+ u'write', u'add', u'delete', u'all'),
),
- Str('attrs*',
+ Str('ipapermallowedattr*',
cli_name='attrs',
label=_('Attributes'),
doc=_('Attributes to which the permission applies'),
- csv=True,
- normalizer=lambda value: value.lower(),
- flags=('ask_create'),
),
- StrEnum('type?',
- cli_name='type',
- label=_('Type'),
- doc=_('Type of IPA object (user, group, host, hostgroup, service, netgroup, dns)'),
- values=(u'user', u'group', u'host', u'service', u'hostgroup', u'netgroup', u'dnsrecord',),
- flags=('ask_create'),
- ),
- Str('memberof?',
- cli_name='memberof',
- label=_('Member of group'), # FIXME: Does this label make sense?
- doc=_('Target members of a group'),
- flags=('ask_create'),
+ StrEnum(
+ 'ipapermbindruletype',
+ cli_name='bindtype',
+ label=_('Bind rule type'),
+ doc=_('Bind rule type'),
+ autofill=True,
+ values=(u'permission',),
+ default=u'permission',
),
- Str('filter?',
- cli_name='filter',
- label=_('Filter'),
- doc=_('Legal LDAP filter (e.g. ou=Engineering)'),
- flags=('ask_create'),
- ),
- Str('subtree?',
+ DNOrURL(
+ 'ipapermlocation?',
cli_name='subtree',
label=_('Subtree'),
doc=_('Subtree to apply permissions to'),
- flags=('ask_create'),
+ default=api.env.basedn,
+ flags={'ask_create'},
+ ),
+ Str(
+ 'ipapermtargetfilter?',
+ cli_name='filter',
+ label=_('ACI target filter'),
+ doc=_('ACI target filter'),
+ ),
+
+ DNParam(
+ 'ipapermtarget?',
+ cli_name='target',
+ label=_('ACI target DN'),
+ flags={'no_option'}
+ ),
+
+ Str('memberof?',
+ label=_('Member of group'), # FIXME: Does this label make sense?
+ doc=_('Target members of a group (sets targetfilter)'),
+ flags={'ask_create', 'virtual_attribute'},
),
Str('targetgroup?',
- cli_name='targetgroup',
label=_('Target group'),
- doc=_('User group to apply permissions to'),
- flags=('ask_create'),
+ doc=_('User group to apply permissions to (sets target)'),
+ flags={'ask_create', 'virtual_attribute'},
+ ),
+ StrEnum(
+ 'type?',
+ label=_('Type'),
+ doc=_('Type of IPA object (sets subtree and filter)'),
+ values=VALID_OBJECT_TYPES,
+ flags={'ask_create', 'virtual_attribute'},
),
+ ) + tuple(
+ Str(old_name + '*',
+ doc=_('Deprecated; use %s' % new_name),
+ flags={'no_option', 'virtual_attribute'})
+ for old_name, new_name in _DEPRECATED_OPTION_ALIASES.items()
)
- # Don't allow SYSTEM permissions to be modified or removed
- def check_system(self, ldap, dn, *keys):
+ def reject_system(self, entry):
+ """Raise if permission entry has unknown flags, or is a SYSTEM perm"""
+ flags = entry.get('ipapermissiontype', [])
+ for flag in flags:
+ if flag not in KNOWN_FLAGS:
+ raise errors.ACIError(
+ info=_('Permission with unknown flag %s may not be '
+ 'modified or removed') % flag)
+ if list(flags) == [u'SYSTEM']:
+ raise errors.ACIError(
+ info=_('A SYSTEM permission may not be modified or removed'))
+
+ def postprocess_result(self, entry, options):
+ """Update a permission entry for output (in place)
+
+ :param entry: The entry to update
+ :param options:
+ Command options. Contains keys such as ``raw``, ``all``,
+ ``pkey_only``, ``version``.
+ """
+ if not options.get('raw') and not options.get('pkey_only'):
+ ipapermtargetfilter = entry.single_value.get('ipapermtargetfilter',
+ '')
+ ipapermtarget = entry.single_value.get('ipapermtarget')
+ ipapermlocation = entry.single_value.get('ipapermlocation')
+
+ # memberof
+ match = re.match('^\(memberof=(.*)\)$', ipapermtargetfilter, re.I)
+ if match:
+ dn = DN(match.group(1))
+ if dn[1:] == DN(self.api.Object.group.container_dn,
+ self.api.env.basedn)[:] and dn[0].attr == 'cn':
+ entry.single_value['memberof'] = dn[0].value
+
+ # targetgroup
+ if ipapermtarget:
+ dn = DN(ipapermtarget)
+ if (dn[1:] == DN(self.api.Object.group.container_dn,
+ self.api.env.basedn)[:] and
+ dn[0].attr == 'cn' and dn[0].value != '*'):
+ entry.single_value['targetgroup'] = dn[0].value
+
+ # type
+ if ipapermtarget and ipapermlocation:
+ for objname in VALID_OBJECT_TYPES:
+ obj = self.api.Object[objname]
+ wantdn = DN(obj.container_dn, self.api.env.basedn)
+ if DN(ipapermlocation) == wantdn:
+ targetdn = DN(
+ (obj.rdn_attribute or obj.primary_key.name, '*'),
+ obj.container_dn,
+ self.api.env.basedn)
+ if ipapermtarget == targetdn:
+ entry.single_value['type'] = objname
+ break
+
+ # old output names
+ if not client_has_capability(options['version'], 'permissions2'):
+ for old_name, new_name in _DEPRECATED_OPTION_ALIASES.items():
+ if new_name in entry:
+ entry[old_name] = entry[new_name]
+ del entry[new_name]
+
+ rights = entry.get('attributelevelrights')
+ if rights:
+ rights['memberof'] = rights['ipapermtargetfilter']
+ rights['targetgroup'] = rights['ipapermtarget']
+
+ type_rights = set(rights['ipapermtarget'])
+ type_rights.intersection_update(rights['ipapermlocation'])
+ rights['type'] = ''.join(sorted(type_rights,
+ key=rights['ipapermtarget'].index))
+
+ if not client_has_capability(options['version'], 'permissions2'):
+ for old_name, new_name in _DEPRECATED_OPTION_ALIASES.items():
+ if new_name in entry:
+ rights[old_name] = rights[new_name]
+ del rights[new_name]
+
+ if options.get('raw'):
+ # Retreive the ACI from LDAP to ensure we get the real thing
+ acientry, acistring = self._get_aci_entry_and_string(entry)
+ entry.single_value['aci'] = acistring
+
+ if not client_has_capability(options['version'], 'permissions2'):
+ # Legacy clients expect some attributes as a single value
+ for attr in 'type', 'targetgroup', 'memberof', 'aci':
+ if attr in entry:
+ entry[attr] = entry.single_value[attr]
+ if 'subtree' in entry:
+ # Legacy clients expect subtree as a URL
+ dn = entry.single_value['subtree']
+ entry['subtree'] = u'ldap:///%s' % dn
+ if 'filter' in entry:
+ # Legacy clients expect filter without parentheses
+ new_filter = []
+ for flt in entry['filter']:
+ assert flt[0] == '(' and flt[-1] == ')'
+ new_filter.append(flt[1:-1])
+ entry['filter'] = new_filter
+
+ def make_aci(self, entry):
+ """Make an ACI string from the given permission entry"""
+
+ aci = ACI()
+ name = entry.single_value['cn']
+ aci.name = 'permission:%s' % name
+ ipapermtarget = entry.single_value.get('ipapermtarget')
+ if ipapermtarget:
+ aci.set_target('ldap:///%s' % ipapermtarget)
+ ipapermtargetfilter = entry.single_value.get('ipapermtargetfilter')
+ if ipapermtargetfilter:
+ aci.set_target_filter(ipapermtargetfilter)
+
+ ipapermbindruletype = entry.single_value.get('ipapermbindruletype',
+ 'permission')
+ if ipapermbindruletype == 'permission':
+ dn = DN(('cn', name), self.container_dn, self.api.env.basedn)
+ aci.set_bindrule('groupdn = "ldap:///%s"' % dn)
+ elif ipapermbindruletype == 'all':
+ aci.set_bindrule('userdn = "ldap:///all"')
+ elif ipapermbindruletype == 'anonymous':
+ aci.set_bindrule('userdn = "ldap:///anyone"')
+ else:
+ raise ValueError(ipapermbindruletype)
+ aci.permissions = entry['ipapermright']
+ aci.set_target_attr(entry.get('ipapermallowedattr', []))
+
+ return aci.export_to_string()
+
+ def add_aci(self, permission_entry):
+ """Add the ACI coresponding to the given permission entry"""
+ ldap = self.api.Backend.ldap2
+ acistring = self.make_aci(permission_entry)
+ location = permission_entry.single_value.get('ipapermlocation',
+ self.api.env.basedn)
+
+ self.log.debug('Adding ACI %r to %s' % (acistring, location))
+ entry = ldap.get_entry(location, ['aci'])
+ entry.setdefault('aci', []).append(acistring)
+ ldap.update_entry(entry)
+
+ def remove_aci(self, permission_entry):
+ """Remove the ACI corresponding to the given permission entry"""
+ self._replace_aci(permission_entry)
+
+ def update_aci(self, permission_entry, old_name=None):
+ """Update the ACI corresponding to the given permission entry"""
+ new_acistring = self.make_aci(permission_entry)
+ self._replace_aci(permission_entry, old_name, new_acistring)
+
+ def _replace_aci(self, permission_entry, old_name=None, new_acistring=None):
+ """Replace ACI corresponding to permission_entry
+
+ :param old_name: the old name of the permission, if different from new
+ :param new_acistring: new ACI string; if None the ACI is just deleted
+ """
+ ldap = self.api.Backend.ldap2
+ acientry, acistring = self._get_aci_entry_and_string(
+ permission_entry, old_name, notfound_ok=True)
+
+ # (pylint thinks `acientry` is just a dict, but it's an LDAPEntry)
+ acidn = acientry.dn # pylint: disable=E1103
+
+ if acistring is not None:
+ self.log.debug('Removing ACI %r from %s' % (acistring, acidn))
+ acientry['aci'].remove(acistring)
+ if new_acistring:
+ self.log.debug('Adding ACI %r to %s' % (new_acistring, acidn))
+ acientry['aci'].append(new_acistring)
try:
- (dn, entry_attrs) = ldap.get_entry(dn, ['ipapermissiontype'])
+ ldap.update_entry(acientry)
+ except errors.EmptyModlist:
+ self.log.info('No changes to ACI')
+
+ def _get_aci_entry_and_string(self, permission_entry, name=None,
+ notfound_ok=False):
+ """Get the entry and ACI corresponding to the permission entry
+
+ :param name: The name of the permission, or None for the cn
+ :param notfound_ok:
+ If true, (acientry, None) will be returned on missing ACI, rather
+ than raising exception
+ """
+ ldap = self.api.Backend.ldap2
+ if name is None:
+ name = permission_entry.single_value['cn']
+ location = permission_entry.single_value.get('ipapermlocation',
+ self.api.env.basedn)
+ wanted_aciname = 'permission:%s' % name
+
+ try:
+ acientry = ldap.get_entry(location, ['aci'])
except errors.NotFound:
- self.handle_not_found(*keys)
- if 'ipapermissiontype' in entry_attrs:
- if 'SYSTEM' in entry_attrs['ipapermissiontype']:
- return False
- return True
+ acientry = {}
+ acis = acientry.get('aci', ())
+ for acistring in acis:
+ aci = ACI(acistring)
+ if aci.name == wanted_aciname:
+ return acientry, acistring
+ else:
+ if notfound_ok:
+ return acientry, None
+ raise errors.NotFound(
+ reason=_('The ACI for permission %(name)s was not found '
+ 'in %(dn)s ') % {'name': name, 'dn': location})
+
+ def upgrade_permission(self, entry, target_entry=None,
+ output_only=False):
+ """Upgrade the given permission entry to V2, in-place
+
+ The entry is only upgraded if it is a plain old-style permission,
+ that is, it has no flags set.
+
+ :param target_entry:
+ If given, ``target_entry`` is filled from information taken
+ from the ACI corresponding to ``entry``.
+ If None, ``entry`` itself is filled
+ :param output_only:
+ If true, the flags are not updated to V2.
+ Used for the -find and -show commands.
+ """
+ if entry.get('ipapermissiontype'):
+ # Only convert old-style, non-SYSTEM permissions -- i.e. no flags
+ return
+ base, acistring = self._get_aci_entry_and_string(entry)
+
+ if not target_entry:
+ target_entry = entry
+
+ # The DN of old permissions is always basedn
+ # (pylint thinks `base` is just a dict, but it's an LDAPEntry)
+ assert base.dn == self.api.env.basedn, base # pylint: disable=E1103
+
+ aci = ACI(acistring)
+
+ if 'targetattr' in aci.target:
+ target_entry['ipapermallowedattr'] = (
+ aci.target['targetattr']['expression'])
+ if 'target' in aci.target:
+ target_entry.single_value['ipapermtarget'] = DN(strip_ldap_prefix(
+ aci.target['target']['expression']))
+ if 'targetfilter' in aci.target:
+ target_entry.single_value['ipapermtargetfilter'] = unicode(
+ aci.target['targetfilter']['expression'])
+ if aci.bindrule['expression'] == 'ldap:///all':
+ target_entry.single_value['ipapermbindruletype'] = u'all'
+ elif aci.bindrule['expression'] == 'ldap:///anyone':
+ target_entry.single_value['ipapermbindruletype'] = u'anonymous'
+ else:
+ target_entry.single_value['ipapermbindruletype'] = u'permission'
+ target_entry['ipapermright'] = aci.permissions
+ if 'targetattr' in aci.target:
+ target_entry['ipapermallowedattr'] = [
+ unicode(a) for a in aci.target['targetattr']['expression']]
- def filter_aci_attributes(self, options):
- """Return option dictionary that only includes ACI attributes"""
- return dict((k, v) for k, v in options.items() if
- k in self.aci_attributes)
+ if not output_only:
+ target_entry['ipapermissiontype'] = ['SYSTEM', 'V2']
+ if 'ipapermissionv2' not in entry['objectclass']:
+ target_entry['objectclass'] = list(entry['objectclass']) + [
+ u'ipapermissionv2']
-@register()
-class permission_add(LDAPCreate):
- __doc__ = _('Add a new permission.')
+ target_entry['ipapermlocation'] = [self.api.env.basedn]
- msg_summary = _('Added permission "%(value)s"')
- has_output_params = LDAPCreate.has_output_params + output_params
-
- def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
- assert isinstance(dn, DN)
- # Test the ACI before going any further
- opts = self.obj.filter_aci_attributes(options)
- opts['test'] = True
- opts['permission'] = keys[-1]
- opts['aciprefix'] = ACI_PREFIX
- self.api.Command.aci_add(keys[-1], **opts)
-
- # Clear the aci attributes out of the permission entry
- for o in options:
- try:
- if o not in ['objectclass']:
- del entry_attrs[o]
- except:
- pass
- return dn
+ # Make sure we're not losing *any info* by the upgrade
+ new_acistring = self.make_aci(target_entry)
+ if not ACI(new_acistring).isequal(aci):
+ raise ValueError('Cannot convert ACI, %r != %r' % (new_acistring,
+ acistring))
- def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
- assert isinstance(dn, DN)
- # Now actually add the aci.
- opts = self.obj.filter_aci_attributes(options)
- opts['test'] = False
- opts['permission'] = keys[-1]
- opts['aciprefix'] = ACI_PREFIX
- try:
- result = self.api.Command.aci_add(keys[-1], **opts)['result']
- for attr in self.obj.aci_attributes:
- if attr in result:
- entry_attrs[attr] = result[attr]
- except errors.InvalidSyntax, e:
- # A syntax error slipped past our attempt at validation, clean up
- self.api.Command.permission_del(keys[-1])
- raise e
- except Exception, e:
- # Something bad happened, clean up as much as we can and return
- # that error
+ def preprocess_options(self, options):
+ """Preprocess options (in-place)"""
+
+ if options.get('subtree'):
+ if isinstance(options['subtree'], (list, tuple)):
+ [options['subtree']] = options['subtree']
try:
- self.api.Command.permission_del(keys[-1])
- except Exception, ignore:
- pass
+ options['subtree'] = strip_ldap_prefix(options['subtree'])
+ except ValueError:
+ raise errors.ValidationError(
+ name='subtree',
+ error='does not start with "ldap:///"')
+
+ # Handle old options
+ for old_name, new_name in _DEPRECATED_OPTION_ALIASES.items():
+ if old_name in options:
+ if client_has_capability(options['version'], 'permissions2'):
+ raise errors.ValidationError(
+ name=old_name,
+ error=_('option was renamed; use %s') % new_name)
+ if new_name in options:
+ raise errors.ValidationError(
+ name=old_name,
+ error=(_('Cannot use %(old_name)s with %(new_name)s') %
+ {'old_name': old_name, 'new_name': new_name}))
+ options[new_name] = options[old_name]
+ del options[old_name]
+
+ # memberof
+ if 'memberof' in options:
+ memberof = options.pop('memberof')
+ if memberof:
+ if 'ipapermtargetfilter' in options:
+ raise errors.ValidationError(
+ name='ipapermtargetfilter',
+ error=_('filter and memberof are mutually exclusive'))
+ try:
+ groupdn = self.api.Object.group.get_dn_if_exists(memberof)
+ except errors.NotFound:
+ raise errors.NotFound(
+ reason=_('%s: group not found') % memberof)
+ options['ipapermtargetfilter'] = u'(memberOf=%s)' % groupdn
+ else:
+ if 'ipapermtargetfilter' not in options:
+ options['ipapermtargetfilter'] = None
+
+ # targetgroup
+ if 'targetgroup' in options:
+ targetgroup = options.pop('targetgroup')
+ if targetgroup:
+ if 'ipapermtarget' in options:
+ raise errors.ValidationError(
+ name='ipapermtarget',
+ error=_('target and targetgroup are mutually exclusive'))
+ try:
+ groupdn = self.api.Object.group.get_dn_if_exists(targetgroup)
+ except errors.NotFound:
+ raise errors.NotFound(
+ reason=_('%s: group not found') % targetgroup)
+ options['ipapermtarget'] = groupdn
+ else:
+ if 'ipapermtarget' not in options:
+ options['ipapermtarget'] = None
+
+ # type
+ if 'type' in options:
+ objtype = options.pop('type')
+ if objtype:
+ if 'ipapermlocation' in options:
+ raise errors.ValidationError(
+ name='ipapermlocation',
+ error=_('subtree and type are mutually exclusive'))
+ if 'ipapermtarget' in options:
+ raise errors.ValidationError(
+ name='ipapermtarget',
+ error=_('target and type are mutually exclusive'))
+ obj = self.api.Object[objtype.lower()]
+ container_dn = DN(obj.container_dn, self.api.env.basedn)
+ options['ipapermtarget'] = DN(
+ (obj.rdn_attribute or obj.primary_key.name, '*'),
+ container_dn)
+ options['ipapermlocation'] = container_dn
+ else:
+ if 'ipapermtarget' not in options:
+ options['ipapermtarget'] = None
+ if 'ipapermlocation' not in options:
+ options['ipapermlocation'] = None
+
+ def validate_permission(self, entry):
+ ldap = self.Backend.ldap2
+
+ # Rough filter validation by a search
+ if 'ipapermtargetfilter' in entry:
try:
- self.api.Command.aci_del(keys[-1], aciprefix=ACI_PREFIX)
- except Exception, ignore:
+ ldap.find_entries(
+ filter=entry.single_value['ipapermtargetfilter'],
+ base_dn=self.env.basedn,
+ scope=ldap.SCOPE_BASE,
+ size_limit=1)
+ except errors.NotFound:
pass
- raise e
- return dn
+ except errors.BadSearchFilter:
+ raise errors.ValidationError(
+ name='ipapermtargetfilter',
+ error=_('Bad search filter'))
+
+ # Ensure there's something in the ACI's filter
+ needed_attrs = (
+ 'ipapermtarget', 'ipapermtargetfilter', 'ipapermallowedattr')
+ if not any(entry.single_value.get(a) for a in needed_attrs):
+ raise errors.ValidationError(
+ name='target',
+ error=_('there must be at least one target entry specifier '
+ '(e.g. target, targetfilter, attrs)'))
+
+ # Ensure there's a right
+ if not entry.get('ipapermright'):
+ raise errors.RequirementError(name='ipapermright')
@register()
-class permission_add_noaci(LDAPCreate):
- __doc__ = _('Add a system permission without an ACI')
+class permission_add_noaci(baseldap.LDAPCreate):
+ __doc__ = _('Add a system permission without an ACI (internal command)')
msg_summary = _('Added permission "%(value)s"')
- has_output_params = LDAPCreate.has_output_params + output_params
NO_CLI = True
+ has_output_params = baseldap.LDAPCreate.has_output_params + output_params
takes_options = (
- StrEnum('permissiontype?',
- label=_('Permission type'),
- values=(u'SYSTEM',),
+ Str('ipapermissiontype+',
+ label=_('Permission flags'),
),
)
- def get_args(self):
- # do not validate system permission names
- yield self.obj.primary_key.clone(pattern=None, pattern_errmsg=None)
-
def get_options(self):
+ perm_options = set(o.name for o in self.obj.takes_params)
for option in super(permission_add_noaci, self).get_options():
- # filter out ACI options
- if option.name in self.obj.aci_attributes:
- continue
- yield option
-
- def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
- assert isinstance(dn, DN)
- permission_type = options.get('permissiontype')
- if permission_type:
- entry_attrs['ipapermissiontype'] = [ permission_type ]
+ # From new options, only cn & ipapermissiontype are supported
+ if option.name in ['ipapermissiontype']:
+ yield option.clone()
+ # Other options such as raw, version are supported
+ elif option.name not in perm_options:
+ yield option.clone()
+
+ def pre_callback(self, ldap, dn, entry, attrs_list, *keys, **options):
+ entry['ipapermissiontype'] = list(options['ipapermissiontype'])
+ entry['objectclass'] = [oc for oc in entry['objectclass']
+ if oc.lower() != 'ipapermissionv2']
return dn
@register()
-class permission_del(LDAPDelete):
+class permission_add(baseldap.LDAPCreate):
+ __doc__ = _('Add a new permission.')
+
+ msg_summary = _('Added permission "%(value)s"')
+ has_output_params = baseldap.LDAPCreate.has_output_params + output_params
+
+ # Need to override args_options_2_params so that processed options apply to
+ # the whole command, not just the callbacks
+ def args_options_2_params(self, *args, **options):
+ if self.env.in_server:
+ self.obj.preprocess_options(options)
+
+ return super(permission_add, self).args_options_2_params(
+ *args, **options)
+
+ def pre_callback(self, ldap, dn, entry, attrs_list, *keys, **options):
+ entry['ipapermissiontype'] = ['SYSTEM', 'V2']
+ entry['cn'] = list(keys)
+ if not entry.get('ipapermlocation'):
+ entry.setdefault('ipapermlocation', [api.env.basedn])
+
+ self.obj.validate_permission(entry)
+ return dn
+
+ def post_callback(self, ldap, dn, entry, *keys, **options):
+ self.obj.add_aci(entry)
+ self.obj.postprocess_result(entry, options)
+ return dn
+
+
+@register()
+class permission_del(baseldap.LDAPDelete):
__doc__ = _('Delete a permission.')
msg_summary = _('Deleted permission "%(value)s"')
- takes_options = LDAPDelete.takes_options + (
+ takes_options = baseldap.LDAPDelete.takes_options + (
Flag('force',
label=_('Force'),
- flags=['no_option', 'no_output'],
+ flags={'no_option', 'no_output'},
doc=_('force delete of SYSTEM permissions'),
),
)
def pre_callback(self, ldap, dn, *keys, **options):
- assert isinstance(dn, DN)
- if not options.get('force') and not self.obj.check_system(ldap, dn, *keys):
- raise errors.ACIError(
- info=_('A SYSTEM permission may not be removed'))
- # remove permission even when the underlying ACI is missing
try:
- self.api.Command.aci_del(keys[-1], aciprefix=ACI_PREFIX)
+ entry = ldap.get_entry(dn, attrs_list=self.obj.default_attributes)
except errors.NotFound:
- pass
+ self.obj.handle_not_found(*keys)
+
+ if not options.get('force'):
+ self.obj.reject_system(entry)
+
+ try:
+ self.obj.remove_aci(entry)
+ except errors.NotFound:
+ errors.NotFound('ACI of permission %s was not found' % keys[0])
+
return dn
@register()
-class permission_mod(LDAPUpdate):
+class permission_mod(baseldap.LDAPUpdate):
__doc__ = _('Modify a permission.')
msg_summary = _('Modified permission "%(value)s"')
- has_output_params = LDAPUpdate.has_output_params + output_params
+ has_output_params = baseldap.LDAPUpdate.has_output_params + output_params
- def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
- assert isinstance(dn, DN)
- if not self.obj.check_system(ldap, dn, *keys):
- raise errors.ACIError(
- info=_('A SYSTEM permission may not be modified'))
+ def args_options_2_params(self, *args, **options):
+ if self.env.in_server:
+ self.obj.preprocess_options(options)
+
+ return super(permission_mod, self).args_options_2_params(
+ *args, **options)
+
+ def pre_callback(self, ldap, dn, entry, attrs_list, *keys, **options):
+ if 'rename' in options and not options['rename']:
+ raise errors.ValidationError(name='rename',
+ error='New name can not be empty')
- # check if permission is in LDAP
try:
- (dn, attrs) = ldap.get_entry(dn, attrs_list)
+ attrs_list = self.obj.default_attributes
+ old_entry = ldap.get_entry(dn, attrs_list=attrs_list)
except errors.NotFound:
self.obj.handle_not_found(*keys)
- # when renaming permission, check if the target permission does not
- # exists already. Then, make changes to underlying ACI
- if 'rename' in options:
- if options['rename']:
- try:
- try:
- new_dn = EditableDN(dn)
- new_dn[0]['cn'] # assure the first RDN has cn as it's type
- except (IndexError, KeyError), e:
- raise ValueError("expected dn starting with 'cn=' but got '%s'" % dn)
- new_dn[0].value = options['rename']
- entry = ldap.get_entry(new_dn, attrs_list)
- raise errors.DuplicateEntry()
- except errors.NotFound:
- pass # permission may be renamed, continue
- else:
- raise errors.ValidationError(
- name='rename', error=_('New name can not be empty'))
-
- opts = self.obj.filter_aci_attributes(options)
- setattr(context, 'aciupdate', False)
- # If there are no options left we don't need to do anything to the
- # underlying ACI.
- if len(opts) > 0:
- opts['permission'] = keys[-1]
- opts['aciprefix'] = ACI_PREFIX
- self.api.Command.aci_mod(keys[-1], **opts)
- setattr(context, 'aciupdate', True)
-
- # Clear the aci attributes out of the permission entry
- for o in self.obj.aci_attributes:
- try:
- del entry_attrs[o]
- except:
- pass
+ self.obj.reject_system(old_entry)
+ self.obj.upgrade_permission(old_entry)
- return dn
+ # Since `entry` only contains the attributes we are currently changing,
+ # it cannot be used directly to generate an ACI.
+ # First we need to copy the original data into it.
+ for key, value in old_entry.iteritems():
+ if key not in options and key != 'cn':
+ entry.setdefault(key, value)
- def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs):
- if call_func.func_name == 'update_entry':
- if isinstance(exc, errors.EmptyModlist):
- aciupdate = getattr(context, 'aciupdate')
- if aciupdate:
- return
- raise exc
+ if not entry.get('ipapermlocation'):
+ entry['ipapermlocation'] = [self.api.env.basedn]
- def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
- assert isinstance(dn, DN)
- # rename the underlying ACI after the change to permission
- cn = keys[-1]
+ self.obj.validate_permission(entry)
- if 'rename' in options:
- self.api.Command.aci_mod(cn,aciprefix=ACI_PREFIX,
- permission=options['rename'])
+ old_location = old_entry.single_value.get('ipapermlocation',
+ self.api.env.basedn)
+ if old_location == options.get('ipapermlocation', old_location):
+ context.permision_moving_aci = False
+ else:
+ context.permision_moving_aci = True
+ try:
+ self.obj.remove_aci(old_entry)
+ except errors.NotFound, e:
+ self.log.error('permission ACI not found: %s' % e)
- self.api.Command.aci_rename(cn, aciprefix=ACI_PREFIX,
- newname=options['rename'])
+ # To pass data to postcallback, we currently need to use the context
+ context.old_entry = old_entry
- cn = options['rename'] # rename finished
+ return dn
- # all common options to permission-mod and show need to be listed here
- common_options = filter_options(options, ['all', 'raw', 'rights'])
- result = self.api.Command.permission_show(cn, **common_options)['result']
+ def post_callback(self, ldap, dn, entry, *keys, **options):
+ old_entry = context.old_entry
- for r in result:
- if not r.startswith('member_'):
- entry_attrs[r] = result[r]
+ if context.permision_moving_aci:
+ self.obj.add_aci(entry)
+ else:
+ self.obj.update_aci(entry, old_entry.single_value['cn'])
+ self.obj.postprocess_result(entry, options)
+ entry['dn'] = entry.dn
return dn
@register()
-class permission_find(LDAPSearch):
+class permission_find(baseldap.LDAPSearch):
__doc__ = _('Search for permissions.')
msg_summary = ngettext(
- '%(count)d permission matched', '%(count)d permissions matched', 0
- )
- has_output_params = LDAPSearch.has_output_params + output_params
+ '%(count)d permission matched', '%(count)d permissions matched', 0)
+ has_output_params = baseldap.LDAPSearch.has_output_params + output_params
- def post_callback(self, ldap, entries, truncated, *args, **options):
+ def args_options_2_params(self, *args, **options):
+ if self.env.in_server:
+ self.obj.preprocess_options(options)
- # There is an option/param overlap: "cn" must be passed as "aciname"
- # to aci-find. Besides that we don't need cn anymore so pop it
- aciname = options.pop('cn', None)
+ return super(permission_find, self).args_options_2_params(
+ *args, **options)
+
+ def post_callback(self, ldap, entries, truncated, *args, **options):
+ attribute_options = [o for o in options
+ if (o in self.options and
+ self.options[o].attribute)]
- pkey_only = options.pop('pkey_only', False)
- if not pkey_only:
+ if not options.get('pkey_only'):
for entry in entries:
- (dn, attrs) = entry
- try:
- common_options = filter_options(options, ['all', 'raw'])
- aci = self.api.Command.aci_show(attrs['cn'][0],
- aciprefix=ACI_PREFIX, **common_options)['result']
-
- # copy information from respective ACI to permission entry
- for attr in self.obj.aci_attributes:
- if attr in aci:
- attrs[attr] = aci[attr]
- except errors.NotFound:
- self.debug('ACI not found for %s' % attrs['cn'][0])
- if truncated:
- # size/time limit met, no need to search acis
- return truncated
+ # Old-style permissions might have matched (e.g. by name)
+ self.obj.upgrade_permission(entry, output_only=True)
- if 'sizelimit' in options:
- max_entries = options['sizelimit']
- else:
- config = ldap.get_ipa_config()[1]
- max_entries = config['ipasearchrecordslimit']
-
- # Now find all the ACIs that match. Once we find them, add any that
- # aren't already in the list along with their permission info.
-
- opts = self.obj.filter_aci_attributes(options)
- if aciname:
- opts['aciname'] = aciname
- opts['aciprefix'] = ACI_PREFIX
- # permission ACI attribute is needed
- aciresults = self.api.Command.aci_find(*args, **opts)
- truncated = truncated or aciresults['truncated']
- results = aciresults['result']
-
- for aci in results:
- found = False
- if 'permission' in aci:
- for entry in entries:
- (dn, attrs) = entry
- if aci['permission'] == attrs['cn'][0]:
- found = True
- break
- if not found:
- common_options = filter_options(options, ['all', 'raw'])
- permission = self.api.Command.permission_show(
- aci['permission'], **common_options)['result']
- dn = permission['dn']
- del permission['dn']
- if pkey_only:
- pk = self.obj.primary_key.name
- new_entry = ldap.make_entry(dn, {pk: permission[pk]})
+ if not truncated:
+ if 'sizelimit' in options:
+ max_entries = options['sizelimit']
+ else:
+ config = ldap.get_ipa_config()[1]
+ max_entries = int(config.single_value['ipasearchrecordslimit'])
+
+ filters = ['(objectclass=ipaPermission)',
+ '(!(ipaPermissionType=V2))']
+ if args:
+ filters.append(ldap.make_filter_from_attr('cn', args[0],
+ exact=False))
+ attrs_list = list(self.obj.default_attributes)
+ attrs_list += list(self.obj.attribute_members)
+ if options.get('all'):
+ attrs_list.append('*')
+ try:
+ legacy_entries = ldap.get_entries(
+ base_dn=DN(self.obj.container_dn, self.api.env.basedn),
+ filter=ldap.combine_filters(filters, rules=ldap.MATCH_ALL),
+ attrs_list=attrs_list)
+ except errors.NotFound:
+ legacy_entries = ()
+ self.log.debug('potential legacy entries: %s', len(legacy_entries))
+ nonlegacy_names = {e.single_value['cn'] for e in entries}
+ for entry in legacy_entries:
+ if entry.single_value['cn'] in nonlegacy_names:
+ continue
+ if len(entries) > max_entries:
+ # We've over the limit, pop the last entry and set
+ # truncated flag
+ # (this is easier to do than checking before adding
+ # the entry to results)
+ entries.pop()
+ truncated = True
+ break
+ self.obj.upgrade_permission(entry, output_only=True)
+ cn = entry.single_value['cn']
+ if any(a.lower() in cn.lower() for a in args if a):
+ entries.append(entry)
+ else:
+ # If all given options match, include the entry
+ # Do a case-insensitive match, on any value if multi-valued
+ for opt in attribute_options:
+ optval = options[opt]
+ if not isinstance(optval, (tuple, list)):
+ optval = [optval]
+ value = entry.get(opt)
+ if not value:
+ break
+ if not all(any(str(ov).lower() in str(v).lower()
+ for v in value) for ov in optval):
+ break
else:
- new_entry = ldap.make_entry(dn, permission)
-
- if (dn, permission) not in entries:
- if len(entries) < max_entries:
- entries.append(new_entry)
- else:
- truncated = True
- break
+ entries.append(entry)
+
+ for entry in entries:
+ if options.get('pkey_only'):
+ for opt_name in entry.keys():
+ if opt_name != self.obj.primary_key.name:
+ del entry[opt_name]
+ else:
+ self.obj.postprocess_result(entry, options)
+
return truncated
@register()
-class permission_show(LDAPRetrieve):
+class permission_show(baseldap.LDAPRetrieve):
__doc__ = _('Display information about a permission.')
+ has_output_params = baseldap.LDAPRetrieve.has_output_params + output_params
- has_output_params = LDAPRetrieve.has_output_params + output_params
- def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
- assert isinstance(dn, DN)
- try:
- common_options = filter_options(options, ['all', 'raw'])
- aci = self.api.Command.aci_show(keys[-1], aciprefix=ACI_PREFIX,
- **common_options)['result']
- for attr in self.obj.aci_attributes:
- if attr in aci:
- entry_attrs[attr] = aci[attr]
- except errors.NotFound:
- self.debug('ACI not found for %s' % entry_attrs['cn'][0])
- if options.get('rights', False) and options.get('all', False):
- # The ACI attributes are just broken-out components of aci so
- # the rights should all match it.
- for attr in self.obj.aci_attributes:
- entry_attrs['attributelevelrights'][attr] = entry_attrs['attributelevelrights']['aci']
+ def post_callback(self, ldap, dn, entry, *keys, **options):
+ self.obj.upgrade_permission(entry, output_only=True)
+ self.obj.postprocess_result(entry, options)
return dn
@register()
-class permission_add_member(LDAPAddMember):
- """
- Add members to a permission.
- """
+class permission_add_member(baseldap.LDAPAddMember):
+ """Add members to a permission."""
NO_CLI = True
@register()
-class permission_remove_member(LDAPRemoveMember):
- """
- Remove members from a permission.
- """
+class permission_remove_member(baseldap.LDAPRemoveMember):
+ """Remove members from a permission."""
NO_CLI = True