diff options
author | Jan Cholasta <jcholast@redhat.com> | 2016-04-28 10:30:05 +0200 |
---|---|---|
committer | Jan Cholasta <jcholast@redhat.com> | 2016-06-03 09:00:34 +0200 |
commit | 6e44557b601f769d23ee74555a72e8b5cc62c0c9 (patch) | |
tree | eedd3e054b0709341b9f58c190ea54f999f7d13a /ipalib | |
parent | ec841e5d7ab29d08de294b3fa863a631cd50e30a (diff) | |
download | freeipa-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 'ipalib')
60 files changed, 1 insertions, 34801 deletions
diff --git a/ipalib/__init__.py b/ipalib/__init__.py index 0070e62db..4a61aced2 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -908,10 +908,8 @@ class API(plugable.API): @property def packages(self): if self.env.in_server: - import ipalib.plugins import ipaserver.plugins result = ( - ipalib.plugins, ipaserver.plugins, ) else: diff --git a/ipalib/plugins/__init__.py b/ipalib/plugins/__init__.py deleted file mode 100644 index e3bf6db35..000000000 --- a/ipalib/plugins/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -# Authors: -# Jason Gerard DeRose <jderose@redhat.com> -# -# Copyright (C) 2008 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/>. - -""" -Sub-package containing all core plugins. -""" diff --git a/ipalib/plugins/aci.py b/ipalib/plugins/aci.py deleted file mode 100644 index 01c929230..000000000 --- a/ipalib/plugins/aci.py +++ /dev/null @@ -1,986 +0,0 @@ -# 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), - ) diff --git a/ipalib/plugins/automember.py b/ipalib/plugins/automember.py deleted file mode 100644 index 89b9dfadc..000000000 --- a/ipalib/plugins/automember.py +++ /dev/null @@ -1,802 +0,0 @@ -# Authors: -# Jr Aquino <jr.aquino@citrix.com> -# -# Copyright (C) 2011 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/>. -import uuid -import time - -import ldap as _ldap -import six - -from ipalib import api, errors, Str, StrEnum, DNParam, Flag, _, ngettext -from ipalib import output, Command -from ipalib.plugable import Registry -from .baseldap import ( - pkey_to_value, - entry_to_dict, - LDAPObject, - LDAPCreate, - LDAPUpdate, - LDAPDelete, - LDAPSearch, - LDAPRetrieve) -from ipalib.request import context -from ipapython.dn import DN - -if six.PY3: - unicode = str - -__doc__ = _(""" -Auto Membership Rule. -""") + _(""" -Bring clarity to the membership of hosts and users by configuring inclusive -or exclusive regex patterns, you can automatically assign a new entries into -a group or hostgroup based upon attribute information. -""") + _(""" -A rule is directly associated with a group by name, so you cannot create -a rule without an accompanying group or hostgroup. -""") + _(""" -A condition is a regular expression used by 389-ds to match a new incoming -entry with an automember rule. If it matches an inclusive rule then the -entry is added to the appropriate group or hostgroup. -""") + _(""" -A default group or hostgroup could be specified for entries that do not -match any rule. In case of user entries this group will be a fallback group -because all users are by default members of group specified in IPA config. -""") + _(""" -The automember-rebuild command can be used to retroactively run automember rules -against existing entries, thus rebuilding their membership. -""") + _(""" -EXAMPLES: -""") + _(""" - Add the initial group or hostgroup: - ipa hostgroup-add --desc="Web Servers" webservers - ipa group-add --desc="Developers" devel -""") + _(""" - Add the initial rule: - ipa automember-add --type=hostgroup webservers - ipa automember-add --type=group devel -""") + _(""" - Add a condition to the rule: - ipa automember-add-condition --key=fqdn --type=hostgroup --inclusive-regex=^web[1-9]+\.example\.com webservers - ipa automember-add-condition --key=manager --type=group --inclusive-regex=^uid=mscott devel -""") + _(""" - Add an exclusive condition to the rule to prevent auto assignment: - ipa automember-add-condition --key=fqdn --type=hostgroup --exclusive-regex=^web5\.example\.com webservers -""") + _(""" - Add a host: - ipa host-add web1.example.com -""") + _(""" - Add a user: - ipa user-add --first=Tim --last=User --password tuser1 --manager=mscott -""") + _(""" - Verify automembership: - ipa hostgroup-show webservers - Host-group: webservers - Description: Web Servers - Member hosts: web1.example.com - - ipa group-show devel - Group name: devel - Description: Developers - GID: 1004200000 - Member users: tuser -""") + _(""" - Remove a condition from the rule: - ipa automember-remove-condition --key=fqdn --type=hostgroup --inclusive-regex=^web[1-9]+\.example\.com webservers -""") + _(""" - Modify the automember rule: - ipa automember-mod -""") + _(""" - Set the default (fallback) target group: - ipa automember-default-group-set --default-group=webservers --type=hostgroup - ipa automember-default-group-set --default-group=ipausers --type=group -""") + _(""" - Remove the default (fallback) target group: - ipa automember-default-group-remove --type=hostgroup - ipa automember-default-group-remove --type=group -""") + _(""" - Show the default (fallback) target group: - ipa automember-default-group-show --type=hostgroup - ipa automember-default-group-show --type=group -""") + _(""" - Find all of the automember rules: - ipa automember-find -""") + _(""" - Display a automember rule: - ipa automember-show --type=hostgroup webservers - ipa automember-show --type=group devel -""") + _(""" - Delete an automember rule: - ipa automember-del --type=hostgroup webservers - ipa automember-del --type=group devel -""") + _(""" - Rebuild membership for all users: - ipa automember-rebuild --type=group -""") + _(""" - Rebuild membership for all hosts: - ipa automember-rebuild --type=hostgroup -""") + _(""" - Rebuild membership for specified users: - ipa automember-rebuild --users=tuser1 --users=tuser2 -""") + _(""" - Rebuild membership for specified hosts: - ipa automember-rebuild --hosts=web1.example.com --hosts=web2.example.com -""") - -register = Registry() - -# Options used by Condition Add and Remove. -INCLUDE_RE = 'automemberinclusiveregex' -EXCLUDE_RE = 'automemberexclusiveregex' - -REBUILD_TASK_CONTAINER = DN(('cn', 'automember rebuild membership'), - ('cn', 'tasks'), - ('cn', 'config')) - - -regex_attrs = ( - Str('automemberinclusiveregex*', - cli_name='inclusive_regex', - label=_('Inclusive Regex'), - doc=_('Inclusive Regex'), - alwaysask=True, - ), - Str('automemberexclusiveregex*', - cli_name='exclusive_regex', - label=_('Exclusive Regex'), - doc=_('Exclusive Regex'), - alwaysask=True, - ), - Str('key', - label=_('Attribute Key'), - doc=_('Attribute to filter via regex. For example fqdn for a host, or manager for a user'), - flags=['no_create', 'no_update', 'no_search'] - ), -) - -group_type = ( - StrEnum('type', - label=_('Grouping Type'), - doc=_('Grouping to which the rule applies'), - values=(u'group', u'hostgroup', ), - ), -) - -automember_rule = ( - Str('cn', - cli_name='automember_rule', - label=_('Automember Rule'), - doc=_('Automember Rule'), - normalizer=lambda value: value.lower(), - ), -) - - -@register() -class automember(LDAPObject): - - """ - Bring automember to a hostgroup with an Auto Membership Rule. - """ - - container_dn = api.env.container_automember - - object_name = 'Automember rule' - object_name_plural = 'Automember rules' - object_class = ['top', 'automemberregexrule'] - permission_filter_objectclasses = ['automemberregexrule'] - default_attributes = [ - 'automemberinclusiveregex', 'automemberexclusiveregex', - 'cn', 'automembertargetgroup', 'description', 'automemberdefaultgroup' - ] - managed_permissions = { - 'System: Read Automember Definitions': { - 'non_object': True, - 'ipapermlocation': DN(container_dn, api.env.basedn), - 'ipapermtargetfilter': {'(objectclass=automemberdefinition)'}, - 'replaces_global_anonymous_aci': True, - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'objectclass', 'cn', 'automemberscope', 'automemberfilter', - 'automembergroupingattr', 'automemberdefaultgroup', - 'automemberdisabled', - }, - 'default_privileges': {'Automember Readers', - 'Automember Task Administrator'}, - }, - 'System: Read Automember Rules': { - 'replaces_global_anonymous_aci': True, - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'cn', 'objectclass', 'automembertargetgroup', 'description', - 'automemberexclusiveregex', 'automemberinclusiveregex', - }, - 'default_privileges': {'Automember Readers', - 'Automember Task Administrator'}, - }, - 'System: Read Automember Tasks': { - 'non_object': True, - 'ipapermlocation': DN('cn=tasks', 'cn=config'), - 'ipapermtarget': DN('cn=*', REBUILD_TASK_CONTAINER), - 'replaces_global_anonymous_aci': True, - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': {'*'}, - 'default_privileges': {'Automember Task Administrator'}, - }, - } - - label = _('Auto Membership Rule') - - takes_params = ( - Str('description?', - cli_name='desc', - label=_('Description'), - doc=_('A description of this auto member rule'), - ), - Str('automemberdefaultgroup?', - cli_name='default_group', - label=_('Default (fallback) Group'), - doc=_('Default group for entries to land'), - flags=['no_create', 'no_update', 'no_search'] - ), - ) - - def dn_exists(self, otype, oname): - ldap = self.api.Backend.ldap2 - dn = self.api.Object[otype].get_dn(oname) - try: - entry = ldap.get_entry(dn, []) - except errors.NotFound: - raise errors.NotFound( - reason=_(u'%(otype)s "%(oname)s" not found') % - dict(otype=otype, oname=oname) - ) - return entry.dn - - def get_dn(self, *keys, **options): - if self.parent_object: - parent_dn = self.api.Object[self.parent_object].get_dn(*keys[:-1]) - else: - parent_dn = DN(self.container_dn, api.env.basedn) - grouptype = options['type'] - try: - ndn = DN(('cn', keys[-1]), ('cn', grouptype), parent_dn) - except IndexError: - ndn = DN(('cn', grouptype), parent_dn) - return ndn - - def check_attr(self, attr): - """ - Verify that the user supplied key is a valid attribute in the schema - """ - ldap = self.api.Backend.ldap2 - obj = ldap.schema.get_obj(_ldap.schema.AttributeType, attr) - if obj is not None: - return obj - else: - raise errors.NotFound(reason=_('%s is not a valid attribute.') % attr) - - -def automember_container_exists(ldap): - try: - ldap.get_entry(DN(api.env.container_automember, api.env.basedn), []) - except errors.NotFound: - return False - return True - - -@register() -class automember_add(LDAPCreate): - __doc__ = _(""" - Add an automember rule. - """) - takes_options = LDAPCreate.takes_options + group_type - takes_args = automember_rule - msg_summary = _('Added automember rule "%(value)s"') - - def pre_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - - entry_attrs['cn'] = keys[-1] - if not automember_container_exists(self.api.Backend.ldap2): - raise errors.NotFound(reason=_('Auto Membership is not configured')) - entry_attrs['automembertargetgroup'] = self.obj.dn_exists(options['type'], keys[-1]) - return dn - - def execute(self, *keys, **options): - result = super(automember_add, self).execute(*keys, **options) - result['value'] = pkey_to_value(keys[-1], options) - return result - - -@register() -class automember_add_condition(LDAPUpdate): - __doc__ = _(""" - Add conditions to an automember rule. - """) - has_output_params = ( - Str('failed', - label=_('Failed to add'), - flags=['suppress_empty'], - ), - ) - - takes_options = regex_attrs + group_type - takes_args = automember_rule - msg_summary = _('Added condition(s) to "%(value)s"') - - # Prepare the output to expect failed results - has_output = ( - output.summary, - output.Entry('result'), - output.value, - output.Output('failed', - type=dict, - doc=_('Conditions that could not be added'), - ), - output.Output('completed', - type=int, - doc=_('Number of conditions added'), - ), - ) - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - # Check to see if the automember rule exists - try: - dn = ldap.get_entry(dn, []).dn - except errors.NotFound: - raise errors.NotFound(reason=_(u'Auto member rule: %s not found!') % keys[0]) - # Define container key - key = options['key'] - # Check to see if the attribute is valid - self.obj.check_attr(key) - - key = '%s=' % key - completed = 0 - failed = {'failed': {}} - - for attr in (INCLUDE_RE, EXCLUDE_RE): - failed['failed'][attr] = [] - if attr in options and options[attr]: - entry_attrs[attr] = [key + condition for condition in options[attr]] - completed += len(entry_attrs[attr]) - try: - old_entry = ldap.get_entry(dn, [attr]) - for regex in old_entry.keys(): - if not isinstance(entry_attrs[regex], (list, tuple)): - entry_attrs[regex] = [entry_attrs[regex]] - duplicate = set(old_entry[regex]) & set(entry_attrs[regex]) - if len(duplicate) > 0: - completed -= 1 - else: - entry_attrs[regex] = list(entry_attrs[regex]) + old_entry[regex] - except errors.NotFound: - failed['failed'][attr].append(regex) - - entry_attrs = entry_to_dict(entry_attrs, **options) - - # Set failed and completed to they can be harvested in the execute super - setattr(context, 'failed', failed) - setattr(context, 'completed', completed) - setattr(context, 'entry_attrs', entry_attrs) - - # Make sure to returned the failed results if there is nothing to remove - if completed == 0: - ldap.get_entry(dn, attrs_list) - raise errors.EmptyModlist - return dn - - def execute(self, *keys, **options): - __doc__ = _(""" - Override this so we can add completed and failed to the return result. - """) - try: - result = super(automember_add_condition, self).execute(*keys, **options) - except errors.EmptyModlist: - result = {'result': getattr(context, 'entry_attrs'), 'value': keys[-1]} - result['failed'] = getattr(context, 'failed') - result['completed'] = getattr(context, 'completed') - result['value'] = pkey_to_value(keys[-1], options) - return result - - -@register() -class automember_remove_condition(LDAPUpdate): - __doc__ = _(""" - Remove conditions from an automember rule. - """) - takes_options = regex_attrs + group_type - takes_args = automember_rule - msg_summary = _('Removed condition(s) from "%(value)s"') - - # Prepare the output to expect failed results - has_output = ( - output.summary, - output.Entry('result'), - output.value, - output.Output('failed', - type=dict, - doc=_('Conditions that could not be removed'), - ), - output.Output('completed', - type=int, - doc=_('Number of conditions removed'), - ), - ) - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - # Check to see if the automember rule exists - try: - ldap.get_entry(dn, []) - except errors.NotFound: - raise errors.NotFound(reason=_(u'Auto member rule: %s not found!') % keys[0]) - - # Define container key - type_attr_default = {'group': 'manager', 'hostgroup': 'fqdn'} - if 'key' in options: - key = options['key'] - else: - key = type_attr_default[options['type']] - - key = '%s=' % key - completed = 0 - failed = {'failed': {}} - - # Check to see if there are existing exclusive conditions present. - dn = ldap.get_entry(dn, [EXCLUDE_RE]).dn - - for attr in (INCLUDE_RE, EXCLUDE_RE): - failed['failed'][attr] = [] - if attr in options and options[attr]: - entry_attrs[attr] = [key + condition for condition in options[attr]] - entry_attrs_ = ldap.get_entry(dn, [attr]) - old_entry = entry_attrs_.get(attr, []) - for regex in entry_attrs[attr]: - if regex in old_entry: - old_entry.remove(regex) - completed += 1 - else: - failed['failed'][attr].append(regex) - entry_attrs[attr] = old_entry - - entry_attrs = entry_to_dict(entry_attrs, **options) - - # Set failed and completed to they can be harvested in the execute super - setattr(context, 'failed', failed) - setattr(context, 'completed', completed) - setattr(context, 'entry_attrs', entry_attrs) - - # Make sure to returned the failed results if there is nothing to remove - if completed == 0: - ldap.get_entry(dn, attrs_list) - raise errors.EmptyModlist - return dn - - def execute(self, *keys, **options): - __doc__ = _(""" - Override this so we can set completed and failed. - """) - try: - result = super(automember_remove_condition, self).execute(*keys, **options) - except errors.EmptyModlist: - result = {'result': getattr(context, 'entry_attrs'), 'value': keys[-1]} - result['failed'] = getattr(context, 'failed') - result['completed'] = getattr(context, 'completed') - result['value'] = pkey_to_value(keys[-1], options) - return result - - -@register() -class automember_mod(LDAPUpdate): - __doc__ = _(""" - Modify an automember rule. - """) - takes_args = automember_rule - takes_options = LDAPUpdate.takes_options + group_type - msg_summary = _('Modified automember rule "%(value)s"') - - def execute(self, *keys, **options): - result = super(automember_mod, self).execute(*keys, **options) - result['value'] = pkey_to_value(keys[-1], options) - return result - - -@register() -class automember_del(LDAPDelete): - __doc__ = _(""" - Delete an automember rule. - """) - takes_args = automember_rule - takes_options = group_type - msg_summary = _('Deleted automember rule "%(value)s"') - - def execute(self, *keys, **options): - result = super(automember_del, self).execute(*keys, **options) - result['value'] = pkey_to_value([keys[-1]], options) - return result - - -@register() -class automember_find(LDAPSearch): - __doc__ = _(""" - Search for automember rules. - """) - takes_options = group_type - has_output_params = LDAPSearch.has_output_params + automember_rule + regex_attrs - - msg_summary = ngettext( - '%(count)d rules matched', '%(count)d rules matched', 0 - ) - - def pre_callback(self, ldap, filters, attrs_list, base_dn, scope, *args, **options): - assert isinstance(base_dn, DN) - scope = ldap.SCOPE_SUBTREE - ndn = DN(('cn', options['type']), base_dn) - return (filters, ndn, scope) - - -@register() -class automember_show(LDAPRetrieve): - __doc__ = _(""" - Display information about an automember rule. - """) - takes_args = automember_rule - takes_options = group_type - has_output_params = LDAPRetrieve.has_output_params + regex_attrs - - def execute(self, *keys, **options): - result = super(automember_show, self).execute(*keys, **options) - result['value'] = pkey_to_value(keys[-1], options) - return result - - -@register() -class automember_default_group_set(LDAPUpdate): - __doc__ = _(""" - Set default (fallback) group for all unmatched entries. - """) - - takes_options = ( - Str('automemberdefaultgroup', - cli_name='default_group', - label=_('Default (fallback) Group'), - doc=_('Default (fallback) group for entries to land'), - flags=['no_create', 'no_update'] - ), - ) + group_type - msg_summary = _('Set default (fallback) group for automember "%(value)s"') - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - dn = DN(('cn', options['type']), api.env.container_automember, - api.env.basedn) - entry_attrs['automemberdefaultgroup'] = self.obj.dn_exists(options['type'], options['automemberdefaultgroup']) - return dn - - def execute(self, *keys, **options): - result = super(automember_default_group_set, self).execute(*keys, **options) - result['value'] = pkey_to_value(options['type'], options) - return result - - -@register() -class automember_default_group_remove(LDAPUpdate): - __doc__ = _(""" - Remove default (fallback) group for all unmatched entries. - """) - - takes_options = group_type - msg_summary = _('Removed default (fallback) group for automember "%(value)s"') - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - dn = DN(('cn', options['type']), api.env.container_automember, - api.env.basedn) - attr = 'automemberdefaultgroup' - - entry_attrs_ = ldap.get_entry(dn, [attr]) - - if attr not in entry_attrs_: - raise errors.NotFound(reason=_(u'No default (fallback) group set')) - else: - entry_attrs[attr] = [] - return entry_attrs_.dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - if 'automemberdefaultgroup' not in entry_attrs: - entry_attrs['automemberdefaultgroup'] = unicode(_('No default (fallback) group set')) - return dn - - def execute(self, *keys, **options): - result = super(automember_default_group_remove, self).execute(*keys, **options) - result['value'] = pkey_to_value(options['type'], options) - return result - - -@register() -class automember_default_group_show(LDAPRetrieve): - __doc__ = _(""" - Display information about the default (fallback) automember groups. - """) - takes_options = group_type - - def pre_callback(self, ldap, dn, attrs_list, *keys, **options): - dn = DN(('cn', options['type']), api.env.container_automember, - api.env.basedn) - return dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - if 'automemberdefaultgroup' not in entry_attrs: - entry_attrs['automemberdefaultgroup'] = unicode(_('No default (fallback) group set')) - return dn - - def execute(self, *keys, **options): - result = super(automember_default_group_show, self).execute(*keys, **options) - result['value'] = pkey_to_value(options['type'], options) - return result - - -@register() -class automember_rebuild(Command): - __doc__ = _('Rebuild auto membership.') - # TODO: Add a --dry-run option: - # https://fedorahosted.org/freeipa/ticket/3936 - takes_options = ( - group_type[0].clone( - required=False, - label=_('Rebuild membership for all members of a grouping') - ), - Str( - 'users*', - label=_('Users'), - doc=_('Rebuild membership for specified users'), - ), - Str( - 'hosts*', - label=_('Hosts'), - doc=_('Rebuild membership for specified hosts'), - ), - Flag( - 'no_wait?', - default=False, - label=_('No wait'), - doc=_("Don't wait for rebuilding membership"), - ), - ) - has_output = output.standard_entry - has_output_params = ( - DNParam( - 'dn', - label=_('Task DN'), - doc=_('DN of the started task'), - ), - ) - - def validate(self, **kw): - """ - Validation rules: - - at least one of 'type', 'users', 'hosts' is required - - 'users' and 'hosts' cannot be combined together - - if 'users' and 'type' are specified, 'type' must be 'group' - - if 'hosts' and 'type' are specified, 'type' must be 'hostgroup' - """ - super(automember_rebuild, self).validate(**kw) - users, hosts, gtype = kw.get('users'), kw.get('hosts'), kw.get('type') - - if not (gtype or users or hosts): - raise errors.MutuallyExclusiveError( - reason=_('at least one of options: type, users, hosts must be ' - 'specified') - ) - - if users and hosts: - raise errors.MutuallyExclusiveError( - reason=_("users and hosts cannot both be set") - ) - if gtype == 'group' and hosts: - raise errors.MutuallyExclusiveError( - reason=_("hosts cannot be set when type is 'group'") - ) - if gtype == 'hostgroup' and users: - raise errors.MutuallyExclusiveError( - reason=_("users cannot be set when type is 'hostgroup'") - ) - - def execute(self, *keys, **options): - ldap = self.api.Backend.ldap2 - cn = str(uuid.uuid4()) - - gtype = options.get('type') - if not gtype: - gtype = 'group' if options.get('users') else 'hostgroup' - - types = { - 'group': ( - 'user', - 'users', - DN(api.env.container_user, api.env.basedn) - ), - 'hostgroup': ( - 'host', - 'hosts', - DN(api.env.container_host, api.env.basedn) - ), - } - - obj_name, opt_name, basedn = types[gtype] - obj = self.api.Object[obj_name] - - names = options.get(opt_name) - if names: - for name in names: - try: - obj.get_dn_if_exists(name) - except errors.NotFound: - obj.handle_not_found(name) - search_filter = ldap.make_filter_from_attr( - obj.primary_key.name, - names, - rules=ldap.MATCH_ANY - ) - else: - search_filter = '(%s=*)' % obj.primary_key.name - - task_dn = DN(('cn', cn), REBUILD_TASK_CONTAINER) - - entry = ldap.make_entry( - task_dn, - objectclass=['top', 'extensibleObject'], - cn=[cn], - basedn=[basedn], - filter=[search_filter], - scope=['sub'], - ttl=[3600]) - ldap.add_entry(entry) - - summary = _('Automember rebuild membership task started') - result = {'dn': task_dn} - - if not options.get('no_wait'): - summary = _('Automember rebuild membership task completed') - result = {} - start_time = time.time() - - while True: - try: - task = ldap.get_entry(task_dn) - except errors.NotFound: - break - - if 'nstaskexitcode' in task: - if str(task.single_value['nstaskexitcode']) == '0': - summary=task.single_value['nstaskstatus'] - break - else: - raise errors.DatabaseError( - desc=task.single_value['nstaskstatus'], - info=_("Task DN = '%s'" % task_dn)) - time.sleep(1) - if time.time() > (start_time + 60): - raise errors.TaskTimeout(task=_('Automember'), task_dn=task_dn) - - return dict( - result=result, - summary=unicode(summary), - value=pkey_to_value(None, options)) diff --git a/ipalib/plugins/automount.py b/ipalib/plugins/automount.py deleted file mode 100644 index c4cf2d6db..000000000 --- a/ipalib/plugins/automount.py +++ /dev/null @@ -1,841 +0,0 @@ -# Authors: -# Rob Crittenden <rcritten@redhat.com> -# Pavel Zuna <pzuna@redhat.com> -# -# Copyright (C) 2008 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/>. - -import six - -from ipalib import api, errors -from ipalib import Str, IA5Str -from ipalib.plugable import Registry -from .baseldap import ( - pkey_to_value, - LDAPObject, - LDAPCreate, - LDAPDelete, - LDAPQuery, - LDAPUpdate, - LDAPSearch, - LDAPRetrieve) -from ipalib import _, ngettext -from ipapython.dn import DN - -if six.PY3: - unicode = str - -__doc__ = _(""" -Automount - -Stores automount(8) configuration for autofs(8) in IPA. - -The base of an automount configuration is the configuration file auto.master. -This is also the base location in IPA. Multiple auto.master configurations -can be stored in separate locations. A location is implementation-specific -with the default being a location named 'default'. For example, you can have -locations by geographic region, by floor, by type, etc. - -Automount has three basic object types: locations, maps and keys. - -A location defines a set of maps anchored in auto.master. This allows you -to store multiple automount configurations. A location in itself isn't -very interesting, it is just a point to start a new automount map. - -A map is roughly equivalent to a discrete automount file and provides -storage for keys. - -A key is a mount point associated with a map. - -When a new location is created, two maps are automatically created for -it: auto.master and auto.direct. auto.master is the root map for all -automount maps for the location. auto.direct is the default map for -direct mounts and is mounted on /-. - -An automount map may contain a submount key. This key defines a mount -location within the map that references another map. This can be done -either using automountmap-add-indirect --parentmap or manually -with automountkey-add and setting info to "-type=autofs :<mapname>". - -EXAMPLES: - -Locations: - - Create a named location, "Baltimore": - ipa automountlocation-add baltimore - - Display the new location: - ipa automountlocation-show baltimore - - Find available locations: - ipa automountlocation-find - - Remove a named automount location: - ipa automountlocation-del baltimore - - Show what the automount maps would look like if they were in the filesystem: - ipa automountlocation-tofiles baltimore - - Import an existing configuration into a location: - ipa automountlocation-import baltimore /etc/auto.master - - The import will fail if any duplicate entries are found. For - continuous operation where errors are ignored, use the --continue - option. - -Maps: - - Create a new map, "auto.share": - ipa automountmap-add baltimore auto.share - - Display the new map: - ipa automountmap-show baltimore auto.share - - Find maps in the location baltimore: - ipa automountmap-find baltimore - - Create an indirect map with auto.share as a submount: - ipa automountmap-add-indirect baltimore --parentmap=auto.share --mount=sub auto.man - - This is equivalent to: - - ipa automountmap-add-indirect baltimore --mount=/man auto.man - ipa automountkey-add baltimore auto.man --key=sub --info="-fstype=autofs ldap:auto.share" - - Remove the auto.share map: - ipa automountmap-del baltimore auto.share - -Keys: - - Create a new key for the auto.share map in location baltimore. This ties - the map we previously created to auto.master: - ipa automountkey-add baltimore auto.master --key=/share --info=auto.share - - Create a new key for our auto.share map, an NFS mount for man pages: - ipa automountkey-add baltimore auto.share --key=man --info="-ro,soft,rsize=8192,wsize=8192 ipa.example.com:/shared/man" - - Find all keys for the auto.share map: - ipa automountkey-find baltimore auto.share - - Find all direct automount keys: - ipa automountkey-find baltimore --key=/- - - Remove the man key from the auto.share map: - ipa automountkey-del baltimore auto.share --key=man -""") - -""" -Developer notes: - -RFC 2707bis http://www.padl.com/~lukeh/rfc2307bis.txt - -A few notes on automount: -- The default parent when adding an indirect map is auto.master -- This uses the short format for automount maps instead of the - URL format. Support for ldap as a map source in nsswitch.conf was added - in autofs version 4.1.3-197. Any version prior to that is not expected - to work. -- An indirect key should not begin with / - -As an example, the following automount files: - -auto.master: -/- auto.direct -/mnt auto.mnt - -auto.mnt: -stuff -ro,soft,rsize=8192,wsize=8192 nfs.example.com:/vol/archive/stuff - -are equivalent to the following LDAP entries: - -# auto.master, automount, example.com -dn: automountmapname=auto.master,cn=automount,dc=example,dc=com -objectClass: automountMap -objectClass: top -automountMapName: auto.master - -# auto.direct, automount, example.com -dn: automountmapname=auto.direct,cn=automount,dc=example,dc=com -objectClass: automountMap -objectClass: top -automountMapName: auto.direct - -# /-, auto.master, automount, example.com -dn: automountkey=/-,automountmapname=auto.master,cn=automount,dc=example,dc=co - m -objectClass: automount -objectClass: top -automountKey: /- -automountInformation: auto.direct - -# auto.mnt, automount, example.com -dn: automountmapname=auto.mnt,cn=automount,dc=example,dc=com -objectClass: automountMap -objectClass: top -automountMapName: auto.mnt - -# /mnt, auto.master, automount, example.com -dn: automountkey=/mnt,automountmapname=auto.master,cn=automount,dc=example,dc= - com -objectClass: automount -objectClass: top -automountKey: /mnt -automountInformation: auto.mnt - -# stuff, auto.mnt, automount, example.com -dn: automountkey=stuff,automountmapname=auto.mnt,cn=automount,dc=example,dc=com -objectClass: automount -objectClass: top -automountKey: stuff -automountInformation: -ro,soft,rsize=8192,wsize=8192 nfs.example.com:/vol/arch - ive/stuff - -""" - -register = Registry() - -DIRECT_MAP_KEY = u'/-' - -@register() -class automountlocation(LDAPObject): - """ - Location container for automount maps. - """ - container_dn = api.env.container_automount - object_name = _('automount location') - object_name_plural = _('automount locations') - object_class = ['nscontainer'] - default_attributes = ['cn'] - label = _('Automount Locations') - label_singular = _('Automount Location') - permission_filter_objectclasses = ['nscontainer'] - managed_permissions = { - 'System: Read Automount Configuration': { - # Single permission for all automount-related entries - 'non_object': True, - 'ipapermlocation': DN(container_dn, api.env.basedn), - 'replaces_global_anonymous_aci': True, - 'ipapermbindruletype': 'anonymous', - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'cn', 'objectclass', - 'automountinformation', 'automountkey', 'description', - 'automountmapname', 'description', - }, - }, - 'System: Add Automount Locations': { - 'ipapermright': {'add'}, - 'default_privileges': {'Automount Administrators'}, - }, - 'System: Remove Automount Locations': { - 'ipapermright': {'delete'}, - 'default_privileges': {'Automount Administrators'}, - }, - } - - takes_params = ( - Str('cn', - cli_name='location', - label=_('Location'), - doc=_('Automount location name.'), - primary_key=True, - ), - ) - - -@register() -class automountlocation_add(LDAPCreate): - __doc__ = _('Create a new automount location.') - - msg_summary = _('Added automount location "%(value)s"') - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - # create auto.master for the new location - self.api.Command['automountmap_add'](keys[-1], u'auto.master') - - # add additional pre-created maps and keys - # IMPORTANT: add pre-created maps/keys to DEFAULT_MAPS/DEFAULT_KEYS - # so that they do not cause conflicts during import operation - self.api.Command['automountmap_add_indirect']( - keys[-1], u'auto.direct', key=DIRECT_MAP_KEY - ) - return dn - - -@register() -class automountlocation_del(LDAPDelete): - __doc__ = _('Delete an automount location.') - - msg_summary = _('Deleted automount location "%(value)s"') - - -@register() -class automountlocation_show(LDAPRetrieve): - __doc__ = _('Display an automount location.') - - -@register() -class automountlocation_find(LDAPSearch): - __doc__ = _('Search for an automount location.') - - msg_summary = ngettext( - '%(count)d automount location matched', - '%(count)d automount locations matched', 0 - ) - - -@register() -class automountlocation_tofiles(LDAPQuery): - __doc__ = _('Generate automount files for a specific location.') - - def execute(self, *args, **options): - self.api.Command['automountlocation_show'](args[0]) - - result = self.api.Command['automountkey_find'](args[0], u'auto.master') - maps = result['result'] - - # maps, truncated - # TODO: handle truncated results - # ?use ldap.find_entries instead of automountkey_find? - - keys = {} - mapnames = [u'auto.master'] - for m in maps: - info = m['automountinformation'][0] - mapnames.append(info) - key = info.split(None) - result = self.api.Command['automountkey_find'](args[0], key[0]) - keys[info] = result['result'] - # TODO: handle truncated results, same as above - - allmaps = self.api.Command['automountmap_find'](args[0])['result'] - orphanmaps = [] - for m in allmaps: - if m['automountmapname'][0] not in mapnames: - orphanmaps.append(m) - - orphankeys = [] - # Collect all the keys for the orphaned maps - for m in orphanmaps: - key = m['automountmapname'] - result = self.api.Command['automountkey_find'](args[0], key[0]) - orphankeys.append(result['result']) - - return dict(result=dict(maps=maps, keys=keys, - orphanmaps=orphanmaps, orphankeys=orphankeys)) - - -@register() -class automountmap(LDAPObject): - """ - Automount map object. - """ - parent_object = 'automountlocation' - container_dn = api.env.container_automount - object_name = _('automount map') - object_name_plural = _('automount maps') - object_class = ['automountmap'] - permission_filter_objectclasses = ['automountmap'] - default_attributes = ['automountmapname', 'description'] - - takes_params = ( - IA5Str('automountmapname', - cli_name='map', - label=_('Map'), - doc=_('Automount map name.'), - primary_key=True, - ), - Str('description?', - cli_name='desc', - label=_('Description'), - ), - ) - - managed_permissions = { - 'System: Add Automount Maps': { - 'ipapermright': {'add'}, - 'replaces': [ - '(target = "ldap:///automountmapname=*,cn=automount,$SUFFIX")(version 3.0;acl "permission:Add Automount maps";allow (add) groupdn = "ldap:///cn=Add Automount maps,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Automount Administrators'}, - }, - 'System: Modify Automount Maps': { - 'ipapermright': {'write'}, - 'ipapermdefaultattr': {'automountmapname', 'description'}, - 'replaces': [ - '(targetattr = "automountmapname || description")(target = "ldap:///automountmapname=*,cn=automount,$SUFFIX")(version 3.0;acl "permission:Modify Automount maps";allow (write) groupdn = "ldap:///cn=Modify Automount maps,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Automount Administrators'}, - }, - 'System: Remove Automount Maps': { - 'ipapermright': {'delete'}, - 'replaces': [ - '(target = "ldap:///automountmapname=*,cn=automount,$SUFFIX")(version 3.0;acl "permission:Remove Automount maps";allow (delete) groupdn = "ldap:///cn=Remove Automount maps,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Automount Administrators'}, - }, - } - - label = _('Automount Maps') - label_singular = _('Automount Map') - - -@register() -class automountmap_add(LDAPCreate): - __doc__ = _('Create a new automount map.') - - msg_summary = _('Added automount map "%(value)s"') - - -@register() -class automountmap_del(LDAPDelete): - __doc__ = _('Delete an automount map.') - - msg_summary = _('Deleted automount map "%(value)s"') - - def post_callback(self, ldap, dn, *keys, **options): - assert isinstance(dn, DN) - # delete optional parental connection (direct maps may not have this) - try: - entry_attrs = ldap.find_entry_by_attr( - 'automountinformation', keys[0], 'automount', - base_dn=DN(self.obj.container_dn, api.env.basedn) - ) - ldap.delete_entry(entry_attrs) - except errors.NotFound: - pass - return True - - -@register() -class automountmap_mod(LDAPUpdate): - __doc__ = _('Modify an automount map.') - - msg_summary = _('Modified automount map "%(value)s"') - - -@register() -class automountmap_find(LDAPSearch): - __doc__ = _('Search for an automount map.') - - msg_summary = ngettext( - '%(count)d automount map matched', - '%(count)d automount maps matched', 0 - ) - - -@register() -class automountmap_show(LDAPRetrieve): - __doc__ = _('Display an automount map.') - - -@register() -class automountkey(LDAPObject): - __doc__ = _('Automount key object.') - - parent_object = 'automountmap' - container_dn = api.env.container_automount - object_name = _('automount key') - object_name_plural = _('automount keys') - object_class = ['automount'] - permission_filter_objectclasses = ['automount'] - default_attributes = [ - 'automountkey', 'automountinformation', 'description' - ] - rdn_is_primary_key = True - rdn_separator = ' ' - - takes_params = ( - IA5Str('automountkey', - cli_name='key', - label=_('Key'), - doc=_('Automount key name.'), - flags=('req_update',), - ), - IA5Str('automountinformation', - cli_name='info', - label=_('Mount information'), - ), - Str('description', - label=_('description'), - primary_key=True, - required=False, - flags=['no_create', 'no_update', 'no_search', 'no_output'], - exclude='webui', - ), - ) - - managed_permissions = { - 'System: Add Automount Keys': { - 'ipapermright': {'add'}, - 'replaces': [ - '(target = "ldap:///automountkey=*,automountmapname=*,cn=automount,$SUFFIX")(version 3.0;acl "permission:Add Automount keys";allow (add) groupdn = "ldap:///cn=Add Automount keys,cn=permissions,cn=pbac,$SUFFIX";)', - '(targetfilter = "(objectclass=automount)")(target = "ldap:///automountmapname=*,cn=automount,$SUFFIX")(version 3.0;acl "permission:Add Automount keys";allow (add) groupdn = "ldap:///cn=Add Automount keys,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Automount Administrators'}, - }, - 'System: Modify Automount Keys': { - 'ipapermright': {'write'}, - 'ipapermdefaultattr': { - 'automountinformation', 'automountkey', 'description', - }, - 'replaces': [ - '(targetattr = "automountkey || automountinformation || description")(targetfilter = "(objectclass=automount)")(target = "ldap:///automountmapname=*,cn=automount,$SUFFIX")(version 3.0;acl "permission:Modify Automount keys";allow (write) groupdn = "ldap:///cn=Modify Automount keys,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Automount Administrators'}, - }, - 'System: Remove Automount Keys': { - 'ipapermright': {'delete'}, - 'replaces': [ - '(target = "ldap:///automountkey=*,automountmapname=*,cn=automount,$SUFFIX")(version 3.0;acl "permission:Remove Automount keys";allow (delete) groupdn = "ldap:///cn=Remove Automount keys,cn=permissions,cn=pbac,$SUFFIX";)', - '(targetfilter = "(objectclass=automount)")(target = "ldap:///automountmapname=*,cn=automount,$SUFFIX")(version 3.0;acl "permission:Remove Automount keys";allow (delete) groupdn = "ldap:///cn=Remove Automount keys,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Automount Administrators'}, - }, - } - - num_parents = 2 - label = _('Automount Keys') - label_singular = _('Automount Key') - already_exists_msg = _('The key,info pair must be unique. A key named %(key)s with info %(info)s already exists') - key_already_exists_msg = _('key named %(key)s already exists') - object_not_found_msg = _('The automount key %(key)s with info %(info)s does not exist') - - def get_dn(self, *keys, **kwargs): - # all commands except for create send pk in keys, too - # create cannot due to validation in frontend.py - ldap = self.backend - if len(keys) == self.num_parents: - try: - pkey = kwargs[self.primary_key.name] - except KeyError: - raise ValueError('Not enough keys and pkey not in kwargs') - parent_keys = keys - else: - pkey = keys[-1] - parent_keys = keys[:-1] - - parent_dn = self.api.Object[self.parent_object].get_dn(*parent_keys) - dn = self.backend.make_dn_from_attr( - self.primary_key.name, - pkey, - parent_dn - ) - # If we're doing an add then just return the dn we created, there - # is no need to check for it. - if kwargs.get('add_operation', False): - return dn - # We had an older mechanism where description consisted of - # 'automountkey automountinformation' so we could support multiple - # direct maps. This made showing keys nearly impossible since it - # required automountinfo to show, which if you had you didn't need - # to look at the key. We still support existing entries but now - # only create this type of dn when the key is /- - # - # First we look with the information given, then try to search for - # the right entry. - try: - dn = ldap.get_entry(dn, ['*']).dn - except errors.NotFound: - if kwargs.get('automountinformation', False): - sfilter = '(&(automountkey=%s)(automountinformation=%s))' % \ - (kwargs['automountkey'], kwargs['automountinformation']) - else: - sfilter = '(automountkey=%s)' % kwargs['automountkey'] - basedn = DN(('automountmapname', parent_keys[1]), - ('cn', parent_keys[0]), self.container_dn, - api.env.basedn) - attrs_list = ['*'] - entries = ldap.get_entries( - basedn, ldap.SCOPE_ONELEVEL, sfilter, attrs_list) - if len(entries) > 1: - raise errors.NotFound(reason=_('More than one entry with key %(key)s found, use --info to select specific entry.') % dict(key=pkey)) - dn = entries[0].dn - - return dn - - def handle_not_found(self, *keys): - pkey = keys[-1] - key = pkey.split(self.rdn_separator)[0] - info = self.rdn_separator.join(pkey.split(self.rdn_separator)[1:]) - raise errors.NotFound( - reason=self.object_not_found_msg % { - 'key': key, 'info': info, - } - ) - - def handle_duplicate_entry(self, *keys): - pkey = keys[-1] - key = pkey.split(self.rdn_separator)[0] - info = self.rdn_separator.join(pkey.split(self.rdn_separator)[1:]) - if info: - raise errors.DuplicateEntry( - message=self.already_exists_msg % { - 'key': key, 'info': info, - } - ) - else: - raise errors.DuplicateEntry( - message=self.key_already_exists_msg % { - 'key': key, - } - ) - - def get_pk(self, key, info=None): - if key == DIRECT_MAP_KEY and info: - return self.rdn_separator.join((key,info)) - else: - return key - - def check_key_uniqueness(self, location, map, **keykw): - info = None - key = keykw.get('automountkey') - if key is None: - return - - entries = self.methods.find(location, map, automountkey=key)['result'] - if len(entries) > 0: - if key == DIRECT_MAP_KEY: - info = keykw.get('automountinformation') - entries = self.methods.find(location, map, **keykw)['result'] - if len(entries) > 0: - self.handle_duplicate_entry(location, map, self.get_pk(key, info)) - else: return - self.handle_duplicate_entry(location, map, self.get_pk(key, info)) - - -@register() -class automountkey_add(LDAPCreate): - __doc__ = _('Create a new automount key.') - - msg_summary = _('Added automount key "%(value)s"') - - internal_options = ['description', 'add_operation'] - - def pre_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - options.pop('add_operation', None) - options.pop('description', None) - self.obj.check_key_uniqueness(keys[-2], keys[-1], **options) - return dn - - def get_args(self): - for key in self.obj.get_ancestor_primary_keys(): - yield key - - def execute(self, *keys, **options): - key = options['automountkey'] - info = options.get('automountinformation', None) - options[self.obj.primary_key.name] = self.obj.get_pk(key, info) - options['add_operation'] = True - result = super(automountkey_add, self).execute(*keys, **options) - result['value'] = pkey_to_value(options['automountkey'], options) - return result - - -@register() -class automountmap_add_indirect(LDAPCreate): - __doc__ = _('Create a new indirect mount point.') - - msg_summary = _('Added automount indirect map "%(value)s"') - - takes_options = LDAPCreate.takes_options + ( - Str('key', - cli_name='mount', - label=_('Mount point'), - ), - Str('parentmap?', - cli_name='parentmap', - label=_('Parent map'), - doc=_('Name of parent automount map (default: auto.master).'), - default=u'auto.master', - autofill=True, - ), - ) - - def execute(self, *keys, **options): - parentmap = options.pop('parentmap', None) - key = options.pop('key') - result = self.api.Command['automountmap_add'](*keys, **options) - try: - if parentmap != u'auto.master': - if key.startswith('/'): - raise errors.ValidationError(name='mount', - error=_('mount point is relative to parent map, ' - 'cannot begin with /')) - location = keys[0] - map = keys[1] - options['automountinformation'] = map - - # Ensure the referenced map exists - self.api.Command['automountmap_show'](location, parentmap) - # Add a submount key - self.api.Command['automountkey_add']( - location, parentmap, automountkey=key, - automountinformation='-fstype=autofs ldap:%s' % map) - else: # adding to auto.master - # Ensure auto.master exists - self.api.Command['automountmap_show'](keys[0], parentmap) - self.api.Command['automountkey_add']( - keys[0], u'auto.master', automountkey=key, - automountinformation=keys[1]) - except Exception: - # The key exists, drop the map - self.api.Command['automountmap_del'](*keys) - raise - return result - - -@register() -class automountkey_del(LDAPDelete): - __doc__ = _('Delete an automount key.') - - msg_summary = _('Deleted automount key "%(value)s"') - - takes_options = LDAPDelete.takes_options + ( - IA5Str('automountkey', - cli_name='key', - label=_('Key'), - doc=_('Automount key name.'), - ), - IA5Str('automountinformation?', - cli_name='info', - label=_('Mount information'), - ), - ) - def get_options(self): - for option in super(automountkey_del, self).get_options(): - if option.name == 'continue': - # TODO: hide for now - remove in future major release - yield option.clone(exclude='webui', - flags=['no_option', 'no_output']) - else: - yield option - - def get_args(self): - for key in self.obj.get_ancestor_primary_keys(): - yield key - - def execute(self, *keys, **options): - keys += (self.obj.get_pk(options['automountkey'], - options.get('automountinformation', None)),) - options[self.obj.primary_key.name] = self.obj.get_pk( - options['automountkey'], - options.get('automountinformation', None)) - result = super(automountkey_del, self).execute(*keys, **options) - result['value'] = pkey_to_value([options['automountkey']], options) - return result - - -@register() -class automountkey_mod(LDAPUpdate): - __doc__ = _('Modify an automount key.') - - msg_summary = _('Modified automount key "%(value)s"') - - internal_options = ['newautomountkey'] - - takes_options = LDAPUpdate.takes_options + ( - IA5Str('newautomountinformation?', - cli_name='newinfo', - label=_('New mount information'), - ), - ) - - def get_args(self): - for key in self.obj.get_ancestor_primary_keys(): - yield key - - def pre_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - if 'newautomountkey' in options: - entry_attrs['automountkey'] = options['newautomountkey'] - if 'newautomountinformation' in options: - entry_attrs['automountinformation'] = options['newautomountinformation'] - return dn - - def execute(self, *keys, **options): - ldap = self.api.Backend.ldap2 - key = options['automountkey'] - info = options.get('automountinformation', None) - keys += (self.obj.get_pk(key, info), ) - - # handle RDN changes - if 'rename' in options or 'newautomountinformation' in options: - new_key = options.get('rename', key) - new_info = options.get('newautomountinformation', info) - - if new_key == DIRECT_MAP_KEY and not new_info: - # automountinformation attribute of existing LDAP object needs - # to be retrieved so that RDN can be generated - dn = self.obj.get_dn(*keys, **options) - entry_attrs_ = ldap.get_entry(dn, ['automountinformation']) - new_info = entry_attrs_.get('automountinformation', [])[0] - - # automounkey attribute cannot be overwritten so that get_dn() - # still works right - options['newautomountkey'] = new_key - - new_rdn = self.obj.get_pk(new_key, new_info) - if new_rdn != keys[-1]: - options['rename'] = new_rdn - - result = super(automountkey_mod, self).execute(*keys, **options) - result['value'] = pkey_to_value(options['automountkey'], options) - return result - - -@register() -class automountkey_find(LDAPSearch): - __doc__ = _('Search for an automount key.') - - msg_summary = ngettext( - '%(count)d automount key matched', - '%(count)d automount keys matched', 0 - ) - - -@register() -class automountkey_show(LDAPRetrieve): - __doc__ = _('Display an automount key.') - - takes_options = LDAPRetrieve.takes_options + ( - IA5Str('automountkey', - cli_name='key', - label=_('Key'), - doc=_('Automount key name.'), - ), - IA5Str('automountinformation?', - cli_name='info', - label=_('Mount information'), - ), - ) - - def get_args(self): - for key in self.obj.get_ancestor_primary_keys(): - yield key - - def execute(self, *keys, **options): - keys += (self.obj.get_pk(options['automountkey'], - options.get('automountinformation', None)), ) - options[self.obj.primary_key.name] = self.obj.get_pk( - options['automountkey'], - options.get('automountinformation', None)) - - result = super(automountkey_show, self).execute(*keys, **options) - result['value'] = pkey_to_value(options['automountkey'], options) - return result diff --git a/ipalib/plugins/baseldap.py b/ipalib/plugins/baseldap.py deleted file mode 100644 index bbd8ba146..000000000 --- a/ipalib/plugins/baseldap.py +++ /dev/null @@ -1,2397 +0,0 @@ -# Authors: -# 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/>. -""" -Base classes for LDAP plugins. -""" - -import re -import time -from copy import deepcopy -import base64 - -import six - -from ipalib import api, crud, errors -from ipalib import Method, Object -from ipalib import Flag, Int, Str -from ipalib.cli import to_cli -from ipalib import output -from ipalib.text import _ -from ipalib.util import json_serialize, validate_hostname -from ipalib.capabilities import client_has_capability -from ipalib.messages import add_message, SearchResultTruncated -from ipapython.dn import DN -from ipapython.version import API_VERSION - -if six.PY3: - unicode = str - -DNA_MAGIC = -1 - -global_output_params = ( - Flag('has_password', - label=_('Password'), - ), - Str('member', - label=_('Failed members'), - ), - Str('member_user?', - label=_('Member users'), - ), - Str('member_group?', - label=_('Member groups'), - ), - Str('memberof_group?', - label=_('Member of groups'), - ), - Str('member_host?', - label=_('Member hosts'), - ), - Str('member_hostgroup?', - label=_('Member host-groups'), - ), - Str('memberof_hostgroup?', - label=_('Member of host-groups'), - ), - Str('memberof_permission?', - label=_('Permissions'), - ), - Str('memberof_privilege?', - label='Privileges', - ), - Str('memberof_role?', - label=_('Roles'), - ), - Str('memberof_sudocmdgroup?', - label=_('Sudo Command Groups'), - ), - Str('member_privilege?', - label='Granted to Privilege', - ), - Str('member_role?', - label=_('Granting privilege to roles'), - ), - Str('member_netgroup?', - label=_('Member netgroups'), - ), - Str('memberof_netgroup?', - label=_('Member of netgroups'), - ), - Str('member_service?', - label=_('Member services'), - ), - Str('member_servicegroup?', - label=_('Member service groups'), - ), - Str('memberof_servicegroup?', - label='Member of service groups', - ), - Str('member_hbacsvc?', - label=_('Member HBAC service'), - ), - Str('member_hbacsvcgroup?', - label=_('Member HBAC service groups'), - ), - Str('memberof_hbacsvcgroup?', - label='Member of HBAC service groups', - ), - Str('member_sudocmd?', - label='Member Sudo commands', - ), - Str('memberof_sudorule?', - label='Member of Sudo rule', - ), - Str('memberof_hbacrule?', - label='Member of HBAC rule', - ), - Str('memberindirect_user?', - label=_('Indirect Member users'), - ), - Str('memberindirect_group?', - label=_('Indirect Member groups'), - ), - Str('memberindirect_host?', - label=_('Indirect Member hosts'), - ), - Str('memberindirect_hostgroup?', - label=_('Indirect Member host-groups'), - ), - Str('memberindirect_role?', - label=_('Indirect Member of roles'), - ), - Str('memberindirect_permission?', - label=_('Indirect Member permissions'), - ), - Str('memberindirect_hbacsvc?', - label=_('Indirect Member HBAC service'), - ), - Str('memberindirect_hbacsvcgrp?', - label=_('Indirect Member HBAC service group'), - ), - Str('memberindirect_netgroup?', - label=_('Indirect Member netgroups'), - ), - Str('memberofindirect_group?', - label='Indirect Member of group', - ), - Str('memberofindirect_netgroup?', - label='Indirect Member of netgroup', - ), - Str('memberofindirect_hostgroup?', - label='Indirect Member of host-group', - ), - Str('memberofindirect_role?', - label='Indirect Member of role', - ), - Str('memberofindirect_sudorule?', - label='Indirect Member of Sudo rule', - ), - Str('memberofindirect_hbacrule?', - label='Indirect Member of HBAC rule', - ), - Str('sourcehost', - label=_('Failed source hosts/hostgroups'), - ), - Str('memberhost', - label=_('Failed hosts/hostgroups'), - ), - Str('memberuser', - label=_('Failed users/groups'), - ), - Str('memberservice', - label=_('Failed service/service groups'), - ), - Str('failed', - label=_('Failed to remove'), - flags=['suppress_empty'], - ), - Str('ipasudorunas', - label=_('Failed RunAs'), - ), - Str('ipasudorunasgroup', - label=_('Failed RunAsGroup'), - ), -) - - -def validate_add_attribute(ugettext, attr): - validate_attribute(ugettext, 'addattr', attr) - -def validate_set_attribute(ugettext, attr): - validate_attribute(ugettext, 'setattr', attr) - -def validate_del_attribute(ugettext, attr): - validate_attribute(ugettext, 'delattr', attr) - -def validate_attribute(ugettext, name, attr): - m = re.match("\s*(.*?)\s*=\s*(.*?)\s*$", attr) - if not m or len(m.groups()) != 2: - raise errors.ValidationError( - name=name, error=_('Invalid format. Should be name=value')) - -def get_effective_rights(ldap, dn, attrs=None): - assert isinstance(dn, DN) - if attrs is None: - attrs = ['*', 'nsaccountlock', 'cospriority'] - rights = ldap.get_effective_rights(dn, attrs) - rdict = {} - if 'attributelevelrights' in rights: - rights = rights['attributelevelrights'] - rights = rights[0].split(', ') - for r in rights: - (k,v) = r.split(':') - if v == 'none': - # the string "none" means "no rights found" - # see https://fedorahosted.org/freeipa/ticket/4359 - v = u'' - rdict[k.strip().lower()] = v - - return rdict - -def entry_from_entry(entry, newentry): - """ - Python is more or less pass-by-value except for immutable objects. So if - you pass in a dict to a function you are free to change members of that - dict but you can't create a new dict in the function and expect to replace - what was passed in. - - In some post-op plugins that is exactly what we want to do, so here is a - clumsy way around the problem. - """ - - # Wipe out the current data - for e in list(entry): - del entry[e] - - # Re-populate it with new wentry - for e in newentry.keys(): - entry[e] = newentry[e] - -def entry_to_dict(entry, **options): - if options.get('raw', False): - result = {} - for attr in entry: - if attr.lower() == 'attributelevelrights': - value = entry[attr] - elif entry.conn.get_attribute_type(attr) is bytes: - value = entry.raw[attr] - else: - value = list(entry.raw[attr]) - for (i, v) in enumerate(value): - try: - value[i] = v.decode('utf-8') - except UnicodeDecodeError: - pass - result[attr] = value - else: - result = dict((k.lower(), v) for (k, v) in entry.items()) - if options.get('all', False): - result['dn'] = entry.dn - return result - -def pkey_to_unicode(key): - if key is None: - key = [] - elif not isinstance(key, (tuple, list)): - key = [key] - key = u','.join(unicode(k) for k in key) - return key - -def pkey_to_value(key, options): - version = options.get('version', API_VERSION) - if client_has_capability(version, 'primary_key_types'): - return key - return pkey_to_unicode(key) - -def wait_for_value(ldap, dn, attr, value): - """ - 389-ds postoperation plugins are executed after the data has been - returned to a client. This means that plugins that add data in a - postop are not included in data returned to the user. - - The downside of waiting is that this increases the time of the - command. - - The updated entry is returned. - """ - # Loop a few times to give the postop-plugin a chance to complete - # Don't sleep for more than 6 seconds. - x = 0 - while x < 20: - # sleep first because the first search, even on a quiet system, - # almost always fails. - time.sleep(.3) - x = x + 1 - - # FIXME: put a try/except around here? I think it is probably better - # to just let the exception filter up to the caller. - entry_attrs = ldap.get_entry(dn, ['*']) - if attr in entry_attrs: - if isinstance(entry_attrs[attr], (list, tuple)): - values = [y.lower() for y in entry_attrs[attr]] - if value.lower() in values: - break - else: - if value.lower() == entry_attrs[attr].lower(): - break - - return entry_attrs - - -def validate_externalhost(ugettext, hostname): - try: - validate_hostname(hostname, check_fqdn=False, allow_underscore=True) - except ValueError as e: - return unicode(e) - - -external_host_param = Str('externalhost*', validate_externalhost, - label=_('External host'), - flags=['no_option'], -) - - -def add_external_pre_callback(membertype, ldap, dn, keys, options): - """ - Pre callback to validate external members. - - This should be called by a command pre callback directly. - - membertype is the type of member - """ - assert isinstance(dn, DN) - - # validate hostname with allowed underscore characters, non-fqdn - # hostnames are allowed - def validate_host(hostname): - validate_hostname(hostname, check_fqdn=False, allow_underscore=True) - - if options.get(membertype): - if membertype == 'host': - validator = validate_host - else: - param = api.Object[membertype].primary_key - - def validator(value): - value = param(value) - param.validate(value) - - for value in options[membertype]: - try: - validator(value) - except errors.ValidationError as e: - raise errors.ValidationError(name=membertype, error=e.error) - except ValueError as e: - raise errors.ValidationError(name=membertype, error=e) - return dn - - -def add_external_post_callback(ldap, dn, entry_attrs, failed, completed, - memberattr, membertype, externalattr, - normalize=True): - """ - Takes the following arguments: - failed - the list of failed entries, these are candidates for possible - external entries to add - completed - the number of successfully added entries so far - memberattr - the attribute name that IPA uses for membership natively - (e.g. memberhost) - membertype - the object type of the member (e.g. host) - externalattr - the attribute name that IPA uses to store the membership - of the entries that are not managed by IPA - (e.g externalhost) - - Returns the number of completed entries so far (the number of entries - handled by IPA incremented by the number of handled external entries) and - dn. - """ - assert isinstance(dn, DN) - - completed_external = 0 - - # Sift through the failures. We assume that these are all - # entries that aren't stored in IPA, aka external entries. - if memberattr in failed and membertype in failed[memberattr]: - entry_attrs_ = ldap.get_entry(dn, [externalattr]) - dn = entry_attrs_.dn - members = entry_attrs.get(memberattr, []) - external_entries = entry_attrs_.get(externalattr, []) - lc_external_entries = set(e.lower() for e in external_entries) - - failed_entries = [] - for entry in failed[memberattr][membertype]: - membername = entry[0].lower() - member_dn = api.Object[membertype].get_dn(membername) - assert isinstance(member_dn, DN) - - if (membername not in lc_external_entries and - member_dn not in members): - # Not an IPA entry, assume external - if normalize: - external_entries.append(membername) - else: - external_entries.append(entry[0]) - lc_external_entries.add(membername) - completed_external += 1 - elif (membername in lc_external_entries and - member_dn not in members): - # Already an external member, reset the error message - msg = unicode(errors.AlreadyGroupMember().message) - newerror = (entry[0], msg) - ind = failed[memberattr][membertype].index(entry) - failed[memberattr][membertype][ind] = newerror - failed_entries.append(membername) - else: - # Really a failure - failed_entries.append(membername) - - if completed_external: - entry_attrs_[externalattr] = external_entries - try: - ldap.update_entry(entry_attrs_) - except errors.EmptyModlist: - pass - failed[memberattr][membertype] = failed_entries - entry_attrs[externalattr] = external_entries - - return (completed + completed_external, dn) - - -def remove_external_post_callback(ldap, dn, entry_attrs, failed, completed, - memberattr, membertype, externalattr): - """ - Takes the following arguments: - failed - the list of failed entries, these are candidates for possible - external entries to remove - completed - the number of successfully removed entries so far - memberattr - the attribute name that IPA uses for membership natively - (e.g. memberhost) - membertype - the object type of the member (e.g. host) - externalattr - the attribute name that IPA uses to store the membership - of the entries that are not managed by IPA - (e.g externalhost) - - Returns the number of completed entries so far (the number of entries - handled by IPA incremented by the number of handled external entries) and - dn. - """ - - assert isinstance(dn, DN) - - # Run through the failures and gracefully remove any member defined - # as an external member. - completed_external = 0 - if memberattr in failed and membertype in failed[memberattr]: - entry_attrs_ = ldap.get_entry(dn, [externalattr]) - dn = entry_attrs_.dn - external_entries = entry_attrs_.get(externalattr, []) - failed_entries = [] - - for entry in failed[memberattr][membertype]: - membername = entry[0].lower() - if membername in external_entries or entry[0] in external_entries: - try: - external_entries.remove(membername) - except ValueError: - external_entries.remove(entry[0]) - completed_external += 1 - else: - msg = unicode(errors.NotGroupMember().message) - newerror = (entry[0], msg) - ind = failed[memberattr][membertype].index(entry) - failed[memberattr][membertype][ind] = newerror - failed_entries.append(membername) - - if completed_external: - entry_attrs_[externalattr] = external_entries - try: - ldap.update_entry(entry_attrs_) - except errors.EmptyModlist: - pass - failed[memberattr][membertype] = failed_entries - entry_attrs[externalattr] = external_entries - - return (completed + completed_external, dn) - - -def host_is_master(ldap, fqdn): - """ - Check to see if this host is a master. - - Raises an exception if a master, otherwise returns nothing. - """ - master_dn = DN(('cn', fqdn), ('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), api.env.basedn) - try: - ldap.get_entry(master_dn, ['objectclass']) - raise errors.ValidationError(name='hostname', error=_('An IPA master host cannot be deleted or disabled')) - except errors.NotFound: - # Good, not a master - return - - -def add_missing_object_class(ldap, objectclass, dn, entry_attrs=None, update=True): - """ - Add object class if missing into entry. Fetches entry if not passed. Updates - the entry by default. - - Returns the entry - """ - - if not entry_attrs: - entry_attrs = ldap.get_entry(dn, ['objectclass']) - if (objectclass.lower() not in (o.lower() for o in entry_attrs['objectclass'])): - entry_attrs['objectclass'].append(objectclass) - if update: - ldap.update_entry(entry_attrs) - return entry_attrs - - -class LDAPObject(Object): - """ - Object representing a LDAP entry. - """ - backend_name = 'ldap2' - - parent_object = '' - container_dn = '' - object_name = _('entry') - object_name_plural = _('entries') - object_class = [] - object_class_config = None - # If an objectclass is possible but not default in an entry. Needed for - # collecting attributes for ACI UI. - possible_objectclasses = [] - limit_object_classes = [] # Only attributes in these are allowed - disallow_object_classes = [] # Disallow attributes in these - permission_filter_objectclasses = None - search_attributes = [] - search_attributes_config = None - default_attributes = [] - search_display_attributes = [] # attributes displayed in LDAPSearch - hidden_attributes = ['objectclass', 'aci'] - # set rdn_attribute only if RDN attribute differs from primary key! - rdn_attribute = '' - uuid_attribute = '' - attribute_members = {} - rdn_is_primary_key = False # Do we need RDN change to do a rename? - password_attributes = [] - # Can bind as this entry (has userPassword or krbPrincipalKey) - bindable = False - relationships = { - # attribute: (label, inclusive param prefix, exclusive param prefix) - 'member': ('Member', '', 'no_'), - 'memberof': ('Member Of', 'in_', 'not_in_'), - 'memberindirect': ( - 'Indirect Member', None, 'no_indirect_' - ), - 'memberofindirect': ( - 'Indirect Member Of', None, 'not_in_indirect_' - ), - } - label = _('Entry') - label_singular = _('Entry') - managed_permissions = {} - - container_not_found_msg = _('container entry (%(container)s) not found') - parent_not_found_msg = _('%(parent)s: %(oname)s not found') - object_not_found_msg = _('%(pkey)s: %(oname)s not found') - already_exists_msg = _('%(oname)s with name "%(pkey)s" already exists') - - def get_dn(self, *keys, **kwargs): - if self.parent_object: - parent_dn = self.api.Object[self.parent_object].get_dn(*keys[:-1]) - else: - parent_dn = DN(self.container_dn, api.env.basedn) - if self.rdn_attribute: - try: - entry_attrs = self.backend.find_entry_by_attr( - self.primary_key.name, keys[-1], self.object_class, [''], - DN(self.container_dn, api.env.basedn) - ) - except errors.NotFound: - pass - else: - return entry_attrs.dn - if self.primary_key and keys[-1] is not None: - return self.backend.make_dn_from_attr( - self.primary_key.name, keys[-1], parent_dn - ) - assert isinstance(parent_dn, DN) - return parent_dn - - def get_dn_if_exists(self, *keys, **kwargs): - dn = self.get_dn(*keys, **kwargs) - entry = self.backend.get_entry(dn, ['']) - return entry.dn - - def get_primary_key_from_dn(self, dn): - assert isinstance(dn, DN) - try: - if self.rdn_attribute: - entry_attrs = self.backend.get_entry( - dn, [self.primary_key.name] - ) - try: - return entry_attrs[self.primary_key.name][0] - except (KeyError, IndexError): - return '' - except errors.NotFound: - pass - try: - return dn[self.primary_key.name] - except KeyError: - # The primary key is not in the DN. - # This shouldn't happen, but we don't want a "show" command to - # crash. - # Just return the entire DN, it's all we have if the entry - # doesn't exist - return unicode(dn) - - def get_ancestor_primary_keys(self): - if self.parent_object: - parent_obj = self.api.Object[self.parent_object] - for key in parent_obj.get_ancestor_primary_keys(): - yield key - if parent_obj.primary_key: - pkey = parent_obj.primary_key - yield pkey.clone_rename( - parent_obj.name + pkey.name, required=True, query=True, - cli_name=parent_obj.name, label=pkey.label - ) - - def has_objectclass(self, classes, objectclass): - oc = [x.lower() for x in classes] - return objectclass.lower() in oc - - def convert_attribute_members(self, entry_attrs, *keys, **options): - if options.get('raw', False): - return - - container_dns = {} - new_attrs = {} - - for attr in self.attribute_members: - try: - value = entry_attrs.raw[attr] - except KeyError: - continue - del entry_attrs[attr] - - for member in value: - memberdn = DN(member) - for ldap_obj_name in self.attribute_members[attr]: - ldap_obj = self.api.Object[ldap_obj_name] - try: - container_dn = container_dns[ldap_obj_name] - except KeyError: - container_dn = DN(ldap_obj.container_dn, api.env.basedn) - container_dns[ldap_obj_name] = container_dn - - if memberdn.endswith(container_dn): - new_value = ldap_obj.get_primary_key_from_dn(memberdn) - new_attr_name = '%s_%s' % (attr, ldap_obj.name) - try: - new_attr = new_attrs[new_attr_name] - except KeyError: - new_attr = entry_attrs.setdefault(new_attr_name, []) - new_attrs[new_attr_name] = new_attr - new_attr.append(new_value) - break - - def get_indirect_members(self, entry_attrs, attrs_list): - if 'memberindirect' in attrs_list: - self.get_memberindirect(entry_attrs) - if 'memberofindirect' in attrs_list: - self.get_memberofindirect(entry_attrs) - - def get_memberindirect(self, group_entry): - """ - Get indirect members - """ - - mo_filter = self.backend.make_filter({'memberof': group_entry.dn}) - filter = self.backend.combine_filters( - ('(member=*)', mo_filter), self.backend.MATCH_ALL) - try: - result = self.backend.get_entries( - self.api.env.basedn, - filter=filter, - attrs_list=['member'], - size_limit=-1, # paged search will get everything anyway - paged_search=True) - except errors.NotFound: - result = [] - - indirect = set() - for entry in result: - indirect.update(entry.raw.get('member', [])) - indirect.difference_update(group_entry.raw.get('member', [])) - - if indirect: - group_entry.raw['memberindirect'] = list(indirect) - - def get_memberofindirect(self, entry): - - dn = entry.dn - filter = self.backend.make_filter( - {'member': dn, 'memberuser': dn, 'memberhost': dn}) - try: - result = self.backend.get_entries( - self.api.env.basedn, - filter=filter, - attrs_list=['']) - except errors.NotFound: - result = [] - - direct = set() - indirect = set(entry.raw.get('memberof', [])) - for group_entry in result: - dn = str(group_entry.dn) - if dn in indirect: - indirect.remove(dn) - direct.add(dn) - - entry.raw['memberof'] = list(direct) - if indirect: - entry.raw['memberofindirect'] = list(indirect) - - def get_password_attributes(self, ldap, dn, entry_attrs): - """ - Search on the entry to determine if it has a password or - keytab set. - - A tuple is used to determine which attribute is set - in entry_attrs. The value is set to True/False whether a - given password type is set. - """ - for (pwattr, attr) in self.password_attributes: - search_filter = '(%s=*)' % pwattr - try: - (entries, truncated) = ldap.find_entries( - search_filter, [pwattr], dn, ldap.SCOPE_BASE - ) - entry_attrs[attr] = True - except errors.NotFound: - entry_attrs[attr] = False - - def handle_not_found(self, *keys): - pkey = '' - if self.primary_key: - pkey = keys[-1] - raise errors.NotFound( - reason=self.object_not_found_msg % { - 'pkey': pkey, 'oname': self.object_name, - } - ) - - def handle_duplicate_entry(self, *keys): - try: - pkey = keys[-1] - except IndexError: - pkey = '' - raise errors.DuplicateEntry( - message=self.already_exists_msg % { - 'pkey': pkey, 'oname': self.object_name, - } - ) - - # list of attributes we want exported to JSON - json_friendly_attributes = ( - 'parent_object', 'container_dn', 'object_name', 'object_name_plural', - 'object_class', 'object_class_config', 'default_attributes', 'label', 'label_singular', - 'hidden_attributes', 'uuid_attribute', 'attribute_members', 'name', - 'takes_params', 'rdn_attribute', 'bindable', 'relationships', - ) - - def __json__(self): - ldap = self.backend - json_dict = dict( - (a, json_serialize(getattr(self, a))) for a in self.json_friendly_attributes - ) - if self.primary_key: - json_dict['primary_key'] = self.primary_key.name - objectclasses = self.object_class - if self.object_class_config: - config = ldap.get_ipa_config() - objectclasses = config.get( - self.object_class_config, objectclasses - ) - objectclasses = objectclasses + self.possible_objectclasses - # Get list of available attributes for this object for use - # in the ACI UI. - attrs = self.api.Backend.ldap2.schema.attribute_types(objectclasses) - attrlist = [] - # Go through the MUST first - for (oid, attr) in attrs[0].items(): - attrlist.append(attr.names[0].lower()) - # And now the MAY - for (oid, attr) in attrs[1].items(): - attrlist.append(attr.names[0].lower()) - json_dict['aciattrs'] = attrlist - attrlist.sort() - json_dict['methods'] = [m for m in self.methods] - json_dict['can_have_permissions'] = bool( - self.permission_filter_objectclasses) - return json_dict - - -# addattr can cause parameters to have more than one value even if not defined -# as multivalue, make sure this isn't the case -def _check_single_value_attrs(params, entry_attrs): - for (a, v) in entry_attrs.items(): - if isinstance(v, (list, tuple)) and len(v) > 1: - if a in params and not params[a].multivalue: - raise errors.OnlyOneValueAllowed(attr=a) - -# setattr or --option='' can cause parameters to be empty that are otherwise -# required, make sure we enforce that. -def _check_empty_attrs(params, entry_attrs): - for (a, v) in entry_attrs.items(): - if v is None or (isinstance(v, six.string_types) and len(v) == 0): - if a in params and params[a].required: - raise errors.RequirementError(name=a) - - -def _check_limit_object_class(attributes, attrs, allow_only): - """ - If the set of objectclasses is limited enforce that only those - are updated in entry_attrs (plus dn) - - allow_only tells us what mode to check in: - - If True then we enforce that the attributes must be in the list of - allowed. - - If False then those attributes are not allowed. - """ - if len(attributes[0]) == 0 and len(attributes[1]) == 0: - return - limitattrs = deepcopy(attrs) - # Go through the MUST first - for (oid, attr) in attributes[0].items(): - if attr.names[0].lower() in limitattrs: - if not allow_only: - raise errors.ObjectclassViolation( - info=_('attribute "%(attribute)s" not allowed') % dict( - attribute=attr.names[0].lower())) - limitattrs.remove(attr.names[0].lower()) - # And now the MAY - for (oid, attr) in attributes[1].items(): - if attr.names[0].lower() in limitattrs: - if not allow_only: - raise errors.ObjectclassViolation( - info=_('attribute "%(attribute)s" not allowed') % dict( - attribute=attr.names[0].lower())) - limitattrs.remove(attr.names[0].lower()) - if len(limitattrs) > 0 and allow_only: - raise errors.ObjectclassViolation( - info=_('attribute "%(attribute)s" not allowed') % dict( - attribute=limitattrs[0])) - - -class BaseLDAPCommand(Method): - """ - Base class for Base LDAP Commands. - """ - setattr_option = Str('setattr*', validate_set_attribute, - cli_name='setattr', - doc=_("""Set an attribute to a name/value pair. Format is attr=value. -For multi-valued attributes, the command replaces the values already present."""), - exclude='webui', - ) - addattr_option = Str('addattr*', validate_add_attribute, - cli_name='addattr', - doc=_("""Add an attribute/value pair. Format is attr=value. The attribute -must be part of the schema."""), - exclude='webui', - ) - delattr_option = Str('delattr*', validate_del_attribute, - cli_name='delattr', - doc=_("""Delete an attribute/value pair. The option will be evaluated -last, after all sets and adds."""), - exclude='webui', - ) - - callback_types = Method.callback_types + ('pre', - 'post', - 'exc') - - def get_summary_default(self, output): - if 'value' in output: - output = dict(output) - output['value'] = pkey_to_unicode(output['value']) - return super(BaseLDAPCommand, self).get_summary_default(output) - - def _convert_2_dict(self, ldap, attrs): - """ - Convert a string in the form of name/value pairs into a dictionary. - - :param attrs: A list of name/value pair strings, in the "name=value" - format. May also be a single string, or None. - """ - - newdict = {} - if attrs is None: - attrs = [] - elif not type(attrs) in (list, tuple): - attrs = [attrs] - for a in attrs: - m = re.match("\s*(.*?)\s*=\s*(.*?)\s*$", a) - attr = str(m.group(1)).lower() - value = m.group(2) - if attr in self.obj.params and attr not in self.params: - # The attribute is managed by IPA, but it didn't get cloned - # to the command. This happens with no_update/no_create attrs. - raise errors.ValidationError( - name=attr, error=_('attribute is not configurable')) - if len(value) == 0: - # None means "delete this attribute" - value = None - - if attr in newdict: - if type(value) in (tuple,): - newdict[attr] += list(value) - else: - newdict[attr].append(value) - else: - if type(value) in (tuple,): - newdict[attr] = list(value) - else: - newdict[attr] = [value] - return newdict - - def process_attr_options(self, entry_attrs, dn, keys, options): - """ - Process all --setattr, --addattr, and --delattr options and add the - resulting value to the list of attributes. --setattr is processed first, - then --addattr and finally --delattr. - - When --setattr is not used then the original LDAP object is looked up - (of course, not when dn is None) and the changes are applied to old - object values. - - Attribute values deleted by --delattr may be deleted from attribute - values set or added by --setattr, --addattr. For example, the following - attributes will result in a NOOP: - - --addattr=attribute=foo --delattr=attribute=foo - - AttrValueNotFound exception may be raised when an attribute value was - not found either by --setattr and --addattr nor in existing LDAP object. - - :param entry_attrs: A list of attributes that will be updated - :param dn: dn of updated LDAP object or None if a new object is created - :param keys: List of command arguments - :param options: List of options - """ - - if all(k not in options for k in ("setattr", "addattr", "delattr")): - return - - ldap = self.obj.backend - - adddict = self._convert_2_dict(ldap, options.get('addattr', [])) - setdict = self._convert_2_dict(ldap, options.get('setattr', [])) - deldict = self._convert_2_dict(ldap, options.get('delattr', [])) - - setattrs = set(setdict) - addattrs = set(adddict) - delattrs = set(deldict) - - if dn is None: - direct_add = addattrs - direct_del = delattrs - needldapattrs = [] - else: - assert isinstance(dn, DN) - direct_add = setattrs & addattrs - direct_del = setattrs & delattrs - needldapattrs = list((addattrs | delattrs) - setattrs) - - for attr, val in setdict.items(): - entry_attrs[attr] = val - - for attr in direct_add: - try: - val = entry_attrs[attr] - except KeyError: - val = [] - else: - if not isinstance(val, (list, tuple)): - val = [val] - elif isinstance(val, tuple): - val = list(val) - val.extend(adddict[attr]) - entry_attrs[attr] = val - - for attr in direct_del: - for delval in deldict[attr]: - try: - entry_attrs[attr].remove(delval) - except ValueError: - raise errors.AttrValueNotFound(attr=attr, value=delval) - - if needldapattrs: - try: - old_entry = self._exc_wrapper(keys, options, ldap.get_entry)( - dn, needldapattrs - ) - except errors.NotFound: - self.obj.handle_not_found(*keys) - - # Provide a nice error message when user tries to delete an - # attribute that does not exist on the entry (and user is not - # adding it) - names = set(n.lower() for n in old_entry) - del_nonexisting = delattrs - (names | setattrs | addattrs) - if del_nonexisting: - raise errors.ValidationError(name=del_nonexisting.pop(), - error=_('No such attribute on this entry')) - - for attr in needldapattrs: - entry_attrs[attr] = old_entry.get(attr, []) - - if attr in addattrs: - entry_attrs[attr].extend(adddict.get(attr, [])) - - for delval in deldict.get(attr, []): - try: - entry_attrs[attr].remove(delval) - except ValueError: - if isinstance(delval, bytes): - # This is a Binary value, base64 encode it - delval = unicode(base64.b64encode(delval)) - raise errors.AttrValueNotFound(attr=attr, value=delval) - - # normalize all values - changedattrs = setattrs | addattrs | delattrs - for attr in changedattrs: - if attr in self.params and self.params[attr].attribute: - # convert single-value params to scalars - param = self.params[attr] - value = entry_attrs[attr] - if not param.multivalue: - if len(value) == 1: - value = value[0] - elif not value: - value = None - else: - raise errors.OnlyOneValueAllowed(attr=attr) - # validate, convert and encode params - try: - value = param(value) - param.validate(value) - except errors.ValidationError as err: - raise errors.ValidationError(name=attr, error=err.error) - except errors.ConversionError as err: - raise errors.ConversionError(name=attr, error=err.error) - if isinstance(value, tuple): - value = list(value) - entry_attrs[attr] = value - else: - # unknown attribute: remove duplicite and invalid values - entry_attrs[attr] = list(set([val for val in entry_attrs[attr] if val])) - if not entry_attrs[attr]: - entry_attrs[attr] = None - elif isinstance(entry_attrs[attr], (tuple, list)) and len(entry_attrs[attr]) == 1: - entry_attrs[attr] = entry_attrs[attr][0] - - @classmethod - def register_pre_callback(cls, callback, first=False): - """Shortcut for register_callback('pre', ...)""" - cls.register_callback('pre', callback, first) - - @classmethod - def register_post_callback(cls, callback, first=False): - """Shortcut for register_callback('post', ...)""" - cls.register_callback('post', callback, first) - - @classmethod - def register_exc_callback(cls, callback, first=False): - """Shortcut for register_callback('exc', ...)""" - cls.register_callback('exc', callback, first) - - def _exc_wrapper(self, keys, options, call_func): - """Function wrapper that automatically calls exception callbacks""" - def wrapped(*call_args, **call_kwargs): - # call call_func first - func = call_func - callbacks = list(self.get_callbacks('exc')) - while True: - try: - return func(*call_args, **call_kwargs) - except errors.ExecutionError as exc: - e = exc - if not callbacks: - raise - # call exc_callback in the next loop - callback = callbacks.pop(0) - def exc_func(*args, **kwargs): - return callback( - self, keys, options, e, call_func, *args, **kwargs) - func = exc_func - return wrapped - - def get_options(self): - for param in super(BaseLDAPCommand, self).get_options(): - yield param - if self.obj.attribute_members: - for o in self.has_output: - if isinstance(o, (output.Entry, output.ListOfEntries)): - yield Flag('no_members', - doc=_('Suppress processing of membership attributes.'), - exclude='webui', - flags={'no_output'}, - ) - break - -class LDAPCreate(BaseLDAPCommand, crud.Create): - """ - Create a new entry in LDAP. - """ - takes_options = (BaseLDAPCommand.setattr_option, BaseLDAPCommand.addattr_option) - - def get_args(self): - for key in self.obj.get_ancestor_primary_keys(): - yield key - for arg in super(LDAPCreate, self).get_args(): - yield arg - - has_output_params = global_output_params - - def execute(self, *keys, **options): - ldap = self.obj.backend - - dn = self.obj.get_dn(*keys, **options) - entry_attrs = ldap.make_entry( - dn, self.args_options_2_entry(*keys, **options)) - - self.process_attr_options(entry_attrs, None, keys, options) - - entry_attrs['objectclass'] = deepcopy(self.obj.object_class) - - if self.obj.object_class_config: - config = ldap.get_ipa_config() - entry_attrs['objectclass'] = config.get( - self.obj.object_class_config, entry_attrs['objectclass'] - ) - - if self.obj.uuid_attribute: - entry_attrs[self.obj.uuid_attribute] = 'autogenerate' - - if self.obj.rdn_attribute: - try: - dn_attr = dn[0].attr - except (IndexError, KeyError): - dn_attr = None - if dn_attr != self.obj.primary_key.name: - self.obj.handle_duplicate_entry(*keys) - entry_attrs.dn = ldap.make_dn( - entry_attrs, self.obj.rdn_attribute, - DN(self.obj.container_dn, api.env.basedn)) - - if options.get('all', False): - attrs_list = ['*'] + self.obj.default_attributes - else: - attrs_list = set(self.obj.default_attributes) - attrs_list.update(entry_attrs.keys()) - if options.get('no_members', False): - attrs_list.difference_update(self.obj.attribute_members) - attrs_list = list(attrs_list) - - for callback in self.get_callbacks('pre'): - entry_attrs.dn = callback( - self, ldap, entry_attrs.dn, entry_attrs, attrs_list, - *keys, **options) - - _check_single_value_attrs(self.params, entry_attrs) - _check_limit_object_class(self.api.Backend.ldap2.schema.attribute_types(self.obj.limit_object_classes), list(entry_attrs), allow_only=True) - _check_limit_object_class(self.api.Backend.ldap2.schema.attribute_types(self.obj.disallow_object_classes), list(entry_attrs), allow_only=False) - - try: - self._exc_wrapper(keys, options, ldap.add_entry)(entry_attrs) - except errors.NotFound: - parent = self.obj.parent_object - if parent: - raise errors.NotFound( - reason=self.obj.parent_not_found_msg % { - 'parent': keys[-2], - 'oname': self.api.Object[parent].object_name, - } - ) - raise errors.NotFound( - reason=self.obj.container_not_found_msg % { - 'container': self.obj.container_dn, - } - ) - except errors.DuplicateEntry: - self.obj.handle_duplicate_entry(*keys) - - try: - if self.obj.rdn_attribute: - # make sure objectclass is either set or None - if self.obj.object_class: - object_class = self.obj.object_class - else: - object_class = None - entry_attrs = self._exc_wrapper(keys, options, ldap.find_entry_by_attr)( - self.obj.primary_key.name, keys[-1], object_class, attrs_list, - DN(self.obj.container_dn, api.env.basedn) - ) - else: - entry_attrs = self._exc_wrapper(keys, options, ldap.get_entry)( - entry_attrs.dn, attrs_list) - except errors.NotFound: - self.obj.handle_not_found(*keys) - - self.obj.get_indirect_members(entry_attrs, attrs_list) - - for callback in self.get_callbacks('post'): - entry_attrs.dn = callback( - self, ldap, entry_attrs.dn, entry_attrs, *keys, **options) - - self.obj.convert_attribute_members(entry_attrs, *keys, **options) - - dn = entry_attrs.dn - entry_attrs = entry_to_dict(entry_attrs, **options) - entry_attrs['dn'] = dn - - if self.obj.primary_key: - pkey = keys[-1] - else: - pkey = None - - return dict(result=entry_attrs, value=pkey_to_value(pkey, options)) - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - return dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - return dn - - def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs): - raise exc - - -class LDAPQuery(BaseLDAPCommand, crud.PKQuery): - """ - Base class for commands that need to retrieve an existing entry. - """ - def get_args(self): - for key in self.obj.get_ancestor_primary_keys(): - yield key - for arg in super(LDAPQuery, self).get_args(): - yield arg - - -class LDAPMultiQuery(LDAPQuery): - """ - Base class for commands that need to retrieve one or more existing entries. - """ - takes_options = ( - Flag('continue', - cli_name='continue', - doc=_('Continuous mode: Don\'t stop on errors.'), - ), - ) - - def get_args(self): - for arg in super(LDAPMultiQuery, self).get_args(): - if self.obj.primary_key and arg.name == self.obj.primary_key.name: - yield arg.clone(multivalue=True) - else: - yield arg - - -class LDAPRetrieve(LDAPQuery): - """ - Retrieve an LDAP entry. - """ - has_output = output.standard_entry - has_output_params = global_output_params - - takes_options = ( - Flag('rights', - label=_('Rights'), - doc=_('Display the access rights of this entry (requires --all). See ipa man page for details.'), - ), - ) - - def execute(self, *keys, **options): - ldap = self.obj.backend - - dn = self.obj.get_dn(*keys, **options) - assert isinstance(dn, DN) - - if options.get('all', False): - attrs_list = ['*'] + self.obj.default_attributes - else: - attrs_list = set(self.obj.default_attributes) - if options.get('no_members', False): - attrs_list.difference_update(self.obj.attribute_members) - attrs_list = list(attrs_list) - - for callback in self.get_callbacks('pre'): - dn = callback(self, ldap, dn, attrs_list, *keys, **options) - assert isinstance(dn, DN) - - try: - entry_attrs = self._exc_wrapper(keys, options, ldap.get_entry)( - dn, attrs_list - ) - except errors.NotFound: - self.obj.handle_not_found(*keys) - - self.obj.get_indirect_members(entry_attrs, attrs_list) - - if options.get('rights', False) and options.get('all', False): - entry_attrs['attributelevelrights'] = get_effective_rights( - ldap, entry_attrs.dn) - - for callback in self.get_callbacks('post'): - entry_attrs.dn = callback( - self, ldap, entry_attrs.dn, entry_attrs, *keys, **options) - - self.obj.convert_attribute_members(entry_attrs, *keys, **options) - - dn = entry_attrs.dn - entry_attrs = entry_to_dict(entry_attrs, **options) - entry_attrs['dn'] = dn - - if self.obj.primary_key: - pkey = keys[-1] - else: - pkey = None - - return dict(result=entry_attrs, value=pkey_to_value(pkey, options)) - - def pre_callback(self, ldap, dn, attrs_list, *keys, **options): - assert isinstance(dn, DN) - return dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - return dn - - def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs): - raise exc - - -class LDAPUpdate(LDAPQuery, crud.Update): - """ - Update an LDAP entry. - """ - - takes_options = ( - BaseLDAPCommand.setattr_option, - BaseLDAPCommand.addattr_option, - BaseLDAPCommand.delattr_option, - Flag('rights', - label=_('Rights'), - doc=_('Display the access rights of this entry (requires --all). See ipa man page for details.'), - ), - ) - - has_output_params = global_output_params - - def _get_rename_option(self): - rdnparam = getattr(self.obj.params, self.obj.primary_key.name) - return rdnparam.clone_rename('rename', - cli_name='rename', required=False, label=_('Rename'), - doc=_('Rename the %(ldap_obj_name)s object') % dict( - ldap_obj_name=self.obj.object_name - ) - ) - - def get_options(self): - for option in super(LDAPUpdate, self).get_options(): - yield option - if self.obj.rdn_is_primary_key: - yield self._get_rename_option() - - def execute(self, *keys, **options): - ldap = self.obj.backend - - if len(options) == 2: # 'all' and 'raw' are always sent - raise errors.EmptyModlist() - - dn = self.obj.get_dn(*keys, **options) - entry_attrs = ldap.make_entry(dn, self.args_options_2_entry(**options)) - - self.process_attr_options(entry_attrs, dn, keys, options) - - if options.get('all', False): - attrs_list = ['*'] + self.obj.default_attributes - else: - attrs_list = set(self.obj.default_attributes) - attrs_list.update(entry_attrs.keys()) - if options.get('no_members', False): - attrs_list.difference_update(self.obj.attribute_members) - attrs_list = list(attrs_list) - - _check_single_value_attrs(self.params, entry_attrs) - _check_empty_attrs(self.obj.params, entry_attrs) - - for callback in self.get_callbacks('pre'): - entry_attrs.dn = callback( - self, ldap, entry_attrs.dn, entry_attrs, attrs_list, - *keys, **options) - - _check_limit_object_class(self.api.Backend.ldap2.schema.attribute_types(self.obj.limit_object_classes), list(entry_attrs), allow_only=True) - _check_limit_object_class(self.api.Backend.ldap2.schema.attribute_types(self.obj.disallow_object_classes), list(entry_attrs), allow_only=False) - - rdnupdate = False - try: - if self.obj.rdn_is_primary_key and 'rename' in options: - if not options['rename']: - raise errors.ValidationError(name='rename', error=u'can\'t be empty') - entry_attrs[self.obj.primary_key.name] = options['rename'] - - if self.obj.rdn_is_primary_key and self.obj.primary_key.name in entry_attrs: - try: - # RDN change - new_dn = DN((self.obj.primary_key.name, - entry_attrs[self.obj.primary_key.name]), - *entry_attrs.dn[1:]) - self._exc_wrapper(keys, options, ldap.move_entry)( - entry_attrs.dn, - new_dn) - - rdnkeys = keys[:-1] + (entry_attrs[self.obj.primary_key.name], ) - entry_attrs.dn = self.obj.get_dn(*rdnkeys) - options['rdnupdate'] = True - rdnupdate = True - except errors.EmptyModlist: - # Attempt to rename to the current name, ignore - pass - finally: - # Delete the primary_key from entry_attrs either way - del entry_attrs[self.obj.primary_key.name] - - # Exception callbacks will need to test for options['rdnupdate'] - # to decide what to do. An EmptyModlist in this context doesn't - # mean an error occurred, just that there were no other updates to - # perform. - update = self._exc_wrapper(keys, options, ldap.get_entry)( - entry_attrs.dn, list(entry_attrs)) - update.update(entry_attrs) - - self._exc_wrapper(keys, options, ldap.update_entry)(update) - except errors.EmptyModlist as e: - if not rdnupdate: - raise e - except errors.NotFound: - self.obj.handle_not_found(*keys) - - try: - entry_attrs = self._exc_wrapper(keys, options, ldap.get_entry)( - entry_attrs.dn, attrs_list) - except errors.NotFound: - raise errors.MidairCollision( - format=_('the entry was deleted while being modified') - ) - - self.obj.get_indirect_members(entry_attrs, attrs_list) - - if options.get('rights', False) and options.get('all', False): - entry_attrs['attributelevelrights'] = get_effective_rights( - ldap, entry_attrs.dn) - - for callback in self.get_callbacks('post'): - entry_attrs.dn = callback( - self, ldap, entry_attrs.dn, entry_attrs, *keys, **options) - - self.obj.convert_attribute_members(entry_attrs, *keys, **options) - - entry_attrs = entry_to_dict(entry_attrs, **options) - - if self.obj.primary_key: - pkey = keys[-1] - else: - pkey = None - - return dict(result=entry_attrs, value=pkey_to_value(pkey, options)) - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - return dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - return dn - - def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs): - raise exc - - -class LDAPDelete(LDAPMultiQuery): - """ - Delete an LDAP entry and all of its direct subentries. - """ - has_output = output.standard_multi_delete - - has_output_params = global_output_params - - subtree_delete = True - - def execute(self, *keys, **options): - ldap = self.obj.backend - - def delete_entry(pkey): - nkeys = keys[:-1] + (pkey, ) - dn = self.obj.get_dn(*nkeys, **options) - assert isinstance(dn, DN) - - for callback in self.get_callbacks('pre'): - dn = callback(self, ldap, dn, *nkeys, **options) - assert isinstance(dn, DN) - - def delete_subtree(base_dn): - assert isinstance(base_dn, DN) - truncated = True - while truncated: - try: - (subentries, truncated) = ldap.find_entries( - None, [''], base_dn, ldap.SCOPE_ONELEVEL - ) - except errors.NotFound: - break - else: - for entry_attrs in subentries: - delete_subtree(entry_attrs.dn) - try: - self._exc_wrapper(nkeys, options, ldap.delete_entry)(base_dn) - except errors.NotFound: - self.obj.handle_not_found(*nkeys) - - try: - self._exc_wrapper(nkeys, options, ldap.delete_entry)(dn) - except errors.NotFound: - self.obj.handle_not_found(*nkeys) - except errors.NotAllowedOnNonLeaf: - if not self.subtree_delete: - raise - # this entry is not a leaf entry, delete all child nodes - delete_subtree(dn) - - for callback in self.get_callbacks('post'): - result = callback(self, ldap, dn, *nkeys, **options) - - return result - - if self.obj.primary_key and isinstance(keys[-1], (list, tuple)): - pkeyiter = keys[-1] - elif keys[-1] is not None: - pkeyiter = [keys[-1]] - else: - pkeyiter = [] - - deleted = [] - failed = [] - for pkey in pkeyiter: - try: - delete_entry(pkey) - except errors.ExecutionError: - if not options.get('continue', False): - raise - failed.append(pkey) - else: - deleted.append(pkey) - deleted = pkey_to_value(deleted, options) - failed = pkey_to_value(failed, options) - - return dict(result=dict(failed=failed), value=deleted) - - def pre_callback(self, ldap, dn, *keys, **options): - assert isinstance(dn, DN) - return dn - - def post_callback(self, ldap, dn, *keys, **options): - assert isinstance(dn, DN) - return True - - def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs): - raise exc - - -class LDAPModMember(LDAPQuery): - """ - Base class for member manipulation. - """ - member_attributes = ['member'] - member_param_doc = _('%s') - member_param_label = _('member %s') - member_count_out = ('%i member processed.', '%i members processed.') - - def get_options(self): - for option in super(LDAPModMember, self).get_options(): - yield option - for attr in self.member_attributes: - for ldap_obj_name in self.obj.attribute_members[attr]: - ldap_obj = self.api.Object[ldap_obj_name] - name = to_cli(ldap_obj_name) - doc = self.member_param_doc % ldap_obj.object_name_plural - label = self.member_param_label % ldap_obj.object_name - yield Str('%s*' % name, cli_name='%ss' % name, doc=doc, - label=label, alwaysask=True) - - def get_member_dns(self, **options): - dns = {} - failed = {} - for attr in self.member_attributes: - dns[attr] = {} - failed[attr] = {} - for ldap_obj_name in self.obj.attribute_members[attr]: - dns[attr][ldap_obj_name] = [] - failed[attr][ldap_obj_name] = [] - names = options.get(to_cli(ldap_obj_name), []) - if not names: - continue - for name in names: - if not name: - continue - ldap_obj = self.api.Object[ldap_obj_name] - try: - dns[attr][ldap_obj_name].append(ldap_obj.get_dn(name)) - except errors.PublicError as e: - failed[attr][ldap_obj_name].append((name, unicode(e))) - return (dns, failed) - - -class LDAPAddMember(LDAPModMember): - """ - Add other LDAP entries to members. - """ - member_param_doc = _('%s to add') - member_count_out = ('%i member added.', '%i members added.') - allow_same = False - - has_output = ( - output.Entry('result'), - output.Output('failed', - type=dict, - doc=_('Members that could not be added'), - ), - output.Output('completed', - type=int, - doc=_('Number of members added'), - ), - ) - - has_output_params = global_output_params - - def execute(self, *keys, **options): - ldap = self.obj.backend - - (member_dns, failed) = self.get_member_dns(**options) - - dn = self.obj.get_dn(*keys, **options) - assert isinstance(dn, DN) - - for callback in self.get_callbacks('pre'): - dn = callback(self, ldap, dn, member_dns, failed, *keys, **options) - assert isinstance(dn, DN) - - completed = 0 - for (attr, objs) in member_dns.items(): - for ldap_obj_name in objs: - for m_dn in member_dns[attr][ldap_obj_name]: - assert isinstance(m_dn, DN) - if not m_dn: - continue - try: - ldap.add_entry_to_group(m_dn, dn, attr, allow_same=self.allow_same) - except errors.PublicError as e: - ldap_obj = self.api.Object[ldap_obj_name] - failed[attr][ldap_obj_name].append(( - ldap_obj.get_primary_key_from_dn(m_dn), - unicode(e),) - ) - else: - completed += 1 - - if options.get('all', False): - attrs_list = ['*'] + self.obj.default_attributes - else: - attrs_list = set(self.obj.default_attributes) - attrs_list.update(member_dns.keys()) - if options.get('no_members', False): - attrs_list.difference_update(self.obj.attribute_members) - attrs_list = list(attrs_list) - - try: - entry_attrs = self._exc_wrapper(keys, options, ldap.get_entry)( - dn, attrs_list - ) - except errors.NotFound: - self.obj.handle_not_found(*keys) - - self.obj.get_indirect_members(entry_attrs, attrs_list) - - for callback in self.get_callbacks('post'): - (completed, entry_attrs.dn) = callback( - self, ldap, completed, failed, entry_attrs.dn, entry_attrs, - *keys, **options) - - self.obj.convert_attribute_members(entry_attrs, *keys, **options) - - dn = entry_attrs.dn - entry_attrs = entry_to_dict(entry_attrs, **options) - entry_attrs['dn'] = dn - - return dict( - completed=completed, - failed=failed, - result=entry_attrs, - ) - - def pre_callback(self, ldap, dn, found, not_found, *keys, **options): - assert isinstance(dn, DN) - return dn - - def post_callback(self, ldap, completed, failed, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - return (completed, dn) - - def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs): - raise exc - - -class LDAPRemoveMember(LDAPModMember): - """ - Remove LDAP entries from members. - """ - member_param_doc = _('%s to remove') - member_count_out = ('%i member removed.', '%i members removed.') - - has_output = ( - output.Entry('result'), - output.Output('failed', - type=dict, - doc=_('Members that could not be removed'), - ), - output.Output('completed', - type=int, - doc=_('Number of members removed'), - ), - ) - - has_output_params = global_output_params - - def execute(self, *keys, **options): - ldap = self.obj.backend - - (member_dns, failed) = self.get_member_dns(**options) - - dn = self.obj.get_dn(*keys, **options) - assert isinstance(dn, DN) - - for callback in self.get_callbacks('pre'): - dn = callback(self, ldap, dn, member_dns, failed, *keys, **options) - assert isinstance(dn, DN) - - completed = 0 - for (attr, objs) in member_dns.items(): - for ldap_obj_name, m_dns in objs.items(): - for m_dn in m_dns: - assert isinstance(m_dn, DN) - if not m_dn: - continue - try: - ldap.remove_entry_from_group(m_dn, dn, attr) - except errors.PublicError as e: - ldap_obj = self.api.Object[ldap_obj_name] - failed[attr][ldap_obj_name].append(( - ldap_obj.get_primary_key_from_dn(m_dn), - unicode(e),) - ) - else: - completed += 1 - - if options.get('all', False): - attrs_list = ['*'] + self.obj.default_attributes - else: - attrs_list = set(self.obj.default_attributes) - attrs_list.update(member_dns.keys()) - if options.get('no_members', False): - attrs_list.difference_update(self.obj.attribute_members) - attrs_list = list(attrs_list) - - # Give memberOf a chance to update entries - time.sleep(.3) - - try: - entry_attrs = self._exc_wrapper(keys, options, ldap.get_entry)( - dn, attrs_list - ) - except errors.NotFound: - self.obj.handle_not_found(*keys) - - self.obj.get_indirect_members(entry_attrs, attrs_list) - - for callback in self.get_callbacks('post'): - (completed, entry_attrs.dn) = callback( - self, ldap, completed, failed, entry_attrs.dn, entry_attrs, - *keys, **options) - - self.obj.convert_attribute_members(entry_attrs, *keys, **options) - - dn = entry_attrs.dn - entry_attrs = entry_to_dict(entry_attrs, **options) - entry_attrs['dn'] = dn - - return dict( - completed=completed, - failed=failed, - result=entry_attrs, - ) - - def pre_callback(self, ldap, dn, found, not_found, *keys, **options): - assert isinstance(dn, DN) - return dn - - def post_callback(self, ldap, completed, failed, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - return (completed, dn) - - def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs): - raise exc - - -def gen_pkey_only_option(cli_name): - return Flag('pkey_only?', - label=_('Primary key only'), - doc=_('Results should contain primary key attribute only ("%s")') \ - % to_cli(cli_name),) - -class LDAPSearch(BaseLDAPCommand, crud.Search): - """ - Retrieve all LDAP entries matching the given criteria. - """ - member_attributes = [] - member_param_incl_doc = _('Search for %(searched_object)s with these %(relationship)s %(ldap_object)s.') - member_param_excl_doc = _('Search for %(searched_object)s without these %(relationship)s %(ldap_object)s.') - - # LDAPSearch sorts all matched records in the end using their primary key - # as a key attribute - # Set the following attribute to False to turn sorting off - sort_result_entries = True - - takes_options = ( - Int('timelimit?', - label=_('Time Limit'), - doc=_('Time limit of search in seconds (0 is unlimited)'), - flags=['no_display'], - minvalue=0, - autofill=False, - ), - Int('sizelimit?', - label=_('Size Limit'), - doc=_('Maximum number of entries returned (0 is unlimited)'), - flags=['no_display'], - minvalue=0, - autofill=False, - ), - ) - - def get_args(self): - for key in self.obj.get_ancestor_primary_keys(): - yield key - for arg in super(LDAPSearch, self).get_args(): - yield arg - - def get_member_options(self, attr): - for ldap_obj_name in self.obj.attribute_members[attr]: - ldap_obj = self.api.Object[ldap_obj_name] - relationship = self.obj.relationships.get( - attr, ['member', '', 'no_'] - ) - doc = self.member_param_incl_doc % dict( - searched_object=self.obj.object_name_plural, - relationship=relationship[0].lower(), - ldap_object=ldap_obj.object_name_plural - ) - name = '%s%s' % (relationship[1], to_cli(ldap_obj_name)) - yield Str( - '%s*' % name, cli_name='%ss' % name, doc=doc, - label=ldap_obj.object_name - ) - doc = self.member_param_excl_doc % dict( - searched_object=self.obj.object_name_plural, - relationship=relationship[0].lower(), - ldap_object=ldap_obj.object_name_plural - ) - name = '%s%s' % (relationship[2], to_cli(ldap_obj_name)) - yield Str( - '%s*' % name, cli_name='%ss' % name, doc=doc, - label=ldap_obj.object_name - ) - - def get_options(self): - for option in super(LDAPSearch, self).get_options(): - if option.name == 'no_members': - # no_members are always true for find commands, do not - # show option in CLI but keep API compatibility - option = option.clone( - default=True, flags=option.flags | {"no_option"}) - yield option - if self.obj.primary_key and \ - 'no_output' not in self.obj.primary_key.flags: - yield gen_pkey_only_option(self.obj.primary_key.cli_name) - for attr in self.member_attributes: - for option in self.get_member_options(attr): - yield option - - def get_member_filter(self, ldap, **options): - filter = '' - for attr in self.member_attributes: - for ldap_obj_name in self.obj.attribute_members[attr]: - ldap_obj = self.api.Object[ldap_obj_name] - relationship = self.obj.relationships.get( - attr, ['member', '', 'no_'] - ) - # Handle positive (MATCH_ALL) and negative (MATCH_NONE) - # searches similarly - param_prefixes = relationship[1:] # e.g. ('in_', 'not_in_') - rules = ldap.MATCH_ALL, ldap.MATCH_NONE - for param_prefix, rule in zip(param_prefixes, rules): - param_name = '%s%s' % (param_prefix, to_cli(ldap_obj_name)) - if options.get(param_name): - dns = [] - for pkey in options[param_name]: - dns.append(ldap_obj.get_dn(pkey)) - flt = ldap.make_filter_from_attr(attr, dns, rule) - filter = ldap.combine_filters( - (filter, flt), ldap.MATCH_ALL - ) - return filter - - has_output_params = global_output_params - - def execute(self, *args, **options): - ldap = self.obj.backend - - index = tuple(self.args).index('criteria') - keys = args[:index] - try: - term = args[index] - except IndexError: - term = None - if self.obj.parent_object: - base_dn = self.api.Object[self.obj.parent_object].get_dn(*keys) - else: - base_dn = DN(self.obj.container_dn, api.env.basedn) - assert isinstance(base_dn, DN) - - search_kw = self.args_options_2_entry(**options) - - if self.obj.search_display_attributes: - defattrs = self.obj.search_display_attributes - else: - defattrs = self.obj.default_attributes - - if options.get('pkey_only', False): - attrs_list = [self.obj.primary_key.name] - elif options.get('all', False): - attrs_list = ['*'] + defattrs - else: - attrs_list = set(defattrs) - attrs_list.update(search_kw.keys()) - if options.get('no_members', False): - attrs_list.difference_update(self.obj.attribute_members) - attrs_list = list(attrs_list) - - if self.obj.search_attributes: - search_attrs = self.obj.search_attributes - else: - search_attrs = self.obj.default_attributes - if self.obj.search_attributes_config: - config = ldap.get_ipa_config() - config_attrs = config.get( - self.obj.search_attributes_config, []) - if len(config_attrs) == 1 and ( - isinstance(config_attrs[0], six.string_types)): - search_attrs = config_attrs[0].split(',') - - search_kw['objectclass'] = self.obj.object_class - attr_filter = ldap.make_filter(search_kw, rules=ldap.MATCH_ALL) - - search_kw = {} - for a in search_attrs: - search_kw[a] = term - term_filter = ldap.make_filter(search_kw, exact=False) - - member_filter = self.get_member_filter(ldap, **options) - - filter = ldap.combine_filters( - (term_filter, attr_filter, member_filter), rules=ldap.MATCH_ALL - ) - - scope = ldap.SCOPE_ONELEVEL - for callback in self.get_callbacks('pre'): - (filter, base_dn, scope) = callback( - self, ldap, filter, attrs_list, base_dn, scope, *args, **options) - assert isinstance(base_dn, DN) - - try: - (entries, truncated) = self._exc_wrapper(args, options, ldap.find_entries)( - filter, attrs_list, base_dn, scope, - time_limit=options.get('timelimit', None), - size_limit=options.get('sizelimit', None) - ) - except errors.EmptyResult: - (entries, truncated) = ([], False) - except errors.NotFound: - self.api.Object[self.obj.parent_object].handle_not_found(*keys) - - for callback in self.get_callbacks('post'): - truncated = callback(self, ldap, entries, truncated, *args, **options) - - if self.sort_result_entries: - if self.obj.primary_key: - def sort_key(x): - return self.obj.primary_key.sort_key( - x[self.obj.primary_key.name][0]) - entries.sort(key=sort_key) - - if not options.get('raw', False): - for e in entries: - self.obj.get_indirect_members(e, attrs_list) - self.obj.convert_attribute_members(e, *args, **options) - - for (i, e) in enumerate(entries): - entries[i] = entry_to_dict(e, **options) - entries[i]['dn'] = e.dn - - result = dict( - result=entries, - count=len(entries), - truncated=bool(truncated), - ) - - try: - ldap.handle_truncated_result(truncated) - except errors.LimitsExceeded as e: - add_message(options['version'], result, SearchResultTruncated( - reason=e)) - - return result - - def pre_callback(self, ldap, filters, attrs_list, base_dn, scope, *args, **options): - assert isinstance(base_dn, DN) - return (filters, base_dn, scope) - - def post_callback(self, ldap, entries, truncated, *args, **options): - return truncated - - def exc_callback(self, args, options, exc, call_func, *call_args, **call_kwargs): - raise exc - - -class LDAPModReverseMember(LDAPQuery): - """ - Base class for reverse member manipulation. - """ - reverse_attributes = ['member'] - reverse_param_doc = _('%s') - reverse_count_out = ('%i member processed.', '%i members processed.') - - has_output_params = global_output_params - - def get_options(self): - for option in super(LDAPModReverseMember, self).get_options(): - yield option - for attr in self.reverse_attributes: - for ldap_obj_name in self.obj.reverse_members[attr]: - ldap_obj = self.api.Object[ldap_obj_name] - name = to_cli(ldap_obj_name) - doc = self.reverse_param_doc % ldap_obj.object_name_plural - yield Str('%s*' % name, cli_name='%ss' % name, doc=doc, - label=ldap_obj.object_name, alwaysask=True) - - -class LDAPAddReverseMember(LDAPModReverseMember): - """ - Add other LDAP entries to members in reverse. - - The call looks like "add A to B" but in fact executes - add B to A to handle reverse membership. - """ - member_param_doc = _('%s to add') - member_count_out = ('%i member added.', '%i members added.') - - show_command = None - member_command = None - reverse_attr = None - member_attr = None - - has_output = ( - output.Entry('result'), - output.Output('failed', - type=dict, - doc=_('Members that could not be added'), - ), - output.Output('completed', - type=int, - doc=_('Number of members added'), - ), - ) - - has_output_params = global_output_params - - def execute(self, *keys, **options): - ldap = self.obj.backend - - # Ensure our target exists - result = self.api.Command[self.show_command](keys[-1])['result'] - dn = result['dn'] - assert isinstance(dn, DN) - - for callback in self.get_callbacks('pre'): - dn = callback(self, ldap, dn, *keys, **options) - assert isinstance(dn, DN) - - if options.get('all', False): - attrs_list = ['*'] + self.obj.default_attributes - else: - attrs_list = set(self.obj.default_attributes) - if options.get('no_members', False): - attrs_list.difference_update(self.obj.attribute_members) - attrs_list = list(attrs_list) - - completed = 0 - failed = {'member': {self.reverse_attr: []}} - for attr in options.get(self.reverse_attr) or []: - try: - options = {'%s' % self.member_attr: keys[-1]} - try: - result = self._exc_wrapper(keys, options, self.api.Command[self.member_command])(attr, **options) - if result['completed'] == 1: - completed = completed + 1 - else: - failed['member'][self.reverse_attr].append((attr, result['failed']['member'][self.member_attr][0][1])) - except errors.NotFound as e: - msg = str(e) - (attr, msg) = msg.split(':', 1) - failed['member'][self.reverse_attr].append((attr, unicode(msg.strip()))) - - except errors.PublicError as e: - failed['member'][self.reverse_attr].append((attr, unicode(e))) - - # Update the member data. - entry_attrs = ldap.get_entry(dn, ['*']) - self.obj.convert_attribute_members(entry_attrs, *keys, **options) - - for callback in self.get_callbacks('post'): - (completed, entry_attrs.dn) = callback( - self, ldap, completed, failed, entry_attrs.dn, entry_attrs, - *keys, **options) - - dn = entry_attrs.dn - entry_attrs = entry_to_dict(entry_attrs, **options) - entry_attrs['dn'] = dn - - return dict( - completed=completed, - failed=failed, - result=entry_attrs, - ) - - def pre_callback(self, ldap, dn, *keys, **options): - assert isinstance(dn, DN) - return dn - - def post_callback(self, ldap, completed, failed, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - return (completed, dn) - - def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs): - raise exc - - -class LDAPRemoveReverseMember(LDAPModReverseMember): - """ - Remove other LDAP entries from members in reverse. - - The call looks like "remove A from B" but in fact executes - remove B from A to handle reverse membership. - """ - member_param_doc = _('%s to remove') - member_count_out = ('%i member removed.', '%i members removed.') - - show_command = None - member_command = None - reverse_attr = None - member_attr = None - - has_output = ( - output.Entry('result'), - output.Output('failed', - type=dict, - doc=_('Members that could not be removed'), - ), - output.Output('completed', - type=int, - doc=_('Number of members removed'), - ), - ) - - has_output_params = global_output_params - - def execute(self, *keys, **options): - ldap = self.obj.backend - - # Ensure our target exists - result = self.api.Command[self.show_command](keys[-1])['result'] - dn = result['dn'] - assert isinstance(dn, DN) - - for callback in self.get_callbacks('pre'): - dn = callback(self, ldap, dn, *keys, **options) - assert isinstance(dn, DN) - - if options.get('all', False): - attrs_list = ['*'] + self.obj.default_attributes - else: - attrs_list = set(self.obj.default_attributes) - if options.get('no_members', False): - attrs_list.difference_update(self.obj.attribute_members) - attrs_list = list(attrs_list) - - completed = 0 - failed = {'member': {self.reverse_attr: []}} - for attr in options.get(self.reverse_attr) or []: - try: - options = {'%s' % self.member_attr: keys[-1]} - try: - result = self._exc_wrapper(keys, options, self.api.Command[self.member_command])(attr, **options) - if result['completed'] == 1: - completed = completed + 1 - else: - failed['member'][self.reverse_attr].append((attr, result['failed']['member'][self.member_attr][0][1])) - except errors.NotFound as e: - msg = str(e) - (attr, msg) = msg.split(':', 1) - failed['member'][self.reverse_attr].append((attr, unicode(msg.strip()))) - - except errors.PublicError as e: - failed['member'][self.reverse_attr].append((attr, unicode(e))) - - # Update the member data. - entry_attrs = ldap.get_entry(dn, ['*']) - self.obj.convert_attribute_members(entry_attrs, *keys, **options) - - for callback in self.get_callbacks('post'): - (completed, entry_attrs.dn) = callback( - self, ldap, completed, failed, entry_attrs.dn, entry_attrs, - *keys, **options) - - dn = entry_attrs.dn - entry_attrs = entry_to_dict(entry_attrs, **options) - entry_attrs['dn'] = dn - - return dict( - completed=completed, - failed=failed, - result=entry_attrs, - ) - - def pre_callback(self, ldap, dn, *keys, **options): - assert isinstance(dn, DN) - return dn - - def post_callback(self, ldap, completed, failed, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - return (completed, dn) - - def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs): - raise exc - - -class LDAPModAttribute(LDAPQuery): - - attribute = None - - has_output = output.standard_entry - - def get_options(self): - for option in super(LDAPModAttribute, self).get_options(): - yield option - - option = self.obj.params[self.attribute] - attribute = 'virtual_attribute' not in option.flags - yield option.clone(attribute=attribute, alwaysask=True) - - def _update_attrs(self, update, entry_attrs): - raise NotImplementedError("%s.update_attrs()", self.__class__.__name__) - - def execute(self, *keys, **options): - ldap = self.obj.backend - - dn = self.obj.get_dn(*keys, **options) - entry_attrs = ldap.make_entry(dn, self.args_options_2_entry(**options)) - - if options.get('all', False): - attrs_list = ['*', self.obj.primary_key.name] - else: - attrs_list = {self.obj.primary_key.name} - attrs_list.update(entry_attrs.keys()) - attrs_list = list(attrs_list) - - for callback in self.get_callbacks('pre'): - entry_attrs.dn = callback( - self, ldap, entry_attrs.dn, entry_attrs, attrs_list, - *keys, **options) - - try: - update = self._exc_wrapper(keys, options, ldap.get_entry)( - entry_attrs.dn, list(entry_attrs)) - self._update_attrs(update, entry_attrs) - - self._exc_wrapper(keys, options, ldap.update_entry)(update) - except errors.NotFound: - self.obj.handle_not_found(*keys) - - try: - entry_attrs = self._exc_wrapper(keys, options, ldap.get_entry)( - entry_attrs.dn, attrs_list) - except errors.NotFound: - raise errors.MidairCollision( - format=_('the entry was deleted while being modified') - ) - - for callback in self.get_callbacks('post'): - entry_attrs.dn = callback( - self, ldap, entry_attrs.dn, entry_attrs, *keys, **options) - - entry_attrs = entry_to_dict(entry_attrs, **options) - - if self.obj.primary_key: - pkey = keys[-1] - else: - pkey = None - - return dict(result=entry_attrs, value=pkey_to_value(pkey, options)) - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, - **options): - assert isinstance(dn, DN) - return dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - return dn - - def exc_callback(self, keys, options, exc, call_func, *call_args, - **call_kwargs): - raise exc - - -class LDAPAddAttribute(LDAPModAttribute): - msg_summary = _('added attribute value to entry %(value)') - - def _update_attrs(self, update, entry_attrs): - for name, value in entry_attrs.items(): - old_value = set(update.get(name, [])) - value_to_add = set(value) - - if not old_value.isdisjoint(value_to_add): - raise errors.ExecutionError( - message=_('\'%s\' already contains one or more values' - % name) - ) - - update[name] = list(old_value | value_to_add) - - -class LDAPRemoveAttribute(LDAPModAttribute): - msg_summary = _('removed attribute values from entry %(value)') - - def _update_attrs(self, update, entry_attrs): - for name, value in entry_attrs.items(): - old_value = set(update.get(name, [])) - value_to_remove = set(value) - - if not value_to_remove.issubset(old_value): - raise errors.AttrValueNotFound( - attr=name, value=_("one or more values to remove")) - - update[name] = list(old_value - value_to_remove) diff --git a/ipalib/plugins/baseuser.py b/ipalib/plugins/baseuser.py deleted file mode 100644 index bbea403d9..000000000 --- a/ipalib/plugins/baseuser.py +++ /dev/null @@ -1,663 +0,0 @@ -# Authors: -# Thierry Bordaz <tbordaz@redhat.com> -# -# Copyright (C) 2014 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/>. - -import string - -import six - -from ipalib import api, errors -from ipalib import Flag, Int, Password, Str, Bool, StrEnum, DateTime, Bytes -from ipalib.plugable import Registry -from .baseldap import ( - DN, LDAPObject, LDAPCreate, LDAPUpdate, LDAPSearch, LDAPDelete, - LDAPRetrieve, LDAPAddMember, LDAPRemoveMember) -from .service import validate_certificate -from ipalib.request import context -from ipalib import _ -from ipapython.ipautil import ipa_generate_password -from ipapython.ipavalidate import Email -from ipalib.util import ( - normalize_sshpubkey, - validate_sshpubkey, - convert_sshpubkey_post, - remove_sshpubkey_from_output_post, - remove_sshpubkey_from_output_list_post, - add_sshpubkey_to_attrs_pre, -) - -if six.PY3: - unicode = str - -__doc__ = _(""" -Baseuser - -This contains common definitions for user/stageuser -""") - -register = Registry() - -NO_UPG_MAGIC = '__no_upg__' - -baseuser_output_params = ( - Flag('has_keytab', - label=_('Kerberos keys available'), - ), - Str('sshpubkeyfp*', - label=_('SSH public key fingerprint'), - ), - ) - -status_baseuser_output_params = ( - Str('server', - label=_('Server'), - ), - Str('krbloginfailedcount', - label=_('Failed logins'), - ), - Str('krblastsuccessfulauth', - label=_('Last successful authentication'), - ), - Str('krblastfailedauth', - label=_('Last failed authentication'), - ), - Str('now', - label=_('Time now'), - ), - ) - -UPG_DEFINITION_DN = DN(('cn', 'UPG Definition'), - ('cn', 'Definitions'), - ('cn', 'Managed Entries'), - ('cn', 'etc'), - api.env.basedn) - -# characters to be used for generating random user passwords -baseuser_pwdchars = string.digits + string.ascii_letters + '_,.@+-=' - -def validate_nsaccountlock(entry_attrs): - if 'nsaccountlock' in entry_attrs: - nsaccountlock = entry_attrs['nsaccountlock'] - if not isinstance(nsaccountlock, (bool, Bool)): - if not isinstance(nsaccountlock, six.string_types): - raise errors.OnlyOneValueAllowed(attr='nsaccountlock') - if nsaccountlock.lower() not in ('true', 'false'): - raise errors.ValidationError(name='nsaccountlock', - error=_('must be TRUE or FALSE')) - -def radius_dn2pk(api, entry_attrs): - cl = entry_attrs.get('ipatokenradiusconfiglink', None) - if cl: - pk = api.Object['radiusproxy'].get_primary_key_from_dn(cl[0]) - entry_attrs['ipatokenradiusconfiglink'] = [pk] - -def convert_nsaccountlock(entry_attrs): - if not 'nsaccountlock' in entry_attrs: - entry_attrs['nsaccountlock'] = False - else: - nsaccountlock = Bool('temp') - entry_attrs['nsaccountlock'] = nsaccountlock.convert(entry_attrs['nsaccountlock'][0]) - -def split_principal(principal): - """ - Split the principal into its components and do some basic validation. - - Automatically append our realm if it wasn't provided. - """ - realm = None - parts = principal.split('@') - user = parts[0].lower() - if len(parts) > 2: - raise errors.MalformedUserPrincipal(principal=principal) - - if len(parts) == 2: - realm = parts[1].upper() - # At some point we'll support multiple realms - if realm != api.env.realm: - raise errors.RealmMismatch() - else: - realm = api.env.realm - - return (user, realm) - -def validate_principal(ugettext, principal): - """ - All the real work is done in split_principal. - """ - (user, realm) = split_principal(principal) - return None - -def normalize_principal(principal): - """ - Ensure that the name in the principal is lower-case. The realm is - upper-case by convention but it isn't required. - - The principal is validated at this point. - """ - (user, realm) = split_principal(principal) - return unicode('%s@%s' % (user, realm)) - - - -def fix_addressbook_permission_bindrule(name, template, is_new, - anonymous_read_aci, - **other_options): - """Fix bind rule type for Read User Addressbook/IPA Attributes permission - - When upgrading from an old IPA that had the global read ACI, - or when installing the first replica with granular read permissions, - we need to keep allowing anonymous access to many user attributes. - This fixup_function changes the bind rule type accordingly. - """ - if is_new and anonymous_read_aci: - template['ipapermbindruletype'] = 'anonymous' - - - -class baseuser(LDAPObject): - """ - baseuser object. - """ - - stage_container_dn = api.env.container_stageuser - active_container_dn = api.env.container_user - delete_container_dn = api.env.container_deleteuser - object_class = ['posixaccount'] - object_class_config = 'ipauserobjectclasses' - possible_objectclasses = [ - 'meporiginentry', 'ipauserauthtypeclass', 'ipauser', - 'ipatokenradiusproxyuser' - ] - disallow_object_classes = ['krbticketpolicyaux'] - permission_filter_objectclasses = ['posixaccount'] - search_attributes_config = 'ipausersearchfields' - default_attributes = [ - 'uid', 'givenname', 'sn', 'homedirectory', 'loginshell', - 'uidnumber', 'gidnumber', 'mail', 'ou', - 'telephonenumber', 'title', 'memberof', 'nsaccountlock', - 'memberofindirect', 'ipauserauthtype', 'userclass', - 'ipatokenradiusconfiglink', 'ipatokenradiususername', - 'krbprincipalexpiration', 'usercertificate;binary', - ] - search_display_attributes = [ - 'uid', 'givenname', 'sn', 'homedirectory', 'loginshell', - 'mail', 'telephonenumber', 'title', 'nsaccountlock', - 'uidnumber', 'gidnumber', 'sshpubkeyfp', - ] - uuid_attribute = 'ipauniqueid' - attribute_members = { - 'manager': ['user'], - 'memberof': ['group', 'netgroup', 'role', 'hbacrule', 'sudorule'], - 'memberofindirect': ['group', 'netgroup', 'role', 'hbacrule', 'sudorule'], - } - rdn_is_primary_key = True - bindable = True - password_attributes = [('userpassword', 'has_password'), - ('krbprincipalkey', 'has_keytab')] - label = _('Users') - label_singular = _('User') - - takes_params = ( - Str('uid', - pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?$', - pattern_errmsg='may only include letters, numbers, _, -, . and $', - maxlength=255, - cli_name='login', - label=_('User login'), - primary_key=True, - default_from=lambda givenname, sn: givenname[0] + sn, - normalizer=lambda value: value.lower(), - ), - Str('givenname', - cli_name='first', - label=_('First name'), - ), - Str('sn', - cli_name='last', - label=_('Last name'), - ), - Str('cn', - label=_('Full name'), - default_from=lambda givenname, sn: '%s %s' % (givenname, sn), - autofill=True, - ), - Str('displayname?', - label=_('Display name'), - default_from=lambda givenname, sn: '%s %s' % (givenname, sn), - autofill=True, - ), - Str('initials?', - label=_('Initials'), - default_from=lambda givenname, sn: '%c%c' % (givenname[0], sn[0]), - autofill=True, - ), - Str('homedirectory?', - cli_name='homedir', - label=_('Home directory'), - ), - Str('gecos?', - label=_('GECOS'), - default_from=lambda givenname, sn: '%s %s' % (givenname, sn), - autofill=True, - ), - Str('loginshell?', - cli_name='shell', - label=_('Login shell'), - ), - Str('krbprincipalname?', validate_principal, - cli_name='principal', - label=_('Kerberos principal'), - default_from=lambda uid: '%s@%s' % (uid.lower(), api.env.realm), - autofill=True, - flags=['no_update'], - normalizer=lambda value: normalize_principal(value), - ), - DateTime('krbprincipalexpiration?', - cli_name='principal_expiration', - label=_('Kerberos principal expiration'), - ), - Str('mail*', - cli_name='email', - label=_('Email address'), - ), - Password('userpassword?', - cli_name='password', - label=_('Password'), - doc=_('Prompt to set the user password'), - # FIXME: This is temporary till bug is fixed causing updates to - # bomb out via the webUI. - exclude='webui', - ), - Flag('random?', - doc=_('Generate a random user password'), - flags=('no_search', 'virtual_attribute'), - default=False, - ), - Str('randompassword?', - label=_('Random password'), - flags=('no_create', 'no_update', 'no_search', 'virtual_attribute'), - ), - Int('uidnumber?', - cli_name='uid', - label=_('UID'), - doc=_('User ID Number (system will assign one if not provided)'), - minvalue=1, - ), - Int('gidnumber?', - label=_('GID'), - doc=_('Group ID Number'), - minvalue=1, - ), - Str('street?', - cli_name='street', - label=_('Street address'), - ), - Str('l?', - cli_name='city', - label=_('City'), - ), - Str('st?', - cli_name='state', - label=_('State/Province'), - ), - Str('postalcode?', - label=_('ZIP'), - ), - Str('telephonenumber*', - cli_name='phone', - label=_('Telephone Number') - ), - Str('mobile*', - label=_('Mobile Telephone Number') - ), - Str('pager*', - label=_('Pager Number') - ), - Str('facsimiletelephonenumber*', - cli_name='fax', - label=_('Fax Number'), - ), - Str('ou?', - cli_name='orgunit', - label=_('Org. Unit'), - ), - Str('title?', - label=_('Job Title'), - ), - # keep backward compatibility using single value manager option - Str('manager?', - label=_('Manager'), - ), - Str('carlicense*', - label=_('Car License'), - ), - Str('ipasshpubkey*', validate_sshpubkey, - cli_name='sshpubkey', - label=_('SSH public key'), - normalizer=normalize_sshpubkey, - flags=['no_search'], - ), - StrEnum('ipauserauthtype*', - cli_name='user_auth_type', - label=_('User authentication types'), - doc=_('Types of supported user authentication'), - values=(u'password', u'radius', u'otp'), - ), - Str('userclass*', - cli_name='class', - label=_('Class'), - doc=_('User category (semantics placed on this attribute are for ' - 'local interpretation)'), - ), - Str('ipatokenradiusconfiglink?', - cli_name='radius', - label=_('RADIUS proxy configuration'), - ), - Str('ipatokenradiususername?', - cli_name='radius_username', - label=_('RADIUS proxy username'), - ), - Str('departmentnumber*', - label=_('Department Number'), - ), - Str('employeenumber?', - label=_('Employee Number'), - ), - Str('employeetype?', - label=_('Employee Type'), - ), - Str('preferredlanguage?', - label=_('Preferred Language'), - pattern='^(([a-zA-Z]{1,8}(-[a-zA-Z]{1,8})?(;q\=((0(\.[0-9]{0,3})?)|(1(\.0{0,3})?)))?' \ - + '(\s*,\s*[a-zA-Z]{1,8}(-[a-zA-Z]{1,8})?(;q\=((0(\.[0-9]{0,3})?)|(1(\.0{0,3})?)))?)*)|(\*))$', - pattern_errmsg='must match RFC 2068 - 14.4, e.g., "da, en-gb;q=0.8, en;q=0.7"', - ), - Bytes('usercertificate*', validate_certificate, - cli_name='certificate', - label=_('Certificate'), - doc=_('Base-64 encoded user certificate'), - ), - ) - - def normalize_and_validate_email(self, email, config=None): - if not config: - config = self.backend.get_ipa_config() - - # check if default email domain should be added - defaultdomain = config.get('ipadefaultemaildomain', [None])[0] - if email: - norm_email = [] - if not isinstance(email, (list, tuple)): - email = [email] - for m in email: - if isinstance(m, six.string_types): - if '@' not in m and defaultdomain: - m = m + u'@' + defaultdomain - if not Email(m): - raise errors.ValidationError(name='email', error=_('invalid e-mail format: %(email)s') % dict(email=m)) - norm_email.append(m) - else: - if not Email(m): - raise errors.ValidationError(name='email', error=_('invalid e-mail format: %(email)s') % dict(email=m)) - norm_email.append(m) - return norm_email - - return email - - def normalize_manager(self, manager, container): - """ - Given a userid verify the user's existence (in the appropriate containter) and return the dn. - """ - if not manager: - return None - - if not isinstance(manager, list): - manager = [manager] - - try: - container_dn = DN(container, api.env.basedn) - for i, mgr in enumerate(manager): - if isinstance(mgr, DN) and mgr.endswith(container_dn): - continue - entry_attrs = self.backend.find_entry_by_attr( - self.primary_key.name, mgr, self.object_class, [''], - container_dn - ) - manager[i] = entry_attrs.dn - except errors.NotFound: - raise errors.NotFound(reason=_('manager %(manager)s not found') % dict(manager=mgr)) - - return manager - - def _user_status(self, user, container): - assert isinstance(user, DN) - return user.endswith(container) - - def active_user(self, user): - assert isinstance(user, DN) - return self._user_status(user, DN(self.active_container_dn, api.env.basedn)) - - def stage_user(self, user): - assert isinstance(user, DN) - return self._user_status(user, DN(self.stage_container_dn, api.env.basedn)) - - def delete_user(self, user): - assert isinstance(user, DN) - return self._user_status(user, DN(self.delete_container_dn, api.env.basedn)) - - def convert_usercertificate_pre(self, entry_attrs): - if 'usercertificate' in entry_attrs: - entry_attrs['usercertificate;binary'] = entry_attrs.pop( - 'usercertificate') - - def convert_usercertificate_post(self, entry_attrs, **options): - if 'usercertificate;binary' in entry_attrs: - entry_attrs['usercertificate'] = entry_attrs.pop( - 'usercertificate;binary') - - def convert_attribute_members(self, entry_attrs, *keys, **options): - super(baseuser, self).convert_attribute_members( - entry_attrs, *keys, **options) - - if options.get("raw", False): - return - - # due the backward compatibility, managers have to be returned in - # 'manager' attribute instead of 'manager_user' - try: - entry_attrs['failed_manager'] = entry_attrs.pop('manager') - except KeyError: - pass - - try: - entry_attrs['manager'] = entry_attrs.pop('manager_user') - except KeyError: - pass - - -class baseuser_add(LDAPCreate): - """ - Prototype command plugin to be implemented by real plugin - """ - def pre_common_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, - **options): - assert isinstance(dn, DN) - self.obj.convert_usercertificate_pre(entry_attrs) - - def post_common_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - self.obj.convert_usercertificate_post(entry_attrs, **options) - self.obj.get_password_attributes(ldap, dn, entry_attrs) - convert_sshpubkey_post(entry_attrs) - radius_dn2pk(self.api, entry_attrs) - -class baseuser_del(LDAPDelete): - """ - Prototype command plugin to be implemented by real plugin - """ - -class baseuser_mod(LDAPUpdate): - """ - Prototype command plugin to be implemented by real plugin - """ - def check_namelength(self, ldap, **options): - if options.get('rename') is not None: - config = ldap.get_ipa_config() - if 'ipamaxusernamelength' in config: - if len(options['rename']) > int(config.get('ipamaxusernamelength')[0]): - raise errors.ValidationError( - name=self.obj.primary_key.cli_name, - error=_('can be at most %(len)d characters') % dict( - len = int(config.get('ipamaxusernamelength')[0]) - ) - ) - def check_mail(self, entry_attrs): - if 'mail' in entry_attrs: - entry_attrs['mail'] = self.obj.normalize_and_validate_email(entry_attrs['mail']) - - def check_manager(self, entry_attrs, container): - if 'manager' in entry_attrs: - entry_attrs['manager'] = self.obj.normalize_manager(entry_attrs['manager'], container) - - def check_userpassword(self, entry_attrs, **options): - if 'userpassword' not in entry_attrs and options.get('random'): - entry_attrs['userpassword'] = ipa_generate_password(baseuser_pwdchars) - # save the password so it can be displayed in post_callback - setattr(context, 'randompassword', entry_attrs['userpassword']) - - def check_objectclass(self, ldap, dn, entry_attrs): - if ('ipasshpubkey' in entry_attrs or 'ipauserauthtype' in entry_attrs - or 'userclass' in entry_attrs or 'ipatokenradiusconfiglink' in entry_attrs): - if 'objectclass' in entry_attrs: - obj_classes = entry_attrs['objectclass'] - else: - _entry_attrs = ldap.get_entry(dn, ['objectclass']) - obj_classes = entry_attrs['objectclass'] = _entry_attrs['objectclass'] - - # IMPORTANT: compare objectclasses as case insensitive - obj_classes = [o.lower() for o in obj_classes] - - if 'ipasshpubkey' in entry_attrs and 'ipasshuser' not in obj_classes: - entry_attrs['objectclass'].append('ipasshuser') - - if 'ipauserauthtype' in entry_attrs and 'ipauserauthtypeclass' not in obj_classes: - entry_attrs['objectclass'].append('ipauserauthtypeclass') - - if 'userclass' in entry_attrs and 'ipauser' not in obj_classes: - entry_attrs['objectclass'].append('ipauser') - - if 'ipatokenradiusconfiglink' in entry_attrs: - cl = entry_attrs['ipatokenradiusconfiglink'] - if cl: - if 'ipatokenradiusproxyuser' not in obj_classes: - entry_attrs['objectclass'].append('ipatokenradiusproxyuser') - - answer = self.api.Object['radiusproxy'].get_dn_if_exists(cl) - entry_attrs['ipatokenradiusconfiglink'] = answer - - def pre_common_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, - **options): - assert isinstance(dn, DN) - add_sshpubkey_to_attrs_pre(self.context, attrs_list) - - self.check_namelength(ldap, **options) - - self.check_mail(entry_attrs) - - self.check_manager(entry_attrs, self.obj.active_container_dn) - - self.check_userpassword(entry_attrs, **options) - - self.check_objectclass(ldap, dn, entry_attrs) - self.obj.convert_usercertificate_pre(entry_attrs) - - def post_common_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - if options.get('random', False): - try: - entry_attrs['randompassword'] = unicode(getattr(context, 'randompassword')) - except AttributeError: - # if both randompassword and userpassword options were used - pass - convert_nsaccountlock(entry_attrs) - self.obj.get_password_attributes(ldap, dn, entry_attrs) - self.obj.convert_usercertificate_post(entry_attrs, **options) - convert_sshpubkey_post(entry_attrs) - remove_sshpubkey_from_output_post(self.context, entry_attrs) - radius_dn2pk(self.api, entry_attrs) - -class baseuser_find(LDAPSearch): - """ - Prototype command plugin to be implemented by real plugin - """ - def args_options_2_entry(self, *args, **options): - newoptions = {} - self.common_enhance_options(newoptions, **options) - options.update(newoptions) - - return super(baseuser_find, self).args_options_2_entry( - *args, **options) - - def common_enhance_options(self, newoptions, **options): - # assure the manager attr is a dn, not just a bare uid - manager = options.get('manager') - if manager is not None: - newoptions['manager'] = self.obj.normalize_manager(manager, self.obj.active_container_dn) - - # Ensure that the RADIUS config link is a dn, not just the name - cl = 'ipatokenradiusconfiglink' - if cl in options: - newoptions[cl] = self.api.Object['radiusproxy'].get_dn(options[cl]) - - def pre_common_callback(self, ldap, filters, attrs_list, base_dn, scope, - *args, **options): - add_sshpubkey_to_attrs_pre(self.context, attrs_list) - - def post_common_callback(self, ldap, entries, lockout=False, **options): - for attrs in entries: - self.obj.convert_usercertificate_post(attrs, **options) - if (lockout): - attrs['nsaccountlock'] = True - else: - convert_nsaccountlock(attrs) - convert_sshpubkey_post(attrs) - remove_sshpubkey_from_output_list_post(self.context, entries) - -class baseuser_show(LDAPRetrieve): - """ - Prototype command plugin to be implemented by real plugin - """ - def pre_common_callback(self, ldap, dn, attrs_list, *keys, **options): - assert isinstance(dn, DN) - add_sshpubkey_to_attrs_pre(self.context, attrs_list) - - def post_common_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - self.obj.get_password_attributes(ldap, dn, entry_attrs) - self.obj.convert_usercertificate_post(entry_attrs, **options) - convert_sshpubkey_post(entry_attrs) - remove_sshpubkey_from_output_post(self.context, entry_attrs) - radius_dn2pk(self.api, entry_attrs) - - -class baseuser_add_manager(LDAPAddMember): - member_attributes = ['manager'] - - -class baseuser_remove_manager(LDAPRemoveMember): - member_attributes = ['manager'] diff --git a/ipalib/plugins/batch.py b/ipalib/plugins/batch.py deleted file mode 100644 index 84a650575..000000000 --- a/ipalib/plugins/batch.py +++ /dev/null @@ -1,143 +0,0 @@ -# Authors: -# Adam Young <ayoung@redhat.com> -# Rob Crittenden <rcritten@redhat.com> -# -# Copyright (c) 2010 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/>. - -""" -Plugin to make multiple ipa calls via one remote procedure call - -To run this code in the lite-server - -curl -H "Content-Type:application/json" -H "Accept:application/json" -H "Accept-Language:en" --negotiate -u : --cacert /etc/ipa/ca.crt -d @batch_request.json -X POST http://localhost:8888/ipa/json - -where the contents of the file batch_request.json follow the below example - -{"method":"batch","params":[[ - {"method":"group_find","params":[[],{}]}, - {"method":"user_find","params":[[],{"whoami":"true","all":"true"}]}, - {"method":"user_show","params":[["admin"],{"all":true}]} - ],{}],"id":1} - -The format of the response is nested the same way. At the top you will see - "error": null, - "id": 1, - "result": { - "count": 3, - "results": [ - - -And then a nested response for each IPA command method sent in the request - -""" - -import six - -from ipalib import api, errors -from ipalib import Command -from ipalib.parameters import Str, Any -from ipalib.output import Output -from ipalib.text import _ -from ipalib.request import context -from ipalib.plugable import Registry -from ipapython.version import API_VERSION - -if six.PY3: - unicode = str - -register = Registry() - -@register() -class batch(Command): - NO_CLI = True - - takes_args = ( - Any('methods*', - doc=_('Nested Methods to execute'), - ), - ) - - take_options = ( - Str('version', - cli_name='version', - doc=_('Client version. Used to determine if server will accept request.'), - exclude='webui', - flags=['no_option', 'no_output'], - default=API_VERSION, - autofill=True, - ), - ) - - has_output = ( - Output('count', int, doc=''), - Output('results', (list, tuple), doc='') - ) - - def execute(self, methods=None, **options): - results = [] - for arg in (methods or []): - params = dict() - name = None - try: - if 'method' not in arg: - raise errors.RequirementError(name='method') - if 'params' not in arg: - raise errors.RequirementError(name='params') - name = arg['method'] - if name not in self.Command: - raise errors.CommandError(name=name) - a, kw = arg['params'] - newkw = dict((str(k), v) for k, v in kw.items()) - params = api.Command[name].args_options_2_params(*a, **newkw) - newkw.setdefault('version', options['version']) - - result = api.Command[name](*a, **newkw) - self.info( - '%s: batch: %s(%s): SUCCESS', - getattr(context, 'principal', 'UNKNOWN'), - name, - ', '.join(api.Command[name]._repr_iter(**params)) - ) - result['error']=None - except Exception as e: - if isinstance(e, errors.RequirementError) or \ - isinstance(e, errors.CommandError): - self.info( - '%s: batch: %s', - context.principal, # pylint: disable=no-member - e.__class__.__name__ - ) - else: - self.info( - '%s: batch: %s(%s): %s', - context.principal, name, # pylint: disable=no-member - ', '.join(api.Command[name]._repr_iter(**params)), - e.__class__.__name__ - ) - if isinstance(e, errors.PublicError): - reported_error = e - else: - reported_error = errors.InternalError() - result = dict( - error=reported_error.strerror, - error_code=reported_error.errno, - error_name=unicode(type(reported_error).__name__), - error_kw=reported_error.kw, - ) - results.append(result) - return dict(count=len(results) , results=results) - diff --git a/ipalib/plugins/caacl.py b/ipalib/plugins/caacl.py deleted file mode 100644 index 60eeb5a33..000000000 --- a/ipalib/plugins/caacl.py +++ /dev/null @@ -1,562 +0,0 @@ -# -# Copyright (C) 2015 FreeIPA Contributors see COPYING for license -# - -import pyhbac - -from ipalib import api, errors, output -from ipalib import Bool, Str, StrEnum -from ipalib.plugable import Registry -from .baseldap import ( - LDAPObject, LDAPSearch, LDAPCreate, LDAPDelete, LDAPQuery, - LDAPUpdate, LDAPRetrieve, LDAPAddMember, LDAPRemoveMember, - global_output_params, pkey_to_value) -from .hbacrule import is_all -from .service import normalize_principal, split_any_principal -from ipalib import _, ngettext -from ipapython.dn import DN - - -__doc__ = _(""" -Manage CA ACL rules. - -This plugin is used to define rules governing which principals are -permitted to have certificates issued using a given certificate -profile. - -PROFILE ID SYNTAX: - -A Profile ID is a string without spaces or punctuation starting with a letter -and followed by a sequence of letters, digits or underscore ("_"). - -EXAMPLES: - - Create a CA ACL "test" that grants all users access to the - "UserCert" profile: - ipa caacl-add test --usercat=all - ipa caacl-add-profile test --certprofiles UserCert - - Display the properties of a named CA ACL: - ipa caacl-show test - - Create a CA ACL to let user "alice" use the "DNP3" profile: - ipa caacl-add-profile alice_dnp3 --certprofiles DNP3 - ipa caacl-add-user alice_dnp3 --user=alice - - Disable a CA ACL: - ipa caacl-disable test - - Remove a CA ACL: - ipa caacl-del test -""") - -register = Registry() - - -def _acl_make_request(principal_type, principal, ca_ref, profile_id): - """Construct HBAC request for the given principal, CA and profile""" - service, name, realm = split_any_principal(principal) - - req = pyhbac.HbacRequest() - req.targethost.name = ca_ref - req.service.name = profile_id - if principal_type == 'user': - req.user.name = name - elif principal_type == 'host': - req.user.name = name - elif principal_type == 'service': - req.user.name = normalize_principal(principal) - groups = [] - if principal_type == 'user': - user_obj = api.Command.user_show(name)['result'] - groups = user_obj.get('memberof_group', []) - groups += user_obj.get('memberofindirect_group', []) - elif principal_type == 'host': - host_obj = api.Command.host_show(name)['result'] - groups = host_obj.get('memberof_hostgroup', []) - groups += host_obj.get('memberofindirect_hostgroup', []) - req.user.groups = sorted(set(groups)) - return req - - -def _acl_make_rule(principal_type, obj): - """Turn CA ACL object into HBAC rule. - - ``principal_type`` - String in {'user', 'host', 'service'} - """ - rule = pyhbac.HbacRule(obj['cn'][0]) - rule.enabled = obj['ipaenabledflag'][0] - rule.srchosts.category = {pyhbac.HBAC_CATEGORY_ALL} - - # add CA(s) - # Hardcoded until caacl plugin arrives - rule.targethosts.category = {pyhbac.HBAC_CATEGORY_ALL} - #if 'ipacacategory' in obj and obj['ipacacategory'][0].lower() == 'all': - # rule.targethosts.category = {pyhbac.HBAC_CATEGORY_ALL} - #else: - # rule.targethosts.names = obj.get('ipacaaclcaref', []) - - # add profiles - if ('ipacertprofilecategory' in obj - and obj['ipacertprofilecategory'][0].lower() == 'all'): - rule.services.category = {pyhbac.HBAC_CATEGORY_ALL} - else: - attr = 'ipamembercertprofile_certprofile' - rule.services.names = obj.get(attr, []) - - # add principals and principal's groups - m = {'user': 'group', 'host': 'hostgroup', 'service': None} - category_attr = '{}category'.format(principal_type) - if category_attr in obj and obj[category_attr][0].lower() == 'all': - rule.users.category = {pyhbac.HBAC_CATEGORY_ALL} - else: - principal_attr = 'member{}_{}'.format(principal_type, principal_type) - rule.users.names = obj.get(principal_attr, []) - if m[principal_type] is not None: - group_attr = 'member{}_{}'.format(principal_type, m[principal_type]) - rule.users.groups = obj.get(group_attr, []) - - return rule - - -def acl_evaluate(principal_type, principal, ca_ref, profile_id): - req = _acl_make_request(principal_type, principal, ca_ref, profile_id) - acls = api.Command.caacl_find(no_members=False)['result'] - rules = [_acl_make_rule(principal_type, obj) for obj in acls] - return req.evaluate(rules) == pyhbac.HBAC_EVAL_ALLOW - - -@register() -class caacl(LDAPObject): - """ - CA ACL object. - """ - container_dn = api.env.container_caacl - object_name = _('CA ACL') - object_name_plural = _('CA ACLs') - object_class = ['ipaassociation', 'ipacaacl'] - permission_filter_objectclasses = ['ipacaacl'] - default_attributes = [ - 'cn', 'description', 'ipaenabledflag', - 'ipacacategory', 'ipamemberca', - 'ipacertprofilecategory', 'ipamembercertprofile', - 'usercategory', 'memberuser', - 'hostcategory', 'memberhost', - 'servicecategory', 'memberservice', - ] - uuid_attribute = 'ipauniqueid' - rdn_attribute = 'ipauniqueid' - attribute_members = { - 'memberuser': ['user', 'group'], - 'memberhost': ['host', 'hostgroup'], - 'memberservice': ['service'], - 'ipamembercertprofile': ['certprofile'], - } - managed_permissions = { - 'System: Read CA ACLs': { - 'replaces_global_anonymous_aci': True, - 'ipapermbindruletype': 'all', - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'cn', 'description', 'ipaenabledflag', - 'ipacacategory', 'ipamemberca', - 'ipacertprofilecategory', 'ipamembercertprofile', - 'usercategory', 'memberuser', - 'hostcategory', 'memberhost', - 'servicecategory', 'memberservice', - 'ipauniqueid', - 'objectclass', 'member', - }, - }, - 'System: Add CA ACL': { - 'ipapermright': {'add'}, - 'replaces': [ - '(target = "ldap:///ipauniqueid=*,cn=caacls,cn=ca,$SUFFIX")(version 3.0;acl "permission:Add CA ACL";allow (add) groupdn = "ldap:///cn=Add CA ACL,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'CA Administrator'}, - }, - 'System: Delete CA ACL': { - 'ipapermright': {'delete'}, - 'replaces': [ - '(target = "ldap:///ipauniqueid=*,cn=caacls,cn=ca,$SUFFIX")(version 3.0;acl "permission:Delete CA ACL";allow (delete) groupdn = "ldap:///cn=Delete CA ACL,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'CA Administrator'}, - }, - 'System: Manage CA ACL Membership': { - 'ipapermright': {'write'}, - 'ipapermdefaultattr': { - 'ipacacategory', 'ipamemberca', - 'ipacertprofilecategory', 'ipamembercertprofile', - 'usercategory', 'memberuser', - 'hostcategory', 'memberhost', - 'servicecategory', 'memberservice' - }, - 'replaces': [ - '(targetattr = "ipamemberca || ipamembercertprofile || memberuser || memberservice || memberhost || ipacacategory || ipacertprofilecategory || usercategory || hostcategory || servicecategory")(target = "ldap:///ipauniqueid=*,cn=caacls,cn=ca,$SUFFIX")(version 3.0;acl "permission:Manage CA ACL membership";allow (write) groupdn = "ldap:///cn=Manage CA ACL membership,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'CA Administrator'}, - }, - 'System: Modify CA ACL': { - 'ipapermright': {'write'}, - 'ipapermdefaultattr': { - 'cn', 'description', 'ipaenabledflag', - }, - 'replaces': [ - '(targetattr = "cn || description || ipaenabledflag")(target = "ldap:///ipauniqueid=*,cn=caacls,cn=ca,$SUFFIX")(version 3.0;acl "permission:Modify CA ACL";allow (write) groupdn = "ldap:///cn=Modify CA ACL,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'CA Administrator'}, - }, - } - - label = _('CA ACLs') - label_singular = _('CA ACL') - - takes_params = ( - Str('cn', - cli_name='name', - label=_('ACL name'), - primary_key=True, - ), - Str('description?', - cli_name='desc', - label=_('Description'), - ), - Bool('ipaenabledflag?', - label=_('Enabled'), - flags=['no_option'], - ), - # Commented until subca plugin arrives - #StrEnum('ipacacategory?', - # cli_name='cacat', - # label=_('CA category'), - # doc=_('CA category the ACL applies to'), - # values=(u'all', ), - #), - StrEnum('ipacertprofilecategory?', - cli_name='profilecat', - label=_('Profile category'), - doc=_('Profile category the ACL applies to'), - values=(u'all', ), - ), - StrEnum('usercategory?', - cli_name='usercat', - label=_('User category'), - doc=_('User category the ACL applies to'), - values=(u'all', ), - ), - StrEnum('hostcategory?', - cli_name='hostcat', - label=_('Host category'), - doc=_('Host category the ACL applies to'), - values=(u'all', ), - ), - StrEnum('servicecategory?', - cli_name='servicecat', - label=_('Service category'), - doc=_('Service category the ACL applies to'), - values=(u'all', ), - ), - # Commented until subca plugin arrives - #Str('ipamemberca_subca?', - # label=_('CAs'), - # flags=['no_create', 'no_update', 'no_search'], - #), - Str('ipamembercertprofile_certprofile?', - label=_('Profiles'), - flags=['no_create', 'no_update', 'no_search'], - ), - Str('memberuser_user?', - label=_('Users'), - flags=['no_create', 'no_update', 'no_search'], - ), - Str('memberuser_group?', - label=_('User Groups'), - flags=['no_create', 'no_update', 'no_search'], - ), - Str('memberhost_host?', - label=_('Hosts'), - flags=['no_create', 'no_update', 'no_search'], - ), - Str('memberhost_hostgroup?', - label=_('Host Groups'), - flags=['no_create', 'no_update', 'no_search'], - ), - Str('memberservice_service?', - label=_('Services'), - flags=['no_create', 'no_update', 'no_search'], - ), - ) - - -@register() -class caacl_add(LDAPCreate): - __doc__ = _('Create a new CA ACL.') - - msg_summary = _('Added CA ACL "%(value)s"') - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - # CA ACLs are enabled by default - entry_attrs['ipaenabledflag'] = ['TRUE'] - return dn - - -@register() -class caacl_del(LDAPDelete): - __doc__ = _('Delete a CA ACL.') - - msg_summary = _('Deleted CA ACL "%(value)s"') - - def pre_callback(self, ldap, dn, *keys, **options): - if keys[0] == 'hosts_services_caIPAserviceCert': - raise errors.ProtectedEntryError( - label=_("CA ACL"), - key=keys[0], - reason=_("default CA ACL can be only disabled")) - return dn - - -@register() -class caacl_mod(LDAPUpdate): - __doc__ = _('Modify a CA ACL.') - - msg_summary = _('Modified CA ACL "%(value)s"') - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - try: - entry_attrs = ldap.get_entry(dn, attrs_list) - dn = entry_attrs.dn - except errors.NotFound: - self.obj.handle_not_found(*keys) - - # Commented until subca plugin arrives - #if is_all(options, 'ipacacategory') and 'ipamemberca' in entry_attrs: - # raise errors.MutuallyExclusiveError(reason=_( - # "CA category cannot be set to 'all' " - # "while there are allowed CAs")) - if (is_all(options, 'ipacertprofilecategory') - and 'ipamembercertprofile' in entry_attrs): - raise errors.MutuallyExclusiveError(reason=_( - "profile category cannot be set to 'all' " - "while there are allowed profiles")) - if is_all(options, 'usercategory') and 'memberuser' in entry_attrs: - raise errors.MutuallyExclusiveError(reason=_( - "user category cannot be set to 'all' " - "while there are allowed users")) - if is_all(options, 'hostcategory') and 'memberhost' in entry_attrs: - raise errors.MutuallyExclusiveError(reason=_( - "host category cannot be set to 'all' " - "while there are allowed hosts")) - if is_all(options, 'servicecategory') and 'memberservice' in entry_attrs: - raise errors.MutuallyExclusiveError(reason=_( - "service category cannot be set to 'all' " - "while there are allowed services")) - return dn - - -@register() -class caacl_find(LDAPSearch): - __doc__ = _('Search for CA ACLs.') - - msg_summary = ngettext( - '%(count)d CA ACL matched', '%(count)d CA ACLs matched', 0 - ) - - -@register() -class caacl_show(LDAPRetrieve): - __doc__ = _('Display the properties of a CA ACL.') - - -@register() -class caacl_enable(LDAPQuery): - __doc__ = _('Enable a CA ACL.') - - msg_summary = _('Enabled CA ACL "%(value)s"') - has_output = output.standard_value - - def execute(self, cn, **options): - ldap = self.obj.backend - - dn = self.obj.get_dn(cn) - try: - entry_attrs = ldap.get_entry(dn, ['ipaenabledflag']) - except errors.NotFound: - self.obj.handle_not_found(cn) - - entry_attrs['ipaenabledflag'] = ['TRUE'] - - try: - ldap.update_entry(entry_attrs) - except errors.EmptyModlist: - pass - - return dict( - result=True, - value=pkey_to_value(cn, options), - ) - - -@register() -class caacl_disable(LDAPQuery): - __doc__ = _('Disable a CA ACL.') - - msg_summary = _('Disabled CA ACL "%(value)s"') - has_output = output.standard_value - - def execute(self, cn, **options): - ldap = self.obj.backend - - dn = self.obj.get_dn(cn) - try: - entry_attrs = ldap.get_entry(dn, ['ipaenabledflag']) - except errors.NotFound: - self.obj.handle_not_found(cn) - - entry_attrs['ipaenabledflag'] = ['FALSE'] - - try: - ldap.update_entry(entry_attrs) - except errors.EmptyModlist: - pass - - return dict( - result=True, - value=pkey_to_value(cn, options), - ) - - -@register() -class caacl_add_user(LDAPAddMember): - __doc__ = _('Add users and groups to a CA ACL.') - - member_attributes = ['memberuser'] - member_count_out = ( - _('%i user or group added.'), - _('%i users or groups added.')) - - def pre_callback(self, ldap, dn, found, not_found, *keys, **options): - assert isinstance(dn, DN) - try: - entry_attrs = ldap.get_entry(dn, self.obj.default_attributes) - dn = entry_attrs.dn - except errors.NotFound: - self.obj.handle_not_found(*keys) - if is_all(entry_attrs, 'usercategory'): - raise errors.MutuallyExclusiveError( - reason=_("users cannot be added when user category='all'")) - return dn - - -@register() -class caacl_remove_user(LDAPRemoveMember): - __doc__ = _('Remove users and groups from a CA ACL.') - - member_attributes = ['memberuser'] - member_count_out = ( - _('%i user or group removed.'), - _('%i users or groups removed.')) - - -@register() -class caacl_add_host(LDAPAddMember): - __doc__ = _('Add target hosts and hostgroups to a CA ACL.') - - member_attributes = ['memberhost'] - member_count_out = ( - _('%i host or hostgroup added.'), - _('%i hosts or hostgroups added.')) - - def pre_callback(self, ldap, dn, found, not_found, *keys, **options): - assert isinstance(dn, DN) - try: - entry_attrs = ldap.get_entry(dn, self.obj.default_attributes) - dn = entry_attrs.dn - except errors.NotFound: - self.obj.handle_not_found(*keys) - if is_all(entry_attrs, 'hostcategory'): - raise errors.MutuallyExclusiveError( - reason=_("hosts cannot be added when host category='all'")) - return dn - - -@register() -class caacl_remove_host(LDAPRemoveMember): - __doc__ = _('Remove target hosts and hostgroups from a CA ACL.') - - member_attributes = ['memberhost'] - member_count_out = ( - _('%i host or hostgroup removed.'), - _('%i hosts or hostgroups removed.')) - - -@register() -class caacl_add_service(LDAPAddMember): - __doc__ = _('Add services to a CA ACL.') - - member_attributes = ['memberservice'] - member_count_out = (_('%i service added.'), _('%i services added.')) - - def pre_callback(self, ldap, dn, found, not_found, *keys, **options): - assert isinstance(dn, DN) - try: - entry_attrs = ldap.get_entry(dn, self.obj.default_attributes) - dn = entry_attrs.dn - except errors.NotFound: - self.obj.handle_not_found(*keys) - if is_all(entry_attrs, 'servicecategory'): - raise errors.MutuallyExclusiveError(reason=_( - "services cannot be added when service category='all'")) - return dn - - -@register() -class caacl_remove_service(LDAPRemoveMember): - __doc__ = _('Remove services from a CA ACL.') - - member_attributes = ['memberservice'] - member_count_out = (_('%i service removed.'), _('%i services removed.')) - - -caacl_output_params = global_output_params + ( - Str('ipamembercertprofile', - label=_('Failed profiles'), - ), - # Commented until caacl plugin arrives - #Str('ipamemberca', - # label=_('Failed CAs'), - #), -) - - -@register() -class caacl_add_profile(LDAPAddMember): - __doc__ = _('Add profiles to a CA ACL.') - - has_output_params = caacl_output_params - - member_attributes = ['ipamembercertprofile'] - member_count_out = (_('%i profile added.'), _('%i profiles added.')) - - def pre_callback(self, ldap, dn, found, not_found, *keys, **options): - assert isinstance(dn, DN) - try: - entry_attrs = ldap.get_entry(dn, self.obj.default_attributes) - dn = entry_attrs.dn - except errors.NotFound: - self.obj.handle_not_found(*keys) - if is_all(entry_attrs, 'ipacertprofilecategory'): - raise errors.MutuallyExclusiveError(reason=_( - "profiles cannot be added when profile category='all'")) - return dn - - -@register() -class caacl_remove_profile(LDAPRemoveMember): - __doc__ = _('Remove profiles from a CA ACL.') - - has_output_params = caacl_output_params - - member_attributes = ['ipamembercertprofile'] - member_count_out = (_('%i profile removed.'), _('%i profiles removed.')) diff --git a/ipalib/plugins/cert.py b/ipalib/plugins/cert.py deleted file mode 100644 index cbb5382fb..000000000 --- a/ipalib/plugins/cert.py +++ /dev/null @@ -1,835 +0,0 @@ -# Authors: -# Andrew Wnuk <awnuk@redhat.com> -# Jason Gerard DeRose <jderose@redhat.com> -# John Dennis <jdennis@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/>. - -import os -import time -import binascii - -from ipalib import Command, Str, Int, Flag -from ipalib import api -from ipalib import errors -from ipalib import pkcs10 -from ipalib import x509 -from ipalib import ngettext -from ipalib.plugable import Registry -from .virtual import VirtualCommand -from .baseldap import pkey_to_value -from .service import split_any_principal -from .certprofile import validate_profile_id -from .caacl import acl_evaluate -from ipalib.text import _ -from ipalib.request import context -from ipalib import output -from .service import validate_principal -from ipapython.dn import DN - -import six -import nss.nss as nss -from nss.error import NSPRError -from pyasn1.error import PyAsn1Error - -if six.PY3: - unicode = str - -__doc__ = _(""" -IPA certificate operations - -Implements a set of commands for managing server SSL certificates. - -Certificate requests exist in the form of a Certificate Signing Request (CSR) -in PEM format. - -The dogtag CA uses just the CN value of the CSR and forces the rest of the -subject to values configured in the server. - -A certificate is stored with a service principal and a service principal -needs a host. - -In order to request a certificate: - -* The host must exist -* The service must exist (or you use the --add option to automatically add it) - -SEARCHING: - -Certificates may be searched on by certificate subject, serial number, -revocation reason, validity dates and the issued date. - -When searching on dates the _from date does a >= search and the _to date -does a <= search. When combined these are done as an AND. - -Dates are treated as GMT to match the dates in the certificates. - -The date format is YYYY-mm-dd. - -EXAMPLES: - - Request a new certificate and add the principal: - ipa cert-request --add --principal=HTTP/lion.example.com example.csr - - Retrieve an existing certificate: - ipa cert-show 1032 - - Revoke a certificate (see RFC 5280 for reason details): - ipa cert-revoke --revocation-reason=6 1032 - - Remove a certificate from revocation hold status: - ipa cert-remove-hold 1032 - - Check the status of a signing request: - ipa cert-status 10 - - Search for certificates by hostname: - ipa cert-find --subject=ipaserver.example.com - - Search for revoked certificates by reason: - ipa cert-find --revocation-reason=5 - - Search for certificates based on issuance date - ipa cert-find --issuedon-from=2013-02-01 --issuedon-to=2013-02-07 - -IPA currently immediately issues (or declines) all certificate requests so -the status of a request is not normally useful. This is for future use -or the case where a CA does not immediately issue a certificate. - -The following revocation reasons are supported: - - * 0 - unspecified - * 1 - keyCompromise - * 2 - cACompromise - * 3 - affiliationChanged - * 4 - superseded - * 5 - cessationOfOperation - * 6 - certificateHold - * 8 - removeFromCRL - * 9 - privilegeWithdrawn - * 10 - aACompromise - -Note that reason code 7 is not used. See RFC 5280 for more details: - -http://www.ietf.org/rfc/rfc5280.txt - -""") - -USER, HOST, SERVICE = range(3) - -register = Registry() - -def validate_pkidate(ugettext, value): - """ - A date in the format of %Y-%m-%d - """ - try: - ts = time.strptime(value, '%Y-%m-%d') - except ValueError as e: - return str(e) - - return None - -def validate_csr(ugettext, csr): - """ - Ensure the CSR is base64-encoded and can be decoded by our PKCS#10 - parser. - """ - if api.env.context == 'cli': - # If we are passed in a pointer to a valid file on the client side - # escape and let the load_files() handle things - if csr and os.path.exists(csr): - return - try: - request = pkcs10.load_certificate_request(csr) - except (TypeError, binascii.Error) as e: - raise errors.Base64DecodeError(reason=str(e)) - except Exception as e: - raise errors.CertificateOperationError(error=_('Failure decoding Certificate Signing Request: %s') % e) - -def normalize_csr(csr): - """ - Strip any leading and trailing cruft around the BEGIN/END block - """ - end_len = 37 - s = csr.find('-----BEGIN NEW CERTIFICATE REQUEST-----') - if s == -1: - s = csr.find('-----BEGIN CERTIFICATE REQUEST-----') - e = csr.find('-----END NEW CERTIFICATE REQUEST-----') - if e == -1: - e = csr.find('-----END CERTIFICATE REQUEST-----') - if e != -1: - end_len = 33 - - if s > -1 and e > -1: - # We're normalizing here, not validating - csr = csr[s:e+end_len] - - return csr - -def _convert_serial_number(num): - """ - Convert a SN given in decimal or hexadecimal. - Returns the number or None if conversion fails. - """ - # plain decimal or hexa with radix prefix - try: - num = int(num, 0) - except ValueError: - try: - # hexa without prefix - num = int(num, 16) - except ValueError: - num = None - - return num - -def validate_serial_number(ugettext, num): - if _convert_serial_number(num) == None: - return u"Decimal or hexadecimal number is required for serial number" - return None - -def normalize_serial_number(num): - # It's been already validated - return unicode(_convert_serial_number(num)) - -def get_host_from_principal(principal): - """ - Given a principal with or without a realm return the - host portion. - """ - validate_principal(None, principal) - realm = principal.find('@') - slash = principal.find('/') - if realm == -1: - realm = len(principal) - hostname = principal[slash+1:realm] - - return hostname - -def ca_enabled_check(): - if not api.Command.ca_is_enabled()['result']: - raise errors.NotFound(reason=_('CA is not configured')) - -def caacl_check(principal_type, principal_string, ca, profile_id): - principal_type_map = {USER: 'user', HOST: 'host', SERVICE: 'service'} - if not acl_evaluate( - principal_type_map[principal_type], - principal_string, ca, profile_id): - raise errors.ACIError(info=_( - "Principal '%(principal)s' " - "is not permitted to use CA '%(ca)s' " - "with profile '%(profile_id)s' for certificate issuance." - ) % dict( - principal=principal_string, - ca=ca or '.', - profile_id=profile_id - ) - ) - -@register() -class cert_request(VirtualCommand): - __doc__ = _('Submit a certificate signing request.') - - takes_args = ( - Str( - 'csr', validate_csr, - label=_('CSR'), - cli_name='csr_file', - normalizer=normalize_csr, - noextrawhitespace=False, - ), - ) - operation="request certificate" - - takes_options = ( - Str('principal', - label=_('Principal'), - doc=_('Principal for this certificate (e.g. HTTP/test.example.com)'), - ), - Str('request_type', - default=u'pkcs10', - autofill=True, - ), - Flag('add', - doc=_("automatically add the principal if it doesn't exist"), - default=False, - autofill=True - ), - Str('profile_id?', validate_profile_id, - label=_("Profile ID"), - doc=_("Certificate Profile to use"), - ) - ) - - has_output_params = ( - Str('certificate', - label=_('Certificate'), - ), - Str('subject', - label=_('Subject'), - ), - Str('issuer', - label=_('Issuer'), - ), - Str('valid_not_before', - label=_('Not Before'), - ), - Str('valid_not_after', - label=_('Not After'), - ), - Str('md5_fingerprint', - label=_('Fingerprint (MD5)'), - ), - Str('sha1_fingerprint', - label=_('Fingerprint (SHA1)'), - ), - Str('serial_number', - label=_('Serial number'), - ), - Str('serial_number_hex', - label=_('Serial number (hex)'), - ), - ) - - has_output = ( - output.Output('result', - type=dict, - doc=_('Dictionary mapping variable name to value'), - ), - ) - - def execute(self, csr, **kw): - ca_enabled_check() - - ldap = self.api.Backend.ldap2 - add = kw.get('add') - request_type = kw.get('request_type') - profile_id = kw.get('profile_id', self.Backend.ra.DEFAULT_PROFILE) - ca = '.' # top-level CA hardcoded until subca plugin implemented - - """ - Access control is partially handled by the ACI titled - 'Hosts can modify service userCertificate'. This is for the case - where a machine binds using a host/ prinicpal. It can only do the - request if the target hostname is in the managedBy attribute which - is managed using the add/del member commands. - - Binding with a user principal one needs to be in the request_certs - taskgroup (directly or indirectly via role membership). - """ - - principal_string = kw.get('principal') - principal = split_any_principal(principal_string) - servicename, principal_name, realm = principal - if servicename is None: - principal_type = USER - elif servicename == 'host': - principal_type = HOST - else: - principal_type = SERVICE - - bind_principal = split_any_principal(getattr(context, 'principal')) - bind_service, bind_name, bind_realm = bind_principal - - if bind_service is None: - bind_principal_type = USER - elif bind_service == 'host': - bind_principal_type = HOST - else: - bind_principal_type = SERVICE - - if bind_principal != principal and bind_principal_type != HOST: - # Can the bound principal request certs for another principal? - self.check_access() - - try: - self.check_access("request certificate ignore caacl") - bypass_caacl = True - except errors.ACIError: - bypass_caacl = False - - if not bypass_caacl: - caacl_check(principal_type, principal_string, ca, profile_id) - - try: - subject = pkcs10.get_subject(csr) - extensions = pkcs10.get_extensions(csr) - subjectaltname = pkcs10.get_subjectaltname(csr) or () - except (NSPRError, PyAsn1Error, ValueError) as e: - raise errors.CertificateOperationError( - error=_("Failure decoding Certificate Signing Request: %s") % e) - - # self-service and host principals may bypass SAN permission check - if bind_principal != principal and bind_principal_type != HOST: - if '2.5.29.17' in extensions: - self.check_access('request certificate with subjectaltname') - - dn = None - principal_obj = None - # See if the service exists and punt if it doesn't and we aren't - # going to add it - try: - if principal_type == SERVICE: - principal_obj = api.Command['service_show'](principal_string, all=True) - elif principal_type == HOST: - principal_obj = api.Command['host_show'](principal_name, all=True) - elif principal_type == USER: - principal_obj = api.Command['user_show'](principal_name, all=True) - except errors.NotFound as e: - if principal_type == SERVICE and add: - principal_obj = api.Command['service_add'](principal_string, force=True) - else: - raise errors.NotFound( - reason=_("The principal for this request doesn't exist.")) - principal_obj = principal_obj['result'] - dn = principal_obj['dn'] - - # Ensure that the DN in the CSR matches the principal - cn = subject.common_name #pylint: disable=E1101 - if not cn: - raise errors.ValidationError(name='csr', - error=_("No Common Name was found in subject of request.")) - - if principal_type in (SERVICE, HOST): - if cn.lower() != principal_name.lower(): - raise errors.ACIError( - info=_("hostname in subject of request '%(cn)s' " - "does not match principal hostname '%(hostname)s'") - % dict(cn=cn, hostname=principal_name)) - elif principal_type == USER: - # check user name - if cn != principal_name: - raise errors.ValidationError( - name='csr', - error=_("DN commonName does not match user's login") - ) - - # check email address - mail = subject.email_address #pylint: disable=E1101 - if mail is not None and mail not in principal_obj.get('mail', []): - raise errors.ValidationError( - name='csr', - error=_( - "DN emailAddress does not match " - "any of user's email addresses") - ) - - # We got this far so the principal entry exists, can we write it? - if not ldap.can_write(dn, "usercertificate"): - raise errors.ACIError(info=_("Insufficient 'write' privilege " - "to the 'userCertificate' attribute of entry '%s'.") % dn) - - # Validate the subject alt name, if any - for name_type, name in subjectaltname: - if name_type == pkcs10.SAN_DNSNAME: - name = unicode(name) - alt_principal_obj = None - alt_principal_string = None - try: - if principal_type == HOST: - alt_principal_string = 'host/%s@%s' % (name, realm) - alt_principal_obj = api.Command['host_show'](name, all=True) - elif principal_type == SERVICE: - alt_principal_string = '%s/%s@%s' % (servicename, name, realm) - alt_principal_obj = api.Command['service_show']( - alt_principal_string, all=True) - elif principal_type == USER: - raise errors.ValidationError( - name='csr', - error=_("subject alt name type %s is forbidden " - "for user principals") % name_type - ) - except errors.NotFound: - # We don't want to issue any certificates referencing - # machines we don't know about. Nothing is stored in this - # host record related to this certificate. - raise errors.NotFound(reason=_('The service principal for ' - 'subject alt name %s in certificate request does not ' - 'exist') % name) - if alt_principal_obj is not None: - altdn = alt_principal_obj['result']['dn'] - if not ldap.can_write(altdn, "usercertificate"): - raise errors.ACIError(info=_( - "Insufficient privilege to create a certificate " - "with subject alt name '%s'.") % name) - if alt_principal_string is not None and not bypass_caacl: - caacl_check( - principal_type, alt_principal_string, ca, profile_id) - elif name_type in (pkcs10.SAN_OTHERNAME_KRB5PRINCIPALNAME, - pkcs10.SAN_OTHERNAME_UPN): - if split_any_principal(name) != principal: - raise errors.ACIError( - info=_("Principal '%s' in subject alt name does not " - "match requested principal") % name) - elif name_type == pkcs10.SAN_RFC822NAME: - if principal_type == USER: - if name not in principal_obj.get('mail', []): - raise errors.ValidationError( - name='csr', - error=_( - "RFC822Name does not match " - "any of user's email addresses") - ) - else: - raise errors.ValidationError( - name='csr', - error=_("subject alt name type %s is forbidden " - "for non-user principals") % name_type - ) - else: - raise errors.ACIError( - info=_("Subject alt name type %s is forbidden") % - name_type) - - # Request the certificate - result = self.Backend.ra.request_certificate( - csr, profile_id, request_type=request_type) - cert = x509.load_certificate(result['certificate']) - result['issuer'] = unicode(cert.issuer) - result['valid_not_before'] = unicode(cert.valid_not_before_str) - result['valid_not_after'] = unicode(cert.valid_not_after_str) - result['md5_fingerprint'] = unicode(nss.data_to_hex(nss.md5_digest(cert.der_data), 64)[0]) - result['sha1_fingerprint'] = unicode(nss.data_to_hex(nss.sha1_digest(cert.der_data), 64)[0]) - - # Success? Then add it to the principal's entry - # (unless the profile tells us not to) - profile = api.Command['certprofile_show'](profile_id) - store = profile['result']['ipacertprofilestoreissued'][0] == 'TRUE' - if store and 'certificate' in result: - cert = str(result.get('certificate')) - kwargs = dict(addattr=u'usercertificate={}'.format(cert)) - if principal_type == SERVICE: - api.Command['service_mod'](principal_string, **kwargs) - elif principal_type == HOST: - api.Command['host_mod'](principal_name, **kwargs) - elif principal_type == USER: - api.Command['user_mod'](principal_name, **kwargs) - - return dict( - result=result - ) - - - -@register() -class cert_status(VirtualCommand): - __doc__ = _('Check the status of a certificate signing request.') - - takes_args = ( - Str('request_id', - label=_('Request id'), - flags=['no_create', 'no_update', 'no_search'], - ), - ) - has_output_params = ( - Str('cert_request_status', - label=_('Request status'), - ), - ) - operation = "certificate status" - - - def execute(self, request_id, **kw): - ca_enabled_check() - self.check_access() - return dict( - result=self.Backend.ra.check_request_status(request_id) - ) - - - -_serial_number = Str('serial_number', - validate_serial_number, - label=_('Serial number'), - doc=_('Serial number in decimal or if prefixed with 0x in hexadecimal'), - normalizer=normalize_serial_number, -) - -@register() -class cert_show(VirtualCommand): - __doc__ = _('Retrieve an existing certificate.') - - takes_args = _serial_number - - has_output_params = ( - Str('certificate', - label=_('Certificate'), - ), - Str('subject', - label=_('Subject'), - ), - Str('issuer', - label=_('Issuer'), - ), - Str('valid_not_before', - label=_('Not Before'), - ), - Str('valid_not_after', - label=_('Not After'), - ), - Str('md5_fingerprint', - label=_('Fingerprint (MD5)'), - ), - Str('sha1_fingerprint', - label=_('Fingerprint (SHA1)'), - ), - Str('revocation_reason', - label=_('Revocation reason'), - ), - Str('serial_number_hex', - label=_('Serial number (hex)'), - ), - ) - - takes_options = ( - Str('out?', - label=_('Output filename'), - doc=_('File to store the certificate in.'), - exclude='webui', - ), - ) - - operation="retrieve certificate" - - def execute(self, serial_number, **options): - ca_enabled_check() - hostname = None - try: - self.check_access() - except errors.ACIError as acierr: - self.debug("Not granted by ACI to retrieve certificate, looking at principal") - bind_principal = getattr(context, 'principal') - if not bind_principal.startswith('host/'): - raise acierr - hostname = get_host_from_principal(bind_principal) - - result=self.Backend.ra.get_certificate(serial_number) - cert = x509.load_certificate(result['certificate']) - result['subject'] = unicode(cert.subject) - result['issuer'] = unicode(cert.issuer) - result['valid_not_before'] = unicode(cert.valid_not_before_str) - result['valid_not_after'] = unicode(cert.valid_not_after_str) - result['md5_fingerprint'] = unicode(nss.data_to_hex(nss.md5_digest(cert.der_data), 64)[0]) - result['sha1_fingerprint'] = unicode(nss.data_to_hex(nss.sha1_digest(cert.der_data), 64)[0]) - if hostname: - # If we have a hostname we want to verify that the subject - # of the certificate matches it, otherwise raise an error - if hostname != cert.subject.common_name: #pylint: disable=E1101 - raise acierr - - return dict(result=result) - - - - -@register() -class cert_revoke(VirtualCommand): - __doc__ = _('Revoke a certificate.') - - takes_args = _serial_number - - has_output_params = ( - Flag('revoked', - label=_('Revoked'), - ), - ) - operation = "revoke certificate" - - # FIXME: The default is 0. Is this really an Int param? - takes_options = ( - Int('revocation_reason', - label=_('Reason'), - doc=_('Reason for revoking the certificate (0-10). Type ' - '"ipa help cert" for revocation reason details. '), - minvalue=0, - maxvalue=10, - default=0, - autofill=True - ), - ) - - def execute(self, serial_number, **kw): - ca_enabled_check() - hostname = None - try: - self.check_access() - except errors.ACIError as acierr: - self.debug("Not granted by ACI to revoke certificate, looking at principal") - try: - # Let cert_show() handle verifying that the subject of the - # cert we're dealing with matches the hostname in the principal - result = api.Command['cert_show'](unicode(serial_number))['result'] - except errors.NotImplementedError: - pass - revocation_reason = kw['revocation_reason'] - if revocation_reason == 7: - raise errors.CertificateOperationError(error=_('7 is not a valid revocation reason')) - return dict( - result=self.Backend.ra.revoke_certificate( - serial_number, revocation_reason=revocation_reason) - ) - - - -@register() -class cert_remove_hold(VirtualCommand): - __doc__ = _('Take a revoked certificate off hold.') - - takes_args = _serial_number - - has_output_params = ( - Flag('unrevoked', - label=_('Unrevoked'), - ), - Str('error_string', - label=_('Error'), - ), - ) - operation = "certificate remove hold" - - def execute(self, serial_number, **kw): - ca_enabled_check() - self.check_access() - return dict( - result=self.Backend.ra.take_certificate_off_hold(serial_number) - ) - - - -@register() -class cert_find(Command): - __doc__ = _('Search for existing certificates.') - - takes_options = ( - Str('subject?', - label=_('Subject'), - doc=_('Subject'), - autofill=False, - ), - Int('revocation_reason?', - label=_('Reason'), - doc=_('Reason for revoking the certificate (0-10). Type ' - '"ipa help cert" for revocation reason details.'), - minvalue=0, - maxvalue=10, - autofill=False, - ), - Int('min_serial_number?', - doc=_("minimum serial number"), - autofill=False, - minvalue=0, - maxvalue=2147483647, - ), - Int('max_serial_number?', - doc=_("maximum serial number"), - autofill=False, - minvalue=0, - maxvalue=2147483647, - ), - Flag('exactly?', - doc=_('match the common name exactly'), - autofill=False, - ), - Str('validnotafter_from?', validate_pkidate, - doc=_('Valid not after from this date (YYYY-mm-dd)'), - autofill=False, - ), - Str('validnotafter_to?', validate_pkidate, - doc=_('Valid not after to this date (YYYY-mm-dd)'), - autofill=False, - ), - Str('validnotbefore_from?', validate_pkidate, - doc=_('Valid not before from this date (YYYY-mm-dd)'), - autofill=False, - ), - Str('validnotbefore_to?', validate_pkidate, - doc=_('Valid not before to this date (YYYY-mm-dd)'), - autofill=False, - ), - Str('issuedon_from?', validate_pkidate, - doc=_('Issued on from this date (YYYY-mm-dd)'), - autofill=False, - ), - Str('issuedon_to?', validate_pkidate, - doc=_('Issued on to this date (YYYY-mm-dd)'), - autofill=False, - ), - Str('revokedon_from?', validate_pkidate, - doc=_('Revoked on from this date (YYYY-mm-dd)'), - autofill=False, - ), - Str('revokedon_to?', validate_pkidate, - doc=_('Revoked on to this date (YYYY-mm-dd)'), - autofill=False, - ), - Int('sizelimit?', - label=_('Size Limit'), - doc=_('Maximum number of certs returned'), - flags=['no_display'], - minvalue=0, - default=100, - ), - ) - - has_output = output.standard_list_of_entries - has_output_params = ( - Str('serial_number_hex', - label=_('Serial number (hex)'), - ), - Str('serial_number', - label=_('Serial number'), - ), - Str('status', - label=_('Status'), - ), - ) - - msg_summary = ngettext( - '%(count)d certificate matched', '%(count)d certificates matched', 0 - ) - - def execute(self, **options): - ca_enabled_check() - ret = dict( - result=self.Backend.ra.find(options) - ) - ret['count'] = len(ret['result']) - ret['truncated'] = False - return ret - - -@register() -class ca_is_enabled(Command): - """ - Checks if any of the servers has the CA service enabled. - """ - NO_CLI = True - has_output = output.standard_value - - def execute(self, *args, **options): - base_dn = DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), - self.api.env.basedn) - filter = '(&(objectClass=ipaConfigObject)(cn=CA))' - try: - self.api.Backend.ldap2.find_entries( - base_dn=base_dn, filter=filter, attrs_list=[]) - except errors.NotFound: - result = False - else: - result = True - return dict(result=result, value=pkey_to_value(None, options)) diff --git a/ipalib/plugins/certprofile.py b/ipalib/plugins/certprofile.py deleted file mode 100644 index 6f314e1a4..000000000 --- a/ipalib/plugins/certprofile.py +++ /dev/null @@ -1,335 +0,0 @@ -# -# Copyright (C) 2015 FreeIPA Contributors see COPYING for license -# - -import re - -from ipalib import api, Bool, Str -from ipalib.plugable import Registry -from .baseldap import ( - LDAPObject, LDAPSearch, LDAPCreate, - LDAPDelete, LDAPUpdate, LDAPRetrieve) -from ipalib.request import context -from ipalib import ngettext -from ipalib.text import _ -from ipapython.dogtag import INCLUDED_PROFILES -from ipapython.version import API_VERSION - -from ipalib import errors - - -__doc__ = _(""" -Manage Certificate Profiles - -Certificate Profiles are used by Certificate Authority (CA) in the signing of -certificates to determine if a Certificate Signing Request (CSR) is acceptable, -and if so what features and extensions will be present on the certificate. - -The Certificate Profile format is the property-list format understood by the -Dogtag or Red Hat Certificate System CA. - -PROFILE ID SYNTAX: - -A Profile ID is a string without spaces or punctuation starting with a letter -and followed by a sequence of letters, digits or underscore ("_"). - -EXAMPLES: - - Import a profile that will not store issued certificates: - ipa certprofile-import ShortLivedUserCert \\ - --file UserCert.profile --desc "User Certificates" \\ - --store=false - - Delete a certificate profile: - ipa certprofile-del ShortLivedUserCert - - Show information about a profile: - ipa certprofile-show ShortLivedUserCert - - Save profile configuration to a file: - ipa certprofile-show caIPAserviceCert --out caIPAserviceCert.cfg - - Search for profiles that do not store certificates: - ipa certprofile-find --store=false - -PROFILE CONFIGURATION FORMAT: - -The profile configuration format is the raw property-list format -used by Dogtag Certificate System. The XML format is not supported. - -The following restrictions apply to profiles managed by FreeIPA: - -- When importing a profile the "profileId" field, if present, must - match the ID given on the command line. - -- The "classId" field must be set to "caEnrollImpl" - -- The "auth.instance_id" field must be set to "raCertAuth" - -- The "certReqInputImpl" input class and "certOutputImpl" output - class must be used. - -""") - - -register = Registry() - - -def ca_enabled_check(): - """Raise NotFound if CA is not enabled. - - This function is defined in multiple plugins to avoid circular imports - (cert depends on certprofile, so we cannot import cert here). - - """ - if not api.Command.ca_is_enabled()['result']: - raise errors.NotFound(reason=_('CA is not configured')) - - -profile_id_pattern = re.compile('^[a-zA-Z]\w*$') - - -def validate_profile_id(ugettext, value): - """Ensure profile ID matches form required by CA.""" - if profile_id_pattern.match(value) is None: - return _('invalid Profile ID') - else: - return None - - -@register() -class certprofile(LDAPObject): - """ - Certificate Profile object. - """ - container_dn = api.env.container_certprofile - object_name = _('Certificate Profile') - object_name_plural = _('Certificate Profiles') - object_class = ['ipacertprofile'] - default_attributes = [ - 'cn', 'description', 'ipacertprofilestoreissued' - ] - search_attributes = [ - 'cn', 'description', 'ipacertprofilestoreissued' - ] - label = _('Certificate Profiles') - label_singular = _('Certificate Profile') - - takes_params = ( - Str('cn', validate_profile_id, - primary_key=True, - cli_name='id', - label=_('Profile ID'), - doc=_('Profile ID for referring to this profile'), - ), - Str('description', - required=True, - cli_name='desc', - label=_('Profile description'), - doc=_('Brief description of this profile'), - ), - Bool('ipacertprofilestoreissued', - default=True, - cli_name='store', - label=_('Store issued certificates'), - doc=_('Whether to store certs issued using this profile'), - ), - ) - - permission_filter_objectclasses = ['ipacertprofile'] - managed_permissions = { - 'System: Read Certificate Profiles': { - 'replaces_global_anonymous_aci': True, - 'ipapermbindruletype': 'all', - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'cn', - 'description', - 'ipacertprofilestoreissued', - 'objectclass', - }, - }, - 'System: Import Certificate Profile': { - 'ipapermright': {'add'}, - 'replaces': [ - '(target = "ldap:///cn=*,cn=certprofiles,cn=ca,$SUFFIX")(version 3.0;acl "permission:Import Certificate Profile";allow (add) groupdn = "ldap:///cn=Import Certificate Profile,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'CA Administrator'}, - }, - 'System: Delete Certificate Profile': { - 'ipapermright': {'delete'}, - 'replaces': [ - '(target = "ldap:///cn=*,cn=certprofiles,cn=ca,$SUFFIX")(version 3.0;acl "permission:Delete Certificate Profile";allow (delete) groupdn = "ldap:///cn=Delete Certificate Profile,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'CA Administrator'}, - }, - 'System: Modify Certificate Profile': { - 'ipapermright': {'write'}, - 'ipapermdefaultattr': { - 'cn', - 'description', - 'ipacertprofilestoreissued', - }, - 'replaces': [ - '(targetattr = "cn || description || ipacertprofilestoreissued")(target = "ldap:///cn=*,cn=certprofiles,cn=ca,$SUFFIX")(version 3.0;acl "permission:Modify Certificate Profile";allow (write) groupdn = "ldap:///cn=Modify Certificate Profile,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'CA Administrator'}, - }, - } - - - -@register() -class certprofile_find(LDAPSearch): - __doc__ = _("Search for Certificate Profiles.") - msg_summary = ngettext( - '%(count)d profile matched', '%(count)d profiles matched', 0 - ) - - def execute(self, *args, **kwargs): - ca_enabled_check() - return super(certprofile_find, self).execute(*args, **kwargs) - - -@register() -class certprofile_show(LDAPRetrieve): - __doc__ = _("Display the properties of a Certificate Profile.") - - has_output_params = LDAPRetrieve.has_output_params + ( - Str('config', - label=_('Profile configuration'), - ), - ) - - takes_options = LDAPRetrieve.takes_options + ( - Str('out?', - doc=_('Write profile configuration to file'), - ), - ) - - def execute(self, *keys, **options): - ca_enabled_check() - result = super(certprofile_show, self).execute(*keys, **options) - - if 'out' in options: - with self.api.Backend.ra_certprofile as profile_api: - result['result']['config'] = profile_api.read_profile(keys[0]) - - return result - - -@register() -class certprofile_import(LDAPCreate): - __doc__ = _("Import a Certificate Profile.") - msg_summary = _('Imported profile "%(value)s"') - takes_options = ( - Str( - 'file', - label=_('Filename of a raw profile. The XML format is not supported.'), - cli_name='file', - flags=('virtual_attribute',), - noextrawhitespace=False, - ), - ) - - PROFILE_ID_PATTERN = re.compile('^profileId=([a-zA-Z]\w*)', re.MULTILINE) - - def pre_callback(self, ldap, dn, entry, entry_attrs, *keys, **options): - ca_enabled_check() - context.profile = options['file'] - - match = self.PROFILE_ID_PATTERN.search(options['file']) - if match is None: - # no profileId found, use CLI value as profileId. - context.profile = u'profileId=%s\n%s' % (keys[0], context.profile) - elif keys[0] != match.group(1): - raise errors.ValidationError(name='file', - error=_("Profile ID '%(cli_value)s' does not match profile data '%(file_value)s'") - % {'cli_value': keys[0], 'file_value': match.group(1)} - ) - return dn - - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - """Import the profile into Dogtag and enable it. - - If the operation fails, remove the LDAP entry. - """ - try: - with self.api.Backend.ra_certprofile as profile_api: - profile_api.create_profile(context.profile) - profile_api.enable_profile(keys[0]) - except: - # something went wrong ; delete entry - ldap.delete_entry(dn) - raise - - return dn - - -@register() -class certprofile_del(LDAPDelete): - __doc__ = _("Delete a Certificate Profile.") - msg_summary = _('Deleted profile "%(value)s"') - - def pre_callback(self, ldap, dn, *keys, **options): - ca_enabled_check() - - if keys[0] in [p.profile_id for p in INCLUDED_PROFILES]: - raise errors.ValidationError(name='profile_id', - error=_("Predefined profile '%(profile_id)s' cannot be deleted") - % {'profile_id': keys[0]} - ) - - return dn - - def post_callback(self, ldap, dn, *keys, **options): - with self.api.Backend.ra_certprofile as profile_api: - profile_api.disable_profile(keys[0]) - profile_api.delete_profile(keys[0]) - return dn - - -@register() -class certprofile_mod(LDAPUpdate): - __doc__ = _("Modify Certificate Profile configuration.") - msg_summary = _('Modified Certificate Profile "%(value)s"') - - takes_options = LDAPUpdate.takes_options + ( - Str( - 'file?', - label=_('File containing profile configuration'), - cli_name='file', - flags=('virtual_attribute',), - noextrawhitespace=False, - ), - ) - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - ca_enabled_check() - # Once a profile id is set it cannot be changed - if 'cn' in entry_attrs: - raise errors.ProtectedEntryError(label='certprofile', key=keys[0], - reason=_('Certificate profiles cannot be renamed')) - if 'file' in options: - with self.api.Backend.ra_certprofile as profile_api: - profile_api.disable_profile(keys[0]) - try: - profile_api.update_profile(keys[0], options['file']) - finally: - profile_api.enable_profile(keys[0]) - - return dn - - def execute(self, *keys, **options): - try: - return super(certprofile_mod, self).execute(*keys, **options) - except errors.EmptyModlist: - if 'file' in options: - # The profile data in Dogtag was updated. - # Do not fail; return result of certprofile-show instead - return self.api.Command.certprofile_show(keys[0], - version=API_VERSION) - else: - # This case is actually an error; re-raise - raise diff --git a/ipalib/plugins/config.py b/ipalib/plugins/config.py deleted file mode 100644 index 46a40ddf7..000000000 --- a/ipalib/plugins/config.py +++ /dev/null @@ -1,358 +0,0 @@ -# Authors: -# Rob Crittenden <rcritten@redhat.com> -# Pavel Zuna <pzuna@redhat.com> -# -# Copyright (C) 2008 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/>. - -from ipalib import api -from ipalib import Bool, Int, Str, IA5Str, StrEnum, DNParam -from ipalib import errors -from ipalib.plugable import Registry -from .baseldap import ( - LDAPObject, - LDAPUpdate, - LDAPRetrieve) -from .selinuxusermap import validate_selinuxuser -from ipalib import _ -from ipapython.dn import DN - -# 389-ds attributes that should be skipped in attribute checks -OPERATIONAL_ATTRIBUTES = ('nsaccountlock', 'member', 'memberof', - 'memberindirect', 'memberofindirect',) - -__doc__ = _(""" -Server configuration - -Manage the default values that IPA uses and some of its tuning parameters. - -NOTES: - -The password notification value (--pwdexpnotify) is stored here so it will -be replicated. It is not currently used to notify users in advance of an -expiring password. - -Some attributes are read-only, provided only for information purposes. These -include: - -Certificate Subject base: the configured certificate subject base, - e.g. O=EXAMPLE.COM. This is configurable only at install time. -Password plug-in features: currently defines additional hashes that the - password will generate (there may be other conditions). - -When setting the order list for mapping SELinux users you may need to -quote the value so it isn't interpreted by the shell. - -EXAMPLES: - - Show basic server configuration: - ipa config-show - - Show all configuration options: - ipa config-show --all - - Change maximum username length to 99 characters: - ipa config-mod --maxusername=99 - - Increase default time and size limits for maximum IPA server search: - ipa config-mod --searchtimelimit=10 --searchrecordslimit=2000 - - Set default user e-mail domain: - ipa config-mod --emaildomain=example.com - - Enable migration mode to make "ipa migrate-ds" command operational: - ipa config-mod --enable-migration=TRUE - - Define SELinux user map order: - ipa config-mod --ipaselinuxusermaporder='guest_u:s0$xguest_u:s0$user_u:s0-s0:c0.c1023$staff_u:s0-s0:c0.c1023$unconfined_u:s0-s0:c0.c1023' -""") - -register = Registry() - -@register() -class config(LDAPObject): - """ - IPA configuration object - """ - object_name = _('configuration options') - default_attributes = [ - 'ipamaxusernamelength', 'ipahomesrootdir', 'ipadefaultloginshell', - 'ipadefaultprimarygroup', 'ipadefaultemaildomain', 'ipasearchtimelimit', - 'ipasearchrecordslimit', 'ipausersearchfields', 'ipagroupsearchfields', - 'ipamigrationenabled', 'ipacertificatesubjectbase', - 'ipapwdexpadvnotify', 'ipaselinuxusermaporder', - 'ipaselinuxusermapdefault', 'ipaconfigstring', 'ipakrbauthzdata', - 'ipauserauthtype' - ] - container_dn = DN(('cn', 'ipaconfig'), ('cn', 'etc')) - permission_filter_objectclasses = ['ipaguiconfig'] - managed_permissions = { - 'System: Read Global Configuration': { - 'replaces_global_anonymous_aci': True, - 'ipapermbindruletype': 'all', - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'cn', 'objectclass', - 'ipacertificatesubjectbase', 'ipaconfigstring', - 'ipadefaultemaildomain', 'ipadefaultloginshell', - 'ipadefaultprimarygroup', 'ipagroupobjectclasses', - 'ipagroupsearchfields', 'ipahomesrootdir', - 'ipakrbauthzdata', 'ipamaxusernamelength', - 'ipamigrationenabled', 'ipapwdexpadvnotify', - 'ipaselinuxusermapdefault', 'ipaselinuxusermaporder', - 'ipasearchrecordslimit', 'ipasearchtimelimit', - 'ipauserauthtype', 'ipauserobjectclasses', - 'ipausersearchfields', 'ipacustomfields', - }, - }, - } - - label = _('Configuration') - label_singular = _('Configuration') - - takes_params = ( - Int('ipamaxusernamelength', - cli_name='maxusername', - label=_('Maximum username length'), - minvalue=1, - maxvalue=255, - ), - IA5Str('ipahomesrootdir', - cli_name='homedirectory', - label=_('Home directory base'), - doc=_('Default location of home directories'), - ), - Str('ipadefaultloginshell', - cli_name='defaultshell', - label=_('Default shell'), - doc=_('Default shell for new users'), - ), - Str('ipadefaultprimarygroup', - cli_name='defaultgroup', - label=_('Default users group'), - doc=_('Default group for new users'), - ), - Str('ipadefaultemaildomain?', - cli_name='emaildomain', - label=_('Default e-mail domain'), - doc=_('Default e-mail domain'), - ), - Int('ipasearchtimelimit', - cli_name='searchtimelimit', - label=_('Search time limit'), - doc=_('Maximum amount of time (seconds) for a search (-1 or 0 is unlimited)'), - minvalue=-1, - ), - Int('ipasearchrecordslimit', - cli_name='searchrecordslimit', - label=_('Search size limit'), - doc=_('Maximum number of records to search (-1 or 0 is unlimited)'), - minvalue=-1, - ), - IA5Str('ipausersearchfields', - cli_name='usersearch', - label=_('User search fields'), - doc=_('A comma-separated list of fields to search in when searching for users'), - ), - IA5Str('ipagroupsearchfields', - cli_name='groupsearch', - label='Group search fields', - doc=_('A comma-separated list of fields to search in when searching for groups'), - ), - Bool('ipamigrationenabled', - cli_name='enable_migration', - label=_('Enable migration mode'), - doc=_('Enable migration mode'), - ), - DNParam('ipacertificatesubjectbase', - cli_name='subject', - label=_('Certificate Subject base'), - doc=_('Base for certificate subjects (OU=Test,O=Example)'), - flags=['no_update'], - ), - Str('ipagroupobjectclasses+', - cli_name='groupobjectclasses', - label=_('Default group objectclasses'), - doc=_('Default group objectclasses (comma-separated list)'), - ), - Str('ipauserobjectclasses+', - cli_name='userobjectclasses', - label=_('Default user objectclasses'), - doc=_('Default user objectclasses (comma-separated list)'), - ), - Int('ipapwdexpadvnotify', - cli_name='pwdexpnotify', - label=_('Password Expiration Notification (days)'), - doc=_('Number of days\'s notice of impending password expiration'), - minvalue=0, - ), - StrEnum('ipaconfigstring*', - cli_name='ipaconfigstring', - label=_('Password plugin features'), - doc=_('Extra hashes to generate in password plug-in'), - values=(u'AllowNThash', - u'KDC:Disable Last Success', u'KDC:Disable Lockout', - u'KDC:Disable Default Preauth for SPNs'), - ), - Str('ipaselinuxusermaporder', - label=_('SELinux user map order'), - doc=_('Order in increasing priority of SELinux users, delimited by $'), - ), - Str('ipaselinuxusermapdefault?', - label=_('Default SELinux user'), - doc=_('Default SELinux user when no match is found in SELinux map rule'), - ), - StrEnum('ipakrbauthzdata*', - cli_name='pac_type', - label=_('Default PAC types'), - doc=_('Default types of PAC supported for services'), - values=(u'MS-PAC', u'PAD', u'nfs:NONE'), - ), - StrEnum('ipauserauthtype*', - cli_name='user_auth_type', - label=_('Default user authentication types'), - doc=_('Default types of supported user authentication'), - values=(u'password', u'radius', u'otp', u'disabled'), - ), - ) - - def get_dn(self, *keys, **kwargs): - return DN(('cn', 'ipaconfig'), ('cn', 'etc'), api.env.basedn) - - - -@register() -class config_mod(LDAPUpdate): - __doc__ = _('Modify configuration options.') - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - if 'ipadefaultprimarygroup' in entry_attrs: - group=entry_attrs['ipadefaultprimarygroup'] - try: - api.Object['group'].get_dn_if_exists(group) - except errors.NotFound: - raise errors.NotFound(message=_("The group doesn't exist")) - kw = {} - if 'ipausersearchfields' in entry_attrs: - kw['ipausersearchfields'] = 'ipauserobjectclasses' - if 'ipagroupsearchfields' in entry_attrs: - kw['ipagroupsearchfields'] = 'ipagroupobjectclasses' - if kw: - config = ldap.get_ipa_config(list(kw.values())) - for (k, v) in kw.items(): - allowed_attrs = ldap.get_allowed_attributes(config[v]) - fields = entry_attrs[k].split(',') - for a in fields: - a = a.strip() - a, tomato, olive = a.partition(';') - if a not in allowed_attrs: - raise errors.ValidationError( - name=k, error=_('attribute "%s" not allowed') % a - ) - - # Set ipasearchrecordslimit to -1 if 0 is used - if 'ipasearchrecordslimit' in entry_attrs: - if entry_attrs['ipasearchrecordslimit'] is 0: - entry_attrs['ipasearchrecordslimit'] = -1 - - # Set ipasearchtimelimit to -1 if 0 is used - if 'ipasearchtimelimit' in entry_attrs: - if entry_attrs['ipasearchtimelimit'] is 0: - entry_attrs['ipasearchtimelimit'] = -1 - - for (attr, obj) in (('ipauserobjectclasses', 'user'), - ('ipagroupobjectclasses', 'group')): - if attr in entry_attrs: - if not entry_attrs[attr]: - raise errors.ValidationError(name=attr, - error=_('May not be empty')) - objectclasses = list(set(entry_attrs[attr]).union( - self.api.Object[obj].possible_objectclasses)) - new_allowed_attrs = ldap.get_allowed_attributes(objectclasses, - raise_on_unknown=True) - checked_attrs = self.api.Object[obj].default_attributes - if self.api.Object[obj].uuid_attribute: - checked_attrs = checked_attrs + [self.api.Object[obj].uuid_attribute] - for obj_attr in checked_attrs: - obj_attr, tomato, olive = obj_attr.partition(';') - if obj_attr in OPERATIONAL_ATTRIBUTES: - continue - if obj_attr in self.api.Object[obj].params and \ - 'virtual_attribute' in \ - self.api.Object[obj].params[obj_attr].flags: - # skip virtual attributes - continue - if obj_attr not in new_allowed_attrs: - raise errors.ValidationError(name=attr, - error=_('%(obj)s default attribute %(attr)s would not be allowed!') \ - % dict(obj=obj, attr=obj_attr)) - - if ('ipaselinuxusermapdefault' in entry_attrs or - 'ipaselinuxusermaporder' in entry_attrs): - config = None - failedattr = 'ipaselinuxusermaporder' - - if 'ipaselinuxusermapdefault' in entry_attrs: - defaultuser = entry_attrs['ipaselinuxusermapdefault'] - failedattr = 'ipaselinuxusermapdefault' - - # validate the new default user first - if defaultuser is not None: - error_message = validate_selinuxuser(_, defaultuser) - - if error_message: - raise errors.ValidationError(name='ipaselinuxusermapdefault', - error=error_message) - - else: - config = ldap.get_ipa_config() - defaultuser = config.get('ipaselinuxusermapdefault', [None])[0] - - if 'ipaselinuxusermaporder' in entry_attrs: - order = entry_attrs['ipaselinuxusermaporder'] - userlist = order.split('$') - - # validate the new user order first - for user in userlist: - if not user: - raise errors.ValidationError(name='ipaselinuxusermaporder', - error=_('A list of SELinux users delimited by $ expected')) - - error_message = validate_selinuxuser(_, user) - if error_message: - error_message = _("SELinux user '%(user)s' is not " - "valid: %(error)s") % dict(user=user, - error=error_message) - raise errors.ValidationError(name='ipaselinuxusermaporder', - error=error_message) - else: - if not config: - config = ldap.get_ipa_config() - order = config['ipaselinuxusermaporder'] - userlist = order[0].split('$') - if defaultuser and defaultuser not in userlist: - raise errors.ValidationError(name=failedattr, - error=_('SELinux user map default user not in order list')) - - return dn - - - -@register() -class config_show(LDAPRetrieve): - __doc__ = _('Show the current configuration.') - diff --git a/ipalib/plugins/delegation.py b/ipalib/plugins/delegation.py deleted file mode 100644 index 0443f0e48..000000000 --- a/ipalib/plugins/delegation.py +++ /dev/null @@ -1,226 +0,0 @@ -# Authors: -# Rob Crittenden <rcritten@redhat.com> -# Martin Kosek <mkosek@redhat.com> -# -# Copyright (C) 2010 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/>. - -from ipalib import _, ngettext -from ipalib import Str -from ipalib import api, crud -from ipalib import output -from ipalib import Object -from ipalib.plugable import Registry -from .baseldap import gen_pkey_only_option, pkey_to_value - -__doc__ = _(""" -Group to Group Delegation - -A permission enables fine-grained delegation of permissions. Access Control -Rules, or instructions (ACIs), grant permission to permissions to perform -given tasks such as adding a user, modifying a group, etc. - -Group to Group Delegations grants the members of one group to update a set -of attributes of members of another group. - -EXAMPLES: - - Add a delegation rule to allow managers to edit employee's addresses: - ipa delegation-add --attrs=street --group=managers --membergroup=employees "managers edit employees' street" - - When managing the list of attributes you need to include all attributes - in the list, including existing ones. Add postalCode to the list: - ipa delegation-mod --attrs=street --attrs=postalCode --group=managers --membergroup=employees "managers edit employees' street" - - Display our updated rule: - ipa delegation-show "managers edit employees' street" - - Delete a rule: - ipa delegation-del "managers edit employees' street" -""") - -register = Registry() - -ACI_PREFIX=u"delegation" - -output_params = ( - Str('aci', - label=_('ACI'), - ), -) - -@register() -class delegation(Object): - """ - Delegation object. - """ - - bindable = False - object_name = _('delegation') - object_name_plural = _('delegations') - label = _('Delegations') - label_singular = _('Delegation') - - takes_params = ( - Str('aciname', - cli_name='name', - label=_('Delegation name'), - doc=_('Delegation name'), - primary_key=True, - ), - Str('permissions*', - cli_name='permissions', - label=_('Permissions'), - doc=_('Permissions to grant (read, write). Default is write.'), - ), - Str('attrs+', - cli_name='attrs', - label=_('Attributes'), - doc=_('Attributes to which the delegation applies'), - normalizer=lambda value: value.lower(), - ), - Str('memberof', - cli_name='membergroup', - label=_('Member user group'), - doc=_('User group to apply delegation to'), - ), - Str('group', - cli_name='group', - label=_('User group'), - doc=_('User group ACI grants access to'), - ), - ) - - def __json__(self): - json_friendly_attributes = ( - 'label', 'label_singular', 'takes_params', 'bindable', 'name', - 'object_name', 'object_name_plural', - ) - json_dict = dict( - (a, getattr(self, a)) for a in json_friendly_attributes - ) - json_dict['primary_key'] = self.primary_key.name - - json_dict['methods'] = [m for m in self.methods] - return json_dict - - def postprocess_result(self, result): - try: - # do not include prefix in result - del result['aciprefix'] - except KeyError: - pass - - - -@register() -class delegation_add(crud.Create): - __doc__ = _('Add a new delegation.') - - msg_summary = _('Added delegation "%(value)s"') - has_output_params = output_params - - def execute(self, aciname, **kw): - if not 'permissions' in kw: - kw['permissions'] = (u'write',) - kw['aciprefix'] = ACI_PREFIX - result = api.Command['aci_add'](aciname, **kw)['result'] - self.obj.postprocess_result(result) - - return dict( - result=result, - value=pkey_to_value(aciname, kw), - ) - - - -@register() -class delegation_del(crud.Delete): - __doc__ = _('Delete a delegation.') - - has_output = output.standard_boolean - msg_summary = _('Deleted delegation "%(value)s"') - - def execute(self, aciname, **kw): - kw['aciprefix'] = ACI_PREFIX - result = api.Command['aci_del'](aciname, **kw) - self.obj.postprocess_result(result) - return dict( - result=True, - value=pkey_to_value(aciname, kw), - ) - - - -@register() -class delegation_mod(crud.Update): - __doc__ = _('Modify a delegation.') - - msg_summary = _('Modified delegation "%(value)s"') - has_output_params = output_params - - def execute(self, aciname, **kw): - kw['aciprefix'] = ACI_PREFIX - result = api.Command['aci_mod'](aciname, **kw)['result'] - self.obj.postprocess_result(result) - - return dict( - result=result, - value=pkey_to_value(aciname, kw), - ) - - - -@register() -class delegation_find(crud.Search): - __doc__ = _('Search for delegations.') - - msg_summary = ngettext( - '%(count)d delegation matched', '%(count)d delegations matched', 0 - ) - - takes_options = (gen_pkey_only_option("name"),) - has_output_params = output_params - - def execute(self, term=None, **kw): - kw['aciprefix'] = ACI_PREFIX - results = api.Command['aci_find'](term, **kw)['result'] - - for aci in results: - self.obj.postprocess_result(aci) - - return dict( - result=results, - count=len(results), - truncated=False, - ) - - - -@register() -class delegation_show(crud.Retrieve): - __doc__ = _('Display information about a delegation.') - - has_output_params = output_params - - def execute(self, aciname, **kw): - result = api.Command['aci_show'](aciname, aciprefix=ACI_PREFIX, **kw)['result'] - self.obj.postprocess_result(result) - return dict( - result=result, - value=pkey_to_value(aciname, kw), - ) - diff --git a/ipalib/plugins/dns.py b/ipalib/plugins/dns.py deleted file mode 100644 index 9cca07c6d..000000000 --- a/ipalib/plugins/dns.py +++ /dev/null @@ -1,4396 +0,0 @@ -# Authors: -# Martin Kosek <mkosek@redhat.com> -# Pavel Zuna <pzuna@redhat.com> -# -# Copyright (C) 2010 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/>. - -from __future__ import absolute_import - -import netaddr -import time -import re -import binascii -import encodings.idna - -import dns.name -import dns.exception -import dns.rdatatype -import dns.resolver -import six - -from ipalib.dns import (get_record_rrtype, - get_rrparam_from_part, - has_cli_options, - iterate_rrparams_by_parts, - record_name_format) -from ipalib.request import context -from ipalib import api, errors, output -from ipalib import Command -from ipalib.capabilities import ( - VERSION_WITHOUT_CAPABILITIES, - client_has_capability) -from ipalib.parameters import (Flag, Bool, Int, Decimal, Str, StrEnum, Any, - DNSNameParam) -from ipalib.plugable import Registry -from .baseldap import ( - pkey_to_value, - LDAPObject, - LDAPCreate, - LDAPUpdate, - LDAPSearch, - LDAPQuery, - LDAPDelete, - LDAPRetrieve) -from ipalib import _ -from ipalib import messages -from ipalib.util import (normalize_zonemgr, - get_dns_forward_zone_update_policy, - get_dns_reverse_zone_update_policy, - get_reverse_zone_default, REVERSE_DNS_ZONES, - normalize_zone, validate_dnssec_global_forwarder, - DNSSECSignatureMissingError, UnresolvableRecordError, - EDNS0UnsupportedError, DNSSECValidationError, - validate_dnssec_zone_forwarder_step1, - validate_dnssec_zone_forwarder_step2, - verify_host_resolvable) -from ipapython.dn import DN -from ipapython.ipautil import CheckedIPAddress -from ipapython.dnsutil import check_zone_overlap -from ipapython.dnsutil import DNSName -from ipapython.dnsutil import related_to_auto_empty_zone - -if six.PY3: - unicode = str - -__doc__ = _(""" -Domain Name System (DNS) -""") + _(""" -Manage DNS zone and resource records. -""") + _(""" -SUPPORTED ZONE TYPES - - * Master zone (dnszone-*), contains authoritative data. - * Forward zone (dnsforwardzone-*), forwards queries to configured forwarders - (a set of DNS servers). -""") + _(""" -USING STRUCTURED PER-TYPE OPTIONS -""") + _(""" -There are many structured DNS RR types where DNS data stored in LDAP server -is not just a scalar value, for example an IP address or a domain name, but -a data structure which may be often complex. A good example is a LOC record -[RFC1876] which consists of many mandatory and optional parts (degrees, -minutes, seconds of latitude and longitude, altitude or precision). -""") + _(""" -It may be difficult to manipulate such DNS records without making a mistake -and entering an invalid value. DNS module provides an abstraction over these -raw records and allows to manipulate each RR type with specific options. For -each supported RR type, DNS module provides a standard option to manipulate -a raw records with format --<rrtype>-rec, e.g. --mx-rec, and special options -for every part of the RR structure with format --<rrtype>-<partname>, e.g. ---mx-preference and --mx-exchanger. -""") + _(""" -When adding a record, either RR specific options or standard option for a raw -value can be used, they just should not be combined in one add operation. When -modifying an existing entry, new RR specific options can be used to change -one part of a DNS record, where the standard option for raw value is used -to specify the modified value. The following example demonstrates -a modification of MX record preference from 0 to 1 in a record without -modifying the exchanger: -ipa dnsrecord-mod --mx-rec="0 mx.example.com." --mx-preference=1 -""") + _(""" - -EXAMPLES: -""") + _(""" - Add new zone: - ipa dnszone-add example.com --admin-email=admin@example.com -""") + _(""" - Add system permission that can be used for per-zone privilege delegation: - ipa dnszone-add-permission example.com -""") + _(""" - Modify the zone to allow dynamic updates for hosts own records in realm EXAMPLE.COM: - ipa dnszone-mod example.com --dynamic-update=TRUE -""") + _(""" - This is the equivalent of: - ipa dnszone-mod example.com --dynamic-update=TRUE \\ - --update-policy="grant EXAMPLE.COM krb5-self * A; grant EXAMPLE.COM krb5-self * AAAA; grant EXAMPLE.COM krb5-self * SSHFP;" -""") + _(""" - Modify the zone to allow zone transfers for local network only: - ipa dnszone-mod example.com --allow-transfer=192.0.2.0/24 -""") + _(""" - Add new reverse zone specified by network IP address: - ipa dnszone-add --name-from-ip=192.0.2.0/24 -""") + _(""" - Add second nameserver for example.com: - ipa dnsrecord-add example.com @ --ns-rec=nameserver2.example.com -""") + _(""" - Add a mail server for example.com: - ipa dnsrecord-add example.com @ --mx-rec="10 mail1" -""") + _(""" - Add another record using MX record specific options: - ipa dnsrecord-add example.com @ --mx-preference=20 --mx-exchanger=mail2 -""") + _(""" - Add another record using interactive mode (started when dnsrecord-add, dnsrecord-mod, - or dnsrecord-del are executed with no options): - ipa dnsrecord-add example.com @ - Please choose a type of DNS resource record to be added - The most common types for this type of zone are: NS, MX, LOC - - DNS resource record type: MX - MX Preference: 30 - MX Exchanger: mail3 - Record name: example.com - MX record: 10 mail1, 20 mail2, 30 mail3 - NS record: nameserver.example.com., nameserver2.example.com. -""") + _(""" - Delete previously added nameserver from example.com: - ipa dnsrecord-del example.com @ --ns-rec=nameserver2.example.com. -""") + _(""" - Add LOC record for example.com: - ipa dnsrecord-add example.com @ --loc-rec="49 11 42.4 N 16 36 29.6 E 227.64m" -""") + _(""" - Add new A record for www.example.com. Create a reverse record in appropriate - reverse zone as well. In this case a PTR record "2" pointing to www.example.com - will be created in zone 2.0.192.in-addr.arpa. - ipa dnsrecord-add example.com www --a-rec=192.0.2.2 --a-create-reverse -""") + _(""" - Add new PTR record for www.example.com - ipa dnsrecord-add 2.0.192.in-addr.arpa. 2 --ptr-rec=www.example.com. -""") + _(""" - Add new SRV records for LDAP servers. Three quarters of the requests - should go to fast.example.com, one quarter to slow.example.com. If neither - is available, switch to backup.example.com. - ipa dnsrecord-add example.com _ldap._tcp --srv-rec="0 3 389 fast.example.com" - ipa dnsrecord-add example.com _ldap._tcp --srv-rec="0 1 389 slow.example.com" - ipa dnsrecord-add example.com _ldap._tcp --srv-rec="1 1 389 backup.example.com" -""") + _(""" - The interactive mode can be used for easy modification: - ipa dnsrecord-mod example.com _ldap._tcp - No option to modify specific record provided. - Current DNS record contents: - - SRV record: 0 3 389 fast.example.com, 0 1 389 slow.example.com, 1 1 389 backup.example.com - - Modify SRV record '0 3 389 fast.example.com'? Yes/No (default No): - Modify SRV record '0 1 389 slow.example.com'? Yes/No (default No): y - SRV Priority [0]: (keep the default value) - SRV Weight [1]: 2 (modified value) - SRV Port [389]: (keep the default value) - SRV Target [slow.example.com]: (keep the default value) - 1 SRV record skipped. Only one value per DNS record type can be modified at one time. - Record name: _ldap._tcp - SRV record: 0 3 389 fast.example.com, 1 1 389 backup.example.com, 0 2 389 slow.example.com -""") + _(""" - After this modification, three fifths of the requests should go to - fast.example.com and two fifths to slow.example.com. -""") + _(""" - An example of the interactive mode for dnsrecord-del command: - ipa dnsrecord-del example.com www - No option to delete specific record provided. - Delete all? Yes/No (default No): (do not delete all records) - Current DNS record contents: - - A record: 192.0.2.2, 192.0.2.3 - - Delete A record '192.0.2.2'? Yes/No (default No): - Delete A record '192.0.2.3'? Yes/No (default No): y - Record name: www - A record: 192.0.2.2 (A record 192.0.2.3 has been deleted) -""") + _(""" - Show zone example.com: - ipa dnszone-show example.com -""") + _(""" - Find zone with "example" in its domain name: - ipa dnszone-find example -""") + _(""" - Find records for resources with "www" in their name in zone example.com: - ipa dnsrecord-find example.com www -""") + _(""" - Find A records with value 192.0.2.2 in zone example.com - ipa dnsrecord-find example.com --a-rec=192.0.2.2 -""") + _(""" - Show records for resource www in zone example.com - ipa dnsrecord-show example.com www -""") + _(""" - Delegate zone sub.example to another nameserver: - ipa dnsrecord-add example.com ns.sub --a-rec=203.0.113.1 - ipa dnsrecord-add example.com sub --ns-rec=ns.sub.example.com. -""") + _(""" - Delete zone example.com with all resource records: - ipa dnszone-del example.com -""") + _(""" - If a global forwarder is configured, all queries for which this server is not - authoritative (e.g. sub.example.com) will be routed to the global forwarder. - Global forwarding configuration can be overridden per-zone. -""") + _(""" - Semantics of forwarding in IPA matches BIND semantics and depends on the type - of zone: - * Master zone: local BIND replies authoritatively to queries for data in - the given zone (including authoritative NXDOMAIN answers) and forwarding - affects only queries for names below zone cuts (NS records) of locally - served zones. - - * Forward zone: forward zone contains no authoritative data. BIND forwards - queries, which cannot be answered from its local cache, to configured - forwarders. -""") + _(""" - Semantics of the --forwarder-policy option: - * none - disable forwarding for the given zone. - * first - forward all queries to configured forwarders. If they fail, - do resolution using DNS root servers. - * only - forward all queries to configured forwarders and if they fail, - return failure. -""") + _(""" - Disable global forwarding for given sub-tree: - ipa dnszone-mod example.com --forward-policy=none -""") + _(""" - This configuration forwards all queries for names outside the example.com - sub-tree to global forwarders. Normal recursive resolution process is used - for names inside the example.com sub-tree (i.e. NS records are followed etc.). -""") + _(""" - Forward all requests for the zone external.example.com to another forwarder - using a "first" policy (it will send the queries to the selected forwarder - and if not answered it will use global root servers): - ipa dnsforwardzone-add external.example.com --forward-policy=first \\ - --forwarder=203.0.113.1 -""") + _(""" - Change forward-policy for external.example.com: - ipa dnsforwardzone-mod external.example.com --forward-policy=only -""") + _(""" - Show forward zone external.example.com: - ipa dnsforwardzone-show external.example.com -""") + _(""" - List all forward zones: - ipa dnsforwardzone-find -""") + _(""" - Delete forward zone external.example.com: - ipa dnsforwardzone-del external.example.com -""") + _(""" - Resolve a host name to see if it exists (will add default IPA domain - if one is not included): - ipa dns-resolve www.example.com - ipa dns-resolve www -""") + _(""" - -GLOBAL DNS CONFIGURATION -""") + _(""" -DNS configuration passed to command line install script is stored in a local -configuration file on each IPA server where DNS service is configured. These -local settings can be overridden with a common configuration stored in LDAP -server: -""") + _(""" - Show global DNS configuration: - ipa dnsconfig-show -""") + _(""" - Modify global DNS configuration and set a list of global forwarders: - ipa dnsconfig-mod --forwarder=203.0.113.113 -""") - -register = Registry() - -# supported resource record types -_record_types = ( - u'A', u'AAAA', u'A6', u'AFSDB', u'APL', u'CERT', u'CNAME', u'DHCID', u'DLV', - u'DNAME', u'DS', u'HIP', u'HINFO', u'IPSECKEY', u'KEY', u'KX', u'LOC', - u'MD', u'MINFO', u'MX', u'NAPTR', u'NS', u'NSEC', u'NXT', u'PTR', u'RRSIG', - u'RP', u'SIG', u'SPF', u'SRV', u'SSHFP', u'TLSA', u'TXT', -) - -# DNS zone record identificator -_dns_zone_record = DNSName.empty - -# attributes derived from record types -_record_attributes = [str(record_name_format % t.lower()) - for t in _record_types] - -# Deprecated -# supported DNS classes, IN = internet, rest is almost never used -_record_classes = (u'IN', u'CS', u'CH', u'HS') - -# IN record class -_IN = dns.rdataclass.IN - -# NS record type -_NS = dns.rdatatype.from_text('NS') - -_output_permissions = ( - output.summary, - output.Output('result', bool, _('True means the operation was successful')), - output.Output('value', unicode, _('Permission value')), -) - - -def _rname_validator(ugettext, zonemgr): - try: - DNSName(zonemgr) # test only if it is valid domain name - except (ValueError, dns.exception.SyntaxError) as e: - return unicode(e) - return None - -def _create_zone_serial(): - """ - Generate serial number for zones. bind-dyndb-ldap expects unix time in - to be used for SOA serial. - - SOA serial in a date format would also work, but it may be set to far - future when many DNS updates are done per day (more than 100). Unix - timestamp is more resilient to this issue. - """ - return int(time.time()) - -def _reverse_zone_name(netstr): - try: - netaddr.IPAddress(str(netstr)) - except (netaddr.AddrFormatError, ValueError): - pass - else: - # use more sensible default prefix than netaddr default - return unicode(get_reverse_zone_default(netstr)) - - net = netaddr.IPNetwork(netstr) - items = net.ip.reverse_dns.split('.') - if net.version == 4: - return u'.'.join(items[4 - net.prefixlen // 8:]) - elif net.version == 6: - return u'.'.join(items[32 - net.prefixlen // 4:]) - else: - return None - -def _validate_ipaddr(ugettext, ipaddr, ip_version=None): - try: - ip = netaddr.IPAddress(str(ipaddr), flags=netaddr.INET_PTON) - - if ip_version is not None: - if ip.version != ip_version: - return _('invalid IP address version (is %(value)d, must be %(required_value)d)!') \ - % dict(value=ip.version, required_value=ip_version) - except (netaddr.AddrFormatError, ValueError): - return _('invalid IP address format') - return None - -def _validate_ip4addr(ugettext, ipaddr): - return _validate_ipaddr(ugettext, ipaddr, 4) - -def _validate_ip6addr(ugettext, ipaddr): - return _validate_ipaddr(ugettext, ipaddr, 6) - -def _validate_ipnet(ugettext, ipnet): - try: - net = netaddr.IPNetwork(ipnet) - except (netaddr.AddrFormatError, ValueError, UnboundLocalError): - return _('invalid IP network format') - return None - -def _validate_bind_aci(ugettext, bind_acis): - if not bind_acis: - return - - bind_acis = bind_acis.split(';') - if bind_acis[-1]: - return _('each ACL element must be terminated with a semicolon') - else: - bind_acis.pop(-1) - - for bind_aci in bind_acis: - if bind_aci in ("any", "none", "localhost", "localnets"): - continue - - if bind_aci.startswith('!'): - bind_aci = bind_aci[1:] - - try: - ip = CheckedIPAddress(bind_aci, parse_netmask=True, - allow_network=True, allow_loopback=True) - except (netaddr.AddrFormatError, ValueError) as e: - return unicode(e) - except UnboundLocalError: - return _(u"invalid address format") - -def _normalize_bind_aci(bind_acis): - if not bind_acis: - return - bind_acis = bind_acis.split(';') - normalized = [] - for bind_aci in bind_acis: - if not bind_aci: - continue - if bind_aci in ("any", "none", "localhost", "localnets"): - normalized.append(bind_aci) - continue - - prefix = "" - if bind_aci.startswith('!'): - bind_aci = bind_aci[1:] - prefix = "!" - - try: - ip = CheckedIPAddress(bind_aci, parse_netmask=True, - allow_network=True, allow_loopback=True) - if '/' in bind_aci: # addr with netmask - netmask = "/%s" % ip.prefixlen - else: - netmask = "" - normalized.append(u"%s%s%s" % (prefix, str(ip), netmask)) - continue - except Exception: - normalized.append(bind_aci) - continue - - acis = u';'.join(normalized) - acis += u';' - return acis - -def _validate_bind_forwarder(ugettext, forwarder): - ip_address, sep, port = forwarder.partition(u' port ') - - ip_address_validation = _validate_ipaddr(ugettext, ip_address) - - if ip_address_validation is not None: - return ip_address_validation - - if sep: - try: - port = int(port) - if port < 0 or port > 65535: - raise ValueError() - except ValueError: - return _('%(port)s is not a valid port' % dict(port=port)) - - return None - -def _validate_nsec3param_record(ugettext, value): - _nsec3param_pattern = (r'^(?P<alg>\d+) (?P<flags>\d+) (?P<iter>\d+) ' - r'(?P<salt>([0-9a-fA-F]{2})+|-)$') - rec = re.compile(_nsec3param_pattern, flags=re.U) - result = rec.match(value) - - if result is None: - return _(u'expected format: <0-255> <0-255> <0-65535> ' - 'even-length_hexadecimal_digits_or_hyphen') - - alg = int(result.group('alg')) - flags = int(result.group('flags')) - iterations = int(result.group('iter')) - salt = result.group('salt') - - if alg > 255: - return _('algorithm value: allowed interval 0-255') - - if flags > 255: - return _('flags value: allowed interval 0-255') - - if iterations > 65535: - return _('iterations value: allowed interval 0-65535') - - if salt == u'-': - return None - - try: - binascii.a2b_hex(salt) - except TypeError as e: - return _('salt value: %(err)s') % {'err': e} - return None - - -def _hostname_validator(ugettext, value): - assert isinstance(value, DNSName) - if len(value.make_absolute().labels) < 3: - return _('invalid domain-name: not fully qualified') - - return None - -def _no_wildcard_validator(ugettext, value): - """Disallow usage of wildcards as RFC 4592 section 4 recommends - """ - assert isinstance(value, DNSName) - if value.is_wild(): - return _('should not be a wildcard domain name (RFC 4592 section 4)') - return None - -def is_forward_record(zone, str_address): - addr = netaddr.IPAddress(str_address) - if addr.version == 4: - result = api.Command['dnsrecord_find'](zone, arecord=str_address) - elif addr.version == 6: - result = api.Command['dnsrecord_find'](zone, aaaarecord=str_address) - else: - raise ValueError('Invalid address family') - - return result['count'] > 0 - -def add_forward_record(zone, name, str_address): - addr = netaddr.IPAddress(str_address) - try: - if addr.version == 4: - api.Command['dnsrecord_add'](zone, name, arecord=str_address) - elif addr.version == 6: - api.Command['dnsrecord_add'](zone, name, aaaarecord=str_address) - else: - raise ValueError('Invalid address family') - except errors.EmptyModlist: - pass # the entry already exists and matches - -def get_reverse_zone(ipaddr): - """ - resolve the reverse zone for IP address and see if it is managed by IPA - server - :param ipaddr: host IP address - :return: tuple containing name of the reverse zone and the name of the - record - """ - ip = netaddr.IPAddress(str(ipaddr)) - revdns = DNSName(unicode(ip.reverse_dns)) - revzone = DNSName(dns.resolver.zone_for_name(revdns)) - - try: - api.Command['dnszone_show'](revzone) - except errors.NotFound: - raise errors.NotFound( - reason=_( - 'DNS reverse zone %(revzone)s for IP address ' - '%(addr)s is not managed by this server') % dict( - addr=ipaddr, revzone=revzone) - ) - - revname = revdns.relativize(revzone) - - return revzone, revname - -def add_records_for_host_validation(option_name, host, domain, ip_addresses, check_forward=True, check_reverse=True): - assert isinstance(host, DNSName) - assert isinstance(domain, DNSName) - - try: - api.Command['dnszone_show'](domain)['result'] - except errors.NotFound: - raise errors.NotFound( - reason=_('DNS zone %(zone)s not found') % dict(zone=domain) - ) - if not isinstance(ip_addresses, (tuple, list)): - ip_addresses = [ip_addresses] - - for ip_address in ip_addresses: - try: - ip = CheckedIPAddress(ip_address, match_local=False) - except Exception as e: - raise errors.ValidationError(name=option_name, error=unicode(e)) - - if check_forward: - if is_forward_record(domain, unicode(ip)): - raise errors.DuplicateEntry( - message=_(u'IP address %(ip)s is already assigned in domain %(domain)s.')\ - % dict(ip=str(ip), domain=domain)) - - if check_reverse: - try: - # we prefer lookup of the IP through the reverse zone - revzone, revname = get_reverse_zone(ip) - reverse = api.Command['dnsrecord_find'](revzone, idnsname=revname) - if reverse['count'] > 0: - raise errors.DuplicateEntry( - message=_(u'Reverse record for IP address %(ip)s already exists in reverse zone %(zone)s.')\ - % dict(ip=str(ip), zone=revzone)) - except errors.NotFound: - pass - - -def add_records_for_host(host, domain, ip_addresses, add_forward=True, add_reverse=True): - assert isinstance(host, DNSName) - assert isinstance(domain, DNSName) - - if not isinstance(ip_addresses, (tuple, list)): - ip_addresses = [ip_addresses] - - for ip_address in ip_addresses: - ip = CheckedIPAddress(ip_address, match_local=False) - - if add_forward: - add_forward_record(domain, host, unicode(ip)) - - if add_reverse: - try: - revzone, revname = get_reverse_zone(ip) - addkw = {'ptrrecord': host.derelativize(domain).ToASCII()} - api.Command['dnsrecord_add'](revzone, revname, **addkw) - except errors.EmptyModlist: - # the entry already exists and matches - pass - -def _dns_name_to_string(value, raw=False): - if isinstance(value, unicode): - try: - value = DNSName(value) - except Exception: - return value - - assert isinstance(value, DNSName) - if raw: - return value.ToASCII() - else: - return unicode(value) - - -def _check_entry_objectclass(entry, objectclasses): - """ - Check if entry contains all objectclasses - """ - if not isinstance(objectclasses, (list, tuple)): - objectclasses = [objectclasses, ] - if not entry.get('objectclass'): - return False - entry_objectclasses = [o.lower() for o in entry['objectclass']] - for o in objectclasses: - if o not in entry_objectclasses: - return False - return True - - -def _check_DN_objectclass(ldap, dn, objectclasses): - try: - entry = ldap.get_entry(dn, [u'objectclass', ]) - except Exception: - return False - else: - return _check_entry_objectclass(entry, objectclasses) - - -class DNSRecord(Str): - # a list of parts that create the actual raw DNS record - parts = None - # an optional list of parameters used in record-specific operations - extra = None - supported = True - # supported RR types: https://fedorahosted.org/bind-dyndb-ldap/browser/doc/schema - - label_format = _("%s record") - part_label_format = "%s %s" - doc_format = _('Raw %s records') - option_group_format = _('%s Record') - see_rfc_msg = _("(see RFC %s for details)") - part_name_format = "%s_part_%s" - extra_name_format = "%s_extra_%s" - cli_name_format = "%s_%s" - format_error_msg = None - - kwargs = Str.kwargs + ( - ('validatedns', bool, True), - ('normalizedns', bool, True), - ) - - # should be replaced in subclasses - rrtype = None - rfc = None - - def __init__(self, name=None, *rules, **kw): - if self.rrtype not in _record_types: - raise ValueError("Unknown RR type: %s. Must be one of %s" % \ - (str(self.rrtype), ", ".join(_record_types))) - if not name: - name = "%s*" % (record_name_format % self.rrtype.lower()) - kw.setdefault('cli_name', '%s_rec' % self.rrtype.lower()) - kw.setdefault('label', self.label_format % self.rrtype) - kw.setdefault('doc', self.doc_format % self.rrtype) - kw.setdefault('option_group', self.option_group_format % self.rrtype) - - if not self.supported: - kw['flags'] = ('no_option',) - - super(DNSRecord, self).__init__(name, *rules, **kw) - - def _get_part_values(self, value): - values = value.split() - if len(values) != len(self.parts): - return None - return tuple(values) - - def _part_values_to_string(self, values, idna=True): - self._validate_parts(values) - parts = [] - for v in values: - if v is None: - continue - elif isinstance(v, DNSName) and idna: - v = v.ToASCII() - elif not isinstance(v, unicode): - v = unicode(v) - parts.append(v) - - return u" ".join(parts) - - def get_parts_from_kw(self, kw, raise_on_none=True): - part_names = tuple(self.part_name_format % (self.rrtype.lower(), part.name) \ - for part in self.parts) - vals = tuple(kw.get(part_name) for part_name in part_names) - - if all(val is None for val in vals): - return - - if raise_on_none: - for val_id,val in enumerate(vals): - if val is None and self.parts[val_id].required: - cli_name = self.cli_name_format % (self.rrtype.lower(), self.parts[val_id].name) - raise errors.ConversionError(name=self.name, - error=_("'%s' is a required part of DNS record") % cli_name) - - return vals - - def _validate_parts(self, parts): - if len(parts) != len(self.parts): - raise errors.ValidationError(name=self.name, - error=_("Invalid number of parts!")) - - def _convert_scalar(self, value, index=None): - if isinstance(value, (tuple, list)): - return self._part_values_to_string(value) - return super(DNSRecord, self)._convert_scalar(value) - - def normalize(self, value): - if self.normalizedns: - if isinstance(value, (tuple, list)): - value = tuple( - self._normalize_parts(v) for v in value \ - if v is not None - ) - elif value is not None: - value = (self._normalize_parts(value),) - - return super(DNSRecord, self).normalize(value) - - def _normalize_parts(self, value): - """ - Normalize a DNS record value using normalizers for its parts. - """ - if self.parts is None: - return value - try: - values = self._get_part_values(value) - if not values: - return value - - converted_values = [ part._convert_scalar(values[part_id]) \ - if values[part_id] is not None else None - for part_id, part in enumerate(self.parts) - ] - - new_values = [ part.normalize(converted_values[part_id]) \ - for part_id, part in enumerate(self.parts) ] - - value = self._convert_scalar(new_values) - except Exception: - # cannot normalize, rather return original value than fail - pass - return value - - def _rule_validatedns(self, _, value): - if not self.validatedns: - return - - if value is None: - return - - if not self.supported: - return _('DNS RR type "%s" is not supported by bind-dyndb-ldap plugin') \ - % self.rrtype - - if self.parts is None: - return - - # validate record format - values = self._get_part_values(value) - if not values: - if not self.format_error_msg: - part_names = [part.name.upper() for part in self.parts] - - if self.rfc: - see_rfc_msg = " " + self.see_rfc_msg % self.rfc - else: - see_rfc_msg = "" - return _('format must be specified as "%(format)s" %(rfcs)s') \ - % dict(format=" ".join(part_names), rfcs=see_rfc_msg) - else: - return self.format_error_msg - - # validate every part - for part_id, part in enumerate(self.parts): - val = part.normalize(values[part_id]) - val = part.convert(val) - part.validate(val) - return None - - def _convert_dnsrecord_part(self, part): - """ - All parts of DNSRecord need to be processed and modified before they - can be added to global DNS API. For example a prefix need to be added - before part name so that the name is unique in the global namespace. - """ - name = self.part_name_format % (self.rrtype.lower(), part.name) - cli_name = self.cli_name_format % (self.rrtype.lower(), part.name) - label = self.part_label_format % (self.rrtype, unicode(part.label)) - option_group = self.option_group_format % self.rrtype - flags = list(part.flags) + ['dnsrecord_part', 'virtual_attribute',] - if not part.required: - flags.append('dnsrecord_optional') - if not self.supported: - flags.append("no_option") - - return part.clone_rename(name, - cli_name=cli_name, - label=label, - required=False, - option_group=option_group, - flags=flags, - hint=self.name,) # name of parent RR param - - def _convert_dnsrecord_extra(self, extra): - """ - Parameters for special per-type behavior need to be processed in the - same way as record parts in _convert_dnsrecord_part(). - """ - name = self.extra_name_format % (self.rrtype.lower(), extra.name) - cli_name = self.cli_name_format % (self.rrtype.lower(), extra.name) - label = self.part_label_format % (self.rrtype, unicode(extra.label)) - option_group = self.option_group_format % self.rrtype - flags = list(extra.flags) + ['dnsrecord_extra', 'virtual_attribute',] - - return extra.clone_rename(name, - cli_name=cli_name, - label=label, - required=False, - option_group=option_group, - flags=flags, - hint=self.name,) # name of parent RR param - - def get_parts(self): - if self.parts is None: - return tuple() - - return tuple(self._convert_dnsrecord_part(part) for part in self.parts) - - def get_extra(self): - if self.extra is None: - return tuple() - - return tuple(self._convert_dnsrecord_extra(extra) for extra in self.extra) - - # callbacks for per-type special record behavior - def dnsrecord_add_pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - - def dnsrecord_add_post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - -class ForwardRecord(DNSRecord): - extra = ( - Flag('create_reverse?', - label=_('Create reverse'), - doc=_('Create reverse record for this IP Address'), - flags=['no_update'] - ), - ) - - def dnsrecord_add_pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - reverse_option = self._convert_dnsrecord_extra(self.extra[0]) - if options.get(reverse_option.name): - records = entry_attrs.get(self.name, []) - if not records: - # --<rrtype>-create-reverse is set, but there are not records - raise errors.RequirementError(name=self.name) - - for record in records: - add_records_for_host_validation(self.name, keys[-1], keys[-2], record, - check_forward=False, - check_reverse=True) - - setattr(context, '%s_reverse' % self.name, entry_attrs.get(self.name)) - - def dnsrecord_add_post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - rev_records = getattr(context, '%s_reverse' % self.name, []) - - if rev_records: - # make sure we don't run this post callback action again in nested - # commands, line adding PTR record in add_records_for_host - delattr(context, '%s_reverse' % self.name) - for record in rev_records: - try: - add_records_for_host(keys[-1], keys[-2], record, - add_forward=False, add_reverse=True) - except Exception as e: - raise errors.NonFatalError( - reason=_('Cannot create reverse record for "%(value)s": %(exc)s') \ - % dict(value=record, exc=unicode(e))) - -class UnsupportedDNSRecord(DNSRecord): - """ - Records which are not supported by IPA CLI, but we allow to show them if - LDAP contains these records. - """ - supported = False - - def _get_part_values(self, value): - return tuple() - - -class ARecord(ForwardRecord): - rrtype = 'A' - rfc = 1035 - parts = ( - Str('ip_address', - _validate_ip4addr, - label=_('IP Address'), - ), - ) - -class A6Record(DNSRecord): - rrtype = 'A6' - rfc = 3226 - parts = ( - Str('data', - label=_('Record data'), - ), - ) - - def _get_part_values(self, value): - # A6 RR type is obsolete and only a raw interface is provided - return (value,) - -class AAAARecord(ForwardRecord): - rrtype = 'AAAA' - rfc = 3596 - parts = ( - Str('ip_address', - _validate_ip6addr, - label=_('IP Address'), - ), - ) - -class AFSDBRecord(DNSRecord): - rrtype = 'AFSDB' - rfc = 1183 - parts = ( - Int('subtype?', - label=_('Subtype'), - minvalue=0, - maxvalue=65535, - ), - DNSNameParam('hostname', - label=_('Hostname'), - ), - ) - -class APLRecord(UnsupportedDNSRecord): - rrtype = 'APL' - rfc = 3123 - -class CERTRecord(DNSRecord): - rrtype = 'CERT' - rfc = 4398 - parts = ( - Int('type', - label=_('Certificate Type'), - minvalue=0, - maxvalue=65535, - ), - Int('key_tag', - label=_('Key Tag'), - minvalue=0, - maxvalue=65535, - ), - Int('algorithm', - label=_('Algorithm'), - minvalue=0, - maxvalue=255, - ), - Str('certificate_or_crl', - label=_('Certificate/CRL'), - ), - ) - -class CNAMERecord(DNSRecord): - rrtype = 'CNAME' - rfc = 1035 - parts = ( - DNSNameParam('hostname', - label=_('Hostname'), - doc=_('A hostname which this alias hostname points to'), - ), - ) - -class DHCIDRecord(UnsupportedDNSRecord): - rrtype = 'DHCID' - rfc = 4701 - -class DNAMERecord(DNSRecord): - rrtype = 'DNAME' - rfc = 2672 - parts = ( - DNSNameParam('target', - label=_('Target'), - ), - ) - - -class DSRecord(DNSRecord): - rrtype = 'DS' - rfc = 4034 - parts = ( - Int('key_tag', - label=_('Key Tag'), - minvalue=0, - maxvalue=65535, - ), - Int('algorithm', - label=_('Algorithm'), - minvalue=0, - maxvalue=255, - ), - Int('digest_type', - label=_('Digest Type'), - minvalue=0, - maxvalue=255, - ), - Str('digest', - label=_('Digest'), - pattern=r'^[0-9a-fA-F]+$', - pattern_errmsg=u'only hexadecimal digits are allowed' - ), - ) - - -class DLVRecord(DSRecord): - # must use same attributes as DSRecord - rrtype = 'DLV' - rfc = 4431 - - -class HINFORecord(UnsupportedDNSRecord): - rrtype = 'HINFO' - rfc = 1035 - - -class HIPRecord(UnsupportedDNSRecord): - rrtype = 'HIP' - rfc = 5205 - -class KEYRecord(UnsupportedDNSRecord): - # managed by BIND itself - rrtype = 'KEY' - rfc = 2535 - -class IPSECKEYRecord(UnsupportedDNSRecord): - rrtype = 'IPSECKEY' - rfc = 4025 - -class KXRecord(DNSRecord): - rrtype = 'KX' - rfc = 2230 - parts = ( - Int('preference', - label=_('Preference'), - doc=_('Preference given to this exchanger. Lower values are more preferred'), - minvalue=0, - maxvalue=65535, - ), - DNSNameParam('exchanger', - label=_('Exchanger'), - doc=_('A host willing to act as a key exchanger'), - ), - ) - -class LOCRecord(DNSRecord): - rrtype = 'LOC' - rfc = 1876 - parts = ( - Int('lat_deg', - label=_('Degrees Latitude'), - minvalue=0, - maxvalue=90, - ), - Int('lat_min?', - label=_('Minutes Latitude'), - minvalue=0, - maxvalue=59, - ), - Decimal('lat_sec?', - label=_('Seconds Latitude'), - minvalue='0.0', - maxvalue='59.999', - precision=3, - ), - StrEnum('lat_dir', - label=_('Direction Latitude'), - values=(u'N', u'S',), - ), - Int('lon_deg', - label=_('Degrees Longitude'), - minvalue=0, - maxvalue=180, - ), - Int('lon_min?', - label=_('Minutes Longitude'), - minvalue=0, - maxvalue=59, - ), - Decimal('lon_sec?', - label=_('Seconds Longitude'), - minvalue='0.0', - maxvalue='59.999', - precision=3, - ), - StrEnum('lon_dir', - label=_('Direction Longitude'), - values=(u'E', u'W',), - ), - Decimal('altitude', - label=_('Altitude'), - minvalue='-100000.00', - maxvalue='42849672.95', - precision=2, - ), - Decimal('size?', - label=_('Size'), - minvalue='0.0', - maxvalue='90000000.00', - precision=2, - ), - Decimal('h_precision?', - label=_('Horizontal Precision'), - minvalue='0.0', - maxvalue='90000000.00', - precision=2, - ), - Decimal('v_precision?', - label=_('Vertical Precision'), - minvalue='0.0', - maxvalue='90000000.00', - precision=2, - ), - ) - - format_error_msg = _("""format must be specified as - "d1 [m1 [s1]] {"N"|"S"} d2 [m2 [s2]] {"E"|"W"} alt["m"] [siz["m"] [hp["m"] [vp["m"]]]]" - where: - d1: [0 .. 90] (degrees latitude) - d2: [0 .. 180] (degrees longitude) - m1, m2: [0 .. 59] (minutes latitude/longitude) - s1, s2: [0 .. 59.999] (seconds latitude/longitude) - alt: [-100000.00 .. 42849672.95] BY .01 (altitude in meters) - siz, hp, vp: [0 .. 90000000.00] (size/precision in meters) - See RFC 1876 for details""") - - def _get_part_values(self, value): - regex = re.compile( - r'(?P<d1>\d{1,2}\s+)' - r'(?:(?P<m1>\d{1,2}\s+)' - r'(?P<s1>\d{1,2}(?:\.\d{1,3})?\s+)?)?' - r'(?P<dir1>[NS])\s+' - r'(?P<d2>\d{1,3}\s+)' - r'(?:(?P<m2>\d{1,2}\s+)' - r'(?P<s2>\d{1,2}(?:\.\d{1,3})?\s+)?)?' - r'(?P<dir2>[WE])\s+' - r'(?P<alt>-?\d{1,8}(?:\.\d{1,2})?)m?' - r'(?:\s+(?P<siz>\d{1,8}(?:\.\d{1,2})?)m?' - r'(?:\s+(?P<hp>\d{1,8}(?:\.\d{1,2})?)m?' - r'(?:\s+(?P<vp>\d{1,8}(?:\.\d{1,2})?)m?\s*)?)?)?$') - - m = regex.match(value) - - if m is None: - return None - - return tuple(x.strip() if x is not None else x for x in m.groups()) - - def _validate_parts(self, parts): - super(LOCRecord, self)._validate_parts(parts) - - # create part_name -> part_id map first - part_name_map = dict((part.name, part_id) \ - for part_id,part in enumerate(self.parts)) - - requirements = ( ('lat_sec', 'lat_min'), - ('lon_sec', 'lon_min'), - ('h_precision', 'size'), - ('v_precision', 'h_precision', 'size') ) - - for req in requirements: - target_part = req[0] - - if parts[part_name_map[target_part]] is not None: - required_parts = req[1:] - if any(parts[part_name_map[part]] is None for part in required_parts): - target_cli_name = self.cli_name_format % (self.rrtype.lower(), req[0]) - required_cli_names = [ self.cli_name_format % (self.rrtype.lower(), part) - for part in req[1:] ] - error = _("'%(required)s' must not be empty when '%(name)s' is set") % \ - dict(required=', '.join(required_cli_names), - name=target_cli_name) - raise errors.ValidationError(name=self.name, error=error) - - -class MDRecord(UnsupportedDNSRecord): - # obsoleted, use MX instead - rrtype = 'MD' - rfc = 1035 - - -class MINFORecord(UnsupportedDNSRecord): - rrtype = 'MINFO' - rfc = 1035 - - -class MXRecord(DNSRecord): - rrtype = 'MX' - rfc = 1035 - parts = ( - Int('preference', - label=_('Preference'), - doc=_('Preference given to this exchanger. Lower values are more preferred'), - minvalue=0, - maxvalue=65535, - ), - DNSNameParam('exchanger', - label=_('Exchanger'), - doc=_('A host willing to act as a mail exchanger'), - ), - ) - -class NSRecord(DNSRecord): - rrtype = 'NS' - rfc = 1035 - - parts = ( - DNSNameParam('hostname', - label=_('Hostname'), - ), - ) - -class NSECRecord(UnsupportedDNSRecord): - # managed by BIND itself - rrtype = 'NSEC' - rfc = 4034 - - -def _validate_naptr_flags(ugettext, flags): - allowed_flags = u'SAUP' - flags = flags.replace('"','').replace('\'','') - - for flag in flags: - if flag not in allowed_flags: - return _('flags must be one of "S", "A", "U", or "P"') - -class NAPTRRecord(DNSRecord): - rrtype = 'NAPTR' - rfc = 2915 - - parts = ( - Int('order', - label=_('Order'), - minvalue=0, - maxvalue=65535, - ), - Int('preference', - label=_('Preference'), - minvalue=0, - maxvalue=65535, - ), - Str('flags', - _validate_naptr_flags, - label=_('Flags'), - normalizer=lambda x:x.upper() - ), - Str('service', - label=_('Service'), - ), - Str('regexp', - label=_('Regular Expression'), - ), - Str('replacement', - label=_('Replacement'), - ), - ) - - -class NXTRecord(UnsupportedDNSRecord): - rrtype = 'NXT' - rfc = 2535 - - -class PTRRecord(DNSRecord): - rrtype = 'PTR' - rfc = 1035 - parts = ( - DNSNameParam('hostname', - #RFC 2317 section 5.2 -- can be relative - label=_('Hostname'), - doc=_('The hostname this reverse record points to'), - ), - ) - -class RPRecord(UnsupportedDNSRecord): - rrtype = 'RP' - rfc = 1183 - -class SRVRecord(DNSRecord): - rrtype = 'SRV' - rfc = 2782 - parts = ( - Int('priority', - label=_('Priority'), - minvalue=0, - maxvalue=65535, - ), - Int('weight', - label=_('Weight'), - minvalue=0, - maxvalue=65535, - ), - Int('port', - label=_('Port'), - minvalue=0, - maxvalue=65535, - ), - DNSNameParam('target', - label=_('Target'), - doc=_('The domain name of the target host or \'.\' if the service is decidedly not available at this domain'), - ), - ) - -def _sig_time_validator(ugettext, value): - time_format = "%Y%m%d%H%M%S" - try: - time.strptime(value, time_format) - except ValueError: - return _('the value does not follow "YYYYMMDDHHMMSS" time format') - - -class SIGRecord(UnsupportedDNSRecord): - # managed by BIND itself - rrtype = 'SIG' - rfc = 2535 - -class SPFRecord(UnsupportedDNSRecord): - rrtype = 'SPF' - rfc = 4408 - -class RRSIGRecord(UnsupportedDNSRecord): - # managed by BIND itself - rrtype = 'RRSIG' - rfc = 4034 - -class SSHFPRecord(DNSRecord): - rrtype = 'SSHFP' - rfc = 4255 - parts = ( - Int('algorithm', - label=_('Algorithm'), - minvalue=0, - maxvalue=255, - ), - Int('fp_type', - label=_('Fingerprint Type'), - minvalue=0, - maxvalue=255, - ), - Str('fingerprint', - label=_('Fingerprint'), - ), - ) - - def _get_part_values(self, value): - # fingerprint part can contain space in LDAP, return it as one part - values = value.split(None, 2) - if len(values) != len(self.parts): - return None - return tuple(values) - - -class TLSARecord(DNSRecord): - rrtype = 'TLSA' - rfc = 6698 - parts = ( - Int('cert_usage', - label=_('Certificate Usage'), - minvalue=0, - maxvalue=255, - ), - Int('selector', - label=_('Selector'), - minvalue=0, - maxvalue=255, - ), - Int('matching_type', - label=_('Matching Type'), - minvalue=0, - maxvalue=255, - ), - Str('cert_association_data', - label=_('Certificate Association Data'), - ), - ) - - -class TXTRecord(DNSRecord): - rrtype = 'TXT' - rfc = 1035 - parts = ( - Str('data', - label=_('Text Data'), - ), - ) - - def _get_part_values(self, value): - # ignore any space in TXT record - return (value,) - -_dns_records = ( - ARecord(), - AAAARecord(), - A6Record(), - AFSDBRecord(), - APLRecord(), - CERTRecord(), - CNAMERecord(), - DHCIDRecord(), - DLVRecord(), - DNAMERecord(), - DSRecord(), - HIPRecord(), - IPSECKEYRecord(), - KEYRecord(), - KXRecord(), - LOCRecord(), - MXRecord(), - NAPTRRecord(), - NSRecord(), - NSECRecord(), - PTRRecord(), - RRSIGRecord(), - RPRecord(), - SIGRecord(), - SPFRecord(), - SRVRecord(), - SSHFPRecord(), - TLSARecord(), - TXTRecord(), -) - -def __dns_record_options_iter(): - for opt in (Any('dnsrecords?', - label=_('Records'), - flags=['no_create', 'no_search', 'no_update'],), - Str('dnstype?', - label=_('Record type'), - flags=['no_create', 'no_search', 'no_update'],), - Str('dnsdata?', - label=_('Record data'), - flags=['no_create', 'no_search', 'no_update'],)): - # These 3 options are used in --structured format. They are defined - # rather in takes_params than has_output_params because of their - # order - they should be printed to CLI before any DNS part param - yield opt - for option in _dns_records: - yield option - - for part in option.get_parts(): - yield part - - for extra in option.get_extra(): - yield extra - -_dns_record_options = tuple(__dns_record_options_iter()) - - -def check_ns_rec_resolvable(zone, name, log): - assert isinstance(zone, DNSName) - assert isinstance(name, DNSName) - - if name.is_empty(): - name = zone.make_absolute() - elif not name.is_absolute(): - # this is a DNS name relative to the zone - name = name.derelativize(zone.make_absolute()) - try: - verify_host_resolvable(name) - except errors.DNSNotARecordError: - raise errors.NotFound( - reason=_('Nameserver \'%(host)s\' does not have a corresponding ' - 'A/AAAA record') % {'host': name} - ) - -def dns_container_exists(ldap): - try: - ldap.get_entry(DN(api.env.container_dns, api.env.basedn), []) - except errors.NotFound: - return False - return True - - -def dnssec_installed(ldap): - """ - * Method opendnssecinstance.get_dnssec_key_masters() CANNOT be used in the - dns plugin, or any plugin accessible for common users! * - Why?: The content of service container is not readable for common users. - - This method only try to find if a DNSSEC service container exists on any - replica. What means that DNSSEC key master is installed. - :param ldap: ldap connection - :return: True if DNSSEC was installed, otherwise False - """ - dn = DN(api.env.container_masters, api.env.basedn) - - filter_attrs = { - u'cn': u'DNSSEC', - u'objectclass': u'ipaConfigObject', - } - only_masters_f = ldap.make_filter(filter_attrs, rules=ldap.MATCH_ALL) - - try: - ldap.find_entries(filter=only_masters_f, base_dn=dn) - except errors.NotFound: - return False - return True - - -def default_zone_update_policy(zone): - if zone.is_reverse(): - return get_dns_reverse_zone_update_policy(api.env.realm, zone.ToASCII()) - else: - return get_dns_forward_zone_update_policy(api.env.realm) - -dnszone_output_params = ( - Str('managedby', - label=_('Managedby permission'), - ), -) - - -def _convert_to_idna(value): - """ - Function converts a unicode value to idna, without extra validation. - If conversion fails, None is returned - """ - assert isinstance(value, unicode) - - try: - idna_val = value - start_dot = u'' - end_dot = u'' - if idna_val.startswith(u'.'): - idna_val = idna_val[1:] - start_dot = u'.' - if idna_val.endswith(u'.'): - idna_val = idna_val[:-1] - end_dot = u'.' - idna_val = encodings.idna.nameprep(idna_val) - idna_val = re.split(r'(?<!\\)\.', idna_val) - idna_val = u'%s%s%s' % (start_dot, - u'.'.join(encodings.idna.ToASCII(x) - for x in idna_val), - end_dot) - return idna_val - except Exception: - pass - return None - - -def _create_idn_filter(cmd, ldap, term=None, **options): - if term: - #include idna values to search - term_idna = _convert_to_idna(term) - if term_idna and term != term_idna: - term = (term, term_idna) - - search_kw = {} - attr_extra_filters = [] - - for attr, value in cmd.args_options_2_entry(**options).items(): - if not isinstance(value, list): - value = [value] - for i, v in enumerate(value): - if isinstance(v, DNSName): - value[i] = v.ToASCII() - elif attr in map_names_to_records: - record = map_names_to_records[attr] - parts = record._get_part_values(v) - if parts is None: - value[i] = v - continue - try: - value[i] = record._part_values_to_string(parts) - except errors.ValidationError: - value[i] = v - - #create MATCH_ANY filter for multivalue - if len(value) > 1: - f = ldap.make_filter({attr: value}, rules=ldap.MATCH_ANY) - attr_extra_filters.append(f) - else: - search_kw[attr] = value - - if cmd.obj.search_attributes: - search_attrs = cmd.obj.search_attributes - else: - search_attrs = cmd.obj.default_attributes - if cmd.obj.search_attributes_config: - config = ldap.get_ipa_config() - config_attrs = config.get(cmd.obj.search_attributes_config, []) - if len(config_attrs) == 1 and (isinstance(config_attrs[0], - six.string_types)): - search_attrs = config_attrs[0].split(',') - - search_kw['objectclass'] = cmd.obj.object_class - attr_filter = ldap.make_filter(search_kw, rules=ldap.MATCH_ALL) - if attr_extra_filters: - #combine filter if there is any idna value - attr_extra_filters.append(attr_filter) - attr_filter = ldap.combine_filters(attr_extra_filters, - rules=ldap.MATCH_ALL) - - search_kw = {} - for a in search_attrs: - search_kw[a] = term - term_filter = ldap.make_filter(search_kw, exact=False) - - member_filter = cmd.get_member_filter(ldap, **options) - - filter = ldap.combine_filters( - (term_filter, attr_filter, member_filter), rules=ldap.MATCH_ALL - ) - return filter - - -map_names_to_records = {record_name_format % record.rrtype.lower(): record - for record in _dns_records if record.supported} - -def _records_idn_postprocess(record, **options): - for attr in record.keys(): - attr = attr.lower() - try: - param = map_names_to_records[attr] - except KeyError: - continue - if not isinstance(param, DNSRecord): - continue - - part_params = param.get_parts() - rrs = [] - for dnsvalue in record[attr]: - parts = param._get_part_values(dnsvalue) - if parts is None: - continue - parts = list(parts) - try: - for (i, p) in enumerate(parts): - if isinstance(part_params[i], DNSNameParam): - parts[i] = DNSName(p) - rrs.append(param._part_values_to_string(parts, - idna=options.get('raw', False))) - except (errors.ValidationError, errors.ConversionError): - rrs.append(dnsvalue) - record[attr] = rrs - -def _normalize_zone(zone): - if isinstance(zone, unicode): - # normalize only non-IDNA zones - try: - zone.encode('ascii') - except UnicodeError: - pass - else: - return zone.lower() - return zone - - -def _get_auth_zone_ldap(api, name): - """ - Find authoritative zone in LDAP for name. Only active zones are considered. - :param name: - :return: (zone, truncated) - zone: authoritative zone, or None if authoritative zone is not in LDAP - """ - assert isinstance(name, DNSName) - ldap = api.Backend.ldap2 - - # Create all possible parent zone names - search_name = name.make_absolute() - zone_names = [] - for i, name in enumerate(search_name): - zone_name_abs = DNSName(search_name[i:]).ToASCII() - zone_names.append(zone_name_abs) - # compatibility with IPA < 4.0, zone name can be relative - zone_names.append(zone_name_abs[:-1]) - - # Create filters - objectclass_filter = ldap.make_filter({'objectclass':'idnszone'}) - zonenames_filter = ldap.make_filter({'idnsname': zone_names}) - zoneactive_filter = ldap.make_filter({'idnsZoneActive': 'true'}) - complete_filter = ldap.combine_filters( - [objectclass_filter, zonenames_filter, zoneactive_filter], - rules=ldap.MATCH_ALL - ) - - try: - entries, truncated = ldap.find_entries( - filter=complete_filter, - attrs_list=['idnsname'], - base_dn=DN(api.env.container_dns, api.env.basedn), - scope=ldap.SCOPE_ONELEVEL - ) - except errors.NotFound: - return None, False - - # always use absolute zones - matched_auth_zones = [entry.single_value['idnsname'].make_absolute() - for entry in entries] - - # return longest match - return max(matched_auth_zones, key=len), truncated - - -def _get_longest_match_ns_delegation_ldap(api, zone, name): - """ - Searches for deepest delegation for name in LDAP zone. - - NOTE: NS record in zone apex is not considered as delegation. - It returns None if there is no delegation outside of zone apex. - - Example: - zone: example.com. - name: ns.sub.example.com. - - records: - extra.ns.sub.example.com. - sub.example.com. - example.com - - result: sub.example.com. - - :param zone: zone name - :param name: - :return: (match, truncated); - match: delegation name if success, or None if no delegation record exists - """ - assert isinstance(zone, DNSName) - assert isinstance(name, DNSName) - - ldap = api.Backend.ldap2 - - # get zone DN - zone_dn = api.Object.dnszone.get_dn(zone) - - if name.is_absolute(): - relative_record_name = name.relativize(zone.make_absolute()) - else: - relative_record_name = name - - # Name is zone apex - if relative_record_name.is_empty(): - return None, False - - # create list of possible record names - possible_record_names = [DNSName(relative_record_name[i:]).ToASCII() - for i in range(len(relative_record_name))] - - # search filters - name_filter = ldap.make_filter({'idnsname': [possible_record_names]}) - objectclass_filter = ldap.make_filter({'objectclass': 'idnsrecord'}) - complete_filter = ldap.combine_filters( - [name_filter, objectclass_filter], - rules=ldap.MATCH_ALL - ) - - try: - entries, truncated = ldap.find_entries( - filter=complete_filter, - attrs_list=['idnsname', 'nsrecord'], - base_dn=zone_dn, - scope=ldap.SCOPE_ONELEVEL - ) - except errors.NotFound: - return None, False - - matched_records = [] - - # test if entry contains NS records - for entry in entries: - if entry.get('nsrecord'): - matched_records.append(entry.single_value['idnsname']) - - if not matched_records: - return None, truncated - - # return longest match - return max(matched_records, key=len), truncated - - -def _find_subtree_forward_zones_ldap(api, name, child_zones_only=False): - """ - Search for forwardzone <name> and all child forwardzones - Filter: (|(*.<name>.)(<name>.)) - :param name: - :param child_zones_only: search only for child zones - :return: (list of zonenames, truncated), list is empty if no zone found - """ - assert isinstance(name, DNSName) - ldap = api.Backend.ldap2 - - # prepare for filter "*.<name>." - search_name = u".%s" % name.make_absolute().ToASCII() - - # we need to search zone with and without last dot, due compatibility - # with IPA < 4.0 - search_names = [search_name, search_name[:-1]] - - # Create filters - objectclass_filter = ldap.make_filter({'objectclass':'idnsforwardzone'}) - zonenames_filter = ldap.make_filter({'idnsname': search_names}, exact=False, - trailing_wildcard=False) - if not child_zones_only: - # find also zone with exact name - exact_name = name.make_absolute().ToASCII() - # we need to search zone with and without last dot, due compatibility - # with IPA < 4.0 - exact_names = [exact_name, exact_name[-1]] - exact_name_filter = ldap.make_filter({'idnsname': exact_names}) - zonenames_filter = ldap.combine_filters([zonenames_filter, - exact_name_filter]) - - zoneactive_filter = ldap.make_filter({'idnsZoneActive': 'true'}) - complete_filter = ldap.combine_filters( - [objectclass_filter, zonenames_filter, zoneactive_filter], - rules=ldap.MATCH_ALL - ) - - try: - entries, truncated = ldap.find_entries( - filter=complete_filter, - attrs_list=['idnsname'], - base_dn=DN(api.env.container_dns, api.env.basedn), - scope=ldap.SCOPE_ONELEVEL - ) - except errors.NotFound: - return [], False - - result = [entry.single_value['idnsname'].make_absolute() - for entry in entries] - - return result, truncated - - -def _get_zone_which_makes_fw_zone_ineffective(api, fwzonename): - """ - Check if forward zone is effective. - - If parent zone exists as authoritative zone, the forward zone will not - forward queries by default. It is necessary to delegate authority - to forward zone with a NS record. - - Example: - - Forward zone: sub.example.com - Zone: example.com - - Forwarding will not work, because the server thinks it is authoritative - for zone and will return NXDOMAIN - - Adding record: sub.example.com NS ns.sub.example.com. - will delegate authority, and IPA DNS server will forward DNS queries. - - :param fwzonename: forwardzone - :return: (zone, truncated) - zone: None if effective, name of authoritative zone otherwise - """ - assert isinstance(fwzonename, DNSName) - - auth_zone, truncated_zone = _get_auth_zone_ldap(api, fwzonename) - if not auth_zone: - return None, truncated_zone - - delegation_record_name, truncated_ns =\ - _get_longest_match_ns_delegation_ldap(api, auth_zone, fwzonename) - - truncated = truncated_ns or truncated_zone - - if delegation_record_name: - return None, truncated - - return auth_zone, truncated - - -def _add_warning_fw_zone_is_not_effective(api, result, fwzone, version): - """ - Adds warning message to result, if required - """ - authoritative_zone, truncated = \ - _get_zone_which_makes_fw_zone_ineffective(api, fwzone) - if authoritative_zone: - # forward zone is not effective and forwarding will not work - messages.add_message( - version, result, - messages.ForwardzoneIsNotEffectiveWarning( - fwzone=fwzone, authzone=authoritative_zone, - ns_rec=fwzone.relativize(authoritative_zone) - ) - ) - - -def _add_warning_fw_policy_conflict_aez(result, fwzone, **options): - """Warn if forwarding policy conflicts with an automatic empty zone.""" - fwd_policy = result['result'].get(u'idnsforwardpolicy', - dnsforwardzone.default_forward_policy) - if ( - fwd_policy != [u'only'] - and related_to_auto_empty_zone(DNSName(fwzone)) - ): - messages.add_message( - options['version'], result, - messages.DNSForwardPolicyConflictWithEmptyZone() - ) - - -class DNSZoneBase(LDAPObject): - """ - Base class for DNS Zone - """ - container_dn = api.env.container_dns - object_class = ['top'] - possible_objectclasses = ['ipadnszone'] - default_attributes = [ - 'idnsname', 'idnszoneactive', 'idnsforwarders', 'idnsforwardpolicy' - ] - - takes_params = ( - DNSNameParam('idnsname', - _no_wildcard_validator, # RFC 4592 section 4 - only_absolute=True, - cli_name='name', - label=_('Zone name'), - doc=_('Zone name (FQDN)'), - default_from=lambda name_from_ip: _reverse_zone_name(name_from_ip), - normalizer=_normalize_zone, - primary_key=True, - ), - Str('name_from_ip?', _validate_ipnet, - label=_('Reverse zone IP network'), - doc=_('IP network to create reverse zone name from'), - flags=('virtual_attribute',), - ), - Bool('idnszoneactive?', - cli_name='zone_active', - label=_('Active zone'), - doc=_('Is zone active?'), - flags=['no_create', 'no_update'], - attribute=True, - ), - Str('idnsforwarders*', - _validate_bind_forwarder, - cli_name='forwarder', - label=_('Zone forwarders'), - doc=_('Per-zone forwarders. A custom port can be specified ' - 'for each forwarder using a standard format "IP_ADDRESS port PORT"'), - ), - StrEnum('idnsforwardpolicy?', - cli_name='forward_policy', - label=_('Forward policy'), - doc=_('Per-zone conditional forwarding policy. Set to "none" to ' - 'disable forwarding to global forwarder for this zone. In ' - 'that case, conditional zone forwarders are disregarded.'), - values=(u'only', u'first', u'none'), - ), - - ) - - def get_dn(self, *keys, **options): - if not dns_container_exists(self.api.Backend.ldap2): - raise errors.NotFound(reason=_('DNS is not configured')) - - zone = keys[-1] - assert isinstance(zone, DNSName) - assert zone.is_absolute() - zone_a = zone.ToASCII() - - # special case when zone is the root zone ('.') - if zone == DNSName.root: - return super(DNSZoneBase, self).get_dn(zone_a, **options) - - # try first relative name, a new zone has to be added as absolute - # otherwise ObjectViolation is raised - zone_a = zone_a[:-1] - dn = super(DNSZoneBase, self).get_dn(zone_a, **options) - try: - self.backend.get_entry(dn, ['']) - except errors.NotFound: - zone_a = u"%s." % zone_a - dn = super(DNSZoneBase, self).get_dn(zone_a, **options) - - return dn - - def permission_name(self, zone): - assert isinstance(zone, DNSName) - return u"Manage DNS zone %s" % zone.ToASCII() - - def get_name_in_zone(self, zone, hostname): - """ - Get name of a record that is to be added to a new zone. I.e. when - we want to add record "ipa.lab.example.com" in a zone "example.com", - this function should return "ipa.lab". Returns None when record cannot - be added to a zone. Returns '@' when the hostname is the zone record. - """ - assert isinstance(zone, DNSName) - assert zone.is_absolute() - assert isinstance(hostname, DNSName) - - if not hostname.is_absolute(): - return hostname - - if hostname.is_subdomain(zone): - return hostname.relativize(zone) - - return None - - def _remove_permission(self, zone): - permission_name = self.permission_name(zone) - try: - self.api.Command['permission_del'](permission_name, force=True) - except errors.NotFound as e: - if zone == DNSName.root: # special case root zone - raise - # compatibility, older IPA versions which allows to create zone - # without absolute zone name - permission_name_rel = self.permission_name( - zone.relativize(DNSName.root) - ) - try: - self.api.Command['permission_del'](permission_name_rel, - force=True) - except errors.NotFound: - raise e # re-raise original exception - - def _make_zonename_absolute(self, entry_attrs, **options): - """ - Zone names can be relative in IPA < 4.0, make sure we always return - absolute zone name from ldap - """ - if options.get('raw'): - return - - if "idnsname" in entry_attrs: - entry_attrs.single_value['idnsname'] = ( - entry_attrs.single_value['idnsname'].make_absolute()) - - -class DNSZoneBase_add(LDAPCreate): - - takes_options = LDAPCreate.takes_options + ( - Flag('skip_overlap_check', - doc=_('Force DNS zone creation even if it will overlap with ' - 'an existing zone.') - ), - ) - - has_output_params = LDAPCreate.has_output_params + dnszone_output_params - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - - try: - entry = ldap.get_entry(dn) - except errors.NotFound: - pass - else: - if _check_entry_objectclass(entry, self.obj.object_class): - self.obj.handle_duplicate_entry(*keys) - else: - raise errors.DuplicateEntry( - message=_(u'Only one zone type is allowed per zone name') - ) - - entry_attrs['idnszoneactive'] = 'TRUE' - - if not options['skip_overlap_check']: - try: - check_zone_overlap(keys[-1]) - except ValueError as e: - raise errors.InvocationError(e.message) - - return dn - - -class DNSZoneBase_del(LDAPDelete): - - def pre_callback(self, ldap, dn, *nkeys, **options): - assert isinstance(dn, DN) - if not _check_DN_objectclass(ldap, dn, self.obj.object_class): - self.obj.handle_not_found(*nkeys) - return dn - - def post_callback(self, ldap, dn, *keys, **options): - try: - self.obj._remove_permission(keys[-1]) - except errors.NotFound: - pass - - return True - - -class DNSZoneBase_mod(LDAPUpdate): - has_output_params = LDAPUpdate.has_output_params + dnszone_output_params - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - self.obj._make_zonename_absolute(entry_attrs, **options) - return dn - - -class DNSZoneBase_find(LDAPSearch): - __doc__ = _('Search for DNS zones (SOA records).') - - has_output_params = LDAPSearch.has_output_params + dnszone_output_params - - def args_options_2_params(self, *args, **options): - # FIXME: Check that name_from_ip is valid. This is necessary because - # custom validation rules, including _validate_ipnet, are not - # used when doing a search. Once we have a parameter type for - # IP network objects, this will no longer be necessary, as the - # parameter type will handle the validation itself (see - # <https://fedorahosted.org/freeipa/ticket/2266>). - if 'name_from_ip' in options: - self.obj.params['name_from_ip'](unicode(options['name_from_ip'])) - return super(DNSZoneBase_find, self).args_options_2_params(*args, **options) - - def args_options_2_entry(self, *args, **options): - if 'name_from_ip' in options: - if 'idnsname' not in options: - options['idnsname'] = self.obj.params['idnsname'].get_default(**options) - del options['name_from_ip'] - search_kw = super(DNSZoneBase_find, self).args_options_2_entry(*args, - **options) - name = search_kw.get('idnsname') - if name: - search_kw['idnsname'] = [name, name.relativize(DNSName.root)] - return search_kw - - def pre_callback(self, ldap, filter, attrs_list, base_dn, scope, *args, **options): - assert isinstance(base_dn, DN) - # Check if DNS container exists must be here for find methods - if not dns_container_exists(self.api.Backend.ldap2): - raise errors.NotFound(reason=_('DNS is not configured')) - filter = _create_idn_filter(self, ldap, *args, **options) - return (filter, base_dn, scope) - - def post_callback(self, ldap, entries, truncated, *args, **options): - for entry_attrs in entries: - self.obj._make_zonename_absolute(entry_attrs, **options) - return truncated - - -class DNSZoneBase_show(LDAPRetrieve): - has_output_params = LDAPRetrieve.has_output_params + dnszone_output_params - - def pre_callback(self, ldap, dn, attrs_list, *keys, **options): - assert isinstance(dn, DN) - if not _check_DN_objectclass(ldap, dn, self.obj.object_class): - self.obj.handle_not_found(*keys) - return dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - self.obj._make_zonename_absolute(entry_attrs, **options) - return dn - - -class DNSZoneBase_disable(LDAPQuery): - has_output = output.standard_value - - def execute(self, *keys, **options): - ldap = self.obj.backend - - dn = self.obj.get_dn(*keys, **options) - try: - entry = ldap.get_entry(dn, ['idnszoneactive', 'objectclass']) - except errors.NotFound: - self.obj.handle_not_found(*keys) - - if not _check_entry_objectclass(entry, self.obj.object_class): - self.obj.handle_not_found(*keys) - - entry['idnszoneactive'] = ['FALSE'] - - try: - ldap.update_entry(entry) - except errors.EmptyModlist: - pass - - return dict(result=True, value=pkey_to_value(keys[-1], options)) - - -class DNSZoneBase_enable(LDAPQuery): - has_output = output.standard_value - - def execute(self, *keys, **options): - ldap = self.obj.backend - - dn = self.obj.get_dn(*keys, **options) - try: - entry = ldap.get_entry(dn, ['idnszoneactive', 'objectclass']) - except errors.NotFound: - self.obj.handle_not_found(*keys) - - if not _check_entry_objectclass(entry, self.obj.object_class): - self.obj.handle_not_found(*keys) - - entry['idnszoneactive'] = ['TRUE'] - - try: - ldap.update_entry(entry) - except errors.EmptyModlist: - pass - - return dict(result=True, value=pkey_to_value(keys[-1], options)) - - -class DNSZoneBase_add_permission(LDAPQuery): - has_output = _output_permissions - msg_summary = _('Added system permission "%(value)s"') - - def execute(self, *keys, **options): - ldap = self.obj.backend - dn = self.obj.get_dn(*keys, **options) - - try: - entry_attrs = ldap.get_entry(dn, ['objectclass']) - except errors.NotFound: - self.obj.handle_not_found(*keys) - else: - if not _check_entry_objectclass(entry_attrs, self.obj.object_class): - self.obj.handle_not_found(*keys) - - permission_name = self.obj.permission_name(keys[-1]) - - # compatibility with older IPA versions which allows relative zonenames - if keys[-1] != DNSName.root: # special case root zone - permission_name_rel = self.obj.permission_name( - keys[-1].relativize(DNSName.root) - ) - try: - self.api.Object['permission'].get_dn_if_exists( - permission_name_rel) - except errors.NotFound: - pass - else: - # permission exists without absolute domain name - raise errors.DuplicateEntry( - message=_('permission "%(value)s" already exists') % { - 'value': permission_name - } - ) - - permission = self.api.Command['permission_add_noaci'](permission_name, - ipapermissiontype=u'SYSTEM' - )['result'] - - dnszone_ocs = entry_attrs.get('objectclass') - if dnszone_ocs: - for oc in dnszone_ocs: - if oc.lower() == 'ipadnszone': - break - else: - dnszone_ocs.append('ipadnszone') - - entry_attrs['managedby'] = [permission['dn']] - ldap.update_entry(entry_attrs) - - return dict( - result=True, - value=pkey_to_value(permission_name, options), - ) - - -class DNSZoneBase_remove_permission(LDAPQuery): - has_output = _output_permissions - msg_summary = _('Removed system permission "%(value)s"') - - def execute(self, *keys, **options): - ldap = self.obj.backend - dn = self.obj.get_dn(*keys, **options) - try: - entry = ldap.get_entry(dn, ['managedby', 'objectclass']) - except errors.NotFound: - self.obj.handle_not_found(*keys) - else: - if not _check_entry_objectclass(entry, self.obj.object_class): - self.obj.handle_not_found(*keys) - - entry['managedby'] = None - - try: - ldap.update_entry(entry) - except errors.EmptyModlist: - # managedBy attribute is clean, lets make sure there is also no - # dangling DNS zone permission - pass - - permission_name = self.obj.permission_name(keys[-1]) - self.obj._remove_permission(keys[-1]) - - return dict( - result=True, - value=pkey_to_value(permission_name, options), - ) - - -@register() -class dnszone(DNSZoneBase): - """ - DNS Zone, container for resource records. - """ - object_name = _('DNS zone') - object_name_plural = _('DNS zones') - object_class = DNSZoneBase.object_class + ['idnsrecord', 'idnszone'] - default_attributes = DNSZoneBase.default_attributes + [ - 'idnssoamname', 'idnssoarname', 'idnssoaserial', 'idnssoarefresh', - 'idnssoaretry', 'idnssoaexpire', 'idnssoaminimum', 'idnsallowquery', - 'idnsallowtransfer', 'idnssecinlinesigning', - ] + _record_attributes - label = _('DNS Zones') - label_singular = _('DNS Zone') - - takes_params = DNSZoneBase.takes_params + ( - DNSNameParam('idnssoamname?', - cli_name='name_server', - label=_('Authoritative nameserver'), - doc=_('Authoritative nameserver domain name'), - default=None, # value will be added in precallback from ldap - ), - DNSNameParam('idnssoarname', - _rname_validator, - cli_name='admin_email', - label=_('Administrator e-mail address'), - doc=_('Administrator e-mail address'), - default=DNSName(u'hostmaster'), - normalizer=normalize_zonemgr, - autofill=True, - ), - Int('idnssoaserial', - cli_name='serial', - label=_('SOA serial'), - doc=_('SOA record serial number'), - minvalue=1, - maxvalue=4294967295, - default_from=_create_zone_serial, - autofill=True, - ), - Int('idnssoarefresh', - cli_name='refresh', - label=_('SOA refresh'), - doc=_('SOA record refresh time'), - minvalue=0, - maxvalue=2147483647, - default=3600, - autofill=True, - ), - Int('idnssoaretry', - cli_name='retry', - label=_('SOA retry'), - doc=_('SOA record retry time'), - minvalue=0, - maxvalue=2147483647, - default=900, - autofill=True, - ), - Int('idnssoaexpire', - cli_name='expire', - label=_('SOA expire'), - doc=_('SOA record expire time'), - default=1209600, - minvalue=0, - maxvalue=2147483647, - autofill=True, - ), - Int('idnssoaminimum', - cli_name='minimum', - label=_('SOA minimum'), - doc=_('How long should negative responses be cached'), - default=3600, - minvalue=0, - maxvalue=2147483647, - autofill=True, - ), - Int('dnsttl?', - cli_name='ttl', - label=_('Time to live'), - doc=_('Time to live for records at zone apex'), - minvalue=0, - maxvalue=2147483647, # see RFC 2181 - ), - StrEnum('dnsclass?', - # Deprecated - cli_name='class', - flags=['no_option'], - values=_record_classes, - ), - Str('idnsupdatepolicy?', - cli_name='update_policy', - label=_('BIND update policy'), - doc=_('BIND update policy'), - default_from=lambda idnsname: default_zone_update_policy(idnsname), - autofill=True - ), - Bool('idnsallowdynupdate?', - cli_name='dynamic_update', - label=_('Dynamic update'), - doc=_('Allow dynamic updates.'), - attribute=True, - default=False, - autofill=True - ), - Str('idnsallowquery?', - _validate_bind_aci, - normalizer=_normalize_bind_aci, - cli_name='allow_query', - label=_('Allow query'), - doc=_('Semicolon separated list of IP addresses or networks which are allowed to issue queries'), - default=u'any;', # anyone can issue queries by default - autofill=True, - ), - Str('idnsallowtransfer?', - _validate_bind_aci, - normalizer=_normalize_bind_aci, - cli_name='allow_transfer', - label=_('Allow transfer'), - doc=_('Semicolon separated list of IP addresses or networks which are allowed to transfer the zone'), - default=u'none;', # no one can issue queries by default - autofill=True, - ), - Bool('idnsallowsyncptr?', - cli_name='allow_sync_ptr', - label=_('Allow PTR sync'), - doc=_('Allow synchronization of forward (A, AAAA) and reverse (PTR) records in the zone'), - ), - Bool('idnssecinlinesigning?', - cli_name='dnssec', - default=False, - label=_('Allow in-line DNSSEC signing'), - doc=_('Allow inline DNSSEC signing of records in the zone'), - ), - Str('nsec3paramrecord?', - _validate_nsec3param_record, - cli_name='nsec3param_rec', - label=_('NSEC3PARAM record'), - doc=_('NSEC3PARAM record for zone in format: hash_algorithm flags iterations salt'), - pattern=r'^\d+ \d+ \d+ (([0-9a-fA-F]{2})+|-)$', - pattern_errmsg=(u'expected format: <0-255> <0-255> <0-65535> ' - 'even-length_hexadecimal_digits_or_hyphen'), - ), - ) - # Permissions will be apllied for forwardzones too - # Store permissions into api.env.basedn, dns container could not exists - managed_permissions = { - 'System: Add DNS Entries': { - 'non_object': True, - 'ipapermright': {'add'}, - 'ipapermlocation': api.env.basedn, - 'ipapermtarget': DN('idnsname=*', 'cn=dns', api.env.basedn), - 'replaces': [ - '(target = "ldap:///idnsname=*,cn=dns,$SUFFIX")(version 3.0;acl "permission:add dns entries";allow (add) groupdn = "ldap:///cn=add dns entries,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'DNS Administrators', 'DNS Servers'}, - }, - 'System: Read DNS Entries': { - 'non_object': True, - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermlocation': api.env.basedn, - 'ipapermtarget': DN('idnsname=*', 'cn=dns', api.env.basedn), - 'ipapermdefaultattr': { - 'objectclass', - 'a6record', 'aaaarecord', 'afsdbrecord', 'aplrecord', 'arecord', - 'certrecord', 'cn', 'cnamerecord', 'dhcidrecord', 'dlvrecord', - 'dnamerecord', 'dnsclass', 'dnsttl', 'dsrecord', - 'hinforecord', 'hiprecord', 'idnsallowdynupdate', - 'idnsallowquery', 'idnsallowsyncptr', 'idnsallowtransfer', - 'idnsforwarders', 'idnsforwardpolicy', 'idnsname', - 'idnssecinlinesigning', 'idnssoaexpire', 'idnssoaminimum', - 'idnssoamname', 'idnssoarefresh', 'idnssoaretry', - 'idnssoarname', 'idnssoaserial', 'idnsupdatepolicy', - 'idnszoneactive', 'ipseckeyrecord','keyrecord', 'kxrecord', - 'locrecord', 'managedby', 'mdrecord', 'minforecord', - 'mxrecord', 'naptrrecord', 'nsecrecord', 'nsec3paramrecord', - 'nsrecord', 'nxtrecord', 'ptrrecord', 'rprecord', 'rrsigrecord', - 'sigrecord', 'spfrecord', 'srvrecord', 'sshfprecord', - 'tlsarecord', 'txtrecord', 'unknownrecord', - }, - 'replaces_system': ['Read DNS Entries'], - 'default_privileges': {'DNS Administrators', 'DNS Servers'}, - }, - 'System: Remove DNS Entries': { - 'non_object': True, - 'ipapermright': {'delete'}, - 'ipapermlocation': api.env.basedn, - 'ipapermtarget': DN('idnsname=*', 'cn=dns', api.env.basedn), - 'replaces': [ - '(target = "ldap:///idnsname=*,cn=dns,$SUFFIX")(version 3.0;acl "permission:remove dns entries";allow (delete) groupdn = "ldap:///cn=remove dns entries,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'DNS Administrators', 'DNS Servers'}, - }, - 'System: Update DNS Entries': { - 'non_object': True, - 'ipapermright': {'write'}, - 'ipapermlocation': api.env.basedn, - 'ipapermtarget': DN('idnsname=*', 'cn=dns', api.env.basedn), - 'ipapermdefaultattr': { - 'a6record', 'aaaarecord', 'afsdbrecord', 'aplrecord', 'arecord', - 'certrecord', 'cn', 'cnamerecord', 'dhcidrecord', 'dlvrecord', - 'dnamerecord', 'dnsclass', 'dnsttl', 'dsrecord', - 'hinforecord', 'hiprecord', 'idnsallowdynupdate', - 'idnsallowquery', 'idnsallowsyncptr', 'idnsallowtransfer', - 'idnsforwarders', 'idnsforwardpolicy', 'idnsname', - 'idnssecinlinesigning', 'idnssoaexpire', 'idnssoaminimum', - 'idnssoamname', 'idnssoarefresh', 'idnssoaretry', - 'idnssoarname', 'idnssoaserial', 'idnsupdatepolicy', - 'idnszoneactive', 'ipseckeyrecord','keyrecord', 'kxrecord', - 'locrecord', 'managedby', 'mdrecord', 'minforecord', - 'mxrecord', 'naptrrecord', 'nsecrecord', 'nsec3paramrecord', - 'nsrecord', 'nxtrecord', 'ptrrecord', 'rprecord', 'rrsigrecord', - 'sigrecord', 'spfrecord', 'srvrecord', 'sshfprecord', - 'tlsarecord', 'txtrecord', 'unknownrecord', - }, - 'replaces': [ - '(targetattr = "idnsname || cn || idnsallowdynupdate || dnsttl || dnsclass || arecord || aaaarecord || a6record || nsrecord || cnamerecord || ptrrecord || srvrecord || txtrecord || mxrecord || mdrecord || hinforecord || minforecord || afsdbrecord || sigrecord || keyrecord || locrecord || nxtrecord || naptrrecord || kxrecord || certrecord || dnamerecord || dsrecord || sshfprecord || rrsigrecord || nsecrecord || idnsname || idnszoneactive || idnssoamname || idnssoarname || idnssoaserial || idnssoarefresh || idnssoaretry || idnssoaexpire || idnssoaminimum || idnsupdatepolicy")(target = "ldap:///idnsname=*,cn=dns,$SUFFIX")(version 3.0;acl "permission:update dns entries";allow (write) groupdn = "ldap:///cn=update dns entries,cn=permissions,cn=pbac,$SUFFIX";)', - '(targetattr = "idnsname || cn || idnsallowdynupdate || dnsttl || dnsclass || arecord || aaaarecord || a6record || nsrecord || cnamerecord || ptrrecord || srvrecord || txtrecord || mxrecord || mdrecord || hinforecord || minforecord || afsdbrecord || sigrecord || keyrecord || locrecord || nxtrecord || naptrrecord || kxrecord || certrecord || dnamerecord || dsrecord || sshfprecord || rrsigrecord || nsecrecord || idnsname || idnszoneactive || idnssoamname || idnssoarname || idnssoaserial || idnssoarefresh || idnssoaretry || idnssoaexpire || idnssoaminimum || idnsupdatepolicy || idnsallowquery || idnsallowtransfer || idnsallowsyncptr || idnsforwardpolicy || idnsforwarders")(target = "ldap:///idnsname=*,cn=dns,$SUFFIX")(version 3.0;acl "permission:update dns entries";allow (write) groupdn = "ldap:///cn=update dns entries,cn=permissions,cn=pbac,$SUFFIX";)', - '(targetattr = "idnsname || cn || idnsallowdynupdate || dnsttl || dnsclass || arecord || aaaarecord || a6record || nsrecord || cnamerecord || ptrrecord || srvrecord || txtrecord || mxrecord || mdrecord || hinforecord || minforecord || afsdbrecord || sigrecord || keyrecord || locrecord || nxtrecord || naptrrecord || kxrecord || certrecord || dnamerecord || dsrecord || sshfprecord || rrsigrecord || nsecrecord || idnsname || idnszoneactive || idnssoamname || idnssoarname || idnssoaserial || idnssoarefresh || idnssoaretry || idnssoaexpire || idnssoaminimum || idnsupdatepolicy || idnsallowquery || idnsallowtransfer || idnsallowsyncptr || idnsforwardpolicy || idnsforwarders || managedby")(target = "ldap:///idnsname=*,cn=dns,$SUFFIX")(version 3.0;acl "permission:update dns entries";allow (write) groupdn = "ldap:///cn=update dns entries,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'DNS Administrators', 'DNS Servers'}, - }, - 'System: Read DNSSEC metadata': { - 'non_object': True, - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermlocation': api.env.basedn, - 'ipapermtarget': DN('cn=dns', api.env.basedn), - 'ipapermtargetfilter': ['(objectclass=idnsSecKey)'], - 'ipapermdefaultattr': { - 'idnsSecAlgorithm', 'idnsSecKeyCreated', 'idnsSecKeyPublish', - 'idnsSecKeyActivate', 'idnsSecKeyInactive', 'idnsSecKeyDelete', - 'idnsSecKeyZone', 'idnsSecKeyRevoke', 'idnsSecKeySep', - 'idnsSecKeyRef', 'cn', 'objectclass', - }, - 'default_privileges': {'DNS Administrators'}, - }, - 'System: Manage DNSSEC metadata': { - 'non_object': True, - 'ipapermright': {'all'}, - 'ipapermlocation': api.env.basedn, - 'ipapermtarget': DN('cn=dns', api.env.basedn), - 'ipapermtargetfilter': ['(objectclass=idnsSecKey)'], - 'ipapermdefaultattr': { - 'idnsSecAlgorithm', 'idnsSecKeyCreated', 'idnsSecKeyPublish', - 'idnsSecKeyActivate', 'idnsSecKeyInactive', 'idnsSecKeyDelete', - 'idnsSecKeyZone', 'idnsSecKeyRevoke', 'idnsSecKeySep', - 'idnsSecKeyRef', 'cn', 'objectclass', - }, - 'default_privileges': {'DNS Servers'}, - }, - 'System: Manage DNSSEC keys': { - 'non_object': True, - 'ipapermright': {'all'}, - 'ipapermlocation': api.env.basedn, - 'ipapermtarget': DN('cn=keys', 'cn=sec', 'cn=dns', api.env.basedn), - 'ipapermdefaultattr': { - 'ipaPublicKey', 'ipaPrivateKey', 'ipaSecretKey', - 'ipaWrappingMech','ipaWrappingKey', - 'ipaSecretKeyRef', 'ipk11Private', 'ipk11Modifiable', 'ipk11Label', - 'ipk11Copyable', 'ipk11Destroyable', 'ipk11Trusted', - 'ipk11CheckValue', 'ipk11StartDate', 'ipk11EndDate', - 'ipk11UniqueId', 'ipk11PublicKeyInfo', 'ipk11Distrusted', - 'ipk11Subject', 'ipk11Id', 'ipk11Local', 'ipk11KeyType', - 'ipk11Derive', 'ipk11KeyGenMechanism', 'ipk11AllowedMechanisms', - 'ipk11Encrypt', 'ipk11Verify', 'ipk11VerifyRecover', 'ipk11Wrap', - 'ipk11WrapTemplate', 'ipk11Sensitive', 'ipk11Decrypt', - 'ipk11Sign', 'ipk11SignRecover', 'ipk11Unwrap', - 'ipk11Extractable', 'ipk11AlwaysSensitive', - 'ipk11NeverExtractable', 'ipk11WrapWithTrusted', - 'ipk11UnwrapTemplate', 'ipk11AlwaysAuthenticate', - 'objectclass', - }, - 'default_privileges': {'DNS Servers'}, - }, - } - - def _rr_zone_postprocess(self, record, **options): - #Decode IDN ACE form to Unicode, raw records are passed directly from LDAP - if options.get('raw', False): - return - _records_idn_postprocess(record, **options) - - def _warning_forwarding(self, result, **options): - if ('idnsforwarders' in result['result']): - messages.add_message(options.get('version', VERSION_WITHOUT_CAPABILITIES), - result, messages.ForwardersWarning()) - - def _warning_name_server_option(self, result, context, **options): - if getattr(context, 'show_warning_nameserver_option', False): - messages.add_message( - options['version'], - result, messages.OptionSemanticChangedWarning( - label=_(u"setting Authoritative nameserver"), - current_behavior=_(u"It is used only for setting the " - u"SOA MNAME attribute."), - hint=_(u"NS record(s) can be edited in zone apex - '@'. ") - ) - ) - - def _warning_fw_zone_is_not_effective(self, result, *keys, **options): - """ - Warning if any operation with zone causes, a child forward zone is - not effective - """ - zone = keys[-1] - affected_fw_zones, truncated = _find_subtree_forward_zones_ldap( - self.api, zone, child_zones_only=True) - if not affected_fw_zones: - return - - for fwzone in affected_fw_zones: - _add_warning_fw_zone_is_not_effective(self.api, result, fwzone, - options['version']) - - def _warning_dnssec_master_is_not_installed(self, result, **options): - dnssec_enabled = result['result'].get("idnssecinlinesigning", False) - if dnssec_enabled and not dnssec_installed(self.api.Backend.ldap2): - messages.add_message( - options['version'], - result, - messages.DNSSECMasterNotInstalled() - ) - - -@register() -class dnszone_add(DNSZoneBase_add): - __doc__ = _('Create new DNS zone (SOA record).') - - takes_options = DNSZoneBase_add.takes_options + ( - Flag('force', - doc=_('Force DNS zone creation even if nameserver is not ' - 'resolvable. (Deprecated)'), - ), - - Flag('skip_nameserver_check', - doc=_('Force DNS zone creation even if nameserver is not ' - 'resolvable.'), - ), - - # Deprecated - # ip-address option is not used anymore, we have to keep it - # due to compability with clients older than 4.1 - Str('ip_address?', - flags=['no_option', ] - ), - ) - - def _warning_deprecated_option(self, result, **options): - if 'ip_address' in options: - messages.add_message( - options['version'], - result, - messages.OptionDeprecatedWarning( - option='ip-address', - additional_info=u"Value will be ignored.") - ) - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - - if options.get('force'): - options['skip_nameserver_check'] = True - - dn = super(dnszone_add, self).pre_callback( - ldap, dn, entry_attrs, attrs_list, *keys, **options) - - nameservers = [normalize_zone(x) for x in - self.api.Object.dnsrecord.get_dns_masters()] - server = normalize_zone(api.env.host) - zone = keys[-1] - - if entry_attrs.get('idnssoamname'): - if zone.is_reverse() and not entry_attrs['idnssoamname'].is_absolute(): - raise errors.ValidationError( - name='name-server', - error=_("Nameserver for reverse zone cannot be a relative DNS name")) - - # verify if user specified server is resolvable - if not options['skip_nameserver_check']: - check_ns_rec_resolvable(keys[0], entry_attrs['idnssoamname'], - self.log) - # show warning about --name-server option - context.show_warning_nameserver_option = True - else: - # user didn't specify SOA mname - if server in nameservers: - # current ipa server is authoritative nameserver in SOA record - entry_attrs['idnssoamname'] = [server] - else: - # a first DNS capable server is authoritative nameserver in SOA record - entry_attrs['idnssoamname'] = [nameservers[0]] - - # all ipa DNS servers should be in NS zone record (as absolute domain name) - entry_attrs['nsrecord'] = nameservers - - return dn - - def execute(self, *keys, **options): - result = super(dnszone_add, self).execute(*keys, **options) - self._warning_deprecated_option(result, **options) - self.obj._warning_forwarding(result, **options) - self.obj._warning_name_server_option(result, context, **options) - self.obj._warning_fw_zone_is_not_effective(result, *keys, **options) - self.obj._warning_dnssec_master_is_not_installed(result, **options) - return result - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - - # Add entry to realmdomains - # except for our own domain, forward zones, reverse zones and root zone - zone = keys[0] - - if (zone != DNSName(api.env.domain).make_absolute() and - not options.get('idnsforwarders') and - not zone.is_reverse() and - zone != DNSName.root): - try: - self.api.Command['realmdomains_mod'](add_domain=unicode(zone), - force=True) - except (errors.EmptyModlist, errors.ValidationError): - pass - - self.obj._rr_zone_postprocess(entry_attrs, **options) - return dn - - - -@register() -class dnszone_del(DNSZoneBase_del): - __doc__ = _('Delete DNS zone (SOA record).') - - msg_summary = _('Deleted DNS zone "%(value)s"') - - def execute(self, *keys, **options): - result = super(dnszone_del, self).execute(*keys, **options) - nkeys = keys[-1] # we can delete more zones - for key in nkeys: - self.obj._warning_fw_zone_is_not_effective(result, key, **options) - return result - - def post_callback(self, ldap, dn, *keys, **options): - super(dnszone_del, self).post_callback(ldap, dn, *keys, **options) - - # Delete entry from realmdomains - # except for our own domain, reverse zone, and root zone - zone = keys[0].make_absolute() - - if (zone != DNSName(api.env.domain).make_absolute() and - not zone.is_reverse() and zone != DNSName.root - ): - try: - self.api.Command['realmdomains_mod']( - del_domain=unicode(zone), force=True) - except (errors.AttrValueNotFound, errors.ValidationError): - pass - - return True - - - -@register() -class dnszone_mod(DNSZoneBase_mod): - __doc__ = _('Modify DNS zone (SOA record).') - - takes_options = DNSZoneBase_mod.takes_options + ( - Flag('force', - label=_('Force'), - doc=_('Force nameserver change even if nameserver not in DNS'), - ), - ) - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - if not _check_DN_objectclass(ldap, dn, self.obj.object_class): - self.obj.handle_not_found(*keys) - if 'idnssoamname' in entry_attrs: - nameserver = entry_attrs['idnssoamname'] - if nameserver: - if not nameserver.is_empty() and not options['force']: - check_ns_rec_resolvable(keys[0], nameserver, self.log) - context.show_warning_nameserver_option = True - else: - # empty value, this option is required by ldap - raise errors.ValidationError( - name='name_server', - error=_(u"is required")) - - return dn - - def execute(self, *keys, **options): - result = super(dnszone_mod, self).execute(*keys, **options) - self.obj._warning_forwarding(result, **options) - self.obj._warning_name_server_option(result, context, **options) - self.obj._warning_dnssec_master_is_not_installed(result, **options) - return result - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - dn = super(dnszone_mod, self).post_callback(ldap, dn, entry_attrs, - *keys, **options) - self.obj._rr_zone_postprocess(entry_attrs, **options) - return dn - - -@register() -class dnszone_find(DNSZoneBase_find): - __doc__ = _('Search for DNS zones (SOA records).') - - takes_options = DNSZoneBase_find.takes_options + ( - Flag('forward_only', - label=_('Forward zones only'), - cli_name='forward_only', - doc=_('Search for forward zones only'), - ), - ) - - def pre_callback(self, ldap, filter, attrs_list, base_dn, scope, *args, **options): - assert isinstance(base_dn, DN) - - filter, base, dn = super(dnszone_find, self).pre_callback(ldap, filter, - attrs_list, base_dn, scope, *args, **options) - - if options.get('forward_only', False): - search_kw = {} - search_kw['idnsname'] = [revzone.ToASCII() for revzone in - REVERSE_DNS_ZONES.keys()] - rev_zone_filter = ldap.make_filter(search_kw, - rules=ldap.MATCH_NONE, - exact=False, - trailing_wildcard=False) - filter = ldap.combine_filters((rev_zone_filter, filter), - rules=ldap.MATCH_ALL) - - return (filter, base_dn, scope) - - def post_callback(self, ldap, entries, truncated, *args, **options): - truncated = super(dnszone_find, self).post_callback(ldap, entries, - truncated, *args, - **options) - for entry_attrs in entries: - self.obj._rr_zone_postprocess(entry_attrs, **options) - return truncated - - - -@register() -class dnszone_show(DNSZoneBase_show): - __doc__ = _('Display information about a DNS zone (SOA record).') - - def execute(self, *keys, **options): - result = super(dnszone_show, self).execute(*keys, **options) - self.obj._warning_forwarding(result, **options) - self.obj._warning_dnssec_master_is_not_installed(result, **options) - return result - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - dn = super(dnszone_show, self).post_callback(ldap, dn, entry_attrs, - *keys, **options) - self.obj._rr_zone_postprocess(entry_attrs, **options) - return dn - - - -@register() -class dnszone_disable(DNSZoneBase_disable): - __doc__ = _('Disable DNS Zone.') - msg_summary = _('Disabled DNS zone "%(value)s"') - - def execute(self, *keys, **options): - result = super(dnszone_disable, self).execute(*keys, **options) - self.obj._warning_fw_zone_is_not_effective(result, *keys, **options) - return result - - -@register() -class dnszone_enable(DNSZoneBase_enable): - __doc__ = _('Enable DNS Zone.') - msg_summary = _('Enabled DNS zone "%(value)s"') - - def execute(self, *keys, **options): - result = super(dnszone_enable, self).execute(*keys, **options) - self.obj._warning_fw_zone_is_not_effective(result, *keys, **options) - return result - - -@register() -class dnszone_add_permission(DNSZoneBase_add_permission): - __doc__ = _('Add a permission for per-zone access delegation.') - - -@register() -class dnszone_remove_permission(DNSZoneBase_remove_permission): - __doc__ = _('Remove a permission for per-zone access delegation.') - - -@register() -class dnsrecord(LDAPObject): - """ - DNS record. - """ - parent_object = 'dnszone' - container_dn = api.env.container_dns - object_name = _('DNS resource record') - object_name_plural = _('DNS resource records') - object_class = ['top', 'idnsrecord'] - permission_filter_objectclasses = ['idnsrecord'] - default_attributes = ['idnsname'] + _record_attributes - rdn_is_primary_key = True - - label = _('DNS Resource Records') - label_singular = _('DNS Resource Record') - - takes_params = ( - DNSNameParam('idnsname', - cli_name='name', - label=_('Record name'), - doc=_('Record name'), - primary_key=True, - ), - Int('dnsttl?', - cli_name='ttl', - label=_('Time to live'), - doc=_('Time to live'), - ), - StrEnum('dnsclass?', - # Deprecated - cli_name='class', - flags=['no_option'], - values=_record_classes, - ), - ) + _dns_record_options - - structured_flag = Flag('structured', - label=_('Structured'), - doc=_('Parse all raw DNS records and return them in a structured way'), - ) - - def _dsrecord_pre_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - dsrecords = entry_attrs.get('dsrecord') - if dsrecords and self.is_pkey_zone_record(*keys): - raise errors.ValidationError( - name='dsrecord', - error=unicode(_('DS record must not be in zone apex (RFC 4035 section 2.4)'))) - - def _nsrecord_pre_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - nsrecords = entry_attrs.get('nsrecord') - if options.get('force', False) or nsrecords is None: - return - for nsrecord in nsrecords: - check_ns_rec_resolvable(keys[0], DNSName(nsrecord), self.log) - - def _idnsname_pre_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - if keys[-1].is_absolute(): - if keys[-1].is_subdomain(keys[-2]): - entry_attrs['idnsname'] = [keys[-1].relativize(keys[-2])] - elif not self.is_pkey_zone_record(*keys): - raise errors.ValidationError(name='idnsname', - error=unicode(_('out-of-zone data: record name must ' - 'be a subdomain of the zone or a ' - 'relative name'))) - # dissallowed wildcard (RFC 4592 section 4) - no_wildcard_rtypes = ['DNAME', 'DS', 'NS'] - if (keys[-1].is_wild() and - any(entry_attrs.get(record_name_format % r.lower()) - for r in no_wildcard_rtypes) - ): - raise errors.ValidationError( - name='idnsname', - error=(_('owner of %(types)s records ' - 'should not be a wildcard domain name (RFC 4592 section 4)') % - {'types': ', '.join(no_wildcard_rtypes)} - ) - ) - - def _ptrrecord_pre_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - ptrrecords = entry_attrs.get('ptrrecord') - if ptrrecords is None: - return - - zone = keys[-2] - if self.is_pkey_zone_record(*keys): - addr = _dns_zone_record - else: - addr = keys[-1] - - zone_len = 0 - for valid_zone in REVERSE_DNS_ZONES: - if zone.is_subdomain(valid_zone): - zone = zone.relativize(valid_zone) - zone_name = valid_zone - zone_len = REVERSE_DNS_ZONES[valid_zone] - - if not zone_len: - allowed_zones = ', '.join([unicode(revzone) for revzone in - REVERSE_DNS_ZONES.keys()]) - raise errors.ValidationError(name='ptrrecord', - error=unicode(_('Reverse zone for PTR record should be a sub-zone of one the following fully qualified domains: %s') % allowed_zones)) - - addr_len = len(addr.labels) - - # Classless zones (0/25.0.0.10.in-addr.arpa.) -> skip check - # zone has to be checked without reverse domain suffix (in-addr.arpa.) - for sign in ('/', '-'): - for name in (zone, addr): - for label in name.labels: - if sign in label: - return - - ip_addr_comp_count = addr_len + len(zone.labels) - if ip_addr_comp_count != zone_len: - raise errors.ValidationError(name='ptrrecord', - error=unicode(_('Reverse zone %(name)s requires exactly ' - '%(count)d IP address components, ' - '%(user_count)d given') - % dict(name=zone_name, - count=zone_len, - user_count=ip_addr_comp_count))) - - def run_precallback_validators(self, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - ldap = self.api.Backend.ldap2 - - for rtype in entry_attrs.keys(): - rtype_cb = getattr(self, '_%s_pre_callback' % rtype, None) - if rtype_cb: - rtype_cb(ldap, dn, entry_attrs, *keys, **options) - - def is_pkey_zone_record(self, *keys): - assert isinstance(keys[-1], DNSName) - assert isinstance(keys[-2], DNSName) - idnsname = keys[-1] - zonename = keys[-2] - if idnsname.is_empty() or idnsname == zonename: - return True - return False - - def check_zone(self, zone, **options): - """ - Check if zone exists and if is master zone - """ - parent_object = self.api.Object[self.parent_object] - dn = parent_object.get_dn(zone, **options) - ldap = self.api.Backend.ldap2 - try: - entry = ldap.get_entry(dn, ['objectclass']) - except errors.NotFound: - parent_object.handle_not_found(zone) - else: - # only master zones can contain records - if 'idnszone' not in [x.lower() for x in entry.get('objectclass', [])]: - raise errors.ValidationError( - name='dnszoneidnsname', - error=_(u'only master zones can contain records') - ) - return dn - - - def get_dn(self, *keys, **options): - if not dns_container_exists(self.api.Backend.ldap2): - raise errors.NotFound(reason=_('DNS is not configured')) - - dn = self.check_zone(keys[-2], **options) - - if self.is_pkey_zone_record(*keys): - return dn - - #Make RR name relative if possible - relative_name = keys[-1].relativize(keys[-2]).ToASCII() - keys = keys[:-1] + (relative_name,) - return super(dnsrecord, self).get_dn(*keys, **options) - - def attr_to_cli(self, attr): - cliname = get_record_rrtype(attr) - if not cliname: - cliname = attr - return cliname - - def get_dns_masters(self): - ldap = self.api.Backend.ldap2 - base_dn = DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), self.api.env.basedn) - ldap_filter = '(&(objectClass=ipaConfigObject)(cn=DNS))' - dns_masters = [] - - try: - entries = ldap.find_entries(filter=ldap_filter, base_dn=base_dn)[0] - - for entry in entries: - try: - master = entry.dn[1]['cn'] - dns_masters.append(master) - except (IndexError, KeyError): - pass - except errors.NotFound: - return [] - - return dns_masters - - def get_record_entry_attrs(self, entry_attrs): - entry_attrs = entry_attrs.copy() - for attr in entry_attrs.keys(): - if attr not in self.params or self.params[attr].primary_key: - del entry_attrs[attr] - return entry_attrs - - def postprocess_record(self, record, **options): - if options.get('structured', False): - for attr in record.keys(): - # attributes in LDAPEntry may not be normalized - attr = attr.lower() - try: - param = self.params[attr] - except KeyError: - continue - - if not isinstance(param, DNSRecord): - continue - parts_params = param.get_parts() - - for dnsvalue in record[attr]: - dnsentry = { - u'dnstype' : unicode(param.rrtype), - u'dnsdata' : dnsvalue - } - values = param._get_part_values(dnsvalue) - if values is None: - continue - for val_id, val in enumerate(values): - if val is not None: - #decode IDN - if isinstance(parts_params[val_id], DNSNameParam): - dnsentry[parts_params[val_id].name] = \ - _dns_name_to_string(val, - options.get('raw', False)) - else: - dnsentry[parts_params[val_id].name] = val - record.setdefault('dnsrecords', []).append(dnsentry) - del record[attr] - - elif not options.get('raw', False): - #Decode IDN ACE form to Unicode, raw records are passed directly from LDAP - _records_idn_postprocess(record, **options) - - def updated_rrattrs(self, old_entry, entry_attrs): - """Returns updated RR attributes - """ - rrattrs = {} - if old_entry is not None: - old_rrattrs = dict((key, value) for key, value in old_entry.items() - if key in self.params and - isinstance(self.params[key], DNSRecord)) - rrattrs.update(old_rrattrs) - new_rrattrs = dict((key, value) for key, value in entry_attrs.items() - if key in self.params and - isinstance(self.params[key], DNSRecord)) - rrattrs.update(new_rrattrs) - return rrattrs - - def check_record_type_collisions(self, keys, rrattrs): - # Test that only allowed combination of record types was created - - # CNAME record validation - cnames = rrattrs.get('cnamerecord') - if cnames is not None: - if len(cnames) > 1: - raise errors.ValidationError(name='cnamerecord', - error=_('only one CNAME record is allowed per name ' - '(RFC 2136, section 1.1.5)')) - if any(rrvalue is not None - and rrattr != 'cnamerecord' - for rrattr, rrvalue in rrattrs.items()): - raise errors.ValidationError(name='cnamerecord', - error=_('CNAME record is not allowed to coexist ' - 'with any other record (RFC 1034, section 3.6.2)')) - - # DNAME record validation - dnames = rrattrs.get('dnamerecord') - if dnames is not None: - if len(dnames) > 1: - raise errors.ValidationError(name='dnamerecord', - error=_('only one DNAME record is allowed per name ' - '(RFC 6672, section 2.4)')) - # DNAME must not coexist with CNAME, but this is already checked earlier - - # NS record validation - # NS record can coexist only with A, AAAA, DS, and other NS records (except zone apex) - # RFC 2181 section 6.1, - allowed_records = ['AAAA', 'A', 'DS', 'NS'] - nsrecords = rrattrs.get('nsrecord') - if nsrecords and not self.is_pkey_zone_record(*keys): - for r_type in _record_types: - if (r_type not in allowed_records - and rrattrs.get(record_name_format % r_type.lower()) - ): - raise errors.ValidationError( - name='nsrecord', - error=_('NS record is not allowed to coexist with an ' - '%(type)s record except when located in a ' - 'zone root record (RFC 2181, section 6.1)') % - {'type': r_type}) - - def check_record_type_dependencies(self, keys, rrattrs): - # Test that all record type dependencies are satisfied - - # DS record validation - # DS record requires to coexists with NS record - dsrecords = rrattrs.get('dsrecord') - nsrecords = rrattrs.get('nsrecord') - # DS record cannot be in zone apex, checked in pre-callback validators - if dsrecords and not nsrecords: - raise errors.ValidationError( - name='dsrecord', - error=_('DS record requires to coexist with an ' - 'NS record (RFC 4592 section 4.6, RFC 4035 section 2.4)')) - - def _entry2rrsets(self, entry_attrs, dns_name, dns_domain): - '''Convert entry_attrs to a dictionary {rdtype: rrset}. - - :returns: - None if entry_attrs is None - {rdtype: None} if RRset of given type is empty - {rdtype: RRset} if RRset of given type is non-empty - ''' - ldap_rrsets = {} - - if not entry_attrs: - # all records were deleted => name should not exist in DNS - return None - - for attr, value in entry_attrs.items(): - rrtype = get_record_rrtype(attr) - if not rrtype: - continue - - rdtype = dns.rdatatype.from_text(rrtype) - if not value: - ldap_rrsets[rdtype] = None # RRset is empty - continue - - try: - # TTL here can be arbitrary value because it is ignored - # during comparison - ldap_rrset = dns.rrset.from_text( - dns_name, 86400, dns.rdataclass.IN, rdtype, - *[str(v) for v in value]) - - # make sure that all names are absolute so RRset - # comparison will work - for ldap_rr in ldap_rrset: - ldap_rr.choose_relativity(origin=dns_domain, - relativize=False) - ldap_rrsets[rdtype] = ldap_rrset - - except dns.exception.SyntaxError as e: - self.log.error('DNS syntax error: %s %s %s: %s', dns_name, - dns.rdatatype.to_text(rdtype), value, e) - raise - - return ldap_rrsets - - def wait_for_modified_attr(self, ldap_rrset, rdtype, dns_name): - '''Wait until DNS resolver returns up-to-date answer for given RRset - or until the maximum number of attempts is reached. - Number of attempts is controlled by self.api.env['wait_for_dns']. - - :param ldap_rrset: - None if given rdtype should not exist or - dns.rrset.RRset to match against data in DNS. - :param dns_name: FQDN to query - :type dns_name: dns.name.Name - :return: None if data in DNS and LDAP match - :raises errors.DNSDataMismatch: if data in DNS and LDAP doesn't match - :raises dns.exception.DNSException: if DNS resolution failed - ''' - resolver = dns.resolver.Resolver() - resolver.set_flags(0) # disable recursion (for NS RR checks) - max_attempts = int(self.api.env['wait_for_dns']) - warn_attempts = max_attempts // 2 - period = 1 # second - attempt = 0 - log_fn = self.log.debug - log_fn('querying DNS server: expecting answer {%s}', ldap_rrset) - wait_template = 'waiting for DNS answer {%s}: got {%s} (attempt %s); '\ - 'waiting %s seconds before next try' - - while attempt < max_attempts: - if attempt >= warn_attempts: - log_fn = self.log.warning - attempt += 1 - try: - dns_answer = resolver.query(dns_name, rdtype, - dns.rdataclass.IN, - raise_on_no_answer=False) - dns_rrset = None - if rdtype == _NS: - # NS records can be in Authority section (sometimes) - dns_rrset = dns_answer.response.get_rrset( - dns_answer.response.authority, dns_name, _IN, rdtype) - - if not dns_rrset: - # Look for NS and other data in Answer section - dns_rrset = dns_answer.rrset - - if dns_rrset == ldap_rrset: - log_fn('DNS answer matches expectations (attempt %s)', - attempt) - return - - log_msg = wait_template % (ldap_rrset, dns_answer.response, - attempt, period) - - except (dns.resolver.NXDOMAIN, - dns.resolver.YXDOMAIN, - dns.resolver.NoNameservers, - dns.resolver.Timeout) as e: - if attempt >= max_attempts: - raise - else: - log_msg = wait_template % (ldap_rrset, type(e), attempt, - period) - - log_fn(log_msg) - time.sleep(period) - - # Maximum number of attempts was reached - else: - raise errors.DNSDataMismatch(expected=ldap_rrset, got=dns_rrset) - - def wait_for_modified_attrs(self, entry_attrs, dns_name, dns_domain): - '''Wait until DNS resolver returns up-to-date answer for given entry - or until the maximum number of attempts is reached. - - :param entry_attrs: - None if the entry was deleted from LDAP or - LDAPEntry instance containing at least all modified attributes. - :param dns_name: FQDN - :type dns_name: dns.name.Name - :raises errors.DNSDataMismatch: if data in DNS and LDAP doesn't match - ''' - - # represent data in LDAP as dictionary rdtype => rrset - ldap_rrsets = self._entry2rrsets(entry_attrs, dns_name, dns_domain) - nxdomain = ldap_rrsets is None - if nxdomain: - # name should not exist => ask for A record and check result - ldap_rrsets = {dns.rdatatype.from_text('A'): None} - - for rdtype, ldap_rrset in ldap_rrsets.items(): - try: - self.wait_for_modified_attr(ldap_rrset, rdtype, dns_name) - - except dns.resolver.NXDOMAIN as e: - if nxdomain: - continue - else: - e = errors.DNSDataMismatch(expected=ldap_rrset, - got="NXDOMAIN") - self.log.error(e) - raise e - - except dns.resolver.NoNameservers as e: - # Do not raise exception if we have got SERVFAILs. - # Maybe the user has created an invalid zone intentionally. - self.log.warning('waiting for DNS answer {%s}: got {%s}; ' - 'ignoring', ldap_rrset, type(e)) - continue - - except dns.exception.DNSException as e: - err_desc = str(type(e)) - err_str = str(e) - if err_str: - err_desc += ": %s" % err_str - e = errors.DNSDataMismatch(expected=ldap_rrset, got=err_desc) - self.log.error(e) - raise e - - def wait_for_modified_entries(self, entries): - '''Call wait_for_modified_attrs for all entries in given dict. - - :param entries: - Dict {(dns_domain, dns_name): entry_for_wait_for_modified_attrs} - ''' - for entry_name, entry in entries.items(): - dns_domain = entry_name[0] - dns_name = entry_name[1].derelativize(dns_domain) - self.wait_for_modified_attrs(entry, dns_name, dns_domain) - - def warning_if_ns_change_cause_fwzone_ineffective(self, result, *keys, - **options): - """Detect if NS record change can make forward zones ineffective due - missing delegation. Run after parent's execute method. - """ - record_name_absolute = keys[-1] - zone = keys[-2] - - if not record_name_absolute.is_absolute(): - record_name_absolute = record_name_absolute.derelativize(zone) - - affected_fw_zones, truncated = _find_subtree_forward_zones_ldap( - self.api, record_name_absolute) - if not affected_fw_zones: - return - - for fwzone in affected_fw_zones: - _add_warning_fw_zone_is_not_effective(self.api, result, fwzone, - options['version']) - - def warning_suspicious_relative_name(self, result, *keys, **options): - """Detect if zone name is suffix of relative record name and warn. - - Zone name: test.zone. - Relative name: record.test.zone - """ - record_name = keys[-1] - zone = keys[-2] - if not record_name.is_absolute() and record_name.is_subdomain( - zone.relativize(DNSName.root)): - messages.add_message( - options['version'], - result, - messages.DNSSuspiciousRelativeName(record=record_name, - zone=zone, - fqdn=record_name + zone) - ) - - -@register() -class dnsrecord_split_parts(Command): - NO_CLI = True - - takes_args = ( - Str('name'), - Str('value'), - ) - - def execute(self, name, value, *args, **options): - result = self.api.Object.dnsrecord.params[name]._get_part_values(value) - return dict(result=result) - - -@register() -class dnsrecord_add(LDAPCreate): - __doc__ = _('Add new DNS resource record.') - - no_option_msg = 'No options to add a specific record provided.\n' \ - "Command help may be consulted for all supported record types." - takes_options = LDAPCreate.takes_options + ( - Flag('force', - label=_('Force'), - flags=['no_option', 'no_output'], - doc=_('force NS record creation even if its hostname is not in DNS'), - ), - dnsrecord.structured_flag, - ) - - def args_options_2_entry(self, *keys, **options): - has_cli_options(self, options, self.no_option_msg) - return super(dnsrecord_add, self).args_options_2_entry(*keys, **options) - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - precallback_attrs = [] - processed_attrs = [] - for option in options: - try: - param = self.params[option] - except KeyError: - continue - - rrparam = get_rrparam_from_part(self, option) - if rrparam is None: - continue - - if 'dnsrecord_part' in param.flags: - if rrparam.name in processed_attrs: - # this record was already entered - continue - if rrparam.name in entry_attrs: - # this record is entered both via parts and raw records - raise errors.ValidationError(name=param.cli_name or param.name, - error=_('Raw value of a DNS record was already set by "%(name)s" option') \ - % dict(name=rrparam.cli_name or rrparam.name)) - - parts = rrparam.get_parts_from_kw(options) - dnsvalue = [rrparam._convert_scalar(parts)] - entry_attrs[rrparam.name] = dnsvalue - processed_attrs.append(rrparam.name) - continue - - if 'dnsrecord_extra' in param.flags: - # do not run precallback for unset flags - if isinstance(param, Flag) and not options[option]: - continue - # extra option is passed, run per-type pre_callback for given RR type - precallback_attrs.append(rrparam.name) - - # Run pre_callback validators - self.obj.run_precallback_validators(dn, entry_attrs, *keys, **options) - - # run precallback also for all new RR type attributes in entry_attrs - for attr in entry_attrs.keys(): - try: - param = self.params[attr] - except KeyError: - continue - - if not isinstance(param, DNSRecord): - continue - precallback_attrs.append(attr) - - precallback_attrs = list(set(precallback_attrs)) - - for attr in precallback_attrs: - # run per-type - try: - param = self.params[attr] - except KeyError: - continue - param.dnsrecord_add_pre_callback(ldap, dn, entry_attrs, attrs_list, *keys, **options) - - # Store all new attrs so that DNSRecord post callback is called for - # new attributes only and not for all attributes in the LDAP entry - setattr(context, 'dnsrecord_precallback_attrs', precallback_attrs) - - # We always want to retrieve all DNS record attributes to test for - # record type collisions (#2601) - try: - old_entry = ldap.get_entry(dn, _record_attributes) - except errors.NotFound: - old_entry = None - else: - for attr in entry_attrs.keys(): - if attr not in _record_attributes: - continue - if entry_attrs[attr] is None: - entry_attrs[attr] = [] - if not isinstance(entry_attrs[attr], (tuple, list)): - vals = [entry_attrs[attr]] - else: - vals = list(entry_attrs[attr]) - entry_attrs[attr] = list(set(old_entry.get(attr, []) + vals)) - - rrattrs = self.obj.updated_rrattrs(old_entry, entry_attrs) - self.obj.check_record_type_dependencies(keys, rrattrs) - self.obj.check_record_type_collisions(keys, rrattrs) - context.dnsrecord_entry_mods = getattr(context, 'dnsrecord_entry_mods', - {}) - context.dnsrecord_entry_mods[(keys[0], keys[1])] = entry_attrs.copy() - - return dn - - def execute(self, *keys, **options): - result = super(dnsrecord_add, self).execute(*keys, **options) - self.obj.warning_suspicious_relative_name(result, *keys, **options) - return result - - def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs): - if call_func.__name__ == 'add_entry': - if isinstance(exc, errors.DuplicateEntry): - # A new record is being added to existing LDAP DNS object - # Update can be safely run as old record values has been - # already merged in pre_callback - ldap = self.obj.backend - entry_attrs = self.obj.get_record_entry_attrs(call_args[0]) - update = ldap.get_entry(entry_attrs.dn, list(entry_attrs)) - update.update(entry_attrs) - ldap.update_entry(update, **call_kwargs) - return - raise exc - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - for attr in getattr(context, 'dnsrecord_precallback_attrs', []): - param = self.params[attr] - param.dnsrecord_add_post_callback(ldap, dn, entry_attrs, *keys, **options) - - if self.obj.is_pkey_zone_record(*keys): - entry_attrs[self.obj.primary_key.name] = [_dns_zone_record] - - self.obj.postprocess_record(entry_attrs, **options) - - if self.api.env['wait_for_dns']: - self.obj.wait_for_modified_entries(context.dnsrecord_entry_mods) - return dn - - - -@register() -class dnsrecord_mod(LDAPUpdate): - __doc__ = _('Modify a DNS resource record.') - - no_option_msg = 'No options to modify a specific record provided.' - - takes_options = LDAPUpdate.takes_options + ( - dnsrecord.structured_flag, - ) - - def args_options_2_entry(self, *keys, **options): - has_cli_options(self, options, self.no_option_msg, True) - return super(dnsrecord_mod, self).args_options_2_entry(*keys, **options) - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - if options.get('rename') and self.obj.is_pkey_zone_record(*keys): - # zone rename is not allowed - raise errors.ValidationError(name='rename', - error=_('DNS zone root record cannot be renamed')) - - # check if any attr should be updated using structured instead of replaced - # format is recordname : (old_value, new_parts) - updated_attrs = {} - for param in iterate_rrparams_by_parts(self, options, skip_extra=True): - parts = param.get_parts_from_kw(options, raise_on_none=False) - - if parts is None: - # old-style modification - continue - - old_value = entry_attrs.get(param.name) - if not old_value: - raise errors.RequirementError(name=param.name) - if isinstance(old_value, (tuple, list)): - if len(old_value) > 1: - raise errors.ValidationError(name=param.name, - error=_('DNS records can be only updated one at a time')) - old_value = old_value[0] - - updated_attrs[param.name] = (old_value, parts) - - # Run pre_callback validators - self.obj.run_precallback_validators(dn, entry_attrs, *keys, **options) - - # current entry is needed in case of per-dns-record-part updates and - # for record type collision check - try: - old_entry = ldap.get_entry(dn, _record_attributes) - except errors.NotFound: - self.obj.handle_not_found(*keys) - - if updated_attrs: - for attr in updated_attrs: - param = self.params[attr] - old_dnsvalue, new_parts = updated_attrs[attr] - - if old_dnsvalue not in old_entry.get(attr, []): - attr_name = unicode(param.label or param.name) - raise errors.AttrValueNotFound(attr=attr_name, - value=old_dnsvalue) - old_entry[attr].remove(old_dnsvalue) - - old_parts = param._get_part_values(old_dnsvalue) - modified_parts = tuple(part if part is not None else old_parts[part_id] \ - for part_id,part in enumerate(new_parts)) - - new_dnsvalue = [param._convert_scalar(modified_parts)] - entry_attrs[attr] = list(set(old_entry[attr] + new_dnsvalue)) - - rrattrs = self.obj.updated_rrattrs(old_entry, entry_attrs) - self.obj.check_record_type_dependencies(keys, rrattrs) - self.obj.check_record_type_collisions(keys, rrattrs) - - context.dnsrecord_entry_mods = getattr(context, 'dnsrecord_entry_mods', - {}) - context.dnsrecord_entry_mods[(keys[0], keys[1])] = entry_attrs.copy() - return dn - - def execute(self, *keys, **options): - result = super(dnsrecord_mod, self).execute(*keys, **options) - - # remove if empty - if not self.obj.is_pkey_zone_record(*keys): - rename = options.get('rename') - if rename is not None: - keys = keys[:-1] + (rename,) - dn = self.obj.get_dn(*keys, **options) - ldap = self.obj.backend - old_entry = ldap.get_entry(dn, _record_attributes) - - del_all = True - for attr in old_entry.keys(): - if old_entry[attr]: - del_all = False - break - - if del_all: - result = self.obj.methods.delentry(*keys, - version=options['version']) - - # we need to modify delete result to match mod output type - # only one value is expected, not a list - if client_has_capability(options['version'], 'primary_key_types'): - assert len(result['value']) == 1 - result['value'] = result['value'][0] - - # indicate that entry was deleted - context.dnsrecord_entry_mods[(keys[0], keys[1])] = None - - if self.api.env['wait_for_dns']: - self.obj.wait_for_modified_entries(context.dnsrecord_entry_mods) - if 'nsrecord' in options: - self.obj.warning_if_ns_change_cause_fwzone_ineffective(result, - *keys, - **options) - return result - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - if self.obj.is_pkey_zone_record(*keys): - entry_attrs[self.obj.primary_key.name] = [_dns_zone_record] - - self.obj.postprocess_record(entry_attrs, **options) - return dn - - -@register() -class dnsrecord_delentry(LDAPDelete): - """ - Delete DNS record entry. - """ - msg_summary = _('Deleted record "%(value)s"') - NO_CLI = True - - - -@register() -class dnsrecord_del(LDAPUpdate): - __doc__ = _('Delete DNS resource record.') - - has_output = output.standard_multi_delete - - no_option_msg = _('Neither --del-all nor options to delete a specific record provided.\n'\ - "Command help may be consulted for all supported record types.") - - takes_options = ( - Flag('del_all', - default=False, - label=_('Delete all associated records'), - ), - dnsrecord.structured_flag, - ) - - def get_options(self): - for option in super(dnsrecord_del, self).get_options(): - if any(flag in option.flags for flag in \ - ('dnsrecord_part', 'dnsrecord_extra',)): - continue - elif option.name in ('rename', ): - # options only valid for dnsrecord-mod - continue - elif isinstance(option, DNSRecord): - yield option.clone(option_group=None) - continue - yield option - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - try: - old_entry = ldap.get_entry(dn, _record_attributes) - except errors.NotFound: - self.obj.handle_not_found(*keys) - - for attr in entry_attrs.keys(): - if attr not in _record_attributes: - continue - if not isinstance(entry_attrs[attr], (tuple, list)): - vals = [entry_attrs[attr]] - else: - vals = entry_attrs[attr] - - for val in vals: - try: - old_entry[attr].remove(val) - except (KeyError, ValueError): - try: - param = self.params[attr] - attr_name = unicode(param.label or param.name) - except Exception: - attr_name = attr - raise errors.AttrValueNotFound(attr=attr_name, value=val) - entry_attrs[attr] = list(set(old_entry[attr])) - - rrattrs = self.obj.updated_rrattrs(old_entry, entry_attrs) - self.obj.check_record_type_dependencies(keys, rrattrs) - - del_all = False - if not self.obj.is_pkey_zone_record(*keys): - record_found = False - for attr in old_entry.keys(): - if old_entry[attr]: - record_found = True - break - del_all = not record_found - - # set del_all flag in context - # when the flag is enabled, the entire DNS record object is deleted - # in a post callback - context.del_all = del_all - context.dnsrecord_entry_mods = getattr(context, 'dnsrecord_entry_mods', - {}) - context.dnsrecord_entry_mods[(keys[0], keys[1])] = entry_attrs.copy() - - return dn - - def execute(self, *keys, **options): - if options.get('del_all', False): - if self.obj.is_pkey_zone_record(*keys): - raise errors.ValidationError( - name='del_all', - error=_('Zone record \'%s\' cannot be deleted') \ - % _dns_zone_record - ) - result = self.obj.methods.delentry(*keys, - version=options['version']) - if self.api.env['wait_for_dns']: - entries = {(keys[0], keys[1]): None} - self.obj.wait_for_modified_entries(entries) - else: - result = super(dnsrecord_del, self).execute(*keys, **options) - result['value'] = pkey_to_value([keys[-1]], options) - - if getattr(context, 'del_all', False) and not \ - self.obj.is_pkey_zone_record(*keys): - result = self.obj.methods.delentry(*keys, - version=options['version']) - context.dnsrecord_entry_mods[(keys[0], keys[1])] = None - - if self.api.env['wait_for_dns']: - self.obj.wait_for_modified_entries(context.dnsrecord_entry_mods) - - if 'nsrecord' in options or options.get('del_all', False): - self.obj.warning_if_ns_change_cause_fwzone_ineffective(result, - *keys, - **options) - return result - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - if self.obj.is_pkey_zone_record(*keys): - entry_attrs[self.obj.primary_key.name] = [_dns_zone_record] - self.obj.postprocess_record(entry_attrs, **options) - return dn - - def args_options_2_entry(self, *keys, **options): - has_cli_options(self, options, self.no_option_msg) - return super(dnsrecord_del, self).args_options_2_entry(*keys, **options) - - -@register() -class dnsrecord_show(LDAPRetrieve): - __doc__ = _('Display DNS resource.') - - takes_options = LDAPRetrieve.takes_options + ( - dnsrecord.structured_flag, - ) - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - if self.obj.is_pkey_zone_record(*keys): - entry_attrs[self.obj.primary_key.name] = [_dns_zone_record] - self.obj.postprocess_record(entry_attrs, **options) - return dn - - - -@register() -class dnsrecord_find(LDAPSearch): - __doc__ = _('Search for DNS resources.') - - takes_options = LDAPSearch.takes_options + ( - dnsrecord.structured_flag, - ) - - def get_options(self): - for option in super(dnsrecord_find, self).get_options(): - if any(flag in option.flags for flag in \ - ('dnsrecord_part', 'dnsrecord_extra',)): - continue - elif isinstance(option, DNSRecord): - yield option.clone(option_group=None) - continue - yield option - - def pre_callback(self, ldap, filter, attrs_list, base_dn, scope, - dnszoneidnsname, *args, **options): - assert isinstance(base_dn, DN) - - # validate if zone is master zone - self.obj.check_zone(dnszoneidnsname, **options) - - filter = _create_idn_filter(self, ldap, *args, **options) - return (filter, base_dn, ldap.SCOPE_SUBTREE) - - def post_callback(self, ldap, entries, truncated, *args, **options): - if entries: - zone_obj = self.api.Object[self.obj.parent_object] - zone_dn = zone_obj.get_dn(args[0]) - if entries[0].dn == zone_dn: - entries[0][zone_obj.primary_key.name] = [_dns_zone_record] - for entry in entries: - self.obj.postprocess_record(entry, **options) - - return truncated - - -@register() -class dns_resolve(Command): - __doc__ = _('Resolve a host name in DNS. (Deprecated)') - - NO_CLI = True - - has_output = output.standard_value - msg_summary = _('Found \'%(value)s\'') - - takes_args = ( - Str('hostname', - label=_('Hostname (FQDN)'), - ), - ) - - def execute(self, *args, **options): - query=args[0] - - try: - verify_host_resolvable(query) - except errors.DNSNotARecordError: - raise errors.NotFound( - reason=_('Host \'%(host)s\' not found') % {'host': query} - ) - result = dict(result=True, value=query) - messages.add_message( - options['version'], result, - messages.CommandDeprecatedWarning( - command='dns-resolve', - additional_info='The command may return an unexpected result, ' - 'the resolution of the DNS domain is done on ' - 'a randomly chosen IPA server.' - ) - ) - return result - - -@register() -class dns_is_enabled(Command): - """ - Checks if any of the servers has the DNS service enabled. - """ - NO_CLI = True - has_output = output.standard_value - - base_dn = DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), api.env.basedn) - filter = '(&(objectClass=ipaConfigObject)(cn=DNS))' - - def execute(self, *args, **options): - ldap = self.api.Backend.ldap2 - dns_enabled = False - - try: - ldap.find_entries(filter=self.filter, base_dn=self.base_dn) - dns_enabled = True - except errors.EmptyResult: - dns_enabled = False - - return dict(result=dns_enabled, value=pkey_to_value(None, options)) - - -@register() -class dnsconfig(LDAPObject): - """ - DNS global configuration object - """ - object_name = _('DNS configuration options') - default_attributes = [ - 'idnsforwardpolicy', 'idnsforwarders', 'idnsallowsyncptr' - ] - - label = _('DNS Global Configuration') - label_singular = _('DNS Global Configuration') - - takes_params = ( - Str('idnsforwarders*', - _validate_bind_forwarder, - cli_name='forwarder', - label=_('Global forwarders'), - doc=_('Global forwarders. A custom port can be specified for each ' - 'forwarder using a standard format "IP_ADDRESS port PORT"'), - ), - StrEnum('idnsforwardpolicy?', - cli_name='forward_policy', - label=_('Forward policy'), - doc=_('Global forwarding policy. Set to "none" to disable ' - 'any configured global forwarders.'), - values=(u'only', u'first', u'none'), - ), - Bool('idnsallowsyncptr?', - cli_name='allow_sync_ptr', - label=_('Allow PTR sync'), - doc=_('Allow synchronization of forward (A, AAAA) and reverse (PTR) records'), - ), - Int('idnszonerefresh?', - deprecated=True, - cli_name='zone_refresh', - label=_('Zone refresh interval'), - doc=_('An interval between regular polls of the name server for new DNS zones'), - minvalue=0, - flags={'no_option'}, - ), - Int('ipadnsversion?', # available only in installer/upgrade - label=_('IPA DNS version'), - ), - ) - managed_permissions = { - 'System: Write DNS Configuration': { - 'non_object': True, - 'ipapermright': {'write'}, - 'ipapermlocation': api.env.basedn, - 'ipapermtarget': DN('cn=dns', api.env.basedn), - 'ipapermtargetfilter': ['(objectclass=idnsConfigObject)'], - 'ipapermdefaultattr': { - 'idnsallowsyncptr', 'idnsforwarders', 'idnsforwardpolicy', - 'idnspersistentsearch', 'idnszonerefresh' - }, - 'replaces': [ - '(targetattr = "idnsforwardpolicy || idnsforwarders || idnsallowsyncptr || idnszonerefresh || idnspersistentsearch")(target = "ldap:///cn=dns,$SUFFIX")(version 3.0;acl "permission:Write DNS Configuration";allow (write) groupdn = "ldap:///cn=Write DNS Configuration,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'DNS Administrators', 'DNS Servers'}, - }, - 'System: Read DNS Configuration': { - 'non_object': True, - 'ipapermright': {'read'}, - 'ipapermlocation': api.env.basedn, - 'ipapermtarget': DN('cn=dns', api.env.basedn), - 'ipapermtargetfilter': ['(objectclass=idnsConfigObject)'], - 'ipapermdefaultattr': { - 'objectclass', - 'idnsallowsyncptr', 'idnsforwarders', 'idnsforwardpolicy', - 'idnspersistentsearch', 'idnszonerefresh', 'ipadnsversion' - }, - 'default_privileges': {'DNS Administrators', 'DNS Servers'}, - }, - } - - def get_dn(self, *keys, **kwargs): - if not dns_container_exists(self.api.Backend.ldap2): - raise errors.NotFound(reason=_('DNS is not configured')) - return DN(api.env.container_dns, api.env.basedn) - - def get_dnsconfig(self, ldap): - entry = ldap.get_entry(self.get_dn(), None) - - return entry - - def postprocess_result(self, result): - if not any(param in result['result'] for param in self.params): - result['summary'] = unicode(_('Global DNS configuration is empty')) - - -@register() -class dnsconfig_mod(LDAPUpdate): - __doc__ = _('Modify global DNS configuration.') - - def get_options(self): - """hide ipadnsversion outside of installer/upgrade""" - for option in super(dnsconfig_mod, self).get_options(): - if option.name == 'ipadnsversion': - option = option.clone(include=('installer', 'updates')) - yield option - - def execute(self, *keys, **options): - # test dnssec forwarders - forwarders = options.get('idnsforwarders') - - result = super(dnsconfig_mod, self).execute(*keys, **options) - self.obj.postprocess_result(result) - - # this check makes sense only when resulting forwarders are non-empty - if result['result'].get('idnsforwarders'): - fwzone = DNSName('.') - _add_warning_fw_policy_conflict_aez(result, fwzone, **options) - - if forwarders: - # forwarders were changed - for forwarder in forwarders: - try: - validate_dnssec_global_forwarder(forwarder, log=self.log) - except DNSSECSignatureMissingError as e: - messages.add_message( - options['version'], - result, messages.DNSServerDoesNotSupportDNSSECWarning( - server=forwarder, error=e, - ) - ) - except EDNS0UnsupportedError as e: - messages.add_message( - options['version'], - result, messages.DNSServerDoesNotSupportEDNS0Warning( - server=forwarder, error=e, - ) - ) - except UnresolvableRecordError as e: - messages.add_message( - options['version'], - result, messages.DNSServerValidationWarning( - server=forwarder, error=e - ) - ) - - return result - - - -@register() -class dnsconfig_show(LDAPRetrieve): - __doc__ = _('Show the current global DNS configuration.') - - def execute(self, *keys, **options): - result = super(dnsconfig_show, self).execute(*keys, **options) - self.obj.postprocess_result(result) - return result - - - -@register() -class dnsforwardzone(DNSZoneBase): - """ - DNS Forward zone, container for resource records. - """ - object_name = _('DNS forward zone') - object_name_plural = _('DNS forward zones') - object_class = DNSZoneBase.object_class + ['idnsforwardzone'] - label = _('DNS Forward Zones') - label_singular = _('DNS Forward Zone') - default_forward_policy = u'first' - - # managed_permissions: permissions was apllied in dnszone class, do NOT - # add them here, they should not be applied twice. - - def _warning_fw_zone_is_not_effective(self, result, *keys, **options): - fwzone = keys[-1] - _add_warning_fw_zone_is_not_effective(self.api, result, fwzone, - options['version']) - - def _warning_if_forwarders_do_not_work(self, result, new_zone, - *keys, **options): - fwzone = keys[-1] - forwarders = options.get('idnsforwarders', []) - any_forwarder_work = False - - for forwarder in forwarders: - try: - validate_dnssec_zone_forwarder_step1(forwarder, fwzone, - log=self.log) - except UnresolvableRecordError as e: - messages.add_message( - options['version'], - result, messages.DNSServerValidationWarning( - server=forwarder, error=e - ) - ) - except EDNS0UnsupportedError as e: - messages.add_message( - options['version'], - result, messages.DNSServerDoesNotSupportEDNS0Warning( - server=forwarder, error=e - ) - ) - else: - any_forwarder_work = True - - if not any_forwarder_work: - # do not test DNSSEC validation if there is no valid forwarder - return - - # resolve IP address of any DNS replica - # FIXME: https://fedorahosted.org/bind-dyndb-ldap/ticket/143 - # we currenly should to test all IPA DNS replica, because DNSSEC - # validation is configured just in named.conf per replica - - ipa_dns_masters = [normalize_zone(x) for x in - self.api.Object.dnsrecord.get_dns_masters()] - - if not ipa_dns_masters: - # something very bad happened, DNS is installed, but no IPA DNS - # servers available - self.log.error("No IPA DNS server can be found, but integrated DNS " - "is installed") - return - - ipa_dns_ip = None - for rdtype in (dns.rdatatype.A, dns.rdatatype.AAAA): - try: - ans = dns.resolver.query(ipa_dns_masters[0], rdtype) - except dns.exception.DNSException: - continue - else: - ipa_dns_ip = str(ans.rrset.items[0]) - break - - if not ipa_dns_ip: - self.log.error("Cannot resolve %s hostname", ipa_dns_masters[0]) - return - - # sleep a bit, adding new zone to BIND from LDAP may take a while - if new_zone: - time.sleep(5) - - # Test if IPA is able to receive replies from forwarders - try: - validate_dnssec_zone_forwarder_step2(ipa_dns_ip, fwzone, - log=self.log) - except DNSSECValidationError as e: - messages.add_message( - options['version'], - result, messages.DNSSECValidationFailingWarning(error=e) - ) - except UnresolvableRecordError as e: - messages.add_message( - options['version'], - result, messages.DNSServerValidationWarning( - server=ipa_dns_ip, error=e - ) - ) - - -@register() -class dnsforwardzone_add(DNSZoneBase_add): - __doc__ = _('Create new DNS forward zone.') - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - - dn = super(dnsforwardzone_add, self).pre_callback(ldap, dn, - entry_attrs, attrs_list, *keys, **options) - - if 'idnsforwardpolicy' not in entry_attrs: - entry_attrs['idnsforwardpolicy'] = self.obj.default_forward_policy - - if (not entry_attrs.get('idnsforwarders') and - entry_attrs['idnsforwardpolicy'] != u'none'): - raise errors.ValidationError(name=u'idnsforwarders', - error=_('Please specify forwarders.')) - - return dn - - def execute(self, *keys, **options): - fwzone = keys[-1] - result = super(dnsforwardzone_add, self).execute(*keys, **options) - self.obj._warning_fw_zone_is_not_effective(result, *keys, **options) - _add_warning_fw_policy_conflict_aez(result, fwzone, **options) - if options.get('idnsforwarders'): - self.obj._warning_if_forwarders_do_not_work( - result, True, *keys, **options) - return result - - -@register() -class dnsforwardzone_del(DNSZoneBase_del): - __doc__ = _('Delete DNS forward zone.') - - msg_summary = _('Deleted DNS forward zone "%(value)s"') - - -@register() -class dnsforwardzone_mod(DNSZoneBase_mod): - __doc__ = _('Modify DNS forward zone.') - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - try: - entry = ldap.get_entry(dn) - except errors.NotFound: - self.obj.handle_not_found(*keys) - - if not _check_entry_objectclass(entry, self.obj.object_class): - self.obj.handle_not_found(*keys) - - policy = self.obj.default_forward_policy - forwarders = [] - - if 'idnsforwarders' in entry_attrs: - forwarders = entry_attrs['idnsforwarders'] - elif 'idnsforwarders' in entry: - forwarders = entry['idnsforwarders'] - - if 'idnsforwardpolicy' in entry_attrs: - policy = entry_attrs['idnsforwardpolicy'] - elif 'idnsforwardpolicy' in entry: - policy = entry['idnsforwardpolicy'] - - if not forwarders and policy != u'none': - raise errors.ValidationError(name=u'idnsforwarders', - error=_('Please specify forwarders.')) - - return dn - - def execute(self, *keys, **options): - fwzone = keys[-1] - result = super(dnsforwardzone_mod, self).execute(*keys, **options) - _add_warning_fw_policy_conflict_aez(result, fwzone, **options) - if options.get('idnsforwarders'): - self.obj._warning_if_forwarders_do_not_work(result, False, *keys, - **options) - return result - -@register() -class dnsforwardzone_find(DNSZoneBase_find): - __doc__ = _('Search for DNS forward zones.') - - -@register() -class dnsforwardzone_show(DNSZoneBase_show): - __doc__ = _('Display information about a DNS forward zone.') - - has_output_params = LDAPRetrieve.has_output_params + dnszone_output_params - - -@register() -class dnsforwardzone_disable(DNSZoneBase_disable): - __doc__ = _('Disable DNS Forward Zone.') - msg_summary = _('Disabled DNS forward zone "%(value)s"') - - -@register() -class dnsforwardzone_enable(DNSZoneBase_enable): - __doc__ = _('Enable DNS Forward Zone.') - msg_summary = _('Enabled DNS forward zone "%(value)s"') - - def execute(self, *keys, **options): - result = super(dnsforwardzone_enable, self).execute(*keys, **options) - self.obj._warning_fw_zone_is_not_effective(result, *keys, **options) - return result - - -@register() -class dnsforwardzone_add_permission(DNSZoneBase_add_permission): - __doc__ = _('Add a permission for per-forward zone access delegation.') - - -@register() -class dnsforwardzone_remove_permission(DNSZoneBase_remove_permission): - __doc__ = _('Remove a permission for per-forward zone access delegation.') diff --git a/ipalib/plugins/domainlevel.py b/ipalib/plugins/domainlevel.py deleted file mode 100644 index 23fa2a1b2..000000000 --- a/ipalib/plugins/domainlevel.py +++ /dev/null @@ -1,137 +0,0 @@ -# -# Copyright (C) 2015 FreeIPA Contributors see COPYING for license -# - -from collections import namedtuple - -from ipalib import _ -from ipalib import Command -from ipalib import errors -from ipalib import output -from ipalib.parameters import Int -from ipalib.plugable import Registry - -from ipapython.dn import DN - - -__doc__ = _(""" -Raise the IPA Domain Level. -""") - -register = Registry() - -DomainLevelRange = namedtuple('DomainLevelRange', ['min', 'max']) - -domainlevel_output = ( - output.Output('result', int, _('Current domain level:')), -) - - -def get_domainlevel_dn(api): - domainlevel_dn = DN( - ('cn', 'Domain Level'), - ('cn', 'ipa'), - ('cn', 'etc'), - api.env.basedn - ) - - return domainlevel_dn - - -def get_domainlevel_range(master_entry): - try: - return DomainLevelRange( - int(master_entry['ipaMinDomainLevel'][0]), - int(master_entry['ipaMaxDomainLevel'][0]) - ) - except KeyError: - return DomainLevelRange(0, 0) - - -def get_master_entries(ldap, api): - """ - Returns list of LDAPEntries representing IPA masters. - """ - - container_masters = DN( - ('cn', 'masters'), - ('cn', 'ipa'), - ('cn', 'etc'), - api.env.basedn - ) - - masters, _ = ldap.find_entries( - filter="(cn=*)", - base_dn=container_masters, - scope=ldap.SCOPE_ONELEVEL, - paged_search=True, # we need to make sure to get all of them - ) - - return masters - - -@register() -class domainlevel_get(Command): - __doc__ = _('Query current Domain Level.') - - has_output = domainlevel_output - - def execute(self, *args, **options): - ldap = self.api.Backend.ldap2 - entry = ldap.get_entry( - get_domainlevel_dn(self.api), - ['ipaDomainLevel'] - ) - - return {'result': int(entry.single_value['ipaDomainLevel'])} - - -@register() -class domainlevel_set(Command): - __doc__ = _('Change current Domain Level.') - - has_output = domainlevel_output - - takes_args = ( - Int('ipadomainlevel', - cli_name='level', - label=_('Domain Level'), - minvalue=0, - ), - ) - - def execute(self, *args, **options): - """ - Checks all the IPA masters for supported domain level ranges. - - If the desired domain level is within the supported range of all - masters, it will be raised. - - Domain level cannot be lowered. - """ - - ldap = self.api.Backend.ldap2 - - current_entry = ldap.get_entry(get_domainlevel_dn(self.api)) - current_value = int(current_entry.single_value['ipadomainlevel']) - desired_value = int(args[0]) - - # Domain level cannot be lowered - if int(desired_value) < int(current_value): - message = _("Domain Level cannot be lowered.") - raise errors.InvalidDomainLevelError(reason=message) - - # Check if every master supports the desired level - for master in get_master_entries(ldap, self.api): - supported = get_domainlevel_range(master) - - if supported.min > desired_value or supported.max < desired_value: - message = _("Domain Level cannot be raised to {0}, server {1} " - "does not support it." - .format(desired_value, master['cn'][0])) - raise errors.InvalidDomainLevelError(reason=message) - - current_entry.single_value['ipaDomainLevel'] = desired_value - ldap.update_entry(current_entry) - - return {'result': int(current_entry.single_value['ipaDomainLevel'])} diff --git a/ipalib/plugins/group.py b/ipalib/plugins/group.py deleted file mode 100644 index 2b0c08050..000000000 --- a/ipalib/plugins/group.py +++ /dev/null @@ -1,690 +0,0 @@ -# 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/>. - -import six - -from ipalib import api -from ipalib import Int, Str, Flag -from ipalib.plugable import Registry -from .baseldap import ( - add_external_post_callback, - pkey_to_value, - remove_external_post_callback, - LDAPObject, - LDAPCreate, - LDAPUpdate, - LDAPDelete, - LDAPSearch, - LDAPRetrieve, - LDAPAddMember, - LDAPRemoveMember, - LDAPQuery, -) -from .idviews import remove_ipaobject_overrides -from . import baseldap -from ipalib import _, ngettext -from ipalib import errors -from ipalib import output -from ipapython.dn import DN - -if six.PY3: - unicode = str - -if api.env.in_server and api.env.context in ['lite', 'server']: - try: - import ipaserver.dcerpc - _dcerpc_bindings_installed = True - except ImportError: - _dcerpc_bindings_installed = False - -__doc__ = _(""" -Groups of users - -Manage groups of users. By default, new groups are POSIX groups. You -can add the --nonposix option to the group-add command to mark a new group -as non-POSIX. You can use the --posix argument with the group-mod command -to convert a non-POSIX group into a POSIX group. POSIX groups cannot be -converted to non-POSIX groups. - -Every group must have a description. - -POSIX groups must have a Group ID (GID) number. Changing a GID is -supported but can have an impact on your file permissions. It is not necessary -to supply a GID when creating a group. IPA will generate one automatically -if it is not provided. - -EXAMPLES: - - Add a new group: - ipa group-add --desc='local administrators' localadmins - - Add a new non-POSIX group: - ipa group-add --nonposix --desc='remote administrators' remoteadmins - - Convert a non-POSIX group to posix: - ipa group-mod --posix remoteadmins - - Add a new POSIX group with a specific Group ID number: - ipa group-add --gid=500 --desc='unix admins' unixadmins - - Add a new POSIX group and let IPA assign a Group ID number: - ipa group-add --desc='printer admins' printeradmins - - Remove a group: - ipa group-del unixadmins - - To add the "remoteadmins" group to the "localadmins" group: - ipa group-add-member --groups=remoteadmins localadmins - - Add multiple users to the "localadmins" group: - ipa group-add-member --users=test1 --users=test2 localadmins - - Remove a user from the "localadmins" group: - ipa group-remove-member --users=test2 localadmins - - Display information about a named group. - ipa group-show localadmins - -External group membership is designed to allow users from trusted domains -to be mapped to local POSIX groups in order to actually use IPA resources. -External members should be added to groups that specifically created as -external and non-POSIX. Such group later should be included into one of POSIX -groups. - -An external group member is currently a Security Identifier (SID) as defined by -the trusted domain. When adding external group members, it is possible to -specify them in either SID, or DOM\\name, or name@domain format. IPA will attempt -to resolve passed name to SID with the use of Global Catalog of the trusted domain. - -Example: - -1. Create group for the trusted domain admins' mapping and their local POSIX group: - - ipa group-add --desc='<ad.domain> admins external map' ad_admins_external --external - ipa group-add --desc='<ad.domain> admins' ad_admins - -2. Add security identifier of Domain Admins of the <ad.domain> to the ad_admins_external - group: - - ipa group-add-member ad_admins_external --external 'AD\\Domain Admins' - -3. Allow members of ad_admins_external group to be associated with ad_admins POSIX group: - - ipa group-add-member ad_admins --groups ad_admins_external - -4. List members of external members of ad_admins_external group to see their SIDs: - - ipa group-show ad_admins_external -""") - -register = Registry() - -PROTECTED_GROUPS = (u'admins', u'trust admins', u'default smb group') - - -@register() -class group(LDAPObject): - """ - Group object. - """ - container_dn = api.env.container_group - object_name = _('group') - object_name_plural = _('groups') - object_class = ['ipausergroup'] - object_class_config = 'ipagroupobjectclasses' - possible_objectclasses = ['posixGroup', 'mepManagedEntry', 'ipaExternalGroup'] - permission_filter_objectclasses = ['posixgroup', 'ipausergroup'] - search_attributes_config = 'ipagroupsearchfields' - default_attributes = [ - 'cn', 'description', 'gidnumber', 'member', 'memberof', - 'memberindirect', 'memberofindirect', 'ipaexternalmember', - ] - uuid_attribute = 'ipauniqueid' - attribute_members = { - 'member': ['user', 'group'], - 'memberof': ['group', 'netgroup', 'role', 'hbacrule', 'sudorule'], - 'memberindirect': ['user', 'group'], - 'memberofindirect': ['group', 'netgroup', 'role', 'hbacrule', - 'sudorule'], - } - rdn_is_primary_key = True - managed_permissions = { - 'System: Read Groups': { - 'replaces_global_anonymous_aci': True, - 'ipapermbindruletype': 'anonymous', - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'businesscategory', 'cn', 'description', 'gidnumber', - 'ipaexternalmember', 'ipauniqueid', 'mepmanagedby', 'o', - 'objectclass', 'ou', 'owner', 'seealso', - 'ipantsecurityidentifier' - }, - }, - 'System: Read Group Membership': { - 'replaces_global_anonymous_aci': True, - 'ipapermbindruletype': 'all', - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'member', 'memberof', 'memberuid', 'memberuser', 'memberhost', - }, - }, - 'System: Add Groups': { - 'ipapermright': {'add'}, - 'replaces': [ - '(target = "ldap:///cn=*,cn=groups,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Add Groups";allow (add) groupdn = "ldap:///cn=Add Groups,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Group Administrators'}, - }, - 'System: Modify Group Membership': { - 'ipapermright': {'write'}, - 'ipapermtargetfilter': [ - '(objectclass=ipausergroup)', - '(!(cn=admins))', - ], - 'ipapermdefaultattr': {'member'}, - 'replaces': [ - '(targetattr = "member")(target = "ldap:///cn=*,cn=groups,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Modify Group membership";allow (write) groupdn = "ldap:///cn=Modify Group membership,cn=permissions,cn=pbac,$SUFFIX";)', - '(targetfilter = "(!(cn=admins))")(targetattr = "member")(target = "ldap:///cn=*,cn=groups,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Modify Group membership";allow (write) groupdn = "ldap:///cn=Modify Group membership,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': { - 'Group Administrators', 'Modify Group membership' - }, - }, - 'System: Modify Groups': { - 'ipapermright': {'write'}, - 'ipapermdefaultattr': { - 'cn', 'description', 'gidnumber', 'ipauniqueid', - 'mepmanagedby', 'objectclass' - }, - 'replaces': [ - '(targetattr = "cn || description || gidnumber || objectclass || mepmanagedby || ipauniqueid")(target = "ldap:///cn=*,cn=groups,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Modify Groups";allow (write) groupdn = "ldap:///cn=Modify Groups,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Group Administrators'}, - }, - 'System: Remove Groups': { - 'ipapermright': {'delete'}, - 'replaces': [ - '(target = "ldap:///cn=*,cn=groups,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Remove Groups";allow (delete) groupdn = "ldap:///cn=Remove Groups,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Group Administrators'}, - }, - 'System: Read Group Compat Tree': { - 'non_object': True, - 'ipapermbindruletype': 'anonymous', - 'ipapermlocation': api.env.basedn, - 'ipapermtarget': DN('cn=groups', 'cn=compat', api.env.basedn), - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'objectclass', 'cn', 'memberuid', 'gidnumber', - }, - }, - 'System: Read Group Views Compat Tree': { - 'non_object': True, - 'ipapermbindruletype': 'anonymous', - 'ipapermlocation': api.env.basedn, - 'ipapermtarget': DN('cn=groups', 'cn=*', 'cn=views', 'cn=compat', api.env.basedn), - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'objectclass', 'cn', 'memberuid', 'gidnumber', - }, - }, - } - - label = _('User Groups') - label_singular = _('User Group') - - takes_params = ( - Str('cn', - pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?$', - pattern_errmsg='may only include letters, numbers, _, -, . and $', - maxlength=255, - cli_name='group_name', - label=_('Group name'), - primary_key=True, - normalizer=lambda value: value.lower(), - ), - Str('description?', - cli_name='desc', - label=_('Description'), - doc=_('Group description'), - ), - Int('gidnumber?', - cli_name='gid', - label=_('GID'), - doc=_('GID (use this option to set it manually)'), - minvalue=1, - ), - ) - - -ipaexternalmember_param = Str('ipaexternalmember*', - cli_name='external', - label=_('External member'), - doc=_('Members of a trusted domain in DOM\\name or name@domain form'), - flags=['no_create', 'no_update', 'no_search'], - ) - - -@register() -class group_add(LDAPCreate): - __doc__ = _('Create a new group.') - - msg_summary = _('Added group "%(value)s"') - - takes_options = LDAPCreate.takes_options + ( - Flag('nonposix', - cli_name='nonposix', - doc=_('Create as a non-POSIX group'), - default=False, - ), - Flag('external', - cli_name='external', - doc=_('Allow adding external non-IPA members from trusted domains'), - default=False, - ), - ) - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - # As both 'external' and 'nonposix' options have default= set for - # them, they will always be present in options dict, thus we can - # safely reference the values - assert isinstance(dn, DN) - if options['external']: - entry_attrs['objectclass'].append('ipaexternalgroup') - if 'gidnumber' in options: - raise errors.MutuallyExclusiveError(reason=_('gid cannot be set for external group')) - elif not options['nonposix']: - entry_attrs['objectclass'].append('posixgroup') - if not 'gidnumber' in options: - entry_attrs['gidnumber'] = baseldap.DNA_MAGIC - return dn - - -@register() -class group_del(LDAPDelete): - __doc__ = _('Delete group.') - - msg_summary = _('Deleted group "%(value)s"') - - def pre_callback(self, ldap, dn, *keys, **options): - assert isinstance(dn, DN) - config = ldap.get_ipa_config() - def_primary_group = config.get('ipadefaultprimarygroup', '') - def_primary_group_dn = group_dn = self.obj.get_dn(def_primary_group) - if dn == def_primary_group_dn: - raise errors.DefaultGroupError() - group_attrs = self.obj.methods.show( - self.obj.get_primary_key_from_dn(dn), all=True - )['result'] - if keys[0] in PROTECTED_GROUPS: - raise errors.ProtectedEntryError(label=_(u'group'), key=keys[0], - reason=_(u'privileged group')) - if 'mepmanagedby' in group_attrs: - raise errors.ManagedGroupError() - - # Remove any ID overrides tied with this group - remove_ipaobject_overrides(ldap, self.obj.api, dn) - - return dn - - def post_callback(self, ldap, dn, *keys, **options): - assert isinstance(dn, DN) - try: - api.Command['pwpolicy_del'](keys[-1]) - except errors.NotFound: - pass - - return True - - -@register() -class group_mod(LDAPUpdate): - __doc__ = _('Modify a group.') - - msg_summary = _('Modified group "%(value)s"') - - takes_options = LDAPUpdate.takes_options + ( - Flag('posix', - cli_name='posix', - doc=_('change to a POSIX group'), - ), - Flag('external', - cli_name='external', - doc=_('change to support external non-IPA members from trusted domains'), - default=False, - ), - ) - - def pre_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - - is_protected_group = keys[-1] in PROTECTED_GROUPS - - if 'rename' in options or 'cn' in entry_attrs: - if is_protected_group: - raise errors.ProtectedEntryError(label=u'group', key=keys[-1], - reason=u'Cannot be renamed') - - if ('posix' in options and options['posix']) or 'gidnumber' in options: - old_entry_attrs = ldap.get_entry(dn, ['objectclass']) - dn = old_entry_attrs.dn - if 'ipaexternalgroup' in old_entry_attrs['objectclass']: - raise errors.ExternalGroupViolation() - if 'posixgroup' in old_entry_attrs['objectclass']: - if options['posix']: - raise errors.AlreadyPosixGroup() - else: - old_entry_attrs['objectclass'].append('posixgroup') - entry_attrs['objectclass'] = old_entry_attrs['objectclass'] - if not 'gidnumber' in options: - entry_attrs['gidnumber'] = baseldap.DNA_MAGIC - - if options['external']: - if is_protected_group: - raise errors.ProtectedEntryError(label=u'group', key=keys[-1], - reason=u'Cannot support external non-IPA members') - old_entry_attrs = ldap.get_entry(dn, ['objectclass']) - dn = old_entry_attrs.dn - if 'posixgroup' in old_entry_attrs['objectclass']: - raise errors.PosixGroupViolation() - if 'ipaexternalgroup' in old_entry_attrs['objectclass']: - raise errors.AlreadyExternalGroup() - else: - old_entry_attrs['objectclass'].append('ipaexternalgroup') - entry_attrs['objectclass'] = old_entry_attrs['objectclass'] - - # Can't check for this in a validator because we lack context - if 'gidnumber' in options and options['gidnumber'] is None: - raise errors.RequirementError(name='gidnumber') - return dn - - def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs): - # Check again for GID requirement in case someone tried to clear it - # using --setattr. - if call_func.__name__ == 'update_entry': - if isinstance(exc, errors.ObjectclassViolation): - if 'gidNumber' in exc.message and 'posixGroup' in exc.message: - raise errors.RequirementError(name='gidnumber') - raise exc - - -@register() -class group_find(LDAPSearch): - __doc__ = _('Search for groups.') - - member_attributes = ['member', 'memberof'] - - msg_summary = ngettext( - '%(count)d group matched', '%(count)d groups matched', 0 - ) - - takes_options = LDAPSearch.takes_options + ( - Flag('private', - cli_name='private', - doc=_('search for private groups'), - ), - Flag('posix', - cli_name='posix', - doc=_('search for POSIX groups'), - ), - Flag('external', - cli_name='external', - doc=_('search for groups with support of external non-IPA members from trusted domains'), - ), - Flag('nonposix', - cli_name='nonposix', - doc=_('search for non-POSIX groups'), - ), - ) - - def pre_callback(self, ldap, filter, attrs_list, base_dn, scope, - criteria=None, **options): - assert isinstance(base_dn, DN) - - # filter groups by pseudo type - filters = [] - if options['posix']: - search_kw = {'objectclass': ['posixGroup']} - filters.append(ldap.make_filter(search_kw, rules=ldap.MATCH_ALL)) - if options['external']: - search_kw = {'objectclass': ['ipaExternalGroup']} - filters.append(ldap.make_filter(search_kw, rules=ldap.MATCH_ALL)) - if options['nonposix']: - search_kw = {'objectclass': ['posixGroup' , 'ipaExternalGroup']} - filters.append(ldap.make_filter(search_kw, rules=ldap.MATCH_NONE)) - - # if looking for private groups, we need to create a new search filter, - # because private groups have different object classes - if options['private']: - # filter based on options, oflt - search_kw = self.args_options_2_entry(**options) - search_kw['objectclass'] = ['posixGroup', 'mepManagedEntry'] - oflt = ldap.make_filter(search_kw, rules=ldap.MATCH_ALL) - - # filter based on 'criteria' argument - search_kw = {} - config = ldap.get_ipa_config() - attrs = config.get(self.obj.search_attributes_config, []) - if len(attrs) == 1 and isinstance(attrs[0], six.string_types): - search_attrs = attrs[0].split(',') - for a in search_attrs: - search_kw[a] = criteria - cflt = ldap.make_filter(search_kw, exact=False) - - filter = ldap.combine_filters((oflt, cflt), rules=ldap.MATCH_ALL) - elif filters: - filters.append(filter) - filter = ldap.combine_filters(filters, rules=ldap.MATCH_ALL) - return (filter, base_dn, scope) - - -@register() -class group_show(LDAPRetrieve): - __doc__ = _('Display information about a named group.') - has_output_params = LDAPRetrieve.has_output_params + (ipaexternalmember_param,) - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - if ('ipaexternalmember' in entry_attrs and - len(entry_attrs['ipaexternalmember']) > 0 and - 'trust_resolve' in self.Command and - not options.get('raw', False)): - sids = entry_attrs['ipaexternalmember'] - result = self.Command.trust_resolve(sids=sids) - for entry in result['result']: - try: - idx = sids.index(entry['sid'][0]) - sids[idx] = entry['name'][0] - except ValueError: - pass - return dn - - -@register() -class group_add_member(LDAPAddMember): - __doc__ = _('Add members to a group.') - - takes_options = (ipaexternalmember_param,) - - def post_callback(self, ldap, completed, failed, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - result = (completed, dn) - if 'ipaexternalmember' in options: - if not _dcerpc_bindings_installed: - raise errors.NotFound(reason=_('Cannot perform external member validation without ' - 'Samba 4 support installed. Make sure you have installed ' - 'server-trust-ad sub-package of IPA on the server')) - domain_validator = ipaserver.dcerpc.DomainValidator(self.api) - if not domain_validator.is_configured(): - raise errors.NotFound(reason=_('Cannot perform join operation without own domain configured. ' - 'Make sure you have run ipa-adtrust-install on the IPA server first')) - sids = [] - failed_sids = [] - for sid in options['ipaexternalmember']: - if domain_validator.is_trusted_sid_valid(sid): - sids.append(sid) - else: - try: - actual_sid = domain_validator.get_trusted_domain_object_sid(sid) - except errors.PublicError as e: - failed_sids.append((sid, e.strerror)) - else: - sids.append(actual_sid) - restore = [] - if 'member' in failed and 'group' in failed['member']: - restore = failed['member']['group'] - failed['member']['group'] = list((id, id) for id in sids) - result = add_external_post_callback(ldap, dn, entry_attrs, - failed=failed, - completed=completed, - memberattr='member', - membertype='group', - externalattr='ipaexternalmember', - normalize=False) - failed['member']['group'] += restore + failed_sids - return result - - -@register() -class group_remove_member(LDAPRemoveMember): - __doc__ = _('Remove members from a group.') - - takes_options = (ipaexternalmember_param,) - - def pre_callback(self, ldap, dn, found, not_found, *keys, **options): - assert isinstance(dn, DN) - if keys[0] in PROTECTED_GROUPS and 'user' in options: - protected_group_name = keys[0] - result = api.Command.group_show(protected_group_name) - users_left = set(result['result'].get('member_user', [])) - users_deleted = set(options['user']) - if users_left.issubset(users_deleted): - raise errors.LastMemberError(key=sorted(users_deleted)[0], - label=_(u'group'), container=protected_group_name) - return dn - - def post_callback(self, ldap, completed, failed, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - result = (completed, dn) - if 'ipaexternalmember' in options: - if not _dcerpc_bindings_installed: - raise errors.NotFound(reason=_('Cannot perform external member validation without ' - 'Samba 4 support installed. Make sure you have installed ' - 'server-trust-ad sub-package of IPA on the server')) - domain_validator = ipaserver.dcerpc.DomainValidator(self.api) - if not domain_validator.is_configured(): - raise errors.NotFound(reason=_('Cannot perform join operation without own domain configured. ' - 'Make sure you have run ipa-adtrust-install on the IPA server first')) - sids = [] - failed_sids = [] - for sid in options['ipaexternalmember']: - if domain_validator.is_trusted_sid_valid(sid): - sids.append(sid) - else: - try: - actual_sid = domain_validator.get_trusted_domain_object_sid(sid) - except errors.PublicError as e: - failed_sids.append((sid, unicode(e))) - else: - sids.append(actual_sid) - restore = [] - if 'member' in failed and 'group' in failed['member']: - restore = failed['member']['group'] - failed['member']['group'] = list((id, id) for id in sids) - result = remove_external_post_callback(ldap, dn, entry_attrs, - failed=failed, - completed=completed, - memberattr='member', - membertype='group', - externalattr='ipaexternalmember', - ) - failed['member']['group'] += restore + failed_sids - return result - - -@register() -class group_detach(LDAPQuery): - __doc__ = _('Detach a managed group from a user.') - - has_output = output.standard_value - msg_summary = _('Detached group "%(value)s" from user "%(value)s"') - - def execute(self, *keys, **options): - """ - This requires updating both the user and the group. We first need to - verify that both the user and group can be updated, then we go - about our work. We don't want a situation where only the user or - group can be modified and we're left in a bad state. - """ - ldap = self.obj.backend - - group_dn = self.obj.get_dn(*keys, **options) - user_dn = self.api.Object['user'].get_dn(*keys) - - try: - user_attrs = ldap.get_entry(user_dn) - except errors.NotFound: - self.obj.handle_not_found(*keys) - is_managed = self.obj.has_objectclass(user_attrs['objectclass'], 'mepmanagedentry') - if (not ldap.can_write(user_dn, "objectclass") or - not (ldap.can_write(user_dn, "mepManagedEntry")) and is_managed): - raise errors.ACIError(info=_('not allowed to modify user entries')) - - group_attrs = ldap.get_entry(group_dn) - is_managed = self.obj.has_objectclass(group_attrs['objectclass'], 'mepmanagedby') - if (not ldap.can_write(group_dn, "objectclass") or - not (ldap.can_write(group_dn, "mepManagedBy")) and is_managed): - raise errors.ACIError(info=_('not allowed to modify group entries')) - - objectclasses = user_attrs['objectclass'] - try: - i = objectclasses.index('mepOriginEntry') - del objectclasses[i] - user_attrs['mepManagedEntry'] = None - ldap.update_entry(user_attrs) - except ValueError: - # Somehow the user isn't managed, let it pass for now. We'll - # let the group throw "Not managed". - pass - - group_attrs = ldap.get_entry(group_dn) - objectclasses = group_attrs['objectclass'] - try: - i = objectclasses.index('mepManagedEntry') - except ValueError: - # this should never happen - raise errors.NotFound(reason=_('Not a managed group')) - del objectclasses[i] - - # Make sure the resulting group has the default group objectclasses - config = ldap.get_ipa_config() - def_objectclass = config.get( - self.obj.object_class_config, objectclasses - ) - objectclasses = list(set(def_objectclass + objectclasses)) - - group_attrs['mepManagedBy'] = None - group_attrs['objectclass'] = objectclasses - ldap.update_entry(group_attrs) - - return dict( - result=True, - value=pkey_to_value(keys[0], options), - ) - diff --git a/ipalib/plugins/hbac.py b/ipalib/plugins/hbac.py deleted file mode 100644 index 59defc1f2..000000000 --- a/ipalib/plugins/hbac.py +++ /dev/null @@ -1,7 +0,0 @@ -# -# Copyright (C) 2016 FreeIPA Contributors see COPYING for license -# - -from ipalib.text import _ - -__doc__ = _('Host-based access control commands') diff --git a/ipalib/plugins/hbacrule.py b/ipalib/plugins/hbacrule.py deleted file mode 100644 index 7d3e4851a..000000000 --- a/ipalib/plugins/hbacrule.py +++ /dev/null @@ -1,605 +0,0 @@ -# Authors: -# 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/>. - -from ipalib import api, errors -from ipalib import AccessTime, Str, StrEnum, Bool -from ipalib.plugable import Registry -from .baseldap import ( - pkey_to_value, - external_host_param, - LDAPObject, - LDAPCreate, - LDAPDelete, - LDAPRetrieve, - LDAPUpdate, - LDAPSearch, - LDAPQuery, - LDAPAddMember, - LDAPRemoveMember) -from ipalib import _, ngettext -from ipalib import output -from ipapython.dn import DN - -__doc__ = _(""" -Host-based access control - -Control who can access what services on what hosts. You -can use HBAC to control which users or groups can -access a service, or group of services, on a target host. - -You can also specify a category of users and target hosts. -This is currently limited to "all", but might be expanded in the -future. - -Target hosts in HBAC rules must be hosts managed by IPA. - -The available services and groups of services are controlled by the -hbacsvc and hbacsvcgroup plug-ins respectively. - -EXAMPLES: - - Create a rule, "test1", that grants all users access to the host "server" from - anywhere: - ipa hbacrule-add --usercat=all test1 - ipa hbacrule-add-host --hosts=server.example.com test1 - - Display the properties of a named HBAC rule: - ipa hbacrule-show test1 - - Create a rule for a specific service. This lets the user john access - the sshd service on any machine from any machine: - ipa hbacrule-add --hostcat=all john_sshd - ipa hbacrule-add-user --users=john john_sshd - ipa hbacrule-add-service --hbacsvcs=sshd john_sshd - - Create a rule for a new service group. This lets the user john access - the FTP service on any machine from any machine: - ipa hbacsvcgroup-add ftpers - ipa hbacsvc-add sftp - ipa hbacsvcgroup-add-member --hbacsvcs=ftp --hbacsvcs=sftp ftpers - ipa hbacrule-add --hostcat=all john_ftp - ipa hbacrule-add-user --users=john john_ftp - ipa hbacrule-add-service --hbacsvcgroups=ftpers john_ftp - - Disable a named HBAC rule: - ipa hbacrule-disable test1 - - Remove a named HBAC rule: - ipa hbacrule-del allow_server -""") - -register = Registry() - -# AccessTime support is being removed for now. -# -# You can also control the times that the rule is active. -# -# The access time(s) of a host are cumulative and are not guaranteed to be -# applied in the order displayed. -# -# Specify that the rule "test1" be active every day between 0800 and 1400: -# ipa hbacrule-add-accesstime --time='periodic daily 0800-1400' test1 -# -# Specify that the rule "test1" be active once, from 10:32 until 10:33 on -# December 16, 2010: -# ipa hbacrule-add-accesstime --time='absolute 201012161032 ~ 201012161033' test1 - - -topic = 'hbac' - -def validate_type(ugettext, type): - if type.lower() == 'deny': - raise errors.ValidationError(name='type', error=_('The deny type has been deprecated.')) - -def is_all(options, attribute): - """ - See if options[attribute] is lower-case 'all' in a safe way. - """ - if attribute in options and options[attribute] is not None: - if type(options[attribute]) in (list, tuple): - value = options[attribute][0].lower() - else: - value = options[attribute].lower() - if value == 'all': - return True - else: - return False - - -@register() -class hbacrule(LDAPObject): - """ - HBAC object. - """ - container_dn = api.env.container_hbac - object_name = _('HBAC rule') - object_name_plural = _('HBAC rules') - object_class = ['ipaassociation', 'ipahbacrule'] - permission_filter_objectclasses = ['ipahbacrule'] - default_attributes = [ - 'cn', 'ipaenabledflag', - 'description', 'usercategory', 'hostcategory', - 'servicecategory', 'ipaenabledflag', - 'memberuser', 'sourcehost', 'memberhost', 'memberservice', - 'externalhost', - ] - uuid_attribute = 'ipauniqueid' - rdn_attribute = 'ipauniqueid' - attribute_members = { - 'memberuser': ['user', 'group'], - 'memberhost': ['host', 'hostgroup'], - 'sourcehost': ['host', 'hostgroup'], - 'memberservice': ['hbacsvc', 'hbacsvcgroup'], - } - managed_permissions = { - 'System: Read HBAC Rules': { - 'replaces_global_anonymous_aci': True, - 'ipapermbindruletype': 'all', - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'accessruletype', 'accesstime', 'cn', 'description', - 'externalhost', 'hostcategory', 'ipaenabledflag', - 'ipauniqueid', 'memberhost', 'memberservice', 'memberuser', - 'servicecategory', 'sourcehost', 'sourcehostcategory', - 'usercategory', 'objectclass', 'member', - }, - }, - 'System: Add HBAC Rule': { - 'ipapermright': {'add'}, - 'replaces': [ - '(target = "ldap:///ipauniqueid=*,cn=hbac,$SUFFIX")(version 3.0;acl "permission:Add HBAC rule";allow (add) groupdn = "ldap:///cn=Add HBAC rule,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'HBAC Administrator'}, - }, - 'System: Delete HBAC Rule': { - 'ipapermright': {'delete'}, - 'replaces': [ - '(target = "ldap:///ipauniqueid=*,cn=hbac,$SUFFIX")(version 3.0;acl "permission:Delete HBAC rule";allow (delete) groupdn = "ldap:///cn=Delete HBAC rule,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'HBAC Administrator'}, - }, - 'System: Manage HBAC Rule Membership': { - 'ipapermright': {'write'}, - 'ipapermdefaultattr': { - 'externalhost', 'memberhost', 'memberservice', 'memberuser' - }, - 'replaces': [ - '(targetattr = "memberuser || externalhost || memberservice || memberhost")(target = "ldap:///ipauniqueid=*,cn=hbac,$SUFFIX")(version 3.0;acl "permission:Manage HBAC rule membership";allow (write) groupdn = "ldap:///cn=Manage HBAC rule membership,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'HBAC Administrator'}, - }, - 'System: Modify HBAC Rule': { - 'ipapermright': {'write'}, - 'ipapermdefaultattr': { - 'accessruletype', 'accesstime', 'cn', 'description', - 'hostcategory', 'ipaenabledflag', 'servicecategory', - 'sourcehost', 'sourcehostcategory', 'usercategory' - }, - 'replaces': [ - '(targetattr = "servicecategory || sourcehostcategory || cn || description || ipaenabledflag || accesstime || usercategory || hostcategory || accessruletype || sourcehost")(target = "ldap:///ipauniqueid=*,cn=hbac,$SUFFIX")(version 3.0;acl "permission:Modify HBAC rule";allow (write) groupdn = "ldap:///cn=Modify HBAC rule,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'HBAC Administrator'}, - }, - } - - label = _('HBAC Rules') - label_singular = _('HBAC Rule') - - takes_params = ( - Str('cn', - cli_name='name', - label=_('Rule name'), - primary_key=True, - ), - StrEnum('accessruletype', validate_type, - cli_name='type', - doc=_('Rule type (allow)'), - label=_('Rule type'), - values=(u'allow', u'deny'), - default=u'allow', - autofill=True, - exclude='webui', - flags=['no_option', 'no_output'], - ), - # FIXME: {user,host,service}categories should expand in the future - StrEnum('usercategory?', - cli_name='usercat', - label=_('User category'), - doc=_('User category the rule applies to'), - values=(u'all', ), - ), - StrEnum('hostcategory?', - cli_name='hostcat', - label=_('Host category'), - doc=_('Host category the rule applies to'), - values=(u'all', ), - ), - StrEnum('sourcehostcategory?', - deprecated=True, - cli_name='srchostcat', - label=_('Source host category'), - doc=_('Source host category the rule applies to'), - values=(u'all', ), - flags={'no_option'}, - ), - StrEnum('servicecategory?', - cli_name='servicecat', - label=_('Service category'), - doc=_('Service category the rule applies to'), - values=(u'all', ), - ), -# AccessTime('accesstime?', -# cli_name='time', -# label=_('Access time'), -# ), - Str('description?', - cli_name='desc', - label=_('Description'), - ), - Bool('ipaenabledflag?', - label=_('Enabled'), - flags=['no_option'], - ), - Str('memberuser_user?', - label=_('Users'), - flags=['no_create', 'no_update', 'no_search'], - ), - Str('memberuser_group?', - label=_('User Groups'), - flags=['no_create', 'no_update', 'no_search'], - ), - Str('memberhost_host?', - label=_('Hosts'), - flags=['no_create', 'no_update', 'no_search'], - ), - Str('memberhost_hostgroup?', - label=_('Host Groups'), - flags=['no_create', 'no_update', 'no_search'], - ), - Str('sourcehost_host?', - deprecated=True, - label=_('Source Hosts'), - flags=['no_create', 'no_update', 'no_search', 'no_option'], - ), - Str('sourcehost_hostgroup?', - deprecated=True, - label=_('Source Host Groups'), - flags=['no_create', 'no_update', 'no_search', 'no_option'], - ), - Str('memberservice_hbacsvc?', - label=_('Services'), - flags=['no_create', 'no_update', 'no_search'], - ), - Str('memberservice_hbacsvcgroup?', - label=_('Service Groups'), - flags=['no_create', 'no_update', 'no_search'], - ), - external_host_param, - ) - - - -@register() -class hbacrule_add(LDAPCreate): - __doc__ = _('Create a new HBAC rule.') - - msg_summary = _('Added HBAC rule "%(value)s"') - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - # HBAC rules are enabled by default - entry_attrs['ipaenabledflag'] = 'TRUE' - return dn - - - -@register() -class hbacrule_del(LDAPDelete): - __doc__ = _('Delete an HBAC rule.') - - msg_summary = _('Deleted HBAC rule "%(value)s"') - - def pre_callback(self, ldap, dn, *keys, **options): - assert isinstance(dn, DN) - kw = dict(seealso=keys[0]) - _entries = api.Command.selinuxusermap_find(None, **kw) - if _entries['count']: - raise errors.DependentEntry(key=keys[0], label=self.api.Object['selinuxusermap'].label_singular, dependent=_entries['result'][0]['cn'][0]) - - return dn - - - -@register() -class hbacrule_mod(LDAPUpdate): - __doc__ = _('Modify an HBAC rule.') - - msg_summary = _('Modified HBAC rule "%(value)s"') - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - try: - entry_attrs = ldap.get_entry(dn, attrs_list) - dn = entry_attrs.dn - except errors.NotFound: - self.obj.handle_not_found(*keys) - - if is_all(options, 'usercategory') and 'memberuser' in entry_attrs: - raise errors.MutuallyExclusiveError(reason=_("user category cannot be set to 'all' while there are allowed users")) - if is_all(options, 'hostcategory') and 'memberhost' in entry_attrs: - raise errors.MutuallyExclusiveError(reason=_("host category cannot be set to 'all' while there are allowed hosts")) - if is_all(options, 'servicecategory') and 'memberservice' in entry_attrs: - raise errors.MutuallyExclusiveError(reason=_("service category cannot be set to 'all' while there are allowed services")) - return dn - - - -@register() -class hbacrule_find(LDAPSearch): - __doc__ = _('Search for HBAC rules.') - - msg_summary = ngettext( - '%(count)d HBAC rule matched', '%(count)d HBAC rules matched', 0 - ) - - - -@register() -class hbacrule_show(LDAPRetrieve): - __doc__ = _('Display the properties of an HBAC rule.') - - - -@register() -class hbacrule_enable(LDAPQuery): - __doc__ = _('Enable an HBAC rule.') - - msg_summary = _('Enabled HBAC rule "%(value)s"') - has_output = output.standard_value - - def execute(self, cn, **options): - ldap = self.obj.backend - - dn = self.obj.get_dn(cn) - try: - entry_attrs = ldap.get_entry(dn, ['ipaenabledflag']) - except errors.NotFound: - self.obj.handle_not_found(cn) - - entry_attrs['ipaenabledflag'] = ['TRUE'] - - try: - ldap.update_entry(entry_attrs) - except errors.EmptyModlist: - pass - - return dict( - result=True, - value=pkey_to_value(cn, options), - ) - - - -@register() -class hbacrule_disable(LDAPQuery): - __doc__ = _('Disable an HBAC rule.') - - msg_summary = _('Disabled HBAC rule "%(value)s"') - has_output = output.standard_value - - def execute(self, cn, **options): - ldap = self.obj.backend - - dn = self.obj.get_dn(cn) - try: - entry_attrs = ldap.get_entry(dn, ['ipaenabledflag']) - except errors.NotFound: - self.obj.handle_not_found(cn) - - entry_attrs['ipaenabledflag'] = ['FALSE'] - - try: - ldap.update_entry(entry_attrs) - except errors.EmptyModlist: - pass - - return dict( - result=True, - value=pkey_to_value(cn, options), - ) - - -# @register() -class hbacrule_add_accesstime(LDAPQuery): - """ - Add an access time to an HBAC rule. - """ - - takes_options = ( - AccessTime('accesstime', - cli_name='time', - label=_('Access time'), - ), - ) - - def execute(self, cn, **options): - ldap = self.obj.backend - - dn = self.obj.get_dn(cn) - - entry_attrs = ldap.get_entry(dn, ['accesstime']) - entry_attrs.setdefault('accesstime', []).append( - options['accesstime'] - ) - try: - ldap.update_entry(entry_attrs) - except errors.EmptyModlist: - pass - except errors.NotFound: - self.obj.handle_not_found(cn) - - return dict(result=True) - - -# @register() -class hbacrule_remove_accesstime(LDAPQuery): - """ - Remove access time to HBAC rule. - """ - takes_options = ( - AccessTime('accesstime?', - cli_name='time', - label=_('Access time'), - ), - ) - - def execute(self, cn, **options): - ldap = self.obj.backend - - dn = self.obj.get_dn(cn) - - entry_attrs = ldap.get_entry(dn, ['accesstime']) - try: - entry_attrs.setdefault('accesstime', []).remove( - options['accesstime'] - ) - ldap.update_entry(entry_attrs) - except (ValueError, errors.EmptyModlist): - pass - except errors.NotFound: - self.obj.handle_not_found(cn) - - return dict(result=True) - - -@register() -class hbacrule_add_user(LDAPAddMember): - __doc__ = _('Add users and groups to an HBAC rule.') - - member_attributes = ['memberuser'] - member_count_out = ('%i object added.', '%i objects added.') - - def pre_callback(self, ldap, dn, found, not_found, *keys, **options): - assert isinstance(dn, DN) - try: - entry_attrs = ldap.get_entry(dn, self.obj.default_attributes) - dn = entry_attrs.dn - except errors.NotFound: - self.obj.handle_not_found(*keys) - if 'usercategory' in entry_attrs and \ - entry_attrs['usercategory'][0].lower() == 'all': - raise errors.MutuallyExclusiveError( - reason=_("users cannot be added when user category='all'")) - return dn - - - -@register() -class hbacrule_remove_user(LDAPRemoveMember): - __doc__ = _('Remove users and groups from an HBAC rule.') - - member_attributes = ['memberuser'] - member_count_out = ('%i object removed.', '%i objects removed.') - - - -@register() -class hbacrule_add_host(LDAPAddMember): - __doc__ = _('Add target hosts and hostgroups to an HBAC rule.') - - member_attributes = ['memberhost'] - member_count_out = ('%i object added.', '%i objects added.') - - def pre_callback(self, ldap, dn, found, not_found, *keys, **options): - assert isinstance(dn, DN) - try: - entry_attrs = ldap.get_entry(dn, self.obj.default_attributes) - dn = entry_attrs.dn - except errors.NotFound: - self.obj.handle_not_found(*keys) - if 'hostcategory' in entry_attrs and \ - entry_attrs['hostcategory'][0].lower() == 'all': - raise errors.MutuallyExclusiveError( - reason=_("hosts cannot be added when host category='all'")) - return dn - - - -@register() -class hbacrule_remove_host(LDAPRemoveMember): - __doc__ = _('Remove target hosts and hostgroups from an HBAC rule.') - - member_attributes = ['memberhost'] - member_count_out = ('%i object removed.', '%i objects removed.') - - - -@register() -class hbacrule_add_sourcehost(LDAPAddMember): - NO_CLI = True - - member_attributes = ['sourcehost'] - member_count_out = ('%i object added.', '%i objects added.') - - def validate(self, **kw): - raise errors.DeprecationError(name='hbacrule_add_sourcehost') - - - -@register() -class hbacrule_remove_sourcehost(LDAPRemoveMember): - NO_CLI = True - - member_attributes = ['sourcehost'] - member_count_out = ('%i object removed.', '%i objects removed.') - - def validate(self, **kw): - raise errors.DeprecationError(name='hbacrule_remove_sourcehost') - - - -@register() -class hbacrule_add_service(LDAPAddMember): - __doc__ = _('Add services to an HBAC rule.') - - member_attributes = ['memberservice'] - member_count_out = ('%i object added.', '%i objects added.') - - def pre_callback(self, ldap, dn, found, not_found, *keys, **options): - assert isinstance(dn, DN) - try: - entry_attrs = ldap.get_entry(dn, self.obj.default_attributes) - dn = entry_attrs.dn - except errors.NotFound: - self.obj.handle_not_found(*keys) - if 'servicecategory' in entry_attrs and \ - entry_attrs['servicecategory'][0].lower() == 'all': - raise errors.MutuallyExclusiveError(reason=_( - "services cannot be added when service category='all'")) - return dn - - - -@register() -class hbacrule_remove_service(LDAPRemoveMember): - __doc__ = _('Remove service and service groups from an HBAC rule.') - - member_attributes = ['memberservice'] - member_count_out = ('%i object removed.', '%i objects removed.') - diff --git a/ipalib/plugins/hbacsvc.py b/ipalib/plugins/hbacsvc.py deleted file mode 100644 index 43d641642..000000000 --- a/ipalib/plugins/hbacsvc.py +++ /dev/null @@ -1,152 +0,0 @@ -# Authors: -# Rob Crittenden <rcritten@redhat.com> -# -# Copyright (C) 2010 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/>. - -from ipalib import api -from ipalib import Str -from ipalib.plugable import Registry -from .baseldap import LDAPObject, LDAPCreate, LDAPDelete -from .baseldap import LDAPUpdate, LDAPSearch, LDAPRetrieve - -from ipalib import _, ngettext - -__doc__ = _(""" -HBAC Services - -The PAM services that HBAC can control access to. The name used here -must match the service name that PAM is evaluating. - -EXAMPLES: - - Add a new HBAC service: - ipa hbacsvc-add tftp - - Modify an existing HBAC service: - ipa hbacsvc-mod --desc="TFTP service" tftp - - Search for HBAC services. This example will return two results, the FTP - service and the newly-added tftp service: - ipa hbacsvc-find ftp - - Delete an HBAC service: - ipa hbacsvc-del tftp - -""") - -register = Registry() - -topic = 'hbac' - -@register() -class hbacsvc(LDAPObject): - """ - HBAC Service object. - """ - container_dn = api.env.container_hbacservice - object_name = _('HBAC service') - object_name_plural = _('HBAC services') - object_class = [ 'ipaobject', 'ipahbacservice' ] - permission_filter_objectclasses = ['ipahbacservice'] - default_attributes = ['cn', 'description', 'memberof'] - uuid_attribute = 'ipauniqueid' - attribute_members = { - 'memberof': ['hbacsvcgroup'], - } - managed_permissions = { - 'System: Read HBAC Services': { - 'replaces_global_anonymous_aci': True, - 'ipapermbindruletype': 'all', - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'cn', 'description', 'ipauniqueid', 'memberof', 'objectclass', - }, - }, - 'System: Add HBAC Services': { - 'ipapermright': {'add'}, - 'replaces': [ - '(target = "ldap:///cn=*,cn=hbacservices,cn=hbac,$SUFFIX")(version 3.0;acl "permission:Add HBAC services";allow (add) groupdn = "ldap:///cn=Add HBAC services,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'HBAC Administrator'}, - }, - 'System: Delete HBAC Services': { - 'ipapermright': {'delete'}, - 'replaces': [ - '(target = "ldap:///cn=*,cn=hbacservices,cn=hbac,$SUFFIX")(version 3.0;acl "permission:Delete HBAC services";allow (delete) groupdn = "ldap:///cn=Delete HBAC services,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'HBAC Administrator'}, - }, - } - - label = _('HBAC Services') - label_singular = _('HBAC Service') - - takes_params = ( - Str('cn', - cli_name='service', - label=_('Service name'), - doc=_('HBAC service'), - primary_key=True, - normalizer=lambda value: value.lower(), - ), - Str('description?', - cli_name='desc', - label=_('Description'), - doc=_('HBAC service description'), - ), - ) - - - -@register() -class hbacsvc_add(LDAPCreate): - __doc__ = _('Add a new HBAC service.') - - msg_summary = _('Added HBAC service "%(value)s"') - - - -@register() -class hbacsvc_del(LDAPDelete): - __doc__ = _('Delete an existing HBAC service.') - - msg_summary = _('Deleted HBAC service "%(value)s"') - - - -@register() -class hbacsvc_mod(LDAPUpdate): - __doc__ = _('Modify an HBAC service.') - - msg_summary = _('Modified HBAC service "%(value)s"') - - - -@register() -class hbacsvc_find(LDAPSearch): - __doc__ = _('Search for HBAC services.') - - msg_summary = ngettext( - '%(count)d HBAC service matched', '%(count)d HBAC services matched', 0 - ) - - - -@register() -class hbacsvc_show(LDAPRetrieve): - __doc__ = _('Display information about an HBAC service.') - diff --git a/ipalib/plugins/hbacsvcgroup.py b/ipalib/plugins/hbacsvcgroup.py deleted file mode 100644 index 41157efc6..000000000 --- a/ipalib/plugins/hbacsvcgroup.py +++ /dev/null @@ -1,176 +0,0 @@ -# Authors: -# Rob Crittenden <rcritten@redhat.com> -# -# Copyright (C) 2010 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/>. - -from ipalib import api, Str -from ipalib.plugable import Registry -from .baseldap import ( - LDAPObject, - LDAPCreate, - LDAPUpdate, - LDAPRetrieve, - LDAPSearch, - LDAPDelete, - LDAPAddMember, - LDAPRemoveMember) -from ipalib import _, ngettext - -__doc__ = _(""" -HBAC Service Groups - -HBAC service groups can contain any number of individual services, -or "members". Every group must have a description. - -EXAMPLES: - - Add a new HBAC service group: - ipa hbacsvcgroup-add --desc="login services" login - - Add members to an HBAC service group: - ipa hbacsvcgroup-add-member --hbacsvcs=sshd --hbacsvcs=login login - - Display information about a named group: - ipa hbacsvcgroup-show login - - Delete an HBAC service group: - ipa hbacsvcgroup-del login -""") - -register = Registry() - -topic = 'hbac' - -@register() -class hbacsvcgroup(LDAPObject): - """ - HBAC service group object. - """ - container_dn = api.env.container_hbacservicegroup - object_name = _('HBAC service group') - object_name_plural = _('HBAC service groups') - object_class = ['ipaobject', 'ipahbacservicegroup'] - permission_filter_objectclasses = ['ipahbacservicegroup'] - default_attributes = [ 'cn', 'description', 'member' ] - uuid_attribute = 'ipauniqueid' - attribute_members = { - 'member': ['hbacsvc'], - } - managed_permissions = { - 'System: Read HBAC Service Groups': { - 'replaces_global_anonymous_aci': True, - 'ipapermbindruletype': 'all', - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'businesscategory', 'cn', 'description', 'ipauniqueid', - 'member', 'o', 'objectclass', 'ou', 'owner', 'seealso', - 'memberuser', 'memberhost', - }, - }, - 'System: Add HBAC Service Groups': { - 'ipapermright': {'add'}, - 'replaces': [ - '(target = "ldap:///cn=*,cn=hbacservicegroups,cn=hbac,$SUFFIX")(version 3.0;acl "permission:Add HBAC service groups";allow (add) groupdn = "ldap:///cn=Add HBAC service groups,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'HBAC Administrator'}, - }, - 'System: Delete HBAC Service Groups': { - 'ipapermright': {'delete'}, - 'replaces': [ - '(target = "ldap:///cn=*,cn=hbacservicegroups,cn=hbac,$SUFFIX")(version 3.0;acl "permission:Delete HBAC service groups";allow (delete) groupdn = "ldap:///cn=Delete HBAC service groups,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'HBAC Administrator'}, - }, - 'System: Manage HBAC Service Group Membership': { - 'ipapermright': {'write'}, - 'ipapermdefaultattr': {'member'}, - 'replaces': [ - '(targetattr = "member")(target = "ldap:///cn=*,cn=hbacservicegroups,cn=hbac,$SUFFIX")(version 3.0;acl "permission:Manage HBAC service group membership";allow (write) groupdn = "ldap:///cn=Manage HBAC service group membership,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'HBAC Administrator'}, - }, - } - - label = _('HBAC Service Groups') - label_singular = _('HBAC Service Group') - - takes_params = ( - Str('cn', - cli_name='name', - label=_('Service group name'), - primary_key=True, - normalizer=lambda value: value.lower(), - ), - Str('description?', - cli_name='desc', - label=_('Description'), - doc=_('HBAC service group description'), - ), - ) - - - -@register() -class hbacsvcgroup_add(LDAPCreate): - __doc__ = _('Add a new HBAC service group.') - - msg_summary = _('Added HBAC service group "%(value)s"') - - - -@register() -class hbacsvcgroup_del(LDAPDelete): - __doc__ = _('Delete an HBAC service group.') - - msg_summary = _('Deleted HBAC service group "%(value)s"') - - - -@register() -class hbacsvcgroup_mod(LDAPUpdate): - __doc__ = _('Modify an HBAC service group.') - - msg_summary = _('Modified HBAC service group "%(value)s"') - - - -@register() -class hbacsvcgroup_find(LDAPSearch): - __doc__ = _('Search for an HBAC service group.') - - msg_summary = ngettext( - '%(count)d HBAC service group matched', '%(count)d HBAC service groups matched', 0 - ) - - - -@register() -class hbacsvcgroup_show(LDAPRetrieve): - __doc__ = _('Display information about an HBAC service group.') - - - -@register() -class hbacsvcgroup_add_member(LDAPAddMember): - __doc__ = _('Add members to an HBAC service group.') - - - -@register() -class hbacsvcgroup_remove_member(LDAPRemoveMember): - __doc__ = _('Remove members from an HBAC service group.') - diff --git a/ipalib/plugins/hbactest.py b/ipalib/plugins/hbactest.py deleted file mode 100644 index 90f3b561a..000000000 --- a/ipalib/plugins/hbactest.py +++ /dev/null @@ -1,499 +0,0 @@ -# Authors: -# Alexander Bokovoy <abokovoy@redhat.com> -# -# Copyright (C) 2011 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/>. - -from ipalib import api, errors, output, util -from ipalib import Command, Str, Flag, Int -from ipalib import _ -from ipapython.dn import DN -from ipalib.plugable import Registry -if api.env.in_server and api.env.context in ['lite', 'server']: - try: - import ipaserver.dcerpc - _dcerpc_bindings_installed = True - except ImportError: - _dcerpc_bindings_installed = False - -import pyhbac -import six - -if six.PY3: - unicode = str - -__doc__ = _(""" -Simulate use of Host-based access controls - -HBAC rules control who can access what services on what hosts. -You can use HBAC to control which users or groups can access a service, -or group of services, on a target host. - -Since applying HBAC rules implies use of a production environment, -this plugin aims to provide simulation of HBAC rules evaluation without -having access to the production environment. - - Test user coming to a service on a named host against - existing enabled rules. - - ipa hbactest --user= --host= --service= - [--rules=rules-list] [--nodetail] [--enabled] [--disabled] - [--sizelimit= ] - - --user, --host, and --service are mandatory, others are optional. - - If --rules is specified simulate enabling of the specified rules and test - the login of the user using only these rules. - - If --enabled is specified, all enabled HBAC rules will be added to simulation - - If --disabled is specified, all disabled HBAC rules will be added to simulation - - If --nodetail is specified, do not return information about rules matched/not matched. - - If both --rules and --enabled are specified, apply simulation to --rules _and_ - all IPA enabled rules. - - If no --rules specified, simulation is run against all IPA enabled rules. - By default there is a IPA-wide limit to number of entries fetched, you can change it - with --sizelimit option. - -EXAMPLES: - - 1. Use all enabled HBAC rules in IPA database to simulate: - $ ipa hbactest --user=a1a --host=bar --service=sshd - -------------------- - Access granted: True - -------------------- - Not matched rules: my-second-rule - Not matched rules: my-third-rule - Not matched rules: myrule - Matched rules: allow_all - - 2. Disable detailed summary of how rules were applied: - $ ipa hbactest --user=a1a --host=bar --service=sshd --nodetail - -------------------- - Access granted: True - -------------------- - - 3. Test explicitly specified HBAC rules: - $ ipa hbactest --user=a1a --host=bar --service=sshd \\ - --rules=myrule --rules=my-second-rule - --------------------- - Access granted: False - --------------------- - Not matched rules: my-second-rule - Not matched rules: myrule - - 4. Use all enabled HBAC rules in IPA database + explicitly specified rules: - $ ipa hbactest --user=a1a --host=bar --service=sshd \\ - --rules=myrule --rules=my-second-rule --enabled - -------------------- - Access granted: True - -------------------- - Not matched rules: my-second-rule - Not matched rules: my-third-rule - Not matched rules: myrule - Matched rules: allow_all - - 5. Test all disabled HBAC rules in IPA database: - $ ipa hbactest --user=a1a --host=bar --service=sshd --disabled - --------------------- - Access granted: False - --------------------- - Not matched rules: new-rule - - 6. Test all disabled HBAC rules in IPA database + explicitly specified rules: - $ ipa hbactest --user=a1a --host=bar --service=sshd \\ - --rules=myrule --rules=my-second-rule --disabled - --------------------- - Access granted: False - --------------------- - Not matched rules: my-second-rule - Not matched rules: my-third-rule - Not matched rules: myrule - - 7. Test all (enabled and disabled) HBAC rules in IPA database: - $ ipa hbactest --user=a1a --host=bar --service=sshd \\ - --enabled --disabled - -------------------- - Access granted: True - -------------------- - Not matched rules: my-second-rule - Not matched rules: my-third-rule - Not matched rules: myrule - Not matched rules: new-rule - Matched rules: allow_all - - -HBACTEST AND TRUSTED DOMAINS - -When an external trusted domain is configured in IPA, HBAC rules are also applied -on users accessing IPA resources from the trusted domain. Trusted domain users and -groups (and their SIDs) can be then assigned to external groups which can be -members of POSIX groups in IPA which can be used in HBAC rules and thus allowing -access to resources protected by the HBAC system. - -hbactest plugin is capable of testing access for both local IPA users and users -from the trusted domains, either by a fully qualified user name or by user SID. -Such user names need to have a trusted domain specified as a short name -(DOMAIN\Administrator) or with a user principal name (UPN), Administrator@ad.test. - -Please note that hbactest executed with a trusted domain user as --user parameter -can be only run by members of "trust admins" group. - -EXAMPLES: - - 1. Test if a user from a trusted domain specified by its shortname matches any - rule: - - $ ipa hbactest --user 'DOMAIN\Administrator' --host `hostname` --service sshd - -------------------- - Access granted: True - -------------------- - Matched rules: allow_all - Matched rules: can_login - - 2. Test if a user from a trusted domain specified by its domain name matches - any rule: - - $ ipa hbactest --user 'Administrator@domain.com' --host `hostname` --service sshd - -------------------- - Access granted: True - -------------------- - Matched rules: allow_all - Matched rules: can_login - - 3. Test if a user from a trusted domain specified by its SID matches any rule: - - $ ipa hbactest --user S-1-5-21-3035198329-144811719-1378114514-500 \\ - --host `hostname` --service sshd - -------------------- - Access granted: True - -------------------- - Matched rules: allow_all - Matched rules: can_login - - 4. Test if other user from a trusted domain specified by its SID matches any rule: - - $ ipa hbactest --user S-1-5-21-3035198329-144811719-1378114514-1203 \\ - --host `hostname` --service sshd - -------------------- - Access granted: True - -------------------- - Matched rules: allow_all - Not matched rules: can_login - - 5. Test if other user from a trusted domain specified by its shortname matches - any rule: - - $ ipa hbactest --user 'DOMAIN\Otheruser' --host `hostname` --service sshd - -------------------- - Access granted: True - -------------------- - Matched rules: allow_all - Not matched rules: can_login -""") - -register = Registry() - -def convert_to_ipa_rule(rule): - # convert a dict with a rule to an pyhbac rule - ipa_rule = pyhbac.HbacRule(rule['cn'][0]) - ipa_rule.enabled = rule['ipaenabledflag'][0] - # Following code attempts to process rule systematically - structure = \ - (('user', 'memberuser', 'user', 'group', ipa_rule.users), - ('host', 'memberhost', 'host', 'hostgroup', ipa_rule.targethosts), - ('sourcehost', 'sourcehost', 'host', 'hostgroup', ipa_rule.srchosts), - ('service', 'memberservice', 'hbacsvc', 'hbacsvcgroup', ipa_rule.services), - ) - for element in structure: - category = '%scategory' % (element[0]) - if (category in rule and rule[category][0] == u'all') or (element[0] == 'sourcehost'): - # rule applies to all elements - # sourcehost is always set to 'all' - element[4].category = set([pyhbac.HBAC_CATEGORY_ALL]) - else: - # rule is about specific entities - # Check if there are explicitly listed entities - attr_name = '%s_%s' % (element[1], element[2]) - if attr_name in rule: - element[4].names = rule[attr_name] - # Now add groups of entities if they are there - attr_name = '%s_%s' % (element[1], element[3]) - if attr_name in rule: - element[4].groups = rule[attr_name] - if 'externalhost' in rule: - ipa_rule.srchosts.names.extend(rule['externalhost']) #pylint: disable=E1101 - return ipa_rule - - -@register() -class hbactest(Command): - __doc__ = _('Simulate use of Host-based access controls') - - has_output = ( - output.summary, - output.Output('warning', (list, tuple, type(None)), _('Warning')), - output.Output('matched', (list, tuple, type(None)), _('Matched rules')), - output.Output('notmatched', (list, tuple, type(None)), _('Not matched rules')), - output.Output('error', (list, tuple, type(None)), _('Non-existent or invalid rules')), - output.Output('value', bool, _('Result of simulation'), ['no_display']), - ) - - takes_options = ( - Str('user', - cli_name='user', - label=_('User name'), - primary_key=True, - ), - Str('sourcehost?', - deprecated=True, - cli_name='srchost', - label=_('Source host'), - flags={'no_option'}, - ), - Str('targethost', - cli_name='host', - label=_('Target host'), - ), - Str('service', - cli_name='service', - label=_('Service'), - ), - Str('rules*', - cli_name='rules', - label=_('Rules to test. If not specified, --enabled is assumed'), - ), - Flag('nodetail?', - cli_name='nodetail', - label=_('Hide details which rules are matched, not matched, or invalid'), - ), - Flag('enabled?', - cli_name='enabled', - label=_('Include all enabled IPA rules into test [default]'), - ), - Flag('disabled?', - cli_name='disabled', - label=_('Include all disabled IPA rules into test'), - ), - Int('sizelimit?', - label=_('Size Limit'), - doc=_('Maximum number of rules to process when no --rules is specified'), - flags=['no_display'], - minvalue=0, - autofill=False, - ), - ) - - def canonicalize(self, host): - """ - Canonicalize the host name -- add default IPA domain if that is missing - """ - if host.find('.') == -1: - return u'%s.%s' % (host, self.env.domain) - return host - - def execute(self, *args, **options): - # First receive all needed information: - # 1. HBAC rules (whether enabled or disabled) - # 2. Required options are (user, target host, service) - # 3. Options: rules to test (--rules, --enabled, --disabled), request for detail output - rules = [] - - # Use all enabled IPA rules by default - all_enabled = True - all_disabled = False - - # We need a local copy of test rules in order find incorrect ones - testrules = {} - if 'rules' in options: - testrules = list(options['rules']) - # When explicit rules are provided, disable assumptions - all_enabled = False - all_disabled = False - - sizelimit = None - if 'sizelimit' in options: - sizelimit = int(options['sizelimit']) - - # Check if --disabled is specified, include all disabled IPA rules - if options['disabled']: - all_disabled = True - all_enabled = False - - # Finally, if enabled is specified implicitly, override above decisions - if options['enabled']: - all_enabled = True - - hbacset = [] - if len(testrules) == 0: - hbacset = self.api.Command.hbacrule_find( - sizelimit=sizelimit, no_members=False)['result'] - else: - for rule in testrules: - try: - hbacset.append(self.api.Command.hbacrule_show(rule)['result']) - except Exception: - pass - - # We have some rules, import them - # --enabled will import all enabled rules (default) - # --disabled will import all disabled rules - # --rules will implicitly add the rules from a rule list - for rule in hbacset: - ipa_rule = convert_to_ipa_rule(rule) - if ipa_rule.name in testrules: - ipa_rule.enabled = True - rules.append(ipa_rule) - testrules.remove(ipa_rule.name) - elif all_enabled and ipa_rule.enabled: - # Option --enabled forces to include all enabled IPA rules into test - rules.append(ipa_rule) - elif all_disabled and not ipa_rule.enabled: - # Option --disabled forces to include all disabled IPA rules into test - ipa_rule.enabled = True - rules.append(ipa_rule) - - # Check if there are unresolved rules left - if len(testrules) > 0: - # Error, unresolved rules are left in --rules - return {'summary' : unicode(_(u'Unresolved rules in --rules')), - 'error': testrules, 'matched': None, 'notmatched': None, - 'warning' : None, 'value' : False} - - # Rules are converted to pyhbac format, build request and then test it - request = pyhbac.HbacRequest() - - if options['user'] != u'all': - # check first if this is not a trusted domain user - if _dcerpc_bindings_installed: - is_valid_sid = ipaserver.dcerpc.is_sid_valid(options['user']) - else: - is_valid_sid = False - components = util.normalize_name(options['user']) - if is_valid_sid or 'domain' in components or 'flatname' in components: - # this is a trusted domain user - if not _dcerpc_bindings_installed: - raise errors.NotFound(reason=_( - 'Cannot perform external member validation without ' - 'Samba 4 support installed. Make sure you have installed ' - 'server-trust-ad sub-package of IPA on the server')) - domain_validator = ipaserver.dcerpc.DomainValidator(self.api) - if not domain_validator.is_configured(): - raise errors.NotFound(reason=_( - 'Cannot search in trusted domains without own domain configured. ' - 'Make sure you have run ipa-adtrust-install on the IPA server first')) - user_sid, group_sids = domain_validator.get_trusted_domain_user_and_groups(options['user']) - request.user.name = user_sid - - # Now search for all external groups that have this user or - # any of its groups in its external members. Found entires - # memberOf links will be then used to gather all groups where - # this group is assigned, including the nested ones - filter_sids = "(&(objectclass=ipaexternalgroup)(|(ipaExternalMember=%s)))" \ - % ")(ipaExternalMember=".join(group_sids + [user_sid]) - - ldap = self.api.Backend.ldap2 - group_container = DN(api.env.container_group, api.env.basedn) - try: - entries, truncated = ldap.find_entries(filter_sids, ['memberof'], group_container) - except errors.NotFound: - request.user.groups = [] - else: - groups = [] - for entry in entries: - memberof_dns = entry.get('memberof', []) - for memberof_dn in memberof_dns: - if memberof_dn.endswith(group_container): - groups.append(memberof_dn[0][0].value) - request.user.groups = sorted(set(groups)) - else: - # try searching for a local user - try: - request.user.name = options['user'] - search_result = self.api.Command.user_show(request.user.name)['result'] - groups = search_result['memberof_group'] - if 'memberofindirect_group' in search_result: - groups += search_result['memberofindirect_group'] - request.user.groups = sorted(set(groups)) - except Exception: - pass - - if options['service'] != u'all': - try: - request.service.name = options['service'] - service_result = self.api.Command.hbacsvc_show(request.service.name)['result'] - if 'memberof_hbacsvcgroup' in service_result: - request.service.groups = service_result['memberof_hbacsvcgroup'] - except Exception: - pass - - if options['targethost'] != u'all': - try: - request.targethost.name = self.canonicalize(options['targethost']) - tgthost_result = self.api.Command.host_show(request.targethost.name)['result'] - groups = tgthost_result['memberof_hostgroup'] - if 'memberofindirect_hostgroup' in tgthost_result: - groups += tgthost_result['memberofindirect_hostgroup'] - request.targethost.groups = sorted(set(groups)) - except Exception: - pass - - matched_rules = [] - notmatched_rules = [] - error_rules = [] - warning_rules = [] - - result = {'warning':None, 'matched':None, 'notmatched':None, 'error':None} - if not options['nodetail']: - # Validate runs rules one-by-one and reports failed ones - for ipa_rule in rules: - try: - res = request.evaluate([ipa_rule]) - if res == pyhbac.HBAC_EVAL_ALLOW: - matched_rules.append(ipa_rule.name) - if res == pyhbac.HBAC_EVAL_DENY: - notmatched_rules.append(ipa_rule.name) - except pyhbac.HbacError as e: - code, rule_name = e.args - if code == pyhbac.HBAC_EVAL_ERROR: - error_rules.append(rule_name) - self.log.info('Native IPA HBAC rule "%s" parsing error: %s' % \ - (rule_name, pyhbac.hbac_result_string(code))) - except (TypeError, IOError) as info: - self.log.error('Native IPA HBAC module error: %s' % info) - - access_granted = len(matched_rules) > 0 - else: - res = request.evaluate(rules) - access_granted = (res == pyhbac.HBAC_EVAL_ALLOW) - - result['summary'] = _('Access granted: %s') % (access_granted) - - - if len(matched_rules) > 0: - result['matched'] = matched_rules - if len(notmatched_rules) > 0: - result['notmatched'] = notmatched_rules - if len(error_rules) > 0: - result['error'] = error_rules - if len(warning_rules) > 0: - result['warning'] = warning_rules - - result['value'] = access_granted - return result diff --git a/ipalib/plugins/host.py b/ipalib/plugins/host.py deleted file mode 100644 index 709b78d5b..000000000 --- a/ipalib/plugins/host.py +++ /dev/null @@ -1,1284 +0,0 @@ -# Authors: -# Rob Crittenden <rcritten@redhat.com> -# Pavel Zuna <pzuna@redhat.com> -# -# Copyright (C) 2008 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/>. - -import string - -import six - -from ipalib import api, errors, util -from ipalib import messages -from ipalib import Str, Flag, Bytes -from ipalib.plugable import Registry -from .baseldap import (LDAPQuery, LDAPObject, LDAPCreate, - LDAPDelete, LDAPUpdate, LDAPSearch, - LDAPRetrieve, LDAPAddMember, - LDAPRemoveMember, host_is_master, - pkey_to_value, add_missing_object_class, - LDAPAddAttribute, LDAPRemoveAttribute) -from .service import (split_principal, validate_certificate, - set_certificate_attrs, ticket_flags_params, update_krbticketflags, - set_kerberos_attrs, rename_ipaallowedtoperform_from_ldap, - rename_ipaallowedtoperform_to_ldap, revoke_certs) -from .dns import (dns_container_exists, - add_records_for_host_validation, add_records_for_host, - get_reverse_zone) -from ipalib import _, ngettext -from ipalib import x509 -from ipalib import output -from ipalib.request import context -from ipalib.util import (normalize_sshpubkey, validate_sshpubkey_no_options, - convert_sshpubkey_post, validate_hostname, - add_sshpubkey_to_attrs_pre, - remove_sshpubkey_from_output_post, - remove_sshpubkey_from_output_list_post) -from ipapython.ipautil import ipa_generate_password, CheckedIPAddress -from ipapython.dnsutil import DNSName -from ipapython.ssh import SSHPublicKey -from ipapython.dn import DN -from functools import reduce - -if six.PY3: - unicode = str - -__doc__ = _(""" -Hosts/Machines - -A host represents a machine. It can be used in a number of contexts: -- service entries are associated with a host -- a host stores the host/ service principal -- a host can be used in Host-based Access Control (HBAC) rules -- every enrolled client generates a host entry -""") + _(""" -ENROLLMENT: - -There are three enrollment scenarios when enrolling a new client: - -1. You are enrolling as a full administrator. The host entry may exist - or not. A full administrator is a member of the hostadmin role - or the admins group. -2. You are enrolling as a limited administrator. The host must already - exist. A limited administrator is a member a role with the - Host Enrollment privilege. -3. The host has been created with a one-time password. -""") + _(""" -RE-ENROLLMENT: - -Host that has been enrolled at some point, and lost its configuration (e.g. VM -destroyed) can be re-enrolled. - -For more information, consult the manual pages for ipa-client-install. - -A host can optionally store information such as where it is located, -the OS that it runs, etc. -""") + _(""" -EXAMPLES: -""") + _(""" - Add a new host: - ipa host-add --location="3rd floor lab" --locality=Dallas test.example.com -""") + _(""" - Delete a host: - ipa host-del test.example.com -""") + _(""" - Add a new host with a one-time password: - ipa host-add --os='Fedora 12' --password=Secret123 test.example.com -""") + _(""" - Add a new host with a random one-time password: - ipa host-add --os='Fedora 12' --random test.example.com -""") + _(""" - Modify information about a host: - ipa host-mod --os='Fedora 12' test.example.com -""") + _(""" - Remove SSH public keys of a host and update DNS to reflect this change: - ipa host-mod --sshpubkey= --updatedns test.example.com -""") + _(""" - Disable the host Kerberos key, SSL certificate and all of its services: - ipa host-disable test.example.com -""") + _(""" - Add a host that can manage this host's keytab and certificate: - ipa host-add-managedby --hosts=test2 test -""") + _(""" - Allow user to create a keytab: - ipa host-allow-create-keytab test2 --users=tuser1 -""") - -register = Registry() - -# Characters to be used by random password generator -# The set was chosen to avoid the need for escaping the characters by user -host_pwd_chars = string.digits + string.ascii_letters + '_,.@+-=' - - -def remove_ptr_rec(ipaddr, host, domain): - """ - Remove PTR record of IP address (ipaddr) - :return: True if PTR record was removed, False if record was not found - """ - api.log.debug('deleting PTR record of ipaddr %s', ipaddr) - try: - revzone, revname = get_reverse_zone(ipaddr) - - # in case domain is in FQDN form with a trailing dot, we needn't add - # another one, in case it has no trailing dot, dnsrecord-del will - # normalize the entry - delkw = {'ptrrecord': "%s.%s" % (host, domain)} - - api.Command['dnsrecord_del'](revzone, revname, **delkw) - except errors.NotFound: - api.log.debug('PTR record of ipaddr %s not found', ipaddr) - return False - - return True - - -def update_sshfp_record(zone, record, entry_attrs): - if 'ipasshpubkey' not in entry_attrs: - return - - pubkeys = entry_attrs['ipasshpubkey'] or () - sshfps = [] - for pubkey in pubkeys: - try: - sshfp = SSHPublicKey(pubkey).fingerprint_dns_sha1() - except (ValueError, UnicodeDecodeError): - continue - if sshfp is not None: - sshfps.append(sshfp) - - try: - sshfp = SSHPublicKey(pubkey).fingerprint_dns_sha256() - except (ValueError, UnicodeDecodeError): - continue - if sshfp is not None: - sshfps.append(sshfp) - - try: - api.Command['dnsrecord_mod'](zone, record, sshfprecord=sshfps) - except errors.EmptyModlist: - pass - - -def convert_ipaassignedidview_post(entry_attrs, options): - """ - Converts the ID View DN to its name for the better looking output. - """ - - if 'ipaassignedidview' in entry_attrs and not options.get('raw'): - idview_name = entry_attrs.single_value['ipaassignedidview'][0].value - entry_attrs.single_value['ipaassignedidview'] = idview_name - - -host_output_params = ( - Flag('has_keytab', - label=_('Keytab'), - ), - Str('managedby_host', - label='Managed by', - ), - Str('managing_host', - label='Managing', - ), - Str('subject', - label=_('Subject'), - ), - Str('serial_number', - label=_('Serial Number'), - ), - Str('serial_number_hex', - label=_('Serial Number (hex)'), - ), - Str('issuer', - label=_('Issuer'), - ), - Str('valid_not_before', - label=_('Not Before'), - ), - Str('valid_not_after', - label=_('Not After'), - ), - Str('md5_fingerprint', - label=_('Fingerprint (MD5)'), - ), - Str('sha1_fingerprint', - label=_('Fingerprint (SHA1)'), - ), - Str('revocation_reason?', - label=_('Revocation reason'), - ), - Str('managedby', - label=_('Failed managedby'), - ), - Str('sshpubkeyfp*', - label=_('SSH public key fingerprint'), - ), - Str('ipaallowedtoperform_read_keys_user', - label=_('Users allowed to retrieve keytab'), - ), - Str('ipaallowedtoperform_read_keys_group', - label=_('Groups allowed to retrieve keytab'), - ), - Str('ipaallowedtoperform_read_keys_host', - label=_('Hosts allowed to retrieve keytab'), - ), - Str('ipaallowedtoperform_read_keys_hostgroup', - label=_('Host Groups allowed to retrieve keytab'), - ), - Str('ipaallowedtoperform_write_keys_user', - label=_('Users allowed to create keytab'), - ), - Str('ipaallowedtoperform_write_keys_group', - label=_('Groups allowed to create keytab'), - ), - Str('ipaallowedtoperform_write_keys_host', - label=_('Hosts allowed to create keytab'), - ), - Str('ipaallowedtoperform_write_keys_hostgroup', - label=_('Host Groups allowed to create keytab'), - ), - Str('ipaallowedtoperform_read_keys', - label=_('Failed allowed to retrieve keytab'), - ), - Str('ipaallowedtoperform_write_keys', - label=_('Failed allowed to create keytab'), - ), -) - - -def validate_ipaddr(ugettext, ipaddr): - """ - Verify that we have either an IPv4 or IPv6 address. - """ - try: - CheckedIPAddress(ipaddr, match_local=False) - except Exception as e: - return unicode(e) - return None - - -def normalize_hostname(hostname): - """Use common fqdn form without the trailing dot""" - if hostname.endswith(u'.'): - hostname = hostname[:-1] - hostname = hostname.lower() - return hostname - - -def _hostname_validator(ugettext, value): - try: - validate_hostname(value) - except ValueError as e: - return _('invalid domain-name: %s') % unicode(e) - - return None - - -@register() -class host(LDAPObject): - """ - Host object. - """ - container_dn = api.env.container_host - object_name = _('host') - object_name_plural = _('hosts') - object_class = ['ipaobject', 'nshost', 'ipahost', 'pkiuser', 'ipaservice'] - possible_objectclasses = ['ipaallowedoperations'] - permission_filter_objectclasses = ['ipahost'] - # object_class_config = 'ipahostobjectclasses' - search_attributes = [ - 'fqdn', 'description', 'l', 'nshostlocation', 'krbprincipalname', - 'nshardwareplatform', 'nsosversion', 'managedby', - ] - default_attributes = [ - 'fqdn', 'description', 'l', 'nshostlocation', 'krbprincipalname', - 'nshardwareplatform', 'nsosversion', 'usercertificate', 'memberof', - 'managedby', 'memberofindirect', 'macaddress', - 'userclass', 'ipaallowedtoperform', 'ipaassignedidview', - ] - uuid_attribute = 'ipauniqueid' - attribute_members = { - 'enrolledby': ['user'], - 'memberof': ['hostgroup', 'netgroup', 'role', 'hbacrule', 'sudorule'], - 'managedby': ['host'], - 'managing': ['host'], - 'memberofindirect': ['hostgroup', 'netgroup', 'role', 'hbacrule', - 'sudorule'], - 'ipaallowedtoperform_read_keys': ['user', 'group', 'host', 'hostgroup'], - 'ipaallowedtoperform_write_keys': ['user', 'group', 'host', 'hostgroup'], - } - bindable = True - relationships = { - 'memberof': ('Member Of', 'in_', 'not_in_'), - 'enrolledby': ('Enrolled by', 'enroll_by_', 'not_enroll_by_'), - 'managedby': ('Managed by', 'man_by_', 'not_man_by_'), - 'managing': ('Managing', 'man_', 'not_man_'), - 'ipaallowedtoperform_read_keys': ('Allow to retrieve keytab by', 'retrieve_keytab_by_', 'not_retrieve_keytab_by_'), - 'ipaallowedtoperform_write_keys': ('Allow to create keytab by', 'write_keytab_by_', 'not_write_keytab_by'), - } - password_attributes = [('userpassword', 'has_password'), - ('krbprincipalkey', 'has_keytab')] - managed_permissions = { - 'System: Read Hosts': { - 'replaces_global_anonymous_aci': True, - 'ipapermbindruletype': 'all', - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'cn', 'description', 'fqdn', 'ipaclientversion', - 'ipakrbauthzdata', 'ipasshpubkey', 'ipauniqueid', - 'krbprincipalname', 'l', 'macaddress', 'nshardwareplatform', - 'nshostlocation', 'nsosversion', 'objectclass', - 'serverhostname', 'usercertificate', 'userclass', - 'enrolledby', 'managedby', 'ipaassignedidview', - 'krbprincipalname', 'krbcanonicalname', 'krbprincipalaliases', - 'krbprincipalexpiration', 'krbpasswordexpiration', - 'krblastpwdchange', - }, - }, - 'System: Read Host Membership': { - 'replaces_global_anonymous_aci': True, - 'ipapermbindruletype': 'all', - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'memberof', - }, - }, - 'System: Add Hosts': { - 'ipapermright': {'add'}, - 'replaces': [ - '(target = "ldap:///fqdn=*,cn=computers,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Add Hosts";allow (add) groupdn = "ldap:///cn=Add Hosts,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Host Administrators'}, - }, - 'System: Add krbPrincipalName to a Host': { - # Allow an admin to enroll a host that has a one-time password. - # When a host is created with a password no krbPrincipalName is set. - # This will let it be added if the client ends up enrolling with - # an administrator instead. - 'ipapermright': {'write'}, - 'ipapermtargetfilter': [ - '(objectclass=ipahost)', - '(!(krbprincipalname=*))', - ], - 'ipapermdefaultattr': {'krbprincipalname'}, - 'replaces': [ - '(target = "ldap:///fqdn=*,cn=computers,cn=accounts,$SUFFIX")(targetfilter = "(!(krbprincipalname=*))")(targetattr = "krbprincipalname")(version 3.0;acl "permission:Add krbPrincipalName to a host"; allow (write) groupdn = "ldap:///cn=Add krbPrincipalName to a host,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Host Administrators', 'Host Enrollment'}, - }, - 'System: Enroll a Host': { - 'ipapermright': {'write'}, - 'ipapermdefaultattr': {'objectclass', 'enrolledby'}, - 'replaces': [ - '(targetattr = "objectclass")(target = "ldap:///fqdn=*,cn=computers,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Enroll a host";allow (write) groupdn = "ldap:///cn=Enroll a host,cn=permissions,cn=pbac,$SUFFIX";)', - '(targetattr = "enrolledby || objectclass")(target = "ldap:///fqdn=*,cn=computers,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Enroll a host";allow (write) groupdn = "ldap:///cn=Enroll a host,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Host Administrators', 'Host Enrollment'}, - }, - 'System: Manage Host SSH Public Keys': { - 'ipapermright': {'write'}, - 'ipapermdefaultattr': {'ipasshpubkey'}, - 'replaces': [ - '(targetattr = "ipasshpubkey")(target = "ldap:///fqdn=*,cn=computers,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Manage Host SSH Public Keys";allow (write) groupdn = "ldap:///cn=Manage Host SSH Public Keys,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Host Administrators'}, - }, - 'System: Manage Host Keytab': { - 'ipapermright': {'write'}, - 'ipapermtargetfilter': [ - '(objectclass=ipahost)', - '(!(memberOf=%s))' % DN('cn=ipaservers', - api.env.container_hostgroup, - api.env.basedn), - ], - 'ipapermdefaultattr': {'krblastpwdchange', 'krbprincipalkey'}, - 'replaces': [ - '(targetattr = "krbprincipalkey || krblastpwdchange")(target = "ldap:///fqdn=*,cn=computers,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Manage host keytab";allow (write) groupdn = "ldap:///cn=Manage host keytab,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Host Administrators', 'Host Enrollment'}, - }, - 'System: Manage Host Keytab Permissions': { - 'ipapermright': {'read', 'search', 'compare', 'write'}, - 'ipapermdefaultattr': { - 'ipaallowedtoperform;write_keys', - 'ipaallowedtoperform;read_keys', 'objectclass' - }, - 'default_privileges': {'Host Administrators'}, - }, - 'System: Modify Hosts': { - 'ipapermright': {'write'}, - 'ipapermdefaultattr': { - 'description', 'l', 'nshardwareplatform', 'nshostlocation', - 'nsosversion', 'macaddress', 'userclass', 'ipaassignedidview', - }, - 'replaces': [ - '(targetattr = "description || l || nshostlocation || nshardwareplatform || nsosversion")(target = "ldap:///fqdn=*,cn=computers,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Modify Hosts";allow (write) groupdn = "ldap:///cn=Modify Hosts,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Host Administrators'}, - }, - 'System: Remove Hosts': { - 'ipapermright': {'delete'}, - 'replaces': [ - '(target = "ldap:///fqdn=*,cn=computers,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Remove Hosts";allow (delete) groupdn = "ldap:///cn=Remove Hosts,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Host Administrators'}, - }, - 'System: Manage Host Certificates': { - 'ipapermbindruletype': 'permission', - 'ipapermright': {'write'}, - 'ipapermdefaultattr': {'usercertificate'}, - 'default_privileges': {'Host Administrators', 'Host Enrollment'}, - }, - 'System: Manage Host Enrollment Password': { - 'ipapermbindruletype': 'permission', - 'ipapermright': {'write'}, - 'ipapermdefaultattr': {'userpassword'}, - 'default_privileges': {'Host Administrators', 'Host Enrollment'}, - }, - 'System: Read Host Compat Tree': { - 'non_object': True, - 'ipapermbindruletype': 'anonymous', - 'ipapermlocation': api.env.basedn, - 'ipapermtarget': DN('cn=computers', 'cn=compat', api.env.basedn), - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'objectclass', 'cn', 'macaddress', - }, - }, - } - - label = _('Hosts') - label_singular = _('Host') - - takes_params = ( - Str('fqdn', _hostname_validator, - cli_name='hostname', - label=_('Host name'), - primary_key=True, - normalizer=normalize_hostname, - ), - Str('description?', - cli_name='desc', - label=_('Description'), - doc=_('A description of this host'), - ), - Str('l?', - cli_name='locality', - label=_('Locality'), - doc=_('Host locality (e.g. "Baltimore, MD")'), - ), - Str('nshostlocation?', - cli_name='location', - label=_('Location'), - doc=_('Host location (e.g. "Lab 2")'), - ), - Str('nshardwareplatform?', - cli_name='platform', - label=_('Platform'), - doc=_('Host hardware platform (e.g. "Lenovo T61")'), - ), - Str('nsosversion?', - cli_name='os', - label=_('Operating system'), - doc=_('Host operating system and version (e.g. "Fedora 9")'), - ), - Str('userpassword?', - cli_name='password', - label=_('User password'), - doc=_('Password used in bulk enrollment'), - ), - Flag('random?', - doc=_('Generate a random password to be used in bulk enrollment'), - flags=('no_search', 'virtual_attribute'), - default=False, - ), - Str('randompassword?', - label=_('Random password'), - flags=('no_create', 'no_update', 'no_search', 'virtual_attribute'), - ), - Bytes('usercertificate*', validate_certificate, - cli_name='certificate', - label=_('Certificate'), - doc=_('Base-64 encoded host certificate'), - ), - Str('krbprincipalname?', - label=_('Principal name'), - flags=['no_create', 'no_update', 'no_search'], - ), - Str('macaddress*', - normalizer=lambda value: value.upper(), - pattern='^([a-fA-F0-9]{2}[:|\-]?){5}[a-fA-F0-9]{2}$', - pattern_errmsg=('Must be of the form HH:HH:HH:HH:HH:HH, where ' - 'each H is a hexadecimal character.'), - label=_('MAC address'), - doc=_('Hardware MAC address(es) on this host'), - ), - Str('ipasshpubkey*', validate_sshpubkey_no_options, - cli_name='sshpubkey', - label=_('SSH public key'), - normalizer=normalize_sshpubkey, - flags=['no_search'], - ), - Str('userclass*', - cli_name='class', - label=_('Class'), - doc=_('Host category (semantics placed on this attribute are for ' - 'local interpretation)'), - ), - Str('ipaassignedidview?', - label=_('Assigned ID View'), - flags=['no_option'], - ), - ) + ticket_flags_params - - def get_dn(self, *keys, **options): - hostname = keys[-1] - dn = super(host, self).get_dn(hostname, **options) - try: - self.backend.get_entry(dn, ['']) - except errors.NotFound: - try: - entry_attrs = self.backend.find_entry_by_attr( - 'serverhostname', hostname, self.object_class, [''], - DN(self.container_dn, api.env.basedn)) - dn = entry_attrs.dn - except errors.NotFound: - pass - return dn - - def get_managed_hosts(self, dn): - host_filter = 'managedBy=%s' % dn - host_attrs = ['fqdn'] - ldap = self.api.Backend.ldap2 - managed_hosts = [] - - try: - (hosts, truncated) = ldap.find_entries( - base_dn=DN(self.container_dn, api.env.basedn), - filter=host_filter, attrs_list=host_attrs) - - for host in hosts: - managed_hosts.append(host.dn) - except errors.NotFound: - return [] - - return managed_hosts - - def suppress_netgroup_memberof(self, ldap, entry_attrs): - """ - We don't want to show managed netgroups so remove them from the - memberofindirect list. - """ - ng_container = DN(api.env.container_netgroup, api.env.basedn) - for member in list(entry_attrs.get('memberofindirect', [])): - memberdn = DN(member) - if not memberdn.endswith(ng_container): - continue - - filter = ldap.make_filter({'objectclass': 'mepmanagedentry'}) - try: - ldap.get_entries(memberdn, ldap.SCOPE_BASE, filter, ['']) - except errors.NotFound: - pass - else: - entry_attrs['memberofindirect'].remove(member) - - -@register() -class host_add(LDAPCreate): - __doc__ = _('Add a new host.') - - has_output_params = LDAPCreate.has_output_params + host_output_params - msg_summary = _('Added host "%(value)s"') - member_attributes = ['managedby'] - takes_options = LDAPCreate.takes_options + ( - Flag('force', - label=_('Force'), - doc=_('force host name even if not in DNS'), - ), - Flag('no_reverse', - doc=_('skip reverse DNS detection'), - ), - Str('ip_address?', validate_ipaddr, - doc=_('Add the host to DNS with this IP address'), - label=_('IP Address'), - ), - ) - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - if options.get('ip_address') and dns_container_exists(ldap): - parts = keys[-1].split('.') - host = parts[0] - domain = unicode('.'.join(parts[1:])) - check_reverse = not options.get('no_reverse', False) - add_records_for_host_validation('ip_address', - DNSName(host), - DNSName(domain).make_absolute(), - options['ip_address'], - check_forward=True, - check_reverse=check_reverse) - if not options.get('force', False) and not 'ip_address' in options: - util.verify_host_resolvable(keys[-1]) - if 'locality' in entry_attrs: - entry_attrs['l'] = entry_attrs['locality'] - entry_attrs['cn'] = keys[-1] - entry_attrs['serverhostname'] = keys[-1].split('.', 1)[0] - if not entry_attrs.get('userpassword', False) and not options.get('random', False): - entry_attrs['krbprincipalname'] = 'host/%s@%s' % ( - keys[-1], self.api.env.realm - ) - if 'krbprincipalaux' not in entry_attrs['objectclass']: - entry_attrs['objectclass'].append('krbprincipalaux') - if 'krbprincipal' not in entry_attrs['objectclass']: - entry_attrs['objectclass'].append('krbprincipal') - else: - if 'krbprincipalaux' in entry_attrs['objectclass']: - entry_attrs['objectclass'].remove('krbprincipalaux') - if 'krbprincipal' in entry_attrs['objectclass']: - entry_attrs['objectclass'].remove('krbprincipal') - if options.get('random'): - entry_attrs['userpassword'] = ipa_generate_password(characters=host_pwd_chars) - # save the password so it can be displayed in post_callback - setattr(context, 'randompassword', entry_attrs['userpassword']) - certs = options.get('usercertificate', []) - certs_der = [x509.normalize_certificate(c) for c in certs] - for cert in certs_der: - x509.verify_cert_subject(ldap, keys[-1], cert) - entry_attrs['usercertificate'] = certs_der - entry_attrs['managedby'] = dn - entry_attrs['objectclass'].append('ieee802device') - entry_attrs['objectclass'].append('ipasshhost') - update_krbticketflags(ldap, entry_attrs, attrs_list, options, False) - if 'krbticketflags' in entry_attrs: - entry_attrs['objectclass'].append('krbticketpolicyaux') - return dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - exc = None - if dns_container_exists(ldap): - try: - parts = keys[-1].split('.') - host = parts[0] - domain = unicode('.'.join(parts[1:])) - - if options.get('ip_address'): - add_reverse = not options.get('no_reverse', False) - - add_records_for_host(DNSName(host), - DNSName(domain).make_absolute(), - options['ip_address'], - add_forward=True, - add_reverse=add_reverse) - del options['ip_address'] - - update_sshfp_record(domain, unicode(parts[0]), entry_attrs) - except Exception as e: - exc = e - if options.get('random', False): - try: - entry_attrs['randompassword'] = unicode(getattr(context, 'randompassword')) - except AttributeError: - # On the off-chance some other extension deletes this from the - # context, don't crash. - pass - if exc: - raise errors.NonFatalError( - reason=_('The host was added but the DNS update failed with: %(exc)s') % dict(exc=exc) - ) - set_certificate_attrs(entry_attrs) - set_kerberos_attrs(entry_attrs, options) - rename_ipaallowedtoperform_from_ldap(entry_attrs, options) - - if options.get('all', False): - entry_attrs['managing'] = self.obj.get_managed_hosts(dn) - self.obj.get_password_attributes(ldap, dn, entry_attrs) - if entry_attrs['has_password']: - # If an OTP is set there is no keytab, at least not one - # fetched anywhere. - entry_attrs['has_keytab'] = False - - convert_sshpubkey_post(entry_attrs) - - return dn - - -@register() -class host_del(LDAPDelete): - __doc__ = _('Delete a host.') - - msg_summary = _('Deleted host "%(value)s"') - member_attributes = ['managedby'] - - takes_options = LDAPDelete.takes_options + ( - Flag('updatedns?', - doc=_('Remove A, AAAA, SSHFP and PTR records of the host(s) ' - 'managed by IPA DNS'), - default=False, - ), - ) - - def pre_callback(self, ldap, dn, *keys, **options): - assert isinstance(dn, DN) - # If we aren't given a fqdn, find it - if _hostname_validator(None, keys[-1]) is not None: - hostentry = api.Command['host_show'](keys[-1])['result'] - fqdn = hostentry['fqdn'][0] - else: - fqdn = keys[-1] - host_is_master(ldap, fqdn) - # Remove all service records for this host - truncated = True - while truncated: - try: - ret = api.Command['service_find'](fqdn) - truncated = ret['truncated'] - services = ret['result'] - except errors.NotFound: - break - else: - for entry_attrs in services: - principal = entry_attrs['krbprincipalname'][0] - (service, hostname, realm) = split_principal(principal) - if hostname.lower() == fqdn: - api.Command['service_del'](principal) - updatedns = options.get('updatedns', False) - if updatedns: - try: - updatedns = dns_container_exists(ldap) - except errors.NotFound: - updatedns = False - - if updatedns: - # Remove A, AAAA, SSHFP and PTR records of the host - parts = fqdn.split('.') - domain = unicode('.'.join(parts[1:])) - # Get all resources for this host - rec_removed = False - try: - record = api.Command['dnsrecord_show']( - domain, parts[0])['result'] - except errors.NotFound: - pass - else: - # remove PTR records first - for attr in ('arecord', 'aaaarecord'): - for val in record.get(attr, []): - rec_removed = ( - remove_ptr_rec(val, parts[0], domain) or - rec_removed - ) - try: - # remove all A, AAAA, SSHFP records of the host - api.Command['dnsrecord_mod']( - domain, - record['idnsname'][0], - arecord=[], - aaaarecord=[], - sshfprecord=[] - ) - except errors.EmptyModlist: - pass - else: - rec_removed = True - - if not rec_removed: - self.add_message( - messages.FailedToRemoveHostDNSRecords( - host=fqdn, - reason=_("No A, AAAA, SSHFP or PTR records found.") - ) - ) - - if self.api.Command.ca_is_enabled()['result']: - try: - entry_attrs = ldap.get_entry(dn, ['usercertificate']) - except errors.NotFound: - self.obj.handle_not_found(*keys) - - revoke_certs(entry_attrs.get('usercertificate', []), self.log) - - return dn - - -@register() -class host_mod(LDAPUpdate): - __doc__ = _('Modify information about a host.') - - has_output_params = LDAPUpdate.has_output_params + host_output_params - msg_summary = _('Modified host "%(value)s"') - member_attributes = ['managedby'] - - takes_options = LDAPUpdate.takes_options + ( - Str('krbprincipalname?', - cli_name='principalname', - label=_('Principal name'), - doc=_('Kerberos principal name for this host'), - attribute=True, - ), - Flag('updatedns?', - doc=_('Update DNS entries'), - default=False, - ), - ) - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - # Allow an existing OTP to be reset but don't allow a OTP to be - # added to an enrolled host. - if options.get('userpassword') or options.get('random'): - entry = {} - self.obj.get_password_attributes(ldap, dn, entry) - if not entry['has_password'] and entry['has_keytab']: - raise errors.ValidationError( - name='password', - error=_('Password cannot be set on enrolled host.')) - - # Once a principal name is set it cannot be changed - if 'cn' in entry_attrs: - raise errors.ACIError(info=_('cn is immutable')) - if 'locality' in entry_attrs: - entry_attrs['l'] = entry_attrs['locality'] - if 'krbprincipalname' in entry_attrs: - entry_attrs_old = ldap.get_entry( - dn, ['objectclass', 'krbprincipalname'] - ) - if 'krbprincipalname' in entry_attrs_old: - msg = 'Principal name already set, it is unchangeable.' - raise errors.ACIError(info=msg) - obj_classes = entry_attrs_old['objectclass'] - if 'krbprincipalaux' not in obj_classes: - obj_classes.append('krbprincipalaux') - entry_attrs['objectclass'] = obj_classes - - # verify certificates - certs = entry_attrs.get('usercertificate') or [] - certs_der = [x509.normalize_certificate(c) for c in certs] - for cert in certs_der: - x509.verify_cert_subject(ldap, keys[-1], cert) - - # revoke removed certificates - if certs and self.api.Command.ca_is_enabled()['result']: - try: - entry_attrs_old = ldap.get_entry(dn, ['usercertificate']) - except errors.NotFound: - self.obj.handle_not_found(*keys) - old_certs = entry_attrs_old.get('usercertificate', []) - old_certs_der = [x509.normalize_certificate(c) for c in old_certs] - removed_certs_der = set(old_certs_der) - set(certs_der) - revoke_certs(removed_certs_der, self.log) - - if certs: - entry_attrs['usercertificate'] = certs_der - - if options.get('random'): - entry_attrs['userpassword'] = ipa_generate_password(characters=host_pwd_chars) - setattr(context, 'randompassword', entry_attrs['userpassword']) - - if 'macaddress' in entry_attrs: - if 'objectclass' in entry_attrs: - obj_classes = entry_attrs['objectclass'] - else: - _entry_attrs = ldap.get_entry(dn, ['objectclass']) - obj_classes = _entry_attrs['objectclass'] - if 'ieee802device' not in obj_classes: - obj_classes.append('ieee802device') - entry_attrs['objectclass'] = obj_classes - - if options.get('updatedns', False) and dns_container_exists(ldap): - parts = keys[-1].split('.') - domain = unicode('.'.join(parts[1:])) - try: - result = api.Command['dnszone_show'](domain)['result'] - domain = result['idnsname'][0] - except errors.NotFound: - self.obj.handle_not_found(*keys) - update_sshfp_record(domain, unicode(parts[0]), entry_attrs) - - if 'ipasshpubkey' in entry_attrs: - if 'objectclass' in entry_attrs: - obj_classes = entry_attrs['objectclass'] - else: - _entry_attrs = ldap.get_entry(dn, ['objectclass']) - obj_classes = entry_attrs['objectclass'] = _entry_attrs['objectclass'] - if 'ipasshhost' not in obj_classes: - obj_classes.append('ipasshhost') - - update_krbticketflags(ldap, entry_attrs, attrs_list, options, True) - - if 'krbticketflags' in entry_attrs: - if 'objectclass' not in entry_attrs: - entry_attrs_old = ldap.get_entry(dn, ['objectclass']) - entry_attrs['objectclass'] = entry_attrs_old['objectclass'] - if 'krbticketpolicyaux' not in entry_attrs['objectclass']: - entry_attrs['objectclass'].append('krbticketpolicyaux') - - add_sshpubkey_to_attrs_pre(self.context, attrs_list) - - return dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - if options.get('random', False): - entry_attrs['randompassword'] = unicode(getattr(context, 'randompassword')) - set_certificate_attrs(entry_attrs) - set_kerberos_attrs(entry_attrs, options) - rename_ipaallowedtoperform_from_ldap(entry_attrs, options) - self.obj.get_password_attributes(ldap, dn, entry_attrs) - if entry_attrs['has_password']: - # If an OTP is set there is no keytab, at least not one - # fetched anywhere. - entry_attrs['has_keytab'] = False - - if options.get('all', False): - entry_attrs['managing'] = self.obj.get_managed_hosts(dn) - - self.obj.suppress_netgroup_memberof(ldap, entry_attrs) - - convert_sshpubkey_post(entry_attrs) - remove_sshpubkey_from_output_post(self.context, entry_attrs) - convert_ipaassignedidview_post(entry_attrs, options) - - return dn - - -@register() -class host_find(LDAPSearch): - __doc__ = _('Search for hosts.') - - has_output_params = LDAPSearch.has_output_params + host_output_params - msg_summary = ngettext( - '%(count)d host matched', '%(count)d hosts matched', 0 - ) - member_attributes = ['memberof', 'enrolledby', 'managedby'] - - def get_options(self): - for option in super(host_find, self).get_options(): - yield option - # "managing" membership has to be added and processed separately - for option in self.get_member_options('managing'): - yield option - - def pre_callback(self, ldap, filter, attrs_list, base_dn, scope, *args, **options): - assert isinstance(base_dn, DN) - if 'locality' in attrs_list: - attrs_list.remove('locality') - attrs_list.append('l') - if 'man_host' in options or 'not_man_host' in options: - hosts = [] - if options.get('man_host') is not None: - for pkey in options.get('man_host', []): - dn = self.obj.get_dn(pkey) - try: - entry_attrs = ldap.get_entry(dn, ['managedby']) - except errors.NotFound: - self.obj.handle_not_found(pkey) - hosts.append(set(entry_attrs.get('managedby', ''))) - hosts = list(reduce(lambda s1, s2: s1 & s2, hosts)) - - if not hosts: - # There is no host managing _all_ hosts in --man-hosts - filter = ldap.combine_filters( - (filter, '(objectclass=disabled)'), ldap.MATCH_ALL - ) - - not_hosts = [] - if options.get('not_man_host') is not None: - for pkey in options.get('not_man_host', []): - dn = self.obj.get_dn(pkey) - try: - entry_attrs = ldap.get_entry(dn, ['managedby']) - except errors.NotFound: - self.obj.handle_not_found(pkey) - not_hosts += entry_attrs.get('managedby', []) - not_hosts = list(set(not_hosts)) - - for target_hosts, filter_op in ((hosts, ldap.MATCH_ANY), - (not_hosts, ldap.MATCH_NONE)): - hosts_avas = [DN(host)[0][0] for host in target_hosts] - hosts_filters = [ldap.make_filter_from_attr(ava.attr, ava.value) - for ava in hosts_avas] - hosts_filter = ldap.combine_filters(hosts_filters, filter_op) - - filter = ldap.combine_filters( - (filter, hosts_filter), ldap.MATCH_ALL - ) - - add_sshpubkey_to_attrs_pre(self.context, attrs_list) - - return (filter.replace('locality', 'l'), base_dn, scope) - - def post_callback(self, ldap, entries, truncated, *args, **options): - if options.get('pkey_only', False): - return truncated - for entry_attrs in entries: - set_certificate_attrs(entry_attrs) - set_kerberos_attrs(entry_attrs, options) - rename_ipaallowedtoperform_from_ldap(entry_attrs, options) - self.obj.suppress_netgroup_memberof(ldap, entry_attrs) - - if options.get('all', False): - entry_attrs['managing'] = self.obj.get_managed_hosts(entry_attrs.dn) - - convert_sshpubkey_post(entry_attrs) - remove_sshpubkey_from_output_post(self.context, entry_attrs) - convert_ipaassignedidview_post(entry_attrs, options) - - remove_sshpubkey_from_output_list_post(self.context, entries) - - return truncated - - -@register() -class host_show(LDAPRetrieve): - __doc__ = _('Display information about a host.') - - has_output_params = LDAPRetrieve.has_output_params + host_output_params - takes_options = LDAPRetrieve.takes_options + ( - Str('out?', - doc=_('file to store certificate in'), - ), - ) - - member_attributes = ['managedby'] - - def pre_callback(self, ldap, dn, attrs_list, *keys, **options): - assert isinstance(dn, DN) - add_sshpubkey_to_attrs_pre(self.context, attrs_list) - return dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - self.obj.get_password_attributes(ldap, dn, entry_attrs) - if entry_attrs['has_password']: - # If an OTP is set there is no keytab, at least not one - # fetched anywhere. - entry_attrs['has_keytab'] = False - - set_certificate_attrs(entry_attrs) - set_kerberos_attrs(entry_attrs, options) - rename_ipaallowedtoperform_from_ldap(entry_attrs, options) - - if options.get('all', False): - entry_attrs['managing'] = self.obj.get_managed_hosts(dn) - - self.obj.suppress_netgroup_memberof(ldap, entry_attrs) - - convert_sshpubkey_post(entry_attrs) - remove_sshpubkey_from_output_post(self.context, entry_attrs) - convert_ipaassignedidview_post(entry_attrs, options) - - return dn - - -@register() -class host_disable(LDAPQuery): - __doc__ = _('Disable the Kerberos key, SSL certificate and all services of a host.') - - has_output = output.standard_value - msg_summary = _('Disabled host "%(value)s"') - - def execute(self, *keys, **options): - ldap = self.obj.backend - - # If we aren't given a fqdn, find it - if _hostname_validator(None, keys[-1]) is not None: - hostentry = api.Command['host_show'](keys[-1])['result'] - fqdn = hostentry['fqdn'][0] - else: - fqdn = keys[-1] - - host_is_master(ldap, fqdn) - - # See if we actually do anthing here, and if not raise an exception - done_work = False - - truncated = True - while truncated: - try: - ret = api.Command['service_find'](fqdn) - truncated = ret['truncated'] - services = ret['result'] - except errors.NotFound: - break - else: - for entry_attrs in services: - principal = entry_attrs['krbprincipalname'][0] - (service, hostname, realm) = split_principal(principal) - if hostname.lower() == fqdn: - try: - api.Command['service_disable'](principal) - done_work = True - except errors.AlreadyInactive: - pass - - dn = self.obj.get_dn(*keys, **options) - try: - entry_attrs = ldap.get_entry(dn, ['usercertificate']) - except errors.NotFound: - self.obj.handle_not_found(*keys) - if self.api.Command.ca_is_enabled()['result']: - certs = entry_attrs.get('usercertificate', []) - - if certs: - revoke_certs(certs, self.log) - # Remove the usercertificate altogether - entry_attrs['usercertificate'] = None - ldap.update_entry(entry_attrs) - done_work = True - - self.obj.get_password_attributes(ldap, dn, entry_attrs) - if entry_attrs['has_keytab']: - ldap.remove_principal_key(dn) - done_work = True - - if not done_work: - raise errors.AlreadyInactive() - - return dict( - result=True, - value=pkey_to_value(keys[0], options), - ) - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - self.obj.suppress_netgroup_memberof(ldap, entry_attrs) - return dn - - -@register() -class host_add_managedby(LDAPAddMember): - __doc__ = _('Add hosts that can manage this host.') - - member_attributes = ['managedby'] - has_output_params = LDAPAddMember.has_output_params + host_output_params - allow_same = True - - def post_callback(self, ldap, completed, failed, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - self.obj.suppress_netgroup_memberof(ldap, entry_attrs) - return (completed, dn) - - -@register() -class host_remove_managedby(LDAPRemoveMember): - __doc__ = _('Remove hosts that can manage this host.') - - member_attributes = ['managedby'] - has_output_params = LDAPRemoveMember.has_output_params + host_output_params - - def post_callback(self, ldap, completed, failed, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - self.obj.suppress_netgroup_memberof(ldap, entry_attrs) - return (completed, dn) - - -@register() -class host_allow_retrieve_keytab(LDAPAddMember): - __doc__ = _('Allow users, groups, hosts or host groups to retrieve a keytab' - ' of this host.') - member_attributes = ['ipaallowedtoperform_read_keys'] - has_output_params = LDAPAddMember.has_output_params + host_output_params - - def pre_callback(self, ldap, dn, found, not_found, *keys, **options): - rename_ipaallowedtoperform_to_ldap(found) - rename_ipaallowedtoperform_to_ldap(not_found) - add_missing_object_class(ldap, u'ipaallowedoperations', dn) - return dn - - def post_callback(self, ldap, completed, failed, dn, entry_attrs, *keys, **options): - rename_ipaallowedtoperform_from_ldap(entry_attrs, options) - rename_ipaallowedtoperform_from_ldap(failed, options) - return (completed, dn) - - -@register() -class host_disallow_retrieve_keytab(LDAPRemoveMember): - __doc__ = _('Disallow users, groups, hosts or host groups to retrieve a ' - 'keytab of this host.') - member_attributes = ['ipaallowedtoperform_read_keys'] - has_output_params = LDAPRemoveMember.has_output_params + host_output_params - - def pre_callback(self, ldap, dn, found, not_found, *keys, **options): - rename_ipaallowedtoperform_to_ldap(found) - rename_ipaallowedtoperform_to_ldap(not_found) - return dn - - def post_callback(self, ldap, completed, failed, dn, entry_attrs, *keys, **options): - rename_ipaallowedtoperform_from_ldap(entry_attrs, options) - rename_ipaallowedtoperform_from_ldap(failed, options) - return (completed, dn) - - -@register() -class host_allow_create_keytab(LDAPAddMember): - __doc__ = _('Allow users, groups, hosts or host groups to create a keytab ' - 'of this host.') - member_attributes = ['ipaallowedtoperform_write_keys'] - has_output_params = LDAPAddMember.has_output_params + host_output_params - - def pre_callback(self, ldap, dn, found, not_found, *keys, **options): - rename_ipaallowedtoperform_to_ldap(found) - rename_ipaallowedtoperform_to_ldap(not_found) - add_missing_object_class(ldap, u'ipaallowedoperations', dn) - return dn - - def post_callback(self, ldap, completed, failed, dn, entry_attrs, *keys, **options): - rename_ipaallowedtoperform_from_ldap(entry_attrs, options) - rename_ipaallowedtoperform_from_ldap(failed, options) - return (completed, dn) - - -@register() -class host_disallow_create_keytab(LDAPRemoveMember): - __doc__ = _('Disallow users, groups, hosts or host groups to create a ' - 'keytab of this host.') - member_attributes = ['ipaallowedtoperform_write_keys'] - has_output_params = LDAPRemoveMember.has_output_params + host_output_params - - def pre_callback(self, ldap, dn, found, not_found, *keys, **options): - rename_ipaallowedtoperform_to_ldap(found) - rename_ipaallowedtoperform_to_ldap(not_found) - return dn - - def post_callback(self, ldap, completed, failed, dn, entry_attrs, *keys, **options): - rename_ipaallowedtoperform_from_ldap(entry_attrs, options) - rename_ipaallowedtoperform_from_ldap(failed, options) - return (completed, dn) - - -@register() -class host_add_cert(LDAPAddAttribute): - __doc__ = _('Add certificates to host entry') - msg_summary = _('Added certificates to host "%(value)s"') - attribute = 'usercertificate' - - -@register() -class host_remove_cert(LDAPRemoveAttribute): - __doc__ = _('Remove certificates from host entry') - msg_summary = _('Removed certificates from host "%(value)s"') - attribute = 'usercertificate' - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - - if 'usercertificate' in options: - revoke_certs(options['usercertificate'], self.log) - - return dn diff --git a/ipalib/plugins/hostgroup.py b/ipalib/plugins/hostgroup.py deleted file mode 100644 index dab354d9c..000000000 --- a/ipalib/plugins/hostgroup.py +++ /dev/null @@ -1,316 +0,0 @@ -# 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/>. - -import six - -from ipalib.plugable import Registry -from .baseldap import (LDAPObject, LDAPCreate, LDAPRetrieve, - LDAPDelete, LDAPUpdate, LDAPSearch, - LDAPAddMember, LDAPRemoveMember, - entry_from_entry, wait_for_value) -from ipalib import Str, api, _, ngettext, errors -from .netgroup import NETGROUP_PATTERN, NETGROUP_PATTERN_ERRMSG -from ipapython.dn import DN - -if six.PY3: - unicode = str - -__doc__ = _(""" -Groups of hosts. - -Manage groups of hosts. This is useful for applying access control to a -number of hosts by using Host-based Access Control. - -EXAMPLES: - - Add a new host group: - ipa hostgroup-add --desc="Baltimore hosts" baltimore - - Add another new host group: - ipa hostgroup-add --desc="Maryland hosts" maryland - - Add members to the hostgroup (using Bash brace expansion): - ipa hostgroup-add-member --hosts={box1,box2,box3} baltimore - - Add a hostgroup as a member of another hostgroup: - ipa hostgroup-add-member --hostgroups=baltimore maryland - - Remove a host from the hostgroup: - ipa hostgroup-remove-member --hosts=box2 baltimore - - Display a host group: - ipa hostgroup-show baltimore - - Delete a hostgroup: - ipa hostgroup-del baltimore -""") - - -def get_complete_hostgroup_member_list(hostgroup): - result = api.Command['hostgroup_show'](hostgroup)['result'] - direct = list(result.get('member_host', [])) - indirect = list(result.get('memberindirect_host', [])) - return direct + indirect - - -register = Registry() - -PROTECTED_HOSTGROUPS = (u'ipaservers',) - - -@register() -class hostgroup(LDAPObject): - """ - Hostgroup object. - """ - container_dn = api.env.container_hostgroup - object_name = _('host group') - object_name_plural = _('host groups') - object_class = ['ipaobject', 'ipahostgroup'] - permission_filter_objectclasses = ['ipahostgroup'] - search_attributes = ['cn', 'description', 'member', 'memberof'] - default_attributes = ['cn', 'description', 'member', 'memberof', - 'memberindirect', 'memberofindirect', - ] - uuid_attribute = 'ipauniqueid' - attribute_members = { - 'member': ['host', 'hostgroup'], - 'memberof': ['hostgroup', 'netgroup', 'hbacrule', 'sudorule'], - 'memberindirect': ['host', 'hostgroup'], - 'memberofindirect': ['hostgroup', 'hbacrule', 'sudorule'], - } - managed_permissions = { - 'System: Read Hostgroups': { - 'replaces_global_anonymous_aci': True, - 'ipapermbindruletype': 'all', - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'businesscategory', 'cn', 'description', 'ipauniqueid', 'o', - 'objectclass', 'ou', 'owner', 'seealso', - }, - }, - 'System: Read Hostgroup Membership': { - 'replaces_global_anonymous_aci': True, - 'ipapermbindruletype': 'all', - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'member', 'memberof', 'memberuser', 'memberhost', - }, - }, - 'System: Add Hostgroups': { - 'ipapermright': {'add'}, - 'replaces': [ - '(target = "ldap:///cn=*,cn=hostgroups,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Add Hostgroups";allow (add) groupdn = "ldap:///cn=Add Hostgroups,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Host Group Administrators'}, - }, - 'System: Modify Hostgroup Membership': { - 'ipapermright': {'write'}, - 'ipapermtargetfilter': [ - '(objectclass=ipahostgroup)', - '(!(cn=ipaservers))', - ], - 'ipapermdefaultattr': {'member'}, - 'replaces': [ - '(targetattr = "member")(target = "ldap:///cn=*,cn=hostgroups,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Modify Hostgroup membership";allow (write) groupdn = "ldap:///cn=Modify Hostgroup membership,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Host Group Administrators'}, - }, - 'System: Modify Hostgroups': { - 'ipapermright': {'write'}, - 'ipapermdefaultattr': {'cn', 'description'}, - 'replaces': [ - '(targetattr = "cn || description")(target = "ldap:///cn=*,cn=hostgroups,cn=accounts,$SUFFIX")(version 3.0; acl "permission:Modify Hostgroups";allow (write) groupdn = "ldap:///cn=Modify Hostgroups,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Host Group Administrators'}, - }, - 'System: Remove Hostgroups': { - 'ipapermright': {'delete'}, - 'replaces': [ - '(target = "ldap:///cn=*,cn=hostgroups,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Remove Hostgroups";allow (delete) groupdn = "ldap:///cn=Remove Hostgroups,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Host Group Administrators'}, - }, - } - - label = _('Host Groups') - label_singular = _('Host Group') - - takes_params = ( - Str('cn', - pattern=NETGROUP_PATTERN, - pattern_errmsg=NETGROUP_PATTERN_ERRMSG, - cli_name='hostgroup_name', - label=_('Host-group'), - doc=_('Name of host-group'), - primary_key=True, - normalizer=lambda value: value.lower(), - ), - Str('description?', - cli_name='desc', - label=_('Description'), - doc=_('A description of this host-group'), - ), - ) - - def suppress_netgroup_memberof(self, ldap, dn, entry_attrs): - """ - We don't want to show managed netgroups so remove them from the - memberOf list. - """ - hgdn = DN(dn) - for member in list(entry_attrs.get('memberof', [])): - ngdn = DN(member) - if ngdn['cn'] != hgdn['cn']: - continue - - filter = ldap.make_filter({'objectclass': 'mepmanagedentry'}) - try: - ldap.get_entries(ngdn, ldap.SCOPE_BASE, filter, ['']) - except errors.NotFound: - pass - else: - entry_attrs['memberof'].remove(member) - - -@register() -class hostgroup_add(LDAPCreate): - __doc__ = _('Add a new hostgroup.') - - msg_summary = _('Added hostgroup "%(value)s"') - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - try: - # check duplicity with hostgroups first to provide proper error - api.Object['hostgroup'].get_dn_if_exists(keys[-1]) - self.obj.handle_duplicate_entry(*keys) - except errors.NotFound: - pass - - try: - # when enabled, a managed netgroup is created for every hostgroup - # make sure that the netgroup can be created - api.Object['netgroup'].get_dn_if_exists(keys[-1]) - raise errors.DuplicateEntry(message=unicode(_( - u'netgroup with name "%s" already exists. ' - u'Hostgroups and netgroups share a common namespace' - ) % keys[-1])) - except errors.NotFound: - pass - - return dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - # Always wait for the associated netgroup to be created so we can - # be sure to ignore it in memberOf - newentry = wait_for_value(ldap, dn, 'objectclass', 'mepOriginEntry') - entry_from_entry(entry_attrs, newentry) - self.obj.suppress_netgroup_memberof(ldap, dn, entry_attrs) - - return dn - - -@register() -class hostgroup_del(LDAPDelete): - __doc__ = _('Delete a hostgroup.') - - msg_summary = _('Deleted hostgroup "%(value)s"') - - def pre_callback(self, ldap, dn, *keys, **options): - if keys[0] in PROTECTED_HOSTGROUPS: - raise errors.ProtectedEntryError(label=_(u'hostgroup'), - key=keys[0], - reason=_(u'privileged hostgroup')) - - return dn - - -@register() -class hostgroup_mod(LDAPUpdate): - __doc__ = _('Modify a hostgroup.') - - msg_summary = _('Modified hostgroup "%(value)s"') - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - self.obj.suppress_netgroup_memberof(ldap, dn, entry_attrs) - return dn - - -@register() -class hostgroup_find(LDAPSearch): - __doc__ = _('Search for hostgroups.') - - member_attributes = ['member', 'memberof'] - msg_summary = ngettext( - '%(count)d hostgroup matched', '%(count)d hostgroups matched', 0 - ) - - def post_callback(self, ldap, entries, truncated, *args, **options): - if options.get('pkey_only', False): - return truncated - for entry in entries: - self.obj.suppress_netgroup_memberof(ldap, entry.dn, entry) - return truncated - - -@register() -class hostgroup_show(LDAPRetrieve): - __doc__ = _('Display information about a hostgroup.') - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - self.obj.suppress_netgroup_memberof(ldap, dn, entry_attrs) - return dn - - -@register() -class hostgroup_add_member(LDAPAddMember): - __doc__ = _('Add members to a hostgroup.') - - def post_callback(self, ldap, completed, failed, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - self.obj.suppress_netgroup_memberof(ldap, dn, entry_attrs) - return (completed, dn) - - -@register() -class hostgroup_remove_member(LDAPRemoveMember): - __doc__ = _('Remove members from a hostgroup.') - - def pre_callback(self, ldap, dn, found, not_found, *keys, **options): - if keys[0] in PROTECTED_HOSTGROUPS and 'host' in options: - result = api.Command.hostgroup_show(keys[0]) - hosts_left = set(result['result'].get('member_host', [])) - hosts_deleted = set(options['host']) - if hosts_left.issubset(hosts_deleted): - raise errors.LastMemberError(key=sorted(hosts_deleted)[0], - label=_(u'hostgroup'), - container=keys[0]) - - return dn - - def post_callback(self, ldap, completed, failed, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - self.obj.suppress_netgroup_memberof(ldap, dn, entry_attrs) - return (completed, dn) - diff --git a/ipalib/plugins/idrange.py b/ipalib/plugins/idrange.py deleted file mode 100644 index ccd67995e..000000000 --- a/ipalib/plugins/idrange.py +++ /dev/null @@ -1,769 +0,0 @@ -# Authors: -# Sumit Bose <sbose@redhat.com> -# -# Copyright (C) 2012 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/>. - -import six - -from ipalib.plugable import Registry -from .baseldap import (LDAPObject, LDAPCreate, LDAPDelete, - LDAPRetrieve, LDAPSearch, LDAPUpdate) -from ipalib import api, Int, Str, StrEnum, _, ngettext -from ipalib import errors -from ipapython.dn import DN - -if six.PY3: - unicode = str - -if api.env.in_server and api.env.context in ['lite', 'server']: - try: - import ipaserver.dcerpc - _dcerpc_bindings_installed = True - except ImportError: - _dcerpc_bindings_installed = False - -ID_RANGE_VS_DNA_WARNING = """======= -WARNING: - -DNA plugin in 389-ds will allocate IDs based on the ranges configured for the -local domain. Currently the DNA plugin *cannot* be reconfigured itself based -on the local ranges set via this family of commands. - -Manual configuration change has to be done in the DNA plugin configuration for -the new local range. Specifically, The dnaNextRange attribute of 'cn=Posix -IDs,cn=Distributed Numeric Assignment Plugin,cn=plugins,cn=config' has to be -modified to match the new range. -======= -""" - -__doc__ = _(""" -ID ranges - -Manage ID ranges used to map Posix IDs to SIDs and back. - -There are two type of ID ranges which are both handled by this utility: - - - the ID ranges of the local domain - - the ID ranges of trusted remote domains - -Both types have the following attributes in common: - - - base-id: the first ID of the Posix ID range - - range-size: the size of the range - -With those two attributes a range object can reserve the Posix IDs starting -with base-id up to but not including base-id+range-size exclusively. - -Additionally an ID range of the local domain may set - - rid-base: the first RID(*) of the corresponding RID range - - secondary-rid-base: first RID of the secondary RID range - -and an ID range of a trusted domain must set - - rid-base: the first RID of the corresponding RID range - - sid: domain SID of the trusted domain - - - -EXAMPLE: Add a new ID range for a trusted domain - -Since there might be more than one trusted domain the domain SID must be given -while creating the ID range. - - ipa idrange-add --base-id=1200000 --range-size=200000 --rid-base=0 \\ - --dom-sid=S-1-5-21-123-456-789 trusted_dom_range - -This ID range is then used by the IPA server and the SSSD IPA provider to -assign Posix UIDs to users from the trusted domain. - -If e.g a range for a trusted domain is configured with the following values: - base-id = 1200000 - range-size = 200000 - rid-base = 0 -the RIDs 0 to 199999 are mapped to the Posix ID from 1200000 to 13999999. So -RID 1000 <-> Posix ID 1201000 - - - -EXAMPLE: Add a new ID range for the local domain - -To create an ID range for the local domain it is not necessary to specify a -domain SID. But since it is possible that a user and a group can have the same -value as Posix ID a second RID interval is needed to handle conflicts. - - ipa idrange-add --base-id=1200000 --range-size=200000 --rid-base=1000 \\ - --secondary-rid-base=1000000 local_range - -The data from the ID ranges of the local domain are used by the IPA server -internally to assign SIDs to IPA users and groups. The SID will then be stored -in the user or group objects. - -If e.g. the ID range for the local domain is configured with the values from -the example above then a new user with the UID 1200007 will get the RID 1007. -If this RID is already used by a group the RID will be 1000007. This can only -happen if a user or a group object was created with a fixed ID because the -automatic assignment will not assign the same ID twice. Since there are only -users and groups sharing the same ID namespace it is sufficient to have only -one fallback range to handle conflicts. - -To find the Posix ID for a given RID from the local domain it has to be -checked first if the RID falls in the primary or secondary RID range and -the rid-base or the secondary-rid-base has to be subtracted, respectively, -and the base-id has to be added to get the Posix ID. - -Typically the creation of ID ranges happens behind the scenes and this CLI -must not be used at all. The ID range for the local domain will be created -during installation or upgrade from an older version. The ID range for a -trusted domain will be created together with the trust by 'ipa trust-add ...'. - -USE CASES: - - Add an ID range from a transitively trusted domain - - If the trusted domain (A) trusts another domain (B) as well and this trust - is transitive 'ipa trust-add domain-A' will only create a range for - domain A. The ID range for domain B must be added manually. - - Add an additional ID range for the local domain - - If the ID range of the local domain is exhausted, i.e. no new IDs can be - assigned to Posix users or groups by the DNA plugin, a new range has to be - created to allow new users and groups to be added. (Currently there is no - connection between this range CLI and the DNA plugin, but a future version - might be able to modify the configuration of the DNS plugin as well) - -In general it is not necessary to modify or delete ID ranges. If there is no -other way to achieve a certain configuration than to modify or delete an ID -range it should be done with great care. Because UIDs are stored in the file -system and are used for access control it might be possible that users are -allowed to access files of other users if an ID range got deleted and reused -for a different domain. - -(*) The RID is typically the last integer of a user or group SID which follows -the domain SID. E.g. if the domain SID is S-1-5-21-123-456-789 and a user from -this domain has the SID S-1-5-21-123-456-789-1010 then 1010 id the RID of the -user. RIDs are unique in a domain, 32bit values and are used for users and -groups. - -{0} -""".format(ID_RANGE_VS_DNA_WARNING)) - -register = Registry() - -@register() -class idrange(LDAPObject): - """ - Range object. - """ - - range_type = ('domain', 'ad', 'ipa') - container_dn = api.env.container_ranges - object_name = ('range') - object_name_plural = ('ranges') - object_class = ['ipaIDrange'] - permission_filter_objectclasses = ['ipaidrange'] - possible_objectclasses = ['ipadomainidrange', 'ipatrustedaddomainrange'] - default_attributes = ['cn', 'ipabaseid', 'ipaidrangesize', 'ipabaserid', - 'ipasecondarybaserid', 'ipanttrusteddomainsid', - 'iparangetype'] - managed_permissions = { - 'System: Read ID Ranges': { - 'replaces_global_anonymous_aci': True, - 'ipapermbindruletype': 'all', - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'cn', 'objectclass', - 'ipabaseid', 'ipaidrangesize', 'iparangetype', - 'ipabaserid', 'ipasecondarybaserid', 'ipanttrusteddomainsid', - }, - }, - } - - label = _('ID Ranges') - label_singular = _('ID Range') - - # The commented range types are planned but not yet supported - range_types = { - u'ipa-local': unicode(_('local domain range')), - # u'ipa-ad-winsync': unicode(_('Active Directory winsync range')), - u'ipa-ad-trust': unicode(_('Active Directory domain range')), - u'ipa-ad-trust-posix': unicode(_('Active Directory trust range with ' - 'POSIX attributes')), - # u'ipa-ipa-trust': unicode(_('IPA trust range')), - } - - takes_params = ( - Str('cn', - cli_name='name', - label=_('Range name'), - primary_key=True, - ), - Int('ipabaseid', - cli_name='base_id', - label=_("First Posix ID of the range"), - ), - Int('ipaidrangesize', - cli_name='range_size', - label=_("Number of IDs in the range"), - ), - Int('ipabaserid?', - cli_name='rid_base', - label=_('First RID of the corresponding RID range'), - ), - Int('ipasecondarybaserid?', - cli_name='secondary_rid_base', - label=_('First RID of the secondary RID range'), - ), - Str('ipanttrusteddomainsid?', - cli_name='dom_sid', - flags=('no_update',), - label=_('Domain SID of the trusted domain'), - ), - Str('ipanttrusteddomainname?', - cli_name='dom_name', - flags=('no_search', 'virtual_attribute', 'no_update'), - label=_('Name of the trusted domain'), - ), - StrEnum('iparangetype?', - label=_('Range type'), - cli_name='type', - doc=(_('ID range type, one of {vals}' - .format(vals=', '.join(range_types.keys())))), - values=tuple(range_types.keys()), - flags=['no_update'], - ) - ) - - def handle_iparangetype(self, entry_attrs, options, keep_objectclass=False): - if not any((options.get('pkey_only', False), - options.get('raw', False))): - range_type = entry_attrs['iparangetype'][0] - entry_attrs['iparangetyperaw'] = [range_type] - entry_attrs['iparangetype'] = [self.range_types.get(range_type, None)] - - # Remove the objectclass - if not keep_objectclass: - if not options.get('all', False) or options.get('pkey_only', False): - entry_attrs.pop('objectclass', None) - - def handle_ipabaserid(self, entry_attrs, options): - if any((options.get('pkey_only', False), options.get('raw', False))): - return - if entry_attrs['iparangetype'][0] == u'ipa-ad-trust-posix': - entry_attrs.pop('ipabaserid', None) - - def check_ids_in_modified_range(self, old_base, old_size, new_base, - new_size): - if new_base is None and new_size is None: - # nothing to check - return - if new_base is None: - new_base = old_base - if new_size is None: - new_size = old_size - old_interval = (old_base, old_base + old_size - 1) - new_interval = (new_base, new_base + new_size - 1) - checked_intervals = [] - low_diff = new_interval[0] - old_interval[0] - if low_diff > 0: - checked_intervals.append((old_interval[0], - min(old_interval[1], new_interval[0] - 1))) - high_diff = old_interval[1] - new_interval[1] - if high_diff > 0: - checked_intervals.append((max(old_interval[0], new_interval[1] + 1), - old_interval[1])) - - if not checked_intervals: - # range is equal or covers the entire old range, nothing to check - return - - ldap = self.backend - id_filter_base = ["(objectclass=posixAccount)", - "(objectclass=posixGroup)", - "(objectclass=ipaIDObject)"] - id_filter_ids = [] - - for id_low, id_high in checked_intervals: - id_filter_ids.append("(&(uidNumber>=%(low)d)(uidNumber<=%(high)d))" - % dict(low=id_low, high=id_high)) - id_filter_ids.append("(&(gidNumber>=%(low)d)(gidNumber<=%(high)d))" - % dict(low=id_low, high=id_high)) - id_filter = ldap.combine_filters( - [ldap.combine_filters(id_filter_base, "|"), - ldap.combine_filters(id_filter_ids, "|")], - "&") - - try: - (objects, truncated) = ldap.find_entries(filter=id_filter, - attrs_list=['uid', 'cn'], - base_dn=DN(api.env.container_accounts, api.env.basedn)) - except errors.NotFound: - # no objects in this range found, allow the command - pass - else: - raise errors.ValidationError(name="ipabaseid,ipaidrangesize", - error=_('range modification leaving objects with ID out ' - 'of the defined range is not allowed')) - - def get_domain_validator(self): - if not _dcerpc_bindings_installed: - raise errors.NotFound(reason=_('Cannot perform SID validation ' - 'without Samba 4 support installed. Make sure you have ' - 'installed server-trust-ad sub-package of IPA on the server')) - - domain_validator = ipaserver.dcerpc.DomainValidator(self.api) - - if not domain_validator.is_configured(): - raise errors.NotFound(reason=_('Cross-realm trusts are not ' - 'configured. Make sure you have run ipa-adtrust-install ' - 'on the IPA server first')) - - return domain_validator - - def validate_trusted_domain_sid(self, sid): - - domain_validator = self.get_domain_validator() - - if not domain_validator.is_trusted_domain_sid_valid(sid): - raise errors.ValidationError(name='domain SID', - error=_('SID is not recognized as a valid SID for a ' - 'trusted domain')) - - def get_trusted_domain_sid_from_name(self, name): - """ Returns unicode string representation for given trusted domain name - or None if SID forthe given trusted domain name could not be found.""" - - domain_validator = self.get_domain_validator() - - sid = domain_validator.get_sid_from_domain_name(name) - - if sid is not None: - sid = unicode(sid) - - return sid - - # checks that primary and secondary rid ranges do not overlap - def are_rid_ranges_overlapping(self, rid_base, secondary_rid_base, size): - - # if any of these is None, the check does not apply - if any(attr is None for attr in (rid_base, secondary_rid_base, size)): - return False - - # sort the bases - if rid_base > secondary_rid_base: - rid_base, secondary_rid_base = secondary_rid_base, rid_base - - # rid_base is now <= secondary_rid_base, - # so the following check is sufficient - if rid_base + size <= secondary_rid_base: - return False - else: - return True - - -@register() -class idrange_add(LDAPCreate): - __doc__ = _(""" - Add new ID range. - - To add a new ID range you always have to specify - - --base-id - --range-size - - Additionally - - --rid-base - --secondary-rid-base - - may be given for a new ID range for the local domain while - - --rid-base - --dom-sid - - must be given to add a new range for a trusted AD domain. - -{0} -""".format(ID_RANGE_VS_DNA_WARNING)) - - msg_summary = _('Added ID range "%(value)s"') - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - - is_set = lambda x: (x in entry_attrs) and (entry_attrs[x] is not None) - - # This needs to stay in options since there is no - # ipanttrusteddomainname attribute in LDAP - if 'ipanttrusteddomainname' in options: - if is_set('ipanttrusteddomainsid'): - raise errors.ValidationError(name='ID Range setup', - error=_('Options dom-sid and dom-name ' - 'cannot be used together')) - - sid = self.obj.get_trusted_domain_sid_from_name( - options['ipanttrusteddomainname']) - - if sid is not None: - entry_attrs['ipanttrusteddomainsid'] = sid - else: - raise errors.ValidationError(name='ID Range setup', - error=_('SID for the specified trusted domain name could ' - 'not be found. Please specify the SID directly ' - 'using dom-sid option.')) - - # ipaNTTrustedDomainSID attribute set, this is AD Trusted domain range - if is_set('ipanttrusteddomainsid'): - entry_attrs['objectclass'].append('ipatrustedaddomainrange') - - # Default to ipa-ad-trust if no type set - if not is_set('iparangetype'): - entry_attrs['iparangetype'] = u'ipa-ad-trust' - - if entry_attrs['iparangetype'] == u'ipa-ad-trust': - if not is_set('ipabaserid'): - raise errors.ValidationError( - name='ID Range setup', - error=_('Options dom-sid/dom-name and rid-base must ' - 'be used together') - ) - elif entry_attrs['iparangetype'] == u'ipa-ad-trust-posix': - if is_set('ipabaserid') and entry_attrs['ipabaserid'] != 0: - raise errors.ValidationError( - name='ID Range setup', - error=_('Option rid-base must not be used when IPA ' - 'range type is ipa-ad-trust-posix') - ) - else: - entry_attrs['ipabaserid'] = 0 - else: - raise errors.ValidationError(name='ID Range setup', - error=_('IPA Range type must be one of ipa-ad-trust ' - 'or ipa-ad-trust-posix when SID of the trusted ' - 'domain is specified')) - - if is_set('ipasecondarybaserid'): - raise errors.ValidationError(name='ID Range setup', - error=_('Options dom-sid/dom-name and secondary-rid-base ' - 'cannot be used together')) - - # Validate SID as the one of trusted domains - self.obj.validate_trusted_domain_sid( - entry_attrs['ipanttrusteddomainsid']) - - # ipaNTTrustedDomainSID attribute not set, this is local domain range - else: - entry_attrs['objectclass'].append('ipadomainidrange') - - # Default to ipa-local if no type set - if 'iparangetype' not in entry_attrs: - entry_attrs['iparangetype'] = 'ipa-local' - - # TODO: can also be ipa-ad-winsync here? - if entry_attrs['iparangetype'] in (u'ipa-ad-trust', - u'ipa-ad-trust-posix'): - raise errors.ValidationError(name='ID Range setup', - error=_('IPA Range type must not be one of ipa-ad-trust ' - 'or ipa-ad-trust-posix when SID of the trusted ' - 'domain is not specified.')) - - # secondary base rid must be set if and only if base rid is set - if is_set('ipasecondarybaserid') != is_set('ipabaserid'): - raise errors.ValidationError(name='ID Range setup', - error=_('Options secondary-rid-base and rid-base must ' - 'be used together')) - - # and they must not overlap - if is_set('ipabaserid') and is_set('ipasecondarybaserid'): - if self.obj.are_rid_ranges_overlapping( - entry_attrs['ipabaserid'], - entry_attrs['ipasecondarybaserid'], - entry_attrs['ipaidrangesize']): - raise errors.ValidationError(name='ID Range setup', - error=_("Primary RID range and secondary RID range" - " cannot overlap")) - - # rid-base and secondary-rid-base must be set if - # ipa-adtrust-install has been run on the system - adtrust_is_enabled = api.Command['adtrust_is_enabled']()['result'] - - if adtrust_is_enabled and not ( - is_set('ipabaserid') and is_set('ipasecondarybaserid')): - raise errors.ValidationError( - name='ID Range setup', - error=_( - 'You must specify both rid-base and ' - 'secondary-rid-base options, because ' - 'ipa-adtrust-install has already been run.' - ) - ) - return dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - self.obj.handle_ipabaserid(entry_attrs, options) - self.obj.handle_iparangetype(entry_attrs, options, - keep_objectclass=True) - return dn - - -@register() -class idrange_del(LDAPDelete): - __doc__ = _('Delete an ID range.') - - msg_summary = _('Deleted ID range "%(value)s"') - - def pre_callback(self, ldap, dn, *keys, **options): - try: - old_attrs = ldap.get_entry(dn, ['ipabaseid', - 'ipaidrangesize', - 'ipanttrusteddomainsid']) - except errors.NotFound: - self.obj.handle_not_found(*keys) - - # Check whether we leave any object with id in deleted range - old_base_id = int(old_attrs.get('ipabaseid', [0])[0]) - old_range_size = int(old_attrs.get('ipaidrangesize', [0])[0]) - self.obj.check_ids_in_modified_range( - old_base_id, old_range_size, 0, 0) - - # Check whether the range does not belong to the active trust - range_sid = old_attrs.get('ipanttrusteddomainsid') - - if range_sid is not None: - # Search for trusted domain with SID specified in the ID range entry - range_sid = range_sid[0] - domain_filter=('(&(objectclass=ipaNTTrustedDomain)' - '(ipanttrusteddomainsid=%s))' % range_sid) - - try: - (trust_domains, truncated) = ldap.find_entries( - base_dn=DN(api.env.container_trusts, api.env.basedn), - filter=domain_filter) - except errors.NotFound: - pass - else: - # If there's an entry, it means that there's active domain - # of a trust that this range belongs to, so raise a - # DependentEntry error - raise errors.DependentEntry( - label='Active Trust domain', - key=keys[0], - dependent=trust_domains[0].dn[0].value) - - - return dn - - -@register() -class idrange_find(LDAPSearch): - __doc__ = _('Search for ranges.') - - msg_summary = ngettext( - '%(count)d range matched', '%(count)d ranges matched', 0 - ) - - # Since all range types are stored within separate containers under - # 'cn=ranges,cn=etc' search can be done on a one-level scope - def pre_callback(self, ldap, filters, attrs_list, base_dn, scope, *args, - **options): - assert isinstance(base_dn, DN) - attrs_list.append('objectclass') - return (filters, base_dn, ldap.SCOPE_ONELEVEL) - - def post_callback(self, ldap, entries, truncated, *args, **options): - for entry in entries: - self.obj.handle_ipabaserid(entry, options) - self.obj.handle_iparangetype(entry, options) - return truncated - - -@register() -class idrange_show(LDAPRetrieve): - __doc__ = _('Display information about a range.') - - def pre_callback(self, ldap, dn, attrs_list, *keys, **options): - assert isinstance(dn, DN) - attrs_list.append('objectclass') - return dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - self.obj.handle_ipabaserid(entry_attrs, options) - self.obj.handle_iparangetype(entry_attrs, options) - return dn - - -@register() -class idrange_mod(LDAPUpdate): - __doc__ = _("""Modify ID range. - -{0} -""".format(ID_RANGE_VS_DNA_WARNING)) - - msg_summary = _('Modified ID range "%(value)s"') - - takes_options = LDAPUpdate.takes_options + ( - Str( - 'ipanttrusteddomainsid?', - deprecated=True, - cli_name='dom_sid', - flags=('no_update', 'no_option'), - label=_('Domain SID of the trusted domain'), - autofill=False, - ), - Str( - 'ipanttrusteddomainname?', - deprecated=True, - cli_name='dom_name', - flags=('no_search', 'virtual_attribute', 'no_update', 'no_option'), - label=_('Name of the trusted domain'), - autofill=False, - ), - ) - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - attrs_list.append('objectclass') - - try: - old_attrs = ldap.get_entry(dn, ['*']) - except errors.NotFound: - self.obj.handle_not_found(*keys) - - if old_attrs['iparangetype'][0] == 'ipa-local': - raise errors.ExecutionError( - message=_('This command can not be used to change ID ' - 'allocation for local IPA domain. Run ' - '`ipa help idrange` for more information') - ) - - is_set = lambda x: (x in entry_attrs) and (entry_attrs[x] is not None) - in_updated_attrs = lambda x:\ - (x in entry_attrs and entry_attrs[x] is not None) or\ - (x not in entry_attrs and x in old_attrs - and old_attrs[x] is not None) - - # This needs to stay in options since there is no - # ipanttrusteddomainname attribute in LDAP - if 'ipanttrusteddomainname' in options: - if is_set('ipanttrusteddomainsid'): - raise errors.ValidationError(name='ID Range setup', - error=_('Options dom-sid and dom-name ' - 'cannot be used together')) - - sid = self.obj.get_trusted_domain_sid_from_name( - options['ipanttrusteddomainname']) - - # we translate the name into sid so further validation can rely - # on ipanttrusteddomainsid attribute only - if sid is not None: - entry_attrs['ipanttrusteddomainsid'] = sid - else: - raise errors.ValidationError(name='ID Range setup', - error=_('SID for the specified trusted domain name could ' - 'not be found. Please specify the SID directly ' - 'using dom-sid option.')) - - if in_updated_attrs('ipanttrusteddomainsid'): - if in_updated_attrs('ipasecondarybaserid'): - raise errors.ValidationError(name='ID Range setup', - error=_('Options dom-sid and secondary-rid-base cannot ' - 'be used together')) - range_type = old_attrs['iparangetype'][0] - if range_type == u'ipa-ad-trust': - if not in_updated_attrs('ipabaserid'): - raise errors.ValidationError( - name='ID Range setup', - error=_('Options dom-sid and rid-base must ' - 'be used together')) - elif (range_type == u'ipa-ad-trust-posix' and - 'ipabaserid' in entry_attrs): - if entry_attrs['ipabaserid'] is None: - entry_attrs['ipabaserid'] = 0 - elif entry_attrs['ipabaserid'] != 0: - raise errors.ValidationError( - name='ID Range setup', - error=_('Option rid-base must not be used when IPA ' - 'range type is ipa-ad-trust-posix') - ) - - if is_set('ipanttrusteddomainsid'): - # Validate SID as the one of trusted domains - # perform this check only if the attribute was changed - self.obj.validate_trusted_domain_sid( - entry_attrs['ipanttrusteddomainsid']) - - # Add trusted AD domain range object class, if it wasn't there - if not 'ipatrustedaddomainrange' in old_attrs['objectclass']: - entry_attrs['objectclass'].append('ipatrustedaddomainrange') - - else: - # secondary base rid must be set if and only if base rid is set - if in_updated_attrs('ipasecondarybaserid') !=\ - in_updated_attrs('ipabaserid'): - raise errors.ValidationError(name='ID Range setup', - error=_('Options secondary-rid-base and rid-base must ' - 'be used together')) - - # ensure that primary and secondary rid ranges do not overlap - if all(in_updated_attrs(base) - for base in ('ipabaserid', 'ipasecondarybaserid')): - - # make sure we are working with updated attributes - rid_range_attributes = ('ipabaserid', 'ipasecondarybaserid', - 'ipaidrangesize') - updated_values = dict() - - for attr in rid_range_attributes: - if is_set(attr): - updated_values[attr] = entry_attrs[attr] - else: - updated_values[attr] = int(old_attrs[attr][0]) - - if self.obj.are_rid_ranges_overlapping( - updated_values['ipabaserid'], - updated_values['ipasecondarybaserid'], - updated_values['ipaidrangesize']): - raise errors.ValidationError(name='ID Range setup', - error=_("Primary RID range and secondary RID range" - " cannot overlap")) - - # check whether ids are in modified range - old_base_id = int(old_attrs.get('ipabaseid', [0])[0]) - old_range_size = int(old_attrs.get('ipaidrangesize', [0])[0]) - new_base_id = entry_attrs.get('ipabaseid') - - if new_base_id is not None: - new_base_id = int(new_base_id) - - new_range_size = entry_attrs.get('ipaidrangesize') - - if new_range_size is not None: - new_range_size = int(new_range_size) - - self.obj.check_ids_in_modified_range(old_base_id, old_range_size, - new_base_id, new_range_size) - - return dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - self.obj.handle_ipabaserid(entry_attrs, options) - self.obj.handle_iparangetype(entry_attrs, options) - return dn - - diff --git a/ipalib/plugins/idviews.py b/ipalib/plugins/idviews.py deleted file mode 100644 index 537f924ce..000000000 --- a/ipalib/plugins/idviews.py +++ /dev/null @@ -1,1123 +0,0 @@ -# Authors: -# Alexander Bokovoy <abokovoy@redhat.com> -# Tomas Babej <tbabej@redhat.com> -# -# Copyright (C) 2014 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/>. -import re - -import six - -from .baseldap import (LDAPQuery, LDAPObject, LDAPCreate, - LDAPDelete, LDAPUpdate, LDAPSearch, - LDAPAddAttribute, LDAPRemoveAttribute, - LDAPRetrieve, global_output_params) -from .hostgroup import get_complete_hostgroup_member_list -from .service import validate_certificate -from ipalib import api, Str, Int, Bytes, Flag, _, ngettext, errors, output -from ipalib.constants import IPA_ANCHOR_PREFIX, SID_ANCHOR_PREFIX -from ipalib.plugable import Registry -from ipalib.util import (normalize_sshpubkey, validate_sshpubkey, - convert_sshpubkey_post) - -from ipapython.dn import DN - -if six.PY3: - unicode = str - -_dcerpc_bindings_installed = False - -if api.env.in_server and api.env.context in ['lite', 'server']: - try: - import ipaserver.dcerpc - _dcerpc_bindings_installed = True - except ImportError: - pass - -__doc__ = _(""" -ID Views -Manage ID Views -IPA allows to override certain properties of users and groups per each host. -This functionality is primarily used to allow migration from older systems or -other Identity Management solutions. -""") - -register = Registry() - -protected_default_trust_view_error = errors.ProtectedEntryError( - label=_('ID View'), - key=u"Default Trust View", - reason=_('system ID View') -) - -fallback_to_ldap_option = Flag( - 'fallback_to_ldap?', - default=False, - label=_('Fallback to AD DC LDAP'), - doc=_("Allow falling back to AD DC LDAP when resolving AD " - "trusted objects. For two-way trusts only."), -) - -DEFAULT_TRUST_VIEW_NAME = "default trust view" - -ANCHOR_REGEX = re.compile( - r':IPA:.*:[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}' - r'|' - r':SID:S-[0-9\-]+' -) - - -@register() -class idview(LDAPObject): - """ - ID View object. - """ - - container_dn = api.env.container_views - object_name = _('ID View') - object_name_plural = _('ID Views') - object_class = ['ipaIDView', 'top'] - default_attributes = ['cn', 'description'] - rdn_is_primary_key = True - - label = _('ID Views') - label_singular = _('ID View') - - takes_params = ( - Str('cn', - cli_name='name', - label=_('ID View Name'), - primary_key=True, - ), - Str('description?', - cli_name='desc', - label=_('Description'), - ), - ) - - permission_filter_objectclasses = ['nsContainer'] - managed_permissions = { - 'System: Read ID Views': { - 'ipapermbindruletype': 'all', - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'cn', 'description', 'objectClass', - }, - }, - } - - -@register() -class idview_add(LDAPCreate): - __doc__ = _('Add a new ID View.') - msg_summary = _('Added ID View "%(value)s"') - - -@register() -class idview_del(LDAPDelete): - __doc__ = _('Delete an ID View.') - msg_summary = _('Deleted ID View "%(value)s"') - - def pre_callback(self, ldap, dn, *keys, **options): - for key in keys: - if key.lower() == DEFAULT_TRUST_VIEW_NAME: - raise protected_default_trust_view_error - - return dn - - -@register() -class idview_mod(LDAPUpdate): - __doc__ = _('Modify an ID View.') - msg_summary = _('Modified an ID View "%(value)s"') - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - for key in keys: - if key.lower() == DEFAULT_TRUST_VIEW_NAME: - raise protected_default_trust_view_error - - return dn - - -@register() -class idview_find(LDAPSearch): - __doc__ = _('Search for an ID View.') - msg_summary = ngettext('%(count)d ID View matched', - '%(count)d ID Views matched', 0) - - -@register() -class idview_show(LDAPRetrieve): - __doc__ = _('Display information about an ID View.') - - takes_options = LDAPRetrieve.takes_options + ( - Flag('show_hosts?', - cli_name='show_hosts', - doc=_('Enumerate all the hosts the view applies to.'), - ), - ) - - has_output_params = global_output_params + ( - Str('useroverrides', - label=_('User object overrides'), - ), - Str('groupoverrides', - label=_('Group object overrides'), - ), - Str('appliedtohosts', - label=_('Hosts the view applies to') - ), - ) - - def show_id_overrides(self, dn, entry_attrs): - ldap = self.obj.backend - - for objectclass, obj_type in [('ipaUserOverride', 'user'), - ('ipaGroupOverride', 'group')]: - - # Attribute to store results is called (user|group)overrides - attr_name = obj_type + 'overrides' - - try: - (overrides, truncated) = ldap.find_entries( - filter="objectclass=%s" % objectclass, - attrs_list=['ipaanchoruuid'], - base_dn=dn, - scope=ldap.SCOPE_ONELEVEL, - paged_search=True) - - resolved_overrides = [] - for override in overrides: - anchor = override.single_value['ipaanchoruuid'] - - try: - name = resolve_anchor_to_object_name(ldap, obj_type, - anchor) - resolved_overrides.append(name) - - except (errors.NotFound, errors.ValidationError): - # Anchor could not be resolved, use raw - resolved_overrides.append(anchor) - - entry_attrs[attr_name] = resolved_overrides - - except errors.NotFound: - # No overrides found, nothing to do - pass - - def enumerate_hosts(self, dn, entry_attrs): - ldap = self.obj.backend - - filter_params = { - 'ipaAssignedIDView': dn, - 'objectClass': 'ipaHost', - } - - try: - (hosts, truncated) = ldap.find_entries( - filter=ldap.make_filter(filter_params, rules=ldap.MATCH_ALL), - attrs_list=['cn'], - base_dn=api.env.container_host + api.env.basedn, - scope=ldap.SCOPE_ONELEVEL, - paged_search=True) - - entry_attrs['appliedtohosts'] = [host.single_value['cn'] - for host in hosts] - except errors.NotFound: - pass - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - self.show_id_overrides(dn, entry_attrs) - - # Enumerating hosts is a potentially expensive operation (uses paged - # search to list all the hosts the ID view applies to). Show the list - # of the hosts only if explicitly asked for (or asked for --all). - # Do not display with --raw, since this attribute does not exist in - # LDAP. - - if ((options.get('show_hosts') or options.get('all')) - and not options.get('raw')): - self.enumerate_hosts(dn, entry_attrs) - - return dn - - -class baseidview_apply(LDAPQuery): - """ - Base class for idview_apply and idview_unapply commands. - """ - - has_output_params = global_output_params - - def execute(self, *keys, **options): - view = keys[-1] if keys else None - ldap = self.obj.backend - - # Test if idview actually exists, if it does not, NotFound is raised - if not options.get('clear_view', False): - view_dn = self.api.Object['idview'].get_dn_if_exists(view) - assert isinstance(view_dn, DN) - - # Check that we're not applying the Default Trust View - if view.lower() == DEFAULT_TRUST_VIEW_NAME: - raise errors.ValidationError( - name=_('ID View'), - error=_('Default Trust View cannot be applied on hosts') - ) - - else: - # In case we are removing assigned view, we modify the host setting - # the ipaAssignedIDView to None - view_dn = None - - completed = 0 - succeeded = {'host': []} - failed = { - 'host': [], - 'hostgroup': [], - } - - # Make sure we ignore None passed via host or hostgroup, since it does - # not make sense - for key in ('host', 'hostgroup'): - if key in options and options[key] is None: - del options[key] - - # Generate a list of all hosts to apply the view to - hosts_to_apply = list(options.get('host', [])) - - for hostgroup in options.get('hostgroup', ()): - try: - hosts_to_apply += get_complete_hostgroup_member_list(hostgroup) - except errors.NotFound: - failed['hostgroup'].append((hostgroup, unicode(_("not found")))) - except errors.PublicError as e: - failed['hostgroup'].append((hostgroup, "%s : %s" % ( - e.__class__.__name__, str(e)))) - - for host in hosts_to_apply: - try: - host_dn = api.Object['host'].get_dn_if_exists(host) - - host_entry = ldap.get_entry(host_dn, - attrs_list=['ipaassignedidview']) - host_entry['ipaassignedidview'] = view_dn - - ldap.update_entry(host_entry) - - # If no exception was raised, view assigment went well - completed = completed + 1 - succeeded['host'].append(host) - except errors.EmptyModlist: - # If view was already applied, complain about it - failed['host'].append((host, - unicode(_("ID View already applied")))) - except errors.NotFound: - failed['host'].append((host, unicode(_("not found")))) - except errors.PublicError as e: - failed['host'].append((host, str(e))) - - # Wrap dictionary containing failures in another dictionary under key - # 'memberhost', since that is output parameter in global_output_params - # and thus we get nice output in the CLI - failed = {'memberhost': failed} - - # Sort the list of affected hosts - succeeded['host'].sort() - - # Note that we're returning the list of affected hosts even if they - # were passed via referencing a hostgroup. This is desired, since we - # want to stress the fact that view is applied on all the current - # member hosts of the hostgroup and not tied with the hostgroup itself. - - return dict( - summary=unicode(_(self.msg_summary % {'value': view})), - succeeded=succeeded, - completed=completed, - failed=failed, - ) - - -@register() -class idview_apply(baseidview_apply): - __doc__ = _('Applies ID View to specified hosts or current members of ' - 'specified hostgroups. If any other ID View is applied to ' - 'the host, it is overridden.') - - member_count_out = (_('ID View applied to %i host.'), - _('ID View applied to %i hosts.')) - - msg_summary = 'Applied ID View "%(value)s"' - - takes_options = ( - Str('host*', - cli_name='hosts', - doc=_('Hosts to apply the ID View to'), - label=_('hosts'), - ), - Str('hostgroup*', - cli_name='hostgroups', - doc=_('Hostgroups to whose hosts apply the ID View to. Please note ' - 'that view is not applied automatically to any hosts added ' - 'to the hostgroup after running the idview-apply command.'), - label=_('hostgroups'), - ), - ) - - has_output = ( - output.summary, - output.Output('succeeded', - type=dict, - doc=_('Hosts that this ID View was applied to.'), - ), - output.Output('failed', - type=dict, - doc=_('Hosts or hostgroups that this ID View could not be ' - 'applied to.'), - ), - output.Output('completed', - type=int, - doc=_('Number of hosts the ID View was applied to:'), - ), - ) - - -@register() -class idview_unapply(baseidview_apply): - __doc__ = _('Clears ID View from specified hosts or current members of ' - 'specified hostgroups.') - - member_count_out = (_('ID View cleared from %i host.'), - _('ID View cleared from %i hosts.')) - - msg_summary = 'Cleared ID Views' - - takes_options = ( - Str('host*', - cli_name='hosts', - doc=_('Hosts to clear (any) ID View from.'), - label=_('hosts'), - ), - Str('hostgroup*', - cli_name='hostgroups', - doc=_('Hostgroups whose hosts should have ID Views cleared. Note ' - 'that view is not cleared automatically from any host added ' - 'to the hostgroup after running idview-unapply command.'), - label=_('hostgroups'), - ), - ) - - has_output = ( - output.summary, - output.Output('succeeded', - type=dict, - doc=_('Hosts that ID View was cleared from.'), - ), - output.Output('failed', - type=dict, - doc=_('Hosts or hostgroups that ID View could not be cleared ' - 'from.'), - ), - output.Output('completed', - type=int, - doc=_('Number of hosts that had a ID View was unset:'), - ), - ) - - # Take no arguments, since ID View reference is not needed to clear - # the hosts - def get_args(self): - return () - - def execute(self, *keys, **options): - options['clear_view'] = True - return super(idview_unapply, self).execute(*keys, **options) - - -# ID overrides helper methods -def verify_trusted_domain_object_type(validator, desired_type, name_or_sid): - - object_type = validator.get_trusted_domain_object_type(name_or_sid) - - if object_type == desired_type: - # In case SSSD returns the same type as the type being - # searched, no problems here. - return True - - elif desired_type == 'user' and object_type == 'both': - # Type both denotes users with magic private groups. - # Overriding attributes for such users is OK. - return True - - elif desired_type == 'group' and object_type == 'both': - # However, overriding attributes for magic private groups - # does not make sense. One should override the GID of - # the user itself. - - raise errors.ConversionError( - name='identifier', - error=_('You are trying to reference a magic private group ' - 'which is not allowed to be overridden. ' - 'Try overriding the GID attribute of the ' - 'corresponding user instead.') - ) - - return False - - -def resolve_object_to_anchor(ldap, obj_type, obj, fallback_to_ldap): - """ - Resolves the user/group name to the anchor uuid: - - first it tries to find the object as user or group in IPA (depending - on the passed obj_type) - - if the IPA lookup failed, lookup object SID in the trusted domains - - Takes options: - ldap - the backend - obj_type - either 'user' or 'group' - obj - the name of the object, e.g 'admin' or 'testuser' - """ - - try: - entry = ldap.get_entry(api.Object[obj_type].get_dn(obj), - attrs_list=['ipaUniqueID', 'objectClass']) - - # First we check this is a valid object to override - # - for groups, it must have ipaUserGroup objectclass - # - for users, it must have posixAccount objectclass - - required_objectclass = { - 'user': 'posixaccount', - 'group': 'ipausergroup', - }[obj_type] - - if required_objectclass not in entry['objectclass']: - raise errors.ValidationError( - name=_('IPA object'), - error=_('system IPA objects (e.g system groups, user ' - 'private groups) cannot be overridden') - ) - - # The domain prefix, this will need to be reworked once we - # introduce IPA-IPA trusts - domain = api.env.domain - uuid = entry.single_value['ipaUniqueID'] - - return "%s%s:%s" % (IPA_ANCHOR_PREFIX, domain, uuid) - except errors.NotFound: - pass - - # If not successfull, try looking up the object in the trusted domain - try: - if _dcerpc_bindings_installed: - domain_validator = ipaserver.dcerpc.DomainValidator(api) - if domain_validator.is_configured(): - sid = domain_validator.get_trusted_domain_object_sid(obj, - fallback_to_ldap=fallback_to_ldap) - - # We need to verify that the object type is correct - type_correct = verify_trusted_domain_object_type( - domain_validator, obj_type, sid) - - if type_correct: - # There is no domain prefix since SID contains information - # about the domain - return SID_ANCHOR_PREFIX + sid - - except errors.ValidationError: - # Domain validator raises Validation Error if object name does not - # contain domain part (either NETBIOS\ prefix or @domain.name suffix) - pass - - # No acceptable object was found - api.Object[obj_type].handle_not_found(obj) - - -def resolve_anchor_to_object_name(ldap, obj_type, anchor): - """ - Resolves IPA Anchor UUID to the actual common object name (uid for users, - cn for groups). - - Takes options: - ldap - the backend - anchor - the anchor, e.g. - ':IPA:ipa.example.com:2cb604ea-39a5-11e4-a37e-001a4a22216f' - """ - - if anchor.startswith(IPA_ANCHOR_PREFIX): - - # Prepare search parameters - accounts_dn = DN(api.env.container_accounts, api.env.basedn) - - # Anchor of the form :IPA:<domain>:<uuid> - # Strip the IPA prefix and the domain prefix - uuid = anchor.rpartition(':')[-1].strip() - - # Set the object type-specific search attributes - objectclass, name_attr = { - 'user': ('posixaccount', 'uid'), - 'group': ('ipausergroup', 'cn'), - }[obj_type] - - entry = ldap.find_entry_by_attr(attr='ipaUniqueID', - value=uuid, - object_class=objectclass, - attrs_list=[name_attr], - base_dn=accounts_dn) - - # Return the name of the object, which is either cn for - # groups or uid for users - return entry.single_value[name_attr] - - elif anchor.startswith(SID_ANCHOR_PREFIX): - - # Parse the SID out from the anchor - sid = anchor[len(SID_ANCHOR_PREFIX):].strip() - - if _dcerpc_bindings_installed: - domain_validator = ipaserver.dcerpc.DomainValidator(api) - if domain_validator.is_configured(): - name = domain_validator.get_trusted_domain_object_from_sid(sid) - - # We need to verify that the object type is correct - type_correct = verify_trusted_domain_object_type( - domain_validator, obj_type, name) - - if type_correct: - return name - - # No acceptable object was found - raise errors.NotFound( - reason=_("Anchor '%(anchor)s' could not be resolved.") - % dict(anchor=anchor)) - - -def remove_ipaobject_overrides(ldap, api, dn): - """ - Removes all ID overrides for given object. This method is to be - consumed by -del commands of the given objects (users, groups). - """ - - entry = ldap.get_entry(dn, attrs_list=['ipaUniqueID']) - object_uuid = entry.single_value['ipaUniqueID'] - - override_filter = '(ipaanchoruuid=:IPA:{0}:{1})'.format(api.env.domain, - object_uuid) - try: - entries, truncated = ldap.find_entries( - override_filter, - base_dn=DN(api.env.container_views, api.env.basedn), - paged_search=True - ) - except errors.EmptyResult: - pass - else: - # In case we found something, delete it - for entry in entries: - ldap.delete_entry(entry) - - -# This is not registered on purpose, it's a base class for ID overrides -class baseidoverride(LDAPObject): - """ - Base ID override object. - """ - - parent_object = 'idview' - container_dn = api.env.container_views - - object_class = ['ipaOverrideAnchor', 'top'] - default_attributes = [ - 'description', 'ipaAnchorUUID', - ] - - takes_params = ( - Str('ipaanchoruuid', - cli_name='anchor', - primary_key=True, - label=_('Anchor to override'), - ), - Str('description?', - cli_name='desc', - label=_('Description'), - ), - ) - - override_object = None - - def get_dn(self, *keys, **options): - # If user passed raw anchor, do not try - # to translate it. - if ANCHOR_REGEX.match(keys[-1]): - anchor = keys[-1] - - # Otherwise, translate object into a - # legitimate object anchor. - else: - anchor = resolve_object_to_anchor( - self.backend, - self.override_object, - keys[-1], - fallback_to_ldap=options['fallback_to_ldap'] - ) - - keys = keys[:-1] + (anchor, ) - return super(baseidoverride, self).get_dn(*keys, **options) - - def set_anchoruuid_from_dn(self, dn, entry_attrs): - # TODO: Use entry_attrs.single_value once LDAPUpdate supports - # lists in primary key fields (baseldap.LDAPUpdate.execute) - entry_attrs['ipaanchoruuid'] = dn[0].value - - def convert_anchor_to_human_readable_form(self, entry_attrs, **options): - if not options.get('raw'): - anchor = entry_attrs.single_value['ipaanchoruuid'] - - if anchor: - try: - object_name = resolve_anchor_to_object_name( - self.backend, - self.override_object, - anchor - ) - entry_attrs.single_value['ipaanchoruuid'] = object_name - except errors.NotFound: - # If we were unable to resolve the anchor, - # keep it in the raw form - pass - except errors.ValidationError: - # Same as above, ValidationError may be raised when SIDs - # are attempted to be converted, but the domain is no - # longer trusted - pass - - def prohibit_ipa_users_in_default_view(self, dn, entry_attrs): - # Check if parent object is Default Trust View, if so, prohibit - # adding overrides for IPA objects - - if dn[1].value.lower() == DEFAULT_TRUST_VIEW_NAME: - if dn[0].value.startswith(IPA_ANCHOR_PREFIX): - raise errors.ValidationError( - name=_('ID View'), - error=_('Default Trust View cannot contain IPA users') - ) - -class baseidoverride_add(LDAPCreate): - __doc__ = _('Add a new ID override.') - msg_summary = _('Added ID override "%(value)s"') - - takes_options = LDAPCreate.takes_options + (fallback_to_ldap_option,) - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - self.obj.set_anchoruuid_from_dn(dn, entry_attrs) - self.obj.prohibit_ipa_users_in_default_view(dn, entry_attrs) - return dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - self.obj.convert_anchor_to_human_readable_form(entry_attrs, **options) - return dn - - -class baseidoverride_del(LDAPDelete): - __doc__ = _('Delete an ID override.') - msg_summary = _('Deleted ID override "%(value)s"') - - takes_options = LDAPDelete.takes_options + (fallback_to_ldap_option,) - - def pre_callback(self, ldap, dn, *keys, **options): - assert isinstance(dn, DN) - - # Make sure the entry we're deleting has all the objectclasses - # this object requires - try: - entry = ldap.get_entry(dn, ['objectclass']) - except errors.NotFound: - self.obj.handle_not_found(*keys) - - required_object_classes = set(self.obj.object_class) - actual_object_classes = set(entry['objectclass']) - - # If not, treat it as a failed search - if not required_object_classes.issubset(actual_object_classes): - self.obj.handle_not_found(*keys) - - return dn - - -class baseidoverride_mod(LDAPUpdate): - __doc__ = _('Modify an ID override.') - msg_summary = _('Modified an ID override "%(value)s"') - - takes_options = LDAPUpdate.takes_options + (fallback_to_ldap_option,) - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - if 'rename' in options: - raise errors.ValidationError( - name=_('ID override'), - error=_('ID overrides cannot be renamed') - ) - - self.obj.prohibit_ipa_users_in_default_view(dn, entry_attrs) - return dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - self.obj.convert_anchor_to_human_readable_form(entry_attrs, **options) - return dn - - -class baseidoverride_find(LDAPSearch): - __doc__ = _('Search for an ID override.') - msg_summary = ngettext('%(count)d ID override matched', - '%(count)d ID overrides matched', 0) - - takes_options = LDAPSearch.takes_options + (fallback_to_ldap_option,) - - def post_callback(self, ldap, entries, truncated, *args, **options): - for entry in entries: - self.obj.convert_anchor_to_human_readable_form(entry, **options) - return truncated - - -class baseidoverride_show(LDAPRetrieve): - __doc__ = _('Display information about an ID override.') - - takes_options = LDAPRetrieve.takes_options + (fallback_to_ldap_option,) - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - self.obj.convert_anchor_to_human_readable_form(entry_attrs, **options) - return dn - - -@register() -class idoverrideuser(baseidoverride): - - object_name = _('User ID override') - object_name_plural = _('User ID overrides') - - label = _('User ID overrides') - label_singular = _('User ID override') - rdn_is_primary_key = True - - permission_filter_objectclasses = ['ipaUserOverride'] - managed_permissions = { - 'System: Read User ID Overrides': { - 'ipapermbindruletype': 'all', - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'objectClass', 'ipaAnchorUUID', 'uidNumber', 'description', - 'homeDirectory', 'uid', 'ipaOriginalUid', 'loginShell', 'gecos', - 'gidNumber', 'ipaSshPubkey', 'usercertificate' - }, - }, - } - - object_class = baseidoverride.object_class + ['ipaUserOverride'] - possible_objectclasses = ['ipasshuser', 'ipaSshGroupOfPubKeys'] - default_attributes = baseidoverride.default_attributes + [ - 'homeDirectory', 'uidNumber', 'uid', 'ipaOriginalUid', 'loginShell', - 'ipaSshPubkey', 'gidNumber', 'gecos', 'usercertificate;binary', - ] - - search_display_attributes = baseidoverride.default_attributes + [ - 'homeDirectory', 'uidNumber', 'uid', 'ipaOriginalUid', 'loginShell', - 'ipaSshPubkey', 'gidNumber', 'gecos', - ] - - takes_params = baseidoverride.takes_params + ( - Str('uid?', - pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?$', - pattern_errmsg='may only include letters, numbers, _, -, . and $', - maxlength=255, - cli_name='login', - label=_('User login'), - normalizer=lambda value: value.lower(), - ), - Int('uidnumber?', - cli_name='uid', - label=_('UID'), - doc=_('User ID Number'), - minvalue=1, - ), - Str('gecos?', - label=_('GECOS'), - ), - Int('gidnumber?', - label=_('GID'), - doc=_('Group ID Number'), - minvalue=1, - ), - Str('homedirectory?', - cli_name='homedir', - label=_('Home directory'), - ), - Str('loginshell?', - cli_name='shell', - label=_('Login shell'), - ), - Str('ipaoriginaluid?', - flags=['no_option', 'no_output'] - ), - Str('ipasshpubkey*', validate_sshpubkey, - cli_name='sshpubkey', - label=_('SSH public key'), - normalizer=normalize_sshpubkey, - flags=['no_search'], - ), - Bytes('usercertificate*', validate_certificate, - cli_name='certificate', - label=_('Certificate'), - doc=_('Base-64 encoded user certificate'), - flags=['no_search',], - ), - ) - - override_object = 'user' - - def update_original_uid_reference(self, entry_attrs): - anchor = entry_attrs.single_value['ipaanchoruuid'] - try: - original_uid = resolve_anchor_to_object_name(self.backend, - self.override_object, - anchor) - entry_attrs['ipaOriginalUid'] = original_uid - - except (errors.NotFound, errors.ValidationError): - # Anchor could not be resolved, this means we had to specify the - # object to manipulate using a raw anchor value already, hence - # we have no way to update the original_uid - pass - - def convert_usercertificate_pre(self, entry_attrs): - if 'usercertificate' in entry_attrs: - entry_attrs['usercertificate;binary'] = entry_attrs.pop( - 'usercertificate') - - def convert_usercertificate_post(self, entry_attrs, **options): - if 'usercertificate;binary' in entry_attrs: - entry_attrs['usercertificate'] = entry_attrs.pop( - 'usercertificate;binary') - - - -@register() -class idoverridegroup(baseidoverride): - - object_name = _('Group ID override') - object_name_plural = _('Group ID overrides') - - label = _('Group ID overrides') - label_singular = _('Group ID override') - rdn_is_primary_key = True - - permission_filter_objectclasses = ['ipaGroupOverride'] - managed_permissions = { - 'System: Read Group ID Overrides': { - 'ipapermbindruletype': 'all', - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'objectClass', 'ipaAnchorUUID', 'gidNumber', - 'description', 'cn', - }, - }, - } - - object_class = baseidoverride.object_class + ['ipaGroupOverride'] - default_attributes = baseidoverride.default_attributes + [ - 'gidNumber', 'cn', - ] - - takes_params = baseidoverride.takes_params + ( - Str('cn?', - pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?$', - pattern_errmsg='may only include letters, numbers, _, -, . and $', - maxlength=255, - cli_name='group_name', - label=_('Group name'), - normalizer=lambda value: value.lower(), - ), - Int('gidnumber?', - cli_name='gid', - label=_('GID'), - doc=_('Group ID Number'), - minvalue=1, - ), - ) - - override_object = 'group' - -@register() -class idoverrideuser_add_cert(LDAPAddAttribute): - __doc__ = _('Add one or more certificates to the idoverrideuser entry') - msg_summary = _('Added certificates to idoverrideuser "%(value)s"') - attribute = 'usercertificate' - - takes_options = LDAPAddAttribute.takes_options + (fallback_to_ldap_option,) - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, - **options): - dn = self.obj.get_dn(*keys, **options) - self.obj.convert_usercertificate_pre(entry_attrs) - - return dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - self.obj.convert_usercertificate_post(entry_attrs, **options) - self.obj.convert_anchor_to_human_readable_form(entry_attrs, **options) - return dn - - -@register() -class idoverrideuser_remove_cert(LDAPRemoveAttribute): - __doc__ = _('Remove one or more certificates to the idoverrideuser entry') - msg_summary = _('Removed certificates from idoverrideuser "%(value)s"') - attribute = 'usercertificate' - - takes_options = LDAPRemoveAttribute.takes_options + (fallback_to_ldap_option,) - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, - **options): - dn = self.obj.get_dn(*keys, **options) - self.obj.convert_usercertificate_pre(entry_attrs) - - return dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - self.obj.convert_usercertificate_post(entry_attrs, **options) - self.obj.convert_anchor_to_human_readable_form(entry_attrs, **options) - - return dn - - -@register() -class idoverrideuser_add(baseidoverride_add): - __doc__ = _('Add a new User ID override.') - msg_summary = _('Added User ID override "%(value)s"') - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - dn = super(idoverrideuser_add, self).pre_callback(ldap, dn, - entry_attrs, attrs_list, *keys, **options) - - entry_attrs['objectclass'].append('ipasshuser') - self.obj.convert_usercertificate_pre(entry_attrs) - - # Update the ipaOriginalUid - self.obj.update_original_uid_reference(entry_attrs) - return dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - dn = super(idoverrideuser_add, self).post_callback(ldap, dn, - entry_attrs, *keys, **options) - convert_sshpubkey_post(entry_attrs) - self.obj.convert_usercertificate_post(entry_attrs, **options) - return dn - - - -@register() -class idoverrideuser_del(baseidoverride_del): - __doc__ = _('Delete an User ID override.') - msg_summary = _('Deleted User ID override "%(value)s"') - - -@register() -class idoverrideuser_mod(baseidoverride_mod): - __doc__ = _('Modify an User ID override.') - msg_summary = _('Modified an User ID override "%(value)s"') - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - dn = super(idoverrideuser_mod, self).pre_callback(ldap, dn, - entry_attrs, attrs_list, *keys, **options) - - # Update the ipaOriginalUid - self.obj.set_anchoruuid_from_dn(dn, entry_attrs) - self.obj.update_original_uid_reference(entry_attrs) - if 'objectclass' in entry_attrs: - obj_classes = entry_attrs['objectclass'] - else: - _entry_attrs = ldap.get_entry(dn, ['objectclass']) - obj_classes = entry_attrs['objectclass'] = _entry_attrs['objectclass'] - - if 'ipasshpubkey' in entry_attrs and 'ipasshuser' not in obj_classes: - obj_classes.append('ipasshuser') - - self.obj.convert_usercertificate_pre(entry_attrs) - return dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - dn = super(idoverrideuser_mod, self).post_callback(ldap, dn, - entry_attrs, *keys, **options) - convert_sshpubkey_post(entry_attrs) - self.obj.convert_usercertificate_post(entry_attrs, **options) - return dn - - -@register() -class idoverrideuser_find(baseidoverride_find): - __doc__ = _('Search for an User ID override.') - msg_summary = ngettext('%(count)d User ID override matched', - '%(count)d User ID overrides matched', 0) - - def post_callback(self, ldap, entries, truncated, *args, **options): - truncated = super(idoverrideuser_find, self).post_callback( - ldap, entries, truncated, *args, **options) - for entry in entries: - convert_sshpubkey_post(entry) - self.obj.convert_usercertificate_post(entry, **options) - return truncated - - -@register() -class idoverrideuser_show(baseidoverride_show): - __doc__ = _('Display information about an User ID override.') - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - dn = super(idoverrideuser_show, self).post_callback(ldap, dn, - entry_attrs, *keys, **options) - convert_sshpubkey_post(entry_attrs) - self.obj.convert_usercertificate_post(entry_attrs, **options) - return dn - - -@register() -class idoverridegroup_add(baseidoverride_add): - __doc__ = _('Add a new Group ID override.') - msg_summary = _('Added Group ID override "%(value)s"') - - -@register() -class idoverridegroup_del(baseidoverride_del): - __doc__ = _('Delete an Group ID override.') - msg_summary = _('Deleted Group ID override "%(value)s"') - - -@register() -class idoverridegroup_mod(baseidoverride_mod): - __doc__ = _('Modify an Group ID override.') - msg_summary = _('Modified an Group ID override "%(value)s"') - - -@register() -class idoverridegroup_find(baseidoverride_find): - __doc__ = _('Search for an Group ID override.') - msg_summary = ngettext('%(count)d Group ID override matched', - '%(count)d Group ID overrides matched', 0) - - -@register() -class idoverridegroup_show(baseidoverride_show): - __doc__ = _('Display information about an Group ID override.') diff --git a/ipalib/plugins/internal.py b/ipalib/plugins/internal.py deleted file mode 100644 index 99b0c04d1..000000000 --- a/ipalib/plugins/internal.py +++ /dev/null @@ -1,859 +0,0 @@ -# Authors: -# Pavel Zuna <pzuna@redhat.com> -# Adam Young <ayoung@redhat.com> -# Endi S. Dewata <edewata@redhat.com> -# -# Copyright (c) 2010 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/>. - -""" -Plugins not accessible directly through the CLI, commands used internally -""" -from ipalib import Command -from ipalib import Str -from ipalib.output import Output -from ipalib.text import _ -from ipalib.util import json_serialize -from ipalib.plugable import Registry - -register = Registry() - -@register() -class json_metadata(Command): - """ - Export plugin meta-data for the webUI. - """ - NO_CLI = True - - - takes_args = ( - Str('objname?', - doc=_('Name of object to export'), - ), - Str('methodname?', - doc=_('Name of method to export'), - ), - ) - - takes_options = ( - Str('object?', - doc=_('Name of object to export'), - ), - Str('method?', - doc=_('Name of method to export'), - ), - Str('command?', - doc=_('Name of command to export'), - ), - ) - - has_output = ( - Output('objects', dict, doc=_('Dict of JSON encoded IPA Objects')), - Output('methods', dict, doc=_('Dict of JSON encoded IPA Methods')), - Output('commands', dict, doc=_('Dict of JSON encoded IPA Commands')), - ) - - def execute(self, objname=None, methodname=None, **options): - objects = dict() - methods = dict() - commands = dict() - - empty = True - - try: - if not objname: - objname = options['object'] - if objname in self.api.Object: - o = self.api.Object[objname] - objects = dict([(o.name, json_serialize(o))]) - elif objname == "all": - objects = dict( - (o.name, json_serialize(o)) for o in self.api.Object() - ) - empty = False - except KeyError: - pass - - try: - if not methodname: - methodname = options['method'] - if methodname in self.api.Method: - m = self.api.Method[methodname] - methods = dict([(m.name, json_serialize(m))]) - elif methodname == "all": - methods = dict( - (m.name, json_serialize(m)) for m in self.api.Method() - ) - empty = False - except KeyError: - pass - - try: - cmdname = options['command'] - if cmdname in self.api.Command: - c = self.api.Command[cmdname] - commands = dict([(c.name, json_serialize(c))]) - elif cmdname == "all": - commands = dict( - (c.name, json_serialize(c)) for c in self.api.Command() - ) - empty = False - except KeyError: - pass - - if empty: - objects = dict( - (o.name, json_serialize(o)) for o in self.api.Object() - ) - methods = dict( - (m.name, json_serialize(m)) for m in self.api.Method() - ) - commands = dict( - (c.name, json_serialize(c)) for c in self.api.Command() - ) - - retval = dict([ - ("objects", objects), - ("methods", methods), - ("commands", commands), - ]) - - return retval - - -@register() -class i18n_messages(Command): - NO_CLI = True - - messages = { - "ajax": { - "401": { - "message": _("Your session has expired. Please re-login."), - }, - }, - "actions": { - "apply": _("Apply"), - "automember_rebuild": _("Rebuild auto membership"), - "automember_rebuild_confirm": _("Are you sure you want to rebuild auto membership?"), - "automember_rebuild_success": _("Automember rebuild membership task completed"), - "confirm": _("Are you sure you want to proceed with the action?"), - "delete_confirm": _("Are you sure you want to delete ${object}?"), - "disable_confirm": _("Are you sure you want to disable ${object}?"), - "enable_confirm": _("Are you sure you want to enable ${object}?"), - "title": _("Actions"), - }, - "association": { - "add": { - "ipasudorunas": _("Add RunAs ${other_entity} into ${entity} ${primary_key}"), - "ipasudorunasgroup": _("Add RunAs Groups into ${entity} ${primary_key}"), - "managedby": _("Add ${other_entity} Managing ${entity} ${primary_key}"), - "member": _("Add ${other_entity} into ${entity} ${primary_key}"), - "memberallowcmd": _("Add Allow ${other_entity} into ${entity} ${primary_key}"), - "memberdenycmd": _("Add Deny ${other_entity} into ${entity} ${primary_key}"), - "memberof": _("Add ${entity} ${primary_key} into ${other_entity}"), - }, - "added": _("${count} item(s) added"), - "direct_membership": _("Direct Membership"), - "filter_placeholder": _("Filter available ${other_entity}"), - "indirect_membership": _("Indirect Membership"), - "no_entries": _("No entries."), - "paging": _("Showing ${start} to ${end} of ${total} entries."), - "remove": { - "ipasudorunas": _("Remove RunAs ${other_entity} from ${entity} ${primary_key}"), - "ipasudorunasgroup": _("Remove RunAs Groups from ${entity} ${primary_key}"), - "managedby": _("Remove ${other_entity} Managing ${entity} ${primary_key}"), - "member": _("Remove ${other_entity} from ${entity} ${primary_key}"), - "memberallowcmd": _("Remove Allow ${other_entity} from ${entity} ${primary_key}"), - "memberdenycmd": _("Remove Deny ${other_entity} from ${entity} ${primary_key}"), - "memberof": _("Remove ${entity} ${primary_key} from ${other_entity}"), - }, - "removed": _("${count} item(s) removed"), - "show_results": _("Show Results"), - }, - "authtype": { - "config_tooltip": _("<p>Implicit method (password) will be used if no method is chosen.</p><p><strong>Password + Two-factor:</strong> LDAP and Kerberos allow authentication with either one of the authentication types but Kerberos uses pre-authentication method which requires to use armor ccache.</p><p><strong>RADIUS with another type:</strong> Kerberos always use RADIUS, but LDAP never does. LDAP only recognize the password and two-factor authentication options.</p>"), - "type_otp": _("Two factor authentication (password + OTP)"), - "type_password": _("Password"), - "type_radius": _("Radius"), - "type_disabled": _("Disable per-user override"), - "user_tooltip": _("<p>Per-user setting, overwrites the global setting if any option is checked.</p><p><strong>Password + Two-factor:</strong> LDAP and Kerberos allow authentication with either one of the authentication types but Kerberos uses pre-authentication method which requires to use armor ccache.</p><p><strong>RADIUS with another type:</strong> Kerberos always use RADIUS, but LDAP never does. LDAP only recognize the password and two-factor authentication options.</p>"), - }, - "buttons": { - "about": _("About"), - "activate": _("Activate"), - "add": _("Add"), - "add_and_add_another": _("Add and Add Another"), - "add_and_close": _("Add and Close"), - "add_and_edit": _("Add and Edit"), - "add_many": _("Add Many"), - "apply": _("Apply"), - "back": _("Back"), - "cancel": _("Cancel"), - "close": _("Close"), - "disable": _("Disable"), - "edit": _("Edit"), - "enable": _("Enable"), - "filter": _("Filter"), - "find": _("Find"), - "get": _("Get"), - "hide": _("Hide"), - "issue": _("Issue"), - "ok": _("OK"), - "refresh": _("Refresh"), - "refresh_title": _("Reload current settings from the server."), - "remove": _("Delete"), - "reset": _("Reset"), - "reset_password_and_login": _("Reset Password and Login"), - "restore": _("Restore"), - "retry": _("Retry"), - "revert": _("Revert"), - "revert_title": ("Undo all unsaved changes."), - "revoke": _("Revoke"), - "save": _("Save"), - "set": _("Set"), - "show": _("Show"), - "unapply": ("Un-apply"), - "update": _("Update"), - "view": _("View"), - }, - "details": { - "collapse_all": _("Collapse All"), - "expand_all": _("Expand All"), - "general": _("General"), - "identity": _("Identity Settings"), - "settings": _("${entity} ${primary_key} Settings"), - "to_top": _("Back to Top"), - "updated": _("${entity} ${primary_key} updated"), - }, - "dialogs": { - "add_confirmation": _("${entity} successfully added"), - "add_title": _("Add ${entity}"), - "available": _("Available"), - "batch_error_message": _("Some operations failed."), - "batch_error_title": _("Operations Error"), - "confirmation": _("Confirmation"), - "dirty_message": _("This page has unsaved changes. Please save or revert."), - "dirty_title": _("Unsaved Changes"), - "edit_title": _("Edit ${entity}"), - "hide_details": _("Hide details"), - "about_title": _("About"), - "about_message": _("${product}, version: ${version}"), - "prospective": _("Prospective"), - "redirection": _("Redirection"), - "remove_empty": _("Select entries to be removed."), - "remove_title": _("Remove ${entity}"), - "result": _("Result"), - "show_details": _("Show details"), - "success": _("Success"), - "validation_title": _("Validation error"), - "validation_message": _("Input form contains invalid or missing values."), - }, - "error_report": { - "options": _("Please try the following options:"), - "problem_persists": _("If the problem persists please contact the system administrator."), - "refresh": _("Refresh the page."), - "reload": _("Reload the browser."), - "main_page": _("Return to the main page and retry the operation"), - "title": _("An error has occurred (${error})"), - }, - "errors": { - "error": _("Error"), - "http_error": _("HTTP Error"), - "internal_error": _("Internal Error"), - "ipa_error": _("IPA Error"), - "no_response": _("No response"), - "unknown_error": _("Unknown Error"), - "url": _("URL"), - }, - "facet_groups": { - "managedby": _("${primary_key} is managed by:"), - "member": _("${primary_key} members:"), - "memberof": _("${primary_key} is a member of:"), - }, - "facets": { - "details": _("Settings"), - "search": _("Search"), - }, - "false": _("False"), - "keytab": { - "add_create": _("Allow ${other_entity} to create keytab of ${primary_key}"), - "add_retrive": _("Allow ${other_entity} to retrieve keytab of ${primary_key}"), - "allowed_to_create": _("Allowed to create keytab"), - "allowed_to_retrieve": _("Allowed to retrieve keytab"), - "remove_create": _("Disallow ${other_entity} to create keytab of ${primary_key}"), - "remove_retrieve": _("Disallow ${other_entity} to retrieve keytab of ${primary_key}"), - }, - "krbauthzdata": { - "inherited": _("Inherited from server configuration"), - "mspac": _("MS-PAC"), - "override": _("Override inherited settings"), - "pad": _("PAD"), - }, - "login": { - "form_auth": _("<i class=\"fa fa-info-circle\"></i> To login with <strong>username and password</strong>, enter them in the corresponding fields, then click Login."), - "header": _("Logged In As"), - "krb_auth_msg": _("<i class=\"fa fa-info-circle\"></i> To login with <strong>Kerberos</strong>, please make sure you have valid tickets (obtainable via kinit) and <a href='http://${host}/ipa/config/unauthorized.html'>configured</a> the browser correctly, then click Login."), - "login": _("Login"), - "logout": _("Logout"), - "logout_error": _("Logout error"), - "password": _("Password"), - "sync_otp_token": _("Sync OTP Token"), - "username": _("Username"), - }, - "measurement_units": { - "number_of_passwords": _("number of passwords"), - "seconds": _("seconds"), - }, - "objects": { - "aci": { - "attribute": _("Attribute"), - }, - "automember": { - "add_condition": _("Add Condition into ${pkey}"), - "add_rule": _("Add Rule"), - "attribute": _("Attribute"), - "default_host_group": _("Default host group"), - "default_user_group": _("Default user group"), - "exclusive": _("Exclusive"), - "expression": _("Expression"), - "hostgrouprule": _("Host group rule"), - "hostgrouprules": _("Host group rules"), - "inclusive": _("Inclusive"), - "usergrouprule": _("User group rule"), - "usergrouprules": _("User group rules"), - }, - "automountkey": { - }, - "automountlocation": { - "identity": _("Automount Location Settings") - }, - "automountmap": { - "map_type": _("Map Type"), - "direct": _("Direct"), - "indirect": _("Indirect"), - }, - "caacl": { - "any_host": _("Any Host"), - "any_service": _("Any Service"), - "any_profile": _("Any Profile"), - "anyone": _("Anyone"), - "ipaenabledflag": _("Rule status"), - "profile": _("Profiles"), - "specified_hosts": _("Specified Hosts and Groups"), - "specified_profiles": _("Specified Profiles"), - "specified_services": _("Specified Services and Groups"), - "specified_users": _("Specified Users and Groups"), - "who": _("Permitted to have certificates issued"), - }, - "cert": { - "aa_compromise": _("AA Compromise"), - "add_principal": _("Add principal"), - "affiliation_changed": _("Affiliation Changed"), - "ca_compromise": _("CA Compromise"), - "certificate": _("Certificate"), - "certificates": _("Certificates"), - "certificate_hold": _("Certificate Hold"), - "cessation_of_operation": _("Cessation of Operation"), - "common_name": _("Common Name"), - "expires_on": _("Expires On"), - "find_issuedon_from": _("Issued on from"), - "find_issuedon_to": _("Issued on to"), - "find_max_serial_number": _("Maximum serial number"), - "find_min_serial_number": _("Minimum serial number"), - "find_revocation_reason": _("Revocation reason"), - "find_revokedon_from": _("Revoked on from"), - "find_revokedon_to": _("Revoked on to"), - "find_subject": _("Subject"), - "find_validnotafter_from": _("Valid not after from"), - "find_validnotafter_to": _("Valid not after to"), - "find_validnotbefore_from": _("Valid not before from"), - "find_validnotbefore_to": _("Valid not before to"), - "fingerprints": _("Fingerprints"), - "get_certificate": _("Get Certificate"), - "issue_certificate": _("Issue New Certificate for ${entity} ${primary_key}"), - "issue_certificate_generic": _("Issue New Certificate"), - "issued_by": _("Issued By"), - "issued_on": _("Issued On"), - "issued_to": _("Issued To"), - "key_compromise": _("Key Compromise"), - "md5_fingerprint": _("MD5 Fingerprint"), - "missing": _("No Valid Certificate"), - "new_certificate": _("New Certificate"), - "note": _("Note"), - "organization": _("Organization"), - "organizational_unit": _("Organizational Unit"), - "present": _("${count} certificate(s) present"), - "privilege_withdrawn": _("Privilege Withdrawn"), - "reason": _("Reason for Revocation"), - "remove_from_crl": _("Remove from CRL"), - "request_message": _("<ol> <li>Create a certificate database or use an existing one. To create a new database:<br/> <code># certutil -N -d <database path></code> </li> <li>Create a CSR with subject <em>CN=<${cn_name}>,O=<realm></em>, for example:<br/> <code># certutil -R -d <database path> -a -g <key size> -s 'CN=${cn},O=${realm}'</code> </li> <li> Copy and paste the CSR (from <em>-----BEGIN NEW CERTIFICATE REQUEST-----</em> to <em>-----END NEW CERTIFICATE REQUEST-----</em>) into the text area below: </li> </ol>"), - "requested": _("Certificate requested"), - "restore_certificate": _("Restore Certificate for ${entity} ${primary_key}"), - "restore_certificate_simple": _("Restore Certificate"), - "restore_confirmation": _("To confirm your intention to restore this certificate, click the \"Restore\" button."), - "restored": _("Certificate restored"), - "revocation_reason": _("Revocation reason"), - "revoke_certificate": _("Revoke Certificate for ${entity} ${primary_key}"), - "revoke_certificate_simple": _("Revoke Certificate"), - "revoke_confirmation": _("To confirm your intention to revoke this certificate, select a reason from the pull-down list, and click the \"Revoke\" button."), - "revoked": _("Certificate Revoked"), - "serial_number": _("Serial Number"), - "serial_number_hex": _("Serial Number (hex)"), - "sha1_fingerprint": _("SHA1 Fingerprint"), - "status": _("Status"), - "superseded": _("Superseded"), - "unspecified": _("Unspecified"), - "valid": _("Valid Certificate Present"), - "validity": _("Validity"), - "view_certificate": _("Certificate for ${entity} ${primary_key}"), - "view_certificate_btn": _("View Certificate"), - }, - "config": { - "group": _("Group Options"), - "search": _("Search Options"), - "selinux": _("SELinux Options"), - "service": _("Service Options"), - "user": _("User Options"), - }, - "delegation": { - }, - "dnsconfig": { - "forward_first": _("Forward first"), - "forward_none": _("Forwarding disabled"), - "forward_only": _("Forward only"), - "options": _("Options"), - }, - "dnsrecord": { - "data": _("Data"), - "deleted_no_data": _("DNS record was deleted because it contained no data."), - "other": _("Other Record Types"), - "ptr_redir_address_err": _("Address not valid, can't redirect"), - "ptr_redir_create": _("Create dns record"), - "ptr_redir_creating": _("Creating record."), - "ptr_redir_creating_err": _("Record creation failed."), - "ptr_redir_record": _("Checking if record exists."), - "ptr_redir_record_err": _("Record not found."), - "ptr_redir_title": _("Redirection to PTR record"), - "ptr_redir_zone": _("Zone found: ${zone}"), - "ptr_redir_zone_err": _("Target reverse zone not found."), - "ptr_redir_zones": _("Fetching DNS zones."), - "ptr_redir_zones_err": _("An error occurred while fetching dns zones."), - "redirection_dnszone": _("You will be redirected to DNS Zone."), - "standard": _("Standard Record Types"), - "title": _("Records for DNS Zone"), - "type": _("Record Type"), - }, - "dnszone": { - "identity": _("DNS Zone Settings"), - "add_permission":_("Add Permission"), - "add_permission_confirm":_("Are you sure you want to add permission for DNS Zone ${object}?"), - "remove_permission": _("Remove Permission"), - "remove_permission_confirm": _("Are you sure you want to remove permission for DNS Zone ${object}?"), - "skip_dns_check": _("Skip DNS check"), - "skip_overlap_check": _("Skip overlap check"), - "soamname_change_message": _("Do you want to check if new authoritative nameserver address is in DNS"), - "soamname_change_title": _("Authoritative nameserver change"), - }, - "domainlevel": { - "label": _("Domain Level"), - "label_singular": _("Domain Level"), - "ipadomainlevel": _("Level"), - "set": _("Set Domain Level"), - }, - "group": { - "details": _("Group Settings"), - "external": _("External"), - "make_external": _("Change to external group"), - "make_posix": _("Change to POSIX group"), - "normal": _("Normal"), - "posix": _("POSIX"), - "type": _("Group Type"), - }, - "hbacrule": { - "any_host": _("Any Host"), - "any_service": _("Any Service"), - "anyone": _("Anyone"), - "host": _("Accessing"), - "ipaenabledflag": _("Rule status"), - "service": _("Via Service"), - "specified_hosts": _("Specified Hosts and Groups"), - "specified_services": _("Specified Services and Groups"), - "specified_users": _("Specified Users and Groups"), - "user": _("Who"), - }, - "hbacsvc": { - }, - "hbacsvcgroup": { - "services": _("Services"), - }, - "hbactest": { - "access_denied": _("Access Denied"), - "access_granted": _("Access Granted"), - "include_disabled": _("Include Disabled"), - "include_enabled": _("Include Enabled"), - "label": _("HBAC Test"), - "matched": _("Matched"), - "missing_values": _("Missing values: "), - "new_test": _("New Test"), - "rules": _("Rules"), - "run_test": _("Run Test"), - "specify_external": _("Specify external ${entity}"), - "unmatched": _("Unmatched"), - }, - "host": { - "certificate": _("Host Certificate"), - "cn": _("Host Name"), - "delete_key_unprovision": _("Delete Key, Unprovision"), - "details": _("Host Settings"), - "enrolled": _("Enrolled"), - "enrollment": _("Enrollment"), - "fqdn": _("Fully Qualified Host Name"), - "generate_otp": _("Generate OTP"), - "generated_otp": _("Generated OTP"), - "keytab": _("Kerberos Key"), - "keytab_missing": _("Kerberos Key Not Present"), - "keytab_present": _("Kerberos Key Present, Host Provisioned"), - "password": _("One-Time-Password"), - "password_missing": _("One-Time-Password Not Present"), - "password_present": _("One-Time-Password Present"), - "password_reset_button": _("Reset OTP"), - "password_reset_title": _("Reset One-Time-Password"), - "password_set_button": _("Set OTP"), - "password_set_success": _("OTP set"), - "password_set_title": _("Set One-Time-Password"), - "status": _("Status"), - "unprovision": _("Unprovision"), - "unprovision_confirmation": _("Are you sure you want to unprovision this host?"), - "unprovision_title": _("Unprovisioning ${entity}"), - "unprovisioned": _("Host unprovisioned"), - }, - "hostgroup": { - "identity": _("Host Group Settings"), - }, - "idoverrideuser": { - "anchor_label": _("User to override"), - "anchor_tooltip": _("Enter trusted or IPA user login. Note: search doesn't list users from trusted domains."), - "anchor_tooltip_ad": _("Enter trusted user login."), - }, - "idoverridegroup": { - "anchor_label": _("Group to override"), - "anchor_tooltip": _("Enter trusted or IPA group name. Note: search doesn't list groups from trusted domains."), - "anchor_tooltip_ad": _("Enter trusted group name."), - }, - "idview": { - "appliesto_tab": _("${primary_key} applies to:"), - "appliedtohosts": _("Applied to hosts"), - "appliedtohosts_title": _("Applied to hosts"), - "apply_hostgroups": _("Apply to host groups"), - "apply_hostgroups_title": _("Apply ID View ${primary_key} on hosts of ${entity}"), - "apply_hosts": _("Apply to hosts"), - "apply_hosts_title": _("Apply ID view ${primary_key} on ${entity}"), - "ipaassignedidview": _("Assigned ID View"), - "overrides_tab": _("${primary_key} overrides:"), - "unapply_hostgroups": _("Un-apply from host groups"), - "unapply_hostgroups_all_title": _("Un-apply ID Views from hosts of hostgroups"), - "unapply_hostgroups_title": _("Un-apply ID View ${primary_key} from hosts of ${entity}"), - "unapply_hosts": _("Un-apply"), - "unapply_hosts_all": _("Un-apply from hosts"), - "unapply_hosts_all_title": _("Un-apply ID Views from hosts"), - "unapply_hosts_confirm": _("Are you sure you want to un-apply ID view from selected entries?"), - "unapply_hosts_title": _("Un-apply ID View ${primary_key} from hosts"), - }, - "krbtpolicy": { - "identity": _("Kerberos Ticket Policy"), - }, - "netgroup": { - "any_host": _("Any Host"), - "anyone": _("Anyone"), - "external": _("External"), - "host": _("Host"), - "hostgroups": _("Host Groups"), - "hosts": _("Hosts"), - "identity": _("Netgroup Settings"), - "specified_hosts": _("Specified Hosts and Groups"), - "specified_users": _("Specified Users and Groups"), - "user": _("User"), - "usergroups": _("User Groups"), - "users": _("Users"), - }, - "otptoken": { - "add_token": _("Add OTP Token"), - "app_link": _("You can use <a href=\"${link}\" target=\"_blank\">FreeOTP<a/> as a software OTP token application."), - "config_title": _("Configure your token"), - "config_instructions": _("Configure your token by scanning the QR code below. Click on the QR code if you see this on the device you want to configure."), - "details": _("OTP Token Settings"), - "disable": _("Disable token"), - "enable": _("Enable token"), - "show_qr": _("Show QR code"), - "show_uri": _("Show configuration uri"), - "type_hotp": _("Counter-based (HOTP)"), - "type_totp": _("Time-based (TOTP)"), - }, - "permission": { - "add_custom_attr": _("Add custom attribute"), - "attribute": _("Attribute"), - "filter": _("Filter"), - "identity": _("Permission settings"), - "managed": _("Attribute breakdown"), - "target": _("Target"), - }, - "privilege": { - "identity": _("Privilege Settings"), - }, - "pwpolicy": { - "identity": _("Password Policy"), - }, - "idrange": { - "details": _("Range Settings"), - "ipabaseid": _("Base ID"), - "ipabaserid": _("Primary RID base"), - "ipaidrangesize": _("Range size"), - "ipanttrusteddomainsid": _("Domain SID"), - "ipasecondarybaserid": _("Secondary RID base"), - "type": _("Range type"), - "type_ad": _("Active Directory domain"), - "type_ad_posix": _("Active Directory domain with POSIX attributes"), - "type_detect": _("Detect"), - "type_local": _("Local domain"), - "type_ipa": _("IPA trust"), - "type_winsync": _("Active Directory winsync"), - }, - "radiusproxy": { - "details": _("RADIUS Proxy Server Settings"), - }, - "realmdomains": { - "identity": _("Realm Domains"), - "check_dns": _("Check DNS"), - "check_dns_confirmation": _("Do you also want to perform DNS check?"), - "force_update": _("Force Update"), - }, - "role": { - "identity": _("Role Settings"), - }, - "selfservice": { - }, - "selinuxusermap": { - "any_host": _("Any Host"), - "anyone": _("Anyone"), - "host": _("Host"), - "specified_hosts": _("Specified Hosts and Groups"), - "specified_users": _("Specified Users and Groups"), - "user": _("User"), - }, - "service": { - "certificate": _("Service Certificate"), - "delete_key_unprovision": _("Delete Key, Unprovision"), - "details": _("Service Settings"), - "host": _("Host Name"), - "missing": _("Kerberos Key Not Present"), - "provisioning": _("Provisioning"), - "service": _("Service"), - "status": _("Status"), - "unprovision": _("Unprovision"), - "unprovision_confirmation": _("Are you sure you want to unprovision this service?"), - "unprovision_title": _("Unprovisioning ${entity}"), - "unprovisioned": _("Service unprovisioned"), - "valid": _("Kerberos Key Present, Service Provisioned"), - }, - "sshkeystore": { - "keys": _("SSH public keys"), - "set_dialog_help": _("SSH public key:"), - "set_dialog_title": _("Set SSH key"), - "show_set_key": _("Show/Set key"), - "status_mod_ns": _("Modified: key not set"), - "status_mod_s": _("Modified"), - "status_new_ns": _("New: key not set"), - "status_new_s": _("New: key set"), - }, - "stageuser": { - "activate_confirm": _("Are you sure you want to activate selected users?"), - "activate_one_confirm": _("Are you sure you want to activate ${object}?"), - "activate_success": _("${count} user(s) activated"), - "label": _("Stage users"), - "preserved_label": _("Preserved users"), - "undel_confirm": _("Are you sure you want to restore selected users?"), - "undel_success": _("${count} user(s) restored"), - "user_categories": _("User categories"), - }, - "sudocmd": { - "groups": _("Groups"), - }, - "sudocmdgroup": { - "commands": _("Commands"), - }, - "sudorule": { - "allow": _("Allow"), - "any_command": _("Any Command"), - "any_group": _("Any Group"), - "any_host": _("Any Host"), - "anyone": _("Anyone"), - "command": _("Run Commands"), - "deny": _("Deny"), - "external": _("External"), - "host": _("Access this host"), - "ipaenabledflag": _("Rule status"), - "option_added": _("Option added"), - "option_removed": _("${count} option(s) removed"), - "options": _("Options"), - "runas": _("As Whom"), - "specified_commands": _("Specified Commands and Groups"), - "specified_groups": _("Specified Groups"), - "specified_hosts": _("Specified Hosts and Groups"), - "specified_users": _("Specified Users and Groups"), - "user": _("Who"), - }, - "topology": { - "segment_details": _("Segment details"), - "replication_config": _("Replication configuration"), - "insufficient_domain_level" : _("Managed topology requires minimal domain level ${domainlevel}"), - }, - "trust": { - "account": _("Account"), - "admin_account": _("Administrative account"), - "blacklists": _("SID blacklists"), - "details": _("Trust Settings"), - "domain": _("Domain"), - "establish_using": _("Establish using"), - "fetch_domains": _("Fetch domains"), - "ipantflatname": _("Domain NetBIOS name"), - "ipanttrusteddomainsid": _("Domain Security Identifier"), - "preshared_password": _("Pre-shared password"), - "trustdirection": _("Trust direction"), - "truststatus": _("Trust status"), - "trusttype": _("Trust type"), - }, - "trustconfig": { - "options": _("Options"), - }, - "user": { - "account": _("Account Settings"), - "account_status": _("Account Status"), - "activeuser_label": _("Active users"), - "contact": _("Contact Settings"), - "delete_mode": _("Delete mode"), - "employee": _("Employee Information"), - "error_changing_status": _("Error changing account status"), - "krbpasswordexpiration": _("Password expiration"), - "mailing": _("Mailing Address"), - "misc": _("Misc. Information"), - "mode_delete": _("delete"), - "mode_preserve": _("preserve"), - "noprivate": _("No private group"), - "status_confirmation": _("Are you sure you want to ${action} the user?<br/>The change will take effect immediately."), - "status_link": _("Click to ${action}"), - "unlock": _("Unlock"), - "unlock_confirm": _("Are you sure you want to unlock user ${object}?"), - }, - }, - "password": { - "current_password": _("Current Password"), - "current_password_required": _("Current password is required"), - "expires_in": _("Your password expires in ${days} days."), - "first_otp": _("First OTP"), - "invalid_password": _("The password or username you entered is incorrect."), - "new_password": _("New Password"), - "new_password_required": _("New password is required"), - "otp": _("OTP"), - "otp_info": _("<i class=\"fa fa-info-circle\"></i> <strong>One-Time-Password(OTP):</strong> Generate new OTP code for each OTP field."), - "otp_long": _("One-Time-Password"), - "otp_sync_fail": _("Token synchronization failed"), - "otp_sync_invalid": _("The username, password or token codes are not correct"), - "otp_sync_success":_("Token was synchronized"), - "password": _("Password"), - "password_and_otp": _("Password or Password+One-Time-Password"), - "password_change_complete": _("Password change complete"), - "password_must_match": _("Passwords must match"), - "reset_failure": _("Password reset was not successful."), - "reset_password": _("Reset Password"), - "reset_password_sentence": _("Reset your password."), - "second_otp": _("Second OTP"), - "token_id": _("Token ID"), - "verify_password": _("Verify Password"), - }, - "search": { - "delete_confirm": _("Are you sure you want to delete selected entries?"), - "deleted": _("${count} item(s) deleted"), - "disable_confirm": _("Are you sure you want to disable selected entries?"), - "disabled": _("${count} item(s) disabled"), - "enable_confirm": _("Are you sure you want to enable selected entries?"), - "enabled": _("${count} item(s) enabled"), - "partial_delete": _("Some entries were not deleted"), - "placeholder": _("Search"), - "quick_links": _("Quick Links"), - "select_all": _("Select All"), - "truncated": _("Query returned more results than the configured size limit. Displaying the first ${counter} results."), - "unselect_all": _("Unselect All"), - }, - "status": { - "disable": _("Disable"), - "disabled": _("Disabled"), - "enable": _("Enable"), - "enabled": _("Enabled"), - "label": _("Status"), - "working": _("Working"), - }, - "tabs": { - "audit": _("Audit"), - "authentication": _("Authentication"), - "automember": _("Automember"), - "automount": _("Automount"), - "cert": _("Certificates"), - "dns": _("DNS"), - "hbac": _("Host Based Access Control"), - "identity": _("Identity"), - "ipaserver": _("IPA Server"), - "network_services": _("Network Services"), - "policy": _("Policy"), - "role": _("Role Based Access Control"), - "sudo": _("Sudo"), - "topology": _("Topology"), - "trust": _("Trusts"), - }, - "true": _("True"), - "widget": { - "first": _("First"), - "last": _("Last"), - "next": _("Next"), - "page": _("Page"), - "prev": _("Prev"), - "undo": _("Undo"), - "undo_title": _("Undo this change."), - "undo_all": _("Undo All"), - "undo_all_title": _("Undo all changes in this field."), - "validation": { - "error": _("Text does not match field pattern"), - "datetime": _("Must be an UTC date/time value (e.g., \"2014-01-20 17:58:01Z\")"), - "decimal": _("Must be a decimal number"), - "format": _("Format error"), - "integer": _("Must be an integer"), - "ip_address": _('Not a valid IP address'), - "ip_v4_address": _('Not a valid IPv4 address'), - "ip_v6_address": _('Not a valid IPv6 address'), - "max_value": _("Maximum value is ${value}"), - "min_value": _("Minimum value is ${value}"), - "net_address": _("Not a valid network address (examples: 2001:db8::/64, 192.0.2.0/24)"), - "parse": _("Parse error"), - "port": _("'${port}' is not a valid port"), - "required": _("Required field"), - "unsupported": _("Unsupported value"), - }, - }, - } - has_output = ( - Output('texts', dict, doc=_('Dict of I18N messages')), - ) - def execute(self, **options): - return dict(texts=json_serialize(self.messages)) diff --git a/ipalib/plugins/krbtpolicy.py b/ipalib/plugins/krbtpolicy.py deleted file mode 100644 index 7cf587661..000000000 --- a/ipalib/plugins/krbtpolicy.py +++ /dev/null @@ -1,243 +0,0 @@ -# Authors: -# Pavel Zuna <pzuna@redhat.com> -# -# Copyright (C) 2010 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/>. - -from ipalib import api, errors, output, _ -from ipalib import Int, Str -from . import baseldap -from .baseldap import entry_to_dict, pkey_to_value -from ipalib.plugable import Registry -from ipapython.dn import DN - -__doc__ = _(""" -Kerberos ticket policy - -There is a single Kerberos ticket policy. This policy defines the -maximum ticket lifetime and the maximum renewal age, the period during -which the ticket is renewable. - -You can also create a per-user ticket policy by specifying the user login. - -For changes to the global policy to take effect, restarting the KDC service -is required, which can be achieved using: - -service krb5kdc restart - -Changes to per-user policies take effect immediately for newly requested -tickets (e.g. when the user next runs kinit). - -EXAMPLES: - - Display the current Kerberos ticket policy: - ipa krbtpolicy-show - - Reset the policy to the default: - ipa krbtpolicy-reset - - Modify the policy to 8 hours max life, 1-day max renewal: - ipa krbtpolicy-mod --maxlife=28800 --maxrenew=86400 - - Display effective Kerberos ticket policy for user 'admin': - ipa krbtpolicy-show admin - - Reset per-user policy for user 'admin': - ipa krbtpolicy-reset admin - - Modify per-user policy for user 'admin': - ipa krbtpolicy-mod admin --maxlife=3600 -""") - -register = Registry() - -# FIXME: load this from a config file? -_default_values = { - 'krbmaxticketlife': 86400, - 'krbmaxrenewableage': 604800, -} - - -@register() -class krbtpolicy(baseldap.LDAPObject): - """ - Kerberos Ticket Policy object - """ - container_dn = DN(('cn', api.env.realm), ('cn', 'kerberos')) - object_name = _('kerberos ticket policy settings') - default_attributes = ['krbmaxticketlife', 'krbmaxrenewableage'] - limit_object_classes = ['krbticketpolicyaux'] - # permission_filter_objectclasses is deliberately missing, - # so it is not possible to create a permission of `--type krbtpolicy`. - # This is because we need two permissions to cover both global and per-user - # policies. - managed_permissions = { - 'System: Read Default Kerberos Ticket Policy': { - 'non_object': True, - 'replaces_global_anonymous_aci': True, - 'ipapermtargetfilter': ['(objectclass=krbticketpolicyaux)'], - 'ipapermlocation': DN(container_dn, api.env.basedn), - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'krbdefaultencsalttypes', 'krbmaxrenewableage', - 'krbmaxticketlife', 'krbsupportedencsalttypes', - 'objectclass', - }, - 'default_privileges': { - 'Kerberos Ticket Policy Readers', - }, - }, - 'System: Read User Kerberos Ticket Policy': { - 'non_object': True, - 'replaces_global_anonymous_aci': True, - 'ipapermlocation': DN(api.env.container_user, api.env.basedn), - 'ipapermtargetfilter': ['(objectclass=krbticketpolicyaux)'], - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'krbmaxrenewableage', 'krbmaxticketlife', - }, - 'default_privileges': { - 'Kerberos Ticket Policy Readers', - }, - }, - } - - label = _('Kerberos Ticket Policy') - label_singular = _('Kerberos Ticket Policy') - - takes_params = ( - Str('uid?', - cli_name='user', - label=_('User name'), - doc=_('Manage ticket policy for specific user'), - primary_key=True, - ), - Int('krbmaxticketlife?', - cli_name='maxlife', - label=_('Max life'), - doc=_('Maximum ticket life (seconds)'), - minvalue=1, - ), - Int('krbmaxrenewableage?', - cli_name='maxrenew', - label=_('Max renew'), - doc=_('Maximum renewable age (seconds)'), - minvalue=1, - ), - ) - - def get_dn(self, *keys, **kwargs): - if keys[-1] is not None: - return self.api.Object.user.get_dn(*keys, **kwargs) - return DN(self.container_dn, api.env.basedn) - - -@register() -class krbtpolicy_mod(baseldap.LDAPUpdate): - __doc__ = _('Modify Kerberos ticket policy.') - - def execute(self, uid=None, **options): - return super(krbtpolicy_mod, self).execute(uid, **options) - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - # disable all flag - # ticket policies are attached to objects with unrelated attributes - if options.get('all'): - options['all'] = False - return dn - - -@register() -class krbtpolicy_show(baseldap.LDAPRetrieve): - __doc__ = _('Display the current Kerberos ticket policy.') - - def execute(self, uid=None, **options): - return super(krbtpolicy_show, self).execute(uid, **options) - - def pre_callback(self, ldap, dn, attrs_list, *keys, **options): - assert isinstance(dn, DN) - # disable all flag - # ticket policies are attached to objects with unrelated attributes - if options.get('all'): - options['all'] = False - return dn - - def post_callback(self, ldap, dn, entry, *keys, **options): - default_entry = None - rights = None - for attrname in self.obj.default_attributes: - if attrname not in entry: - if keys[-1] is not None: - # User entry doesn't override the attribute. - # Check if this is caused by insufficient read rights - if rights is None: - rights = baseldap.get_effective_rights( - ldap, dn, self.obj.default_attributes) - if 'r' not in rights.get(attrname.lower(), ''): - raise errors.ACIError( - info=_('Ticket policy for %s could not be read') % - keys[-1]) - # Fallback to the default - if default_entry is None: - try: - default_dn = self.obj.get_dn(None) - default_entry = ldap.get_entry(default_dn) - except errors.NotFound: - default_entry = {} - if attrname in default_entry: - entry[attrname] = default_entry[attrname] - if attrname not in entry: - raise errors.ACIError( - info=_('Default ticket policy could not be read')) - return dn - - -@register() -class krbtpolicy_reset(baseldap.LDAPQuery): - __doc__ = _('Reset Kerberos ticket policy to the default values.') - - has_output = output.standard_entry - - def execute(self, uid=None, **options): - ldap = self.obj.backend - - dn = self.obj.get_dn(uid, **options) - - def_values = {} - # if reseting policy for a user - just his values - if uid is not None: - for a in self.obj.default_attributes: - def_values[a] = None - # if reseting global policy - set values to default - else: - def_values = _default_values - - entry = ldap.get_entry(dn, def_values.keys()) - entry.update(def_values) - try: - ldap.update_entry(entry) - except errors.EmptyModlist: - pass - - if uid is not None: - # policy for user was deleted, retrieve global policy - dn = self.obj.get_dn(None) - entry_attrs = ldap.get_entry(dn, self.obj.default_attributes) - - entry_attrs = entry_to_dict(entry_attrs, **options) - - return dict(result=entry_attrs, value=pkey_to_value(uid, options)) diff --git a/ipalib/plugins/migration.py b/ipalib/plugins/migration.py deleted file mode 100644 index 7f634a7cc..000000000 --- a/ipalib/plugins/migration.py +++ /dev/null @@ -1,920 +0,0 @@ -# Authors: -# 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/>. - -import re -from ldap import MOD_ADD -from ldap import SCOPE_BASE, SCOPE_ONELEVEL, SCOPE_SUBTREE - -import six - -from ipalib import api, errors, output -from ipalib import Command, Password, Str, Flag, StrEnum, DNParam, Bool -from ipalib.cli import to_cli -from ipalib.plugable import Registry -from .user import NO_UPG_MAGIC -if api.env.in_server and api.env.context in ['lite', 'server']: - try: - from ipaserver.plugins.ldap2 import ldap2 - except Exception as e: - raise e -from ipalib import _ -from ipapython.dn import DN -from ipapython.ipautil import write_tmp_file -import datetime -from ipaplatform.paths import paths - -if six.PY3: - unicode = str - -__doc__ = _(""" -Migration to IPA - -Migrate users and groups from an LDAP server to IPA. - -This performs an LDAP query against the remote server searching for -users and groups in a container. In order to migrate passwords you need -to bind as a user that can read the userPassword attribute on the remote -server. This is generally restricted to high-level admins such as -cn=Directory Manager in 389-ds (this is the default bind user). - -The default user container is ou=People. - -The default group container is ou=Groups. - -Users and groups that already exist on the IPA server are skipped. - -Two LDAP schemas define how group members are stored: RFC2307 and -RFC2307bis. RFC2307bis uses member and uniquemember to specify group -members, RFC2307 uses memberUid. The default schema is RFC2307bis. - -The schema compat feature allows IPA to reformat data for systems that -do not support RFC2307bis. It is recommended that this feature is disabled -during migration to reduce system overhead. It can be re-enabled after -migration. To migrate with it enabled use the "--with-compat" option. - -Migrated users do not have Kerberos credentials, they have only their -LDAP password. To complete the migration process, users need to go -to http://ipa.example.com/ipa/migration and authenticate using their -LDAP password in order to generate their Kerberos credentials. - -Migration is disabled by default. Use the command ipa config-mod to -enable it: - - ipa config-mod --enable-migration=TRUE - -If a base DN is not provided with --basedn then IPA will use either -the value of defaultNamingContext if it is set or the first value -in namingContexts set in the root of the remote LDAP server. - -Users are added as members to the default user group. This can be a -time-intensive task so during migration this is done in a batch -mode for every 100 users. As a result there will be a window in which -users will be added to IPA but will not be members of the default -user group. - -EXAMPLES: - - The simplest migration, accepting all defaults: - ipa migrate-ds ldap://ds.example.com:389 - - Specify the user and group container. This can be used to migrate user - and group data from an IPA v1 server: - ipa migrate-ds --user-container='cn=users,cn=accounts' \\ - --group-container='cn=groups,cn=accounts' \\ - ldap://ds.example.com:389 - - Since IPA v2 server already contain predefined groups that may collide with - groups in migrated (IPA v1) server (for example admins, ipausers), users - having colliding group as their primary group may happen to belong to - an unknown group on new IPA v2 server. - Use --group-overwrite-gid option to overwrite GID of already existing groups - to prevent this issue: - ipa migrate-ds --group-overwrite-gid \\ - --user-container='cn=users,cn=accounts' \\ - --group-container='cn=groups,cn=accounts' \\ - ldap://ds.example.com:389 - - Migrated users or groups may have object class and accompanied attributes - unknown to the IPA v2 server. These object classes and attributes may be - left out of the migration process: - ipa migrate-ds --user-container='cn=users,cn=accounts' \\ - --group-container='cn=groups,cn=accounts' \\ - --user-ignore-objectclass=radiusprofile \\ - --user-ignore-attribute=radiusgroupname \\ - ldap://ds.example.com:389 - -LOGGING - -Migration will log warnings and errors to the Apache error log. This -file should be evaluated post-migration to correct or investigate any -issues that were discovered. - -For every 100 users migrated an info-level message will be displayed to -give the current progress and duration to make it possible to track -the progress of migration. - -If the log level is debug, either by setting debug = True in -/etc/ipa/default.conf or /etc/ipa/server.conf, then an entry will be printed -for each user added plus a summary when the default user group is -updated. -""") - -register = Registry() - -# USER MIGRATION CALLBACKS AND VARS - -_krb_err_msg = _('Kerberos principal %s already exists. Use \'ipa user-mod\' to set it manually.') -_krb_failed_msg = _('Unable to determine if Kerberos principal %s already exists. Use \'ipa user-mod\' to set it manually.') -_grp_err_msg = _('Failed to add user to the default group. Use \'ipa group-add-member\' to add manually.') -_ref_err_msg = _('Migration of LDAP search reference is not supported.') -_dn_err_msg = _('Malformed DN') - -_supported_schemas = (u'RFC2307bis', u'RFC2307') - -# search scopes for users and groups when migrating -_supported_scopes = {u'base': SCOPE_BASE, u'onelevel': SCOPE_ONELEVEL, u'subtree': SCOPE_SUBTREE} -_default_scope = u'onelevel' - - -def _pre_migrate_user(ldap, pkey, dn, entry_attrs, failed, config, ctx, **kwargs): - assert isinstance(dn, DN) - attr_blacklist = ['krbprincipalkey','memberofindirect','memberindirect'] - attr_blacklist.extend(kwargs.get('attr_blacklist', [])) - ds_ldap = ctx['ds_ldap'] - has_upg = ctx['has_upg'] - search_bases = kwargs.get('search_bases', None) - valid_gids = kwargs['valid_gids'] - invalid_gids = kwargs['invalid_gids'] - - if 'gidnumber' not in entry_attrs: - raise errors.NotFound(reason=_('%(user)s is not a POSIX user') % dict(user=pkey)) - else: - # See if the gidNumber at least points to a valid group on the remote - # server. - if entry_attrs['gidnumber'][0] in invalid_gids: - api.log.warning('GID number %s of migrated user %s does not point to a known group.' \ - % (entry_attrs['gidnumber'][0], pkey)) - elif entry_attrs['gidnumber'][0] not in valid_gids: - try: - remote_entry = ds_ldap.find_entry_by_attr( - 'gidnumber', entry_attrs['gidnumber'][0], 'posixgroup', - [''], search_bases['group'] - ) - valid_gids.add(entry_attrs['gidnumber'][0]) - except errors.NotFound: - api.log.warning('GID number %s of migrated user %s does not point to a known group.' \ - % (entry_attrs['gidnumber'][0], pkey)) - invalid_gids.add(entry_attrs['gidnumber'][0]) - except errors.SingleMatchExpected as e: - # GID number matched more groups, this should not happen - api.log.warning('GID number %s of migrated user %s should match 1 group, but it matched %d groups' \ - % (entry_attrs['gidnumber'][0], pkey, e.found)) - except errors.LimitsExceeded as e: - api.log.warning('Search limit exceeded searching for GID %s' % entry_attrs['gidnumber'][0]) - - # We don't want to create a UPG so set the magic value in description - # to let the DS plugin know. - entry_attrs.setdefault('description', []) - entry_attrs['description'].append(NO_UPG_MAGIC) - - # fill in required attributes by IPA - entry_attrs['ipauniqueid'] = 'autogenerate' - if 'homedirectory' not in entry_attrs: - homes_root = config.get('ipahomesrootdir', (paths.HOME_DIR, ))[0] - home_dir = '%s/%s' % (homes_root, pkey) - home_dir = home_dir.replace('//', '/').rstrip('/') - entry_attrs['homedirectory'] = home_dir - - if 'loginshell' not in entry_attrs: - default_shell = config.get('ipadefaultloginshell', [paths.SH])[0] - entry_attrs.setdefault('loginshell', default_shell) - - # do not migrate all attributes - for attr in attr_blacklist: - entry_attrs.pop(attr, None) - - # do not migrate all object classes - if 'objectclass' in entry_attrs: - for object_class in kwargs.get('oc_blacklist', []): - try: - entry_attrs['objectclass'].remove(object_class) - except ValueError: # object class not present - pass - - # generate a principal name and check if it isn't already taken - principal = u'%s@%s' % (pkey, api.env.realm) - try: - ldap.find_entry_by_attr( - 'krbprincipalname', principal, 'krbprincipalaux', [''], - DN(api.env.container_user, api.env.basedn) - ) - except errors.NotFound: - entry_attrs['krbprincipalname'] = principal - except errors.LimitsExceeded: - failed[pkey] = unicode(_krb_failed_msg % principal) - else: - failed[pkey] = unicode(_krb_err_msg % principal) - - # Fix any attributes with DN syntax that point to entries in the old - # tree - - for attr in entry_attrs.keys(): - if ldap.has_dn_syntax(attr): - for ind, value in enumerate(entry_attrs[attr]): - if not isinstance(value, DN): - # value is not DN instance, the automatic encoding may have - # failed due to missing schema or the remote attribute type OID was - # not detected as DN type. Try to work this around - api.log.debug('%s: value %s of type %s in attribute %s is not a DN' - ', convert it', pkey, value, type(value), attr) - try: - value = DN(value) - except ValueError as e: - api.log.warning('%s: skipping normalization of value %s of type %s ' - 'in attribute %s which could not be converted to DN: %s', - pkey, value, type(value), attr, e) - continue - try: - remote_entry = ds_ldap.get_entry(value, [api.Object.user.primary_key.name, api.Object.group.primary_key.name]) - except errors.NotFound: - api.log.warning('%s: attribute %s refers to non-existent entry %s' % (pkey, attr, value)) - continue - if value.endswith(search_bases['user']): - primary_key = api.Object.user.primary_key.name - container = api.env.container_user - elif value.endswith(search_bases['group']): - primary_key = api.Object.group.primary_key.name - container = api.env.container_group - else: - api.log.warning('%s: value %s in attribute %s does not belong into any known container' % (pkey, value, attr)) - continue - - if not remote_entry.get(primary_key): - api.log.warning('%s: there is no primary key %s to migrate for %s' % (pkey, primary_key, attr)) - continue - - api.log.debug('converting DN value %s for %s in %s' % (value, attr, dn)) - rdnval = remote_entry[primary_key][0].lower() - entry_attrs[attr][ind] = DN((primary_key, rdnval), container, api.env.basedn) - - return dn - - -def _post_migrate_user(ldap, pkey, dn, entry_attrs, failed, config, ctx): - assert isinstance(dn, DN) - - if 'def_group_dn' in ctx: - _update_default_group(ldap, ctx, False) - - if 'description' in entry_attrs and NO_UPG_MAGIC in entry_attrs['description']: - entry_attrs['description'].remove(NO_UPG_MAGIC) - try: - update_attrs = ldap.get_entry(dn, ['description']) - update_attrs['description'] = entry_attrs['description'] - ldap.update_entry(update_attrs) - except (errors.EmptyModlist, errors.NotFound): - pass - -def _update_default_group(ldap, ctx, force): - migrate_cnt = ctx['migrate_cnt'] - group_dn = ctx['def_group_dn'] - - # Purposely let this fire when migrate_cnt == 0 so on re-running migration - # it can catch any users migrated but not added to the default group. - if force or migrate_cnt % 100 == 0: - s = datetime.datetime.now() - searchfilter = "(&(objectclass=posixAccount)(!(memberof=%s)))" % group_dn - try: - (result, truncated) = ldap.find_entries(searchfilter, - [''], DN(api.env.container_user, api.env.basedn), - scope=ldap.SCOPE_SUBTREE, time_limit=-1, size_limit=-1) - except errors.NotFound: - api.log.debug('All users have default group set') - return - - member_dns = [m.dn for m in result] - modlist = [(MOD_ADD, 'member', ldap.encode(member_dns))] - try: - with ldap.error_handler(): - ldap.conn.modify_s(str(group_dn), modlist) - except errors.DatabaseError as e: - api.log.error('Adding new members to default group failed: %s \n' - 'members: %s', e, ','.join(member_dns)) - - e = datetime.datetime.now() - d = e - s - mode = " (forced)" if force else "" - api.log.info('Adding %d users to group%s duration %s', - len(member_dns), mode, d) - -# GROUP MIGRATION CALLBACKS AND VARS - -def _pre_migrate_group(ldap, pkey, dn, entry_attrs, failed, config, ctx, **kwargs): - - def convert_members_rfc2307bis(member_attr, search_bases, overwrite=False): - """ - Convert DNs in member attributes to work in IPA. - """ - new_members = [] - entry_attrs.setdefault(member_attr, []) - for m in entry_attrs[member_attr]: - try: - m = DN(m) - except ValueError as e: - # This should be impossible unless the remote server - # doesn't enforce syntax checking. - api.log.error('Malformed DN %s: %s' % (m, e)) - continue - try: - rdnval = m[0].value - except IndexError: - api.log.error('Malformed DN %s has no RDN?' % m) - continue - - if m.endswith(search_bases['user']): - api.log.debug('migrating %s user %s', member_attr, m) - m = DN((api.Object.user.primary_key.name, rdnval), - api.env.container_user, api.env.basedn) - elif m.endswith(search_bases['group']): - api.log.debug('migrating %s group %s', member_attr, m) - m = DN((api.Object.group.primary_key.name, rdnval), - api.env.container_group, api.env.basedn) - else: - api.log.error('entry %s does not belong into any known container' % m) - continue - - new_members.append(m) - - del entry_attrs[member_attr] - if overwrite: - entry_attrs['member'] = [] - entry_attrs['member'] += new_members - - def convert_members_rfc2307(member_attr): - """ - Convert usernames in member attributes to work in IPA. - """ - new_members = [] - entry_attrs.setdefault(member_attr, []) - for m in entry_attrs[member_attr]: - memberdn = DN((api.Object.user.primary_key.name, m), - api.env.container_user, api.env.basedn) - new_members.append(memberdn) - entry_attrs['member'] = new_members - - assert isinstance(dn, DN) - attr_blacklist = ['memberofindirect','memberindirect'] - attr_blacklist.extend(kwargs.get('attr_blacklist', [])) - - schema = kwargs.get('schema', None) - entry_attrs['ipauniqueid'] = 'autogenerate' - if schema == 'RFC2307bis': - search_bases = kwargs.get('search_bases', None) - if not search_bases: - raise ValueError('Search bases not specified') - - convert_members_rfc2307bis('member', search_bases, overwrite=True) - convert_members_rfc2307bis('uniquemember', search_bases) - elif schema == 'RFC2307': - convert_members_rfc2307('memberuid') - else: - raise ValueError('Schema %s not supported' % schema) - - # do not migrate all attributes - for attr in attr_blacklist: - entry_attrs.pop(attr, None) - - # do not migrate all object classes - if 'objectclass' in entry_attrs: - for object_class in kwargs.get('oc_blacklist', []): - try: - entry_attrs['objectclass'].remove(object_class) - except ValueError: # object class not present - pass - - return dn - - -def _group_exc_callback(ldap, dn, entry_attrs, exc, options): - assert isinstance(dn, DN) - if isinstance(exc, errors.DuplicateEntry): - if options.get('groupoverwritegid', False) and \ - entry_attrs.get('gidnumber') is not None: - try: - new_entry_attrs = ldap.get_entry(dn, ['gidnumber']) - new_entry_attrs['gidnumber'] = entry_attrs['gidnumber'] - ldap.update_entry(new_entry_attrs) - except errors.EmptyModlist: - # no change to the GID - pass - # mark as success - return - elif not options.get('groupoverwritegid', False) and \ - entry_attrs.get('gidnumber') is not None: - msg = unicode(exc) - # add information about possibility to overwrite GID - msg = msg + unicode(_('. Check GID of the existing group. ' \ - 'Use --group-overwrite-gid option to overwrite the GID')) - raise errors.DuplicateEntry(message=msg) - - raise exc - -# DS MIGRATION PLUGIN - -def construct_filter(template, oc_list): - oc_subfilter = ''.join([ '(objectclass=%s)' % oc for oc in oc_list]) - return template % oc_subfilter - -def validate_ldapuri(ugettext, ldapuri): - m = re.match('^ldaps?://[-\w\.]+(:\d+)?$', ldapuri) - if not m: - err_msg = _('Invalid LDAP URI.') - raise errors.ValidationError(name='ldap_uri', error=err_msg) - - -@register() -class migrate_ds(Command): - __doc__ = _('Migrate users and groups from DS to IPA.') - - migrate_objects = { - # OBJECT_NAME: (search_filter, pre_callback, post_callback) - # - # OBJECT_NAME - is the name of an LDAPObject subclass - # search_filter - is the filter to retrieve objects from DS - # pre_callback - is called for each object just after it was - # retrieved from DS and before being added to IPA - # post_callback - is called for each object after it was added to IPA - # exc_callback - is called when adding entry to IPA raises an exception - # - # {pre, post}_callback parameters: - # ldap - ldap2 instance connected to IPA - # pkey - primary key value of the object (uid for users, etc.) - # dn - dn of the object as it (will be/is) stored in IPA - # entry_attrs - attributes of the object - # failed - a list of so-far failed objects - # config - IPA config entry attributes - # ctx - object context, used to pass data between callbacks - # - # If pre_callback return value evaluates to False, migration - # of the current object is aborted. - 'user': { - 'filter_template' : '(&(|%s)(uid=*))', - 'oc_option' : 'userobjectclass', - 'oc_blacklist_option' : 'userignoreobjectclass', - 'attr_blacklist_option' : 'userignoreattribute', - 'pre_callback' : _pre_migrate_user, - 'post_callback' : _post_migrate_user, - 'exc_callback' : None - }, - 'group': { - 'filter_template' : '(&(|%s)(cn=*))', - 'oc_option' : 'groupobjectclass', - 'oc_blacklist_option' : 'groupignoreobjectclass', - 'attr_blacklist_option' : 'groupignoreattribute', - 'pre_callback' : _pre_migrate_group, - 'post_callback' : None, - 'exc_callback' : _group_exc_callback, - }, - } - migrate_order = ('user', 'group') - - takes_args = ( - Str('ldapuri', validate_ldapuri, - cli_name='ldap_uri', - label=_('LDAP URI'), - doc=_('LDAP URI of DS server to migrate from'), - ), - Password('bindpw', - cli_name='password', - label=_('Password'), - confirm=False, - doc=_('bind password'), - ), - ) - - takes_options = ( - DNParam('binddn?', - cli_name='bind_dn', - label=_('Bind DN'), - default=DN(('cn', 'directory manager')), - autofill=True, - ), - DNParam('usercontainer', - cli_name='user_container', - label=_('User container'), - doc=_('DN of container for users in DS relative to base DN'), - default=DN(('ou', 'people')), - autofill=True, - ), - DNParam('groupcontainer', - cli_name='group_container', - label=_('Group container'), - doc=_('DN of container for groups in DS relative to base DN'), - default=DN(('ou', 'groups')), - autofill=True, - ), - Str('userobjectclass+', - cli_name='user_objectclass', - label=_('User object class'), - doc=_('Objectclasses used to search for user entries in DS'), - default=(u'person',), - autofill=True, - ), - Str('groupobjectclass+', - cli_name='group_objectclass', - label=_('Group object class'), - doc=_('Objectclasses used to search for group entries in DS'), - default=(u'groupOfUniqueNames', u'groupOfNames'), - autofill=True, - ), - Str('userignoreobjectclass*', - cli_name='user_ignore_objectclass', - label=_('Ignore user object class'), - doc=_('Objectclasses to be ignored for user entries in DS'), - default=tuple(), - autofill=True, - ), - Str('userignoreattribute*', - cli_name='user_ignore_attribute', - label=_('Ignore user attribute'), - doc=_('Attributes to be ignored for user entries in DS'), - default=tuple(), - autofill=True, - ), - Str('groupignoreobjectclass*', - cli_name='group_ignore_objectclass', - label=_('Ignore group object class'), - doc=_('Objectclasses to be ignored for group entries in DS'), - default=tuple(), - autofill=True, - ), - Str('groupignoreattribute*', - cli_name='group_ignore_attribute', - label=_('Ignore group attribute'), - doc=_('Attributes to be ignored for group entries in DS'), - default=tuple(), - autofill=True, - ), - Flag('groupoverwritegid', - cli_name='group_overwrite_gid', - label=_('Overwrite GID'), - doc=_('When migrating a group already existing in IPA domain overwrite the '\ - 'group GID and report as success'), - ), - StrEnum('schema?', - cli_name='schema', - label=_('LDAP schema'), - doc=_('The schema used on the LDAP server. Supported values are RFC2307 and RFC2307bis. The default is RFC2307bis'), - values=_supported_schemas, - default=_supported_schemas[0], - autofill=True, - ), - Flag('continue?', - label=_('Continue'), - doc=_('Continuous operation mode. Errors are reported but the process continues'), - default=False, - ), - DNParam('basedn?', - cli_name='base_dn', - label=_('Base DN'), - doc=_('Base DN on remote LDAP server'), - ), - Flag('compat?', - cli_name='with_compat', - label=_('Ignore compat plugin'), - doc=_('Allows migration despite the usage of compat plugin'), - default=False, - ), - Str('cacertfile?', - cli_name='ca_cert_file', - label=_('CA certificate'), - doc=_('Load CA certificate of LDAP server from FILE'), - default=None, - noextrawhitespace=False, - ), - Bool('use_def_group?', - cli_name='use_default_group', - label=_('Add to default group'), - doc=_('Add migrated users without a group to a default group ' - '(default: true)'), - default=True, - autofill=True, - ), - StrEnum('scope', - cli_name='scope', - label=_('Search scope'), - doc=_('LDAP search scope for users and groups: base, onelevel, or ' - 'subtree. Defaults to onelevel'), - values=tuple(_supported_scopes.keys()), - default=_default_scope, - autofill=True, - ), - ) - - has_output = ( - output.Output('result', - type=dict, - doc=_('Lists of objects migrated; categorized by type.'), - ), - output.Output('failed', - type=dict, - doc=_('Lists of objects that could not be migrated; categorized by type.'), - ), - output.Output('enabled', - type=bool, - doc=_('False if migration mode was disabled.'), - ), - output.Output('compat', - type=bool, - doc=_('False if migration fails because the compatibility plug-in is enabled.'), - ), - ) - - exclude_doc = _('%s to exclude from migration') - - truncated_err_msg = _('''\ -search results for objects to be migrated -have been truncated by the server; -migration process might be incomplete\n''') - - def get_options(self): - """ - Call get_options of the baseclass and add "exclude" options - for each type of object being migrated. - """ - for option in super(migrate_ds, self).get_options(): - yield option - for ldap_obj_name in self.migrate_objects: - ldap_obj = self.api.Object[ldap_obj_name] - name = 'exclude_%ss' % to_cli(ldap_obj_name) - doc = self.exclude_doc % ldap_obj.object_name_plural - yield Str( - '%s*' % name, cli_name=name, doc=doc, default=tuple(), - autofill=True - ) - - def normalize_options(self, options): - """ - Convert all "exclude" option values to lower-case. - - Also, empty List parameters are converted to None, but the migration - plugin doesn't like that - convert back to empty lists. - """ - names = ['userobjectclass', 'groupobjectclass', - 'userignoreobjectclass', 'userignoreattribute', - 'groupignoreobjectclass', 'groupignoreattribute'] - names.extend('exclude_%ss' % to_cli(n) for n in self.migrate_objects) - for name in names: - if options[name]: - options[name] = tuple( - v.lower() for v in options[name] - ) - else: - options[name] = tuple() - - def _get_search_bases(self, options, ds_base_dn, migrate_order): - search_bases = dict() - for ldap_obj_name in migrate_order: - container = options.get('%scontainer' % to_cli(ldap_obj_name)) - if container: - # Don't append base dn if user already appended it in the container dn - if container.endswith(ds_base_dn): - search_base = container - else: - search_base = DN(container, ds_base_dn) - else: - search_base = ds_base_dn - search_bases[ldap_obj_name] = search_base - return search_bases - - def migrate(self, ldap, config, ds_ldap, ds_base_dn, options): - """ - Migrate objects from DS to LDAP. - """ - assert isinstance(ds_base_dn, DN) - migrated = {} # {'OBJ': ['PKEY1', 'PKEY2', ...], ...} - failed = {} # {'OBJ': {'PKEY1': 'Failed 'cos blabla', ...}, ...} - search_bases = self._get_search_bases(options, ds_base_dn, self.migrate_order) - migration_start = datetime.datetime.now() - - scope = _supported_scopes[options.get('scope')] - - for ldap_obj_name in self.migrate_order: - ldap_obj = self.api.Object[ldap_obj_name] - - template = self.migrate_objects[ldap_obj_name]['filter_template'] - oc_list = options[to_cli(self.migrate_objects[ldap_obj_name]['oc_option'])] - search_filter = construct_filter(template, oc_list) - - exclude = options['exclude_%ss' % to_cli(ldap_obj_name)] - context = dict(ds_ldap = ds_ldap) - - migrated[ldap_obj_name] = [] - failed[ldap_obj_name] = {} - - try: - entries, truncated = ds_ldap.find_entries( - search_filter, ['*'], search_bases[ldap_obj_name], - scope, - time_limit=0, size_limit=-1, - search_refs=True # migrated DS may contain search references - ) - except errors.NotFound: - if not options.get('continue',False): - raise errors.NotFound( - reason=_('%(container)s LDAP search did not return any result ' - '(search base: %(search_base)s, ' - 'objectclass: %(objectclass)s)') - % {'container': ldap_obj_name, - 'search_base': search_bases[ldap_obj_name], - 'objectclass': ', '.join(oc_list)} - ) - else: - truncated = False - entries = [] - if truncated: - self.log.error( - '%s: %s' % ( - ldap_obj.name, self.truncated_err_msg - ) - ) - - blacklists = {} - for blacklist in ('oc_blacklist', 'attr_blacklist'): - blacklist_option = self.migrate_objects[ldap_obj_name][blacklist+'_option'] - if blacklist_option is not None: - blacklists[blacklist] = options.get(blacklist_option, tuple()) - else: - blacklists[blacklist] = tuple() - - # get default primary group for new users - if 'def_group_dn' not in context and options.get('use_def_group'): - def_group = config.get('ipadefaultprimarygroup') - context['def_group_dn'] = api.Object.group.get_dn(def_group) - try: - ldap.get_entry(context['def_group_dn'], ['gidnumber', 'cn']) - except errors.NotFound: - error_msg = _('Default group for new users not found') - raise errors.NotFound(reason=error_msg) - - context['has_upg'] = ldap.has_upg() - - valid_gids = set() - invalid_gids = set() - migrate_cnt = 0 - context['migrate_cnt'] = 0 - for entry_attrs in entries: - context['migrate_cnt'] = migrate_cnt - s = datetime.datetime.now() - - ava = entry_attrs.dn[0][0] - if ava.attr == ldap_obj.primary_key.name: - # In case if pkey attribute is in the migrated object DN - # and the original LDAP is multivalued, make sure that - # we pick the correct value (the unique one stored in DN) - pkey = ava.value.lower() - else: - pkey = entry_attrs[ldap_obj.primary_key.name][0].lower() - - if pkey in exclude: - continue - - entry_attrs.dn = ldap_obj.get_dn(pkey) - entry_attrs['objectclass'] = list( - set( - config.get( - ldap_obj.object_class_config, ldap_obj.object_class - ) + [o.lower() for o in entry_attrs['objectclass']] - ) - ) - entry_attrs[ldap_obj.primary_key.name][0] = entry_attrs[ldap_obj.primary_key.name][0].lower() - - callback = self.migrate_objects[ldap_obj_name]['pre_callback'] - if callable(callback): - try: - entry_attrs.dn = callback( - ldap, pkey, entry_attrs.dn, entry_attrs, - failed[ldap_obj_name], config, context, - schema=options['schema'], - search_bases=search_bases, - valid_gids=valid_gids, - invalid_gids=invalid_gids, - **blacklists - ) - if not entry_attrs.dn: - continue - except errors.NotFound as e: - failed[ldap_obj_name][pkey] = unicode(e.reason) - continue - - try: - ldap.add_entry(entry_attrs) - except errors.ExecutionError as e: - callback = self.migrate_objects[ldap_obj_name]['exc_callback'] - if callable(callback): - try: - callback( - ldap, entry_attrs.dn, entry_attrs, e, options) - except errors.ExecutionError as e: - failed[ldap_obj_name][pkey] = unicode(e) - continue - else: - failed[ldap_obj_name][pkey] = unicode(e) - continue - - migrated[ldap_obj_name].append(pkey) - - callback = self.migrate_objects[ldap_obj_name]['post_callback'] - if callable(callback): - callback( - ldap, pkey, entry_attrs.dn, entry_attrs, - failed[ldap_obj_name], config, context) - e = datetime.datetime.now() - d = e - s - total_dur = e - migration_start - migrate_cnt += 1 - if migrate_cnt > 0 and migrate_cnt % 100 == 0: - api.log.info("%d %ss migrated. %s elapsed." % (migrate_cnt, ldap_obj_name, total_dur)) - api.log.debug("%d %ss migrated, duration: %s (total %s)" % (migrate_cnt, ldap_obj_name, d, total_dur)) - - if 'def_group_dn' in context: - _update_default_group(ldap, context, True) - - return (migrated, failed) - - def execute(self, ldapuri, bindpw, **options): - ldap = self.api.Backend.ldap2 - self.normalize_options(options) - config = ldap.get_ipa_config() - - ds_base_dn = options.get('basedn') - if ds_base_dn is not None: - assert isinstance(ds_base_dn, DN) - - # check if migration mode is enabled - if config.get('ipamigrationenabled', ('FALSE', ))[0] == 'FALSE': - return dict(result={}, failed={}, enabled=False, compat=True) - - # connect to DS - ds_ldap = ldap2(self.api, ldap_uri=ldapuri) - - cacert = None - if options.get('cacertfile') is not None: - #store CA cert into file - tmp_ca_cert_f = write_tmp_file(options['cacertfile']) - cacert = tmp_ca_cert_f.name - - #start TLS connection - ds_ldap.connect(bind_dn=options['binddn'], bind_pw=bindpw, - tls_cacertfile=cacert) - - tmp_ca_cert_f.close() - else: - ds_ldap.connect(bind_dn=options['binddn'], bind_pw=bindpw) - - #check whether the compat plugin is enabled - if not options.get('compat'): - try: - ldap.get_entry(DN(('cn', 'compat'), (api.env.basedn))) - return dict(result={}, failed={}, enabled=True, compat=False) - except errors.NotFound: - pass - - if not ds_base_dn: - # retrieve base DN from remote LDAP server - entries, truncated = ds_ldap.find_entries( - '', ['namingcontexts', 'defaultnamingcontext'], DN(''), - ds_ldap.SCOPE_BASE, size_limit=-1, time_limit=0, - ) - if 'defaultnamingcontext' in entries[0]: - ds_base_dn = DN(entries[0]['defaultnamingcontext'][0]) - assert isinstance(ds_base_dn, DN) - else: - try: - ds_base_dn = DN(entries[0]['namingcontexts'][0]) - assert isinstance(ds_base_dn, DN) - except (IndexError, KeyError) as e: - raise Exception(str(e)) - - # migrate! - (migrated, failed) = self.migrate( - ldap, config, ds_ldap, ds_base_dn, options - ) - - return dict(result=migrated, failed=failed, enabled=True, compat=True) diff --git a/ipalib/plugins/misc.py b/ipalib/plugins/misc.py deleted file mode 100644 index 0628bb19b..000000000 --- a/ipalib/plugins/misc.py +++ /dev/null @@ -1,138 +0,0 @@ -# Authors: -# Jason Gerard DeRose <jderose@redhat.com> -# -# Copyright (C) 2008 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/>. - -import re -from ipalib import LocalOrRemote, _, ngettext -from ipalib.output import Output, summary -from ipalib import Flag -from ipalib.plugable import Registry - -__doc__ = _(""" -Misc plug-ins -""") - -register = Registry() - -# FIXME: We should not let env return anything in_server -# when mode == 'production'. This would allow an attacker to see the -# configuration of the server, potentially revealing compromising -# information. However, it's damn handy for testing/debugging. - - -@register() -class env(LocalOrRemote): - __doc__ = _('Show environment variables.') - - msg_summary = _('%(count)d variables') - - takes_args = ( - 'variables*', - ) - - takes_options = LocalOrRemote.takes_options + ( - Flag('all', - cli_name='all', - doc=_('retrieve and print all attributes from the server. Affects command output.'), - exclude='webui', - flags=['no_option', 'no_output'], - default=True, - ), - ) - - has_output = ( - Output('result', - type=dict, - doc=_('Dictionary mapping variable name to value'), - ), - Output('total', - type=int, - doc=_('Total number of variables env (>= count)'), - flags=['no_display'], - ), - Output('count', - type=int, - doc=_('Number of variables returned (<= total)'), - flags=['no_display'], - ), - summary, - ) - - def __find_keys(self, variables): - keys = set() - for query in variables: - if '*' in query: - pat = re.compile(query.replace('*', '.*') + '$') - for key in self.env: - if pat.match(key): - keys.add(key) - elif query in self.env: - keys.add(query) - return keys - - def execute(self, variables=None, **options): - if variables is None: - keys = self.env - else: - keys = self.__find_keys(variables) - ret = dict( - result=dict( - (key, self.env[key]) for key in keys - ), - count=len(keys), - total=len(self.env), - ) - if len(keys) > 1: - ret['summary'] = self.msg_summary % ret - else: - ret['summary'] = None - return ret - - - -@register() -class plugins(LocalOrRemote): - __doc__ = _('Show all loaded plugins.') - - msg_summary = ngettext( - '%(count)d plugin loaded', '%(count)d plugins loaded', 0 - ) - - takes_options = LocalOrRemote.takes_options + ( - Flag('all', - cli_name='all', - doc=_('retrieve and print all attributes from the server. Affects command output.'), - exclude='webui', - flags=['no_option', 'no_output'], - default=True, - ), - ) - - has_output = ( - Output('result', dict, 'Dictionary mapping plugin names to bases'), - Output('count', - type=int, - doc=_('Number of plugins loaded'), - ), - summary, - ) - - def execute(self, **options): - return dict( - result=dict(self.api.plugins), - ) diff --git a/ipalib/plugins/netgroup.py b/ipalib/plugins/netgroup.py deleted file mode 100644 index f76a0ba3a..000000000 --- a/ipalib/plugins/netgroup.py +++ /dev/null @@ -1,387 +0,0 @@ -# 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/>. - -import six - -from ipalib import api, errors -from ipalib import Str, StrEnum, Flag -from ipalib.plugable import Registry -from .baseldap import ( - external_host_param, - add_external_pre_callback, - add_external_post_callback, - remove_external_post_callback, - LDAPObject, - LDAPCreate, - LDAPDelete, - LDAPUpdate, - LDAPSearch, - LDAPRetrieve, - LDAPAddMember, - LDAPRemoveMember) -from ipalib import _, ngettext -from .hbacrule import is_all -from ipapython.dn import DN - -if six.PY3: - unicode = str - -__doc__ = _(""" -Netgroups - -A netgroup is a group used for permission checking. It can contain both -user and host values. - -EXAMPLES: - - Add a new netgroup: - ipa netgroup-add --desc="NFS admins" admins - - Add members to the netgroup: - ipa netgroup-add-member --users=tuser1 --users=tuser2 admins - - Remove a member from the netgroup: - ipa netgroup-remove-member --users=tuser2 admins - - Display information about a netgroup: - ipa netgroup-show admins - - Delete a netgroup: - ipa netgroup-del admins -""") - -register = Registry() - -NETGROUP_PATTERN='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]*$' -NETGROUP_PATTERN_ERRMSG='may only include letters, numbers, _, -, and .' - -# according to most common use cases the netgroup pattern should fit -# also the nisdomain pattern -NISDOMAIN_PATTERN=NETGROUP_PATTERN -NISDOMAIN_PATTERN_ERRMSG=NETGROUP_PATTERN_ERRMSG - -output_params = ( - Str('memberuser_user?', - label='Member User', - ), - Str('memberuser_group?', - label='Member Group', - ), - Str('memberhost_host?', - label=_('Member Host'), - ), - Str('memberhost_hostgroup?', - label='Member Hostgroup', - ), - ) - - -@register() -class netgroup(LDAPObject): - """ - Netgroup object. - """ - container_dn = api.env.container_netgroup - object_name = _('netgroup') - object_name_plural = _('netgroups') - object_class = ['ipaobject', 'ipaassociation', 'ipanisnetgroup'] - permission_filter_objectclasses = ['ipanisnetgroup'] - search_attributes = [ - 'cn', 'description', 'memberof', 'externalhost', 'nisdomainname', - 'memberuser', 'memberhost', 'member', 'usercategory', 'hostcategory', - ] - default_attributes = [ - 'cn', 'description', 'memberof', 'externalhost', 'nisdomainname', - 'memberuser', 'memberhost', 'member', 'memberindirect', - 'usercategory', 'hostcategory', - ] - uuid_attribute = 'ipauniqueid' - rdn_attribute = 'ipauniqueid' - attribute_members = { - 'member': ['netgroup'], - 'memberof': ['netgroup'], - 'memberindirect': ['netgroup'], - 'memberuser': ['user', 'group'], - 'memberhost': ['host', 'hostgroup'], - } - relationships = { - 'member': ('Member', '', 'no_'), - 'memberof': ('Member Of', 'in_', 'not_in_'), - 'memberindirect': ( - 'Indirect Member', None, 'no_indirect_' - ), - 'memberuser': ('Member', '', 'no_'), - 'memberhost': ('Member', '', 'no_'), - } - managed_permissions = { - 'System: Read Netgroups': { - 'replaces_global_anonymous_aci': True, - 'ipapermbindruletype': 'all', - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'cn', 'description', 'hostcategory', 'ipaenabledflag', - 'ipauniqueid', 'nisdomainname', 'usercategory', 'objectclass', - }, - }, - 'System: Read Netgroup Membership': { - 'replaces_global_anonymous_aci': True, - 'ipapermbindruletype': 'all', - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'externalhost', 'member', 'memberof', 'memberuser', - 'memberhost', 'objectclass', - }, - }, - 'System: Add Netgroups': { - 'ipapermright': {'add'}, - 'replaces': [ - '(target = "ldap:///ipauniqueid=*,cn=ng,cn=alt,$SUFFIX")(version 3.0;acl "permission:Add netgroups";allow (add) groupdn = "ldap:///cn=Add netgroups,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Netgroups Administrators'}, - }, - 'System: Modify Netgroup Membership': { - 'ipapermright': {'write'}, - 'ipapermdefaultattr': { - 'externalhost', 'member', 'memberhost', 'memberuser' - }, - 'replaces': [ - '(targetattr = "memberhost || externalhost || memberuser || member")(target = "ldap:///ipauniqueid=*,cn=ng,cn=alt,$SUFFIX")(version 3.0;acl "permission:Modify netgroup membership";allow (write) groupdn = "ldap:///cn=Modify netgroup membership,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Netgroups Administrators'}, - }, - 'System: Modify Netgroups': { - 'ipapermright': {'write'}, - 'ipapermdefaultattr': {'description'}, - 'replaces': [ - '(targetattr = "description")(target = "ldap:///ipauniqueid=*,cn=ng,cn=alt,$SUFFIX")(version 3.0; acl "permission:Modify netgroups";allow (write) groupdn = "ldap:///cn=Modify netgroups,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Netgroups Administrators'}, - }, - 'System: Remove Netgroups': { - 'ipapermright': {'delete'}, - 'replaces': [ - '(target = "ldap:///ipauniqueid=*,cn=ng,cn=alt,$SUFFIX")(version 3.0;acl "permission:Remove netgroups";allow (delete) groupdn = "ldap:///cn=Remove netgroups,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Netgroups Administrators'}, - }, - 'System: Read Netgroup Compat Tree': { - 'non_object': True, - 'ipapermbindruletype': 'anonymous', - 'ipapermlocation': api.env.basedn, - 'ipapermtarget': DN('cn=ng', 'cn=compat', api.env.basedn), - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'objectclass', 'cn', 'membernisnetgroup', 'nisnetgrouptriple', - }, - }, - } - - label = _('Netgroups') - label_singular = _('Netgroup') - - takes_params = ( - Str('cn', - pattern=NETGROUP_PATTERN, - pattern_errmsg=NETGROUP_PATTERN_ERRMSG, - cli_name='name', - label=_('Netgroup name'), - primary_key=True, - normalizer=lambda value: value.lower(), - ), - Str('description?', - cli_name='desc', - label=_('Description'), - doc=_('Netgroup description'), - ), - Str('nisdomainname?', - pattern=NISDOMAIN_PATTERN, - pattern_errmsg=NISDOMAIN_PATTERN_ERRMSG, - cli_name='nisdomain', - label=_('NIS domain name'), - ), - Str('ipauniqueid?', - cli_name='uuid', - label='IPA unique ID', - doc=_('IPA unique ID'), - flags=['no_create', 'no_update'], - ), - StrEnum('usercategory?', - cli_name='usercat', - label=_('User category'), - doc=_('User category the rule applies to'), - values=(u'all', ), - ), - StrEnum('hostcategory?', - cli_name='hostcat', - label=_('Host category'), - doc=_('Host category the rule applies to'), - values=(u'all', ), - ), - external_host_param, - ) - - -@register() -class netgroup_add(LDAPCreate): - __doc__ = _('Add a new netgroup.') - - has_output_params = LDAPCreate.has_output_params + output_params - msg_summary = _('Added netgroup "%(value)s"') - - msg_collision = _(u'hostgroup with name "%s" already exists. ' \ - u'Hostgroups and netgroups share a common namespace') - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - entry_attrs.setdefault('nisdomainname', self.api.env.domain) - - try: - test_dn = self.obj.get_dn(keys[-1]) - netgroup = ldap.get_entry(test_dn, ['objectclass']) - if 'mepManagedEntry' in netgroup.get('objectclass', []): - raise errors.DuplicateEntry(message=unicode(self.msg_collision % keys[-1])) - else: - self.obj.handle_duplicate_entry(*keys) - except errors.NotFound: - pass - - try: - # when enabled, a managed netgroup is created for every hostgroup - # make sure that we don't create a collision if the plugin is - # (temporarily) disabled - api.Object['hostgroup'].get_dn_if_exists(keys[-1]) - raise errors.DuplicateEntry(message=unicode(self.msg_collision % keys[-1])) - except errors.NotFound: - pass - - return dn - - -@register() -class netgroup_del(LDAPDelete): - __doc__ = _('Delete a netgroup.') - - msg_summary = _('Deleted netgroup "%(value)s"') - - - -@register() -class netgroup_mod(LDAPUpdate): - __doc__ = _('Modify a netgroup.') - - has_output_params = LDAPUpdate.has_output_params + output_params - msg_summary = _('Modified netgroup "%(value)s"') - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - try: - entry_attrs = ldap.get_entry(dn, attrs_list) - dn = entry_attrs.dn - except errors.NotFound: - self.obj.handle_not_found(*keys) - if is_all(options, 'usercategory') and 'memberuser' in entry_attrs: - raise errors.MutuallyExclusiveError(reason=_("user category cannot be set to 'all' while there are allowed users")) - if is_all(options, 'hostcategory') and 'memberhost' in entry_attrs: - raise errors.MutuallyExclusiveError(reason=_("host category cannot be set to 'all' while there are allowed hosts")) - return dn - - -@register() -class netgroup_find(LDAPSearch): - __doc__ = _('Search for a netgroup.') - - member_attributes = ['member', 'memberuser', 'memberhost', 'memberof'] - has_output_params = LDAPSearch.has_output_params + output_params - msg_summary = ngettext( - '%(count)d netgroup matched', '%(count)d netgroups matched', 0 - ) - - takes_options = LDAPSearch.takes_options + ( - Flag('private', - exclude='webui', - flags=['no_option', 'no_output'], - ), - Flag('managed', - cli_name='managed', - doc=_('search for managed groups'), - default_from=lambda private: private, - ), - ) - - def pre_callback(self, ldap, filter, attrs_list, base_dn, scope, *args, **options): - assert isinstance(base_dn, DN) - # Do not display private mepManagedEntry netgroups by default - # If looking for managed groups, we need to omit the negation search filter - - search_kw = {} - search_kw['objectclass'] = ['mepManagedEntry'] - if not options['managed']: - local_filter = ldap.make_filter(search_kw, rules=ldap.MATCH_NONE) - else: - local_filter = ldap.make_filter(search_kw, rules=ldap.MATCH_ALL) - filter = ldap.combine_filters((local_filter, filter), rules=ldap.MATCH_ALL) - return (filter, base_dn, scope) - - -@register() -class netgroup_show(LDAPRetrieve): - __doc__ = _('Display information about a netgroup.') - - has_output_params = LDAPRetrieve.has_output_params + output_params - - -@register() -class netgroup_add_member(LDAPAddMember): - __doc__ = _('Add members to a netgroup.') - - member_attributes = ['memberuser', 'memberhost', 'member'] - has_output_params = LDAPAddMember.has_output_params + output_params - - def pre_callback(self, ldap, dn, found, not_found, *keys, **options): - assert isinstance(dn, DN) - return add_external_pre_callback('host', ldap, dn, keys, options) - - def post_callback(self, ldap, completed, failed, dn, entry_attrs, - *keys, **options): - assert isinstance(dn, DN) - return add_external_post_callback(ldap, dn, entry_attrs, - failed=failed, - completed=completed, - memberattr='memberhost', - membertype='host', - externalattr='externalhost') - - -@register() -class netgroup_remove_member(LDAPRemoveMember): - __doc__ = _('Remove members from a netgroup.') - - member_attributes = ['memberuser', 'memberhost', 'member'] - has_output_params = LDAPRemoveMember.has_output_params + output_params - - def post_callback(self, ldap, completed, failed, dn, entry_attrs, - *keys, **options): - assert isinstance(dn, DN) - return remove_external_post_callback(ldap, dn, entry_attrs, - failed=failed, - completed=completed, - memberattr='memberhost', - membertype='host', - externalattr='externalhost') diff --git a/ipalib/plugins/otp.py b/ipalib/plugins/otp.py deleted file mode 100644 index 306c87388..000000000 --- a/ipalib/plugins/otp.py +++ /dev/null @@ -1,7 +0,0 @@ -# -# Copyright (C) 2016 FreeIPA Contributors see COPYING for license -# - -from ipalib.text import _ - -__doc__ = _('One time password commands') diff --git a/ipalib/plugins/otpconfig.py b/ipalib/plugins/otpconfig.py deleted file mode 100644 index c7710468f..000000000 --- a/ipalib/plugins/otpconfig.py +++ /dev/null @@ -1,121 +0,0 @@ -# Authors: -# Nathaniel McCallum <npmccallum@redhat.com> -# -# Copyright (C) 2014 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/>. - -from ipalib import _, api, Int -from ipalib.plugable import Registry -from .baseldap import DN, LDAPObject, LDAPUpdate, LDAPRetrieve - -__doc__ = _(""" -OTP configuration - -Manage the default values that IPA uses for OTP tokens. - -EXAMPLES: - - Show basic OTP configuration: - ipa otpconfig-show - - Show all OTP configuration options: - ipa otpconfig-show --all - - Change maximum TOTP authentication window to 10 minutes: - ipa otpconfig-mod --totp-auth-window=600 - - Change maximum TOTP synchronization window to 12 hours: - ipa otpconfig-mod --totp-sync-window=43200 - - Change maximum HOTP authentication window to 5: - ipa hotpconfig-mod --hotp-auth-window=5 - - Change maximum HOTP synchronization window to 50: - ipa hotpconfig-mod --hotp-sync-window=50 -""") - -register = Registry() - -topic = 'otp' - - -@register() -class otpconfig(LDAPObject): - object_name = _('OTP configuration options') - default_attributes = [ - 'ipatokentotpauthwindow', - 'ipatokentotpsyncwindow', - 'ipatokenhotpauthwindow', - 'ipatokenhotpsyncwindow', - ] - - container_dn = DN(('cn', 'otp'), ('cn', 'etc')) - permission_filter_objectclasses = ['ipatokenotpconfig'] - managed_permissions = { - 'System: Read OTP Configuration': { - 'replaces_global_anonymous_aci': True, - 'ipapermbindruletype': 'all', - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'ipatokentotpauthwindow', 'ipatokentotpsyncwindow', - 'ipatokenhotpauthwindow', 'ipatokenhotpsyncwindow', - 'cn', - }, - }, - } - - label = _('OTP Configuration') - label_singular = _('OTP Configuration') - - takes_params = ( - Int('ipatokentotpauthwindow', - cli_name='totp_auth_window', - label=_('TOTP authentication Window'), - doc=_('TOTP authentication time variance (seconds)'), - minvalue=5, - ), - Int('ipatokentotpsyncwindow', - cli_name='totp_sync_window', - label=_('TOTP Synchronization Window'), - doc=_('TOTP synchronization time variance (seconds)'), - minvalue=5, - ), - Int('ipatokenhotpauthwindow', - cli_name='hotp_auth_window', - label=_('HOTP Authentication Window'), - doc=_('HOTP authentication skip-ahead'), - minvalue=1, - ), - Int('ipatokenhotpsyncwindow', - cli_name='hotp_sync_window', - label=_('HOTP Synchronization Window'), - doc=_('HOTP synchronization skip-ahead'), - minvalue=1, - ), - ) - - def get_dn(self, *keys, **kwargs): - return self.container_dn + api.env.basedn - - -@register() -class otpconfig_mod(LDAPUpdate): - __doc__ = _('Modify OTP configuration options.') - - -@register() -class otpconfig_show(LDAPRetrieve): - __doc__ = _('Show the current OTP configuration.') diff --git a/ipalib/plugins/otptoken.py b/ipalib/plugins/otptoken.py deleted file mode 100644 index fda05ce0b..000000000 --- a/ipalib/plugins/otptoken.py +++ /dev/null @@ -1,464 +0,0 @@ -# Authors: -# Nathaniel McCallum <npmccallum@redhat.com> -# -# 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 -# 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/>. - -from .baseldap import LDAPObject, LDAPAddMember, LDAPRemoveMember -from .baseldap import LDAPCreate, LDAPDelete, LDAPUpdate, LDAPSearch, LDAPRetrieve -from ipalib import api, Int, Str, Bool, DateTime, Flag, Bytes, IntEnum, StrEnum, _, ngettext -from ipalib.plugable import Registry -from ipalib.errors import ( - PasswordMismatch, - ConversionError, - NotFound, - ValidationError) -from ipalib.request import context -from ipapython.dn import DN - -import base64 -import uuid -import os - -import six -from six.moves import urllib - -if six.PY3: - unicode = str - -__doc__ = _(""" -OTP Tokens -""") + _(""" -Manage OTP tokens. -""") + _(""" -IPA supports the use of OTP tokens for multi-factor authentication. This -code enables the management of OTP tokens. -""") + _(""" -EXAMPLES: -""") + _(""" - Add a new token: - ipa otptoken-add --type=totp --owner=jdoe --desc="My soft token" -""") + _(""" - Examine the token: - ipa otptoken-show a93db710-a31a-4639-8647-f15b2c70b78a -""") + _(""" - Change the vendor: - ipa otptoken-mod a93db710-a31a-4639-8647-f15b2c70b78a --vendor="Red Hat" -""") + _(""" - Delete a token: - ipa otptoken-del a93db710-a31a-4639-8647-f15b2c70b78a -""") - -register = Registry() - -topic = 'otp' - -TOKEN_TYPES = { - u'totp': ['ipatokentotpclockoffset', 'ipatokentotptimestep'], - u'hotp': ['ipatokenhotpcounter'] -} - -# NOTE: For maximum compatibility, KEY_LENGTH % 5 == 0 -KEY_LENGTH = 20 - -class OTPTokenKey(Bytes): - """A binary password type specified in base32.""" - - password = True - - kwargs = Bytes.kwargs + ( - ('confirm', bool, True), - ) - - def _convert_scalar(self, value, index=None): - if isinstance(value, (tuple, list)) and len(value) == 2: - (p1, p2) = value - if p1 != p2: - raise PasswordMismatch(name=self.name) - value = p1 - - if isinstance(value, unicode): - try: - value = base64.b32decode(value, True) - except TypeError as e: - raise ConversionError(name=self.name, error=str(e)) - - return super(OTPTokenKey, self)._convert_scalar(value) - -def _convert_owner(userobj, entry_attrs, options): - if 'ipatokenowner' in entry_attrs and not options.get('raw', False): - entry_attrs['ipatokenowner'] = [userobj.get_primary_key_from_dn(o) - for o in entry_attrs['ipatokenowner']] - -def _normalize_owner(userobj, entry_attrs): - owner = entry_attrs.get('ipatokenowner', None) - if owner: - try: - entry_attrs['ipatokenowner'] = userobj._normalize_manager(owner)[0] - except NotFound: - userobj.handle_not_found(owner) - -def _check_interval(not_before, not_after): - if not_before and not_after: - return not_before <= not_after - return True - -def _set_token_type(entry_attrs, **options): - klasses = [x.lower() for x in entry_attrs.get('objectclass', [])] - for ttype in TOKEN_TYPES.keys(): - cls = 'ipatoken' + ttype - if cls.lower() in klasses: - entry_attrs['type'] = ttype.upper() - - if not options.get('all', False) or options.get('pkey_only', False): - entry_attrs.pop('objectclass', None) - -@register() -class otptoken(LDAPObject): - """ - OTP Token object. - """ - container_dn = api.env.container_otp - object_name = _('OTP token') - object_name_plural = _('OTP tokens') - object_class = ['ipatoken'] - possible_objectclasses = ['ipatokentotp', 'ipatokenhotp'] - default_attributes = [ - 'ipatokenuniqueid', 'description', 'ipatokenowner', - 'ipatokendisabled', 'ipatokennotbefore', 'ipatokennotafter', - 'ipatokenvendor', 'ipatokenmodel', 'ipatokenserial', 'managedby' - ] - attribute_members = { - 'managedby': ['user'], - } - relationships = { - 'managedby': ('Managed by', 'man_by_', 'not_man_by_'), - } - rdn_is_primary_key = True - - label = _('OTP Tokens') - label_singular = _('OTP Token') - - takes_params = ( - Str('ipatokenuniqueid', - cli_name='id', - label=_('Unique ID'), - primary_key=True, - flags=('optional_create'), - ), - StrEnum('type?', - label=_('Type'), - doc=_('Type of the token'), - default=u'totp', - autofill=True, - values=tuple(list(TOKEN_TYPES) + [x.upper() for x in TOKEN_TYPES]), - flags=('virtual_attribute', 'no_update'), - ), - Str('description?', - cli_name='desc', - label=_('Description'), - doc=_('Token description (informational only)'), - ), - Str('ipatokenowner?', - cli_name='owner', - label=_('Owner'), - doc=_('Assigned user of the token (default: self)'), - ), - Str('managedby_user?', - label=_('Manager'), - doc=_('Assigned manager of the token (default: self)'), - flags=['no_create', 'no_update', 'no_search'], - ), - Bool('ipatokendisabled?', - cli_name='disabled', - label=_('Disabled'), - doc=_('Mark the token as disabled (default: false)') - ), - DateTime('ipatokennotbefore?', - cli_name='not_before', - label=_('Validity start'), - doc=_('First date/time the token can be used'), - ), - DateTime('ipatokennotafter?', - cli_name='not_after', - label=_('Validity end'), - doc=_('Last date/time the token can be used'), - ), - Str('ipatokenvendor?', - cli_name='vendor', - label=_('Vendor'), - doc=_('Token vendor name (informational only)'), - ), - Str('ipatokenmodel?', - cli_name='model', - label=_('Model'), - doc=_('Token model (informational only)'), - ), - Str('ipatokenserial?', - cli_name='serial', - label=_('Serial'), - doc=_('Token serial (informational only)'), - ), - OTPTokenKey('ipatokenotpkey?', - cli_name='key', - label=_('Key'), - doc=_('Token secret (Base32; default: random)'), - default_from=lambda: os.urandom(KEY_LENGTH), - autofill=True, - flags=('no_display', 'no_update', 'no_search'), - ), - StrEnum('ipatokenotpalgorithm?', - cli_name='algo', - label=_('Algorithm'), - doc=_('Token hash algorithm'), - default=u'sha1', - autofill=True, - flags=('no_update'), - values=(u'sha1', u'sha256', u'sha384', u'sha512'), - ), - IntEnum('ipatokenotpdigits?', - cli_name='digits', - label=_('Digits'), - doc=_('Number of digits each token code will have'), - values=(6, 8), - default=6, - autofill=True, - flags=('no_update'), - ), - Int('ipatokentotpclockoffset?', - cli_name='offset', - label=_('Clock offset'), - doc=_('TOTP token / FreeIPA server time difference'), - default=0, - autofill=True, - flags=('no_update'), - ), - Int('ipatokentotptimestep?', - cli_name='interval', - label=_('Clock interval'), - doc=_('Length of TOTP token code validity'), - default=30, - autofill=True, - minvalue=5, - flags=('no_update'), - ), - Int('ipatokenhotpcounter?', - cli_name='counter', - label=_('Counter'), - doc=_('Initial counter for the HOTP token'), - default=0, - autofill=True, - minvalue=0, - flags=('no_update'), - ), - ) - - -@register() -class otptoken_add(LDAPCreate): - __doc__ = _('Add a new OTP token.') - msg_summary = _('Added OTP token "%(value)s"') - - takes_options = LDAPCreate.takes_options + ( - Flag('qrcode?', label=_('(deprecated)'), flags=('no_option')), - Flag('no_qrcode', label=_('Do not display QR code'), default=False), - ) - - has_output_params = LDAPCreate.has_output_params + ( - Str('uri?', label=_('URI')), - ) - - def execute(self, ipatokenuniqueid=None, **options): - return super(otptoken_add, self).execute(ipatokenuniqueid, **options) - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - # Fill in a default UUID when not specified. - if entry_attrs.get('ipatokenuniqueid', None) is None: - entry_attrs['ipatokenuniqueid'] = str(uuid.uuid4()) - dn = DN("ipatokenuniqueid=%s" % entry_attrs['ipatokenuniqueid'], dn) - - if not _check_interval(options.get('ipatokennotbefore', None), - options.get('ipatokennotafter', None)): - raise ValidationError(name='not_after', - error='is before the validity start') - - # Set the object class and defaults for specific token types - options['type'] = options['type'].lower() - entry_attrs['objectclass'] = otptoken.object_class + ['ipatoken' + options['type']] - for ttype, tattrs in TOKEN_TYPES.items(): - if ttype != options['type']: - for tattr in tattrs: - if tattr in entry_attrs: - del entry_attrs[tattr] - - # If owner was not specified, default to the person adding this token. - # If managedby was not specified, attempt a sensible default. - if 'ipatokenowner' not in entry_attrs or 'managedby' not in entry_attrs: - result = self.api.Command.user_find( - whoami=True, no_members=False)['result'] - if result: - cur_uid = result[0]['uid'][0] - prev_uid = entry_attrs.setdefault('ipatokenowner', cur_uid) - if cur_uid == prev_uid: - entry_attrs.setdefault('managedby', result[0]['dn']) - - # Resolve the owner's dn - _normalize_owner(self.api.Object.user, entry_attrs) - - # Get the issuer for the URI - owner = entry_attrs.get('ipatokenowner', None) - issuer = api.env.realm - if owner is not None: - try: - issuer = ldap.get_entry(owner, ['krbprincipalname'])['krbprincipalname'][0] - except (NotFound, IndexError): - pass - - # Build the URI parameters - args = {} - args['issuer'] = issuer - args['secret'] = base64.b32encode(entry_attrs['ipatokenotpkey']) - args['digits'] = entry_attrs['ipatokenotpdigits'] - args['algorithm'] = entry_attrs['ipatokenotpalgorithm'].upper() - if options['type'] == 'totp': - args['period'] = entry_attrs['ipatokentotptimestep'] - elif options['type'] == 'hotp': - args['counter'] = entry_attrs['ipatokenhotpcounter'] - - # Build the URI - label = urllib.parse.quote(entry_attrs['ipatokenuniqueid']) - parameters = urllib.parse.urlencode(args) - uri = u'otpauth://%s/%s:%s?%s' % (options['type'], issuer, label, parameters) - setattr(context, 'uri', uri) - - attrs_list.append("objectclass") - return dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - entry_attrs['uri'] = getattr(context, 'uri') - _set_token_type(entry_attrs, **options) - _convert_owner(self.api.Object.user, entry_attrs, options) - return super(otptoken_add, self).post_callback(ldap, dn, entry_attrs, *keys, **options) - - -@register() -class otptoken_del(LDAPDelete): - __doc__ = _('Delete an OTP token.') - msg_summary = _('Deleted OTP token "%(value)s"') - - -@register() -class otptoken_mod(LDAPUpdate): - __doc__ = _('Modify a OTP token.') - msg_summary = _('Modified OTP token "%(value)s"') - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - notafter_set = True - notbefore = options.get('ipatokennotbefore', None) - notafter = options.get('ipatokennotafter', None) - # notbefore xor notafter, exactly one of them is not None - if bool(notbefore) ^ bool(notafter): - result = self.api.Command.otptoken_show(keys[-1])['result'] - if notbefore is None: - notbefore = result.get('ipatokennotbefore', [None])[0] - if notafter is None: - notafter_set = False - notafter = result.get('ipatokennotafter', [None])[0] - - if not _check_interval(notbefore, notafter): - if notafter_set: - raise ValidationError(name='not_after', - error='is before the validity start') - else: - raise ValidationError(name='not_before', - error='is after the validity end') - _normalize_owner(self.api.Object.user, entry_attrs) - - # ticket #4681: if the owner of the token is changed and the - # user also manages this token, then we should automatically - # set the 'managedby' attribute to the new owner - if 'ipatokenowner' in entry_attrs and 'managedby' not in entry_attrs: - new_owner = entry_attrs.get('ipatokenowner', None) - prev_entry = ldap.get_entry(dn, attrs_list=['ipatokenowner', - 'managedby']) - prev_owner = prev_entry.get('ipatokenowner', None) - prev_managedby = prev_entry.get('managedby', None) - - if (new_owner != prev_owner) and (prev_owner == prev_managedby): - entry_attrs.setdefault('managedby', new_owner) - - attrs_list.append("objectclass") - return dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - _set_token_type(entry_attrs, **options) - _convert_owner(self.api.Object.user, entry_attrs, options) - return super(otptoken_mod, self).post_callback(ldap, dn, entry_attrs, *keys, **options) - - -@register() -class otptoken_find(LDAPSearch): - __doc__ = _('Search for OTP token.') - msg_summary = ngettext('%(count)d OTP token matched', '%(count)d OTP tokens matched', 0) - - def pre_callback(self, ldap, filters, attrs_list, *args, **kwargs): - # This is a hack, but there is no other way to - # replace the objectClass when searching - type = kwargs.get('type', '') - if type not in TOKEN_TYPES: - type = '' - filters = filters.replace("(objectclass=ipatoken)", - "(objectclass=ipatoken%s)" % type) - - attrs_list.append("objectclass") - return super(otptoken_find, self).pre_callback(ldap, filters, attrs_list, *args, **kwargs) - - def args_options_2_entry(self, *args, **options): - entry = super(otptoken_find, self).args_options_2_entry(*args, **options) - _normalize_owner(self.api.Object.user, entry) - return entry - - def post_callback(self, ldap, entries, truncated, *args, **options): - for entry in entries: - _set_token_type(entry, **options) - _convert_owner(self.api.Object.user, entry, options) - return super(otptoken_find, self).post_callback(ldap, entries, truncated, *args, **options) - - -@register() -class otptoken_show(LDAPRetrieve): - __doc__ = _('Display information about an OTP token.') - - def pre_callback(self, ldap, dn, attrs_list, *keys, **options): - attrs_list.append("objectclass") - return super(otptoken_show, self).pre_callback(ldap, dn, attrs_list, *keys, **options) - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - _set_token_type(entry_attrs, **options) - _convert_owner(self.api.Object.user, entry_attrs, options) - return super(otptoken_show, self).post_callback(ldap, dn, entry_attrs, *keys, **options) - -@register() -class otptoken_add_managedby(LDAPAddMember): - __doc__ = _('Add users that can manage this token.') - - member_attributes = ['managedby'] - -@register() -class otptoken_remove_managedby(LDAPRemoveMember): - __doc__ = _('Remove users that can manage this token.') - - member_attributes = ['managedby'] diff --git a/ipalib/plugins/passwd.py b/ipalib/plugins/passwd.py deleted file mode 100644 index c4e220815..000000000 --- a/ipalib/plugins/passwd.py +++ /dev/null @@ -1,139 +0,0 @@ -# Authors: -# Rob Crittenden <rcritten@redhat.com> -# -# Copyright (C) 2008 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/>. - -from ipalib import api, errors, krb_utils -from ipalib import Command -from ipalib import Str, Password -from ipalib import _ -from ipalib import output -from ipalib.plugable import Registry -from .baseuser import validate_principal, normalize_principal -from ipalib.request import context -from ipapython.dn import DN - -__doc__ = _(""" -Set a user's password - -If someone other than a user changes that user's password (e.g., Helpdesk -resets it) then the password will need to be changed the first time it -is used. This is so the end-user is the only one who knows the password. - -The IPA password policy controls how often a password may be changed, -what strength requirements exist, and the length of the password history. - -EXAMPLES: - - To reset your own password: - ipa passwd - - To change another user's password: - ipa passwd tuser1 -""") - -register = Registry() - -# We only need to prompt for the current password when changing a password -# for yourself, but the parameter is still required -MAGIC_VALUE = u'CHANGING_PASSWORD_FOR_ANOTHER_USER' - -def get_current_password(principal): - """ - If the user is changing their own password then return None so the - current password is prompted for, otherwise return a fixed value to - be ignored later. - """ - current_principal = krb_utils.get_principal() - if current_principal == normalize_principal(principal): - return None - else: - return MAGIC_VALUE - -@register() -class passwd(Command): - __doc__ = _("Set a user's password.") - - takes_args = ( - Str('principal', validate_principal, - cli_name='user', - label=_('User name'), - primary_key=True, - autofill=True, - default_from=lambda: krb_utils.get_principal(), - normalizer=lambda value: normalize_principal(value), - ), - Password('password', - label=_('New Password'), - ), - Password('current_password', - label=_('Current Password'), - confirm=False, - default_from=lambda principal: get_current_password(principal), - autofill=True, - sortorder=-1, - ), - ) - - takes_options = ( - Password('otp?', - label=_('OTP'), - doc=_('One Time Password'), - confirm=False, - ), - ) - - has_output = output.standard_value - msg_summary = _('Changed password for "%(value)s"') - - def execute(self, principal, password, current_password, **options): - """ - Execute the passwd operation. - - The dn should not be passed as a keyword argument as it is constructed - by this method. - - Returns the entry - - :param principal: The login name or principal of the user - :param password: the new password - :param current_password: the existing password, if applicable - """ - ldap = self.api.Backend.ldap2 - - entry_attrs = ldap.find_entry_by_attr( - 'krbprincipalname', principal, 'posixaccount', [''], - DN(api.env.container_user, api.env.basedn) - ) - - if principal == getattr(context, 'principal') and \ - current_password == MAGIC_VALUE: - # No cheating - self.log.warning('User attempted to change password using magic value') - raise errors.ACIError(info=_('Invalid credentials')) - - if current_password == MAGIC_VALUE: - ldap.modify_password(entry_attrs.dn, password) - else: - otp = options.get('otp') - ldap.modify_password(entry_attrs.dn, password, current_password, otp) - - return dict( - result=True, - value=principal, - ) - diff --git a/ipalib/plugins/permission.py b/ipalib/plugins/permission.py deleted file mode 100644 index 9f19358da..000000000 --- a/ipalib/plugins/permission.py +++ /dev/null @@ -1,1395 +0,0 @@ -# Authors: -# Petr Viktorin <pviktori@redhat.com> -# -# 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 -# 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/>. - -import re -import traceback - -import six - -from . import baseldap -from .privilege import validate_permission_to_privilege -from ipalib import errors -from ipalib.parameters import Str, StrEnum, DNParam, Flag -from ipalib import api, _, ngettext -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 - -if six.PY3: - unicode = str - -__doc__ = _(""" -Permissions -""") + _(""" -A permission enables fine-grained delegation of rights. A permission is -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, 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. 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. 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. -""") + _(""" -Managed permissions -""") + _(""" -Permissions that come with IPA by default can be so-called "managed" -permissions. These have a default set of attributes they apply to, -but the administrator can add/remove individual attributes to/from the set. -""") + _(""" -Deleting or renaming a managed permission, as well as changing its target, -is not allowed. -""") + _(""" -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" -""") - -register = Registry() - -_DEPRECATED_OPTION_ALIASES = { - 'permissions': 'ipapermright', - 'filter': 'extratargetfilter', - 'subtree': 'ipapermlocation', -} - -KNOWN_FLAGS = {'SYSTEM', 'V2', 'MANAGED'} - -output_params = ( - Str('aci', - label=_('ACI'), - ), -) - - -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):] - - -def prevalidate_filter(ugettext, value): - if not value.startswith('(') or not value.endswith(')'): - return _('must be enclosed in parentheses') - - -class DNOrURL(DNParam): - """DN parameter that allows, and strips, a "ldap:///" prefix on input - - Used for ``subtree`` to maintain backward compatibility. - """ - - def _convert_scalar(self, value, index=None): - if isinstance(value, six.string_types) and value.startswith('ldap:///'): - value = strip_ldap_prefix(value) - return super(DNOrURL, self)._convert_scalar(value) - - -def validate_type(ugettext, typestr): - try: - obj = api.Object[typestr] - except KeyError: - return _('"%s" is not an object type') % typestr - if not getattr(obj, 'permission_filter_objectclasses', None): - return _('"%s" is not a valid permission type') % typestr - - -def _disallow_colon(option): - """Given a "cn" option, return a new "cn" option with ':' disallowed - - Used in permission-add and for --rename in permission-mod to prevent user - from creating new permissions with ":" in the name. - """ - return option.clone( - pattern='^[-_ a-zA-Z0-9.]+$', - pattern_errmsg="May only contain letters, numbers, -, _, ., and space", - ) - - -@register() -class permission(baseldap.LDAPObject): - """ - Permission object. - """ - container_dn = api.env.container_permission - object_name = _('permission') - object_name_plural = _('permissions') - # For use the complete object_class list, including 'top', so - # the updater doesn't try to delete 'top' every time. - object_class = ['top', 'groupofnames', 'ipapermission', 'ipapermissionv2'] - permission_filter_objectclasses = ['ipapermission'] - default_attributes = ['cn', 'member', 'memberof', - 'memberindirect', 'ipapermissiontype', 'objectclass', - 'ipapermdefaultattr', 'ipapermincludedattr', 'ipapermexcludedattr', - 'ipapermbindruletype', 'ipapermlocation', 'ipapermright', - 'ipapermtargetfilter', 'ipapermtarget' - ] - attribute_members = { - 'member': ['privilege'], - 'memberindirect': ['role'], - } - rdn_is_primary_key = True - managed_permissions = { - 'System: Read Permissions': { - 'replaces_global_anonymous_aci': True, - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'businesscategory', 'cn', 'description', 'ipapermissiontype', - 'o', 'objectclass', 'ou', 'owner', 'seealso', - 'ipapermdefaultattr', 'ipapermincludedattr', - 'ipapermexcludedattr', 'ipapermbindruletype', 'ipapermtarget', - 'ipapermlocation', 'ipapermright', 'ipapermtargetfilter', - 'member', 'memberof', 'memberuser', 'memberhost', - }, - 'default_privileges': {'RBAC Readers'}, - }, - 'System: Read ACIs': { - # Readable ACIs are needed for reading legacy permissions. - 'non_object': True, - 'ipapermlocation': api.env.basedn, - 'replaces_global_anonymous_aci': True, - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': {'aci'}, - 'default_privileges': {'RBAC Readers'}, - }, - 'System: Modify Privilege Membership': { - 'ipapermright': {'write'}, - 'ipapermdefaultattr': {'member'}, - 'replaces': [ - '(targetattr = "member")(target = "ldap:///cn=*,cn=permissions,cn=pbac,$SUFFIX")(version 3.0;acl "permission:Modify privilege membership";allow (write) groupdn = "ldap:///cn=Modify privilege membership,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Delegation Administrator'}, - }, - } - - label = _('Permissions') - label_singular = _('Permission') - - takes_params = ( - Str('cn', - cli_name='name', - label=_('Permission name'), - primary_key=True, - pattern='^[-_ a-zA-Z0-9.:/]+$', - pattern_errmsg="May only contain letters, numbers, " - "-, _, ., :, /, and space", - ), - StrEnum( - 'ipapermright*', - cli_name='right', - deprecated_cli_aliases={'permissions'}, - label=_('Granted rights'), - 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'), - flags={'ask_create'}, - ), - Str('attrs*', - label=_('Effective attributes'), - doc=_('All attributes to which the permission applies'), - flags={'virtual_attribute', 'allow_mod_for_managed_permission'}, - ), - Str('ipapermincludedattr*', - cli_name='includedattrs', - label=_('Included attributes'), - doc=_('User-specified attributes to which the permission applies'), - flags={'no_create', 'allow_mod_for_managed_permission'}, - ), - Str('ipapermexcludedattr*', - cli_name='excludedattrs', - label=_('Excluded attributes'), - doc=_('User-specified attributes to which the permission ' - 'explicitly does not apply'), - flags={'no_create', 'allow_mod_for_managed_permission'}, - ), - Str('ipapermdefaultattr*', - cli_name='defaultattrs', - label=_('Default attributes'), - doc=_('Attributes to which the permission applies by default'), - flags={'no_create', 'no_update'}, - ), - StrEnum( - 'ipapermbindruletype', - cli_name='bindtype', - label=_('Bind rule type'), - doc=_('Bind rule type'), - autofill=True, - values=(u'permission', u'all', u'anonymous'), - default=u'permission', - flags={'allow_mod_for_managed_permission'}, - ), - DNOrURL( - 'ipapermlocation?', - cli_name='subtree', - label=_('Subtree'), - doc=_('Subtree to apply permissions to'), - flags={'ask_create'}, - ), - Str( - 'extratargetfilter*', prevalidate_filter, - cli_name='filter', - label=_('Extra target filter'), - doc=_('Extra target filter'), - flags={'virtual_attribute'}, - ), - Str( - 'ipapermtargetfilter*', prevalidate_filter, - cli_name='rawfilter', - label=_('Raw target filter'), - doc=_('All target filters, including those implied by ' - 'type and memberof'), - ), - - DNParam( - 'ipapermtarget?', - cli_name='target', - label=_('Target DN'), - doc=_('Optional DN to apply the permission to ' - '(must be in the subtree, but may not yet exist)'), - ), - - DNParam( - 'ipapermtargetto?', - cli_name='targetto', - label=_('Target DN subtree'), - doc=_('Optional DN subtree where an entry can be moved to ' - '(must be in the subtree, but may not yet exist)'), - ), - - DNParam( - 'ipapermtargetfrom?', - cli_name='targetfrom', - label=_('Origin DN subtree'), - doc=_('Optional DN subtree from where an entry can be moved ' - '(must be in the subtree, but may not yet exist)'), - ), - - Str('memberof*', - label=_('Member of group'), # FIXME: Does this label make sense? - doc=_('Target members of a group (sets memberOf targetfilter)'), - flags={'ask_create', 'virtual_attribute'}, - ), - Str('targetgroup?', - label=_('Target group'), - doc=_('User group to apply permissions to (sets target)'), - flags={'ask_create', 'virtual_attribute'}, - ), - Str( - 'type?', validate_type, - label=_('Type'), - doc=_('Type of IPA object ' - '(sets subtree and objectClass targetfilter)'), - 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() - ) - - 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 _get_filter_attr_info(self, entry): - """Get information on filter-related virtual attributes - - Returns a dict with this information: - 'implicit_targetfilters': targetfilters implied by memberof and type - 'memberof': list of names of groups from memberof - 'type': the type - """ - ipapermtargetfilter = entry.get('ipapermtargetfilter', []) - ipapermlocation = entry.single_value.get('ipapermlocation') - - implicit_targetfilters = set() - result = {'implicit_targetfilters': implicit_targetfilters} - - # memberof - memberof = [] - for targetfilter in ipapermtargetfilter: - match = re.match('^\(memberof=(.*)\)$', targetfilter, re.I) - if match: - try: - dn = DN(match.group(1)) - except ValueError: - # Malformed DN; e.g. (memberof=*) - continue - groups_dn = DN(self.api.Object.group.container_dn, - self.api.env.basedn) - if dn[1:] == groups_dn[:] and dn[0].attr == 'cn': - memberof.append(dn[0].value) - implicit_targetfilters.add(match.group(0)) - if memberof: - result['memberof'] = memberof - - # type - if ipapermtargetfilter and ipapermlocation: - for obj in self.api.Object(): - filt = self.make_type_filter(obj) - if not filt: - continue - - wantdn = DN(obj.container_dn, self.api.env.basedn) - if DN(ipapermlocation) != wantdn: - continue - - if filt in ipapermtargetfilter: - result['type'] = [unicode(obj.name)] - implicit_targetfilters.add(filt) - break - - return result - - 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``. - """ - old_client = not client_has_capability( - options['version'], 'permissions2') - - if not options.get('raw') and not options.get('pkey_only'): - ipapermtargetfilter = entry.get('ipapermtargetfilter', []) - ipapermtarget = entry.single_value.get('ipapermtarget') - - # 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 - - filter_attr_info = self._get_filter_attr_info(entry) - if 'type' in filter_attr_info: - entry['type'] = filter_attr_info['type'] - if 'memberof' in filter_attr_info: - entry['memberof'] = filter_attr_info['memberof'] - if 'implicit_targetfilters' in filter_attr_info: - extratargetfilter = sorted( - set(ipapermtargetfilter) - - filter_attr_info['implicit_targetfilters']) - if extratargetfilter: - entry['extratargetfilter'] = extratargetfilter - - # old output names - if old_client: - 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: - if 'ipapermtarget' in rights: - rights['targetgroup'] = rights['ipapermtarget'] - if 'ipapermtargetfilter' in rights: - rights['memberof'] = rights['ipapermtargetfilter'] - - type_rights = set(rights['ipapermtargetfilter']) - location_rights = set(rights.get('ipapermlocation', '')) - type_rights.intersection_update(location_rights) - rights['type'] = ''.join(sorted( - type_rights, key=rights['ipapermtargetfilter'].index)) - - if 'ipapermincludedattr' in rights: - rights['attrs'] = ''.join(sorted( - set(rights['ipapermincludedattr']) & - set(rights.get('ipapermexcludedattr', '')), - key=rights['ipapermincludedattr'].index)) - - if old_client: - 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 - try: - acientry, acistring = self._get_aci_entry_and_string(entry) - except errors.NotFound: - if list(entry.get('ipapermissiontype')) == ['SYSTEM']: - # SYSTEM permissions don't have normal ACIs - pass - else: - raise - else: - entry.single_value['aci'] = acistring - else: - effective_attrs = self.get_effective_attrs(entry) - if effective_attrs: - entry['attrs'] = effective_attrs - if (not options.get('all') and - not entry.get('ipapermexcludedattr') and - not entry.get('ipapermdefaultattr')): - entry.pop('ipapermincludedattr', None) - - if old_client: - # Legacy clients expect some attributes as a single value - for attr in 'type', 'targetgroup', 'aci': - if attr in entry: - entry[attr] = entry.single_value[attr] - # memberof was also single-valued, but not any more - if entry.get('memberof'): - joined_value = u', '.join(str(m) for m in entry['memberof']) - entry['memberof'] = joined_value - 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 - - if not options['raw'] and not options['all']: - # Don't return the raw target filter by default - entry.pop('ipapermtargetfilter', None) - - def get_effective_attrs(self, entry): - attrs = set(entry.get('ipapermdefaultattr', ())) - attrs.update(entry.get('ipapermincludedattr', ())) - if ('read' in entry.get('ipapermright', ()) and - 'objectclass' in (x.lower() for x in attrs)): - # Add special-cased operational attributes - # We want to allow reading these whenever reading the objectclass - # is allowed. - # (But they can still be excluded explicitly, at least in managed - # permissions). - attrs.update((u'entryusn', u'createtimestamp', u'modifytimestamp')) - attrs.difference_update(entry.get('ipapermexcludedattr', ())) - return sorted(attrs) - - def make_aci(self, entry): - """Make an ACI string from the given permission entry""" - - aci_parts = [] - name = entry.single_value['cn'] - - # targetattr - attrs = self.get_effective_attrs(entry) - if attrs: - aci_parts.append("(targetattr = \"%s\")" % ' || '.join(attrs)) - - # target - ipapermtarget = entry.single_value.get('ipapermtarget') - if ipapermtarget: - aci_parts.append("(target = \"%s\")" % - 'ldap:///%s' % ipapermtarget) - - # target_to - ipapermtargetto = entry.single_value.get('ipapermtargetto') - if ipapermtargetto: - aci_parts.append("(target_to = \"%s\")" % - 'ldap:///%s' % ipapermtargetto) - - # target_from - ipapermtargetfrom = entry.single_value.get('ipapermtargetfrom') - if ipapermtargetfrom: - aci_parts.append("(target_from = \"%s\")" % - 'ldap:///%s' % ipapermtargetfrom) - - # targetfilter - ipapermtargetfilter = entry.get('ipapermtargetfilter') - if ipapermtargetfilter: - assert all(f.startswith('(') and f.endswith(')') - for f in ipapermtargetfilter) - if len(ipapermtargetfilter) == 1: - filter = ipapermtargetfilter[0] - else: - filter = '(&%s)' % ''.join(sorted(ipapermtargetfilter)) - aci_parts.append("(targetfilter = \"%s\")" % filter) - - # version, name, rights, bind rule - ipapermbindruletype = entry.single_value.get('ipapermbindruletype', - 'permission') - if ipapermbindruletype == 'permission': - dn = DN(('cn', name), self.container_dn, self.api.env.basedn) - bindrule = 'groupdn = "ldap:///%s"' % dn - elif ipapermbindruletype == 'all': - bindrule = 'userdn = "ldap:///all"' - elif ipapermbindruletype == 'anonymous': - bindrule = 'userdn = "ldap:///anyone"' - else: - raise ValueError(ipapermbindruletype) - - aci_parts.append('(version 3.0;acl "permission:%s";allow (%s) %s;)' % ( - name, ','.join(sorted(entry['ipapermright'])), bindrule)) - - return ''.join(aci_parts) - - 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)) - try: - entry = ldap.get_entry(location, ['aci']) - except errors.NotFound: - raise errors.NotFound(reason=_('Entry %s not found') % location) - entry.setdefault('aci', []).append(acistring) - ldap.update_entry(entry) - - def remove_aci(self, permission_entry): - """Remove the ACI corresponding to the given permission entry - - :return: tuple: - - entry - - removed ACI string, or None if none existed previously - """ - return self._replace_aci(permission_entry) - - def update_aci(self, permission_entry, old_name=None): - """Update the ACI corresponding to the given permission entry - - :return: tuple: - - entry - - removed ACI string, or None if none existed previously - """ - new_acistring = self.make_aci(permission_entry) - return 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 - :return: tuple: - - entry - - removed ACI string, or None if none existed previously - """ - 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.setdefault('aci', []).append(new_acistring) - try: - ldap.update_entry(acientry) - except errors.EmptyModlist: - self.log.debug('No changes to ACI') - return acientry, acistring - - def _get_aci_entry_and_string(self, permission_entry, name=None, - notfound_ok=False, cached_acientry=None): - """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 - :param cached_acientry: See upgrade_permission() - """ - 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 - - if (cached_acientry and - cached_acientry.dn == location and - 'aci' in cached_acientry): - acientry = cached_acientry - else: - try: - acientry = ldap.get_entry(location, ['aci']) - except errors.NotFound: - acientry = ldap.make_entry(location) - acis = acientry.get('aci', ()) - for acistring in acis: - try: - aci = ACI(acistring) - except SyntaxError as e: - self.log.warning('Unparseable ACI %s: %s (at %s)', - acistring, e, location) - continue - 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, cached_acientry=None): - """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 & objectclass are not updated to V2. - Used for the -find and -show commands. - :param cached_acientry: - Optional pre-retreived entry that contains the existing ACI. - If it is None or its DN does not match the location DN, - cached_acientry is ignored and the entry is retreived from LDAP. - """ - 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, cached_acientry=cached_acientry) - - 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 '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['ipapermincludedattr'] = [ - unicode(a) for a in aci.target['targetattr']['expression']] - - if not output_only: - target_entry['ipapermissiontype'] = ['SYSTEM', 'V2'] - if 'ipapermissionv2' not in entry['objectclass']: - target_entry['objectclass'] = list(entry['objectclass']) + [ - u'ipapermissionv2'] - - target_entry['ipapermlocation'] = [self.api.env.basedn] - - # 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 make_type_filter(self, obj): - """Make a filter for a --type based permission from an Object""" - objectclasses = getattr(obj, 'permission_filter_objectclasses', None) - if not objectclasses: - return None - filters = [u'(objectclass=%s)' % o for o in objectclasses] - if len(filters) == 1: - return filters[0] - else: - return '(|%s)' % ''.join(sorted(filters)) - - def preprocess_options(self, options, - return_filter_ops=False, - merge_targetfilter=False): - """Preprocess options (in-place) - - :param options: A dictionary of options - :param return_filter_ops: - If false, assumes there is no pre-existing entry; - additional values of ipapermtargetfilter are added to options. - If true, a dictionary of operations on ipapermtargetfilter is - returned. - These operations must be performed after the existing entry - is retrieved. - The dict has the following keys: - - remove: list of regular expression objects; - implicit values that match any of them should be removed - - add: list of values to be added, after any removals - :merge_targetfilter: - If true, the extratargetfilter is copied into ipapermtargetfilter. - """ - - if 'extratargetfilter' in options: - if 'ipapermtargetfilter' in options: - raise errors.ValidationError( - name='ipapermtargetfilter', - error=_('cannot specify full target filter ' - 'and extra target filter simultaneously')) - if merge_targetfilter: - options['ipapermtargetfilter'] = options['extratargetfilter'] - - filter_ops = {'add': [], 'remove': []} - - if options.get('subtree'): - if isinstance(options['subtree'], (list, tuple)): - [options['subtree']] = options['subtree'] - try: - 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: - filter_ops['remove'].append(re.compile(r'\(memberOf=.*\)', re.I)) - memberof = options.pop('memberof') - for group in (memberof or ()): - try: - groupdn = self.api.Object.group.get_dn_if_exists(group) - except errors.NotFound: - raise errors.NotFound( - reason=_('%s: group not found') % group) - filter_ops['add'].append(u'(memberOf=%s)' % groupdn) - - # 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') - filter_ops['remove'].append(re.compile(r'\(objectclass=.*\)', re.I)) - filter_ops['remove'].append(re.compile( - r'\(\|(\(objectclass=[^(]*\))+\)', re.I)) - if objtype: - if 'ipapermlocation' in options: - raise errors.ValidationError( - name='ipapermlocation', - error=_('subtree and type are mutually exclusive')) - obj = self.api.Object[objtype.lower()] - filt = self.make_type_filter(obj) - if not filt: - raise errors.ValidationError( - _('"%s" is not a valid permission type') % objtype) - filter_ops['add'].append(filt) - container_dn = DN(obj.container_dn, self.api.env.basedn) - options['ipapermlocation'] = container_dn - else: - if 'ipapermlocation' not in options: - options['ipapermlocation'] = None - - if return_filter_ops: - return filter_ops - elif filter_ops['add']: - options['ipapermtargetfilter'] = list(options.get( - 'ipapermtargetfilter') or []) + filter_ops['add'] - - def validate_permission(self, entry): - ldap = self.Backend.ldap2 - - # Rough filter validation by a search - if entry.get('ipapermtargetfilter'): - try: - ldap.find_entries( - filter=ldap.combine_filters(entry['ipapermtargetfilter'], - rules='&'), - base_dn=self.env.basedn, - scope=ldap.SCOPE_BASE, - size_limit=1) - except errors.NotFound: - pass - except errors.BadSearchFilter: - raise errors.ValidationError( - name='ipapermtargetfilter', - error=_('Bad search filter')) - - # Ensure location exists - if entry.get('ipapermlocation'): - location = DN(entry.single_value['ipapermlocation']) - try: - ldap.get_entry(location, attrs_list=[]) - except errors.NotFound: - raise errors.ValidationError( - name='ipapermlocation', - error=_('Entry %s does not exist') % location) - - # Ensure there's something in the ACI's filter - needed_attrs = ( - 'ipapermtarget', 'ipapermtargetfilter', - 'ipapermincludedattr', 'ipapermexcludedattr', 'ipapermdefaultattr') - if not any(v for a in needed_attrs for v in (entry.get(a) or ())): - 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(baseldap.LDAPCreate): - __doc__ = _('Add a system permission without an ACI (internal command)') - - msg_summary = _('Added permission "%(value)s"') - NO_CLI = True - has_output_params = baseldap.LDAPCreate.has_output_params + output_params - - takes_options = ( - Str('ipapermissiontype+', - label=_('Permission flags'), - ), - ) - - 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(): - # 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_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 execute so that processed options apply to - # the whole command, not just the callbacks - def execute(self, *keys, **options): - self.obj.preprocess_options(options, merge_targetfilter=True) - return super(permission_add, self).execute(*keys, **options) - - def get_args(self): - for arg in super(permission_add, self).get_args(): - if arg.name == 'cn': - yield _disallow_colon(arg) - else: - yield arg - - 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]) - - if 'attrs' in options: - if 'ipapermincludedattr' in options: - raise errors.ValidationError( - name='attrs', - error=_('attrs and included attributes are ' - 'mutually exclusive')) - entry['ipapermincludedattr'] = list(options.pop('attrs') or ()) - - self.obj.validate_permission(entry) - return dn - - def post_callback(self, ldap, dn, entry, *keys, **options): - try: - self.obj.add_aci(entry) - except Exception as e: - # Adding the ACI failed. - # We want to be 100% sure the ACI is not there, so try to - # remove it. (This is a no-op if the ACI was not added.) - self.obj.remove_aci(entry) - # Remove the entry. - # The permission entry serves as a "lock" tho prevent - # permission-add commands started at the same time from - # interfering. As long as the entry is there, the other - # permission-add will fail with DuplicateEntry. - # So deleting entry ("releasing the lock") must be the last - # thing we do here. - try: - self.api.Backend['ldap2'].delete_entry(entry) - except errors.NotFound: - pass - if isinstance(e, errors.NotFound): - # add_aci may raise NotFound if the subtree is only virtual - # like cn=compat,SUFFIX and thus passes the LDAP get entry test - location = DN(entry.single_value['ipapermlocation']) - raise errors.ValidationError( - name='ipapermlocation', - error=_('Cannot store permission ACI to %s') % location) - # Re-raise original exception - raise - 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 = baseldap.LDAPDelete.takes_options + ( - Flag('force', - label=_('Force'), - flags={'no_option', 'no_output'}, - doc=_('force delete of SYSTEM permissions'), - ), - ) - - def pre_callback(self, ldap, dn, *keys, **options): - try: - entry = ldap.get_entry(dn, attrs_list=self.obj.default_attributes) - except errors.NotFound: - self.obj.handle_not_found(*keys) - - if not options.get('force'): - self.obj.reject_system(entry) - if entry.get('ipapermdefaultattr'): - raise errors.ACIError( - info=_('cannot delete managed permissions')) - - try: - self.obj.remove_aci(entry) - except errors.NotFound: - errors.NotFound( - reason=_('ACI of permission %s was not found') % keys[0]) - - return dn - - -@register() -class permission_mod(baseldap.LDAPUpdate): - __doc__ = _('Modify a permission.') - - msg_summary = _('Modified permission "%(value)s"') - has_output_params = baseldap.LDAPUpdate.has_output_params + output_params - - def execute(self, *keys, **options): - context.filter_ops = self.obj.preprocess_options( - options, return_filter_ops=True) - return super(permission_mod, self).execute(*keys, **options) - - def get_options(self): - for opt in super(permission_mod, self).get_options(): - if opt.name == 'rename': - yield _disallow_colon(opt) - else: - yield opt - - 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') - - try: - 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) - - self.obj.reject_system(old_entry) - self.obj.upgrade_permission(old_entry) - - if 'MANAGED' in old_entry.get('ipapermissiontype', ()): - for option_name in sorted(options): - if option_name == 'rename': - raise errors.ValidationError( - name=option_name, - error=_('cannot rename managed permissions')) - option = self.options[option_name] - allow_mod = 'allow_mod_for_managed_permission' in option.flags - if (option.attribute and not allow_mod or - option_name == 'extratargetfilter'): - raise errors.ValidationError( - name=option_name, - error=_('not modifiable on managed permissions')) - if context.filter_ops.get('add'): - raise errors.ValidationError( - name='ipapermtargetfilter', - error=_('not modifiable on managed permissions')) - else: - if options.get('ipapermexcludedattr'): - # prevent setting excluded attributes on normal permissions - # (but do allow deleting them all) - raise errors.ValidationError( - name='ipapermexcludedattr', - error=_('only available on managed permissions')) - - if 'attrs' in options: - if any(a in options for a in ('ipapermincludedattr', - 'ipapermexcludedattr')): - raise errors.ValidationError( - name='attrs', - error=_('attrs and included/excluded attributes are ' - 'mutually exclusive')) - attrs = set(options.pop('attrs') or ()) - defaults = set(old_entry.get('ipapermdefaultattr', ())) - entry['ipapermincludedattr'] = list(attrs - defaults) - entry['ipapermexcludedattr'] = list(defaults - attrs) - - # Check setting bindtype for an assigned permission - if options.get('ipapermbindruletype') and old_entry.get('member'): - raise errors.ValidationError( - name='ipapermbindruletype', - error=_('cannot set bindtype for a permission that is ' - 'assigned to a privilege')) - - # 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.items(): - if (key not in options and - key != 'cn' and - key not in self.obj.attribute_members): - entry.setdefault(key, value) - - # For extratargetfilter, add it to the implicit filters - # to get the full target filter - if 'extratargetfilter' in options: - filter_attr_info = self.obj._get_filter_attr_info(entry) - entry['ipapermtargetfilter'] = ( - list(options['extratargetfilter'] or []) + - list(filter_attr_info['implicit_targetfilters'])) - - filter_ops = context.filter_ops - old_filter_attr_info = self.obj._get_filter_attr_info(old_entry) - old_implicit_filters = old_filter_attr_info['implicit_targetfilters'] - removes = filter_ops.get('remove', []) - new_filters = set( - filt for filt in (entry.get('ipapermtargetfilter') or []) - if filt not in old_implicit_filters or - not any(rem.match(filt) for rem in removes)) - new_filters.update(filter_ops.get('add', [])) - new_filters.update(options.get('ipapermtargetfilter') or []) - entry['ipapermtargetfilter'] = list(new_filters) - - if not entry.get('ipapermlocation'): - entry['ipapermlocation'] = [self.api.env.basedn] - - self.obj.validate_permission(entry) - - 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: - context.old_aci_info = self.obj.remove_aci(old_entry) - except errors.NotFound as e: - self.log.error('permission ACI not found: %s' % e) - - # To pass data to postcallback, we currently need to use the context - context.old_entry = old_entry - - return dn - - def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs): - if call_func.__name__ == 'update_entry': - self._revert_aci() - raise exc - - def _revert_aci(self): - old_aci_info = getattr(context, 'old_aci_info', None) - if old_aci_info: - # Try to roll back the old ACI - entry, old_aci_string = old_aci_info - if old_aci_string: - self.log.warning('Reverting ACI on %s to %s' % (entry.dn, - old_aci_string)) - entry['aci'].append(old_aci_string) - self.Backend.ldap2.update_entry(entry) - - def post_callback(self, ldap, dn, entry, *keys, **options): - old_entry = context.old_entry - - try: - if context.permision_moving_aci: - self.obj.add_aci(entry) - else: - self.obj.update_aci(entry, old_entry.single_value['cn']) - except Exception: - # Don't revert attribute which doesn't exist in LDAP - entry.pop('attributelevelrights', None) - - self.log.error('Error updating ACI: %s' % traceback.format_exc()) - self.log.warning('Reverting entry') - old_entry.reset_modlist(entry) - ldap.update_entry(old_entry) - self._revert_aci() - raise - self.obj.postprocess_result(entry, options) - entry['dn'] = entry.dn - return dn - - -@register() -class permission_find(baseldap.LDAPSearch): - __doc__ = _('Search for permissions.') - - msg_summary = ngettext( - '%(count)d permission matched', '%(count)d permissions matched', 0) - has_output_params = baseldap.LDAPSearch.has_output_params + output_params - - def execute(self, *keys, **options): - self.obj.preprocess_options(options, merge_targetfilter=True) - return super(permission_find, self).execute(*keys, **options) - - def pre_callback(self, ldap, filters, attrs_list, base_dn, scope, - *args, **options): - if 'attrs' in options and 'ipapermincludedattr' in options: - raise errors.ValidationError( - name='attrs', - error=_('attrs and included/excluded attributes are ' - 'mutually exclusive')) - - if options.get('attrs'): - # Effective attributes: - # each attr must be in either default or included, - # but not in excluded - filters = ldap.combine_filters( - [filters] + [ - '(&' - '(|' - '(ipapermdefaultattr=%(attr)s)' - '(ipapermincludedattr=%(attr)s))' - '(!(ipapermexcludedattr=%(attr)s)))' % {'attr': attr} - for attr in options['attrs'] - ], - ldap.MATCH_ALL, - ) - - return filters, base_dn, scope - - def post_callback(self, ldap, entries, truncated, *args, **options): - if 'attrs' in options: - options['ipapermincludedattr'] = options['attrs'] - - attribute_options = [o for o in options - if (o in self.options and - self.options[o].attribute)] - - if not options.get('pkey_only'): - for entry in entries: - # Old-style permissions might have matched (e.g. by name) - self.obj.upgrade_permission(entry, output_only=True) - - if not truncated: - if 'sizelimit' in options: - max_entries = options['sizelimit'] - else: - max_entries = self.api.Backend.ldap2.size_limit - - filters = ['(objectclass=ipaPermission)', - '(!(ipaPermissionType=V2))'] - if 'name' in options: - filters.append(ldap.make_filter_from_attr('cn', - options['name'], - 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) - # Retrieve the root entry (with all legacy ACIs) at once - root_entry = ldap.get_entry(DN(api.env.basedn), ['aci']) - except errors.NotFound: - legacy_entries = () - cached_root_entry = None - 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 max_entries > 0 and 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) - # (max_entries <= 0 means unlimited) - entries.pop() - truncated = True - break - self.obj.upgrade_permission(entry, output_only=True, - cached_acientry=root_entry) - # 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: - # Each search term must be present in some - # attribute value - for arg in args: - if arg: - arg = arg.lower() - if not any(arg in str(value).lower() - for values in entry.values() - for value in values): - break - else: - entries.append(entry) - - for entry in entries: - if options.get('pkey_only'): - for opt_name in list(entry): - 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(baseldap.LDAPRetrieve): - __doc__ = _('Display information about a permission.') - has_output_params = baseldap.LDAPRetrieve.has_output_params + output_params - - 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(baseldap.LDAPAddMember): - """Add members to a permission.""" - NO_CLI = True - - def pre_callback(self, ldap, dn, member_dns, failed, *keys, **options): - # We can only add permissions with bind rule type set to - # "permission" (or old-style permissions) - validate_permission_to_privilege(self.api, keys[-1]) - return dn - - -@register() -class permission_remove_member(baseldap.LDAPRemoveMember): - """Remove members from a permission.""" - NO_CLI = True diff --git a/ipalib/plugins/ping.py b/ipalib/plugins/ping.py deleted file mode 100644 index 6a514125c..000000000 --- a/ipalib/plugins/ping.py +++ /dev/null @@ -1,70 +0,0 @@ -# Authors: -# Rob Crittenden <rcritten@redhat.com> -# -# Copyright (C) 2010 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/>. - -from ipalib import Command -from ipalib import output -from ipalib import _ -from ipalib.plugable import Registry -from ipapython.version import VERSION, API_VERSION - -__doc__ = _(""" -Ping the remote IPA server to ensure it is running. - -The ping command sends an echo request to an IPA server. The server -returns its version information. This is used by an IPA client -to confirm that the server is available and accepting requests. - -The server from xmlrpc_uri in /etc/ipa/default.conf is contacted first. -If it does not respond then the client will contact any servers defined -by ldap SRV records in DNS. - -EXAMPLES: - - Ping an IPA server: - ipa ping - ------------------------------------------ - IPA server version 2.1.9. API version 2.20 - ------------------------------------------ - - Ping an IPA server verbosely: - ipa -v ping - ipa: INFO: trying https://ipa.example.com/ipa/xml - ipa: INFO: Forwarding 'ping' to server 'https://ipa.example.com/ipa/xml' - ----------------------------------------------------- - IPA server version 2.1.9. API version 2.20 - ----------------------------------------------------- -""") - -register = Registry() - - -@register() -class ping(Command): - __doc__ = _('Ping a remote server.') - - has_output = ( - output.summary, - ) - - def execute(self, **options): - """ - A possible enhancement would be to take an argument and echo it - back but a fixed value works for now. - """ - return dict(summary=u'IPA server version %s. API version %s' % (VERSION, API_VERSION)) diff --git a/ipalib/plugins/pkinit.py b/ipalib/plugins/pkinit.py deleted file mode 100644 index 9aa101063..000000000 --- a/ipalib/plugins/pkinit.py +++ /dev/null @@ -1,105 +0,0 @@ -# Authors: -# Simo Sorce <ssorce@redhat.com> -# -# Copyright (C) 2010 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/>. - -from ipalib import api, errors -from ipalib import Str -from ipalib import Object, Command -from ipalib import _ -from ipalib.plugable import Registry -from ipapython.dn import DN - -__doc__ = _(""" -Kerberos pkinit options - -Enable or disable anonymous pkinit using the principal -WELLKNOWN/ANONYMOUS@REALM. The server must have been installed with -pkinit support. - -EXAMPLES: - - Enable anonymous pkinit: - ipa pkinit-anonymous enable - - Disable anonymous pkinit: - ipa pkinit-anonymous disable - -For more information on anonymous pkinit see: - -http://k5wiki.kerberos.org/wiki/Projects/Anonymous_pkinit -""") - -register = Registry() - -@register() -class pkinit(Object): - """ - PKINIT Options - """ - object_name = _('pkinit') - - label=_('PKINIT') - - -def valid_arg(ugettext, action): - """ - Accepts only Enable/Disable. - """ - a = action.lower() - if a != 'enable' and a != 'disable': - raise errors.ValidationError( - name='action', - error=_('Unknown command %s') % action - ) - -@register() -class pkinit_anonymous(Command): - __doc__ = _('Enable or Disable Anonymous PKINIT.') - - princ_name = 'WELLKNOWN/ANONYMOUS@%s' % api.env.realm - default_dn = DN(('krbprincipalname', princ_name), ('cn', api.env.realm), ('cn', 'kerberos'), api.env.basedn) - - takes_args = ( - Str('action', valid_arg), - ) - - def execute(self, action, **options): - ldap = self.api.Backend.ldap2 - set_lock = False - lock = None - - entry_attrs = ldap.get_entry(self.default_dn, ['nsaccountlock']) - - if 'nsaccountlock' in entry_attrs: - lock = entry_attrs['nsaccountlock'][0].lower() - - if action.lower() == 'enable': - if lock == 'true': - set_lock = True - lock = None - elif action.lower() == 'disable': - if lock != 'true': - set_lock = True - lock = 'TRUE' - - if set_lock: - entry_attrs['nsaccountlock'] = lock - ldap.update_entry(entry_attrs) - - return dict(result=True) - diff --git a/ipalib/plugins/privilege.py b/ipalib/plugins/privilege.py deleted file mode 100644 index b46807c3f..000000000 --- a/ipalib/plugins/privilege.py +++ /dev/null @@ -1,251 +0,0 @@ -# Authors: -# Rob Crittenden <rcritten@redhat.com> -# -# Copyright (C) 2010 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/>. - -from .baseldap import ( - LDAPObject, - LDAPCreate, - LDAPDelete, - LDAPUpdate, - LDAPSearch, - LDAPRetrieve, - LDAPAddMember, - LDAPRemoveMember, - LDAPAddReverseMember, - LDAPRemoveReverseMember) -from ipalib import api, _, ngettext, errors -from ipalib.plugable import Registry -from ipalib import Str -from ipalib import output -from ipapython.dn import DN - -__doc__ = _(""" -Privileges - -A privilege combines permissions into a logical task. A permission provides -the rights to do a single task. There are some IPA operations that require -multiple permissions to succeed. A privilege is where permissions are -combined in order to perform a specific task. - -For example, adding a user requires the following permissions: - * Creating a new user entry - * Resetting a user password - * Adding the new user to the default IPA users group - -Combining these three low-level tasks into a higher level task in the -form of a privilege named "Add User" makes it easier to manage Roles. - -A privilege may not contain other privileges. - -See role and permission for additional information. -""") - -register = Registry() - - -def validate_permission_to_privilege(api, permission): - ldap = api.Backend.ldap2 - ldapfilter = ldap.combine_filters(rules='&', filters=[ - '(objectClass=ipaPermissionV2)', '(!(ipaPermBindRuleType=permission))', - ldap.make_filter_from_attr('cn', permission, rules='|')]) - try: - entries, truncated = ldap.find_entries( - filter=ldapfilter, - attrs_list=['cn', 'ipapermbindruletype'], - base_dn=DN(api.env.container_permission, api.env.basedn), - size_limit=1) - except errors.NotFound: - pass - else: - entry = entries[0] - message = _('cannot add permission "%(perm)s" with bindtype ' - '"%(bindtype)s" to a privilege') - raise errors.ValidationError( - name='permission', - error=message % { - 'perm': entry.single_value['cn'], - 'bindtype': entry.single_value.get( - 'ipapermbindruletype', 'permission')}) - - -@register() -class privilege(LDAPObject): - """ - Privilege object. - """ - container_dn = api.env.container_privilege - object_name = _('privilege') - object_name_plural = _('privileges') - object_class = ['nestedgroup', 'groupofnames'] - permission_filter_objectclasses = ['groupofnames'] - default_attributes = ['cn', 'description', 'member', 'memberof'] - attribute_members = { - 'member': ['role'], - 'memberof': ['permission'], - } - reverse_members = { - 'member': ['permission'], - } - rdn_is_primary_key = True - managed_permissions = { - 'System: Read Privileges': { - 'replaces_global_anonymous_aci': True, - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'businesscategory', 'cn', 'description', 'member', 'memberof', - 'o', 'objectclass', 'ou', 'owner', 'seealso', 'memberuser', - 'memberhost', - }, - 'default_privileges': {'RBAC Readers'}, - }, - 'System: Add Privileges': { - 'ipapermright': {'add'}, - 'default_privileges': {'Delegation Administrator'}, - }, - 'System: Modify Privileges': { - 'ipapermright': {'write'}, - 'ipapermdefaultattr': { - 'businesscategory', 'cn', 'description', 'o', 'ou', 'owner', - 'seealso', - }, - 'default_privileges': {'Delegation Administrator'}, - }, - 'System: Remove Privileges': { - 'ipapermright': {'delete'}, - 'default_privileges': {'Delegation Administrator'}, - }, - } - - label = _('Privileges') - label_singular = _('Privilege') - - takes_params = ( - Str('cn', - cli_name='name', - label=_('Privilege name'), - primary_key=True, - ), - Str('description?', - cli_name='desc', - label=_('Description'), - doc=_('Privilege description'), - ), - ) - - -@register() -class privilege_add(LDAPCreate): - __doc__ = _('Add a new privilege.') - - msg_summary = _('Added privilege "%(value)s"') - - -@register() -class privilege_del(LDAPDelete): - __doc__ = _('Delete a privilege.') - - msg_summary = _('Deleted privilege "%(value)s"') - - -@register() -class privilege_mod(LDAPUpdate): - __doc__ = _('Modify a privilege.') - - msg_summary = _('Modified privilege "%(value)s"') - - -@register() -class privilege_find(LDAPSearch): - __doc__ = _('Search for privileges.') - - msg_summary = ngettext( - '%(count)d privilege matched', '%(count)d privileges matched', 0 - ) - - -@register() -class privilege_show(LDAPRetrieve): - __doc__ = _('Display information about a privilege.') - - -@register() -class privilege_add_member(LDAPAddMember): - __doc__ = _('Add members to a privilege.') - - NO_CLI=True - - -@register() -class privilege_remove_member(LDAPRemoveMember): - """ - Remove members from a privilege - """ - NO_CLI=True - - -@register() -class privilege_add_permission(LDAPAddReverseMember): - __doc__ = _('Add permissions to a privilege.') - - show_command = 'privilege_show' - member_command = 'permission_add_member' - reverse_attr = 'permission' - member_attr = 'privilege' - - has_output = ( - output.Entry('result'), - output.Output('failed', - type=dict, - doc=_('Members that could not be added'), - ), - output.Output('completed', - type=int, - doc=_('Number of permissions added'), - ), - ) - - def pre_callback(self, ldap, dn, *keys, **options): - if options.get('permission'): - # We can only add permissions with bind rule type set to - # "permission" (or old-style permissions) - validate_permission_to_privilege(self.api, options['permission']) - return dn - - -@register() -class privilege_remove_permission(LDAPRemoveReverseMember): - __doc__ = _('Remove permissions from a privilege.') - - show_command = 'privilege_show' - member_command = 'permission_remove_member' - reverse_attr = 'permission' - member_attr = 'privilege' - - permission_count_out = ('%i permission removed.', '%i permissions removed.') - - has_output = ( - output.Entry('result'), - output.Output('failed', - type=dict, - doc=_('Members that could not be added'), - ), - output.Output('completed', - type=int, - doc=_('Number of permissions removed'), - ), - ) diff --git a/ipalib/plugins/pwpolicy.py b/ipalib/plugins/pwpolicy.py deleted file mode 100644 index 5a2202aa0..000000000 --- a/ipalib/plugins/pwpolicy.py +++ /dev/null @@ -1,611 +0,0 @@ -# Authors: -# Pavel Zuna <pzuna@redhat.com> -# Martin Kosek <mkosek@redhat.com> -# -# Copyright (C) 2010 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/>. - -from ipalib import api -from ipalib import Int, Str, DNParam -from ipalib import errors -from .baseldap import ( - LDAPObject, - LDAPCreate, - LDAPDelete, - LDAPUpdate, - LDAPRetrieve, - LDAPSearch) -from ipalib import _ -from ipalib.plugable import Registry -from ipalib.request import context -from ipapython.ipautil import run -from ipapython.dn import DN -from distutils import version - -import six - -if six.PY3: - unicode = str - -__doc__ = _(""" -Password policy - -A password policy sets limitations on IPA passwords, including maximum -lifetime, minimum lifetime, the number of passwords to save in -history, the number of character classes required (for stronger passwords) -and the minimum password length. - -By default there is a single, global policy for all users. You can also -create a password policy to apply to a group. Each user is only subject -to one password policy, either the group policy or the global policy. A -group policy stands alone; it is not a super-set of the global policy plus -custom settings. - -Each group password policy requires a unique priority setting. If a user -is in multiple groups that have password policies, this priority determines -which password policy is applied. A lower value indicates a higher priority -policy. - -Group password policies are automatically removed when the groups they -are associated with are removed. - -EXAMPLES: - - Modify the global policy: - ipa pwpolicy-mod --minlength=10 - - Add a new group password policy: - ipa pwpolicy-add --maxlife=90 --minlife=1 --history=10 --minclasses=3 --minlength=8 --priority=10 localadmins - - Display the global password policy: - ipa pwpolicy-show - - Display a group password policy: - ipa pwpolicy-show localadmins - - Display the policy that would be applied to a given user: - ipa pwpolicy-show --user=tuser1 - - Modify a group password policy: - ipa pwpolicy-mod --minclasses=2 localadmins -""") - -register = Registry() - -@register() -class cosentry(LDAPObject): - """ - Class of Service object used for linking policies with groups - """ - NO_CLI = True - - container_dn = DN(('cn', 'costemplates'), api.env.container_accounts) - object_class = ['top', 'costemplate', 'extensibleobject', 'krbcontainer'] - permission_filter_objectclasses = ['costemplate'] - default_attributes = ['cn', 'cospriority', 'krbpwdpolicyreference'] - managed_permissions = { - 'System: Read Group Password Policy costemplate': { - 'replaces_global_anonymous_aci': True, - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'cn', 'cospriority', 'krbpwdpolicyreference', 'objectclass', - }, - 'default_privileges': { - 'Password Policy Readers', - 'Password Policy Administrator', - }, - }, - 'System: Add Group Password Policy costemplate': { - 'ipapermright': {'add'}, - 'replaces': [ - '(target = "ldap:///cn=*,cn=costemplates,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Add Group Password Policy costemplate";allow (add) groupdn = "ldap:///cn=Add Group Password Policy costemplate,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Password Policy Administrator'}, - }, - 'System: Delete Group Password Policy costemplate': { - 'ipapermright': {'delete'}, - 'replaces': [ - '(target = "ldap:///cn=*,cn=costemplates,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Delete Group Password Policy costemplate";allow (delete) groupdn = "ldap:///cn=Delete Group Password Policy costemplate,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Password Policy Administrator'}, - }, - 'System: Modify Group Password Policy costemplate': { - 'ipapermright': {'write'}, - 'ipapermdefaultattr': {'cospriority'}, - 'replaces': [ - '(targetattr = "cospriority")(target = "ldap:///cn=*,cn=costemplates,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Modify Group Password Policy costemplate";allow (write) groupdn = "ldap:///cn=Modify Group Password Policy costemplate,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Password Policy Administrator'}, - }, - } - - takes_params = ( - Str('cn', primary_key=True), - DNParam('krbpwdpolicyreference'), - Int('cospriority', minvalue=0), - ) - - priority_not_unique_msg = _( - 'priority must be a unique value (%(prio)d already used by %(gname)s)' - ) - - def get_dn(self, *keys, **options): - group_dn = self.api.Object.group.get_dn(keys[-1]) - return self.backend.make_dn_from_attr( - 'cn', group_dn, DN(self.container_dn, api.env.basedn) - ) - - def check_priority_uniqueness(self, *keys, **options): - if options.get('cospriority') is not None: - entries = self.methods.find( - cospriority=options['cospriority'] - )['result'] - if len(entries) > 0: - group_name = self.api.Object.group.get_primary_key_from_dn( - DN(entries[0]['cn'][0])) - raise errors.ValidationError( - name='priority', - error=self.priority_not_unique_msg % { - 'prio': options['cospriority'], - 'gname': group_name, - } - ) - - -@register() -class cosentry_add(LDAPCreate): - NO_CLI = True - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - - # check for existence of the group - group_dn = self.api.Object.group.get_dn(keys[-1]) - try: - result = ldap.get_entry(group_dn, ['objectclass']) - except errors.NotFound: - self.api.Object.group.handle_not_found(keys[-1]) - - oc = [x.lower() for x in result['objectclass']] - if 'mepmanagedentry' in oc: - raise errors.ManagedPolicyError() - self.obj.check_priority_uniqueness(*keys, **options) - del entry_attrs['cn'] - return dn - - -@register() -class cosentry_del(LDAPDelete): - NO_CLI = True - - -@register() -class cosentry_mod(LDAPUpdate): - NO_CLI = True - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - new_cospriority = options.get('cospriority') - if new_cospriority is not None: - cos_entry = self.api.Command.cosentry_show(keys[-1])['result'] - old_cospriority = int(cos_entry['cospriority'][0]) - - # check uniqueness only when the new priority differs - if old_cospriority != new_cospriority: - self.obj.check_priority_uniqueness(*keys, **options) - return dn - - -@register() -class cosentry_show(LDAPRetrieve): - NO_CLI = True - - -@register() -class cosentry_find(LDAPSearch): - NO_CLI = True - - -global_policy_name = 'global_policy' -global_policy_dn = DN(('cn', global_policy_name), ('cn', api.env.realm), ('cn', 'kerberos'), api.env.basedn) - -@register() -class pwpolicy(LDAPObject): - """ - Password Policy object - """ - container_dn = DN(('cn', api.env.realm), ('cn', 'kerberos')) - object_name = _('password policy') - object_name_plural = _('password policies') - object_class = ['top', 'nscontainer', 'krbpwdpolicy'] - permission_filter_objectclasses = ['krbpwdpolicy'] - default_attributes = [ - 'cn', 'cospriority', 'krbmaxpwdlife', 'krbminpwdlife', - 'krbpwdhistorylength', 'krbpwdmindiffchars', 'krbpwdminlength', - 'krbpwdmaxfailure', 'krbpwdfailurecountinterval', - 'krbpwdlockoutduration', - ] - managed_permissions = { - 'System: Read Group Password Policy': { - 'replaces_global_anonymous_aci': True, - 'ipapermbindruletype': 'permission', - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'cn', 'cospriority', 'krbmaxpwdlife', 'krbminpwdlife', - 'krbpwdfailurecountinterval', 'krbpwdhistorylength', - 'krbpwdlockoutduration', 'krbpwdmaxfailure', - 'krbpwdmindiffchars', 'krbpwdminlength', 'objectclass', - }, - 'default_privileges': { - 'Password Policy Readers', - 'Password Policy Administrator', - }, - }, - 'System: Add Group Password Policy': { - 'ipapermright': {'add'}, - 'replaces': [ - '(target = "ldap:///cn=*,cn=$REALM,cn=kerberos,$SUFFIX")(version 3.0;acl "permission:Add Group Password Policy";allow (add) groupdn = "ldap:///cn=Add Group Password Policy,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Password Policy Administrator'}, - }, - 'System: Delete Group Password Policy': { - 'ipapermright': {'delete'}, - 'replaces': [ - '(target = "ldap:///cn=*,cn=$REALM,cn=kerberos,$SUFFIX")(version 3.0;acl "permission:Delete Group Password Policy";allow (delete) groupdn = "ldap:///cn=Delete Group Password Policy,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Password Policy Administrator'}, - }, - 'System: Modify Group Password Policy': { - 'ipapermright': {'write'}, - 'ipapermdefaultattr': { - 'krbmaxpwdlife', 'krbminpwdlife', 'krbpwdfailurecountinterval', - 'krbpwdhistorylength', 'krbpwdlockoutduration', - 'krbpwdmaxfailure', 'krbpwdmindiffchars', 'krbpwdminlength' - }, - 'replaces': [ - '(targetattr = "krbmaxpwdlife || krbminpwdlife || krbpwdhistorylength || krbpwdmindiffchars || krbpwdminlength || krbpwdmaxfailure || krbpwdfailurecountinterval || krbpwdlockoutduration")(target = "ldap:///cn=*,cn=$REALM,cn=kerberos,$SUFFIX")(version 3.0;acl "permission:Modify Group Password Policy";allow (write) groupdn = "ldap:///cn=Modify Group Password Policy,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Password Policy Administrator'}, - }, - } - - MIN_KRB5KDC_WITH_LOCKOUT = "1.8" - has_lockout = False - lockout_params = () - - result = run(['klist', '-V'], raiseonerr=False, capture_output=True) - if result.returncode == 0: - verstr = result.output.split()[-1] - ver = version.LooseVersion(verstr) - min = version.LooseVersion(MIN_KRB5KDC_WITH_LOCKOUT) - if ver >= min: - has_lockout = True - - if has_lockout: - lockout_params = ( - Int('krbpwdmaxfailure?', - cli_name='maxfail', - label=_('Max failures'), - doc=_('Consecutive failures before lockout'), - minvalue=0, - ), - Int('krbpwdfailurecountinterval?', - cli_name='failinterval', - label=_('Failure reset interval'), - doc=_('Period after which failure count will be reset (seconds)'), - minvalue=0, - ), - Int('krbpwdlockoutduration?', - cli_name='lockouttime', - label=_('Lockout duration'), - doc=_('Period for which lockout is enforced (seconds)'), - minvalue=0, - ), - ) - - label = _('Password Policies') - label_singular = _('Password Policy') - - takes_params = ( - Str('cn?', - cli_name='group', - label=_('Group'), - doc=_('Manage password policy for specific group'), - primary_key=True, - ), - Int('krbmaxpwdlife?', - cli_name='maxlife', - label=_('Max lifetime (days)'), - doc=_('Maximum password lifetime (in days)'), - minvalue=0, - maxvalue=20000, # a little over 54 years - ), - Int('krbminpwdlife?', - cli_name='minlife', - label=_('Min lifetime (hours)'), - doc=_('Minimum password lifetime (in hours)'), - minvalue=0, - ), - Int('krbpwdhistorylength?', - cli_name='history', - label=_('History size'), - doc=_('Password history size'), - minvalue=0, - ), - Int('krbpwdmindiffchars?', - cli_name='minclasses', - label=_('Character classes'), - doc=_('Minimum number of character classes'), - minvalue=0, - maxvalue=5, - ), - Int('krbpwdminlength?', - cli_name='minlength', - label=_('Min length'), - doc=_('Minimum length of password'), - minvalue=0, - ), - Int('cospriority', - cli_name='priority', - label=_('Priority'), - doc=_('Priority of the policy (higher number means lower priority'), - minvalue=0, - flags=('virtual_attribute',), - ), - ) + lockout_params - - def get_dn(self, *keys, **options): - if keys[-1] is not None: - return self.backend.make_dn_from_attr( - self.primary_key.name, keys[-1], - DN(self.container_dn, api.env.basedn) - ) - return global_policy_dn - - def convert_time_for_output(self, entry_attrs, **options): - # Convert seconds to hours and days for displaying to user - if not options.get('raw', False): - if 'krbmaxpwdlife' in entry_attrs: - entry_attrs['krbmaxpwdlife'][0] = unicode( - int(entry_attrs['krbmaxpwdlife'][0]) // 86400 - ) - if 'krbminpwdlife' in entry_attrs: - entry_attrs['krbminpwdlife'][0] = unicode( - int(entry_attrs['krbminpwdlife'][0]) // 3600 - ) - - def convert_time_on_input(self, entry_attrs): - # Convert hours and days to seconds for writing to LDAP - if 'krbmaxpwdlife' in entry_attrs and entry_attrs['krbmaxpwdlife']: - entry_attrs['krbmaxpwdlife'] = entry_attrs['krbmaxpwdlife'] * 86400 - if 'krbminpwdlife' in entry_attrs and entry_attrs['krbminpwdlife']: - entry_attrs['krbminpwdlife'] = entry_attrs['krbminpwdlife'] * 3600 - - def validate_lifetime(self, entry_attrs, add=False, *keys): - """ - Ensure that the maximum lifetime is greater than the minimum. - If there is no minimum lifetime set then don't return an error. - """ - maxlife=entry_attrs.get('krbmaxpwdlife', None) - minlife=entry_attrs.get('krbminpwdlife', None) - existing_entry = {} - if not add: # then read existing entry - existing_entry = self.api.Command.pwpolicy_show(keys[-1], - all=True, - )['result'] - if minlife is None and 'krbminpwdlife' in existing_entry: - minlife = int(existing_entry['krbminpwdlife'][0]) * 3600 - if maxlife is None and 'krbmaxpwdlife' in existing_entry: - maxlife = int(existing_entry['krbmaxpwdlife'][0]) * 86400 - - if maxlife is not None and minlife is not None: - if minlife > maxlife: - raise errors.ValidationError( - name='maxlife', - error=_('Maximum password life must be greater than minimum.'), - ) - - def add_cospriority(self, entry, pwpolicy_name, rights=True): - if pwpolicy_name and pwpolicy_name != global_policy_name: - cos_entry = self.api.Command.cosentry_show( - pwpolicy_name, - rights=rights, all=rights - )['result'] - if cos_entry.get('cospriority') is not None: - entry['cospriority'] = cos_entry['cospriority'] - if rights: - entry['attributelevelrights']['cospriority'] = \ - cos_entry['attributelevelrights']['cospriority'] - - -@register() -class pwpolicy_add(LDAPCreate): - __doc__ = _('Add a new group password policy.') - - def get_args(self): - yield self.obj.primary_key.clone(attribute=True, required=True) - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - self.obj.convert_time_on_input(entry_attrs) - self.obj.validate_lifetime(entry_attrs, True) - self.api.Command.cosentry_add( - keys[-1], krbpwdpolicyreference=dn, - cospriority=options.get('cospriority') - ) - return dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - self.log.info('%r' % entry_attrs) - # attribute rights are not allowed for pwpolicy_add - self.obj.add_cospriority(entry_attrs, keys[-1], rights=False) - self.obj.convert_time_for_output(entry_attrs, **options) - return dn - - -@register() -class pwpolicy_del(LDAPDelete): - __doc__ = _('Delete a group password policy.') - - def get_args(self): - yield self.obj.primary_key.clone( - attribute=True, required=True, multivalue=True - ) - - def pre_callback(self, ldap, dn, *keys, **options): - assert isinstance(dn, DN) - if dn == global_policy_dn: - raise errors.ValidationError( - name='group', - error=_('cannot delete global password policy') - ) - return dn - - def post_callback(self, ldap, dn, *keys, **options): - assert isinstance(dn, DN) - try: - self.api.Command.cosentry_del(keys[-1]) - except errors.NotFound: - pass - return True - - -@register() -class pwpolicy_mod(LDAPUpdate): - __doc__ = _('Modify a group password policy.') - - def execute(self, cn=None, **options): - return super(pwpolicy_mod, self).execute(cn, **options) - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - self.obj.convert_time_on_input(entry_attrs) - self.obj.validate_lifetime(entry_attrs, False, *keys) - setattr(context, 'cosupdate', False) - if options.get('cospriority') is not None: - if keys[-1] is None: - raise errors.ValidationError( - name='priority', - error=_('priority cannot be set on global policy') - ) - try: - self.api.Command.cosentry_mod( - keys[-1], cospriority=options['cospriority'] - ) - except errors.EmptyModlist as e: - if len(entry_attrs) == 1: # cospriority only was passed - raise e - else: - setattr(context, 'cosupdate', True) - return dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - rights = options.get('all', False) and options.get('rights', False) - self.obj.add_cospriority(entry_attrs, keys[-1], rights) - self.obj.convert_time_for_output(entry_attrs, **options) - return dn - - def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs): - if call_func.__name__ == 'update_entry': - if isinstance(exc, errors.EmptyModlist): - entry_attrs = call_args[0] - cosupdate = getattr(context, 'cosupdate') - if not entry_attrs or cosupdate: - return - raise exc - - -@register() -class pwpolicy_show(LDAPRetrieve): - __doc__ = _('Display information about password policy.') - - takes_options = LDAPRetrieve.takes_options + ( - Str('user?', - label=_('User'), - doc=_('Display effective policy for a specific user'), - ), - ) - - def execute(self, cn=None, **options): - return super(pwpolicy_show, self).execute(cn, **options) - - def pre_callback(self, ldap, dn, attrs_list, *keys, **options): - assert isinstance(dn, DN) - if options.get('user') is not None: - user_entry = self.api.Command.user_show( - options['user'], all=True - )['result'] - if 'krbpwdpolicyreference' in user_entry: - return user_entry.get('krbpwdpolicyreference', [dn])[0] - return dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - rights = options.get('all', False) and options.get('rights', False) - self.obj.add_cospriority(entry_attrs, keys[-1], rights) - self.obj.convert_time_for_output(entry_attrs, **options) - return dn - - -@register() -class pwpolicy_find(LDAPSearch): - __doc__ = _('Search for group password policies.') - - # this command does custom sorting in post_callback - sort_result_entries = False - - def priority_sort_key(self, entry): - """Key for sorting password policies - - returns a pair: (is_global, priority) - """ - # global policy will be always last in the output - if entry['cn'][0] == global_policy_name: - return True, 0 - else: - # policies with higher priority (lower number) will be at the - # beginning of the list - try: - cospriority = int(entry['cospriority'][0]) - except KeyError: - # if cospriority is not present in the entry, rather return 0 - # than crash - cospriority = 0 - return False, cospriority - - def post_callback(self, ldap, entries, truncated, *args, **options): - for e in entries: - # When pkey_only flag is on, entries should contain only a cn. - # Add a cospriority attribute that will be used for sorting. - # Attribute rights are not allowed for pwpolicy_find. - self.obj.add_cospriority(e, e['cn'][0], rights=False) - - self.obj.convert_time_for_output(e, **options) - - # do custom entry sorting by its cospriority - entries.sort(key=self.priority_sort_key) - - if options.get('pkey_only', False): - # remove cospriority that was used for sorting - for e in entries: - try: - del e['cospriority'] - except KeyError: - pass - - return truncated diff --git a/ipalib/plugins/radiusproxy.py b/ipalib/plugins/radiusproxy.py deleted file mode 100644 index 44d87b9ae..000000000 --- a/ipalib/plugins/radiusproxy.py +++ /dev/null @@ -1,175 +0,0 @@ -# Authors: -# Nathaniel McCallum <npmccallum@redhat.com> -# -# 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 -# 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/>. - -from .baseldap import ( - LDAPObject, - LDAPCreate, - LDAPDelete, - LDAPUpdate, - LDAPSearch, - LDAPRetrieve) -from ipalib import api, Str, Int, Password, _, ngettext -from ipalib import errors -from ipalib.plugable import Registry -from ipalib.util import validate_hostname, validate_ipaddr -from ipalib.errors import ValidationError -import re - -__doc__ = _(""" -RADIUS Proxy Servers -""") + _(""" -Manage RADIUS Proxy Servers. -""") + _(""" -IPA supports the use of an external RADIUS proxy server for krb5 OTP -authentications. This permits a great deal of flexibility when -integrating with third-party authentication services. -""") + _(""" -EXAMPLES: -""") + _(""" - Add a new server: - ipa radiusproxy-add MyRADIUS --server=radius.example.com:1812 -""") + _(""" - Find all servers whose entries include the string "example.com": - ipa radiusproxy-find example.com -""") + _(""" - Examine the configuration: - ipa radiusproxy-show MyRADIUS -""") + _(""" - Change the secret: - ipa radiusproxy-mod MyRADIUS --secret -""") + _(""" - Delete a configuration: - ipa radiusproxy-del MyRADIUS -""") - -register = Registry() - -LDAP_ATTRIBUTE = re.compile("^[a-zA-Z][a-zA-Z0-9-]*$") -def validate_attributename(ugettext, attr): - if not LDAP_ATTRIBUTE.match(attr): - raise ValidationError(name="ipatokenusermapattribute", - error=_('invalid attribute name')) - -def validate_radiusserver(ugettext, server): - split = server.rsplit(':', 1) - server = split[0] - if len(split) == 2: - try: - port = int(split[1]) - if (port < 0 or port > 65535): - raise ValueError() - except ValueError: - raise ValidationError(name="ipatokenradiusserver", - error=_('invalid port number')) - - if validate_ipaddr(server): - return - - try: - validate_hostname(server, check_fqdn=True, allow_underscore=True) - except ValueError as e: - raise errors.ValidationError(name="ipatokenradiusserver", - error=str(e)) - - -@register() -class radiusproxy(LDAPObject): - """ - RADIUS Server object. - """ - container_dn = api.env.container_radiusproxy - object_name = _('RADIUS proxy server') - object_name_plural = _('RADIUS proxy servers') - object_class = ['ipatokenradiusconfiguration'] - default_attributes = ['cn', 'description', 'ipatokenradiusserver', - 'ipatokenradiustimeout', 'ipatokenradiusretries', 'ipatokenusermapattribute' - ] - search_attributes = ['cn', 'description', 'ipatokenradiusserver'] - rdn_is_primary_key = True - label = _('RADIUS Servers') - label_singular = _('RADIUS Server') - - takes_params = ( - Str('cn', - cli_name='name', - label=_('RADIUS proxy server name'), - primary_key=True, - ), - Str('description?', - cli_name='desc', - label=_('Description'), - doc=_('A description of this RADIUS proxy server'), - ), - Str('ipatokenradiusserver+', validate_radiusserver, - cli_name='server', - label=_('Server'), - doc=_('The hostname or IP (with or without port)'), - ), - Password('ipatokenradiussecret', - cli_name='secret', - label=_('Secret'), - doc=_('The secret used to encrypt data'), - confirm=True, - flags=['no_option'], - ), - Int('ipatokenradiustimeout?', - cli_name='timeout', - label=_('Timeout'), - doc=_('The total timeout across all retries (in seconds)'), - minvalue=1, - ), - Int('ipatokenradiusretries?', - cli_name='retries', - label=_('Retries'), - doc=_('The number of times to retry authentication'), - minvalue=0, - maxvalue=10, - ), - Str('ipatokenusermapattribute?', validate_attributename, - cli_name='userattr', - label=_('User attribute'), - doc=_('The username attribute on the user object'), - ), - ) - -@register() -class radiusproxy_add(LDAPCreate): - __doc__ = _('Add a new RADIUS proxy server.') - msg_summary = _('Added RADIUS proxy server "%(value)s"') - -@register() -class radiusproxy_del(LDAPDelete): - __doc__ = _('Delete a RADIUS proxy server.') - msg_summary = _('Deleted RADIUS proxy server "%(value)s"') - -@register() -class radiusproxy_mod(LDAPUpdate): - __doc__ = _('Modify a RADIUS proxy server.') - msg_summary = _('Modified RADIUS proxy server "%(value)s"') - -@register() -class radiusproxy_find(LDAPSearch): - __doc__ = _('Search for RADIUS proxy servers.') - msg_summary = ngettext( - '%(count)d RADIUS proxy server matched', '%(count)d RADIUS proxy servers matched', 0 - ) - -@register() -class radiusproxy_show(LDAPRetrieve): - __doc__ = _('Display information about a RADIUS proxy server.') diff --git a/ipalib/plugins/realmdomains.py b/ipalib/plugins/realmdomains.py deleted file mode 100644 index 3f8561091..000000000 --- a/ipalib/plugins/realmdomains.py +++ /dev/null @@ -1,340 +0,0 @@ -# Authors: -# Ana Krivokapic <akrivoka@redhat.com> -# -# 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 -# 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/>. - -import six - -from ipalib import api, errors, messages -from ipalib import Str, Flag -from ipalib import _ -from ipalib.plugable import Registry -from .baseldap import LDAPObject, LDAPUpdate, LDAPRetrieve -from ipalib.util import has_soa_or_ns_record, validate_domain_name -from ipalib.util import detect_dns_zone_realm_type -from ipapython.dn import DN -from ipapython.ipautil import get_domain_name - -if six.PY3: - unicode = str - -__doc__ = _(""" -Realm domains - -Manage the list of domains associated with IPA realm. - -EXAMPLES: - - Display the current list of realm domains: - ipa realmdomains-show - - Replace the list of realm domains: - ipa realmdomains-mod --domain=example.com - ipa realmdomains-mod --domain={example1.com,example2.com,example3.com} - - Add a domain to the list of realm domains: - ipa realmdomains-mod --add-domain=newdomain.com - - Delete a domain from the list of realm domains: - ipa realmdomains-mod --del-domain=olddomain.com -""") - -register = Registry() - -def _domain_name_normalizer(d): - return d.lower().rstrip('.') - -def _domain_name_validator(ugettext, value): - try: - validate_domain_name(value, allow_slash=False) - except ValueError as e: - return unicode(e) - - -@register() -class realmdomains(LDAPObject): - """ - List of domains associated with IPA realm. - """ - container_dn = api.env.container_realm_domains - permission_filter_objectclasses = ['domainrelatedobject'] - object_name = _('Realm domains') - search_attributes = ['associateddomain'] - default_attributes = ['associateddomain'] - managed_permissions = { - 'System: Read Realm Domains': { - 'replaces_global_anonymous_aci': True, - 'ipapermbindruletype': 'all', - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'objectclass', 'cn', 'associateddomain', - }, - }, - 'System: Modify Realm Domains': { - 'ipapermbindruletype': 'permission', - 'ipapermright': {'write'}, - 'ipapermdefaultattr': { - 'associatedDomain', - }, - 'default_privileges': {'DNS Administrators'}, - }, - } - - label = _('Realm Domains') - label_singular = _('Realm Domains') - - takes_params = ( - Str('associateddomain+', - _domain_name_validator, - normalizer=_domain_name_normalizer, - cli_name='domain', - label=_('Domain'), - ), - Str('add_domain?', - _domain_name_validator, - normalizer=_domain_name_normalizer, - cli_name='add_domain', - label=_('Add domain'), - ), - Str('del_domain?', - _domain_name_validator, - normalizer=_domain_name_normalizer, - cli_name='del_domain', - label=_('Delete domain'), - ), - ) - - - -@register() -class realmdomains_mod(LDAPUpdate): - __doc__ = _('Modify realm domains.') - - takes_options = LDAPUpdate.takes_options + ( - Flag('force', - label=_('Force'), - doc=_('Force adding domain even if not in DNS'), - ), - ) - - def validate_domains(self, domains, force): - """ - Validates the list of domains as candidates for additions to the - realmdomains list. - - Requirements: - - Each domain has SOA or NS record - - Each domain belongs to the current realm - """ - - # Unless forced, check that each domain has SOA or NS records - if not force: - invalid_domains = [ - d for d in domains - if not has_soa_or_ns_record(d) - ] - - if invalid_domains: - raise errors.ValidationError( - name='domain', - error= _( - "DNS zone for each realmdomain must contain " - "SOA or NS records. No records found for: %s" - ) % ','.join(invalid_domains) - ) - - # Check realm alliegence for each domain - domains_with_realm = [ - (domain, detect_dns_zone_realm_type(self.api, domain)) - for domain in domains - ] - - foreign_domains = [ - domain for domain, realm in domains_with_realm - if realm == 'foreign' - ] - - unknown_domains = [ - domain for domain, realm in domains_with_realm - if realm == 'unknown' - ] - - # If there are any foreing realm domains, bail out - if foreign_domains: - raise errors.ValidationError( - name='domain', - error=_( - 'The following domains do not belong ' - 'to this realm: %(domains)s' - ) % dict(domains=','.join(foreign_domains)) - ) - - # If there are any unknown domains, error out, - # asking for _kerberos TXT records - - # Note: This can be forced, since realmdomains-mod - # is called from dnszone-add where we know that - # the domain being added belongs to our realm - if not force and unknown_domains: - raise errors.ValidationError( - name='domain', - error=_( - 'The realm of the following domains could ' - 'not be detected: %(domains)s. If these are ' - 'domains that belong to the this realm, please ' - 'create a _kerberos TXT record containing "%(realm)s" ' - 'in each of them.' - ) % dict(domains=','.join(unknown_domains), - realm=self.api.env.realm) - ) - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - associateddomain = entry_attrs.get('associateddomain') - add_domain = entry_attrs.get('add_domain') - del_domain = entry_attrs.get('del_domain') - force = options.get('force') - - current_domain = get_domain_name() - - # User specified the list of domains explicitly - if associateddomain: - if add_domain or del_domain: - raise errors.MutuallyExclusiveError( - reason=_( - "The --domain option cannot be used together " - "with --add-domain or --del-domain. Use --domain " - "to specify the whole realm domain list explicitly, " - "to add/remove individual domains, use " - "--add-domain/del-domain.") - ) - - # Make sure our domain is included in the list - if current_domain not in associateddomain: - raise errors.ValidationError( - name='realmdomain list', - error=_("IPA server domain cannot be omitted") - ) - - # Validate that each domain satisfies the requirements - # for realmdomain - self.validate_domains(domains=associateddomain, force=force) - - return dn - - # If --add-domain or --del-domain options were provided, read - # the curent list from LDAP, modify it, and write the changes back - domains = ldap.get_entry(dn)['associateddomain'] - - if add_domain: - self.validate_domains(domains=[add_domain], force=force) - del entry_attrs['add_domain'] - domains.append(add_domain) - - if del_domain: - if del_domain == current_domain: - raise errors.ValidationError( - name='del_domain', - error=_("IPA server domain cannot be deleted") - ) - del entry_attrs['del_domain'] - - try: - domains.remove(del_domain) - except ValueError: - raise errors.AttrValueNotFound( - attr='associateddomain', - value=del_domain - ) - - entry_attrs['associateddomain'] = domains - return dn - - def execute(self, *keys, **options): - dn = self.obj.get_dn(*keys, **options) - ldap = self.obj.backend - - domains_old = set(ldap.get_entry(dn)['associateddomain']) - result = super(realmdomains_mod, self).execute(*keys, **options) - domains_new = set(ldap.get_entry(dn)['associateddomain']) - - domains_added = domains_new - domains_old - domains_deleted = domains_old - domains_new - - # Add a _kerberos TXT record for zones that correspond with - # domains which were added - for domain in domains_added: - - # Skip our own domain - if domain == api.env.domain: - continue - - try: - self.api.Command['dnsrecord_add']( - unicode(domain), - u'_kerberos', - txtrecord=api.env.realm - ) - except (errors.EmptyModlist, errors.NotFound, - errors.ValidationError) as error: - - # If creation of the _kerberos TXT record failed, prompt - # for manual intervention - messages.add_message( - options['version'], - result, - messages.KerberosTXTRecordCreationFailure( - domain=domain, - error=unicode(error), - realm=self.api.env.realm - ) - ) - - # Delete _kerberos TXT record from zones that correspond with - # domains which were deleted - for domain in domains_deleted: - - # Skip our own domain - if domain == api.env.domain: - continue - - try: - self.api.Command['dnsrecord_del']( - unicode(domain), - u'_kerberos', - txtrecord=api.env.realm - ) - except (errors.AttrValueNotFound, errors.NotFound, - errors.ValidationError) as error: - # If deletion of the _kerberos TXT record failed, prompt - # for manual intervention - messages.add_message( - options['version'], - result, - messages.KerberosTXTRecordDeletionFailure( - domain=domain, error=unicode(error) - ) - ) - - return result - - - -@register() -class realmdomains_show(LDAPRetrieve): - __doc__ = _('Display the list of realm domains.') - diff --git a/ipalib/plugins/role.py b/ipalib/plugins/role.py deleted file mode 100644 index f4f0c98d9..000000000 --- a/ipalib/plugins/role.py +++ /dev/null @@ -1,252 +0,0 @@ -# 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/>. - -from ipalib.plugable import Registry -from .baseldap import ( - LDAPObject, - LDAPCreate, - LDAPDelete, - LDAPUpdate, - LDAPSearch, - LDAPRetrieve, - LDAPAddMember, - LDAPRemoveMember, - LDAPAddReverseMember, - LDAPRemoveReverseMember) -from ipalib import api, Str, _, ngettext -from ipalib import output - -__doc__ = _(""" -Roles - -A role is used for fine-grained delegation. A permission grants the ability -to perform given low-level tasks (add a user, modify a group, etc.). A -privilege combines one or more permissions into a higher-level abstraction -such as useradmin. A useradmin would be able to add, delete and modify users. - -Privileges are assigned to Roles. - -Users, groups, hosts and hostgroups may be members of a Role. - -Roles can not contain other roles. - -EXAMPLES: - - Add a new role: - ipa role-add --desc="Junior-level admin" junioradmin - - Add some privileges to this role: - ipa role-add-privilege --privileges=addusers junioradmin - ipa role-add-privilege --privileges=change_password junioradmin - ipa role-add-privilege --privileges=add_user_to_default_group junioradmin - - Add a group of users to this role: - ipa group-add --desc="User admins" useradmins - ipa role-add-member --groups=useradmins junioradmin - - Display information about a role: - ipa role-show junioradmin - - The result of this is that any users in the group 'junioradmin' can - add users, reset passwords or add a user to the default IPA user group. -""") - -register = Registry() - -@register() -class role(LDAPObject): - """ - Role object. - """ - container_dn = api.env.container_rolegroup - object_name = _('role') - object_name_plural = _('roles') - object_class = ['groupofnames', 'nestedgroup'] - permission_filter_objectclasses = ['groupofnames'] - default_attributes = ['cn', 'description', 'member', 'memberof'] - # Role could have a lot of indirect members, but they are not in - # attribute_members therefore they don't have to be in default_attributes - # 'memberindirect', 'memberofindirect', - - attribute_members = { - 'member': ['user', 'group', 'host', 'hostgroup', 'service'], - 'memberof': ['privilege'], - } - reverse_members = { - 'member': ['privilege'], - } - rdn_is_primary_key = True - managed_permissions = { - 'System: Read Roles': { - 'replaces_global_anonymous_aci': True, - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'businesscategory', 'cn', 'description', 'member', 'memberof', - 'o', 'objectclass', 'ou', 'owner', 'seealso', 'memberuser', - 'memberhost', - }, - 'default_privileges': {'RBAC Readers'}, - }, - 'System: Add Roles': { - 'ipapermright': {'add'}, - 'replaces': [ - '(target = "ldap:///cn=*,cn=roles,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Add Roles";allow (add) groupdn = "ldap:///cn=Add Roles,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Delegation Administrator'}, - }, - 'System: Modify Role Membership': { - 'ipapermright': {'write'}, - 'ipapermdefaultattr': {'member'}, - 'replaces': [ - '(targetattr = "member")(target = "ldap:///cn=*,cn=roles,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Modify Role membership";allow (write) groupdn = "ldap:///cn=Modify Role membership,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Delegation Administrator'}, - }, - 'System: Modify Roles': { - 'ipapermright': {'write'}, - 'ipapermdefaultattr': {'cn', 'description'}, - 'replaces': [ - '(targetattr = "cn || description")(target = "ldap:///cn=*,cn=roles,cn=accounts,$SUFFIX")(version 3.0; acl "permission:Modify Roles";allow (write) groupdn = "ldap:///cn=Modify Roles,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Delegation Administrator'}, - }, - 'System: Remove Roles': { - 'ipapermright': {'delete'}, - 'replaces': [ - '(target = "ldap:///cn=*,cn=roles,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Remove Roles";allow (delete) groupdn = "ldap:///cn=Remove Roles,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Delegation Administrator'}, - }, - } - - label = _('Roles') - label_singular = _('Role') - - takes_params = ( - Str('cn', - cli_name='name', - label=_('Role name'), - primary_key=True, - ), - Str('description?', - cli_name='desc', - label=_('Description'), - doc=_('A description of this role-group'), - ), - ) - - - -@register() -class role_add(LDAPCreate): - __doc__ = _('Add a new role.') - - msg_summary = _('Added role "%(value)s"') - - - -@register() -class role_del(LDAPDelete): - __doc__ = _('Delete a role.') - - msg_summary = _('Deleted role "%(value)s"') - - - -@register() -class role_mod(LDAPUpdate): - __doc__ = _('Modify a role.') - - msg_summary = _('Modified role "%(value)s"') - - - -@register() -class role_find(LDAPSearch): - __doc__ = _('Search for roles.') - - msg_summary = ngettext( - '%(count)d role matched', '%(count)d roles matched', 0 - ) - - - -@register() -class role_show(LDAPRetrieve): - __doc__ = _('Display information about a role.') - - - -@register() -class role_add_member(LDAPAddMember): - __doc__ = _('Add members to a role.') - - - -@register() -class role_remove_member(LDAPRemoveMember): - __doc__ = _('Remove members from a role.') - - - -@register() -class role_add_privilege(LDAPAddReverseMember): - __doc__ = _('Add privileges to a role.') - - show_command = 'role_show' - member_command = 'privilege_add_member' - reverse_attr = 'privilege' - member_attr = 'role' - - has_output = ( - output.Entry('result'), - output.Output('failed', - type=dict, - doc=_('Members that could not be added'), - ), - output.Output('completed', - type=int, - doc=_('Number of privileges added'), - ), - ) - - - -@register() -class role_remove_privilege(LDAPRemoveReverseMember): - __doc__ = _('Remove privileges from a role.') - - show_command = 'role_show' - member_command = 'privilege_remove_member' - reverse_attr = 'privilege' - member_attr = 'role' - - has_output = ( - output.Entry('result'), - output.Output('failed', - type=dict, - doc=_('Members that could not be added'), - ), - output.Output('completed', - type=int, - doc=_('Number of privileges removed'), - ), - ) - diff --git a/ipalib/plugins/schema.py b/ipalib/plugins/schema.py deleted file mode 100644 index 8bc230350..000000000 --- a/ipalib/plugins/schema.py +++ /dev/null @@ -1,660 +0,0 @@ -# -# Copyright (C) 2016 FreeIPA Contributors see COPYING for license -# - -import importlib -import itertools -import sys - -import six - -from ipalib import errors -from ipalib.crud import PKQuery, Retrieve, Search -from ipalib.frontend import Command, Method, Object -from ipalib.output import Entry, ListOfEntries, ListOfPrimaryKeys, PrimaryKey -from ipalib.parameters import Any, Bool, Flag, Int, Str -from ipalib.plugable import Registry -from ipalib.text import _ -from ipapython.version import API_VERSION - -__doc__ = _(""" -API Schema -""") + _(""" -Provides API introspection capabilities. -""") + _(""" -EXAMPLES: -""") + _(""" - Show user-find details: - ipa command-show user-find -""") + _(""" - Find user-find parameters: - ipa param-find user-find -""") - -if six.PY3: - unicode = str - -register = Registry() - - -class BaseMetaObject(Object): - takes_params = ( - Str( - 'name', - label=_("Name"), - primary_key=True, - normalizer=lambda name: name.replace(u'-', u'_'), - flags={'no_search'}, - ), - Str( - 'doc?', - label=_("Documentation"), - flags={'no_search'}, - ), - ) - - def _get_obj(self, obj, **kwargs): - raise NotImplementedError() - - def _retrieve(self, *args, **kwargs): - raise NotImplementedError() - - def retrieve(self, *args, **kwargs): - obj = self._retrieve(*args, **kwargs) - obj = self._get_obj(obj, **kwargs) - return obj - - def _search(self, *args, **kwargs): - raise NotImplementedError() - - def _split_search_args(self, criteria=None): - return [], criteria - - def search(self, *args, **kwargs): - args, criteria = self._split_search_args(*args) - - result = self._search(*args, **kwargs) - result = (self._get_obj(r, **kwargs) for r in result) - - if criteria: - criteria = criteria.lower() - result = (r for r in result - if (criteria in r['name'].lower() or - criteria in r.get('doc', u'').lower())) - - if not kwargs.get('all', False) and kwargs.get('pkey_only', False): - result = ({'name': r['name']} for r in result) - - return result - - -class BaseMetaRetrieve(Retrieve): - def execute(self, *args, **options): - obj = self.obj.retrieve(*args, **options) - return dict(result=obj, value=args[-1]) - - -class BaseMetaSearch(Search): - def get_options(self): - for option in super(BaseMetaSearch, self).get_options(): - yield option - - yield Flag( - 'pkey_only?', - label=_("Primary key only"), - doc=_("Results should contain primary key attribute only " - "(\"%s\")") % 'name', - ) - - def execute(self, criteria=None, **options): - result = list(self.obj.search(criteria, **options)) - return dict(result=result, count=len(result), truncated=False) - - -class MetaObject(BaseMetaObject): - takes_params = BaseMetaObject.takes_params + ( - Str( - 'topic_topic?', - label=_("Help topic"), - flags={'no_search'}, - ), - ) - - -class MetaRetrieve(BaseMetaRetrieve): - pass - - -class MetaSearch(BaseMetaSearch): - pass - - -@register() -class command(MetaObject): - takes_params = BaseMetaObject.takes_params + ( - Str( - 'args_param*', - label=_("Arguments"), - flags={'no_search'}, - ), - Str( - 'options_param*', - label=_("Options"), - flags={'no_search'}, - ), - Str( - 'output_params_param*', - label=_("Output parameters"), - flags={'no_search'}, - ), - Bool( - 'no_cli?', - label=_("Exclude from CLI"), - flags={'no_search'}, - ), - ) - - def _get_obj(self, command, **kwargs): - obj = dict() - obj['name'] = unicode(command.name) - - if command.doc: - obj['doc'] = unicode(command.doc) - - if command.topic: - try: - topic = self.api.Object.topic.retrieve(unicode(command.topic)) - except errors.NotFound: - pass - else: - obj['topic_topic'] = topic['name'] - - if command.NO_CLI: - obj['no_cli'] = True - - if len(command.args): - obj['args_param'] = tuple(unicode(n) for n in command.args) - - if len(command.options): - obj['options_param'] = tuple( - unicode(n) for n in command.options if n != 'version') - - if len(command.output_params): - obj['output_params_param'] = tuple( - unicode(n) for n in command.output_params - if n not in command.params) - - return obj - - def _retrieve(self, name, **kwargs): - try: - return self.api.Command[name] - except KeyError: - raise errors.NotFound( - reason=_("%(pkey)s: %(oname)s not found") % { - 'pkey': name, 'oname': self.name, - } - ) - - def _search(self, **kwargs): - return self.api.Command() - - -@register() -class command_show(MetaRetrieve): - __doc__ = _("Display information about a command.") - - -@register() -class command_find(MetaSearch): - __doc__ = _("Search for commands.") - - -@register() -class command_defaults(PKQuery): - NO_CLI = True - - takes_options = ( - Str('params*'), - Any('kw?'), - ) - - def execute(self, name, **options): - command = self.api.Command[name] - - params = options.get('params', []) - - kw = options.get('kw', {}) - if not isinstance(kw, dict): - raise errors.ConversionError(name=name, - error=_("must be a dictionary")) - - result = command.get_default(**kw) - result = {n: v for n, v in result.items() if n in params} - - return dict(result=result) - - -@register() -class topic_(MetaObject): - name = 'topic' - - def __init__(self, api): - super(topic_, self).__init__(api) - self.__topics = None - - def __get_topics(self): - if self.__topics is None: - topics = {} - object.__setattr__(self, '_topic___topics', topics) - - for command in self.api.Command(): - topic_name = command.topic - - while topic_name is not None and topic_name not in topics: - topic = topics[topic_name] = {'name': topic_name} - - for package in self.api.packages: - module_name = '.'.join((package.__name__, topic_name)) - try: - module = sys.modules[module_name] - except KeyError: - try: - module = importlib.import_module(module_name) - except ImportError: - continue - - if module.__doc__ is not None: - topic['doc'] = unicode(module.__doc__).strip() - - try: - topic_name = module.topic - except AttributeError: - topic_name = None - else: - topic['topic_topic'] = topic_name - - return self.__topics - - def _get_obj(self, topic, **kwargs): - return topic - - def _retrieve(self, name, **kwargs): - try: - return self.__get_topics()[name] - except KeyError: - raise errors.NotFound( - reason=_("%(pkey)s: %(oname)s not found") % { - 'pkey': name, 'oname': self.name, - } - ) - - def _search(self, **kwargs): - return self.__get_topics().values() - - -@register() -class topic_show(MetaRetrieve): - __doc__ = _("Display information about a help topic.") - - -@register() -class topic_find(MetaSearch): - __doc__ = _("Search for help topics.") - - -class BaseParam(BaseMetaObject): - takes_params = BaseMetaObject.takes_params + ( - Str( - 'type?', - label=_("Type"), - flags={'no_search'}, - ), - Bool( - 'required?', - label=_("Required"), - flags={'no_search'}, - ), - Bool( - 'multivalue?', - label=_("Multi-value"), - flags={'no_search'}, - ), - ) - - def _split_search_args(self, commandname, criteria=None): - return [commandname], criteria - - -class BaseParamMethod(Method): - def get_args(self): - parent = self.api.Object.command - parent_key = parent.primary_key - yield parent_key.clone_rename( - parent.name + parent_key.name, - cli_name=parent.name, - label=parent_key.label, - required=True, - query=True, - ) - - for arg in super(BaseParamMethod, self).get_args(): - yield arg - - -class BaseParamRetrieve(BaseParamMethod, BaseMetaRetrieve): - pass - - -class BaseParamSearch(BaseParamMethod, BaseMetaSearch): - pass - - -@register() -class param(BaseParam): - takes_params = BaseParam.takes_params + ( - Bool( - 'alwaysask?', - label=_("Always ask"), - flags={'no_search'}, - ), - Bool( - 'autofill?', - label=_("Autofill"), - flags={'no_search'}, - ), - Str( - 'cli_metavar?', - label=_("CLI metavar"), - flags={'no_search'}, - ), - Str( - 'cli_name?', - label=_("CLI name"), - flags={'no_search'}, - ), - Bool( - 'confirm', - label=_("Confirm (password)"), - flags={'no_search'}, - ), - Str( - 'default*', - label=_("Default"), - flags={'no_search'}, - ), - Str( - 'default_from_param*', - label=_("Default from"), - flags={'no_search'}, - ), - Str( - 'deprecated_cli_aliases*', - label=_("Deprecated CLI aliases"), - flags={'no_search'}, - ), - Str( - 'exclude*', - label=_("Exclude from"), - flags={'no_search'}, - ), - Str( - 'hint?', - label=_("Hint"), - flags={'no_search'}, - ), - Str( - 'include*', - label=_("Include in"), - flags={'no_search'}, - ), - Str( - 'label?', - label=_("Label"), - flags={'no_search'}, - ), - Bool( - 'no_convert?', - label=_("Convert on server"), - flags={'no_search'}, - ), - Str( - 'option_group?', - label=_("Option group"), - flags={'no_search'}, - ), - Int( - 'sortorder?', - label=_("Sort order"), - flags={'no_search'}, - ), - Bool( - 'dnsrecord_extra?', - label=_("Extra field (DNS record)"), - flags={'no_search'}, - ), - Bool( - 'dnsrecord_part?', - label=_("Part (DNS record)"), - flags={'no_search'}, - ), - Bool( - 'no_option?', - label=_("No option"), - flags={'no_search'}, - ), - Bool( - 'suppress_empty?', - label=_("Suppress empty"), - flags={'no_search'}, - ), - Bool( - 'sensitive?', - label=_("Sensitive"), - flags={'no_search'}, - ), - ) - - def _get_obj(self, param, **kwargs): - obj = dict() - obj['name'] = unicode(param.name) - - if param.type is unicode: - obj['type'] = u'str' - elif param.type is bytes: - obj['type'] = u'bytes' - elif param.type is not None: - obj['type'] = unicode(param.type.__name__) - - if not param.required: - obj['required'] = False - if param.multivalue: - obj['multivalue'] = True - if param.password: - obj['sensitive'] = True - - for key, value in param._Param__clonekw.items(): - if key in ('alwaysask', - 'autofill', - 'confirm', - 'sortorder'): - obj[key] = value - elif key in ('cli_metavar', - 'cli_name', - 'doc', - 'hint', - 'label', - 'option_group'): - obj[key] = unicode(value) - elif key == 'default': - if param.multivalue: - obj[key] = [unicode(v) for v in value] - else: - obj[key] = [unicode(value)] - elif key == 'default_from': - obj['default_from_param'] = list(unicode(k) - for k in value.keys) - elif key in ('deprecated_cli_aliases', - 'exclude', - 'include'): - obj[key] = list(unicode(v) for v in value) - elif key in ('exponential', - 'normalizer', - 'only_absolute', - 'precision'): - obj['no_convert'] = True - - for flag in (param.flags or []): - if flag in ('dnsrecord_extra', - 'dnsrecord_part', - 'no_option', - 'suppress_empty'): - obj[flag] = True - - return obj - - def _retrieve(self, commandname, name, **kwargs): - command = self.api.Command[commandname] - - if name != 'version': - try: - return command.params[name] - except KeyError: - try: - return command.output_params[name] - except KeyError: - pass - - raise errors.NotFound( - reason=_("%(pkey)s: %(oname)s not found") % { - 'pkey': name, 'oname': self.name, - } - ) - - def _search(self, commandname, **kwargs): - command = self.api.Command[commandname] - - result = itertools.chain( - (p for p in command.params() if p.name != 'version'), - (p for p in command.output_params() - if p.name not in command.params)) - - return result - - -@register() -class param_show(BaseParamRetrieve): - __doc__ = _("Display information about a command parameter.") - - -@register() -class param_find(BaseParamSearch): - __doc__ = _("Search command parameters.") - - -@register() -class output(BaseParam): - takes_params = BaseParam.takes_params + ( - Bool( - 'no_display?', - label=_("Do not display"), - flags={'no_search'}, - ), - ) - - def _get_obj(self, command_output, **kwargs): - command, output = command_output - required = True - multivalue = False - - if isinstance(output, (Entry, ListOfEntries)): - type_type = dict - multivalue = isinstance(output, ListOfEntries) - elif isinstance(output, (PrimaryKey, ListOfPrimaryKeys)): - if getattr(command, 'obj', None) and command.obj.primary_key: - type_type = command.obj.primary_key.type - else: - type_type = type(None) - multivalue = isinstance(output, ListOfPrimaryKeys) - elif isinstance(output.type, tuple): - if tuple in output.type or list in output.type: - type_type = None - multivalue = True - else: - type_type = output.type[0] - required = type(None) not in output.type - else: - type_type = output.type - - obj = dict() - obj['name'] = unicode(output.name) - - if type_type is unicode: - obj['type'] = u'str' - elif type_type is bytes: - obj['type'] = u'bytes' - elif type_type is not None: - obj['type'] = unicode(type_type.__name__) - - if not required: - obj['required'] = False - - if multivalue: - obj['multivalue'] = True - - if 'doc' in output.__dict__: - obj['doc'] = unicode(output.doc) - - if 'flags' in output.__dict__: - if 'no_display' in output.flags: - obj['no_display'] = True - - return obj - - def _retrieve(self, commandname, name, **kwargs): - command = self.api.Command[commandname] - try: - return (command, command.output[name]) - except KeyError: - raise errors.NotFound( - reason=_("%(pkey)s: %(oname)s not found") % { - 'pkey': name, 'oname': self.name, - } - ) - - def _search(self, commandname, **kwargs): - command = self.api.Command[commandname] - return ((command, output) for output in command.output()) - - -@register() -class output_show(BaseParamRetrieve): - __doc__ = _("Display information about a command output.") - - -@register() -class output_find(BaseParamSearch): - __doc__ = _("Search for command outputs.") - - -@register() -class schema(Command): - NO_CLI = True - - def execute(self, *args, **kwargs): - commands = list(self.api.Object.command.search(**kwargs)) - for command in commands: - name = command['name'] - command['params'] = list( - self.api.Object.param.search(name, **kwargs)) - command['output'] = list( - self.api.Object.output.search(name, **kwargs)) - - topics = list(self.api.Object.topic.search(**kwargs)) - - schema = dict() - schema['version'] = API_VERSION - schema['commands'] = commands - schema['topics'] = topics - - return dict(result=schema) diff --git a/ipalib/plugins/selfservice.py b/ipalib/plugins/selfservice.py deleted file mode 100644 index 4ff6ac744..000000000 --- a/ipalib/plugins/selfservice.py +++ /dev/null @@ -1,224 +0,0 @@ -# Authors: -# Rob Crittenden <rcritten@redhat.com> -# -# Copyright (C) 2010 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/>. - -from ipalib import _, ngettext -from ipalib import Str -from ipalib import api, crud, errors -from ipalib import output -from ipalib import Object -from ipalib.plugable import Registry -from .baseldap import gen_pkey_only_option, pkey_to_value - -__doc__ = _(""" -Self-service Permissions - -A permission enables fine-grained delegation of permissions. Access Control -Rules, or instructions (ACIs), grant permission to permissions to perform -given tasks such as adding a user, modifying a group, etc. - -A Self-service permission defines what an object can change in its own entry. - - -EXAMPLES: - - Add a self-service rule to allow users to manage their address (using Bash - brace expansion): - ipa selfservice-add --permissions=write --attrs={street,postalCode,l,c,st} "Users manage their own address" - - When managing the list of attributes you need to include all attributes - in the list, including existing ones. - Add telephoneNumber to the list (using Bash brace expansion): - ipa selfservice-mod --attrs={street,postalCode,l,c,st,telephoneNumber} "Users manage their own address" - - Display our updated rule: - ipa selfservice-show "Users manage their own address" - - Delete a rule: - ipa selfservice-del "Users manage their own address" -""") - -register = Registry() - -ACI_PREFIX=u"selfservice" - -output_params = ( - Str('aci', - label=_('ACI'), - ), -) - - -@register() -class selfservice(Object): - """ - Selfservice object. - """ - - bindable = False - object_name = _('self service permission') - object_name_plural = _('self service permissions') - label = _('Self Service Permissions') - label_singular = _('Self Service Permission') - - takes_params = ( - Str('aciname', - cli_name='name', - label=_('Self-service name'), - doc=_('Self-service name'), - primary_key=True, - pattern='^[-_ a-zA-Z0-9]+$', - pattern_errmsg="May only contain letters, numbers, -, _, and space", - ), - Str('permissions*', - cli_name='permissions', - label=_('Permissions'), - doc=_('Permissions to grant (read, write). Default is write.'), - ), - Str('attrs+', - cli_name='attrs', - label=_('Attributes'), - doc=_('Attributes to which the permission applies.'), - normalizer=lambda value: value.lower(), - ), - ) - - def __json__(self): - json_friendly_attributes = ( - 'label', 'label_singular', 'takes_params', 'bindable', 'name', - 'object_name', 'object_name_plural', - ) - json_dict = dict( - (a, getattr(self, a)) for a in json_friendly_attributes - ) - json_dict['primary_key'] = self.primary_key.name - json_dict['methods'] = [m for m in self.methods] - return json_dict - - def postprocess_result(self, result): - try: - # do not include prefix in result - del result['aciprefix'] - except KeyError: - pass - - - -@register() -class selfservice_add(crud.Create): - __doc__ = _('Add a new self-service permission.') - - msg_summary = _('Added selfservice "%(value)s"') - has_output_params = output_params - - def execute(self, aciname, **kw): - if not 'permissions' in kw: - kw['permissions'] = (u'write',) - kw['selfaci'] = True - kw['aciprefix'] = ACI_PREFIX - result = api.Command['aci_add'](aciname, **kw)['result'] - self.obj.postprocess_result(result) - - return dict( - result=result, - value=pkey_to_value(aciname, kw), - ) - - - -@register() -class selfservice_del(crud.Delete): - __doc__ = _('Delete a self-service permission.') - - has_output = output.standard_boolean - msg_summary = _('Deleted selfservice "%(value)s"') - - def execute(self, aciname, **kw): - result = api.Command['aci_del'](aciname, aciprefix=ACI_PREFIX) - self.obj.postprocess_result(result) - - return dict( - result=True, - value=pkey_to_value(aciname, kw), - ) - - - -@register() -class selfservice_mod(crud.Update): - __doc__ = _('Modify a self-service permission.') - - msg_summary = _('Modified selfservice "%(value)s"') - has_output_params = output_params - - def execute(self, aciname, **kw): - if 'attrs' in kw and kw['attrs'] is None: - raise errors.RequirementError(name='attrs') - - kw['aciprefix'] = ACI_PREFIX - result = api.Command['aci_mod'](aciname, **kw)['result'] - self.obj.postprocess_result(result) - - return dict( - result=result, - value=pkey_to_value(aciname, kw), - ) - - - -@register() -class selfservice_find(crud.Search): - __doc__ = _('Search for a self-service permission.') - - msg_summary = ngettext( - '%(count)d selfservice matched', '%(count)d selfservices matched', 0 - ) - - takes_options = (gen_pkey_only_option("name"),) - has_output_params = output_params - - def execute(self, term=None, **kw): - kw['selfaci'] = True - kw['aciprefix'] = ACI_PREFIX - result = api.Command['aci_find'](term, **kw)['result'] - - for aci in result: - self.obj.postprocess_result(aci) - - return dict( - result=result, - count=len(result), - truncated=False, - ) - - - -@register() -class selfservice_show(crud.Retrieve): - __doc__ = _('Display information about a self-service permission.') - - has_output_params = output_params - - def execute(self, aciname, **kw): - result = api.Command['aci_show'](aciname, aciprefix=ACI_PREFIX, **kw)['result'] - self.obj.postprocess_result(result) - return dict( - result=result, - value=pkey_to_value(aciname, kw), - ) - diff --git a/ipalib/plugins/selinuxusermap.py b/ipalib/plugins/selinuxusermap.py deleted file mode 100644 index 8f660d089..000000000 --- a/ipalib/plugins/selinuxusermap.py +++ /dev/null @@ -1,569 +0,0 @@ -# Authors: -# Rob Crittenden <rcritten@redhat.com> -# -# Copyright (C) 2011 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/>. - -import re - -from ipalib import api, errors -from ipalib import Str, StrEnum, Bool -from ipalib.plugable import Registry -from .baseldap import ( - pkey_to_value, - LDAPObject, - LDAPCreate, - LDAPDelete, - LDAPUpdate, - LDAPSearch, - LDAPRetrieve, - LDAPQuery, - LDAPAddMember, - LDAPRemoveMember) -from ipalib import _, ngettext -from ipalib import output -from .hbacrule import is_all -from ipapython.dn import DN - -__doc__ = _(""" -SELinux User Mapping - -Map IPA users to SELinux users by host. - -Hosts, hostgroups, users and groups can be either defined within -the rule or it may point to an existing HBAC rule. When using ---hbacrule option to selinuxusermap-find an exact match is made on the -HBAC rule name, so only one or zero entries will be returned. - -EXAMPLES: - - Create a rule, "test1", that sets all users to xguest_u:s0 on the host "server": - ipa selinuxusermap-add --usercat=all --selinuxuser=xguest_u:s0 test1 - ipa selinuxusermap-add-host --hosts=server.example.com test1 - - Create a rule, "test2", that sets all users to guest_u:s0 and uses an existing HBAC rule for users and hosts: - ipa selinuxusermap-add --usercat=all --hbacrule=webserver --selinuxuser=guest_u:s0 test2 - - Display the properties of a rule: - ipa selinuxusermap-show test2 - - Create a rule for a specific user. This sets the SELinux context for - user john to unconfined_u:s0-s0:c0.c1023 on any machine: - ipa selinuxusermap-add --hostcat=all --selinuxuser=unconfined_u:s0-s0:c0.c1023 john_unconfined - ipa selinuxusermap-add-user --users=john john_unconfined - - Disable a rule: - ipa selinuxusermap-disable test1 - - Enable a rule: - ipa selinuxusermap-enable test1 - - Find a rule referencing a specific HBAC rule: - ipa selinuxusermap-find --hbacrule=allow_some - - Remove a rule: - ipa selinuxusermap-del john_unconfined - -SEEALSO: - - The list controlling the order in which the SELinux user map is applied - and the default SELinux user are available in the config-show command. -""") - -register = Registry() - -notboth_err = _('HBAC rule and local members cannot both be set') - - -def validate_selinuxuser(ugettext, user): - """ - An SELinux user has 3 components: user:MLS:MCS. user and MLS are required. - user traditionally ends with _u but this is not mandatory. - The regex is ^[a-zA-Z][a-zA-Z_]* - - The MLS part can only be: - Level: s[0-15](-s[0-15]) - - Then MCS could be c[0-1023].c[0-1023] and/or c[0-1023]-c[0-c0123] - Meaning - s0 s0-s1 s0-s15:c0.c1023 s0-s1:c0,c2,c15.c26 s0-s0:c0.c1023 - - Returns a message on invalid, returns nothing on valid. - """ - regex_name = re.compile(r'^[a-zA-Z][a-zA-Z_]*$') - regex_mls = re.compile(r'^s[0-9][1-5]{0,1}(-s[0-9][1-5]{0,1}){0,1}$') - regex_mcs = re.compile(r'^c(\d+)([.,-]c(\d+))*?$') - - # If we add in ::: we don't have to check to see if some values are - # empty - (name, mls, mcs, ignore) = (user + ':::').split(':', 3) - - if not regex_name.match(name): - return _('Invalid SELinux user name, only a-Z and _ are allowed') - if not mls or not regex_mls.match(mls): - return _('Invalid MLS value, must match s[0-15](-s[0-15])') - m = regex_mcs.match(mcs) - if mcs and (not m or (m.group(3) and (int(m.group(3)) > 1023))): - return _('Invalid MCS value, must match c[0-1023].c[0-1023] ' - 'and/or c[0-1023]-c[0-c0123]') - - return None - - -def validate_selinuxuser_inlist(ldap, user): - """ - Ensure the user is in the list of allowed SELinux users. - - Returns nothing if the user is found, raises an exception otherwise. - """ - config = ldap.get_ipa_config() - item = config.get('ipaselinuxusermaporder', []) - if len(item) != 1: - raise errors.NotFound(reason=_('SELinux user map list not ' - 'found in configuration')) - userlist = item[0].split('$') - if user not in userlist: - raise errors.NotFound( - reason=_('SELinux user %(user)s not found in ' - 'ordering list (in config)') % dict(user=user)) - - return - - -@register() -class selinuxusermap(LDAPObject): - """ - SELinux User Map object. - """ - container_dn = api.env.container_selinux - object_name = _('SELinux User Map rule') - object_name_plural = _('SELinux User Map rules') - object_class = ['ipaassociation', 'ipaselinuxusermap'] - permission_filter_objectclasses = ['ipaselinuxusermap'] - default_attributes = [ - 'cn', 'ipaenabledflag', - 'description', 'usercategory', 'hostcategory', - 'ipaenabledflag', 'memberuser', 'memberhost', - 'seealso', 'ipaselinuxuser', - ] - uuid_attribute = 'ipauniqueid' - rdn_attribute = 'ipauniqueid' - attribute_members = { - 'memberuser': ['user', 'group'], - 'memberhost': ['host', 'hostgroup'], - } - managed_permissions = { - 'System: Read SELinux User Maps': { - 'replaces_global_anonymous_aci': True, - 'ipapermbindruletype': 'all', - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'accesstime', 'cn', 'description', 'hostcategory', - 'ipaenabledflag', 'ipaselinuxuser', 'ipauniqueid', - 'memberhost', 'memberuser', 'seealso', 'usercategory', - 'objectclass', 'member', - }, - }, - 'System: Add SELinux User Maps': { - 'ipapermright': {'add'}, - 'replaces': [ - '(target = "ldap:///ipauniqueid=*,cn=usermap,cn=selinux,$SUFFIX")(version 3.0;acl "permission:Add SELinux User Maps";allow (add) groupdn = "ldap:///cn=Add SELinux User Maps,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'SELinux User Map Administrators'}, - }, - 'System: Modify SELinux User Maps': { - 'ipapermright': {'write'}, - 'ipapermdefaultattr': { - 'cn', 'ipaenabledflag', 'ipaselinuxuser', 'memberhost', - 'memberuser', 'seealso' - }, - 'replaces': [ - '(targetattr = "cn || memberuser || memberhost || seealso || ipaselinuxuser || ipaenabledflag")(target = "ldap:///ipauniqueid=*,cn=usermap,cn=selinux,$SUFFIX")(version 3.0;acl "permission:Modify SELinux User Maps";allow (write) groupdn = "ldap:///cn=Modify SELinux User Maps,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'SELinux User Map Administrators'}, - }, - 'System: Remove SELinux User Maps': { - 'ipapermright': {'delete'}, - 'replaces': [ - '(target = "ldap:///ipauniqueid=*,cn=usermap,cn=selinux,$SUFFIX")(version 3.0;acl "permission:Remove SELinux User Maps";allow (delete) groupdn = "ldap:///cn=Remove SELinux User Maps,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'SELinux User Map Administrators'}, - }, - } - - # These maps will not show as members of other entries - - label = _('SELinux User Maps') - label_singular = _('SELinux User Map') - - takes_params = ( - Str('cn', - cli_name='name', - label=_('Rule name'), - primary_key=True, - ), - Str('ipaselinuxuser', validate_selinuxuser, - cli_name='selinuxuser', - label=_('SELinux User'), - ), - Str('seealso?', - cli_name='hbacrule', - label=_('HBAC Rule'), - doc=_('HBAC Rule that defines the users, groups and hostgroups'), - ), - StrEnum('usercategory?', - cli_name='usercat', - label=_('User category'), - doc=_('User category the rule applies to'), - values=(u'all', ), - ), - StrEnum('hostcategory?', - cli_name='hostcat', - label=_('Host category'), - doc=_('Host category the rule applies to'), - values=(u'all', ), - ), - Str('description?', - cli_name='desc', - label=_('Description'), - ), - Bool('ipaenabledflag?', - label=_('Enabled'), - flags=['no_option'], - ), - Str('memberuser_user?', - label=_('Users'), - flags=['no_create', 'no_update', 'no_search'], - ), - Str('memberuser_group?', - label=_('User Groups'), - flags=['no_create', 'no_update', 'no_search'], - ), - Str('memberhost_host?', - label=_('Hosts'), - flags=['no_create', 'no_update', 'no_search'], - ), - Str('memberhost_hostgroup?', - label=_('Host Groups'), - flags=['no_create', 'no_update', 'no_search'], - ), - ) - - def _normalize_seealso(self, seealso): - """ - Given a HBAC rule name verify its existence and return the dn. - """ - if not seealso: - return None - - try: - dn = DN(seealso) - return str(dn) - except ValueError: - try: - entry_attrs = self.backend.find_entry_by_attr( - self.api.Object['hbacrule'].primary_key.name, - seealso, - self.api.Object['hbacrule'].object_class, - [''], - DN(self.api.Object['hbacrule'].container_dn, api.env.basedn)) - seealso = entry_attrs.dn - except errors.NotFound: - raise errors.NotFound(reason=_('HBAC rule %(rule)s not found') % dict(rule=seealso)) - - return seealso - - def _convert_seealso(self, ldap, entry_attrs, **options): - """ - Convert an HBAC rule dn into a name - """ - if options.get('raw', False): - return - - if 'seealso' in entry_attrs: - hbac_attrs = ldap.get_entry(entry_attrs['seealso'][0], ['cn']) - entry_attrs['seealso'] = hbac_attrs['cn'][0] - - - -@register() -class selinuxusermap_add(LDAPCreate): - __doc__ = _('Create a new SELinux User Map.') - - msg_summary = _('Added SELinux User Map "%(value)s"') - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - # rules are enabled by default - entry_attrs['ipaenabledflag'] = 'TRUE' - validate_selinuxuser_inlist(ldap, entry_attrs['ipaselinuxuser']) - - # hbacrule is not allowed when usercat or hostcat is set - is_to_be_set = lambda x: x in entry_attrs and entry_attrs[x] != None - - are_local_members_to_be_set = any(is_to_be_set(attr) - for attr in ('usercategory', - 'hostcategory')) - - is_hbacrule_to_be_set = is_to_be_set('seealso') - - if is_hbacrule_to_be_set and are_local_members_to_be_set: - raise errors.MutuallyExclusiveError(reason=notboth_err) - - if is_hbacrule_to_be_set: - entry_attrs['seealso'] = self.obj._normalize_seealso(entry_attrs['seealso']) - - return dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - self.obj._convert_seealso(ldap, entry_attrs, **options) - - return dn - - - -@register() -class selinuxusermap_del(LDAPDelete): - __doc__ = _('Delete a SELinux User Map.') - - msg_summary = _('Deleted SELinux User Map "%(value)s"') - - - -@register() -class selinuxusermap_mod(LDAPUpdate): - __doc__ = _('Modify a SELinux User Map.') - - msg_summary = _('Modified SELinux User Map "%(value)s"') - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - try: - _entry_attrs = ldap.get_entry(dn, attrs_list) - except errors.NotFound: - self.obj.handle_not_found(*keys) - - is_to_be_deleted = lambda x: (x in _entry_attrs and x in entry_attrs) and \ - entry_attrs[x] == None - - # makes sure the local members and hbacrule is not set at the same time - # memberuser or memberhost could have been set using --setattr - is_to_be_set = lambda x: ((x in _entry_attrs and _entry_attrs[x] != None) or \ - (x in entry_attrs and entry_attrs[x] != None)) and \ - not is_to_be_deleted(x) - - are_local_members_to_be_set = any(is_to_be_set(attr) - for attr in ('usercategory', - 'hostcategory', - 'memberuser', - 'memberhost')) - - is_hbacrule_to_be_set = is_to_be_set('seealso') - - # this can disable all modifications if hbacrule and local members were - # set at the same time bypassing this commad, e.g. using ldapmodify - if are_local_members_to_be_set and is_hbacrule_to_be_set: - raise errors.MutuallyExclusiveError(reason=notboth_err) - - if is_all(entry_attrs, 'usercategory') and 'memberuser' in entry_attrs: - raise errors.MutuallyExclusiveError(reason="user category " - "cannot be set to 'all' while there are allowed users") - if is_all(entry_attrs, 'hostcategory') and 'memberhost' in entry_attrs: - raise errors.MutuallyExclusiveError(reason="host category " - "cannot be set to 'all' while there are allowed hosts") - - if 'ipaselinuxuser' in entry_attrs: - validate_selinuxuser_inlist(ldap, entry_attrs['ipaselinuxuser']) - - if 'seealso' in entry_attrs: - entry_attrs['seealso'] = self.obj._normalize_seealso(entry_attrs['seealso']) - return dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - self.obj._convert_seealso(ldap, entry_attrs, **options) - return dn - - - -@register() -class selinuxusermap_find(LDAPSearch): - __doc__ = _('Search for SELinux User Maps.') - - msg_summary = ngettext( - '%(count)d SELinux User Map matched', '%(count)d SELinux User Maps matched', 0 - ) - - def execute(self, *args, **options): - # If searching on hbacrule we need to find the uuid to search on - if options.get('seealso'): - hbacrule = options['seealso'] - - try: - hbac = api.Command['hbacrule_show'](hbacrule, -all=True)['result'] - dn = hbac['dn'] - except errors.NotFound: - return dict(count=0, result=[], truncated=False) - options['seealso'] = dn - - return super(selinuxusermap_find, self).execute(*args, **options) - - def post_callback(self, ldap, entries, truncated, *args, **options): - if options.get('pkey_only', False): - return truncated - for attrs in entries: - self.obj._convert_seealso(ldap, attrs, **options) - return truncated - - - -@register() -class selinuxusermap_show(LDAPRetrieve): - __doc__ = _('Display the properties of a SELinux User Map rule.') - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - self.obj._convert_seealso(ldap, entry_attrs, **options) - return dn - - - -@register() -class selinuxusermap_enable(LDAPQuery): - __doc__ = _('Enable an SELinux User Map rule.') - - msg_summary = _('Enabled SELinux User Map "%(value)s"') - has_output = output.standard_value - - def execute(self, cn, **options): - ldap = self.obj.backend - - dn = self.obj.get_dn(cn) - try: - entry_attrs = ldap.get_entry(dn, ['ipaenabledflag']) - except errors.NotFound: - self.obj.handle_not_found(cn) - - entry_attrs['ipaenabledflag'] = ['TRUE'] - - try: - ldap.update_entry(entry_attrs) - except errors.EmptyModlist: - raise errors.AlreadyActive() - - return dict( - result=True, - value=pkey_to_value(cn, options), - ) - - - -@register() -class selinuxusermap_disable(LDAPQuery): - __doc__ = _('Disable an SELinux User Map rule.') - - msg_summary = _('Disabled SELinux User Map "%(value)s"') - has_output = output.standard_value - - def execute(self, cn, **options): - ldap = self.obj.backend - - dn = self.obj.get_dn(cn) - try: - entry_attrs = ldap.get_entry(dn, ['ipaenabledflag']) - except errors.NotFound: - self.obj.handle_not_found(cn) - - entry_attrs['ipaenabledflag'] = ['FALSE'] - - try: - ldap.update_entry(entry_attrs) - except errors.EmptyModlist: - raise errors.AlreadyInactive() - - return dict( - result=True, - value=pkey_to_value(cn, options), - ) - - - -@register() -class selinuxusermap_add_user(LDAPAddMember): - __doc__ = _('Add users and groups to an SELinux User Map rule.') - - member_attributes = ['memberuser'] - member_count_out = ('%i object added.', '%i objects added.') - - def pre_callback(self, ldap, dn, found, not_found, *keys, **options): - assert isinstance(dn, DN) - try: - entry_attrs = ldap.get_entry(dn, self.obj.default_attributes) - dn = entry_attrs.dn - except errors.NotFound: - self.obj.handle_not_found(*keys) - if 'usercategory' in entry_attrs and \ - entry_attrs['usercategory'][0].lower() == 'all': - raise errors.MutuallyExclusiveError( - reason=_("users cannot be added when user category='all'")) - if 'seealso' in entry_attrs: - raise errors.MutuallyExclusiveError(reason=notboth_err) - return dn - - - -@register() -class selinuxusermap_remove_user(LDAPRemoveMember): - __doc__ = _('Remove users and groups from an SELinux User Map rule.') - - member_attributes = ['memberuser'] - member_count_out = ('%i object removed.', '%i objects removed.') - - - -@register() -class selinuxusermap_add_host(LDAPAddMember): - __doc__ = _('Add target hosts and hostgroups to an SELinux User Map rule.') - - member_attributes = ['memberhost'] - member_count_out = ('%i object added.', '%i objects added.') - - def pre_callback(self, ldap, dn, found, not_found, *keys, **options): - assert isinstance(dn, DN) - try: - entry_attrs = ldap.get_entry(dn, self.obj.default_attributes) - dn = entry_attrs.dn - except errors.NotFound: - self.obj.handle_not_found(*keys) - if 'hostcategory' in entry_attrs and \ - entry_attrs['hostcategory'][0].lower() == 'all': - raise errors.MutuallyExclusiveError( - reason=_("hosts cannot be added when host category='all'")) - if 'seealso' in entry_attrs: - raise errors.MutuallyExclusiveError(reason=notboth_err) - return dn - - - -@register() -class selinuxusermap_remove_host(LDAPRemoveMember): - __doc__ = _('Remove target hosts and hostgroups from an SELinux User Map rule.') - - member_attributes = ['memberhost'] - member_count_out = ('%i object removed.', '%i objects removed.') - diff --git a/ipalib/plugins/server.py b/ipalib/plugins/server.py deleted file mode 100644 index 6faaf8ec5..000000000 --- a/ipalib/plugins/server.py +++ /dev/null @@ -1,260 +0,0 @@ -# -# Copyright (C) 2015 FreeIPA Contributors see COPYING for license -# - -import dbus -import dbus.mainloop.glib - -from ipalib import api, crud, errors, messages -from ipalib import Int, Str -from ipalib.plugable import Registry -from .baseldap import ( - LDAPSearch, - LDAPRetrieve, - LDAPDelete, - LDAPObject) -from ipalib.request import context -from ipalib import _, ngettext -from ipalib import output - -__doc__ = _(""" -IPA servers -""") + _(""" -Get information about installed IPA servers. -""") + _(""" -EXAMPLES: -""") + _(""" - Find all servers: - ipa server-find -""") + _(""" - Show specific server: - ipa server-show ipa.example.com -""") - -register = Registry() - - -@register() -class server(LDAPObject): - """ - IPA server - """ - container_dn = api.env.container_masters - object_name = _('server') - object_name_plural = _('servers') - object_class = ['top'] - search_attributes = ['cn'] - default_attributes = [ - 'cn', 'iparepltopomanagedsuffix', 'ipamindomainlevel', - 'ipamaxdomainlevel' - ] - label = _('IPA Servers') - label_singular = _('IPA Server') - attribute_members = { - 'iparepltopomanagedsuffix': ['topologysuffix'], - } - relationships = { - 'iparepltopomanagedsuffix': ('Managed', '', 'no_'), - } - takes_params = ( - Str( - 'cn', - cli_name='name', - primary_key=True, - label=_('Server name'), - doc=_('IPA server hostname'), - ), - Str( - 'iparepltopomanagedsuffix*', - flags={'no_create', 'no_update', 'no_search'}, - ), - Str( - 'iparepltopomanagedsuffix_topologysuffix*', - label=_('Managed suffixes'), - flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'}, - ), - Int( - 'ipamindomainlevel', - cli_name='minlevel', - label=_('Min domain level'), - doc=_('Minimum domain level'), - flags={'no_create', 'no_update'}, - ), - Int( - 'ipamaxdomainlevel', - cli_name='maxlevel', - label=_('Max domain level'), - doc=_('Maximum domain level'), - flags={'no_create', 'no_update'}, - ), - ) - - def _get_suffixes(self): - suffixes = self.api.Command.topologysuffix_find( - all=True, raw=True, - )['result'] - suffixes = [(s['iparepltopoconfroot'][0], s['dn']) for s in suffixes] - return suffixes - - def _apply_suffixes(self, entry, suffixes): - # change suffix DNs to topologysuffix entry DNs - # this fixes LDAPObject.convert_attribute_members() for suffixes - suffixes = dict(suffixes) - if 'iparepltopomanagedsuffix' in entry: - entry['iparepltopomanagedsuffix'] = [ - suffixes.get(m, m) for m in entry['iparepltopomanagedsuffix'] - ] - - -@register() -class server_find(LDAPSearch): - __doc__ = _('Search for IPA servers.') - - msg_summary = ngettext( - '%(count)d IPA server matched', - '%(count)d IPA servers matched', 0 - ) - member_attributes = ['iparepltopomanagedsuffix'] - - def get_options(self): - for option in super(server_find, self).get_options(): - if option.name == 'topologysuffix': - option = option.clone(cli_name='topologysuffixes') - elif option.name == 'no_topologysuffix': - option = option.clone(cli_name='no_topologysuffixes') - yield option - - def get_member_filter(self, ldap, **options): - options.pop('topologysuffix', None) - options.pop('no_topologysuffix', None) - - return super(server_find, self).get_member_filter(ldap, **options) - - def pre_callback(self, ldap, filters, attrs_list, base_dn, scope, - *args, **options): - included = options.get('topologysuffix') - excluded = options.get('no_topologysuffix') - - if included or excluded: - topologysuffix = self.api.Object.topologysuffix - suffixes = self.obj._get_suffixes() - suffixes = {s[1]: s[0] for s in suffixes} - - if included: - included = [topologysuffix.get_dn(pk) for pk in included] - try: - included = [suffixes[dn] for dn in included] - except KeyError: - # force empty result - filter = '(!(objectclass=*))' - else: - filter = ldap.make_filter_from_attr( - 'iparepltopomanagedsuffix', included, ldap.MATCH_ALL - ) - filters = ldap.combine_filters( - (filters, filter), ldap.MATCH_ALL - ) - - if excluded: - excluded = [topologysuffix.get_dn(pk) for pk in excluded] - excluded = [suffixes[dn] for dn in excluded if dn in suffixes] - filter = ldap.make_filter_from_attr( - 'iparepltopomanagedsuffix', excluded, ldap.MATCH_NONE - ) - filters = ldap.combine_filters( - (filters, filter), ldap.MATCH_ALL - ) - - return (filters, base_dn, scope) - - def post_callback(self, ldap, entries, truncated, *args, **options): - if not options.get('raw', False): - suffixes = self.obj._get_suffixes() - for entry in entries: - self.obj._apply_suffixes(entry, suffixes) - - return truncated - - -@register() -class server_show(LDAPRetrieve): - __doc__ = _('Show IPA server.') - - def post_callback(self, ldap, dn, entry, *keys, **options): - if not options.get('raw', False): - suffixes = self.obj._get_suffixes() - self.obj._apply_suffixes(entry, suffixes) - - return dn - - -@register() -class server_del(LDAPDelete): - __doc__ = _('Delete IPA server.') - NO_CLI = True - msg_summary = _('Deleted IPA server "%(value)s"') - - -@register() -class server_conncheck(crud.PKQuery): - __doc__ = _("Check connection to remote IPA server.") - - NO_CLI = True - - takes_args = ( - Str( - 'remote_cn', - cli_name='remote_name', - label=_('Remote server name'), - doc=_('Remote IPA server hostname'), - ), - ) - - has_output = output.standard_value - - def execute(self, *keys, **options): - # the server must be the local host - if keys[-2] != api.env.host: - raise errors.ValidationError( - name='cn', error=_("must be \"%s\"") % api.env.host) - - # the server entry must exist - try: - self.obj.get_dn_if_exists(*keys[:-1]) - except errors.NotFound: - self.obj.handle_not_found(keys[-2]) - - # the user must have the Replication Administrators privilege - privilege = u'Replication Administrators' - privilege_dn = self.api.Object.privilege.get_dn(privilege) - ldap = self.obj.backend - filter = ldap.make_filter({ - 'krbprincipalname': context.principal, # pylint: disable=no-member - 'memberof': privilege_dn}, - rules=ldap.MATCH_ALL) - try: - ldap.find_entries(base_dn=self.api.env.basedn, filter=filter) - except errors.NotFound: - raise errors.ACIError( - info=_("not allowed to perform server connection check")) - - dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) - - bus = dbus.SystemBus() - obj = bus.get_object('org.freeipa.server', '/', - follow_name_owner_changes=True) - server = dbus.Interface(obj, 'org.freeipa.server') - - ret, stdout, stderr = server.conncheck(keys[-1]) - - result = dict( - result=(ret == 0), - value=keys[-2], - ) - - for line in stdout.splitlines(): - messages.add_message(options['version'], - result, - messages.ExternalCommandOutput(line=line)) - - return result diff --git a/ipalib/plugins/service.py b/ipalib/plugins/service.py deleted file mode 100644 index 7e3735583..000000000 --- a/ipalib/plugins/service.py +++ /dev/null @@ -1,889 +0,0 @@ -# Authors: -# Jason Gerard DeRose <jderose@redhat.com> -# Rob Crittenden <rcritten@redhat.com> -# Pavel Zuna <pzuna@redhat.com> -# -# Copyright (C) 2008 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/>. - -import six - -from ipalib import api, errors -from ipalib import Bytes, StrEnum, Bool, Str, Flag -from ipalib.plugable import Registry -from .baseldap import ( - host_is_master, - add_missing_object_class, - pkey_to_value, - LDAPObject, - LDAPCreate, - LDAPDelete, - LDAPUpdate, - LDAPSearch, - LDAPRetrieve, - LDAPAddMember, - LDAPRemoveMember, - LDAPQuery, - LDAPAddAttribute, - LDAPRemoveAttribute) -from ipalib import x509 -from ipalib import _, ngettext -from ipalib import util -from ipalib import output -from ipapython.dn import DN - -import nss.nss as nss - - -if six.PY3: - unicode = str - -__doc__ = _(""" -Services - -A IPA service represents a service that runs on a host. The IPA service -record can store a Kerberos principal, an SSL certificate, or both. - -An IPA service can be managed directly from a machine, provided that -machine has been given the correct permission. This is true even for -machines other than the one the service is associated with. For example, -requesting an SSL certificate using the host service principal credentials -of the host. To manage a service using host credentials you need to -kinit as the host: - - # kinit -kt /etc/krb5.keytab host/ipa.example.com@EXAMPLE.COM - -Adding an IPA service allows the associated service to request an SSL -certificate or keytab, but this is performed as a separate step; they -are not produced as a result of adding the service. - -Only the public aspect of a certificate is stored in a service record; -the private key is not stored. - -EXAMPLES: - - Add a new IPA service: - ipa service-add HTTP/web.example.com - - Allow a host to manage an IPA service certificate: - ipa service-add-host --hosts=web.example.com HTTP/web.example.com - ipa role-add-member --hosts=web.example.com certadmin - - Override a default list of supported PAC types for the service: - ipa service-mod HTTP/web.example.com --pac-type=MS-PAC - - A typical use case where overriding the PAC type is needed is NFS. - Currently the related code in the Linux kernel can only handle Kerberos - tickets up to a maximal size. Since the PAC data can become quite large it - is recommended to set --pac-type=NONE for NFS services. - - Delete an IPA service: - ipa service-del HTTP/web.example.com - - Find all IPA services associated with a host: - ipa service-find web.example.com - - Find all HTTP services: - ipa service-find HTTP - - Disable the service Kerberos key and SSL certificate: - ipa service-disable HTTP/web.example.com - - Request a certificate for an IPA service: - ipa cert-request --principal=HTTP/web.example.com example.csr -""") + _(""" - Allow user to create a keytab: - ipa service-allow-create-keytab HTTP/web.example.com --users=tuser1 -""") + _(""" - Generate and retrieve a keytab for an IPA service: - ipa-getkeytab -s ipa.example.com -p HTTP/web.example.com -k /etc/httpd/httpd.keytab - -""") - -register = Registry() - -output_params = ( - Flag('has_keytab', - label=_('Keytab'), - ), - Str('managedby_host', - label='Managed by', - ), - Str('subject', - label=_('Subject'), - ), - Str('serial_number', - label=_('Serial Number'), - ), - Str('serial_number_hex', - label=_('Serial Number (hex)'), - ), - Str('issuer', - label=_('Issuer'), - ), - Str('valid_not_before', - label=_('Not Before'), - ), - Str('valid_not_after', - label=_('Not After'), - ), - Str('md5_fingerprint', - label=_('Fingerprint (MD5)'), - ), - Str('sha1_fingerprint', - label=_('Fingerprint (SHA1)'), - ), - Str('revocation_reason?', - label=_('Revocation reason'), - ), - Str('ipaallowedtoperform_read_keys_user', - label=_('Users allowed to retrieve keytab'), - ), - Str('ipaallowedtoperform_read_keys_group', - label=_('Groups allowed to retrieve keytab'), - ), - Str('ipaallowedtoperform_read_keys_host', - label=_('Hosts allowed to retrieve keytab'), - ), - Str('ipaallowedtoperform_read_keys_hostgroup', - label=_('Host Groups allowed to retrieve keytab'), - ), - Str('ipaallowedtoperform_write_keys_user', - label=_('Users allowed to create keytab'), - ), - Str('ipaallowedtoperform_write_keys_group', - label=_('Groups allowed to create keytab'), - ), - Str('ipaallowedtoperform_write_keys_host', - label=_('Hosts allowed to create keytab'), - ), - Str('ipaallowedtoperform_write_keys_hostgroup', - label=_('Host Groups allowed to create keytab'), - ), - Str('ipaallowedtoperform_read_keys', - label=_('Failed allowed to retrieve keytab'), - ), - Str('ipaallowedtoperform_write_keys', - label=_('Failed allowed to create keytab'), - ), -) - -ticket_flags_params = ( - Bool('ipakrbrequirespreauth?', - cli_name='requires_pre_auth', - label=_('Requires pre-authentication'), - doc=_('Pre-authentication is required for the service'), - flags=['virtual_attribute', 'no_search'], - ), - Bool('ipakrbokasdelegate?', - cli_name='ok_as_delegate', - label=_('Trusted for delegation'), - doc=_('Client credentials may be delegated to the service'), - flags=['virtual_attribute', 'no_search'], - ), -) - -_ticket_flags_map = { - 'ipakrbrequirespreauth': 0x00000080, - 'ipakrbokasdelegate': 0x00100000, -} - -_ticket_flags_default = _ticket_flags_map['ipakrbrequirespreauth'] - -def split_any_principal(principal): - service = hostname = realm = None - - # Break down the principal into its component parts, which may or - # may not include the realm. - sp = principal.split('/') - name_and_realm = None - if len(sp) > 2: - raise errors.MalformedServicePrincipal(reason=_('unable to determine service')) - elif len(sp) == 2: - service = sp[0] - if len(service) == 0: - raise errors.MalformedServicePrincipal(reason=_('blank service')) - name_and_realm = sp[1] - else: - name_and_realm = sp[0] - - sr = name_and_realm.split('@') - if len(sr) > 2: - raise errors.MalformedServicePrincipal( - reason=_('unable to determine realm')) - - hostname = sr[0].lower() - if len(sr) == 2: - realm = sr[1].upper() - # At some point we'll support multiple realms - if realm != api.env.realm: - raise errors.RealmMismatch() - else: - realm = api.env.realm - - # Note that realm may be None. - return service, hostname, realm - -def split_principal(principal): - service, name, realm = split_any_principal(principal) - if service is None: - raise errors.MalformedServicePrincipal(reason=_('missing service')) - return service, name, realm - -def validate_principal(ugettext, principal): - (service, hostname, principal) = split_principal(principal) - return None - -def normalize_principal(principal): - # The principal is already validated when it gets here - (service, hostname, realm) = split_principal(principal) - # Put the principal back together again - principal = '%s/%s@%s' % (service, hostname, realm) - return unicode(principal) - -def validate_certificate(ugettext, cert): - """ - Check whether the certificate is properly encoded to DER - """ - if api.env.in_server: - x509.validate_certificate(cert, datatype=x509.DER) - - -def revoke_certs(certs, logger=None): - """ - revoke the certificates removed from host/service entry - """ - for cert in certs: - try: - cert = x509.normalize_certificate(cert) - except errors.CertificateFormatError as e: - if logger is not None: - logger.info("Problem decoding certificate: %s" % e) - - serial = unicode(x509.get_serial_number(cert, x509.DER)) - - try: - result = api.Command['cert_show'](unicode(serial))['result'] - except errors.CertificateOperationError: - continue - if 'revocation_reason' in result: - continue - if x509.normalize_certificate(result['certificate']) != cert: - continue - - try: - api.Command['cert_revoke'](unicode(serial), - revocation_reason=4) - except errors.NotImplementedError: - # some CA's might not implement revoke - pass - - - -def set_certificate_attrs(entry_attrs): - """ - Set individual attributes from some values from a certificate. - - entry_attrs is a dict of an entry - - returns nothing - """ - if not 'usercertificate' in entry_attrs: - return - if type(entry_attrs['usercertificate']) in (list, tuple): - cert = entry_attrs['usercertificate'][0] - else: - cert = entry_attrs['usercertificate'] - cert = x509.normalize_certificate(cert) - cert = x509.load_certificate(cert, datatype=x509.DER) - entry_attrs['subject'] = unicode(cert.subject) - entry_attrs['serial_number'] = unicode(cert.serial_number) - entry_attrs['serial_number_hex'] = u'0x%X' % cert.serial_number - entry_attrs['issuer'] = unicode(cert.issuer) - entry_attrs['valid_not_before'] = unicode(cert.valid_not_before_str) - entry_attrs['valid_not_after'] = unicode(cert.valid_not_after_str) - entry_attrs['md5_fingerprint'] = unicode(nss.data_to_hex(nss.md5_digest(cert.der_data), 64)[0]) - entry_attrs['sha1_fingerprint'] = unicode(nss.data_to_hex(nss.sha1_digest(cert.der_data), 64)[0]) - -def check_required_principal(ldap, hostname, service): - """ - Raise an error if the host of this prinicipal is an IPA master and one - of the principals required for proper execution. - """ - try: - host_is_master(ldap, hostname) - except errors.ValidationError as e: - service_types = ['HTTP', 'ldap', 'DNS', 'dogtagldap'] - if service in service_types: - raise errors.ValidationError(name='principal', error=_('This principal is required by the IPA master')) - -def update_krbticketflags(ldap, entry_attrs, attrs_list, options, existing): - add = remove = 0 - - for (name, value) in _ticket_flags_map.items(): - if name not in options: - continue - if options[name]: - add |= value - else: - remove |= value - - if not add and not remove: - return - - if 'krbticketflags' not in entry_attrs and existing: - old_entry_attrs = ldap.get_entry(entry_attrs.dn, ['krbticketflags']) - else: - old_entry_attrs = entry_attrs - - try: - ticket_flags = old_entry_attrs.single_value['krbticketflags'] - ticket_flags = int(ticket_flags) - except (KeyError, ValueError): - ticket_flags = _ticket_flags_default - - ticket_flags |= add - ticket_flags &= ~remove - - entry_attrs['krbticketflags'] = [ticket_flags] - attrs_list.append('krbticketflags') - -def set_kerberos_attrs(entry_attrs, options): - if options.get('raw', False): - return - - try: - ticket_flags = entry_attrs.single_value.get('krbticketflags', - _ticket_flags_default) - ticket_flags = int(ticket_flags) - except ValueError: - return - - all_opt = options.get('all', False) - - for (name, value) in _ticket_flags_map.items(): - if name in options or all_opt: - entry_attrs[name] = bool(ticket_flags & value) - -def rename_ipaallowedtoperform_from_ldap(entry_attrs, options): - if options.get('raw', False): - return - - for subtype in ('read_keys', 'write_keys'): - name = 'ipaallowedtoperform;%s' % subtype - if name in entry_attrs: - new_name = 'ipaallowedtoperform_%s' % subtype - entry_attrs[new_name] = entry_attrs.pop(name) - -def rename_ipaallowedtoperform_to_ldap(entry_attrs): - for subtype in ('read_keys', 'write_keys'): - name = 'ipaallowedtoperform_%s' % subtype - if name in entry_attrs: - new_name = 'ipaallowedtoperform;%s' % subtype - entry_attrs[new_name] = entry_attrs.pop(name) - -@register() -class service(LDAPObject): - """ - Service object. - """ - container_dn = api.env.container_service - object_name = _('service') - object_name_plural = _('services') - object_class = [ - 'krbprincipal', 'krbprincipalaux', 'krbticketpolicyaux', 'ipaobject', - 'ipaservice', 'pkiuser' - ] - possible_objectclasses = ['ipakrbprincipal', 'ipaallowedoperations'] - permission_filter_objectclasses = ['ipaservice'] - search_attributes = ['krbprincipalname', 'managedby', 'ipakrbauthzdata'] - default_attributes = ['krbprincipalname', 'usercertificate', 'managedby', - 'ipakrbauthzdata', 'memberof', 'ipaallowedtoperform', 'krbprincipalauthind'] - uuid_attribute = 'ipauniqueid' - attribute_members = { - 'managedby': ['host'], - 'memberof': ['role'], - 'ipaallowedtoperform_read_keys': ['user', 'group', 'host', 'hostgroup'], - 'ipaallowedtoperform_write_keys': ['user', 'group', 'host', 'hostgroup'], - } - bindable = True - relationships = { - 'managedby': ('Managed by', 'man_by_', 'not_man_by_'), - 'ipaallowedtoperform_read_keys': ('Allow to retrieve keytab by', 'retrieve_keytab_by_', 'not_retrieve_keytab_by_'), - 'ipaallowedtoperform_write_keys': ('Allow to create keytab by', 'write_keytab_by_', 'not_write_keytab_by'), - } - password_attributes = [('krbprincipalkey', 'has_keytab')] - managed_permissions = { - 'System: Read Services': { - 'replaces_global_anonymous_aci': True, - 'ipapermbindruletype': 'all', - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'objectclass', - 'ipauniqueid', 'managedby', 'memberof', 'usercertificate', - 'krbprincipalname', 'krbcanonicalname', 'krbprincipalaliases', - 'krbprincipalexpiration', 'krbpasswordexpiration', - 'krblastpwdchange', 'ipakrbauthzdata', 'ipakrbprincipalalias', - 'krbobjectreferences', - }, - }, - 'System: Add Services': { - 'ipapermright': {'add'}, - 'replaces': [ - '(target = "ldap:///krbprincipalname=*,cn=services,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Add Services";allow (add) groupdn = "ldap:///cn=Add Services,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Service Administrators'}, - }, - 'System: Manage Service Keytab': { - 'ipapermright': {'write'}, - 'ipapermdefaultattr': {'krblastpwdchange', 'krbprincipalkey'}, - 'replaces': [ - '(targetattr = "krbprincipalkey || krblastpwdchange")(target = "ldap:///krbprincipalname=*,cn=services,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Manage service keytab";allow (write) groupdn = "ldap:///cn=Manage service keytab,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Service Administrators', 'Host Administrators'}, - }, - 'System: Manage Service Keytab Permissions': { - 'ipapermright': {'read', 'search', 'compare', 'write'}, - 'ipapermdefaultattr': { - 'ipaallowedtoperform;write_keys', - 'ipaallowedtoperform;read_keys', 'objectclass' - }, - 'default_privileges': {'Service Administrators', 'Host Administrators'}, - }, - 'System: Modify Services': { - 'ipapermright': {'write'}, - 'ipapermdefaultattr': {'usercertificate'}, - 'replaces': [ - '(targetattr = "usercertificate")(target = "ldap:///krbprincipalname=*,cn=services,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Modify Services";allow (write) groupdn = "ldap:///cn=Modify Services,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Service Administrators'}, - }, - 'System: Remove Services': { - 'ipapermright': {'delete'}, - 'replaces': [ - '(target = "ldap:///krbprincipalname=*,cn=services,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Remove Services";allow (delete) groupdn = "ldap:///cn=Remove Services,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Service Administrators'}, - }, - } - - label = _('Services') - label_singular = _('Service') - - takes_params = ( - Str('krbprincipalname', validate_principal, - cli_name='principal', - label=_('Principal'), - doc=_('Service principal'), - primary_key=True, - normalizer=lambda value: normalize_principal(value), - ), - Bytes('usercertificate*', validate_certificate, - cli_name='certificate', - label=_('Certificate'), - doc=_('Base-64 encoded service certificate'), - flags=['no_search',], - ), - StrEnum('ipakrbauthzdata*', - cli_name='pac_type', - label=_('PAC type'), - doc=_("Override default list of supported PAC types." - " Use 'NONE' to disable PAC support for this service," - " e.g. this might be necessary for NFS services."), - values=(u'MS-PAC', u'PAD', u'NONE'), - ), - Str('krbprincipalauthind*', - cli_name='auth_ind', - label=_('Authentication Indicators'), - doc=_("Defines a whitelist for Authentication Indicators." - " Use 'otp' to allow OTP-based 2FA authentications." - " Use 'radius' to allow RADIUS-based 2FA authentications." - " Other values may be used for custom configurations."), - ), - ) + ticket_flags_params - - def validate_ipakrbauthzdata(self, entry): - new_value = entry.get('ipakrbauthzdata', []) - - if not new_value: - return - - if not isinstance(new_value, (list, tuple)): - new_value = set([new_value]) - else: - new_value = set(new_value) - - if u'NONE' in new_value and len(new_value) > 1: - raise errors.ValidationError(name='ipakrbauthzdata', - error=_('NONE value cannot be combined with other PAC types')) - - def get_dn(self, *keys, **kwargs): - keys = (normalize_principal(k) for k in keys) - return super(service, self).get_dn(*keys, **kwargs) - - -@register() -class service_add(LDAPCreate): - __doc__ = _('Add a new IPA service.') - - msg_summary = _('Added service "%(value)s"') - member_attributes = ['managedby'] - has_output_params = LDAPCreate.has_output_params + output_params - takes_options = LDAPCreate.takes_options + ( - Flag('force', - label=_('Force'), - doc=_('force principal name even if not in DNS'), - ), - ) - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - (service, hostname, realm) = split_principal(keys[-1]) - if service.lower() == 'host' and not options['force']: - raise errors.HostService() - - try: - hostresult = api.Command['host_show'](hostname)['result'] - except errors.NotFound: - raise errors.NotFound( - reason=_("The host '%s' does not exist to add a service to.") % - hostname) - - self.obj.validate_ipakrbauthzdata(entry_attrs) - - certs = options.get('usercertificate', []) - certs_der = [x509.normalize_certificate(c) for c in certs] - for dercert in certs_der: - x509.verify_cert_subject(ldap, hostname, dercert) - entry_attrs['usercertificate'] = certs_der - - if not options.get('force', False): - # We know the host exists if we've gotten this far but we - # really want to discourage creating services for hosts that - # don't exist in DNS. - util.verify_host_resolvable(hostname) - if not 'managedby' in entry_attrs: - entry_attrs['managedby'] = hostresult['dn'] - - # Enforce ipaKrbPrincipalAlias to aid case-insensitive searches - # as krbPrincipalName/krbCanonicalName are case-sensitive in Kerberos - # schema - entry_attrs['ipakrbprincipalalias'] = keys[-1] - - # Objectclass ipakrbprincipal providing ipakrbprincipalalias is not in - # in a list of default objectclasses, add it manually - entry_attrs['objectclass'].append('ipakrbprincipal') - - update_krbticketflags(ldap, entry_attrs, attrs_list, options, False) - - return dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - set_kerberos_attrs(entry_attrs, options) - rename_ipaallowedtoperform_from_ldap(entry_attrs, options) - return dn - - - -@register() -class service_del(LDAPDelete): - __doc__ = _('Delete an IPA service.') - - msg_summary = _('Deleted service "%(value)s"') - member_attributes = ['managedby'] - def pre_callback(self, ldap, dn, *keys, **options): - assert isinstance(dn, DN) - # In the case of services we don't want IPA master services to be - # deleted. This is a limited few though. If the user has their own - # custom services allow them to manage them. - (service, hostname, realm) = split_principal(keys[-1]) - check_required_principal(ldap, hostname, service) - if self.api.Command.ca_is_enabled()['result']: - try: - entry_attrs = ldap.get_entry(dn, ['usercertificate']) - except errors.NotFound: - self.obj.handle_not_found(*keys) - revoke_certs(entry_attrs.get('usercertificate', []), self.log) - - return dn - - - -@register() -class service_mod(LDAPUpdate): - __doc__ = _('Modify an existing IPA service.') - - msg_summary = _('Modified service "%(value)s"') - takes_options = LDAPUpdate.takes_options - has_output_params = LDAPUpdate.has_output_params + output_params - - member_attributes = ['managedby'] - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - - self.obj.validate_ipakrbauthzdata(entry_attrs) - - (service, hostname, realm) = split_principal(keys[-1]) - - # verify certificates - certs = entry_attrs.get('usercertificate') or [] - certs_der = [x509.normalize_certificate(c) for c in certs] - for dercert in certs_der: - x509.verify_cert_subject(ldap, hostname, dercert) - # revoke removed certificates - if certs and self.api.Command.ca_is_enabled()['result']: - try: - entry_attrs_old = ldap.get_entry(dn, ['usercertificate']) - except errors.NotFound: - self.obj.handle_not_found(*keys) - old_certs = entry_attrs_old.get('usercertificate', []) - old_certs_der = [x509.normalize_certificate(c) for c in old_certs] - removed_certs_der = set(old_certs_der) - set(certs_der) - revoke_certs(removed_certs_der, self.log) - - if certs: - entry_attrs['usercertificate'] = certs_der - - update_krbticketflags(ldap, entry_attrs, attrs_list, options, True) - - return dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - set_certificate_attrs(entry_attrs) - set_kerberos_attrs(entry_attrs, options) - rename_ipaallowedtoperform_from_ldap(entry_attrs, options) - return dn - - - -@register() -class service_find(LDAPSearch): - __doc__ = _('Search for IPA services.') - - msg_summary = ngettext( - '%(count)d service matched', '%(count)d services matched', 0 - ) - member_attributes = ['managedby'] - takes_options = LDAPSearch.takes_options - has_output_params = LDAPSearch.has_output_params + output_params - - def pre_callback(self, ldap, filter, attrs_list, base_dn, scope, *args, **options): - assert isinstance(base_dn, DN) - # lisp style! - custom_filter = '(&(objectclass=ipaService)' \ - '(!(objectClass=posixAccount))' \ - '(!(|(krbprincipalname=kadmin/*)' \ - '(krbprincipalname=K/M@*)' \ - '(krbprincipalname=krbtgt/*))' \ - ')' \ - ')' - return ( - ldap.combine_filters((custom_filter, filter), rules=ldap.MATCH_ALL), - base_dn, scope - ) - - def post_callback(self, ldap, entries, truncated, *args, **options): - if options.get('pkey_only', False): - return truncated - for entry_attrs in entries: - self.obj.get_password_attributes(ldap, entry_attrs.dn, entry_attrs) - set_certificate_attrs(entry_attrs) - set_kerberos_attrs(entry_attrs, options) - rename_ipaallowedtoperform_from_ldap(entry_attrs, options) - return truncated - - - -@register() -class service_show(LDAPRetrieve): - __doc__ = _('Display information about an IPA service.') - - member_attributes = ['managedby'] - takes_options = LDAPRetrieve.takes_options + ( - Str('out?', - doc=_('file to store certificate in'), - ), - ) - has_output_params = LDAPRetrieve.has_output_params + output_params - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - self.obj.get_password_attributes(ldap, dn, entry_attrs) - - set_certificate_attrs(entry_attrs) - set_kerberos_attrs(entry_attrs, options) - rename_ipaallowedtoperform_from_ldap(entry_attrs, options) - - return dn - - -@register() -class service_add_host(LDAPAddMember): - __doc__ = _('Add hosts that can manage this service.') - - member_attributes = ['managedby'] - has_output_params = LDAPAddMember.has_output_params + output_params - - - -@register() -class service_remove_host(LDAPRemoveMember): - __doc__ = _('Remove hosts that can manage this service.') - - member_attributes = ['managedby'] - has_output_params = LDAPRemoveMember.has_output_params + output_params - - -@register() -class service_allow_retrieve_keytab(LDAPAddMember): - __doc__ = _('Allow users, groups, hosts or host groups to retrieve a keytab' - ' of this service.') - member_attributes = ['ipaallowedtoperform_read_keys'] - has_output_params = LDAPAddMember.has_output_params + output_params - - def pre_callback(self, ldap, dn, found, not_found, *keys, **options): - rename_ipaallowedtoperform_to_ldap(found) - rename_ipaallowedtoperform_to_ldap(not_found) - add_missing_object_class(ldap, u'ipaallowedoperations', dn) - return dn - - def post_callback(self, ldap, completed, failed, dn, entry_attrs, *keys, **options): - rename_ipaallowedtoperform_from_ldap(entry_attrs, options) - rename_ipaallowedtoperform_from_ldap(failed, options) - return (completed, dn) - - -@register() -class service_disallow_retrieve_keytab(LDAPRemoveMember): - __doc__ = _('Disallow users, groups, hosts or host groups to retrieve a ' - 'keytab of this service.') - member_attributes = ['ipaallowedtoperform_read_keys'] - has_output_params = LDAPRemoveMember.has_output_params + output_params - - def pre_callback(self, ldap, dn, found, not_found, *keys, **options): - rename_ipaallowedtoperform_to_ldap(found) - rename_ipaallowedtoperform_to_ldap(not_found) - return dn - - def post_callback(self, ldap, completed, failed, dn, entry_attrs, *keys, **options): - rename_ipaallowedtoperform_from_ldap(entry_attrs, options) - rename_ipaallowedtoperform_from_ldap(failed, options) - return (completed, dn) - - -@register() -class service_allow_create_keytab(LDAPAddMember): - __doc__ = _('Allow users, groups, hosts or host groups to create a keytab ' - 'of this service.') - member_attributes = ['ipaallowedtoperform_write_keys'] - has_output_params = LDAPAddMember.has_output_params + output_params - - def pre_callback(self, ldap, dn, found, not_found, *keys, **options): - rename_ipaallowedtoperform_to_ldap(found) - rename_ipaallowedtoperform_to_ldap(not_found) - add_missing_object_class(ldap, u'ipaallowedoperations', dn) - return dn - - def post_callback(self, ldap, completed, failed, dn, entry_attrs, *keys, **options): - rename_ipaallowedtoperform_from_ldap(entry_attrs, options) - rename_ipaallowedtoperform_from_ldap(failed, options) - return (completed, dn) - - -@register() -class service_disallow_create_keytab(LDAPRemoveMember): - __doc__ = _('Disallow users, groups, hosts or host groups to create a ' - 'keytab of this service.') - member_attributes = ['ipaallowedtoperform_write_keys'] - has_output_params = LDAPRemoveMember.has_output_params + output_params - - def pre_callback(self, ldap, dn, found, not_found, *keys, **options): - rename_ipaallowedtoperform_to_ldap(found) - rename_ipaallowedtoperform_to_ldap(not_found) - return dn - - def post_callback(self, ldap, completed, failed, dn, entry_attrs, *keys, **options): - rename_ipaallowedtoperform_from_ldap(entry_attrs, options) - rename_ipaallowedtoperform_from_ldap(failed, options) - return (completed, dn) - - -@register() -class service_disable(LDAPQuery): - __doc__ = _('Disable the Kerberos key and SSL certificate of a service.') - - has_output = output.standard_value - msg_summary = _('Disabled service "%(value)s"') - has_output_params = LDAPQuery.has_output_params + output_params - - def execute(self, *keys, **options): - ldap = self.obj.backend - - dn = self.obj.get_dn(*keys, **options) - entry_attrs = ldap.get_entry(dn, ['usercertificate']) - - (service, hostname, realm) = split_principal(keys[-1]) - check_required_principal(ldap, hostname, service) - - # See if we do any work at all here and if not raise an exception - done_work = False - - if self.api.Command.ca_is_enabled()['result']: - certs = entry_attrs.get('usercertificate', []) - - if len(certs) > 0: - revoke_certs(certs, self.log) - # Remove the usercertificate altogether - entry_attrs['usercertificate'] = None - ldap.update_entry(entry_attrs) - done_work = True - - self.obj.get_password_attributes(ldap, dn, entry_attrs) - if entry_attrs['has_keytab']: - ldap.remove_principal_key(dn) - done_work = True - - if not done_work: - raise errors.AlreadyInactive() - - return dict( - result=True, - value=pkey_to_value(keys[0], options), - ) - - -@register() -class service_add_cert(LDAPAddAttribute): - __doc__ = _('Add new certificates to a service') - msg_summary = _('Added certificates to service principal "%(value)s"') - attribute = 'usercertificate' - - -@register() -class service_remove_cert(LDAPRemoveAttribute): - __doc__ = _('Remove certificates from a service') - msg_summary = _('Removed certificates from service principal "%(value)s"') - attribute = 'usercertificate' - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - - if 'usercertificate' in options: - revoke_certs(options['usercertificate'], self.log) - - return dn diff --git a/ipalib/plugins/servicedelegation.py b/ipalib/plugins/servicedelegation.py deleted file mode 100644 index 958c3b739..000000000 --- a/ipalib/plugins/servicedelegation.py +++ /dev/null @@ -1,550 +0,0 @@ -# -# Copyright (C) 2015 FreeIPA Contributors see COPYING for license -# - -import six - -from ipalib import api -from ipalib import Str -from ipalib.plugable import Registry -from .baseldap import ( - LDAPObject, - LDAPAddMember, - LDAPRemoveMember, - LDAPCreate, - LDAPDelete, - LDAPSearch, - LDAPRetrieve) -from .service import normalize_principal -from ipalib import _, ngettext -from ipalib import errors -from ipapython.dn import DN - -if six.PY3: - unicode = str - -__doc__ = _(""" -Service Constrained Delegation - -Manage rules to allow constrained delegation of credentials so -that a service can impersonate a user when communicating with another -service without requiring the user to actually forward their TGT. -This makes for a much better method of delegating credentials as it -prevents exposure of the short term secret of the user. - -The naming convention is to append the word "target" or "targets" to -a matching rule name. This is not mandatory but helps conceptually -to associate rules and targets. - -A rule consists of two things: - - A list of targets the rule applies to - - A list of memberPrincipals that are allowed to delegate for - those targets - -A target consists of a list of principals that can be delegated. - -In English, a rule says that this principal can delegate as this -list of principals, as defined by these targets. - -EXAMPLES: - - Add a new constrained delegation rule: - ipa servicedelegationrule-add ftp-delegation - - Add a new constrained delegation target: - ipa servicedelegationtarget-add ftp-delegation-target - - Add a principal to the rule: - ipa servicedelegationrule-add-member --principals=ftp/ipa.example.com \ - ftp-delegation - - Add our target to the rule: - ipa servicedelegationrule-add-target \ - --servicedelegationtargets=ftp-delegation-target ftp-delegation - - Add a principal to the target: - ipa servicedelegationtarget-add-member --principals=ldap/ipa.example.com \ - ftp-delegation-target - - Display information about a named delegation rule and target: - ipa servicedelegationrule_show ftp-delegation - ipa servicedelegationtarget_show ftp-delegation-target - - Remove a constrained delegation: - ipa servicedelegationrule-del ftp-delegation-target - ipa servicedelegationtarget-del ftp-delegation - -In this example the ftp service can get a TGT for the ldap service on -the bound user's behalf. - -It is strongly discouraged to modify the delegations that ship with -IPA, ipa-http-delegation and its targets ipa-cifs-delegation-targets and -ipa-ldap-delegation-targets. Incorrect changes can remove the ability -to delegate, causing the framework to stop functioning. -""") - -register = Registry() - -PROTECTED_CONSTRAINT_RULES = ( - u'ipa-http-delegation', -) - -PROTECTED_CONSTRAINT_TARGETS = ( - u'ipa-cifs-delegation-targets', - u'ipa-ldap-delegation-targets', - -) - - -output_params = ( - Str( - 'ipaallowedtarget_servicedelegationtarget', - label=_('Allowed Target'), - ), - Str( - 'ipaallowedtoimpersonate', - label=_('Allowed to Impersonate'), - ), - Str( - 'memberprincipal', - label=_('Member principals'), - ), - Str( - 'failed_memberprincipal', - label=_('Failed members'), - ), - Str( - 'ipaallowedtarget', - label=_('Failed targets'), - ), -) - - -class servicedelegation(LDAPObject): - """ - Service Constrained Delegation base object. - - This jams a couple of concepts into a single plugin because the - data is all stored in one place. There is a "rule" which has the - objectclass ipakrb5delegationacl. This is the entry that controls - the delegation. Other entries that lack this objectclass are - targets and define what services can be impersonated. - """ - container_dn = api.env.container_s4u2proxy - object_class = ['groupofprincipals', 'top'] - - managed_permissions = { - 'System: Read Service Delegations': { - 'ipapermbindruletype': 'permission', - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermtargetfilter': {'(objectclass=groupofprincipals)'}, - 'ipapermdefaultattr': { - 'cn', 'objectclass', 'memberprincipal', - 'ipaallowedtarget', - }, - 'default_privileges': {'Service Administrators'}, - }, - 'System: Add Service Delegations': { - 'ipapermright': {'add'}, - 'ipapermtargetfilter': {'(objectclass=groupofprincipals)'}, - 'default_privileges': {'Service Administrators'}, - }, - 'System: Remove Service Delegations': { - 'ipapermright': {'delete'}, - 'ipapermtargetfilter': {'(objectclass=groupofprincipals)'}, - 'default_privileges': {'Service Administrators'}, - }, - 'System: Modify Service Delegation Membership': { - 'ipapermright': {'write'}, - 'ipapermtargetfilter': {'(objectclass=groupofprincipals)'}, - 'ipapermdefaultattr': {'memberprincipal', 'ipaallowedtarget'}, - 'default_privileges': {'Service Administrators'}, - }, - } - - rdn_is_primary_key = True - - takes_params = ( - Str( - 'cn', - pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_ .-]{0,253}[a-zA-Z0-9_.-]?$', - pattern_errmsg='may only include letters, numbers, _, -, ., ' - 'and a space inside', - maxlength=255, - cli_name='delegation_name', - label=_('Delegation name'), - primary_key=True, - ), - ) - - -class servicedelegation_add_member(LDAPAddMember): - __doc__ = _('Add target to a named service delegation.') - member_attrs = ['memberprincipal'] - member_attributes = [] - member_names = {} - principal_attr = 'memberprincipal' - principal_failedattr = 'failed_memberprincipal' - - has_output_params = LDAPAddMember.has_output_params + output_params - - def get_options(self): - for option in super(servicedelegation_add_member, self).get_options(): - yield option - for attr in self.member_attrs: - name = self.member_names[attr] - doc = self.member_param_doc % name - yield Str('%s*' % name, cli_name='%ss' % name, doc=doc, - label=_('member %s') % name, alwaysask=True) - - def get_member_dns(self, **options): - """ - There are no member_dns to return. memberPrincipal needs - special handling since it is just a principal, not a - full dn. - """ - return dict(), dict() - - def post_callback(self, ldap, completed, failed, dn, entry_attrs, - *keys, **options): - """ - Add memberPrincipal values. This is done afterward because it isn't - a DN and the LDAPAddMember method explicitly only handles DNs. - - A separate fake attribute name is used for failed members. This is - a reverse of the way this is typically handled in the *Member - routines, where a successful addition will be represented as - member/memberof_<attribute>. In this case, because memberPrincipal - isn't a DN, I'm doing the reverse, and creating a fake failed - attribute instead. - """ - ldap = self.obj.backend - members = [] - failed[self.principal_failedattr] = {} - failed[self.principal_failedattr][self.principal_attr] = [] - names = options.get(self.member_names[self.principal_attr], []) - ldap_obj = self.api.Object['service'] - if names: - for name in names: - if not name: - continue - name = normalize_principal(name) - obj_dn = ldap_obj.get_dn(name) - try: - ldap.get_entry(obj_dn, ['krbprincipalname']) - except errors.NotFound as e: - failed[self.principal_failedattr][ - self.principal_attr].append((name, unicode(e))) - continue - try: - if name not in entry_attrs.get(self.principal_attr, []): - members.append(name) - else: - raise errors.AlreadyGroupMember() - except errors.PublicError as e: - failed[self.principal_failedattr][ - self.principal_attr].append((name, unicode(e))) - else: - completed += 1 - - if members: - value = entry_attrs.setdefault(self.principal_attr, []) - value.extend(members) - - try: - ldap.update_entry(entry_attrs) - except errors.EmptyModlist: - pass - - return completed, dn - - -class servicedelegation_remove_member(LDAPRemoveMember): - __doc__ = _('Remove member from a named service delegation.') - - member_attrs = ['memberprincipal'] - member_attributes = [] - member_names = {} - principal_attr = 'memberprincipal' - principal_failedattr = 'failed_memberprincipal' - - has_output_params = LDAPRemoveMember.has_output_params + output_params - - def get_options(self): - for option in super( - servicedelegation_remove_member, self).get_options(): - yield option - for attr in self.member_attrs: - name = self.member_names[attr] - doc = self.member_param_doc % name - yield Str('%s*' % name, cli_name='%ss' % name, doc=doc, - label=_('member %s') % name, alwaysask=True) - - def get_member_dns(self, **options): - """ - Need to ignore memberPrincipal for now and handle the difference - in objectclass between a rule and a target. - """ - dns = {} - failed = {} - for attr in self.member_attrs: - dns[attr] = {} - if attr.lower() == 'memberprincipal': - # This will be handled later. memberprincipal isn't a - # DN so will blow up in assertions in baseldap. - continue - failed[attr] = {} - for ldap_obj_name in self.obj.attribute_members[attr]: - dns[attr][ldap_obj_name] = [] - failed[attr][ldap_obj_name] = [] - names = options.get(self.member_names[attr], []) - if not names: - continue - for name in names: - if not name: - continue - ldap_obj = self.api.Object[ldap_obj_name] - try: - dns[attr][ldap_obj_name].append(ldap_obj.get_dn(name)) - except errors.PublicError as e: - failed[attr][ldap_obj_name].append((name, unicode(e))) - return dns, failed - - def post_callback(self, ldap, completed, failed, dn, entry_attrs, - *keys, **options): - """ - Remove memberPrincipal values. This is done afterward because it - isn't a DN and the LDAPAddMember method explicitly only handles DNs. - - See servicedelegation_add_member() for an explanation of what - failedattr is. - """ - ldap = self.obj.backend - failed[self.principal_failedattr] = {} - failed[self.principal_failedattr][self.principal_attr] = [] - names = options.get(self.member_names[self.principal_attr], []) - if names: - for name in names: - if not name: - continue - name = normalize_principal(name) - try: - if name in entry_attrs.get(self.principal_attr, []): - entry_attrs[self.principal_attr].remove(name) - else: - raise errors.NotGroupMember() - except errors.PublicError as e: - failed[self.principal_failedattr][ - self.principal_attr].append((name, unicode(e))) - else: - completed += 1 - - try: - ldap.update_entry(entry_attrs) - except errors.EmptyModlist: - pass - - return completed, dn - - -@register() -class servicedelegationrule(servicedelegation): - """ - A service delegation rule. This is the ACL that controls - what can be delegated to whom. - """ - object_name = _('service delegation rule') - object_name_plural = _('service delegation rules') - object_class = ['ipakrb5delegationacl', 'groupofprincipals', 'top'] - default_attributes = [ - 'cn', 'memberprincipal', 'ipaallowedtarget', - 'ipaallowedtoimpersonate', - ] - attribute_members = { - # memberprincipal is not listed because it isn't a DN - 'ipaallowedtarget': ['servicedelegationtarget'], - } - - label = _('Service delegation rules') - label_singular = _('Service delegation rule') - - -@register() -class servicedelegationrule_add(LDAPCreate): - __doc__ = _('Create a new service delegation rule.') - - msg_summary = _('Added service delegation rule "%(value)s"') - - -@register() -class servicedelegationrule_del(LDAPDelete): - __doc__ = _('Delete service delegation.') - - msg_summary = _('Deleted service delegation "%(value)s"') - - def pre_callback(self, ldap, dn, *keys, **options): - assert isinstance(dn, DN) - if keys[0] in PROTECTED_CONSTRAINT_RULES: - raise errors.ProtectedEntryError( - label=_(u'service delegation rule'), - key=keys[0], - reason=_(u'privileged service delegation rule') - ) - return dn - - -@register() -class servicedelegationrule_find(LDAPSearch): - __doc__ = _('Search for service delegations rule.') - - has_output_params = LDAPSearch.has_output_params + output_params - - msg_summary = ngettext( - '%(count)d service delegation rule matched', - '%(count)d service delegation rules matched', 0 - ) - - -@register() -class servicedelegationrule_show(LDAPRetrieve): - __doc__ = _('Display information about a named service delegation rule.') - - has_output_params = LDAPRetrieve.has_output_params + output_params - - -@register() -class servicedelegationrule_add_member(servicedelegation_add_member): - __doc__ = _('Add member to a named service delegation rule.') - - member_names = { - 'memberprincipal': 'principal', - } - - -@register() -class servicedelegationrule_remove_member(servicedelegation_remove_member): - __doc__ = _('Remove member from a named service delegation rule.') - member_names = { - 'memberprincipal': 'principal', - } - - -@register() -class servicedelegationrule_add_target(LDAPAddMember): - __doc__ = _('Add target to a named service delegation rule.') - - member_attributes = ['ipaallowedtarget'] - attribute_members = { - 'ipaallowedtarget': ['servicedelegationtarget'], - } - has_output_params = LDAPAddMember.has_output_params + output_params - - -@register() -class servicedelegationrule_remove_target(LDAPRemoveMember): - __doc__ = _('Remove target from a named service delegation rule.') - member_attributes = ['ipaallowedtarget'] - attribute_members = { - 'ipaallowedtarget': ['servicedelegationtarget'], - } - has_output_params = LDAPRemoveMember.has_output_params + output_params - - -@register() -class servicedelegationtarget(servicedelegation): - object_name = _('service delegation target') - object_name_plural = _('service delegation targets') - object_class = ['groupofprincipals', 'top'] - default_attributes = [ - 'cn', 'memberprincipal', - ] - attribute_members = {} - - label = _('Service delegation targets') - label_singular = _('Service delegation target') - - -@register() -class servicedelegationtarget_add(LDAPCreate): - __doc__ = _('Create a new service delegation target.') - - msg_summary = _('Added service delegation target "%(value)s"') - - -@register() -class servicedelegationtarget_del(LDAPDelete): - __doc__ = _('Delete service delegation target.') - - msg_summary = _('Deleted service delegation target "%(value)s"') - - def pre_callback(self, ldap, dn, *keys, **options): - assert isinstance(dn, DN) - if keys[0] in PROTECTED_CONSTRAINT_TARGETS: - raise errors.ProtectedEntryError( - label=_(u'service delegation target'), - key=keys[0], - reason=_(u'privileged service delegation target') - ) - return dn - - -@register() -class servicedelegationtarget_find(LDAPSearch): - __doc__ = _('Search for service delegation target.') - - has_output_params = LDAPSearch.has_output_params + output_params - - msg_summary = ngettext( - '%(count)d service delegation target matched', - '%(count)d service delegation targets matched', 0 - ) - - def pre_callback(self, ldap, filters, attrs_list, base_dn, scope, - term=None, **options): - """ - Exclude rules from the search output. A target contains a subset - of a rule objectclass. - """ - search_kw = self.args_options_2_entry(**options) - search_kw['objectclass'] = self.obj.object_class - attr_filter = ldap.make_filter(search_kw, rules=ldap.MATCH_ALL) - rule_kw = {'objectclass': 'ipakrb5delegationacl'} - target_filter = ldap.make_filter(rule_kw, rules=ldap.MATCH_NONE) - attr_filter = ldap.combine_filters( - (target_filter, attr_filter), rules=ldap.MATCH_ALL - ) - - search_kw = {} - for a in self.obj.default_attributes: - search_kw[a] = term - - term_filter = ldap.make_filter(search_kw, exact=False) - - sfilter = ldap.combine_filters( - (term_filter, attr_filter), rules=ldap.MATCH_ALL - ) - return sfilter, base_dn, ldap.SCOPE_ONELEVEL - - -@register() -class servicedelegationtarget_show(LDAPRetrieve): - __doc__ = _('Display information about a named service delegation target.') - - has_output_params = LDAPRetrieve.has_output_params + output_params - - -@register() -class servicedelegationtarget_add_member(servicedelegation_add_member): - __doc__ = _('Add member to a named service delegation target.') - - member_names = { - 'memberprincipal': 'principal', - } - - -@register() -class servicedelegationtarget_remove_member(servicedelegation_remove_member): - __doc__ = _('Remove member from a named service delegation target.') - member_names = { - 'memberprincipal': 'principal', - } diff --git a/ipalib/plugins/session.py b/ipalib/plugins/session.py deleted file mode 100644 index b03b6b410..000000000 --- a/ipalib/plugins/session.py +++ /dev/null @@ -1,33 +0,0 @@ -# -# Copyright (C) 2015 FreeIPA Contributors see COPYING for license -# - -from ipalib import api, Command -from ipalib.request import context -from ipalib.plugable import Registry - -if api.env.in_server: - from ipalib.session import session_mgr - -register = Registry() - - -@register() -class session_logout(Command): - ''' - RPC command used to log the current user out of their session. - ''' - NO_CLI = True - - def execute(self, *args, **options): - session_data = getattr(context, 'session_data', None) - if session_data is None: - self.debug('session logout command: no session_data found') - else: - session_id = session_data.get('session_id') - self.debug('session logout command: session_id=%s', session_id) - - # Notifiy registered listeners - session_mgr.auth_mgr.logout(session_data) - - return dict(result=None) diff --git a/ipalib/plugins/stageuser.py b/ipalib/plugins/stageuser.py deleted file mode 100644 index 86b1935f3..000000000 --- a/ipalib/plugins/stageuser.py +++ /dev/null @@ -1,745 +0,0 @@ -# Authors: -# Thierry Bordaz <tbordaz@redhat.com> -# -# Copyright (C) 2014 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/>. - -import posixpath -from copy import deepcopy - -import six - -from ipalib import api, errors -from ipalib import Bool -from ipalib.plugable import Registry -from .baseldap import ( - LDAPCreate, - LDAPQuery, - DN) -from . import baseldap -from .baseuser import ( - baseuser, - baseuser_add, - baseuser_del, - baseuser_mod, - baseuser_find, - baseuser_show, - NO_UPG_MAGIC, - baseuser_pwdchars, - baseuser_output_params, - status_baseuser_output_params, - baseuser_add_manager, - baseuser_remove_manager) -from ipalib.request import context -from ipalib import _, ngettext -from ipalib import output -from ipaplatform.paths import paths -from ipapython.ipautil import ipa_generate_password -from ipalib.capabilities import client_has_capability - -if six.PY3: - unicode = str - -__doc__ = _(""" -Stageusers - -Manage stage user entries. - -Stage user entries are directly under the container: "cn=stage users, -cn=accounts, cn=provisioning, SUFFIX". -Users can not authenticate with those entries (even if the entries -contain credentials). Those entries are only candidate to become Active entries. - -Active user entries are Posix users directly under the container: "cn=accounts, SUFFIX". -Users can authenticate with Active entries, at the condition they have -credentials. - -Deleted user entries are Posix users directly under the container: "cn=deleted users, -cn=accounts, cn=provisioning, SUFFIX". -Users can not authenticate with those entries, even if the entries contain credentials. - -The stage user container contains entries: - - created by 'stageuser-add' commands that are Posix users, - - created by external provisioning system. - -A valid stage user entry MUST have: - - entry RDN is 'uid', - - ipaUniqueID is 'autogenerate'. - -IPA supports a wide range of username formats, but you need to be aware of any -restrictions that may apply to your particular environment. For example, -usernames that start with a digit or usernames that exceed a certain length -may cause problems for some UNIX systems. -Use 'ipa config-mod' to change the username format allowed by IPA tools. - - -EXAMPLES: - - Add a new stageuser: - ipa stageuser-add --first=Tim --last=User --password tuser1 - - Add a stageuser from the deleted users container: - ipa stageuser-add --first=Tim --last=User --from-delete tuser1 - -""") - -register = Registry() - - -stageuser_output_params = baseuser_output_params - -status_output_params = status_baseuser_output_params - -@register() -class stageuser(baseuser): - """ - Stage User object - A Stage user is not an Active user and can not be used to bind with. - Stage container is: cn=staged users,cn=accounts,cn=provisioning,SUFFIX - Stage entry conforms the schema - Stage entry RDN attribute is 'uid' - Stage entry are disabled (nsAccountLock: True) through cos - """ - - container_dn = baseuser.stage_container_dn - label = _('Stage Users') - label_singular = _('Stage User') - object_name = _('stage user') - object_name_plural = _('stage users') - managed_permissions = { - # - # Stage container - # - # Allowed to create stage user - 'System: Add Stage User': { - 'ipapermlocation': DN(baseuser.stage_container_dn, api.env.basedn), - 'ipapermbindruletype': 'permission', - 'ipapermtarget': DN('uid=*', baseuser.stage_container_dn, api.env.basedn), - 'ipapermtargetfilter': {'(objectclass=*)'}, - 'ipapermright': {'add'}, - 'ipapermdefaultattr': {'*'}, - 'default_privileges': {'Stage User Administrators', 'Stage User Provisioning'}, - }, - # Allow to read kerberos/password - 'System: Read Stage User password': { - 'ipapermlocation': DN(baseuser.stage_container_dn, api.env.basedn), - 'ipapermbindruletype': 'permission', - 'ipapermtarget': DN('uid=*', baseuser.stage_container_dn, api.env.basedn), - 'ipapermtargetfilter': {'(objectclass=*)'}, - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'userPassword', 'krbPrincipalKey', - }, - 'default_privileges': {'Stage User Administrators'}, - }, - # Allow to update stage user - 'System: Modify Stage User': { - 'ipapermlocation': DN(baseuser.stage_container_dn, api.env.basedn), - 'ipapermbindruletype': 'permission', - 'ipapermtarget': DN('uid=*', baseuser.stage_container_dn, api.env.basedn), - 'ipapermtargetfilter': {'(objectclass=*)'}, - 'ipapermright': {'write'}, - 'ipapermdefaultattr': {'*'}, - 'default_privileges': {'Stage User Administrators'}, - }, - # Allow to delete stage user - 'System: Remove Stage User': { - 'ipapermlocation': DN(baseuser.stage_container_dn, api.env.basedn), - 'ipapermbindruletype': 'permission', - 'ipapermtarget': DN('uid=*', baseuser.stage_container_dn, api.env.basedn), - 'ipapermtargetfilter': {'(objectclass=*)'}, - 'ipapermright': {'delete'}, - 'ipapermdefaultattr': {'*'}, - 'default_privileges': {'Stage User Administrators'}, - }, - # Allow to read any attributes of stage users - 'System: Read Stage Users': { - 'ipapermlocation': DN(baseuser.stage_container_dn, api.env.basedn), - 'ipapermbindruletype': 'permission', - 'ipapermtarget': DN('uid=*', baseuser.stage_container_dn, api.env.basedn), - 'ipapermtargetfilter': {'(objectclass=*)'}, - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': {'*'}, - 'default_privileges': {'Stage User Administrators'}, - }, - # - # Preserve container - # - # Allow to read Preserved User - 'System: Read Preserved Users': { - 'ipapermlocation': DN(baseuser.delete_container_dn, api.env.basedn), - 'ipapermbindruletype': 'permission', - 'ipapermtarget': DN('uid=*', baseuser.delete_container_dn, api.env.basedn), - 'ipapermtargetfilter': {'(objectclass=posixaccount)'}, - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': {'*'}, - 'default_privileges': {'Stage User Administrators'}, - }, - # Allow to update Preserved User - 'System: Modify Preserved Users': { - 'ipapermlocation': DN(baseuser.delete_container_dn, api.env.basedn), - 'ipapermbindruletype': 'permission', - 'ipapermtarget': DN('uid=*', baseuser.delete_container_dn, api.env.basedn), - 'ipapermtargetfilter': {'(objectclass=posixaccount)'}, - 'ipapermright': {'write'}, - 'ipapermdefaultattr': {'*'}, - 'default_privileges': {'Stage User Administrators'}, - }, - # Allow to reset Preserved User password - 'System: Reset Preserved User password': { - 'ipapermlocation': DN(baseuser.delete_container_dn, api.env.basedn), - 'ipapermbindruletype': 'permission', - 'ipapermtarget': DN('uid=*', baseuser.delete_container_dn, api.env.basedn), - 'ipapermtargetfilter': {'(objectclass=posixaccount)'}, - 'ipapermright': {'read', 'search', 'write'}, - 'ipapermdefaultattr': { - 'userPassword', 'krbPrincipalKey','krbPasswordExpiration','krbLastPwdChange' - }, - 'default_privileges': {'Stage User Administrators'}, - }, - # Allow to delete preserved user - 'System: Remove preserved User': { - 'ipapermlocation': DN(baseuser.delete_container_dn, api.env.basedn), - 'ipapermbindruletype': 'permission', - 'ipapermtarget': DN('uid=*', baseuser.delete_container_dn, api.env.basedn), - 'ipapermtargetfilter': {'(objectclass=*)'}, - 'ipapermright': {'delete'}, - 'ipapermdefaultattr': {'*'}, - 'default_privileges': {'Stage User Administrators'}, - }, - # - # Active container - # - # Stage user administrators need write right on RDN when - # the active user is deleted (preserved) - 'System: Modify User RDN': { - 'ipapermlocation': DN(baseuser.active_container_dn, api.env.basedn), - 'ipapermbindruletype': 'permission', - 'ipapermtarget': DN('uid=*', baseuser.active_container_dn, api.env.basedn), - 'ipapermtargetfilter': {'(objectclass=posixaccount)'}, - 'ipapermright': {'write'}, - 'ipapermdefaultattr': {'uid'}, - 'default_privileges': {'Stage User Administrators'}, - }, - # - # Cross containers autorization - # - # Allow to move active user to preserve container (user-del --preserve) - # Note: targetfilter is the target parent container - 'System: Preserve User': { - 'ipapermlocation': DN(api.env.basedn), - 'ipapermbindruletype': 'permission', - 'ipapermtargetfrom': DN(baseuser.active_container_dn, api.env.basedn), - 'ipapermtargetto': DN(baseuser.delete_container_dn, api.env.basedn), - 'ipapermtargetfilter': {'(objectclass=nsContainer)'}, - 'ipapermright': {'moddn'}, - 'default_privileges': {'Stage User Administrators'}, - }, - # Allow to move preserved user to active container (user-undel) - # Note: targetfilter is the target parent container - 'System: Undelete User': { - 'ipapermlocation': DN(api.env.basedn), - 'ipapermbindruletype': 'permission', - 'ipapermtargetfrom': DN(baseuser.delete_container_dn, api.env.basedn), - 'ipapermtargetto': DN(baseuser.active_container_dn, api.env.basedn), - 'ipapermtargetfilter': {'(objectclass=nsContainer)'}, - 'ipapermright': {'moddn'}, - 'default_privileges': {'Stage User Administrators'}, - }, - } - -@register() -class stageuser_add(baseuser_add): - __doc__ = _('Add a new stage user.') - - msg_summary = _('Added stage user "%(value)s"') - - has_output_params = baseuser_add.has_output_params + stageuser_output_params - - takes_options = LDAPCreate.takes_options + ( - Bool( - 'from_delete?', - deprecated=True, - doc=_('Create Stage user in from a delete user'), - cli_name='from_delete', - flags={'no_option'}, - ), - ) - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - - # then givenname and sn are required attributes - if 'givenname' not in entry_attrs: - raise errors.RequirementError(name='givenname', error=_('givenname is required')) - - if 'sn' not in entry_attrs: - raise errors.RequirementError(name='sn', error=_('sn is required')) - - # we don't want an user private group to be created for this user - # add NO_UPG_MAGIC description attribute to let the DS plugin know - entry_attrs.setdefault('description', []) - entry_attrs['description'].append(NO_UPG_MAGIC) - - # uidNumber/gidNumber - entry_attrs.setdefault('uidnumber', baseldap.DNA_MAGIC) - entry_attrs.setdefault('gidnumber', baseldap.DNA_MAGIC) - - if not client_has_capability( - options['version'], 'optional_uid_params'): - # https://fedorahosted.org/freeipa/ticket/2886 - # Old clients say 999 (OLD_DNA_MAGIC) when they really mean - # "assign a value dynamically". - OLD_DNA_MAGIC = 999 - if entry_attrs.get('uidnumber') == OLD_DNA_MAGIC: - entry_attrs['uidnumber'] = baseldap.DNA_MAGIC - if entry_attrs.get('gidnumber') == OLD_DNA_MAGIC: - entry_attrs['gidnumber'] = baseldap.DNA_MAGIC - - - # Check the lenght of the RDN (uid) value - config = ldap.get_ipa_config() - if 'ipamaxusernamelength' in config: - if len(keys[-1]) > int(config.get('ipamaxusernamelength')[0]): - raise errors.ValidationError( - name=self.obj.primary_key.cli_name, - error=_('can be at most %(len)d characters') % dict( - len = int(config.get('ipamaxusernamelength')[0]) - ) - ) - default_shell = config.get('ipadefaultloginshell', [paths.SH])[0] - entry_attrs.setdefault('loginshell', default_shell) - # hack so we can request separate first and last name in CLI - full_name = '%s %s' % (entry_attrs['givenname'], entry_attrs['sn']) - entry_attrs.setdefault('cn', full_name) - - # Homedirectory - # (order is : option, placeholder (TBD), CLI default value (here in config)) - if 'homedirectory' not in entry_attrs: - # get home's root directory from config - homes_root = config.get('ipahomesrootdir', [paths.HOME_DIR])[0] - # build user's home directory based on his uid - entry_attrs['homedirectory'] = posixpath.join(homes_root, keys[-1]) - - # Kerberos principal - entry_attrs.setdefault('krbprincipalname', '%s@%s' % (entry_attrs['uid'], api.env.realm)) - - - # If requested, generate a userpassword - if 'userpassword' not in entry_attrs and options.get('random'): - entry_attrs['userpassword'] = ipa_generate_password(baseuser_pwdchars) - # save the password so it can be displayed in post_callback - setattr(context, 'randompassword', entry_attrs['userpassword']) - - # Check the email or create it - if 'mail' in entry_attrs: - entry_attrs['mail'] = self.obj.normalize_and_validate_email(entry_attrs['mail'], config) - else: - # No e-mail passed in. If we have a default e-mail domain set - # then we'll add it automatically. - defaultdomain = config.get('ipadefaultemaildomain', [None])[0] - if defaultdomain: - entry_attrs['mail'] = self.obj.normalize_and_validate_email(keys[-1], config) - - # If the manager is defined, check it is a ACTIVE user to validate it - if 'manager' in entry_attrs: - entry_attrs['manager'] = self.obj.normalize_manager(entry_attrs['manager'], self.obj.active_container_dn) - - if ('objectclass' in entry_attrs - and 'userclass' in entry_attrs - and 'ipauser' not in entry_attrs['objectclass']): - entry_attrs['objectclass'].append('ipauser') - - if 'ipatokenradiusconfiglink' in entry_attrs: - cl = entry_attrs['ipatokenradiusconfiglink'] - if cl: - if 'objectclass' not in entry_attrs: - _entry = ldap.get_entry(dn, ['objectclass']) - entry_attrs['objectclass'] = _entry['objectclass'] - - if 'ipatokenradiusproxyuser' not in entry_attrs['objectclass']: - entry_attrs['objectclass'].append('ipatokenradiusproxyuser') - - answer = self.api.Object['radiusproxy'].get_dn_if_exists(cl) - entry_attrs['ipatokenradiusconfiglink'] = answer - - self.pre_common_callback(ldap, dn, entry_attrs, attrs_list, *keys, - **options) - - return dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - config = ldap.get_ipa_config() - - # Fetch the entry again to update memberof, mep data, etc updated - # at the end of the transaction. - newentry = ldap.get_entry(dn, ['*']) - entry_attrs.update(newentry) - - if options.get('random', False): - try: - entry_attrs['randompassword'] = unicode(getattr(context, 'randompassword')) - except AttributeError: - # if both randompassword and userpassword options were used - pass - - self.post_common_callback(ldap, dn, entry_attrs, *keys, **options) - return dn - -@register() -class stageuser_del(baseuser_del): - __doc__ = _('Delete a stage user.') - - msg_summary = _('Deleted stage user "%(value)s"') - -@register() -class stageuser_mod(baseuser_mod): - __doc__ = _('Modify a stage user.') - - msg_summary = _('Modified stage user "%(value)s"') - - has_output_params = baseuser_mod.has_output_params + stageuser_output_params - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - self.pre_common_callback(ldap, dn, entry_attrs, attrs_list, *keys, - **options) - # Make sure it is not possible to authenticate with a Stage user account - if 'nsaccountlock' in entry_attrs: - del entry_attrs['nsaccountlock'] - return dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - self.post_common_callback(ldap, dn, entry_attrs, **options) - if 'nsaccountlock' in entry_attrs: - del entry_attrs['nsaccountlock'] - return dn - -@register() -class stageuser_find(baseuser_find): - __doc__ = _('Search for stage users.') - - member_attributes = ['memberof'] - has_output_params = baseuser_find.has_output_params + stageuser_output_params - - def pre_callback(self, ldap, filter, attrs_list, base_dn, scope, *keys, **options): - assert isinstance(base_dn, DN) - self.pre_common_callback(ldap, filter, attrs_list, base_dn, scope, - *keys, **options) - - container_filter = "(objectclass=posixaccount)" - # provisioning system can create non posixaccount stage user - # but then they have to create inetOrgPerson stage user - stagefilter = filter.replace(container_filter, - "(|%s(objectclass=inetOrgPerson))" % container_filter) - self.log.debug("stageuser_find: pre_callback new filter=%s " % (stagefilter)) - return (stagefilter, base_dn, scope) - - def post_callback(self, ldap, entries, truncated, *args, **options): - if options.get('pkey_only', False): - return truncated - self.post_common_callback(ldap, entries, lockout=True, **options) - return truncated - - msg_summary = ngettext( - '%(count)d user matched', '%(count)d users matched', 0 - ) - -@register() -class stageuser_show(baseuser_show): - __doc__ = _('Display information about a stage user.') - - has_output_params = baseuser_show.has_output_params + stageuser_output_params - - def pre_callback(self, ldap, dn, attrs_list, *keys, **options): - assert isinstance(dn, DN) - self.pre_common_callback(ldap, dn, attrs_list, *keys, **options) - return dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - entry_attrs['nsaccountlock'] = True - self.post_common_callback(ldap, dn, entry_attrs, *keys, **options) - return dn - - -@register() -class stageuser_activate(LDAPQuery): - __doc__ = _('Activate a stage user.') - - msg_summary = _('Activate a stage user "%(value)s"') - - preserved_DN_syntax_attrs = ('manager', 'managedby', 'secretary') - - searched_operational_attributes = ['uidNumber', 'gidNumber', 'nsAccountLock', 'ipauniqueid'] - - has_output = output.standard_entry - has_output_params = LDAPQuery.has_output_params + stageuser_output_params - - def _check_validy(self, dn, entry): - if dn[0].attr != 'uid': - raise errors.ValidationError( - name=self.obj.primary_key.cli_name, - error=_('Entry RDN is not \'uid\''), - ) - for attr in ('cn', 'sn', 'uid'): - if attr not in entry: - raise errors.ValidationError( - name=self.obj.primary_key.cli_name, - error=_('Entry has no \'%(attribute)s\'') % dict(attribute=attr), - ) - - def _build_new_entry(self, ldap, dn, entry_from, entry_to): - config = ldap.get_ipa_config() - - if 'uidnumber' not in entry_from: - entry_to['uidnumber'] = baseldap.DNA_MAGIC - if 'gidnumber' not in entry_from: - entry_to['gidnumber'] = baseldap.DNA_MAGIC - if 'homedirectory' not in entry_from: - # get home's root directory from config - homes_root = config.get('ipahomesrootdir', [paths.HOME_DIR])[0] - # build user's home directory based on his uid - entry_to['homedirectory'] = posixpath.join(homes_root, dn[0].value) - if 'ipamaxusernamelength' in config: - if len(dn[0].value) > int(config.get('ipamaxusernamelength')[0]): - raise errors.ValidationError( - name=self.obj.primary_key.cli_name, - error=_('can be at most %(len)d characters') % dict( - len = int(config.get('ipamaxusernamelength')[0]) - ) - ) - if 'loginshell' not in entry_from: - default_shell = config.get('ipadefaultloginshell', [paths.SH])[0] - if default_shell: - entry_to.setdefault('loginshell', default_shell) - - if 'givenname' not in entry_from: - entry_to['givenname'] = entry_from['cn'][0].split()[0] - - if 'krbprincipalname' not in entry_from: - entry_to['krbprincipalname'] = '%s@%s' % (entry_from['uid'][0], api.env.realm) - - def __dict_new_entry(self, *args, **options): - ldap = self.obj.backend - - entry_attrs = self.args_options_2_entry(*args, **options) - entry_attrs = ldap.make_entry(DN(), entry_attrs) - - self.process_attr_options(entry_attrs, None, args, options) - - entry_attrs['objectclass'] = deepcopy(self.obj.object_class) - - if self.obj.object_class_config: - config = ldap.get_ipa_config() - entry_attrs['objectclass'] = config.get( - self.obj.object_class_config, entry_attrs['objectclass'] - ) - - return(entry_attrs) - - def __merge_values(self, args, options, entry_from, entry_to, attr): - ''' - This routine merges the values of attr taken from entry_from, into entry_to. - If attr is a syntax DN attribute, it is replaced by an empty value. It is a preferable solution - compare to skiping it because the final entry may no longer conform the schema. - An exception of this is for a limited set of syntax DN attribute that we want to - preserved (defined in preserved_DN_syntax_attrs) - see http://www.freeipa.org/page/V3/User_Life-Cycle_Management#Adjustment_of_DN_syntax_attributes - ''' - if not attr in entry_to: - if isinstance(entry_from[attr], (list, tuple)): - # attr is multi value attribute - entry_to[attr] = [] - else: - # attr single valued attribute - entry_to[attr] = None - - # At this point entry_to contains for all resulting attributes - # either a list (possibly empty) or a value (possibly None) - - for value in entry_from[attr]: - # merge all the values from->to - v = self.__value_2_add(args, options, attr, value) - if (isinstance(v, str) and v in ('', None)) or \ - (isinstance(v, unicode) and v in (u'', None)): - try: - v.decode('utf-8') - self.log.debug("merge: %s:%r wiped" % (attr, v)) - except Exception: - self.log.debug("merge %s: [no_print %s]" % (attr, v.__class__.__name__)) - if isinstance(entry_to[attr], (list, tuple)): - # multi value attribute - if v not in entry_to[attr]: - # it may has been added before in the loop - # so add it only if it not present - entry_to[attr].append(v) - else: - # single value attribute - # keep the value defined in staging - entry_to[attr] = v - else: - try: - v.decode('utf-8') - self.log.debug("Add: %s:%r" % (attr, v)) - except Exception: - self.log.debug("Add %s: [no_print %s]" % (attr, v.__class__.__name__)) - - if isinstance(entry_to[attr], (list, tuple)): - # multi value attribute - if attr.lower() == 'objectclass': - entry_to[attr] = [oc.lower() for oc in entry_to[attr]] - value = value.lower() - if value not in entry_to[attr]: - entry_to[attr].append(value) - else: - if value not in entry_to[attr]: - entry_to[attr].append(value) - else: - # single value attribute - if value: - entry_to[attr] = value - - def __value_2_add(self, args, options, attr, value): - ''' - If the attribute is NOT syntax DN it returns its value. - Else it checks if the value can be preserved. - To be preserved: - - attribute must be in preserved_DN_syntax_attrs - - value must be an active user DN (in Active container) - - the active user entry exists - ''' - ldap = self.obj.backend - - if ldap.has_dn_syntax(attr): - if attr.lower() in self.preserved_DN_syntax_attrs: - # we are about to add a DN syntax value - # Check this is a valid DN - if not isinstance(value, DN): - return u'' - - if not self.obj.active_user(value): - return u'' - - # Check that this value is a Active user - try: - entry_attrs = self._exc_wrapper(args, options, ldap.get_entry)(value, ['dn']) - return value - except errors.NotFound: - return u'' - else: - return u'' - else: - return value - - def execute(self, *args, **options): - - ldap = self.obj.backend - - staging_dn = self.obj.get_dn(*args, **options) - assert isinstance(staging_dn, DN) - - # retrieve the current entry - try: - entry_attrs = self._exc_wrapper(args, options, ldap.get_entry)( - staging_dn, ['*'] - ) - except errors.NotFound: - self.obj.handle_not_found(*args) - entry_attrs = dict((k.lower(), v) for (k, v) in entry_attrs.items()) - - # Check it does not exist an active entry with the same RDN - active_dn = DN(staging_dn[0], api.env.container_user, api.env.basedn) - try: - test_entry_attrs = self._exc_wrapper(args, options, ldap.get_entry)( - active_dn, ['dn'] - ) - assert isinstance(staging_dn, DN) - raise errors.DuplicateEntry( - message=_('active user with name "%(user)s" already exists') % - dict(user=args[-1])) - except errors.NotFound: - pass - - # Check the original entry is valid - self._check_validy(staging_dn, entry_attrs) - - # Time to build the new entry - result_entry = {'dn' : active_dn} - new_entry_attrs = self.__dict_new_entry() - for (attr, values) in entry_attrs.items(): - self.__merge_values(args, options, entry_attrs, new_entry_attrs, attr) - result_entry[attr] = values - - # Allow Managed entry plugin to do its work - if 'description' in new_entry_attrs and NO_UPG_MAGIC in new_entry_attrs['description']: - new_entry_attrs['description'].remove(NO_UPG_MAGIC) - if result_entry['description'] == NO_UPG_MAGIC: - del result_entry['description'] - - for (k, v) in new_entry_attrs.items(): - self.log.debug("new entry: k=%r and v=%r)" % (k, v)) - - self._build_new_entry(ldap, staging_dn, entry_attrs, new_entry_attrs) - - # Add the Active entry - entry = ldap.make_entry(active_dn, new_entry_attrs) - self._exc_wrapper(args, options, ldap.add_entry)(entry) - - # Now delete the Staging entry - try: - self._exc_wrapper(args, options, ldap.delete_entry)(staging_dn) - except: - try: - self.log.error("Fail to delete the Staging user after activating it %s " % (staging_dn)) - self._exc_wrapper(args, options, ldap.delete_entry)(active_dn) - except Exception: - self.log.error("Fail to cleanup activation. The user remains active %s" % (active_dn)) - raise - - # add the user we just created into the default primary group - config = ldap.get_ipa_config() - def_primary_group = config.get('ipadefaultprimarygroup') - group_dn = self.api.Object['group'].get_dn(def_primary_group) - - # if the user is already a member of default primary group, - # do not raise error - # this can happen if automember rule or default group is set - try: - ldap.add_entry_to_group(active_dn, group_dn) - except errors.AlreadyGroupMember: - pass - - # Now retrieve the activated entry - result = self.api.Command.user_show( - args[-1], - all=options.get('all', False), - raw=options.get('raw', False), - version=options.get('version'), - ) - result['summary'] = unicode( - _('Stage user %s activated' % staging_dn[0].value)) - - return result - - -@register() -class stageuser_add_manager(baseuser_add_manager): - __doc__ = _("Add a manager to the stage user entry") - - -@register() -class stageuser_remove_manager(baseuser_remove_manager): - __doc__ = _("Remove a manager to the stage user entry") diff --git a/ipalib/plugins/sudo.py b/ipalib/plugins/sudo.py deleted file mode 100644 index eb1f49ff9..000000000 --- a/ipalib/plugins/sudo.py +++ /dev/null @@ -1,7 +0,0 @@ -# -# Copyright (C) 2016 FreeIPA Contributors see COPYING for license -# - -from ipalib.text import _ - -__doc__ = _('commands for controlling sudo configuration') diff --git a/ipalib/plugins/sudocmd.py b/ipalib/plugins/sudocmd.py deleted file mode 100644 index e3ae33a84..000000000 --- a/ipalib/plugins/sudocmd.py +++ /dev/null @@ -1,203 +0,0 @@ -# Authors: -# Jr Aquino <jr.aquino@citrixonline.com> -# -# Copyright (C) 2010 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/>. - -from ipalib import api, errors -from ipalib import Str -from ipalib.plugable import Registry -from .baseldap import ( - LDAPObject, - LDAPCreate, - LDAPDelete, - LDAPUpdate, - LDAPSearch, - LDAPRetrieve) -from ipalib import _, ngettext -from ipapython.dn import DN - -__doc__ = _(""" -Sudo Commands - -Commands used as building blocks for sudo - -EXAMPLES: - - Create a new command - ipa sudocmd-add --desc='For reading log files' /usr/bin/less - - Remove a command - ipa sudocmd-del /usr/bin/less - -""") - -register = Registry() - -topic = 'sudo' - -@register() -class sudocmd(LDAPObject): - """ - Sudo Command object. - """ - container_dn = api.env.container_sudocmd - object_name = _('sudo command') - object_name_plural = _('sudo commands') - object_class = ['ipaobject', 'ipasudocmd'] - permission_filter_objectclasses = ['ipasudocmd'] - # object_class_config = 'ipahostobjectclasses' - search_attributes = [ - 'sudocmd', 'description', - ] - default_attributes = [ - 'sudocmd', 'description', 'memberof', - ] - attribute_members = { - 'memberof': ['sudocmdgroup'], - } - uuid_attribute = 'ipauniqueid' - rdn_attribute = 'ipauniqueid' - managed_permissions = { - 'System: Read Sudo Commands': { - 'replaces_global_anonymous_aci': True, - 'ipapermbindruletype': 'all', - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'description', 'ipauniqueid', 'memberof', 'objectclass', - 'sudocmd', - }, - }, - 'System: Add Sudo Command': { - 'ipapermright': {'add'}, - 'replaces': [ - '(target = "ldap:///sudocmd=*,cn=sudocmds,cn=sudo,$SUFFIX")(version 3.0;acl "permission:Add Sudo command";allow (add) groupdn = "ldap:///cn=Add Sudo command,cn=permissions,cn=pbac,$SUFFIX";)', - '(targetfilter = "(objectclass=ipasudocmd)")(target = "ldap:///cn=sudocmds,cn=sudo,$SUFFIX")(version 3.0;acl "permission:Add Sudo command";allow (add) groupdn = "ldap:///cn=Add Sudo command,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Sudo Administrator'}, - }, - 'System: Delete Sudo Command': { - 'ipapermright': {'delete'}, - 'replaces': [ - '(target = "ldap:///sudocmd=*,cn=sudocmds,cn=sudo,$SUFFIX")(version 3.0;acl "permission:Delete Sudo command";allow (delete) groupdn = "ldap:///cn=Delete Sudo command,cn=permissions,cn=pbac,$SUFFIX";)', - '(targetfilter = "(objectclass=ipasudocmd)")(target = "ldap:///cn=sudocmds,cn=sudo,$SUFFIX")(version 3.0;acl "permission:Delete Sudo command";allow (delete) groupdn = "ldap:///cn=Delete Sudo command,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Sudo Administrator'}, - }, - 'System: Modify Sudo Command': { - 'ipapermright': {'write'}, - 'ipapermdefaultattr': {'description'}, - 'replaces': [ - '(targetattr = "description")(target = "ldap:///sudocmd=*,cn=sudocmds,cn=sudo,$SUFFIX")(version 3.0;acl "permission:Modify Sudo command";allow (write) groupdn = "ldap:///cn=Modify Sudo command,cn=permissions,cn=pbac,$SUFFIX";)', - '(targetfilter = "(objectclass=ipasudocmd)")(targetattr = "description")(target = "ldap:///cn=sudocmds,cn=sudo,$SUFFIX")(version 3.0;acl "permission:Modify Sudo command";allow (write) groupdn = "ldap:///cn=Modify Sudo command,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Sudo Administrator'}, - }, - } - - label = _('Sudo Commands') - label_singular = _('Sudo Command') - - takes_params = ( - Str('sudocmd', - cli_name='command', - label=_('Sudo Command'), - primary_key=True, - ), - Str('description?', - cli_name='desc', - label=_('Description'), - doc=_('A description of this command'), - ), - ) - - def get_dn(self, *keys, **options): - if keys[-1].endswith('.'): - keys[-1] = keys[-1][:-1] - dn = super(sudocmd, self).get_dn(*keys, **options) - try: - self.backend.get_entry(dn, ['']) - except errors.NotFound: - try: - entry_attrs = self.backend.find_entry_by_attr( - 'sudocmd', keys[-1], self.object_class, [''], - DN(self.container_dn, api.env.basedn)) - dn = entry_attrs.dn - except errors.NotFound: - pass - return dn - - -@register() -class sudocmd_add(LDAPCreate): - __doc__ = _('Create new Sudo Command.') - - msg_summary = _('Added Sudo Command "%(value)s"') - - -@register() -class sudocmd_del(LDAPDelete): - __doc__ = _('Delete Sudo Command.') - - msg_summary = _('Deleted Sudo Command "%(value)s"') - - def pre_callback(self, ldap, dn, *keys, **options): - filters = [ - ldap.make_filter_from_attr(attr, dn) - for attr in ('memberallowcmd', 'memberdenycmd')] - filter = ldap.combine_filters(filters, ldap.MATCH_ANY) - filter = ldap.combine_filters( - (filter, ldap.make_filter_from_attr('objectClass', 'ipasudorule')), - ldap.MATCH_ALL) - dependent_sudorules = [] - try: - entries, truncated = ldap.find_entries( - filter, ['cn'], - base_dn=DN(api.env.container_sudorule, api.env.basedn)) - except errors.NotFound: - pass - else: - for entry_attrs in entries: - [cn] = entry_attrs['cn'] - dependent_sudorules.append(cn) - - if dependent_sudorules: - raise errors.DependentEntry( - key=keys[0], label='sudorule', - dependent=', '.join(dependent_sudorules)) - return dn - - -@register() -class sudocmd_mod(LDAPUpdate): - __doc__ = _('Modify Sudo Command.') - - msg_summary = _('Modified Sudo Command "%(value)s"') - - -@register() -class sudocmd_find(LDAPSearch): - __doc__ = _('Search for Sudo Commands.') - - msg_summary = ngettext( - '%(count)d Sudo Command matched', '%(count)d Sudo Commands matched', 0 - ) - - -@register() -class sudocmd_show(LDAPRetrieve): - __doc__ = _('Display Sudo Command.') - diff --git a/ipalib/plugins/sudocmdgroup.py b/ipalib/plugins/sudocmdgroup.py deleted file mode 100644 index 9e8c016fd..000000000 --- a/ipalib/plugins/sudocmdgroup.py +++ /dev/null @@ -1,195 +0,0 @@ -# Authors: -# Jr Aquino <jr.aquino@citrixonline.com> -# -# Copyright (C) 2010 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/>. - -from ipalib import api -from ipalib import Str -from ipalib.plugable import Registry -from .baseldap import ( - LDAPObject, - LDAPCreate, - LDAPDelete, - LDAPUpdate, - LDAPSearch, - LDAPRetrieve, - LDAPAddMember, - LDAPRemoveMember) -from ipalib import _, ngettext - -__doc__ = _(""" -Groups of Sudo Commands - -Manage groups of Sudo Commands. - -EXAMPLES: - - Add a new Sudo Command Group: - ipa sudocmdgroup-add --desc='administrators commands' admincmds - - Remove a Sudo Command Group: - ipa sudocmdgroup-del admincmds - - Manage Sudo Command Group membership, commands: - ipa sudocmdgroup-add-member --sudocmds=/usr/bin/less --sudocmds=/usr/bin/vim admincmds - - Manage Sudo Command Group membership, commands: - ipa sudocmdgroup-remove-member --sudocmds=/usr/bin/less admincmds - - Show a Sudo Command Group: - ipa sudocmdgroup-show admincmds -""") - -register = Registry() - -topic = 'sudo' - -@register() -class sudocmdgroup(LDAPObject): - """ - Sudo Command Group object. - """ - container_dn = api.env.container_sudocmdgroup - object_name = _('sudo command group') - object_name_plural = _('sudo command groups') - object_class = ['ipaobject', 'ipasudocmdgrp'] - permission_filter_objectclasses = ['ipasudocmdgrp'] - default_attributes = [ - 'cn', 'description', 'member', - ] - uuid_attribute = 'ipauniqueid' - attribute_members = { - 'member': ['sudocmd'], - } - managed_permissions = { - 'System: Read Sudo Command Groups': { - 'replaces_global_anonymous_aci': True, - 'ipapermbindruletype': 'all', - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'businesscategory', 'cn', 'description', 'ipauniqueid', - 'member', 'o', 'objectclass', 'ou', 'owner', 'seealso', - 'memberuser', 'memberhost', - }, - }, - 'System: Add Sudo Command Group': { - 'ipapermright': {'add'}, - 'replaces': [ - '(target = "ldap:///cn=*,cn=sudocmdgroups,cn=sudo,$SUFFIX")(version 3.0;acl "permission:Add Sudo command group";allow (add) groupdn = "ldap:///cn=Add Sudo command group,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Sudo Administrator'}, - }, - 'System: Delete Sudo Command Group': { - 'ipapermright': {'delete'}, - 'replaces': [ - '(target = "ldap:///cn=*,cn=sudocmdgroups,cn=sudo,$SUFFIX")(version 3.0;acl "permission:Delete Sudo command group";allow (delete) groupdn = "ldap:///cn=Delete Sudo command group,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Sudo Administrator'}, - }, - 'System: Modify Sudo Command Group': { - 'ipapermright': {'write'}, - 'ipapermdefaultattr': {'description'}, - 'default_privileges': {'Sudo Administrator'}, - }, - 'System: Manage Sudo Command Group Membership': { - 'ipapermright': {'write'}, - 'ipapermdefaultattr': {'member'}, - 'replaces': [ - '(targetattr = "member")(target = "ldap:///cn=*,cn=sudocmdgroups,cn=sudo,$SUFFIX")(version 3.0;acl "permission:Manage Sudo command group membership";allow (write) groupdn = "ldap:///cn=Manage Sudo command group membership,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Sudo Administrator'}, - }, - } - - label = _('Sudo Command Groups') - label_singular = _('Sudo Command Group') - - takes_params = ( - Str('cn', - cli_name='sudocmdgroup_name', - label=_('Sudo Command Group'), - primary_key=True, - normalizer=lambda value: value.lower(), - ), - Str('description?', - cli_name='desc', - label=_('Description'), - doc=_('Group description'), - ), - Str('membercmd_sudocmd?', - label=_('Commands'), - flags=['no_create', 'no_update', 'no_search'], - ), - Str('membercmd_sudocmdgroup?', - label=_('Sudo Command Groups'), - flags=['no_create', 'no_update', 'no_search'], - ), - ) - - - -@register() -class sudocmdgroup_add(LDAPCreate): - __doc__ = _('Create new Sudo Command Group.') - - msg_summary = _('Added Sudo Command Group "%(value)s"') - - - -@register() -class sudocmdgroup_del(LDAPDelete): - __doc__ = _('Delete Sudo Command Group.') - - msg_summary = _('Deleted Sudo Command Group "%(value)s"') - - - -@register() -class sudocmdgroup_mod(LDAPUpdate): - __doc__ = _('Modify Sudo Command Group.') - - msg_summary = _('Modified Sudo Command Group "%(value)s"') - - - -@register() -class sudocmdgroup_find(LDAPSearch): - __doc__ = _('Search for Sudo Command Groups.') - - msg_summary = ngettext( - '%(count)d Sudo Command Group matched', - '%(count)d Sudo Command Groups matched', 0 - ) - - - -@register() -class sudocmdgroup_show(LDAPRetrieve): - __doc__ = _('Display Sudo Command Group.') - - - -@register() -class sudocmdgroup_add_member(LDAPAddMember): - __doc__ = _('Add members to Sudo Command Group.') - - - -@register() -class sudocmdgroup_remove_member(LDAPRemoveMember): - __doc__ = _('Remove members from Sudo Command Group.') - diff --git a/ipalib/plugins/sudorule.py b/ipalib/plugins/sudorule.py deleted file mode 100644 index 15d03c659..000000000 --- a/ipalib/plugins/sudorule.py +++ /dev/null @@ -1,998 +0,0 @@ -# Authors: -# Jr Aquino <jr.aquino@citrixonline.com> -# -# Copyright (C) 2010-2014 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/>. - -import netaddr -import six - -from ipalib import api, errors -from ipalib import Str, StrEnum, Bool, Int -from ipalib.plugable import Registry -from .baseldap import (LDAPObject, LDAPCreate, LDAPDelete, - LDAPUpdate, LDAPSearch, LDAPRetrieve, - LDAPQuery, LDAPAddMember, LDAPRemoveMember, - add_external_pre_callback, - add_external_post_callback, - remove_external_post_callback, - output, entry_to_dict, pkey_to_value, - external_host_param) -from .hbacrule import is_all -from ipalib import _, ngettext -from ipalib.util import validate_hostmask -from ipapython.dn import DN - -if six.PY3: - unicode = str - -__doc__ = _(""" -Sudo Rules -""") + _(""" -Sudo (su "do") allows a system administrator to delegate authority to -give certain users (or groups of users) the ability to run some (or all) -commands as root or another user while providing an audit trail of the -commands and their arguments. -""") + _(""" -FreeIPA provides a means to configure the various aspects of Sudo: - Users: The user(s)/group(s) allowed to invoke Sudo. - Hosts: The host(s)/hostgroup(s) which the user is allowed to to invoke Sudo. - Allow Command: The specific command(s) permitted to be run via Sudo. - Deny Command: The specific command(s) prohibited to be run via Sudo. - RunAsUser: The user(s) or group(s) of users whose rights Sudo will be invoked with. - RunAsGroup: The group(s) whose gid rights Sudo will be invoked with. - Options: The various Sudoers Options that can modify Sudo's behavior. -""") + _(""" -An order can be added to a sudorule to control the order in which they -are evaluated (if the client supports it). This order is an integer and -must be unique. -""") + _(""" -FreeIPA provides a designated binddn to use with Sudo located at: -uid=sudo,cn=sysaccounts,cn=etc,dc=example,dc=com -""") + _(""" -To enable the binddn run the following command to set the password: -LDAPTLS_CACERT=/etc/ipa/ca.crt /usr/bin/ldappasswd -S -W \ --h ipa.example.com -ZZ -D "cn=Directory Manager" \ -uid=sudo,cn=sysaccounts,cn=etc,dc=example,dc=com -""") + _(""" -EXAMPLES: -""") + _(""" - Create a new rule: - ipa sudorule-add readfiles -""") + _(""" - Add sudo command object and add it as allowed command in the rule: - ipa sudocmd-add /usr/bin/less - ipa sudorule-add-allow-command readfiles --sudocmds /usr/bin/less -""") + _(""" - Add a host to the rule: - ipa sudorule-add-host readfiles --hosts server.example.com -""") + _(""" - Add a user to the rule: - ipa sudorule-add-user readfiles --users jsmith -""") + _(""" - Add a special Sudo rule for default Sudo server configuration: - ipa sudorule-add defaults -""") + _(""" - Set a default Sudo option: - ipa sudorule-add-option defaults --sudooption '!authenticate' -""") - -register = Registry() - -topic = 'sudo' - - -def deprecated(attribute): - raise errors.ValidationError( - name=attribute, - error=_('this option has been deprecated.')) - - -hostmask_membership_param = Str('hostmask?', validate_hostmask, - label=_('host masks of allowed hosts'), - flags=['no_create', 'no_update', 'no_search'], - multivalue=True, - ) - -def validate_externaluser(ugettext, value): - deprecated('externaluser') - - -def validate_runasextuser(ugettext, value): - deprecated('runasexternaluser') - - -def validate_runasextgroup(ugettext, value): - deprecated('runasexternalgroup') - - -@register() -class sudorule(LDAPObject): - """ - Sudo Rule object. - """ - container_dn = api.env.container_sudorule - object_name = _('sudo rule') - object_name_plural = _('sudo rules') - object_class = ['ipaassociation', 'ipasudorule'] - permission_filter_objectclasses = ['ipasudorule'] - default_attributes = [ - 'cn', 'ipaenabledflag', 'externaluser', - 'description', 'usercategory', 'hostcategory', - 'cmdcategory', 'memberuser', 'memberhost', - 'memberallowcmd', 'memberdenycmd', 'ipasudoopt', - 'ipasudorunas', 'ipasudorunasgroup', - 'ipasudorunasusercategory', 'ipasudorunasgroupcategory', - 'sudoorder', 'hostmask', 'externalhost', 'ipasudorunasextusergroup', - 'ipasudorunasextgroup', 'ipasudorunasextuser' - ] - uuid_attribute = 'ipauniqueid' - rdn_attribute = 'ipauniqueid' - attribute_members = { - 'memberuser': ['user', 'group'], - 'memberhost': ['host', 'hostgroup'], - 'memberallowcmd': ['sudocmd', 'sudocmdgroup'], - 'memberdenycmd': ['sudocmd', 'sudocmdgroup'], - 'ipasudorunas': ['user', 'group'], - 'ipasudorunasgroup': ['group'], - } - managed_permissions = { - 'System: Read Sudo Rules': { - 'replaces_global_anonymous_aci': True, - 'ipapermbindruletype': 'all', - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'cmdcategory', 'cn', 'description', 'externalhost', - 'externaluser', 'hostcategory', 'hostmask', 'ipaenabledflag', - 'ipasudoopt', 'ipasudorunas', 'ipasudorunasextgroup', - 'ipasudorunasextuser', 'ipasudorunasextusergroup', - 'ipasudorunasgroup', - 'ipasudorunasgroupcategory', 'ipasudorunasusercategory', - 'ipauniqueid', 'memberallowcmd', 'memberdenycmd', - 'memberhost', 'memberuser', 'sudonotafter', 'sudonotbefore', - 'sudoorder', 'usercategory', 'objectclass', 'member', - }, - }, - 'System: Read Sudoers compat tree': { - 'non_object': True, - 'ipapermlocation': api.env.basedn, - 'ipapermtarget': DN('ou=sudoers', api.env.basedn), - 'ipapermbindruletype': 'anonymous', - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'objectclass', 'cn', 'ou', - 'sudouser', 'sudohost', 'sudocommand', 'sudorunas', - 'sudorunasuser', 'sudorunasgroup', 'sudooption', - 'sudonotbefore', 'sudonotafter', 'sudoorder', 'description', - }, - }, - 'System: Add Sudo rule': { - 'ipapermright': {'add'}, - 'replaces': [ - '(target = "ldap:///ipauniqueid=*,cn=sudorules,cn=sudo,$SUFFIX")(version 3.0;acl "permission:Add Sudo rule";allow (add) groupdn = "ldap:///cn=Add Sudo rule,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Sudo Administrator'}, - }, - 'System: Delete Sudo rule': { - 'ipapermright': {'delete'}, - 'replaces': [ - '(target = "ldap:///ipauniqueid=*,cn=sudorules,cn=sudo,$SUFFIX")(version 3.0;acl "permission:Delete Sudo rule";allow (delete) groupdn = "ldap:///cn=Delete Sudo rule,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Sudo Administrator'}, - }, - 'System: Modify Sudo rule': { - 'ipapermright': {'write'}, - 'ipapermdefaultattr': { - 'description', 'ipaenabledflag', 'usercategory', - 'hostcategory', 'cmdcategory', 'ipasudorunasusercategory', - 'ipasudorunasgroupcategory', 'externaluser', - 'ipasudorunasextusergroup', - 'ipasudorunasextuser', 'ipasudorunasextgroup', 'memberdenycmd', - 'memberallowcmd', 'memberuser', 'memberhost', 'externalhost', - 'sudonotafter', 'hostmask', 'sudoorder', 'sudonotbefore', - 'ipasudorunas', 'externalhost', 'ipasudorunasgroup', - 'ipasudoopt', 'memberhost', - }, - 'replaces': [ - '(targetattr = "description || ipaenabledflag || usercategory || hostcategory || cmdcategory || ipasudorunasusercategory || ipasudorunasgroupcategory || externaluser || ipasudorunasextuser || ipasudorunasextgroup || memberdenycmd || memberallowcmd || memberuser")(target = "ldap:///ipauniqueid=*,cn=sudorules,cn=sudo,$SUFFIX")(version 3.0;acl "permission:Modify Sudo rule";allow (write) groupdn = "ldap:///cn=Modify Sudo rule,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'Sudo Administrator'}, - }, - } - - label = _('Sudo Rules') - label_singular = _('Sudo Rule') - - takes_params = ( - Str('cn', - cli_name='sudorule_name', - label=_('Rule name'), - primary_key=True, - ), - Str('description?', - cli_name='desc', - label=_('Description'), - ), - Bool('ipaenabledflag?', - label=_('Enabled'), - flags=['no_option'], - ), - StrEnum('usercategory?', - cli_name='usercat', - label=_('User category'), - doc=_('User category the rule applies to'), - values=(u'all', ), - ), - StrEnum('hostcategory?', - cli_name='hostcat', - label=_('Host category'), - doc=_('Host category the rule applies to'), - values=(u'all', ), - ), - StrEnum('cmdcategory?', - cli_name='cmdcat', - label=_('Command category'), - doc=_('Command category the rule applies to'), - values=(u'all', ), - ), - StrEnum('ipasudorunasusercategory?', - cli_name='runasusercat', - label=_('RunAs User category'), - doc=_('RunAs User category the rule applies to'), - values=(u'all', ), - ), - StrEnum('ipasudorunasgroupcategory?', - cli_name='runasgroupcat', - label=_('RunAs Group category'), - doc=_('RunAs Group category the rule applies to'), - values=(u'all', ), - ), - Int('sudoorder?', - cli_name='order', - label=_('Sudo order'), - doc=_('integer to order the Sudo rules'), - default=0, - minvalue=0, - ), - Str('memberuser_user?', - label=_('Users'), - flags=['no_create', 'no_update', 'no_search'], - ), - Str('memberuser_group?', - label=_('User Groups'), - flags=['no_create', 'no_update', 'no_search'], - ), - Str('externaluser?', validate_externaluser, - cli_name='externaluser', - label=_('External User'), - doc=_('External User the rule applies to (sudorule-find only)'), - ), - Str('memberhost_host?', - label=_('Hosts'), - flags=['no_create', 'no_update', 'no_search'], - ), - Str('memberhost_hostgroup?', - label=_('Host Groups'), - flags=['no_create', 'no_update', 'no_search'], - ), - Str('hostmask', validate_hostmask, - normalizer=lambda x: unicode(netaddr.IPNetwork(x).cidr), - label=_('Host Masks'), - flags=['no_create', 'no_update', 'no_search'], - multivalue=True, - ), - external_host_param, - Str('memberallowcmd_sudocmd?', - label=_('Sudo Allow Commands'), - flags=['no_create', 'no_update', 'no_search'], - ), - Str('memberdenycmd_sudocmd?', - label=_('Sudo Deny Commands'), - flags=['no_create', 'no_update', 'no_search'], - ), - Str('memberallowcmd_sudocmdgroup?', - label=_('Sudo Allow Command Groups'), - flags=['no_create', 'no_update', 'no_search'], - ), - Str('memberdenycmd_sudocmdgroup?', - label=_('Sudo Deny Command Groups'), - flags=['no_create', 'no_update', 'no_search'], - ), - Str('ipasudorunas_user?', - label=_('RunAs Users'), - doc=_('Run as a user'), - flags=['no_create', 'no_update', 'no_search'], - ), - Str('ipasudorunas_group?', - label=_('Groups of RunAs Users'), - doc=_('Run as any user within a specified group'), - flags=['no_create', 'no_update', 'no_search'], - ), - Str('ipasudorunasextuser?', validate_runasextuser, - cli_name='runasexternaluser', - label=_('RunAs External User'), - doc=_('External User the commands can run as (sudorule-find only)'), - ), - Str('ipasudorunasextusergroup?', - cli_name='runasexternalusergroup', - label=_('External Groups of RunAs Users'), - doc=_('External Groups of users that the command can run as'), - flags=['no_create', 'no_update', 'no_search'], - ), - Str('ipasudorunasgroup_group?', - label=_('RunAs Groups'), - doc=_('Run with the gid of a specified POSIX group'), - flags=['no_create', 'no_update', 'no_search'], - ), - Str('ipasudorunasextgroup?', validate_runasextgroup, - cli_name='runasexternalgroup', - label=_('RunAs External Group'), - doc=_('External Group the commands can run as (sudorule-find only)'), - ), - Str('ipasudoopt?', - label=_('Sudo Option'), - flags=['no_create', 'no_update', 'no_search'], - ), - ) - - order_not_unique_msg = _( - 'order must be a unique value (%(order)d already used by %(rule)s)' - ) - - def check_order_uniqueness(self, *keys, **options): - if options.get('sudoorder') is not None: - entries = self.methods.find( - sudoorder=options['sudoorder'] - )['result'] - - if len(entries) > 0: - rule_name = entries[0]['cn'][0] - raise errors.ValidationError( - name='order', - error=self.order_not_unique_msg % { - 'order': options['sudoorder'], - 'rule': rule_name, - } - ) - - -@register() -class sudorule_add(LDAPCreate): - __doc__ = _('Create new Sudo Rule.') - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - self.obj.check_order_uniqueness(*keys, **options) - # Sudo Rules are enabled by default - entry_attrs['ipaenabledflag'] = 'TRUE' - return dn - - msg_summary = _('Added Sudo Rule "%(value)s"') - - -@register() -class sudorule_del(LDAPDelete): - __doc__ = _('Delete Sudo Rule.') - - msg_summary = _('Deleted Sudo Rule "%(value)s"') - - -@register() -class sudorule_mod(LDAPUpdate): - __doc__ = _('Modify Sudo Rule.') - - msg_summary = _('Modified Sudo Rule "%(value)s"') - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - - if 'sudoorder' in options: - new_order = options.get('sudoorder') - old_entry = self.api.Command.sudorule_show(keys[-1])['result'] - if 'sudoorder' in old_entry: - old_order = int(old_entry['sudoorder'][0]) - if old_order != new_order: - self.obj.check_order_uniqueness(*keys, **options) - else: - self.obj.check_order_uniqueness(*keys, **options) - - try: - _entry_attrs = ldap.get_entry(dn, self.obj.default_attributes) - except errors.NotFound: - self.obj.handle_not_found(*keys) - - error = _("%(type)s category cannot be set to 'all' " - "while there are allowed %(objects)s") - - category_info = [( - 'usercategory', - ['memberuser', 'externaluser'], - error % {'type': _('user'), 'objects': _('users')} - ), - ( - 'hostcategory', - ['memberhost', 'externalhost', 'hostmask'], - error % {'type': _('host'), 'objects': _('hosts')} - ), - ( - 'cmdcategory', - ['memberallowcmd'], - error % {'type': _('command'), 'objects': _('commands')} - ), - ( - 'ipasudorunasusercategory', - ['ipasudorunas', 'ipasudorunasextuser', - 'ipasudorunasextusergroup'], - error % {'type': _('runAs user'), 'objects': _('runAs users')} - ), - ( - 'ipasudorunasgroupcategory', - ['ipasudorunasgroup', 'ipasudorunasextgroup'], - error % {'type': _('group runAs'), 'objects': _('runAs groups')} - ), - ] - - - # Enforce the checks for all the categories - for category, member_attrs, error in category_info: - any_member_attrs_set = any(attr in _entry_attrs - for attr in member_attrs) - - if is_all(options, category) and any_member_attrs_set: - raise errors.MutuallyExclusiveError(reason=error) - - return dn - - -@register() -class sudorule_find(LDAPSearch): - __doc__ = _('Search for Sudo Rule.') - - msg_summary = ngettext( - '%(count)d Sudo Rule matched', '%(count)d Sudo Rules matched', 0 - ) - - -@register() -class sudorule_show(LDAPRetrieve): - __doc__ = _('Display Sudo Rule.') - - -@register() -class sudorule_enable(LDAPQuery): - __doc__ = _('Enable a Sudo Rule.') - - def execute(self, cn, **options): - ldap = self.obj.backend - - dn = self.obj.get_dn(cn) - try: - entry_attrs = ldap.get_entry(dn, ['ipaenabledflag']) - except errors.NotFound: - self.obj.handle_not_found(cn) - - entry_attrs['ipaenabledflag'] = ['TRUE'] - - try: - ldap.update_entry(entry_attrs) - except errors.EmptyModlist: - pass - - return dict(result=True) - - -@register() -class sudorule_disable(LDAPQuery): - __doc__ = _('Disable a Sudo Rule.') - - def execute(self, cn, **options): - ldap = self.obj.backend - - dn = self.obj.get_dn(cn) - try: - entry_attrs = ldap.get_entry(dn, ['ipaenabledflag']) - except errors.NotFound: - self.obj.handle_not_found(cn) - - entry_attrs['ipaenabledflag'] = ['FALSE'] - - try: - ldap.update_entry(entry_attrs) - except errors.EmptyModlist: - pass - - return dict(result=True) - - -@register() -class sudorule_add_allow_command(LDAPAddMember): - __doc__ = _('Add commands and sudo command groups affected by Sudo Rule.') - - member_attributes = ['memberallowcmd'] - member_count_out = ('%i object added.', '%i objects added.') - - def pre_callback(self, ldap, dn, found, not_found, *keys, **options): - assert isinstance(dn, DN) - - try: - _entry_attrs = ldap.get_entry(dn, self.obj.default_attributes) - except errors.NotFound: - self.obj.handle_not_found(*keys) - - if is_all(_entry_attrs, 'cmdcategory'): - raise errors.MutuallyExclusiveError( - reason=_("commands cannot be added when command " - "category='all'")) - - return dn - - -@register() -class sudorule_remove_allow_command(LDAPRemoveMember): - __doc__ = _('Remove commands and sudo command groups affected by Sudo Rule.') - - member_attributes = ['memberallowcmd'] - member_count_out = ('%i object removed.', '%i objects removed.') - - -@register() -class sudorule_add_deny_command(LDAPAddMember): - __doc__ = _('Add commands and sudo command groups affected by Sudo Rule.') - - member_attributes = ['memberdenycmd'] - member_count_out = ('%i object added.', '%i objects added.') - - def pre_callback(self, ldap, dn, found, not_found, *keys, **options): - assert isinstance(dn, DN) - return dn - - -@register() -class sudorule_remove_deny_command(LDAPRemoveMember): - __doc__ = _('Remove commands and sudo command groups affected by Sudo Rule.') - - member_attributes = ['memberdenycmd'] - member_count_out = ('%i object removed.', '%i objects removed.') - - -@register() -class sudorule_add_user(LDAPAddMember): - __doc__ = _('Add users and groups affected by Sudo Rule.') - - member_attributes = ['memberuser'] - member_count_out = ('%i object added.', '%i objects added.') - - def pre_callback(self, ldap, dn, found, not_found, *keys, **options): - assert isinstance(dn, DN) - - try: - _entry_attrs = ldap.get_entry(dn, self.obj.default_attributes) - except errors.NotFound: - self.obj.handle_not_found(*keys) - - if is_all(_entry_attrs, 'usercategory'): - raise errors.MutuallyExclusiveError( - reason=_("users cannot be added when user category='all'")) - - return add_external_pre_callback('user', ldap, dn, keys, options) - - def post_callback(self, ldap, completed, failed, dn, entry_attrs, - *keys, **options): - assert isinstance(dn, DN) - return add_external_post_callback(ldap, dn, entry_attrs, - failed=failed, - completed=completed, - memberattr='memberuser', - membertype='user', - externalattr='externaluser') - - -@register() -class sudorule_remove_user(LDAPRemoveMember): - __doc__ = _('Remove users and groups affected by Sudo Rule.') - - member_attributes = ['memberuser'] - member_count_out = ('%i object removed.', '%i objects removed.') - - def post_callback(self, ldap, completed, failed, dn, entry_attrs, - *keys, **options): - assert isinstance(dn, DN) - return remove_external_post_callback(ldap, dn, entry_attrs, - failed=failed, - completed=completed, - memberattr='memberuser', - membertype='user', - externalattr='externaluser') - - -@register() -class sudorule_add_host(LDAPAddMember): - __doc__ = _('Add hosts and hostgroups affected by Sudo Rule.') - - member_attributes = ['memberhost'] - member_count_out = ('%i object added.', '%i objects added.') - - def get_options(self): - for option in super(sudorule_add_host, self).get_options(): - yield option - yield hostmask_membership_param - - def pre_callback(self, ldap, dn, found, not_found, *keys, **options): - assert isinstance(dn, DN) - try: - _entry_attrs = ldap.get_entry(dn, self.obj.default_attributes) - except errors.NotFound: - self.obj.handle_not_found(*keys) - - if is_all(_entry_attrs, 'hostcategory'): - raise errors.MutuallyExclusiveError( - reason=_("hosts cannot be added when host category='all'")) - - return add_external_pre_callback('host', ldap, dn, keys, options) - - def post_callback(self, ldap, completed, failed, dn, entry_attrs, - *keys, **options): - assert isinstance(dn, DN) - try: - _entry_attrs = ldap.get_entry(dn, self.obj.default_attributes) - except errors.NotFound: - self.obj.handle_not_found(*keys) - - if 'hostmask' in options: - norm = lambda x: unicode(netaddr.IPNetwork(x).cidr) - - old_masks = set(norm(m) for m in _entry_attrs.get('hostmask', [])) - new_masks = set(norm(m) for m in options['hostmask']) - - num_added = len(new_masks - old_masks) - - if num_added: - entry_attrs['hostmask'] = list(old_masks | new_masks) - try: - ldap.update_entry(entry_attrs) - except errors.EmptyModlist: - pass - completed = completed + num_added - - return add_external_post_callback(ldap, dn, entry_attrs, - failed=failed, - completed=completed, - memberattr='memberhost', - membertype='host', - externalattr='externalhost') - - -@register() -class sudorule_remove_host(LDAPRemoveMember): - __doc__ = _('Remove hosts and hostgroups affected by Sudo Rule.') - - member_attributes = ['memberhost'] - member_count_out = ('%i object removed.', '%i objects removed.') - - def get_options(self): - for option in super(sudorule_remove_host, self).get_options(): - yield option - yield hostmask_membership_param - - def post_callback(self, ldap, completed, failed, dn, entry_attrs, - *keys, **options): - assert isinstance(dn, DN) - - try: - _entry_attrs = ldap.get_entry(dn, self.obj.default_attributes) - except errors.NotFound: - self.obj.handle_not_found(*keys) - - if 'hostmask' in options: - def norm(x): - return unicode(netaddr.IPNetwork(x).cidr) - - old_masks = set(norm(m) for m in _entry_attrs.get('hostmask', [])) - removed_masks = set(norm(m) for m in options['hostmask']) - - num_added = len(removed_masks & old_masks) - - if num_added: - entry_attrs['hostmask'] = list(old_masks - removed_masks) - try: - ldap.update_entry(entry_attrs) - except errors.EmptyModlist: - pass - completed = completed + num_added - - return remove_external_post_callback(ldap, dn, entry_attrs, - failed=failed, - completed=completed, - memberattr='memberhost', - membertype='host', - externalattr='externalhost') - - -@register() -class sudorule_add_runasuser(LDAPAddMember): - __doc__ = _('Add users and groups for Sudo to execute as.') - - member_attributes = ['ipasudorunas'] - member_count_out = ('%i object added.', '%i objects added.') - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - - def check_validity(runas): - v = unicode(runas) - if v.upper() == u'ALL': - return False - return True - - try: - _entry_attrs = ldap.get_entry(dn, self.obj.default_attributes) - except errors.NotFound: - self.obj.handle_not_found(*keys) - - if any((is_all(_entry_attrs, 'ipasudorunasusercategory'), - is_all(_entry_attrs, 'ipasudorunasgroupcategory'))): - - raise errors.MutuallyExclusiveError( - reason=_("users cannot be added when runAs user or runAs " - "group category='all'")) - - if 'user' in options: - for name in options['user']: - if not check_validity(name): - raise errors.ValidationError(name='runas-user', - error=unicode(_("RunAsUser does not accept " - "'%(name)s' as a user name")) % - dict(name=name)) - - if 'group' in options: - for name in options['group']: - if not check_validity(name): - raise errors.ValidationError(name='runas-user', - error=unicode(_("RunAsUser does not accept " - "'%(name)s' as a group name")) % - dict(name=name)) - - return add_external_pre_callback('user', ldap, dn, keys, options) - - def post_callback(self, ldap, completed, failed, dn, entry_attrs, - *keys, **options): - assert isinstance(dn, DN) - - # Since external_post_callback returns the total number of completed - # entries yet (that is, any external users it added plus the value of - # passed variable 'completed', we need to pass 0 as completed, - # so that the entries added by the framework are not counted twice - # (once in each call of add_external_post_callback) - - (completed_ex_users, dn) = add_external_post_callback(ldap, dn, - entry_attrs, - failed=failed, - completed=0, - memberattr='ipasudorunas', - membertype='user', - externalattr='ipasudorunasextuser', - ) - - (completed_ex_groups, dn) = add_external_post_callback(ldap, dn, - entry_attrs=entry_attrs, - failed=failed, - completed=0, - memberattr='ipasudorunas', - membertype='group', - externalattr='ipasudorunasextusergroup', - ) - - return (completed + completed_ex_users + completed_ex_groups, dn) - - -@register() -class sudorule_remove_runasuser(LDAPRemoveMember): - __doc__ = _('Remove users and groups for Sudo to execute as.') - - member_attributes = ['ipasudorunas'] - member_count_out = ('%i object removed.', '%i objects removed.') - - def post_callback(self, ldap, completed, failed, dn, entry_attrs, - *keys, **options): - assert isinstance(dn, DN) - - # Since external_post_callback returns the total number of completed - # entries yet (that is, any external users it added plus the value of - # passed variable 'completed', we need to pass 0 as completed, - # so that the entries added by the framework are not counted twice - # (once in each call of remove_external_post_callback) - - (completed_ex_users, dn) = remove_external_post_callback(ldap, dn, - entry_attrs=entry_attrs, - failed=failed, - completed=0, - memberattr='ipasudorunas', - membertype='user', - externalattr='ipasudorunasextuser', - ) - - (completed_ex_groups, dn) = remove_external_post_callback(ldap, dn, - entry_attrs=entry_attrs, - failed=failed, - completed=0, - memberattr='ipasudorunas', - membertype='group', - externalattr='ipasudorunasextusergroup', - ) - - return (completed + completed_ex_users + completed_ex_groups, dn) - - -@register() -class sudorule_add_runasgroup(LDAPAddMember): - __doc__ = _('Add group for Sudo to execute as.') - - member_attributes = ['ipasudorunasgroup'] - member_count_out = ('%i object added.', '%i objects added.') - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - - def check_validity(runas): - v = unicode(runas) - if v.upper() == u'ALL': - return False - return True - - try: - _entry_attrs = ldap.get_entry(dn, self.obj.default_attributes) - except errors.NotFound: - self.obj.handle_not_found(*keys) - if is_all(_entry_attrs, 'ipasudorunasusercategory') or \ - is_all(_entry_attrs, 'ipasudorunasgroupcategory'): - raise errors.MutuallyExclusiveError( - reason=_("users cannot be added when runAs user or runAs " - "group category='all'")) - - if 'group' in options: - for name in options['group']: - if not check_validity(name): - raise errors.ValidationError(name='runas-group', - error=unicode(_("RunAsGroup does not accept " - "'%(name)s' as a group name")) % - dict(name=name)) - - return add_external_pre_callback('group', ldap, dn, keys, options) - - def post_callback(self, ldap, completed, failed, dn, entry_attrs, - *keys, **options): - assert isinstance(dn, DN) - return add_external_post_callback(ldap, dn, entry_attrs, - failed=failed, - completed=completed, - memberattr='ipasudorunasgroup', - membertype='group', - externalattr='ipasudorunasextgroup', - ) - - -@register() -class sudorule_remove_runasgroup(LDAPRemoveMember): - __doc__ = _('Remove group for Sudo to execute as.') - - member_attributes = ['ipasudorunasgroup'] - member_count_out = ('%i object removed.', '%i objects removed.') - - def post_callback(self, ldap, completed, failed, dn, entry_attrs, - *keys, **options): - assert isinstance(dn, DN) - return remove_external_post_callback(ldap, dn, entry_attrs, - failed=failed, - completed=completed, - memberattr='ipasudorunasgroup', - membertype='group', - externalattr='ipasudorunasextgroup', - ) - - -@register() -class sudorule_add_option(LDAPQuery): - __doc__ = _('Add an option to the Sudo Rule.') - - has_output = output.standard_entry - takes_options = ( - Str('ipasudoopt', - cli_name='sudooption', - label=_('Sudo Option'), - ), - ) - - def execute(self, cn, **options): - ldap = self.obj.backend - - dn = self.obj.get_dn(cn) - - if not options['ipasudoopt'].strip(): - raise errors.EmptyModlist() - entry_attrs = ldap.get_entry(dn, ['ipasudoopt']) - - try: - if options['ipasudoopt'] not in entry_attrs['ipasudoopt']: - entry_attrs.setdefault('ipasudoopt', []).append( - options['ipasudoopt']) - else: - raise errors.DuplicateEntry - except KeyError: - entry_attrs.setdefault('ipasudoopt', []).append( - options['ipasudoopt']) - try: - ldap.update_entry(entry_attrs) - except errors.EmptyModlist: - pass - except errors.NotFound: - self.obj.handle_not_found(cn) - - attrs_list = self.obj.default_attributes - entry_attrs = ldap.get_entry(dn, attrs_list) - - entry_attrs = entry_to_dict(entry_attrs, **options) - - return dict(result=entry_attrs, value=pkey_to_value(cn, options)) - - -@register() -class sudorule_remove_option(LDAPQuery): - __doc__ = _('Remove an option from Sudo Rule.') - - has_output = output.standard_entry - takes_options = ( - Str('ipasudoopt', - cli_name='sudooption', - label=_('Sudo Option'), - ), - ) - - def execute(self, cn, **options): - ldap = self.obj.backend - - dn = self.obj.get_dn(cn) - - if not options['ipasudoopt'].strip(): - raise errors.EmptyModlist() - - entry_attrs = ldap.get_entry(dn, ['ipasudoopt']) - - try: - if options['ipasudoopt'] in entry_attrs['ipasudoopt']: - entry_attrs.setdefault('ipasudoopt', []).remove( - options['ipasudoopt']) - ldap.update_entry(entry_attrs) - else: - raise errors.AttrValueNotFound( - attr='ipasudoopt', - value=options['ipasudoopt'] - ) - except ValueError: - pass - except KeyError: - raise errors.AttrValueNotFound( - attr='ipasudoopt', - value=options['ipasudoopt'] - ) - except errors.NotFound: - self.obj.handle_not_found(cn) - - attrs_list = self.obj.default_attributes - entry_attrs = ldap.get_entry(dn, attrs_list) - - entry_attrs = entry_to_dict(entry_attrs, **options) - - return dict(result=entry_attrs, value=pkey_to_value(cn, options)) diff --git a/ipalib/plugins/topology.py b/ipalib/plugins/topology.py deleted file mode 100644 index a6e638479..000000000 --- a/ipalib/plugins/topology.py +++ /dev/null @@ -1,503 +0,0 @@ -# -# Copyright (C) 2015 FreeIPA Contributors see COPYING for license -# - -import six - -from ipalib import api, errors -from ipalib import Int, Str, StrEnum, Flag, DNParam -from ipalib.plugable import Registry -from .baseldap import ( - LDAPObject, LDAPSearch, LDAPCreate, LDAPDelete, LDAPUpdate, LDAPQuery, - LDAPRetrieve) -from ipalib import _, ngettext -from ipalib import output -from ipalib.constants import DOMAIN_LEVEL_1 -from ipalib.util import create_topology_graph, get_topology_connection_errors -from ipapython.dn import DN - -if six.PY3: - unicode = str - -__doc__ = _(""" -Topology - -Management of a replication topology at domain level 1. -""") + _(""" -IPA server's data is stored in LDAP server in two suffixes: -* domain suffix, e.g., 'dc=example,dc=com', contains all domain related data -* ca suffix, 'o=ipaca', is present only on server with CA installed. It - contains data for Certificate Server component -""") + _(""" -Data stored on IPA servers is replicated to other IPA servers. The way it is -replicated is defined by replication agreements. Replication agreements needs -to be set for both suffixes separately. On domain level 0 they are managed -using ipa-replica-manage and ipa-csreplica-manage tools. With domain level 1 -they are managed centrally using `ipa topology*` commands. -""") + _(""" -Agreements are represented by topology segments. By default topology segment -represents 2 replication agreements - one for each direction, e.g., A to B and -B to A. Creation of unidirectional segments is not allowed. -""") + _(""" -To verify that no server is disconnected in the topology of the given suffix, -use: - ipa topologysuffix-verify $suffix -""") + _(""" - -Examples: - Find all IPA servers: - ipa server-find -""") + _(""" - Find all suffixes: - ipa topologysuffix-find -""") + _(""" - Add topology segment to 'domain' suffix: - ipa topologysegment-add domain --left IPA_SERVER_A --right IPA_SERVER_B -""") + _(""" - Add topology segment to 'ca' suffix: - ipa topologysegment-add ca --left IPA_SERVER_A --right IPA_SERVER_B -""") + _(""" - List all topology segments in 'domain' suffix: - ipa topologysegment-find domain -""") + _(""" - List all topology segments in 'ca' suffix: - ipa topologysegment-find ca -""") + _(""" - Delete topology segment in 'domain' suffix: - ipa topologysegment-del domain segment_name -""") + _(""" - Delete topology segment in 'ca' suffix: - ipa topologysegment-del ca segment_name -""") + _(""" - Verify topology of 'domain' suffix: - ipa topologysuffix-verify domain -""") + _(""" - Verify topology of 'ca' suffix: - ipa topologysuffix-verify ca -""") - -register = Registry() - - -def validate_domain_level(api): - current = int(api.Command.domainlevel_get()['result']) - if current < DOMAIN_LEVEL_1: - raise errors.InvalidDomainLevelError( - reason=_('Topology management requires minimum domain level {0} ' - .format(DOMAIN_LEVEL_1)) - ) - - -@register() -class topologysegment(LDAPObject): - """ - Topology segment. - """ - parent_object = 'topologysuffix' - container_dn = api.env.container_topology - object_name = _('segment') - object_name_plural = _('segments') - object_class = ['iparepltoposegment'] - default_attributes = [ - 'cn', - 'ipaReplTopoSegmentdirection', 'ipaReplTopoSegmentrightNode', - 'ipaReplTopoSegmentLeftNode', 'nsds5replicastripattrs', - 'nsds5replicatedattributelist', 'nsds5replicatedattributelisttotal', - 'nsds5replicatimeout', 'nsds5replicaenabled' - ] - search_display_attributes = [ - 'cn', 'ipaReplTopoSegmentdirection', 'ipaReplTopoSegmentrightNode', - 'ipaReplTopoSegmentLeftNode' - ] - - label = _('Topology Segments') - label_singular = _('Topology Segment') - - takes_params = ( - Str( - 'cn', - maxlength=255, - cli_name='name', - primary_key=True, - label=_('Segment name'), - default_from=lambda iparepltoposegmentleftnode, iparepltoposegmentrightnode: - '%s-to-%s' % (iparepltoposegmentleftnode, iparepltoposegmentrightnode), - normalizer=lambda value: value.lower(), - doc=_('Arbitrary string identifying the segment'), - ), - Str( - 'iparepltoposegmentleftnode', - pattern='^[a-zA-Z0-9.][a-zA-Z0-9.-]{0,252}[a-zA-Z0-9.$-]?$', - pattern_errmsg='may only include letters, numbers, -, . and $', - maxlength=255, - cli_name='leftnode', - label=_('Left node'), - normalizer=lambda value: value.lower(), - doc=_('Left replication node - an IPA server'), - flags={'no_update'}, - ), - Str( - 'iparepltoposegmentrightnode', - pattern='^[a-zA-Z0-9.][a-zA-Z0-9.-]{0,252}[a-zA-Z0-9.$-]?$', - pattern_errmsg='may only include letters, numbers, -, . and $', - maxlength=255, - cli_name='rightnode', - label=_('Right node'), - normalizer=lambda value: value.lower(), - doc=_('Right replication node - an IPA server'), - flags={'no_update'}, - ), - StrEnum( - 'iparepltoposegmentdirection', - cli_name='direction', - label=_('Connectivity'), - values=(u'both', u'left-right', u'right-left'), - default=u'both', - autofill=True, - doc=_('Direction of replication between left and right replication ' - 'node'), - flags={'no_option', 'no_update'}, - ), - Str( - 'nsds5replicastripattrs?', - cli_name='stripattrs', - label=_('Attributes to strip'), - normalizer=lambda value: value.lower(), - doc=_('A space separated list of attributes which are removed from ' - 'replication updates.') - ), - Str( - 'nsds5replicatedattributelist?', - cli_name='replattrs', - label='Attributes to replicate', - doc=_('Attributes that are not replicated to a consumer server ' - 'during a fractional update. E.g., `(objectclass=*) ' - '$ EXCLUDE accountlockout memberof'), - ), - Str( - 'nsds5replicatedattributelisttotal?', - cli_name='replattrstotal', - label=_('Attributes for total update'), - doc=_('Attributes that are not replicated to a consumer server ' - 'during a total update. E.g. (objectclass=*) $ EXCLUDE ' - 'accountlockout'), - ), - Int( - 'nsds5replicatimeout?', - cli_name='timeout', - label=_('Session timeout'), - minvalue=0, - doc=_('Number of seconds outbound LDAP operations waits for a ' - 'response from the remote replica before timing out and ' - 'failing'), - ), - StrEnum( - 'nsds5replicaenabled?', - cli_name='enabled', - label=_('Replication agreement enabled'), - doc=_('Whether a replication agreement is active, meaning whether ' - 'replication is occurring per that agreement'), - values=(u'on', u'off'), - flags={'no_option'}, - ), - ) - - def validate_nodes(self, ldap, dn, entry_attrs): - leftnode = entry_attrs.get('iparepltoposegmentleftnode') - rightnode = entry_attrs.get('iparepltoposegmentrightnode') - - if not leftnode and not rightnode: - return # nothing to check - - # check if nodes are IPA servers - masters = self.api.Command.server_find( - '', sizelimit=0, no_members=False)['result'] - m_hostnames = [master['cn'][0].lower() for master in masters] - - if leftnode and leftnode not in m_hostnames: - raise errors.ValidationError( - name='leftnode', - error=_('left node is not a topology node: %(leftnode)s') % - dict(leftnode=leftnode) - ) - - if rightnode and rightnode not in m_hostnames: - raise errors.ValidationError( - name='rightnode', - error=_('right node is not a topology node: %(rightnode)s') % - dict(rightnode=rightnode) - ) - - # prevent creation of reflexive relation - key = 'leftnode' - if not leftnode or not rightnode: # get missing end - _entry_attrs = ldap.get_entry(dn, ['*']) - if not leftnode: - key = 'rightnode' - leftnode = _entry_attrs['iparepltoposegmentleftnode'][0] - else: - rightnode = _entry_attrs['iparepltoposegmentrightnode'][0] - - if leftnode == rightnode: - raise errors.ValidationError( - name=key, - error=_('left node and right node must not be the same') - ) - - -@register() -class topologysegment_find(LDAPSearch): - __doc__ = _('Search for topology segments.') - - msg_summary = ngettext( - '%(count)d segment matched', - '%(count)d segments matched', 0 - ) - - -@register() -class topologysegment_add(LDAPCreate): - __doc__ = _('Add a new segment.') - - msg_summary = _('Added segment "%(value)s"') - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - validate_domain_level(self.api) - self.obj.validate_nodes(ldap, dn, entry_attrs) - return dn - - -@register() -class topologysegment_del(LDAPDelete): - __doc__ = _('Delete a segment.') - - msg_summary = _('Deleted segment "%(value)s"') - - def pre_callback(self, ldap, dn, *keys, **options): - assert isinstance(dn, DN) - validate_domain_level(self.api) - return dn - - -@register() -class topologysegment_mod(LDAPUpdate): - __doc__ = _('Modify a segment.') - - msg_summary = _('Modified segment "%(value)s"') - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - validate_domain_level(self.api) - self.obj.validate_nodes(ldap, dn, entry_attrs) - return dn - - -@register() -class topologysegment_reinitialize(LDAPQuery): - __doc__ = _('Request a full re-initialization of the node ' - 'retrieving data from the other node.') - - has_output = output.standard_value - msg_summary = _('%(value)s') - - takes_options = ( - Flag( - 'left?', - doc=_('Initialize left node'), - default=False, - ), - Flag( - 'right?', - doc=_('Initialize right node'), - default=False, - ), - Flag( - 'stop?', - doc=_('Stop already started refresh of chosen node(s)'), - default=False, - ), - ) - - def execute(self, *keys, **options): - dn = self.obj.get_dn(*keys, **options) - validate_domain_level(self.api) - - entry = self.obj.backend.get_entry( - dn, [ - 'nsds5beginreplicarefresh;left', - 'nsds5beginreplicarefresh;right' - ]) - - left = options.get('left') - right = options.get('right') - stop = options.get('stop') - - if not left and not right: - raise errors.OptionError( - _('left or right node has to be specified') - ) - - if left and right: - raise errors.OptionError( - _('only one node can be specified') - ) - - action = u'start' - msg = _('Replication refresh for segment: "%(pkey)s" requested.') - if stop: - action = u'stop' - msg = _('Stopping of replication refresh for segment: "' - '%(pkey)s" requested.') - - # left and right are swapped because internally it's a push not - # pull operation - if right: - entry['nsds5beginreplicarefresh;left'] = [action] - if left: - entry['nsds5beginreplicarefresh;right'] = [action] - - self.obj.backend.update_entry(entry) - - msg = msg % {'pkey': keys[-1]} - return dict( - result=True, - value=msg, - ) - - -@register() -class topologysegment_show(LDAPRetrieve): - __doc__ = _('Display a segment.') - - -@register() -class topologysuffix(LDAPObject): - """ - Suffix managed by the topology plugin. - """ - container_dn = api.env.container_topology - object_name = _('suffix') - object_name_plural = _('suffixes') - object_class = ['iparepltopoconf'] - default_attributes = ['cn', 'ipaReplTopoConfRoot'] - search_display_attributes = ['cn', 'ipaReplTopoConfRoot'] - label = _('Topology suffixes') - label_singular = _('Topology suffix') - - takes_params = ( - Str( - 'cn', - cli_name='name', - primary_key=True, - label=_('Suffix name'), - ), - DNParam( - 'iparepltopoconfroot', - cli_name='suffix_dn', - label=_('Managed LDAP suffix DN'), - ), - ) - - -@register() -class topologysuffix_find(LDAPSearch): - __doc__ = _('Search for topology suffixes.') - - msg_summary = ngettext( - '%(count)d topology suffix matched', - '%(count)d topology suffixes matched', 0 - ) - - -@register() -class topologysuffix_del(LDAPDelete): - __doc__ = _('Delete a topology suffix.') - - NO_CLI = True - - msg_summary = _('Deleted topology suffix "%(value)s"') - - def pre_callback(self, ldap, dn, *keys, **options): - assert isinstance(dn, DN) - validate_domain_level(self.api) - return dn - - -@register() -class topologysuffix_add(LDAPCreate): - __doc__ = _('Add a new topology suffix to be managed.') - - NO_CLI = True - - msg_summary = _('Added topology suffix "%(value)s"') - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - validate_domain_level(self.api) - return dn - - -@register() -class topologysuffix_mod(LDAPUpdate): - __doc__ = _('Modify a topology suffix.') - - NO_CLI = True - - msg_summary = _('Modified topology suffix "%(value)s"') - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - validate_domain_level(self.api) - return dn - - -@register() -class topologysuffix_show(LDAPRetrieve): - __doc__ = _('Show managed suffix.') - - -@register() -class topologysuffix_verify(LDAPQuery): - __doc__ = _(''' -Verify replication topology for suffix. - -Checks done: - 1. check if a topology is not disconnected. In other words if there are - replication paths between all servers. - 2. check if servers don't have more than the recommended number of - replication agreements -''') - - def execute(self, *keys, **options): - - validate_domain_level(self.api) - - masters = self.api.Command.server_find( - '', sizelimit=0, no_members=False)['result'] - segments = self.api.Command.topologysegment_find( - keys[0], sizelimit=0)['result'] - graph = create_topology_graph(masters, segments) - master_cns = [m['cn'][0] for m in masters] - master_cns.sort() - - # check if each master can contact others - connect_errors = get_topology_connection_errors(graph) - - # check if suggested maximum number of agreements per replica - max_agmts_errors = [] - for m in master_cns: - # chosen direction doesn't matter much given that 'both' is the - # only allowed direction - suppliers = graph.get_tails(m) - if len(suppliers) > self.api.env.recommended_max_agmts: - max_agmts_errors.append((m, suppliers)) - - return dict( - result={ - 'in_order': not connect_errors and not max_agmts_errors, - 'connect_errors': connect_errors, - 'max_agmts_errors': max_agmts_errors, - 'max_agmts': self.api.env.recommended_max_agmts - }, - ) diff --git a/ipalib/plugins/trust.py b/ipalib/plugins/trust.py deleted file mode 100644 index ee0ab5d10..000000000 --- a/ipalib/plugins/trust.py +++ /dev/null @@ -1,1725 +0,0 @@ -# Authors: -# Alexander Bokovoy <abokovoy@redhat.com> -# Martin Kosek <mkosek@redhat.com> -# -# Copyright (C) 2011 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/>. - -import six - -from ipalib.messages import ( - add_message, - BrokenTrust) -from ipalib.plugable import Registry -from .baseldap import ( - pkey_to_value, - entry_to_dict, - LDAPCreate, - LDAPDelete, - LDAPUpdate, - LDAPSearch, - LDAPRetrieve, - LDAPObject, - LDAPQuery) -from .dns import dns_container_exists -from ipapython.dn import DN -from ipapython.ipautil import realm_to_suffix -from ipapython.ipa_log_manager import root_logger -from ipalib import api, Str, StrEnum, Password, Bool, _, ngettext, Int, Flag -from ipalib import Command -from ipalib import errors -from ipalib import output -from ldap import SCOPE_SUBTREE -from time import sleep - -if six.PY3: - unicode = str - -try: - import pysss_murmur #pylint: disable=F0401 - _murmur_installed = True -except Exception as e: - _murmur_installed = False - -try: - import pysss_nss_idmap #pylint: disable=F0401 - _nss_idmap_installed = True -except Exception as e: - _nss_idmap_installed = False - -if api.env.in_server and api.env.context in ['lite', 'server']: - try: - import ipaserver.dcerpc #pylint: disable=F0401 - from ipaserver.dcerpc import TRUST_ONEWAY, TRUST_BIDIRECTIONAL - import dbus - import dbus.mainloop.glib - _bindings_installed = True - except ImportError: - _bindings_installed = False - -__doc__ = _(""" -Cross-realm trusts - -Manage trust relationship between IPA and Active Directory domains. - -In order to allow users from a remote domain to access resources in IPA -domain, trust relationship needs to be established. Currently IPA supports -only trusts between IPA and Active Directory domains under control of Windows -Server 2008 or later, with functional level 2008 or later. - -Please note that DNS on both IPA and Active Directory domain sides should be -configured properly to discover each other. Trust relationship relies on -ability to discover special resources in the other domain via DNS records. - -Examples: - -1. Establish cross-realm trust with Active Directory using AD administrator - credentials: - - ipa trust-add --type=ad <ad.domain> --admin <AD domain administrator> --password - -2. List all existing trust relationships: - - ipa trust-find - -3. Show details of the specific trust relationship: - - ipa trust-show <ad.domain> - -4. Delete existing trust relationship: - - ipa trust-del <ad.domain> - -Once trust relationship is established, remote users will need to be mapped -to local POSIX groups in order to actually use IPA resources. The mapping should -be done via use of external membership of non-POSIX group and then this group -should be included into one of local POSIX groups. - -Example: - -1. Create group for the trusted domain admins' mapping and their local POSIX group: - - ipa group-add --desc='<ad.domain> admins external map' ad_admins_external --external - ipa group-add --desc='<ad.domain> admins' ad_admins - -2. Add security identifier of Domain Admins of the <ad.domain> to the ad_admins_external - group: - - ipa group-add-member ad_admins_external --external 'AD\\Domain Admins' - -3. Allow members of ad_admins_external group to be associated with ad_admins POSIX group: - - ipa group-add-member ad_admins --groups ad_admins_external - -4. List members of external members of ad_admins_external group to see their SIDs: - - ipa group-show ad_admins_external - - -GLOBAL TRUST CONFIGURATION - -When IPA AD trust subpackage is installed and ipa-adtrust-install is run, -a local domain configuration (SID, GUID, NetBIOS name) is generated. These -identifiers are then used when communicating with a trusted domain of the -particular type. - -1. Show global trust configuration for Active Directory type of trusts: - - ipa trustconfig-show --type ad - -2. Modify global configuration for all trusts of Active Directory type and set - a different fallback primary group (fallback primary group GID is used as - a primary user GID if user authenticating to IPA domain does not have any other - primary GID already set): - - ipa trustconfig-mod --type ad --fallback-primary-group "alternative AD group" - -3. Change primary fallback group back to default hidden group (any group with - posixGroup object class is allowed): - - ipa trustconfig-mod --type ad --fallback-primary-group "Default SMB Group" -""") - -register = Registry() - -trust_output_params = ( - Str('trustdirection', - label=_('Trust direction')), - Str('trusttype', - label=_('Trust type')), - Str('truststatus', - label=_('Trust status')), -) - -_trust_type_dict = {1 : _('Non-Active Directory domain'), - 2 : _('Active Directory domain'), - 3 : _('RFC4120-compliant Kerberos realm')} -_trust_direction_dict = {1 : _('Trusting forest'), - 2 : _('Trusted forest'), - 3 : _('Two-way trust')} -_trust_status_dict = {True : _('Established and verified'), - False : _('Waiting for confirmation by remote side')} -_trust_type_dict_unknown = _('Unknown') - -_trust_type_option = StrEnum('trust_type', - cli_name='type', - label=_('Trust type (ad for Active Directory, default)'), - values=(u'ad',), - default=u'ad', - autofill=True, - ) - -DEFAULT_RANGE_SIZE = 200000 - -DBUS_IFACE_TRUST = 'com.redhat.idm.trust' - -CRED_STYLE_SAMBA = 1 -CRED_STYLE_KERBEROS = 2 - -def trust_type_string(level): - """ - Returns a string representing a type of the trust. The original field is an enum: - LSA_TRUST_TYPE_DOWNLEVEL = 0x00000001, - LSA_TRUST_TYPE_UPLEVEL = 0x00000002, - LSA_TRUST_TYPE_MIT = 0x00000003 - """ - string = _trust_type_dict.get(int(level), _trust_type_dict_unknown) - return unicode(string) - -def trust_direction_string(level): - """ - Returns a string representing a direction of the trust. The original field is a bitmask taking two bits in use - LSA_TRUST_DIRECTION_INBOUND = 0x00000001, - LSA_TRUST_DIRECTION_OUTBOUND = 0x00000002 - """ - string = _trust_direction_dict.get(int(level), _trust_type_dict_unknown) - return unicode(string) - -def trust_status_string(level): - string = _trust_status_dict.get(level, _trust_type_dict_unknown) - return unicode(string) - -def make_trust_dn(env, trust_type, dn): - assert isinstance(dn, DN) - if trust_type: - container_dn = DN(('cn', trust_type), env.container_trusts, env.basedn) - return DN(dn, container_dn) - return dn - -def find_adtrust_masters(ldap, api): - """ - Returns a list of names of IPA servers with ADTRUST component configured. - """ - - try: - entries, truncated = ldap.find_entries( - "cn=ADTRUST", - base_dn=api.env.container_masters + api.env.basedn - ) - except errors.NotFound: - entries = [] - - return [entry.dn[1].value for entry in entries] - -def verify_samba_component_presence(ldap, api): - """ - Verifies that Samba is installed and configured on this particular master. - If Samba is not available, provide a heplful hint with the list of masters - capable of running the commands. - """ - - adtrust_present = api.Command['adtrust_is_enabled']()['result'] - - hint = _( - ' Alternatively, following servers are capable of running this ' - 'command: %(masters)s' - ) - - def raise_missing_component_error(message): - masters_with_adtrust = find_adtrust_masters(ldap, api) - - # If there are any masters capable of running Samba requiring commands - # let's advertise them directly - if masters_with_adtrust: - message += hint % dict(masters=', '.join(masters_with_adtrust)) - - raise errors.NotFound( - name=_('AD Trust setup'), - reason=message, - ) - - # We're ok in this case, bail out - if adtrust_present and _bindings_installed: - return - - # First check for packages missing - elif not _bindings_installed: - error_message=_( - 'Cannot perform the selected command without Samba 4 support ' - 'installed. Make sure you have installed server-trust-ad ' - 'sub-package of IPA.' - ) - - raise_missing_component_error(error_message) - - # Packages present, but ADTRUST instance is not configured - elif not adtrust_present: - error_message=_( - 'Cannot perform the selected command without Samba 4 instance ' - 'configured on this machine. Make sure you have run ' - 'ipa-adtrust-install on this server.' - ) - - raise_missing_component_error(error_message) - - -def generate_creds(trustinstance, style, **options): - """ - Generate string representing credentials using trust instance - Input: - trustinstance -- ipaserver.dcerpc.TrustInstance object - style -- style of credentials - CRED_STYLE_SAMBA -- for using with Samba bindings - CRED_STYLE_KERBEROS -- for obtaining Kerberos ticket - **options -- options with realm_admin and realm_passwd keys - - Result: - a string representing credentials with first % separating username and password - None is returned if realm_passwd key returns nothing from options - """ - creds = None - password = options.get('realm_passwd', None) - if password: - admin_name = options.get('realm_admin') - sp = [] - sep = '@' - if style == CRED_STYLE_SAMBA: - sep = "\\" - sp = admin_name.split(sep) - if len(sp) == 1: - sp.insert(0, trustinstance.remote_domain.info['name']) - elif style == CRED_STYLE_KERBEROS: - sp = admin_name.split('\\') - if len(sp) > 1: - sp = [sp[1]] - else: - sp = admin_name.split(sep) - if len(sp) == 1: - sp.append(trustinstance.remote_domain.info['dns_forest'].upper()) - creds = u"{name}%{password}".format(name=sep.join(sp), - password=password) - return creds - -def add_range(myapi, trustinstance, range_name, dom_sid, *keys, **options): - """ - First, we try to derive the parameters of the ID range based on the - information contained in the Active Directory. - - If that was not successful, we go for our usual defaults (random base, - range size 200 000, ipa-ad-trust range type). - - Any of these can be overridden by passing appropriate CLI options - to the trust-add command. - """ - - range_size = None - range_type = None - base_id = None - - # First, get information about ID space from AD - # However, we skip this step if other than ipa-ad-trust-posix - # range type is enforced - - if options.get('range_type', None) in (None, u'ipa-ad-trust-posix'): - - # Get the base dn - domain = keys[-1] - basedn = realm_to_suffix(domain) - - # Search for information contained in - # CN=ypservers,CN=ypServ30,CN=RpcServices,CN=System - info_filter = '(objectClass=msSFU30DomainInfo)' - info_dn = DN('CN=ypservers,CN=ypServ30,CN=RpcServices,CN=System')\ - + basedn - - # Get the domain validator - domain_validator = ipaserver.dcerpc.DomainValidator(myapi) - if not domain_validator.is_configured(): - raise errors.NotFound( - reason=_('Cannot search in trusted domains without own ' - 'domain configured. Make sure you have run ' - 'ipa-adtrust-install on the IPA server first')) - - creds = None - if trustinstance: - # Re-use AD administrator credentials if they were provided - creds = generate_creds(trustinstance, style=CRED_STYLE_KERBEROS, **options) - if creds: - domain_validator._admin_creds = creds - # KDC might not get refreshed data at the first time, - # retry several times - for retry in range(10): - info_list = domain_validator.search_in_dc(domain, - info_filter, - None, - SCOPE_SUBTREE, - basedn=info_dn, - quiet=True) - - if info_list: - info = info_list[0] - break - else: - sleep(2) - - required_msSFU_attrs = ['msSFU30MaxUidNumber', 'msSFU30OrderNumber'] - - if not info_list: - # We were unable to gain UNIX specific info from the AD - root_logger.debug("Unable to gain POSIX info from the AD") - else: - if all(attr in info for attr in required_msSFU_attrs): - root_logger.debug("Able to gain POSIX info from the AD") - range_type = u'ipa-ad-trust-posix' - - max_uid = info.get('msSFU30MaxUidNumber') - max_gid = info.get('msSFU30MaxGidNumber', None) - max_id = int(max(max_uid, max_gid)[0]) - - base_id = int(info.get('msSFU30OrderNumber')[0]) - range_size = (1 + (max_id - base_id) // DEFAULT_RANGE_SIZE)\ - * DEFAULT_RANGE_SIZE - - # Second, options given via the CLI options take precedence to discovery - if options.get('range_type', None): - range_type = options.get('range_type', None) - elif not range_type: - range_type = u'ipa-ad-trust' - - if options.get('range_size', None): - range_size = options.get('range_size', None) - elif not range_size: - range_size = DEFAULT_RANGE_SIZE - - if options.get('base_id', None): - base_id = options.get('base_id', None) - elif not base_id: - # Generate random base_id if not discovered nor given via CLI - base_id = DEFAULT_RANGE_SIZE + ( - pysss_murmur.murmurhash3( - dom_sid, - len(dom_sid), 0xdeadbeef - ) % 10000 - ) * DEFAULT_RANGE_SIZE - - # Finally, add new ID range - myapi.Command['idrange_add'](range_name, - ipabaseid=base_id, - ipaidrangesize=range_size, - ipabaserid=0, - iparangetype=range_type, - ipanttrusteddomainsid=dom_sid) - - # Return the values that were generated inside this function - return range_type, range_size, base_id - -def fetch_trusted_domains_over_dbus(myapi, log, forest_name): - if not _bindings_installed: - return - # Calling oddjobd-activated service via DBus has some quirks: - # - Oddjobd registers multiple canonical names on the same address - # - python-dbus only follows name owner changes when mainloop is in use - # See https://fedorahosted.org/oddjob/ticket/2 for details - dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) - try: - _ret = 0 - _stdout = '' - _stderr = '' - bus = dbus.SystemBus() - intf = bus.get_object(DBUS_IFACE_TRUST,"/", follow_name_owner_changes=True) - fetch_domains_method = intf.get_dbus_method('fetch_domains', dbus_interface=DBUS_IFACE_TRUST) - (_ret, _stdout, _stderr) = fetch_domains_method(forest_name) - except dbus.DBusException as e: - log.error('Failed to call %(iface)s.fetch_domains helper.' - 'DBus exception is %(exc)s.' % dict(iface=DBUS_IFACE_TRUST, exc=str(e))) - if _ret != 0: - log.error('Helper was called for forest %(forest)s, return code is %(ret)d' % dict(forest=forest_name, ret=_ret)) - log.error('Standard output from the helper:\n%s---\n' % (_stdout)) - log.error('Error output from the helper:\n%s--\n' % (_stderr)) - raise errors.ServerCommandError(server=myapi.env.host, - error=_('Fetching domains from trusted forest failed. ' - 'See details in the error_log')) - return - -@register() -class trust(LDAPObject): - """ - Trust object. - """ - trust_types = ('ad', 'ipa') - container_dn = api.env.container_trusts - object_name = _('trust') - object_name_plural = _('trusts') - object_class = ['ipaNTTrustedDomain'] - default_attributes = ['cn', 'ipantflatname', 'ipanttrusteddomainsid', - 'ipanttrusttype', 'ipanttrustattributes', 'ipanttrustdirection', - 'ipanttrustpartner', 'ipanttrustforesttrustinfo', - 'ipanttrustposixoffset', 'ipantsupportedencryptiontypes' ] - search_display_attributes = ['cn', 'ipantflatname', - 'ipanttrusteddomainsid', 'ipanttrusttype'] - managed_permissions = { - 'System: Read Trust Information': { - # Allow reading of attributes needed for SSSD subdomains support - 'non_object': True, - 'ipapermlocation': DN(container_dn, api.env.basedn), - 'replaces_global_anonymous_aci': True, - 'ipapermbindruletype': 'all', - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'cn', 'objectclass', - 'ipantflatname', 'ipantsecurityidentifier', - 'ipanttrusteddomainsid', 'ipanttrustpartner', - 'ipantsidblacklistincoming', 'ipantsidblacklistoutgoing', - 'ipanttrustdirection' - }, - }, - - 'System: Read system trust accounts': { - 'non_object': True, - 'ipapermlocation': DN(container_dn, api.env.basedn), - 'replaces_global_anonymous_aci': True, - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'uidnumber', 'gidnumber', 'krbprincipalname' - }, - 'default_privileges': {'ADTrust Agents'}, - }, - } - - label = _('Trusts') - label_singular = _('Trust') - - takes_params = ( - Str('cn', - cli_name='realm', - label=_('Realm name'), - primary_key=True, - ), - Str('ipantflatname', - cli_name='flat_name', - label=_('Domain NetBIOS name'), - flags=['no_create', 'no_update']), - Str('ipanttrusteddomainsid', - cli_name='sid', - label=_('Domain Security Identifier'), - flags=['no_create', 'no_update']), - Str('ipantsidblacklistincoming*', - cli_name='sid_blacklist_incoming', - label=_('SID blacklist incoming'), - flags=['no_create']), - Str('ipantsidblacklistoutgoing*', - cli_name='sid_blacklist_outgoing', - label=_('SID blacklist outgoing'), - flags=['no_create']), - ) - - def validate_sid_blacklists(self, entry_attrs): - if not _bindings_installed: - # SID validator is not available, return - # Even if invalid SID gets in the trust entry, it won't crash - # the validation process as it is translated to SID S-0-0 - return - for attr in ('ipantsidblacklistincoming', 'ipantsidblacklistoutgoing'): - values = entry_attrs.get(attr) - if not values: - continue - for value in values: - if not ipaserver.dcerpc.is_sid_valid(value): - raise errors.ValidationError(name=attr, - error=_("invalid SID: %(value)s") % dict(value=value)) - - def get_dn(self, *keys, **kwargs): - trust_type = kwargs.get('trust_type') - - sdn = [('cn', x) for x in keys] - sdn.reverse() - - if trust_type is None: - ldap = self.backend - trustfilter = ldap.make_filter({ - 'objectclass': ['ipaNTTrustedDomain'], - 'cn': [keys[-1]]}, - rules=ldap.MATCH_ALL - ) - - # more type of objects can be located in subtree (for example - # cross-realm principals). we need this attr do detect trust - # entries - trustfilter = ldap.combine_filters( - (trustfilter, "ipaNTTrustPartner=*"), - rules=ldap.MATCH_ALL - ) - - try: - result = ldap.get_entries( - DN(self.container_dn, self.env.basedn), - ldap.SCOPE_SUBTREE, trustfilter, [''] - ) - except errors.NotFound: - self.handle_not_found(keys[-1]) - - if len(result) > 1: - raise errors.OnlyOneValueAllowed(attr='trust domain') - - return result[0].dn - - return make_trust_dn(self.env, trust_type, DN(*sdn)) - - def warning_if_ad_trust_dom_have_missing_SID(self, result, **options): - """Due bug https://fedorahosted.org/freeipa/ticket/5665 there might be - AD trust domain without generated SID, warn user about it. - """ - ldap = self.api.Backend.ldap2 - - try: - entries, truncated = ldap.find_entries( - base_dn=DN(self.api.env.container_adtrusts, - self.api.env.basedn), - scope=ldap.SCOPE_ONELEVEL, - attrs_list=['cn'], - filter='(&(ipaNTTrustPartner=*)' - '(!(ipaNTSecurityIdentifier=*)))', - ) - except errors.NotFound: - pass - else: - for entry in entries: - add_message( - options['version'], - result, - BrokenTrust(domain=entry.single_value['cn']) - ) - - -@register() -class trust_add(LDAPCreate): - __doc__ = _(''' -Add new trust to use. - -This command establishes trust relationship to another domain -which becomes 'trusted'. As result, users of the trusted domain -may access resources of this domain. - -Only trusts to Active Directory domains are supported right now. - -The command can be safely run multiple times against the same domain, -this will cause change to trust relationship credentials on both -sides. - ''') - - range_types = { - u'ipa-ad-trust': unicode(_('Active Directory domain range')), - u'ipa-ad-trust-posix': unicode(_('Active Directory trust range with ' - 'POSIX attributes')), - } - - takes_options = LDAPCreate.takes_options + ( - _trust_type_option, - Str('realm_admin?', - cli_name='admin', - label=_("Active Directory domain administrator"), - ), - Password('realm_passwd?', - cli_name='password', - label=_("Active Directory domain administrator's password"), - confirm=False, - ), - Str('realm_server?', - cli_name='server', - label=_('Domain controller for the Active Directory domain (optional)'), - ), - Password('trust_secret?', - cli_name='trust_secret', - label=_('Shared secret for the trust'), - confirm=False, - ), - Int('base_id?', - cli_name='base_id', - label=_('First Posix ID of the range reserved for the trusted domain'), - ), - Int('range_size?', - cli_name='range_size', - label=_('Size of the ID range reserved for the trusted domain'), - ), - StrEnum('range_type?', - label=_('Range type'), - cli_name='range_type', - doc=(_('Type of trusted domain ID range, one of {vals}' - .format(vals=', '.join(range_types.keys())))), - values=tuple(range_types.keys()), - ), - Bool('bidirectional?', - label=_('Two-way trust'), - cli_name='two_way', - doc=(_('Establish bi-directional trust. By default trust is inbound one-way only.')), - default=False, - ), - ) - - msg_summary = _('Added Active Directory trust for realm "%(value)s"') - msg_summary_existing = _('Re-established trust to domain "%(value)s"') - has_output_params = LDAPCreate.has_output_params + trust_output_params - - def execute(self, *keys, **options): - ldap = self.obj.backend - - verify_samba_component_presence(ldap, self.api) - - full_join = self.validate_options(*keys, **options) - old_range, range_name, dom_sid = self.validate_range(*keys, **options) - result = self.execute_ad(full_join, *keys, **options) - - if not old_range: - # Store the created range type, since for POSIX trusts no - # ranges for the subdomains should be added, POSIX attributes - # provide a global mapping across all subdomains - (created_range_type, _, _) = add_range(self.api, self.trustinstance, - range_name, dom_sid, - *keys, **options) - else: - created_range_type = old_range['result']['iparangetype'][0] - - trust_filter = "cn=%s" % result['value'] - (trusts, truncated) = ldap.find_entries( - base_dn=DN(self.api.env.container_trusts, self.api.env.basedn), - filter=trust_filter) - - result['result'] = entry_to_dict(trusts[0], **options) - - # Fetch topology of the trust forest -- we need always to do it - # for AD trusts, regardless of the type of idranges associated with it - # Note that add_new_domains_from_trust will add needed ranges for - # the algorithmic ID mapping case. - if (options.get('trust_type') == u'ad' and - options.get('trust_secret') is None): - if options.get('bidirectional') == True: - # Bidirectional trust allows us to use cross-realm TGT, so we can - # run the call under original user's credentials - res = fetch_domains_from_trust(self.api, self.trustinstance, - result['result'], **options) - domains = add_new_domains_from_trust(self.api, self.trustinstance, - result['result'], res, **options) - else: - # One-way trust is more complex. We don't have cross-realm TGT - # and cannot use IPA principals to authenticate against AD. - # Instead, we have to use our trusted domain object's (TDO) - # account in AD. Access to the credentials is limited and IPA - # framework cannot access it directly. Instead, we call out to - # oddjobd-activated higher privilege process that will use TDO - # object credentials to authenticate to AD with Kerberos, - # run DCE RPC calls to do discovery and will call - # add_new_domains_from_trust() on its own. - fetch_trusted_domains_over_dbus(self.api, self.log, result['value']) - - # Format the output into human-readable values - result['result']['trusttype'] = [trust_type_string( - result['result']['ipanttrusttype'][0])] - result['result']['trustdirection'] = [trust_direction_string( - result['result']['ipanttrustdirection'][0])] - result['result']['truststatus'] = [trust_status_string( - result['verified'])] - - del result['verified'] - result['result'].pop('ipanttrustauthoutgoing', None) - result['result'].pop('ipanttrustauthincoming', None) - - return result - - def validate_options(self, *keys, **options): - trusted_realm_domain = keys[-1] - - if not _murmur_installed and 'base_id' not in options: - raise errors.ValidationError( - name=_('missing base_id'), - error=_( - 'pysss_murmur is not available on the server ' - 'and no base-id is given.' - ) - ) - - if 'trust_type' not in options: - raise errors.RequirementError(name='trust_type') - - if options['trust_type'] != u'ad': - raise errors.ValidationError( - name=_('trust type'), - error=_('only "ad" is supported') - ) - - # Detect IPA-AD domain clash - if self.api.env.domain.lower() == trusted_realm_domain.lower(): - raise errors.ValidationError( - name=_('domain'), - error=_('Cannot establish a trust to AD deployed in the same ' - 'domain as IPA. Such setup is not supported.') - ) - - # If domain name and realm does not match, IPA server is not be able - # to establish trust with Active Directory. - - realm_not_matching_domain = (self.api.env.domain.upper() != self.api.env.realm) - - if options['trust_type'] == u'ad' and realm_not_matching_domain: - raise errors.ValidationError( - name=_('Realm-domain mismatch'), - error=_('To establish trust with Active Directory, the ' - 'domain name and the realm name of the IPA server ' - 'must match') - ) - - self.trustinstance = ipaserver.dcerpc.TrustDomainJoins(self.api) - if not self.trustinstance.configured: - raise errors.NotFound( - name=_('AD Trust setup'), - reason=_( - 'Cannot perform join operation without own domain ' - 'configured. Make sure you have run ipa-adtrust-install ' - 'on the IPA server first' - ) - ) - - # Obtain a list of IPA realm domains - result = self.api.Command.realmdomains_show()['result'] - realm_domains = result['associateddomain'] - - # Do not allow the AD's trusted realm domain in the list - # of our realm domains - if trusted_realm_domain.lower() in realm_domains: - raise errors.ValidationError( - name=_('AD Trust setup'), - error=_( - 'Trusted domain %(domain)s is included among ' - 'IPA realm domains. It needs to be removed ' - 'prior to establishing the trust. See the ' - '"ipa realmdomains-mod --del-domain" command.' - ) % dict(domain=trusted_realm_domain) - ) - - self.realm_server = options.get('realm_server') - self.realm_admin = options.get('realm_admin') - self.realm_passwd = options.get('realm_passwd') - - if self.realm_admin: - names = self.realm_admin.split('@') - - if len(names) > 1: - # realm admin name is in UPN format, user@realm, check that - # realm is the same as the one that we are attempting to trust - if trusted_realm_domain.lower() != names[-1].lower(): - raise errors.ValidationError( - name=_('AD Trust setup'), - error=_( - 'Trusted domain and administrator account use ' - 'different realms' - ) - ) - self.realm_admin = names[0] - - if not self.realm_passwd: - raise errors.ValidationError( - name=_('AD Trust setup'), - error=_('Realm administrator password should be specified') - ) - return True - - return False - - def validate_range(self, *keys, **options): - # If a range for this trusted domain already exists, - # '--base-id' or '--range-size' options should not be specified - range_name = keys[-1].upper() + '_id_range' - range_type = options.get('range_type') - - try: - old_range = self.api.Command['idrange_show'](range_name, raw=True) - except errors.NotFound: - old_range = None - - if options.get('trust_type') == u'ad': - if range_type and range_type not in (u'ipa-ad-trust', - u'ipa-ad-trust-posix'): - raise errors.ValidationError( - name=_('id range type'), - error=_( - 'Only the ipa-ad-trust and ipa-ad-trust-posix are ' - 'allowed values for --range-type when adding an AD ' - 'trust.' - )) - - base_id = options.get('base_id') - range_size = options.get('range_size') - - if old_range and (base_id or range_size): - raise errors.ValidationError( - name=_('id range'), - error=_( - 'An id range already exists for this trust. ' - 'You should either delete the old range, or ' - 'exclude --base-id/--range-size options from the command.' - ) - ) - - # If a range for this trusted domain already exists, - # domain SID must also match - self.trustinstance.populate_remote_domain( - keys[-1], - self.realm_server, - self.realm_admin, - self.realm_passwd - ) - dom_sid = self.trustinstance.remote_domain.info['sid'] - - if old_range: - old_dom_sid = old_range['result']['ipanttrusteddomainsid'][0] - old_range_type = old_range['result']['iparangetype'][0] - - if old_dom_sid != dom_sid: - raise errors.ValidationError( - name=_('range exists'), - error=_( - 'ID range with the same name but different domain SID ' - 'already exists. The ID range for the new trusted ' - 'domain must be created manually.' - ) - ) - - if range_type and range_type != old_range_type: - raise errors.ValidationError(name=_('range type change'), - error=_('ID range for the trusted domain already exists, ' - 'but it has a different type. Please remove the ' - 'old range manually, or do not enforce type ' - 'via --range-type option.')) - - return old_range, range_name, dom_sid - - def execute_ad(self, full_join, *keys, **options): - # Join domain using full credentials and with random trustdom - # secret (will be generated by the join method) - - # First see if the trust is already in place - # Force retrieval of the trust object by not passing trust_type - try: - dn = self.obj.get_dn(keys[-1]) - except errors.NotFound: - dn = None - - trust_type = TRUST_ONEWAY - if options.get('bidirectional', False): - trust_type = TRUST_BIDIRECTIONAL - # 1. Full access to the remote domain. Use admin credentials and - # generate random trustdom password to do work on both sides - if full_join: - try: - result = self.trustinstance.join_ad_full_credentials( - keys[-1], - self.realm_server, - self.realm_admin, - self.realm_passwd, - trust_type - ) - except errors.NotFound: - error_message=_("Unable to resolve domain controller for '%s' domain. ") % (keys[-1]) - instructions=[] - if dns_container_exists(self.obj.backend): - try: - dns_zone = self.api.Command.dnszone_show(keys[-1])['result'] - if ('idnsforwardpolicy' in dns_zone) and dns_zone['idnsforwardpolicy'][0] == u'only': - instructions.append(_("Forward policy is defined for it in IPA DNS, " - "perhaps forwarder points to incorrect host?")) - except (errors.NotFound, KeyError) as e: - instructions.append(_("IPA manages DNS, please verify " - "your DNS configuration and " - "make sure that service records " - "of the '%(domain)s' domain can " - "be resolved. Examples how to " - "configure DNS with CLI commands " - "or the Web UI can be found in " - "the documentation. " ) % - dict(domain=keys[-1])) - else: - instructions.append(_("Since IPA does not manage DNS records, ensure DNS " - "is configured to resolve '%(domain)s' domain from " - "IPA hosts and back.") % dict(domain=keys[-1])) - raise errors.NotFound(reason=error_message, instructions=instructions) - - if result is None: - raise errors.ValidationError(name=_('AD Trust setup'), - error=_('Unable to verify write permissions to the AD')) - - ret = dict( - value=pkey_to_value( - self.trustinstance.remote_domain.info['dns_domain'], - options), - verified=result['verified'] - ) - if dn: - ret['summary'] = self.msg_summary_existing % ret - return ret - - # 2. We don't have access to the remote domain and trustdom password - # is provided. Do the work on our side and inform what to do on remote - # side. - if options.get('trust_secret'): - result = self.trustinstance.join_ad_ipa_half( - keys[-1], - self.realm_server, - options['trust_secret'], - trust_type - ) - ret = dict( - value=pkey_to_value( - self.trustinstance.remote_domain.info['dns_domain'], - options), - verified=result['verified'] - ) - if dn: - ret['summary'] = self.msg_summary_existing % ret - return ret - else: - raise errors.ValidationError( - name=_('AD Trust setup'), - error=_('Not enough arguments specified to perform trust ' - 'setup')) - -@register() -class trust_del(LDAPDelete): - __doc__ = _('Delete a trust.') - - msg_summary = _('Deleted trust "%(value)s"') - -@register() -class trust_mod(LDAPUpdate): - __doc__ = _(""" - Modify a trust (for future use). - - Currently only the default option to modify the LDAP attributes is - available. More specific options will be added in coming releases. - """) - - msg_summary = _('Modified trust "%(value)s" ' - '(change will be effective in 60 seconds)') - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - assert isinstance(dn, DN) - - self.obj.validate_sid_blacklists(entry_attrs) - - return dn - -@register() -class trust_find(LDAPSearch): - __doc__ = _('Search for trusts.') - has_output_params = LDAPSearch.has_output_params + trust_output_params +\ - (Str('ipanttrusttype'),) - - msg_summary = ngettext( - '%(count)d trust matched', '%(count)d trusts matched', 0 - ) - - # Since all trusts types are stored within separate containers under 'cn=trusts', - # search needs to be done on a sub-tree scope - def pre_callback(self, ldap, filters, attrs_list, base_dn, scope, *args, **options): - # list only trust, not trust domains - trust_filter = '(ipaNTTrustPartner=*)' - filter = ldap.combine_filters((filters, trust_filter), rules=ldap.MATCH_ALL) - return (filter, base_dn, ldap.SCOPE_SUBTREE) - - def execute(self, *args, **options): - result = super(trust_find, self).execute(*args, **options) - - self.obj.warning_if_ad_trust_dom_have_missing_SID(result, **options) - - return result - - def post_callback(self, ldap, entries, truncated, *args, **options): - if options.get('pkey_only', False): - return truncated - - for attrs in entries: - # Translate ipanttrusttype to trusttype if --raw not used - trust_type = attrs.get('ipanttrusttype', [None])[0] - if not options.get('raw', False) and trust_type is not None: - attrs['trusttype'] = trust_type_string(attrs['ipanttrusttype'][0]) - del attrs['ipanttrusttype'] - - return truncated - -@register() -class trust_show(LDAPRetrieve): - __doc__ = _('Display information about a trust.') - has_output_params = LDAPRetrieve.has_output_params + trust_output_params +\ - (Str('ipanttrusttype'), Str('ipanttrustdirection')) - - def execute(self, *keys, **options): - result = super(trust_show, self).execute(*keys, **options) - - self.obj.warning_if_ad_trust_dom_have_missing_SID(result, **options) - - return result - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - - assert isinstance(dn, DN) - # Translate ipanttrusttype to trusttype - # and ipanttrustdirection to trustdirection - # if --raw not used - - if not options.get('raw', False): - trust_type = entry_attrs.get('ipanttrusttype', [None])[0] - if trust_type is not None: - entry_attrs['trusttype'] = trust_type_string(trust_type) - del entry_attrs['ipanttrusttype'] - - dir_str = entry_attrs.get('ipanttrustdirection', [None])[0] - if dir_str is not None: - entry_attrs['trustdirection'] = [trust_direction_string(dir_str)] - del entry_attrs['ipanttrustdirection'] - - return dn - - -_trustconfig_dn = { - u'ad': DN(('cn', api.env.domain), api.env.container_cifsdomains, api.env.basedn), -} - - -@register() -class trustconfig(LDAPObject): - """ - Trusts global configuration object - """ - object_name = _('trust configuration') - default_attributes = [ - 'cn', 'ipantsecurityidentifier', 'ipantflatname', 'ipantdomainguid', - 'ipantfallbackprimarygroup', - ] - - label = _('Global Trust Configuration') - label_singular = _('Global Trust Configuration') - - takes_params = ( - Str('cn', - label=_('Domain'), - flags=['no_update'], - ), - Str('ipantsecurityidentifier', - label=_('Security Identifier'), - flags=['no_update'], - ), - Str('ipantflatname', - label=_('NetBIOS name'), - flags=['no_update'], - ), - Str('ipantdomainguid', - label=_('Domain GUID'), - flags=['no_update'], - ), - Str('ipantfallbackprimarygroup', - cli_name='fallback_primary_group', - label=_('Fallback primary group'), - ), - ) - - def get_dn(self, *keys, **kwargs): - trust_type = kwargs.get('trust_type') - if trust_type is None: - raise errors.RequirementError(name='trust_type') - try: - return _trustconfig_dn[kwargs['trust_type']] - except KeyError: - raise errors.ValidationError(name='trust_type', - error=_("unsupported trust type")) - - def _normalize_groupdn(self, entry_attrs): - """ - Checks that group with given name/DN exists and updates the entry_attrs - """ - if 'ipantfallbackprimarygroup' not in entry_attrs: - return - - group = entry_attrs['ipantfallbackprimarygroup'] - if isinstance(group, (list, tuple)): - group = group[0] - - if group is None: - return - - try: - dn = DN(group) - # group is in a form of a DN - try: - self.backend.get_entry(dn) - except errors.NotFound: - self.api.Object['group'].handle_not_found(group) - # DN is valid, we can just return - return - except ValueError: - # The search is performed for groups with "posixgroup" objectclass - # and not "ipausergroup" so that it can also match groups like - # "Default SMB Group" which does not have this objectclass. - try: - group_entry = self.backend.find_entry_by_attr( - self.api.Object['group'].primary_key.name, - group, - ['posixgroup'], - [''], - DN(self.api.env.container_group, self.api.env.basedn)) - except errors.NotFound: - self.api.Object['group'].handle_not_found(group) - else: - entry_attrs['ipantfallbackprimarygroup'] = [group_entry.dn] - - def _convert_groupdn(self, entry_attrs, options): - """ - Convert an group dn into a name. As we use CN as user RDN, its value - can be extracted from the DN without further LDAP queries. - """ - if options.get('raw', False): - return - - try: - groupdn = entry_attrs['ipantfallbackprimarygroup'][0] - except (IndexError, KeyError): - groupdn = None - - if groupdn is None: - return - assert isinstance(groupdn, DN) - - entry_attrs['ipantfallbackprimarygroup'] = [groupdn[0][0].value] - - -@register() -class trustconfig_mod(LDAPUpdate): - __doc__ = _('Modify global trust configuration.') - - takes_options = LDAPUpdate.takes_options + (_trust_type_option,) - msg_summary = _('Modified "%(value)s" trust configuration') - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - self.obj._normalize_groupdn(entry_attrs) - return dn - - def execute(self, *keys, **options): - result = super(trustconfig_mod, self).execute(*keys, **options) - result['value'] = pkey_to_value(options['trust_type'], options) - return result - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - self.obj._convert_groupdn(entry_attrs, options) - return dn - - - -@register() -class trustconfig_show(LDAPRetrieve): - __doc__ = _('Show global trust configuration.') - - takes_options = LDAPRetrieve.takes_options + (_trust_type_option,) - - def execute(self, *keys, **options): - result = super(trustconfig_show, self).execute(*keys, **options) - result['value'] = pkey_to_value(options['trust_type'], options) - return result - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - self.obj._convert_groupdn(entry_attrs, options) - return dn - - -if _nss_idmap_installed: - _idmap_type_dict = { - pysss_nss_idmap.ID_USER : 'user', - pysss_nss_idmap.ID_GROUP : 'group', - pysss_nss_idmap.ID_BOTH : 'both', - } - def idmap_type_string(level): - string = _idmap_type_dict.get(int(level), 'unknown') - return unicode(string) - -@register() -class trust_resolve(Command): - NO_CLI = True - __doc__ = _('Resolve security identifiers of users and groups in trusted domains') - - takes_options = ( - Str('sids+', - label = _('Security Identifiers (SIDs)'), - ), - ) - - has_output_params = ( - Str('name', label= _('Name')), - Str('sid', label= _('SID')), - ) - - has_output = ( - output.ListOfEntries('result'), - ) - - def execute(self, *keys, **options): - result = list() - if not _nss_idmap_installed: - return dict(result=result) - try: - sids = [str(x) for x in options['sids']] - xlate = pysss_nss_idmap.getnamebysid(sids) - for sid in xlate: - entry = dict() - entry['sid'] = [unicode(sid)] - entry['name'] = [unicode(xlate[sid][pysss_nss_idmap.NAME_KEY])] - entry['type'] = [idmap_type_string(xlate[sid][pysss_nss_idmap.TYPE_KEY])] - result.append(entry) - except ValueError as e: - pass - - return dict(result=result) - - - -@register() -class adtrust_is_enabled(Command): - NO_CLI = True - - __doc__ = _('Determine whether ipa-adtrust-install has been run on this ' - 'system') - - def execute(self, *keys, **options): - ldap = self.api.Backend.ldap2 - adtrust_dn = DN( - ('cn', 'ADTRUST'), - ('cn', self.api.env.host), - ('cn', 'masters'), - ('cn', 'ipa'), - ('cn', 'etc'), - self.api.env.basedn - ) - - try: - ldap.get_entry(adtrust_dn) - except errors.NotFound: - return dict(result=False) - - return dict(result=True) - - - -@register() -class compat_is_enabled(Command): - NO_CLI = True - - __doc__ = _('Determine whether Schema Compatibility plugin is configured ' - 'to serve trusted domain users and groups') - - def execute(self, *keys, **options): - ldap = self.api.Backend.ldap2 - users_dn = DN( - ('cn', 'users'), - ('cn', 'Schema Compatibility'), - ('cn', 'plugins'), - ('cn', 'config') - ) - groups_dn = DN( - ('cn', 'groups'), - ('cn', 'Schema Compatibility'), - ('cn', 'plugins'), - ('cn', 'config') - ) - - try: - users_entry = ldap.get_entry(users_dn) - except errors.NotFound: - return dict(result=False) - - attr = users_entry.get('schema-compat-lookup-nsswitch') - if not attr or 'user' not in attr: - return dict(result=False) - - try: - groups_entry = ldap.get_entry(groups_dn) - except errors.NotFound: - return dict(result=False) - - attr = groups_entry.get('schema-compat-lookup-nsswitch') - if not attr or 'group' not in attr: - return dict(result=False) - - return dict(result=True) - - - -@register() -class sidgen_was_run(Command): - """ - This command tries to determine whether the sidgen task was run during - ipa-adtrust-install. It does that by simply checking the "editors" group - for the presence of the ipaNTSecurityIdentifier attribute - if the - attribute is present, the sidgen task was run. - - Since this command relies on the existence of the "editors" group, it will - fail loudly in case this group does not exist. - """ - NO_CLI = True - - __doc__ = _('Determine whether ipa-adtrust-install has been run with ' - 'sidgen task') - - def execute(self, *keys, **options): - ldap = self.api.Backend.ldap2 - editors_dn = DN( - ('cn', 'editors'), - ('cn', 'groups'), - ('cn', 'accounts'), - api.env.basedn - ) - - try: - editors_entry = ldap.get_entry(editors_dn) - except errors.NotFound: - raise errors.NotFound( - name=_('sidgen_was_run'), - reason=_( - 'This command relies on the existence of the "editors" ' - 'group, but this group was not found.' - ) - ) - - attr = editors_entry.get('ipaNTSecurityIdentifier') - if not attr: - return dict(result=False) - - return dict(result=True) - - -@register() -class trustdomain(LDAPObject): - """ - Object representing a domain of the AD trust. - """ - parent_object = 'trust' - trust_type_idx = {'2':u'ad'} - object_name = _('trust domain') - object_name_plural = _('trust domains') - object_class = ['ipaNTTrustedDomain'] - default_attributes = ['cn', 'ipantflatname', 'ipanttrusteddomainsid', 'ipanttrustpartner'] - search_display_attributes = ['cn', 'ipantflatname', 'ipanttrusteddomainsid', ] - - label = _('Trusted domains') - label_singular = _('Trusted domain') - - takes_params = ( - Str('cn', - label=_('Domain name'), - cli_name='domain', - primary_key=True - ), - Str('ipantflatname?', - cli_name='flat_name', - label=_('Domain NetBIOS name'), - ), - Str('ipanttrusteddomainsid?', - cli_name='sid', - label=_('Domain Security Identifier'), - ), - Str('ipanttrustpartner?', - label=_('Trusted domain partner'), - flags=['no_display', 'no_option'], - ), - ) - - # LDAPObject.get_dn() only passes all but last element of keys and no kwargs - # to the parent object's get_dn() no matter what you pass to it. Make own get_dn() - # as we really need all elements to construct proper dn. - def get_dn(self, *keys, **kwargs): - sdn = [('cn', x) for x in keys] - sdn.reverse() - trust_type = kwargs.get('trust_type') - if not trust_type: - trust_type=u'ad' - - dn=make_trust_dn(self.env, trust_type, DN(*sdn)) - return dn - -@register() -class trustdomain_find(LDAPSearch): - __doc__ = _('Search domains of the trust') - - has_output_params = LDAPSearch.has_output_params + ( - Flag('domain_enabled', label= _('Domain enabled')), - ) - def pre_callback(self, ldap, filters, attrs_list, base_dn, scope, *args, **options): - return (filters, base_dn, ldap.SCOPE_SUBTREE) - - def post_callback(self, ldap, entries, truncated, *args, **options): - if options.get('pkey_only', False): - return truncated - trust_dn = self.obj.get_dn(args[0], trust_type=u'ad') - trust_entry = ldap.get_entry(trust_dn) - for entry in entries: - sid = entry['ipanttrusteddomainsid'][0] - - blacklist = trust_entry.get('ipantsidblacklistincoming') - if blacklist is None: - continue - - if sid in blacklist: - entry['domain_enabled'] = [False] - else: - entry['domain_enabled'] = [True] - return truncated - - - -@register() -class trustdomain_mod(LDAPUpdate): - __doc__ = _('Modify trustdomain of the trust') - - NO_CLI = True - takes_options = LDAPUpdate.takes_options + (_trust_type_option,) - -@register() -class trustdomain_add(LDAPCreate): - __doc__ = _('Allow access from the trusted domain') - NO_CLI = True - - takes_options = LDAPCreate.takes_options + (_trust_type_option,) - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - if 'ipanttrustpartner' in options: - entry_attrs['ipanttrustpartner'] = [options['ipanttrustpartner']] - return dn - -@register() -class trustdomain_del(LDAPDelete): - __doc__ = _('Remove infromation about the domain associated with the trust.') - - msg_summary = _('Removed information about the trusted domain "%(value)s"') - - def execute(self, *keys, **options): - ldap = self.api.Backend.ldap2 - verify_samba_component_presence(ldap, self.api) - - # Note that pre-/post- callback handling for LDAPDelete is causing pre_callback - # to always receive empty keys. We need to catch the case when root domain is being deleted - - for domain in keys[1]: - # Fetch the trust to verify that the entered domain is trusted - self.api.Command.trust_show(domain) - - if keys[0].lower() == domain: - raise errors.ValidationError(name='domain', - error=_("cannot delete root domain of the trust, " - "use trust-del to delete the trust itself")) - try: - res = self.api.Command.trustdomain_enable(keys[0], domain) - except errors.AlreadyActive: - pass - - result = super(trustdomain_del, self).execute(*keys, **options) - result['value'] = pkey_to_value(keys[1], options) - return result - - -def fetch_domains_from_trust(myapi, trustinstance, trust_entry, **options): - trust_name = trust_entry['cn'][0] - # We want to use Kerberos if we have admin credentials even with SMB calls - # as eventually use of NTLMSSP will be deprecated for trusted domain operations - # If admin credentials are missing, 'creds' will be None and fetch_domains - # will use HTTP/ipa.master@IPA.REALM principal, e.g. Kerberos authentication - # as well. - creds = generate_creds(trustinstance, style=CRED_STYLE_KERBEROS, **options) - server = options.get('realm_server', None) - domains = ipaserver.dcerpc.fetch_domains(myapi, - trustinstance.local_flatname, - trust_name, creds=creds, server=server) - return domains - -def add_new_domains_from_trust(myapi, trustinstance, trust_entry, domains, **options): - result = [] - if not domains: - return result - - trust_name = trust_entry['cn'][0] - # trust range must exist by the time add_new_domains_from_trust is called - range_name = trust_name.upper() + '_id_range' - old_range = myapi.Command.idrange_show(range_name, raw=True)['result'] - idrange_type = old_range['iparangetype'][0] - - for dom in domains: - dom['trust_type'] = u'ad' - try: - name = dom['cn'] - del dom['cn'] - if 'all' in options: - dom['all'] = options['all'] - if 'raw' in options: - dom['raw'] = options['raw'] - - res = myapi.Command.trustdomain_add(trust_name, name, **dom) - result.append(res['result']) - - if idrange_type != u'ipa-ad-trust-posix': - range_name = name.upper() + '_id_range' - dom['range_type'] = u'ipa-ad-trust' - add_range(myapi, trustinstance, range_name, dom['ipanttrusteddomainsid'], - trust_name, name, **dom) - except errors.DuplicateEntry: - # Ignore updating duplicate entries - pass - return result - -@register() -class trust_fetch_domains(LDAPRetrieve): - __doc__ = _('Refresh list of the domains associated with the trust') - - has_output = output.standard_list_of_entries - takes_options = LDAPRetrieve.takes_options + ( - Str('realm_server?', - cli_name='server', - label=_('Domain controller for the Active Directory domain (optional)'), - ), - ) - - def execute(self, *keys, **options): - ldap = self.api.Backend.ldap2 - verify_samba_component_presence(ldap, self.api) - - trust = self.api.Command.trust_show(keys[0], raw=True)['result'] - - result = dict() - result['result'] = [] - result['count'] = 0 - result['truncated'] = False - - # For one-way trust fetch over DBus. we don't get the list in this case. - if int(trust['ipanttrustdirection'][0]) != TRUST_BIDIRECTIONAL: - fetch_trusted_domains_over_dbus(self.api, self.log, keys[0]) - result['summary'] = unicode(_('List of trust domains successfully refreshed. Use trustdomain-find command to list them.')) - return result - - trustinstance = ipaserver.dcerpc.TrustDomainJoins(self.api) - if not trustinstance.configured: - raise errors.NotFound( - name=_('AD Trust setup'), - reason=_( - 'Cannot perform join operation without own domain ' - 'configured. Make sure you have run ipa-adtrust-install ' - 'on the IPA server first' - ) - ) - res = fetch_domains_from_trust(self.api, trustinstance, trust, **options) - domains = add_new_domains_from_trust(self.api, trustinstance, trust, res, **options) - - if len(domains) > 0: - result['summary'] = unicode(_('List of trust domains successfully refreshed')) - else: - result['summary'] = unicode(_('No new trust domains were found')) - - result['result'] = domains - result['count'] = len(domains) - return result - - -@register() -class trustdomain_enable(LDAPQuery): - __doc__ = _('Allow use of IPA resources by the domain of the trust') - - has_output = output.standard_value - msg_summary = _('Enabled trust domain "%(value)s"') - - def execute(self, *keys, **options): - ldap = self.api.Backend.ldap2 - verify_samba_component_presence(ldap, self.api) - - if keys[0].lower() == keys[1].lower(): - raise errors.ValidationError(name='domain', - error=_("Root domain of the trust is always enabled for the existing trust")) - try: - trust_dn = self.obj.get_dn(keys[0], trust_type=u'ad') - trust_entry = ldap.get_entry(trust_dn) - except errors.NotFound: - self.api.Object[self.obj.parent_object].handle_not_found(keys[0]) - - dn = self.obj.get_dn(keys[0], keys[1], trust_type=u'ad') - try: - entry = ldap.get_entry(dn) - sid = entry['ipanttrusteddomainsid'][0] - if sid in trust_entry['ipantsidblacklistincoming']: - trust_entry['ipantsidblacklistincoming'].remove(sid) - ldap.update_entry(trust_entry) - # Force MS-PAC cache re-initialization on KDC side - domval = ipaserver.dcerpc.DomainValidator(self.api) - (ccache_name, principal) = domval.kinit_as_http(keys[0]) - else: - raise errors.AlreadyActive() - except errors.NotFound: - self.obj.handle_not_found(*keys) - - return dict( - result=True, - value=pkey_to_value(keys[1], options), - ) - - -@register() -class trustdomain_disable(LDAPQuery): - __doc__ = _('Disable use of IPA resources by the domain of the trust') - - has_output = output.standard_value - msg_summary = _('Disabled trust domain "%(value)s"') - - def execute(self, *keys, **options): - ldap = self.api.Backend.ldap2 - verify_samba_component_presence(ldap, self.api) - - if keys[0].lower() == keys[1].lower(): - raise errors.ValidationError(name='domain', - error=_("cannot disable root domain of the trust, use trust-del to delete the trust itself")) - try: - trust_dn = self.obj.get_dn(keys[0], trust_type=u'ad') - trust_entry = ldap.get_entry(trust_dn) - except errors.NotFound: - self.api.Object[self.obj.parent_object].handle_not_found(keys[0]) - - dn = self.obj.get_dn(keys[0], keys[1], trust_type=u'ad') - try: - entry = ldap.get_entry(dn) - sid = entry['ipanttrusteddomainsid'][0] - if not (sid in trust_entry['ipantsidblacklistincoming']): - trust_entry['ipantsidblacklistincoming'].append(sid) - ldap.update_entry(trust_entry) - # Force MS-PAC cache re-initialization on KDC side - domval = ipaserver.dcerpc.DomainValidator(self.api) - (ccache_name, principal) = domval.kinit_as_http(keys[0]) - else: - raise errors.AlreadyInactive() - except errors.NotFound: - self.obj.handle_not_found(*keys) - - return dict( - result=True, - value=pkey_to_value(keys[1], options), - ) - diff --git a/ipalib/plugins/user.py b/ipalib/plugins/user.py deleted file mode 100644 index adc59fcba..000000000 --- a/ipalib/plugins/user.py +++ /dev/null @@ -1,1151 +0,0 @@ -# Authors: -# Jason Gerard DeRose <jderose@redhat.com> -# Pavel Zuna <pzuna@redhat.com> -# -# Copyright (C) 2008 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/>. - -import time -from time import gmtime, strftime -import posixpath -import os - -import six - -from ipalib import api -from ipalib import errors -from ipalib import Bool, Flag, Str -from .baseuser import ( - baseuser, - baseuser_add, - baseuser_del, - baseuser_mod, - baseuser_find, - baseuser_show, - NO_UPG_MAGIC, - UPG_DEFINITION_DN, - baseuser_output_params, - status_baseuser_output_params, - baseuser_pwdchars, - validate_nsaccountlock, - convert_nsaccountlock, - fix_addressbook_permission_bindrule, - baseuser_add_manager, - baseuser_remove_manager) -from .idviews import remove_ipaobject_overrides -from ipalib.plugable import Registry -from .baseldap import ( - pkey_to_value, - LDAPCreate, - LDAPSearch, - LDAPQuery, - LDAPMultiQuery, - LDAPAddAttribute, - LDAPRemoveAttribute) -from . import baseldap -from ipalib.request import context -from ipalib import _, ngettext -from ipalib import output -from ipaplatform.paths import paths -from ipapython.dn import DN -from ipapython.ipautil import ipa_generate_password -from ipalib.capabilities import client_has_capability - -if api.env.in_server: - from ipaserver.plugins.ldap2 import ldap2 - -if six.PY3: - unicode = str - -__doc__ = _(""" -Users - -Manage user entries. All users are POSIX users. - -IPA supports a wide range of username formats, but you need to be aware of any -restrictions that may apply to your particular environment. For example, -usernames that start with a digit or usernames that exceed a certain length -may cause problems for some UNIX systems. -Use 'ipa config-mod' to change the username format allowed by IPA tools. - -Disabling a user account prevents that user from obtaining new Kerberos -credentials. It does not invalidate any credentials that have already -been issued. - -Password management is not a part of this module. For more information -about this topic please see: ipa help passwd - -Account lockout on password failure happens per IPA master. The user-status -command can be used to identify which master the user is locked out on. -It is on that master the administrator must unlock the user. - -EXAMPLES: - - Add a new user: - ipa user-add --first=Tim --last=User --password tuser1 - - Find all users whose entries include the string "Tim": - ipa user-find Tim - - Find all users with "Tim" as the first name: - ipa user-find --first=Tim - - Disable a user account: - ipa user-disable tuser1 - - Enable a user account: - ipa user-enable tuser1 - - Delete a user: - ipa user-del tuser1 -""") - -register = Registry() - - -user_output_params = baseuser_output_params - -status_output_params = status_baseuser_output_params - - -def check_protected_member(user, protected_group_name=u'admins'): - ''' - Ensure the last enabled member of a protected group cannot be deleted or - disabled by raising LastMemberError. - ''' - - # Get all users in the protected group - result = api.Command.user_find(in_group=protected_group_name) - - # Build list of users in the protected group who are enabled - result = result['result'] - enabled_users = [entry['uid'][0] for entry in result if not entry['nsaccountlock']] - - # If the user is the last enabled user raise LastMemberError exception - if enabled_users == [user]: - raise errors.LastMemberError(key=user, label=_(u'group'), - container=protected_group_name) - -@register() -class user(baseuser): - """ - User object. - """ - - container_dn = baseuser.active_container_dn - label = _('Users') - label_singular = _('User') - object_name = _('user') - object_name_plural = _('users') - managed_permissions = { - 'System: Read User Standard Attributes': { - 'replaces_global_anonymous_aci': True, - 'ipapermbindruletype': 'anonymous', - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'objectclass', 'cn', 'sn', 'description', 'title', 'uid', - 'displayname', 'givenname', 'initials', 'manager', 'gecos', - 'gidnumber', 'homedirectory', 'loginshell', 'uidnumber', - 'ipantsecurityidentifier' - }, - }, - 'System: Read User Addressbook Attributes': { - 'replaces_global_anonymous_aci': True, - 'ipapermbindruletype': 'all', - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'seealso', 'telephonenumber', - 'facsimiletelephonenumber', 'l', 'ou', 'st', 'postalcode', 'street', - 'destinationindicator', 'internationalisdnnumber', - 'physicaldeliveryofficename', 'postaladdress', 'postofficebox', - 'preferreddeliverymethod', 'registeredaddress', - 'teletexterminalidentifier', 'telexnumber', 'x121address', - 'carlicense', 'departmentnumber', 'employeenumber', - 'employeetype', 'preferredlanguage', 'mail', 'mobile', 'pager', - 'audio', 'businesscategory', 'homephone', 'homepostaladdress', - 'jpegphoto', 'labeleduri', 'o', 'photo', 'roomnumber', - 'secretary', 'usercertificate', - 'usersmimecertificate', 'x500uniqueidentifier', - 'inetuserhttpurl', 'inetuserstatus', - }, - 'fixup_function': fix_addressbook_permission_bindrule, - }, - 'System: Read User IPA Attributes': { - 'replaces_global_anonymous_aci': True, - 'ipapermbindruletype': 'all', - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'ipauniqueid', 'ipasshpubkey', 'ipauserauthtype', 'userclass', - }, - 'fixup_function': fix_addressbook_permission_bindrule, - }, - 'System: Read User Kerberos Attributes': { - 'replaces_global_anonymous_aci': True, - 'ipapermbindruletype': 'all', - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'krbprincipalname', 'krbcanonicalname', 'krbprincipalaliases', - 'krbprincipalexpiration', 'krbpasswordexpiration', - 'krblastpwdchange', 'nsaccountlock', 'krbprincipaltype', - }, - }, - 'System: Read User Kerberos Login Attributes': { - 'replaces_global_anonymous_aci': True, - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'krblastsuccessfulauth', 'krblastfailedauth', - 'krblastpwdchange', 'krblastadminunlock', - 'krbloginfailedcount', 'krbpwdpolicyreference', - 'krbticketpolicyreference', 'krbupenabled', - }, - 'default_privileges': {'User Administrators'}, - }, - 'System: Read User Membership': { - 'replaces_global_anonymous_aci': True, - 'ipapermbindruletype': 'all', - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'memberof', - }, - }, - 'System: Read UPG Definition': { - # Required for adding users - 'replaces_global_anonymous_aci': True, - 'non_object': True, - 'ipapermlocation': UPG_DEFINITION_DN, - 'ipapermtarget': UPG_DEFINITION_DN, - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': {'*'}, - 'default_privileges': {'User Administrators'}, - }, - 'System: Add Users': { - 'ipapermright': {'add'}, - 'replaces': [ - '(target = "ldap:///uid=*,cn=users,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Add Users";allow (add) groupdn = "ldap:///cn=Add Users,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'User Administrators'}, - }, - 'System: Add User to default group': { - 'non_object': True, - 'ipapermright': {'write'}, - 'ipapermlocation': DN(api.env.container_group, api.env.basedn), - 'ipapermtarget': DN('cn=ipausers', api.env.container_group, - api.env.basedn), - 'ipapermdefaultattr': {'member'}, - 'replaces': [ - '(targetattr = "member")(target = "ldap:///cn=ipausers,cn=groups,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Add user to default group";allow (write) groupdn = "ldap:///cn=Add user to default group,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'User Administrators'}, - }, - 'System: Change User password': { - 'ipapermright': {'write'}, - 'ipapermtargetfilter': [ - '(objectclass=posixaccount)', - '(!(memberOf=%s))' % DN('cn=admins', - api.env.container_group, - api.env.basedn), - ], - 'ipapermdefaultattr': { - 'krbprincipalkey', 'passwordhistory', 'sambalmpassword', - 'sambantpassword', 'userpassword' - }, - 'replaces': [ - '(target = "ldap:///uid=*,cn=users,cn=accounts,$SUFFIX")(targetattr = "userpassword || krbprincipalkey || sambalmpassword || sambantpassword || passwordhistory")(version 3.0;acl "permission:Change a user password";allow (write) groupdn = "ldap:///cn=Change a user password,cn=permissions,cn=pbac,$SUFFIX";)', - '(targetfilter = "(!(memberOf=cn=admins,cn=groups,cn=accounts,$SUFFIX))")(target = "ldap:///uid=*,cn=users,cn=accounts,$SUFFIX")(targetattr = "userpassword || krbprincipalkey || sambalmpassword || sambantpassword || passwordhistory")(version 3.0;acl "permission:Change a user password";allow (write) groupdn = "ldap:///cn=Change a user password,cn=permissions,cn=pbac,$SUFFIX";)', - '(targetattr = "userPassword || krbPrincipalKey || sambaLMPassword || sambaNTPassword || passwordHistory")(version 3.0; acl "Windows PassSync service can write passwords"; allow (write) userdn="ldap:///uid=passsync,cn=sysaccounts,cn=etc,$SUFFIX";)', - ], - 'default_privileges': { - 'User Administrators', - 'Modify Users and Reset passwords', - 'PassSync Service', - }, - }, - 'System: Manage User SSH Public Keys': { - 'ipapermright': {'write'}, - 'ipapermdefaultattr': {'ipasshpubkey'}, - 'replaces': [ - '(targetattr = "ipasshpubkey")(target = "ldap:///uid=*,cn=users,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Manage User SSH Public Keys";allow (write) groupdn = "ldap:///cn=Manage User SSH Public Keys,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'User Administrators'}, - }, - 'System: Manage User Certificates': { - 'ipapermright': {'write'}, - 'ipapermdefaultattr': {'usercertificate'}, - 'default_privileges': { - 'User Administrators', - 'Modify Users and Reset passwords', - }, - }, - 'System: Modify Users': { - 'ipapermright': {'write'}, - 'ipapermdefaultattr': { - 'businesscategory', 'carlicense', 'cn', 'departmentnumber', - 'description', 'displayname', 'employeetype', - 'employeenumber', 'facsimiletelephonenumber', - 'gecos', 'givenname', 'homephone', 'inetuserhttpurl', - 'initials', 'l', 'labeleduri', 'loginshell', 'manager', 'mail', - 'mepmanagedentry', 'mobile', 'objectclass', 'ou', 'pager', - 'postalcode', 'roomnumber', 'secretary', 'seealso', 'sn', 'st', - 'street', 'telephonenumber', 'title', 'userclass', - 'preferredlanguage', - }, - 'replaces': [ - '(targetattr = "givenname || sn || cn || displayname || title || initials || loginshell || gecos || homephone || mobile || pager || facsimiletelephonenumber || telephonenumber || street || roomnumber || l || st || postalcode || manager || secretary || description || carlicense || labeleduri || inetuserhttpurl || seealso || employeetype || businesscategory || ou || mepmanagedentry || objectclass")(target = "ldap:///uid=*,cn=users,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Modify Users";allow (write) groupdn = "ldap:///cn=Modify Users,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': { - 'User Administrators', - 'Modify Users and Reset passwords', - }, - }, - 'System: Remove Users': { - 'ipapermright': {'delete'}, - 'replaces': [ - '(target = "ldap:///uid=*,cn=users,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Remove Users";allow (delete) groupdn = "ldap:///cn=Remove Users,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'User Administrators'}, - }, - 'System: Unlock User': { - 'ipapermright': {'write'}, - 'ipapermdefaultattr': { - 'krblastadminunlock', 'krbloginfailedcount', 'nsaccountlock', - }, - 'replaces': [ - '(targetattr = "krbLastAdminUnlock || krbLoginFailedCount")(target = "ldap:///uid=*,cn=users,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Unlock user accounts";allow (write) groupdn = "ldap:///cn=Unlock user accounts,cn=permissions,cn=pbac,$SUFFIX";)', - ], - 'default_privileges': {'User Administrators'}, - }, - 'System: Read User Compat Tree': { - 'non_object': True, - 'ipapermbindruletype': 'anonymous', - 'ipapermlocation': api.env.basedn, - 'ipapermtarget': DN('cn=users', 'cn=compat', api.env.basedn), - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'objectclass', 'uid', 'cn', 'gecos', 'gidnumber', 'uidnumber', - 'homedirectory', 'loginshell', - }, - }, - 'System: Read User Views Compat Tree': { - 'non_object': True, - 'ipapermbindruletype': 'anonymous', - 'ipapermlocation': api.env.basedn, - 'ipapermtarget': DN('cn=users', 'cn=*', 'cn=views', 'cn=compat', api.env.basedn), - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'objectclass', 'uid', 'cn', 'gecos', 'gidnumber', 'uidnumber', - 'homedirectory', 'loginshell', - }, - }, - 'System: Read User NT Attributes': { - 'ipapermbindruletype': 'permission', - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'ntuserdomainid', 'ntuniqueid', 'ntuseracctexpires', - 'ntusercodepage', 'ntuserdeleteaccount', 'ntuserlastlogoff', - 'ntuserlastlogon', - }, - 'default_privileges': {'PassSync Service'}, - }, - } - - takes_params = baseuser.takes_params + ( - Bool('nsaccountlock?', - label=_('Account disabled'), - flags=['no_option'], - ), - Bool('preserved?', - label=_('Preserved user'), - default=False, - flags=['virtual_attribute', 'no_create', 'no_update'], - ), - ) - - def get_either_dn(self, *keys, **options): - ''' - Returns the DN of a user - The user can be active (active container) or delete (delete container) - If the user does not exist, returns the Active user DN - ''' - ldap = self.backend - # Check that this value is a Active user - try: - active_dn = self.get_dn(*keys, **options) - ldap.get_entry(active_dn, ['dn']) - - # The Active user exists - dn = active_dn - except errors.NotFound: - # Check that this value is a Delete user - delete_dn = DN(active_dn[0], self.delete_container_dn, api.env.basedn) - try: - ldap.get_entry(delete_dn, ['dn']) - - # The Delete user exists - dn = delete_dn - except errors.NotFound: - # The user is neither Active/Delete -> returns that Active DN - dn = active_dn - - return dn - - def _normalize_manager(self, manager): - """ - Given a userid verify the user's existence and return the dn. - """ - return super(user, self).normalize_manager(manager, self.active_container_dn) - - def get_preserved_attribute(self, entry, options): - if options.get('raw', False): - return - delete_container_dn = DN(self.delete_container_dn, api.env.basedn) - if entry.dn.endswith(delete_container_dn): - entry['preserved'] = True - elif options.get('all', False): - entry['preserved'] = False - - -@register() -class user_add(baseuser_add): - __doc__ = _('Add a new user.') - - msg_summary = _('Added user "%(value)s"') - - has_output_params = baseuser_add.has_output_params + user_output_params - - takes_options = LDAPCreate.takes_options + ( - Flag('noprivate', - cli_name='noprivate', - doc=_('Don\'t create user private group'), - ), - ) - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - dn = self.obj.get_either_dn(*keys, **options) - if not options.get('noprivate', False): - try: - # The Managed Entries plugin will allow a user to be created - # even if a group has a duplicate name. This would leave a user - # without a private group. Check for both the group and the user. - self.api.Object['group'].get_dn_if_exists(keys[-1]) - try: - self.api.Command['user_show'](keys[-1]) - self.obj.handle_duplicate_entry(*keys) - except errors.NotFound: - raise errors.ManagedGroupExistsError(group=keys[-1]) - except errors.NotFound: - pass - else: - # we don't want an user private group to be created for this user - # add NO_UPG_MAGIC description attribute to let the DS plugin know - entry_attrs.setdefault('description', []) - entry_attrs['description'].append(NO_UPG_MAGIC) - - entry_attrs.setdefault('uidnumber', baseldap.DNA_MAGIC) - - if not client_has_capability( - options['version'], 'optional_uid_params'): - # https://fedorahosted.org/freeipa/ticket/2886 - # Old clients say 999 (OLD_DNA_MAGIC) when they really mean - # "assign a value dynamically". - OLD_DNA_MAGIC = 999 - if entry_attrs.get('uidnumber') == OLD_DNA_MAGIC: - entry_attrs['uidnumber'] = baseldap.DNA_MAGIC - if entry_attrs.get('gidnumber') == OLD_DNA_MAGIC: - entry_attrs['gidnumber'] = baseldap.DNA_MAGIC - - validate_nsaccountlock(entry_attrs) - config = ldap.get_ipa_config() - if 'ipamaxusernamelength' in config: - if len(keys[-1]) > int(config.get('ipamaxusernamelength')[0]): - raise errors.ValidationError( - name=self.obj.primary_key.cli_name, - error=_('can be at most %(len)d characters') % dict( - len = int(config.get('ipamaxusernamelength')[0]) - ) - ) - default_shell = config.get('ipadefaultloginshell', [paths.SH])[0] - entry_attrs.setdefault('loginshell', default_shell) - # hack so we can request separate first and last name in CLI - full_name = '%s %s' % (entry_attrs['givenname'], entry_attrs['sn']) - entry_attrs.setdefault('cn', full_name) - if 'homedirectory' not in entry_attrs: - # get home's root directory from config - homes_root = config.get('ipahomesrootdir', [paths.HOME_DIR])[0] - # build user's home directory based on his uid - entry_attrs['homedirectory'] = posixpath.join(homes_root, keys[-1]) - entry_attrs.setdefault('krbprincipalname', '%s@%s' % (entry_attrs['uid'], api.env.realm)) - - if entry_attrs.get('gidnumber') is None: - # gidNumber wasn't specified explicity, find out what it should be - if not options.get('noprivate', False) and ldap.has_upg(): - # User Private Groups - uidNumber == gidNumber - entry_attrs['gidnumber'] = entry_attrs['uidnumber'] - else: - # we're adding new users to a default group, get its gidNumber - # get default group name from config - def_primary_group = config.get('ipadefaultprimarygroup') - group_dn = self.api.Object['group'].get_dn(def_primary_group) - try: - group_attrs = ldap.get_entry(group_dn, ['gidnumber']) - except errors.NotFound: - error_msg = _('Default group for new users not found') - raise errors.NotFound(reason=error_msg) - if 'gidnumber' not in group_attrs: - error_msg = _('Default group for new users is not POSIX') - raise errors.NotFound(reason=error_msg) - entry_attrs['gidnumber'] = group_attrs['gidnumber'] - - if 'userpassword' not in entry_attrs and options.get('random'): - entry_attrs['userpassword'] = ipa_generate_password(baseuser_pwdchars) - # save the password so it can be displayed in post_callback - setattr(context, 'randompassword', entry_attrs['userpassword']) - - if 'mail' in entry_attrs: - entry_attrs['mail'] = self.obj.normalize_and_validate_email(entry_attrs['mail'], config) - else: - # No e-mail passed in. If we have a default e-mail domain set - # then we'll add it automatically. - defaultdomain = config.get('ipadefaultemaildomain', [None])[0] - if defaultdomain: - entry_attrs['mail'] = self.obj.normalize_and_validate_email(keys[-1], config) - - if 'manager' in entry_attrs: - entry_attrs['manager'] = self.obj.normalize_manager(entry_attrs['manager'], self.obj.active_container_dn) - - if 'userclass' in entry_attrs and \ - 'ipauser' not in entry_attrs['objectclass']: - entry_attrs['objectclass'].append('ipauser') - - if 'ipauserauthtype' in entry_attrs and \ - 'ipauserauthtypeclass' not in entry_attrs['objectclass']: - entry_attrs['objectclass'].append('ipauserauthtypeclass') - - rcl = entry_attrs.get('ipatokenradiusconfiglink', None) - if rcl: - if 'ipatokenradiusproxyuser' not in entry_attrs['objectclass']: - entry_attrs['objectclass'].append('ipatokenradiusproxyuser') - - answer = self.api.Object['radiusproxy'].get_dn_if_exists(rcl) - entry_attrs['ipatokenradiusconfiglink'] = answer - - self.pre_common_callback(ldap, dn, entry_attrs, attrs_list, *keys, - **options) - - return dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - config = ldap.get_ipa_config() - # add the user we just created into the default primary group - def_primary_group = config.get('ipadefaultprimarygroup') - group_dn = self.api.Object['group'].get_dn(def_primary_group) - - # if the user is already a member of default primary group, - # do not raise error - # this can happen if automember rule or default group is set - try: - ldap.add_entry_to_group(dn, group_dn) - except errors.AlreadyGroupMember: - pass - - # delete description attribute NO_UPG_MAGIC if present - if options.get('noprivate', False): - if not options.get('all', False): - desc_attr = ldap.get_entry(dn, ['description']) - entry_attrs.update(desc_attr) - if 'description' in entry_attrs and NO_UPG_MAGIC in entry_attrs['description']: - entry_attrs['description'].remove(NO_UPG_MAGIC) - kw = {'setattr': unicode('description=%s' % ','.join(entry_attrs['description']))} - try: - self.api.Command['user_mod'](keys[-1], **kw) - except (errors.EmptyModlist, errors.NotFound): - pass - - # Fetch the entry again to update memberof, mep data, etc updated - # at the end of the transaction. - newentry = ldap.get_entry(dn, ['*']) - entry_attrs.update(newentry) - - if options.get('random', False): - try: - entry_attrs['randompassword'] = unicode(getattr(context, 'randompassword')) - except AttributeError: - # if both randompassword and userpassword options were used - pass - - self.obj.get_preserved_attribute(entry_attrs, options) - - self.post_common_callback(ldap, dn, entry_attrs, *keys, **options) - - return dn - - -@register() -class user_del(baseuser_del): - __doc__ = _('Delete a user.') - - msg_summary = _('Deleted user "%(value)s"') - - takes_options = baseuser_del.takes_options + ( - Bool('preserve?', - exclude='cli', - ), - ) - - def _preserve_user(self, pkey, delete_container, **options): - assert isinstance(delete_container, DN) - - dn = self.obj.get_either_dn(pkey, **options) - delete_dn = DN(dn[0], delete_container) - ldap = self.obj.backend - self.log.debug("preserve move %s -> %s" % (dn, delete_dn)) - - if dn.endswith(delete_container): - raise errors.ExecutionError( - _('%s: user is already preserved' % pkey) - ) - # Check that this value is a Active user - try: - original_entry_attrs = self._exc_wrapper( - pkey, options, ldap.get_entry)(dn, ['dn']) - except errors.NotFound: - self.obj.handle_not_found(pkey) - - for callback in self.get_callbacks('pre'): - dn = callback(self, ldap, dn, pkey, **options) - assert isinstance(dn, DN) - - # start to move the entry to Delete container - self._exc_wrapper(pkey, options, ldap.move_entry)(dn, delete_dn, - del_old=True) - - # Then clear the credential attributes - attrs_to_clear = ['krbPrincipalKey', 'krbLastPwdChange', - 'krbPasswordExpiration', 'userPassword'] - - entry_attrs = self._exc_wrapper(pkey, options, ldap.get_entry)( - delete_dn, attrs_to_clear) - - clearedCredential = False - for attr in attrs_to_clear: - if attr.lower() in entry_attrs: - del entry_attrs[attr] - clearedCredential = True - if clearedCredential: - self._exc_wrapper(pkey, options, ldap.update_entry)(entry_attrs) - - # Then restore some original entry attributes - attrs_to_restore = ['secretary', 'managedby', 'manager', 'ipauniqueid', - 'uidnumber', 'gidnumber', 'passwordHistory'] - - entry_attrs = self._exc_wrapper( - pkey, options, ldap.get_entry)(delete_dn, attrs_to_restore) - - restoreAttr = False - for attr in attrs_to_restore: - if ((attr.lower() in original_entry_attrs) and - not (attr.lower() in entry_attrs)): - restoreAttr = True - entry_attrs[attr.lower()] = original_entry_attrs[attr.lower()] - if restoreAttr: - self._exc_wrapper(pkey, options, ldap.update_entry)(entry_attrs) - - def pre_callback(self, ldap, dn, *keys, **options): - dn = self.obj.get_either_dn(*keys, **options) - - # For User life Cycle: user-del is a common plugin - # command to delete active user (active container) and - # delete user (delete container). - # If the target entry is a Delete entry, skip the orphaning/removal - # of OTP tokens. - check_protected_member(keys[-1]) - - if not options.get('preserve', False): - # Remove any ID overrides tied with this user - try: - remove_ipaobject_overrides(self.obj.backend, self.obj.api, dn) - except errors.NotFound: - self.obj.handle_not_found(*keys) - - if dn.endswith(DN(self.obj.delete_container_dn, api.env.basedn)): - return dn - - # Delete all tokens owned and managed by this user. - # Orphan all tokens owned but not managed by this user. - owner = self.api.Object.user.get_primary_key_from_dn(dn) - results = self.api.Command.otptoken_find( - ipatokenowner=owner, no_members=False)['result'] - for token in results: - orphan = not [x for x in token.get('managedby_user', []) if x == owner] - token = self.api.Object.otptoken.get_primary_key_from_dn(token['dn']) - if orphan: - self.api.Command.otptoken_mod(token, ipatokenowner=None) - else: - self.api.Command.otptoken_del(token) - - return dn - - def execute(self, *keys, **options): - - # We are going to permanent delete or the user is already in the delete container. - delete_container = DN(self.obj.delete_container_dn, self.api.env.basedn) - - # The user to delete is active and there is no 'no_preserve' option - if options.get('preserve', False): - failed = [] - preserved = [] - for pkey in keys[-1]: - try: - self._preserve_user(pkey, delete_container, **options) - preserved.append(pkey_to_value(pkey, options)) - except Exception: - if not options.get('continue', False): - raise - failed.append(pkey_to_value(pkey, options)) - - val = dict(result=dict(failed=failed), value=preserved) - return val - else: - return super(user_del, self).execute(*keys, **options) - - -@register() -class user_mod(baseuser_mod): - __doc__ = _('Modify a user.') - - msg_summary = _('Modified user "%(value)s"') - - has_output_params = baseuser_mod.has_output_params + user_output_params - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): - dn = self.obj.get_either_dn(*keys, **options) - self.pre_common_callback(ldap, dn, entry_attrs, attrs_list, *keys, - **options) - validate_nsaccountlock(entry_attrs) - return dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - self.post_common_callback(ldap, dn, entry_attrs, *keys, **options) - self.obj.get_preserved_attribute(entry_attrs, options) - return dn - - -@register() -class user_find(baseuser_find): - __doc__ = _('Search for users.') - - member_attributes = ['memberof'] - has_output_params = baseuser_find.has_output_params + user_output_params - - msg_summary = ngettext( - '%(count)d user matched', '%(count)d users matched', 0 - ) - - takes_options = LDAPSearch.takes_options + ( - Flag('whoami', - label=_('Self'), - doc=_('Display user record for current Kerberos principal'), - ), - ) - - def pre_callback(self, ldap, filter, attrs_list, base_dn, scope, *keys, **options): - assert isinstance(base_dn, DN) - self.pre_common_callback(ldap, filter, attrs_list, base_dn, scope, - *keys, **options) - - if options.get('whoami'): - return ("(&(objectclass=posixaccount)(krbprincipalname=%s))"%\ - getattr(context, 'principal'), base_dn, scope) - - preserved = options.get('preserved', False) - if preserved is None: - base_dn = self.api.env.basedn - scope = ldap.SCOPE_SUBTREE - elif preserved: - base_dn = DN(self.obj.delete_container_dn, self.api.env.basedn) - else: - base_dn = DN(self.obj.active_container_dn, self.api.env.basedn) - - return (filter, base_dn, scope) - - def post_callback(self, ldap, entries, truncated, *args, **options): - if options.get('pkey_only', False): - return truncated - - if options.get('preserved', False) is None: - base_dns = ( - DN(self.obj.active_container_dn, self.api.env.basedn), - DN(self.obj.delete_container_dn, self.api.env.basedn), - ) - entries[:] = [e for e in entries - if any(e.dn.endswith(bd) for bd in base_dns)] - - self.post_common_callback(ldap, entries, lockout=False, **options) - for entry in entries: - self.obj.get_preserved_attribute(entry, options) - - return truncated - - -@register() -class user_show(baseuser_show): - __doc__ = _('Display information about a user.') - - has_output_params = baseuser_show.has_output_params + user_output_params - takes_options = baseuser_show.takes_options + ( - Str('out?', - doc=_('file to store certificate in'), - ), - ) - - def pre_callback(self, ldap, dn, attrs_list, *keys, **options): - dn = self.obj.get_either_dn(*keys, **options) - self.pre_common_callback(ldap, dn, attrs_list, *keys, **options) - return dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - convert_nsaccountlock(entry_attrs) - self.post_common_callback(ldap, dn, entry_attrs, *keys, **options) - self.obj.get_preserved_attribute(entry_attrs, options) - return dn - - -@register() -class user_undel(LDAPQuery): - __doc__ = _('Undelete a delete user account.') - - has_output = output.standard_value - msg_summary = _('Undeleted user account "%(value)s"') - - def execute(self, *keys, **options): - ldap = self.obj.backend - - # First check that the user exists and is a delete one - delete_dn = self.obj.get_either_dn(*keys, **options) - try: - entry_attrs = self._exc_wrapper(keys, options, ldap.get_entry)(delete_dn) - except errors.NotFound: - self.obj.handle_not_found(*keys) - if delete_dn.endswith(DN(self.obj.active_container_dn, - api.env.basedn)): - raise errors.InvocationError( - message=_('user "%s" is already active') % keys[-1]) - - active_dn = DN(delete_dn[0], self.obj.active_container_dn, api.env.basedn) - - # start to move the entry to the Active container - self._exc_wrapper(keys, options, ldap.move_entry)(delete_dn, active_dn, del_old=True) - - # add the user we just undelete into the default primary group - config = ldap.get_ipa_config() - def_primary_group = config.get('ipadefaultprimarygroup') - group_dn = self.api.Object['group'].get_dn(def_primary_group) - - # if the user is already a member of default primary group, - # do not raise error - # this can happen if automember rule or default group is set - try: - ldap.add_entry_to_group(active_dn, group_dn) - except errors.AlreadyGroupMember: - pass - - return dict( - result=True, - value=pkey_to_value(keys[0], options), - ) - - -@register() -class user_stage(LDAPMultiQuery): - __doc__ = _('Move deleted user into staged area') - - has_output = output.standard_multi_delete - msg_summary = _('Staged user account "%(value)s"') - - def execute(self, *keys, **options): - staged = [] - failed = [] - - for key in keys[-1]: - single_keys = keys[:-1] + (key,) - multi_keys = keys[:-1] + ((key,),) - - user = self.api.Command.user_show(*single_keys, all=True)['result'] - new_options = {} - for param in self.api.Command.stageuser_add.options(): - try: - value = user[param.name] - except KeyError: - continue - if param.multivalue and not isinstance(value, (list, tuple)): - value = [value] - elif not param.multivalue and isinstance(value, (list, tuple)): - value = value[0] - new_options[param.name] = value - - try: - self.api.Command.stageuser_add(*single_keys, **new_options) - try: - self.api.Command.user_del(*multi_keys, preserve=False) - except errors.ExecutionError: - self.api.Command.stageuser_del(*multi_keys) - raise - except errors.ExecutionError: - if not options['continue']: - raise - failed.append(key) - else: - staged.append(key) - - return dict( - result=dict( - failed=pkey_to_value(failed, options), - ), - value=pkey_to_value(staged, options), - ) - - -@register() -class user_disable(LDAPQuery): - __doc__ = _('Disable a user account.') - - has_output = output.standard_value - msg_summary = _('Disabled user account "%(value)s"') - - def execute(self, *keys, **options): - ldap = self.obj.backend - - check_protected_member(keys[-1]) - - dn = self.obj.get_either_dn(*keys, **options) - ldap.deactivate_entry(dn) - - return dict( - result=True, - value=pkey_to_value(keys[0], options), - ) - - -@register() -class user_enable(LDAPQuery): - __doc__ = _('Enable a user account.') - - has_output = output.standard_value - has_output_params = LDAPQuery.has_output_params + user_output_params - msg_summary = _('Enabled user account "%(value)s"') - - def execute(self, *keys, **options): - ldap = self.obj.backend - - dn = self.obj.get_either_dn(*keys, **options) - - ldap.activate_entry(dn) - - return dict( - result=True, - value=pkey_to_value(keys[0], options), - ) - - -@register() -class user_unlock(LDAPQuery): - __doc__ = _(""" - Unlock a user account - - An account may become locked if the password is entered incorrectly too - many times within a specific time period as controlled by password - policy. A locked account is a temporary condition and may be unlocked by - an administrator.""") - - has_output = output.standard_value - msg_summary = _('Unlocked account "%(value)s"') - - def execute(self, *keys, **options): - dn = self.obj.get_either_dn(*keys, **options) - entry = self.obj.backend.get_entry( - dn, ['krbLastAdminUnlock', 'krbLoginFailedCount']) - - entry['krbLastAdminUnlock'] = [strftime("%Y%m%d%H%M%SZ", gmtime())] - entry['krbLoginFailedCount'] = ['0'] - - self.obj.backend.update_entry(entry) - - return dict( - result=True, - value=pkey_to_value(keys[0], options), - ) - - -@register() -class user_status(LDAPQuery): - __doc__ = _(""" - Lockout status of a user account - - An account may become locked if the password is entered incorrectly too - many times within a specific time period as controlled by password - policy. A locked account is a temporary condition and may be unlocked by - an administrator. - - This connects to each IPA master and displays the lockout status on - each one. - - To determine whether an account is locked on a given server you need - to compare the number of failed logins and the time of the last failure. - For an account to be locked it must exceed the maxfail failures within - the failinterval duration as specified in the password policy associated - with the user. - - The failed login counter is modified only when a user attempts a log in - so it is possible that an account may appear locked but the last failed - login attempt is older than the lockouttime of the password policy. This - means that the user may attempt a login again. """) - - has_output = output.standard_list_of_entries - has_output_params = LDAPSearch.has_output_params + status_output_params - - def execute(self, *keys, **options): - ldap = self.obj.backend - dn = self.obj.get_either_dn(*keys, **options) - attr_list = ['krbloginfailedcount', 'krblastsuccessfulauth', 'krblastfailedauth', 'nsaccountlock'] - - disabled = False - masters = [] - # Get list of masters - try: - (masters, truncated) = ldap.find_entries( - None, ['*'], DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), api.env.basedn), - ldap.SCOPE_ONELEVEL - ) - except errors.NotFound: - # If this happens we have some pretty serious problems - self.error('No IPA masters found!') - - entries = [] - count = 0 - for master in masters: - host = master['cn'][0] - if host == api.env.host: - other_ldap = self.obj.backend - else: - other_ldap = ldap2(self.api, ldap_uri='ldap://%s' % host) - try: - other_ldap.connect(ccache=os.environ['KRB5CCNAME']) - except Exception as e: - self.error("user_status: Connecting to %s failed with %s" % (host, str(e))) - newresult = {'dn': dn} - newresult['server'] = _("%(host)s failed: %(error)s") % dict(host=host, error=str(e)) - entries.append(newresult) - count += 1 - continue - try: - entry = other_ldap.get_entry(dn, attr_list) - newresult = {'dn': dn} - for attr in ['krblastsuccessfulauth', 'krblastfailedauth']: - newresult[attr] = entry.get(attr, [u'N/A']) - newresult['krbloginfailedcount'] = entry.get('krbloginfailedcount', u'0') - if not options.get('raw', False): - for attr in ['krblastsuccessfulauth', 'krblastfailedauth']: - try: - if newresult[attr][0] == u'N/A': - continue - newtime = time.strptime(newresult[attr][0], '%Y%m%d%H%M%SZ') - newresult[attr][0] = unicode(time.strftime('%Y-%m-%dT%H:%M:%SZ', newtime)) - except Exception as e: - self.debug("time conversion failed with %s" % str(e)) - newresult['server'] = host - if options.get('raw', False): - time_format = '%Y%m%d%H%M%SZ' - else: - time_format = '%Y-%m-%dT%H:%M:%SZ' - newresult['now'] = unicode(strftime(time_format, gmtime())) - convert_nsaccountlock(entry) - if 'nsaccountlock' in entry: - disabled = entry['nsaccountlock'] - self.obj.get_preserved_attribute(entry, options) - entries.append(newresult) - count += 1 - except errors.NotFound: - self.obj.handle_not_found(*keys) - except Exception as e: - self.error("user_status: Retrieving status for %s failed with %s" % (dn, str(e))) - newresult = {'dn': dn} - newresult['server'] = _("%(host)s failed") % dict(host=host) - entries.append(newresult) - count += 1 - - if host != api.env.host: - other_ldap.disconnect() - - return dict(result=entries, - count=count, - truncated=False, - summary=unicode(_('Account disabled: %(disabled)s' % - dict(disabled=disabled))), - ) - - -@register() -class user_add_cert(LDAPAddAttribute): - __doc__ = _('Add one or more certificates to the user entry') - msg_summary = _('Added certificates to user "%(value)s"') - attribute = 'usercertificate' - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, - **options): - dn = self.obj.get_either_dn(*keys, **options) - - self.obj.convert_usercertificate_pre(entry_attrs) - - return dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - - self.obj.convert_usercertificate_post(entry_attrs, **options) - - return dn - - -@register() -class user_remove_cert(LDAPRemoveAttribute): - __doc__ = _('Remove one or more certificates to the user entry') - msg_summary = _('Removed certificates from user "%(value)s"') - attribute = 'usercertificate' - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, - **options): - dn = self.obj.get_either_dn(*keys, **options) - - self.obj.convert_usercertificate_pre(entry_attrs) - - return dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - assert isinstance(dn, DN) - - self.obj.convert_usercertificate_post(entry_attrs, **options) - - return dn - - -@register() -class user_add_manager(baseuser_add_manager): - __doc__ = _("Add a manager to the user entry") - - -@register() -class user_remove_manager(baseuser_remove_manager): - __doc__ = _("Remove a manager to the user entry") diff --git a/ipalib/plugins/vault.py b/ipalib/plugins/vault.py deleted file mode 100644 index 05db63cdc..000000000 --- a/ipalib/plugins/vault.py +++ /dev/null @@ -1,1215 +0,0 @@ -# Authors: -# Endi S. Dewata <edewata@redhat.com> -# -# Copyright (C) 2015 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/>. - -from ipalib.frontend import Command, Object -from ipalib import api, errors -from ipalib import Bytes, Flag, Str, StrEnum -from ipalib import output -from ipalib.crud import PKQuery, Retrieve -from ipalib.plugable import Registry -from .baseldap import LDAPObject, LDAPCreate, LDAPDelete,\ - LDAPSearch, LDAPUpdate, LDAPRetrieve, LDAPAddMember, LDAPRemoveMember,\ - LDAPModMember, pkey_to_value -from ipalib.request import context -from .baseuser import split_principal -from .service import normalize_principal -from ipalib import _, ngettext -from ipapython.dn import DN - -if api.env.in_server: - import pki.account - import pki.key - -__doc__ = _(""" -Vaults -""") + _(""" -Manage vaults. -""") + _(""" -Vault is a secure place to store a secret. -""") + _(""" -Based on the ownership there are three vault categories: -* user/private vault -* service vault -* shared vault -""") + _(""" -User vaults are vaults owned used by a particular user. Private -vaults are vaults owned the current user. Service vaults are -vaults owned by a service. Shared vaults are owned by the admin -but they can be used by other users or services. -""") + _(""" -Based on the security mechanism there are three types of -vaults: -* standard vault -* symmetric vault -* asymmetric vault -""") + _(""" -Standard vault uses a secure mechanism to transport and -store the secret. The secret can only be retrieved by users -that have access to the vault. -""") + _(""" -Symmetric vault is similar to the standard vault, but it -pre-encrypts the secret using a password before transport. -The secret can only be retrieved using the same password. -""") + _(""" -Asymmetric vault is similar to the standard vault, but it -pre-encrypts the secret using a public key before transport. -The secret can only be retrieved using the private key. -""") + _(""" -EXAMPLES: -""") + _(""" - List vaults: - ipa vault-find - [--user <user>|--service <service>|--shared] -""") + _(""" - Add a standard vault: - ipa vault-add <name> - [--user <user>|--service <service>|--shared] - --type standard -""") + _(""" - Add a symmetric vault: - ipa vault-add <name> - [--user <user>|--service <service>|--shared] - --type symmetric --password-file password.txt -""") + _(""" - Add an asymmetric vault: - ipa vault-add <name> - [--user <user>|--service <service>|--shared] - --type asymmetric --public-key-file public.pem -""") + _(""" - Show a vault: - ipa vault-show <name> - [--user <user>|--service <service>|--shared] -""") + _(""" - Modify vault description: - ipa vault-mod <name> - [--user <user>|--service <service>|--shared] - --desc <description> -""") + _(""" - Modify vault type: - ipa vault-mod <name> - [--user <user>|--service <service>|--shared] - --type <type> - [old password/private key] - [new password/public key] -""") + _(""" - Modify symmetric vault password: - ipa vault-mod <name> - [--user <user>|--service <service>|--shared] - --change-password - ipa vault-mod <name> - [--user <user>|--service <service>|--shared] - --old-password <old password> - --new-password <new password> - ipa vault-mod <name> - [--user <user>|--service <service>|--shared] - --old-password-file <old password file> - --new-password-file <new password file> -""") + _(""" - Modify asymmetric vault keys: - ipa vault-mod <name> - [--user <user>|--service <service>|--shared] - --private-key-file <old private key file> - --public-key-file <new public key file> -""") + _(""" - Delete a vault: - ipa vault-del <name> - [--user <user>|--service <service>|--shared] -""") + _(""" - Display vault configuration: - ipa vaultconfig-show -""") + _(""" - Archive data into standard vault: - ipa vault-archive <name> - [--user <user>|--service <service>|--shared] - --in <input file> -""") + _(""" - Archive data into symmetric vault: - ipa vault-archive <name> - [--user <user>|--service <service>|--shared] - --in <input file> - --password-file password.txt -""") + _(""" - Archive data into asymmetric vault: - ipa vault-archive <name> - [--user <user>|--service <service>|--shared] - --in <input file> -""") + _(""" - Retrieve data from standard vault: - ipa vault-retrieve <name> - [--user <user>|--service <service>|--shared] - --out <output file> -""") + _(""" - Retrieve data from symmetric vault: - ipa vault-retrieve <name> - [--user <user>|--service <service>|--shared] - --out <output file> - --password-file password.txt -""") + _(""" - Retrieve data from asymmetric vault: - ipa vault-retrieve <name> - [--user <user>|--service <service>|--shared] - --out <output file> --private-key-file private.pem -""") + _(""" - Add vault owners: - ipa vault-add-owner <name> - [--user <user>|--service <service>|--shared] - [--users <users>] [--groups <groups>] [--services <services>] -""") + _(""" - Delete vault owners: - ipa vault-remove-owner <name> - [--user <user>|--service <service>|--shared] - [--users <users>] [--groups <groups>] [--services <services>] -""") + _(""" - Add vault members: - ipa vault-add-member <name> - [--user <user>|--service <service>|--shared] - [--users <users>] [--groups <groups>] [--services <services>] -""") + _(""" - Delete vault members: - ipa vault-remove-member <name> - [--user <user>|--service <service>|--shared] - [--users <users>] [--groups <groups>] [--services <services>] -""") - - -register = Registry() - -vault_options = ( - Str( - 'service?', - doc=_('Service name of the service vault'), - normalizer=normalize_principal, - ), - Flag( - 'shared?', - doc=_('Shared vault'), - ), - Str( - 'username?', - cli_name='user', - doc=_('Username of the user vault'), - ), -) - - -class VaultModMember(LDAPModMember): - def get_options(self): - for param in super(VaultModMember, self).get_options(): - if param.name == 'service' and param not in vault_options: - param = param.clone_rename('services') - yield param - - def get_member_dns(self, **options): - if 'services' in options: - options['service'] = options.pop('services') - else: - options.pop('service', None) - return super(VaultModMember, self).get_member_dns(**options) - - def post_callback(self, ldap, completed, failed, dn, entry_attrs, *keys, **options): - for fail in failed.itervalues(): - fail['services'] = fail.pop('service', []) - self.obj.get_container_attribute(entry_attrs, options) - return completed, dn - - -@register() -class vaultcontainer(LDAPObject): - __doc__ = _(""" - Vault Container object. - """) - - container_dn = api.env.container_vault - - object_name = _('vaultcontainer') - object_name_plural = _('vaultcontainers') - object_class = ['ipaVaultContainer'] - permission_filter_objectclasses = ['ipaVaultContainer'] - - attribute_members = { - 'owner': ['user', 'group', 'service'], - } - - label = _('Vault Containers') - label_singular = _('Vault Container') - - managed_permissions = { - 'System: Read Vault Containers': { - 'ipapermlocation': api.env.basedn, - 'ipapermtarget': DN(api.env.container_vault, api.env.basedn), - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'objectclass', 'cn', 'description', 'owner', - }, - 'default_privileges': {'Vault Administrators'}, - }, - 'System: Add Vault Containers': { - 'ipapermlocation': api.env.basedn, - 'ipapermtarget': DN(api.env.container_vault, api.env.basedn), - 'ipapermright': {'add'}, - 'default_privileges': {'Vault Administrators'}, - }, - 'System: Delete Vault Containers': { - 'ipapermlocation': api.env.basedn, - 'ipapermtarget': DN(api.env.container_vault, api.env.basedn), - 'ipapermright': {'delete'}, - 'default_privileges': {'Vault Administrators'}, - }, - 'System: Modify Vault Containers': { - 'ipapermlocation': api.env.basedn, - 'ipapermtarget': DN(api.env.container_vault, api.env.basedn), - 'ipapermright': {'write'}, - 'ipapermdefaultattr': { - 'objectclass', 'cn', 'description', - }, - 'default_privileges': {'Vault Administrators'}, - }, - 'System: Manage Vault Container Ownership': { - 'ipapermlocation': api.env.basedn, - 'ipapermtarget': DN(api.env.container_vault, api.env.basedn), - 'ipapermright': {'write'}, - 'ipapermdefaultattr': { - 'owner', - }, - 'default_privileges': {'Vault Administrators'}, - }, - } - - takes_params = ( - Str( - 'owner_user?', - label=_('Owner users'), - ), - Str( - 'owner_group?', - label=_('Owner groups'), - ), - Str( - 'owner_service?', - label=_('Owner services'), - ), - Str( - 'owner?', - label=_('Failed owners'), - ), - Str( - 'service?', - label=_('Vault service'), - flags={'virtual_attribute'}, - ), - Flag( - 'shared?', - label=_('Shared vault'), - flags={'virtual_attribute'}, - ), - Str( - 'username?', - label=_('Vault user'), - flags={'virtual_attribute'}, - ), - ) - - def get_dn(self, *keys, **options): - """ - Generates vault DN from parameters. - """ - service = options.get('service') - shared = options.get('shared') - user = options.get('username') - - count = (bool(service) + bool(shared) + bool(user)) - if count > 1: - raise errors.MutuallyExclusiveError( - reason=_('Service, shared and user options ' + - 'cannot be specified simultaneously')) - - parent_dn = super(vaultcontainer, self).get_dn(*keys, **options) - - if not count: - principal = getattr(context, 'principal') - - if principal.startswith('host/'): - raise errors.NotImplementedError( - reason=_('Host is not supported')) - - (name, realm) = split_principal(principal) - if '/' in name: - service = principal - else: - user = name - - if service: - dn = DN(('cn', service), ('cn', 'services'), parent_dn) - elif shared: - dn = DN(('cn', 'shared'), parent_dn) - elif user: - dn = DN(('cn', user), ('cn', 'users'), parent_dn) - else: - raise RuntimeError - - return dn - - def get_container_attribute(self, entry, options): - if options.get('raw', False): - return - container_dn = DN(self.container_dn, self.api.env.basedn) - if entry.dn.endswith(DN(('cn', 'services'), container_dn)): - entry['service'] = entry.dn[0]['cn'] - elif entry.dn.endswith(DN(('cn', 'shared'), container_dn)): - entry['shared'] = True - elif entry.dn.endswith(DN(('cn', 'users'), container_dn)): - entry['username'] = entry.dn[0]['cn'] - - -@register() -class vaultcontainer_show(LDAPRetrieve): - __doc__ = _('Display information about a vault container.') - - takes_options = LDAPRetrieve.takes_options + vault_options - - has_output_params = LDAPRetrieve.has_output_params - - def pre_callback(self, ldap, dn, attrs_list, *keys, **options): - assert isinstance(dn, DN) - - if not self.api.Command.kra_is_enabled()['result']: - raise errors.InvocationError( - format=_('KRA service is not enabled')) - - return dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - self.obj.get_container_attribute(entry_attrs, options) - return dn - - -@register() -class vaultcontainer_del(LDAPDelete): - __doc__ = _('Delete a vault container.') - - takes_options = LDAPDelete.takes_options + vault_options - - msg_summary = _('Deleted vault container') - - subtree_delete = False - - def pre_callback(self, ldap, dn, *keys, **options): - assert isinstance(dn, DN) - - if not self.api.Command.kra_is_enabled()['result']: - raise errors.InvocationError( - format=_('KRA service is not enabled')) - - return dn - - def execute(self, *keys, **options): - keys = keys + (u'',) - return super(vaultcontainer_del, self).execute(*keys, **options) - - -@register() -class vaultcontainer_add_owner(VaultModMember, LDAPAddMember): - __doc__ = _('Add owners to a vault container.') - - takes_options = LDAPAddMember.takes_options + vault_options - - member_attributes = ['owner'] - member_param_label = _('owner %s') - member_count_out = ('%i owner added.', '%i owners added.') - - has_output = ( - output.Entry('result'), - output.Output( - 'failed', - type=dict, - doc=_('Owners that could not be added'), - ), - output.Output( - 'completed', - type=int, - doc=_('Number of owners added'), - ), - ) - - -@register() -class vaultcontainer_remove_owner(VaultModMember, LDAPRemoveMember): - __doc__ = _('Remove owners from a vault container.') - - takes_options = LDAPRemoveMember.takes_options + vault_options - - member_attributes = ['owner'] - member_param_label = _('owner %s') - member_count_out = ('%i owner removed.', '%i owners removed.') - - has_output = ( - output.Entry('result'), - output.Output( - 'failed', - type=dict, - doc=_('Owners that could not be removed'), - ), - output.Output( - 'completed', - type=int, - doc=_('Number of owners removed'), - ), - ) - - -@register() -class vault(LDAPObject): - __doc__ = _(""" - Vault object. - """) - - container_dn = api.env.container_vault - - object_name = _('vault') - object_name_plural = _('vaults') - - object_class = ['ipaVault'] - permission_filter_objectclasses = ['ipaVault'] - default_attributes = [ - 'cn', - 'description', - 'ipavaulttype', - 'ipavaultsalt', - 'ipavaultpublickey', - 'owner', - 'member', - ] - search_display_attributes = [ - 'cn', - 'description', - 'ipavaulttype', - ] - attribute_members = { - 'owner': ['user', 'group', 'service'], - 'member': ['user', 'group', 'service'], - } - - label = _('Vaults') - label_singular = _('Vault') - - managed_permissions = { - 'System: Read Vaults': { - 'ipapermlocation': api.env.basedn, - 'ipapermtarget': DN(api.env.container_vault, api.env.basedn), - 'ipapermright': {'read', 'search', 'compare'}, - 'ipapermdefaultattr': { - 'objectclass', 'cn', 'description', 'ipavaulttype', - 'ipavaultsalt', 'ipavaultpublickey', 'owner', 'member', - 'memberuser', 'memberhost', - }, - 'default_privileges': {'Vault Administrators'}, - }, - 'System: Add Vaults': { - 'ipapermlocation': api.env.basedn, - 'ipapermtarget': DN(api.env.container_vault, api.env.basedn), - 'ipapermright': {'add'}, - 'default_privileges': {'Vault Administrators'}, - }, - 'System: Delete Vaults': { - 'ipapermlocation': api.env.basedn, - 'ipapermtarget': DN(api.env.container_vault, api.env.basedn), - 'ipapermright': {'delete'}, - 'default_privileges': {'Vault Administrators'}, - }, - 'System: Modify Vaults': { - 'ipapermlocation': api.env.basedn, - 'ipapermtarget': DN(api.env.container_vault, api.env.basedn), - 'ipapermright': {'write'}, - 'ipapermdefaultattr': { - 'objectclass', 'cn', 'description', 'ipavaulttype', - 'ipavaultsalt', 'ipavaultpublickey', - }, - 'default_privileges': {'Vault Administrators'}, - }, - 'System: Manage Vault Ownership': { - 'ipapermlocation': api.env.basedn, - 'ipapermtarget': DN(api.env.container_vault, api.env.basedn), - 'ipapermright': {'write'}, - 'ipapermdefaultattr': { - 'owner', - }, - 'default_privileges': {'Vault Administrators'}, - }, - 'System: Manage Vault Membership': { - 'ipapermlocation': api.env.basedn, - 'ipapermtarget': DN(api.env.container_vault, api.env.basedn), - 'ipapermright': {'write'}, - 'ipapermdefaultattr': { - 'member', - }, - 'default_privileges': {'Vault Administrators'}, - }, - } - - takes_params = ( - Str( - 'cn', - cli_name='name', - label=_('Vault name'), - primary_key=True, - pattern='^[a-zA-Z0-9_.-]+$', - pattern_errmsg='may only include letters, numbers, _, ., and -', - maxlength=255, - ), - Str( - 'description?', - cli_name='desc', - label=_('Description'), - doc=_('Vault description'), - ), - StrEnum( - 'ipavaulttype?', - cli_name='type', - label=_('Type'), - doc=_('Vault type'), - values=(u'standard', u'symmetric', u'asymmetric', ), - default=u'symmetric', - autofill=True, - ), - Bytes( - 'ipavaultsalt?', - cli_name='salt', - label=_('Salt'), - doc=_('Vault salt'), - flags=['no_search'], - ), - Bytes( - 'ipavaultpublickey?', - cli_name='public_key', - label=_('Public key'), - doc=_('Vault public key'), - flags=['no_search'], - ), - Str( - 'owner_user?', - label=_('Owner users'), - flags=['no_create', 'no_update', 'no_search'], - ), - Str( - 'owner_group?', - label=_('Owner groups'), - flags=['no_create', 'no_update', 'no_search'], - ), - Str( - 'owner_service?', - label=_('Owner services'), - flags=['no_create', 'no_update', 'no_search'], - ), - Str( - 'owner?', - label=_('Failed owners'), - flags=['no_create', 'no_update', 'no_search'], - ), - Str( - 'service?', - label=_('Vault service'), - flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'}, - ), - Flag( - 'shared?', - label=_('Shared vault'), - flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'}, - ), - Str( - 'username?', - label=_('Vault user'), - flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'}, - ), - ) - - def get_dn(self, *keys, **options): - """ - Generates vault DN from parameters. - """ - service = options.get('service') - shared = options.get('shared') - user = options.get('username') - - count = (bool(service) + bool(shared) + bool(user)) - if count > 1: - raise errors.MutuallyExclusiveError( - reason=_('Service, shared, and user options ' + - 'cannot be specified simultaneously')) - - # TODO: create container_dn after object initialization then reuse it - container_dn = DN(self.container_dn, self.api.env.basedn) - - dn = super(vault, self).get_dn(*keys, **options) - assert dn.endswith(container_dn) - rdns = DN(*dn[:-len(container_dn)]) - - if not count: - principal = getattr(context, 'principal') - - if principal.startswith('host/'): - raise errors.NotImplementedError( - reason=_('Host is not supported')) - - (name, realm) = split_principal(principal) - if '/' in name: - service = principal - else: - user = name - - if service: - parent_dn = DN(('cn', service), ('cn', 'services'), container_dn) - elif shared: - parent_dn = DN(('cn', 'shared'), container_dn) - elif user: - parent_dn = DN(('cn', user), ('cn', 'users'), container_dn) - else: - raise RuntimeError - - return DN(rdns, parent_dn) - - def create_container(self, dn, owner_dn): - """ - Creates vault container and its parents. - """ - - # TODO: create container_dn after object initialization then reuse it - container_dn = DN(self.container_dn, self.api.env.basedn) - - entries = [] - - while dn: - assert dn.endswith(container_dn) - - rdn = dn[0] - entry = self.backend.make_entry( - dn, - { - 'objectclass': ['ipaVaultContainer'], - 'cn': rdn['cn'], - 'owner': [owner_dn], - }) - - # if entry can be added, return - try: - self.backend.add_entry(entry) - break - - except errors.NotFound: - pass - - # otherwise, create parent entry first - dn = DN(*dn[1:]) - entries.insert(0, entry) - - # then create the entries again - for entry in entries: - self.backend.add_entry(entry) - - def get_key_id(self, dn): - """ - Generates a client key ID to archive/retrieve data in KRA. - """ - - # TODO: create container_dn after object initialization then reuse it - container_dn = DN(self.container_dn, self.api.env.basedn) - - # make sure the DN is a vault DN - if not dn.endswith(container_dn, 1): - raise ValueError('Invalid vault DN: %s' % dn) - - # construct the vault ID from the bottom up - id = u'' - for rdn in dn[:-len(container_dn)]: - name = rdn['cn'] - id = u'/' + name + id - - return 'ipa:' + id - - def get_container_attribute(self, entry, options): - if options.get('raw', False): - return - container_dn = DN(self.container_dn, self.api.env.basedn) - if entry.dn.endswith(DN(('cn', 'services'), container_dn)): - entry['service'] = entry.dn[1]['cn'] - elif entry.dn.endswith(DN(('cn', 'shared'), container_dn)): - entry['shared'] = True - elif entry.dn.endswith(DN(('cn', 'users'), container_dn)): - entry['username'] = entry.dn[1]['cn'] - - -@register() -class vault_add_internal(LDAPCreate): - - NO_CLI = True - - takes_options = LDAPCreate.takes_options + vault_options - - msg_summary = _('Added vault "%(value)s"') - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, - **options): - assert isinstance(dn, DN) - - if not self.api.Command.kra_is_enabled()['result']: - raise errors.InvocationError( - format=_('KRA service is not enabled')) - - principal = getattr(context, 'principal') - (name, realm) = split_principal(principal) - if '/' in name: - owner_dn = self.api.Object.service.get_dn(name) - else: - owner_dn = self.api.Object.user.get_dn(name) - - parent_dn = DN(*dn[1:]) - - try: - self.obj.create_container(parent_dn, owner_dn) - except errors.DuplicateEntry as e: - pass - - # vault should be owned by the creator - entry_attrs['owner'] = owner_dn - - return dn - - def post_callback(self, ldap, dn, entry, *keys, **options): - self.obj.get_container_attribute(entry, options) - return dn - - -@register() -class vault_del(LDAPDelete): - __doc__ = _('Delete a vault.') - - takes_options = LDAPDelete.takes_options + vault_options - - msg_summary = _('Deleted vault "%(value)s"') - - def pre_callback(self, ldap, dn, *keys, **options): - assert isinstance(dn, DN) - - if not self.api.Command.kra_is_enabled()['result']: - raise errors.InvocationError( - format=_('KRA service is not enabled')) - - return dn - - def post_callback(self, ldap, dn, *args, **options): - assert isinstance(dn, DN) - - kra_client = self.api.Backend.kra.get_client() - - kra_account = pki.account.AccountClient(kra_client.connection) - kra_account.login() - - client_key_id = self.obj.get_key_id(dn) - - # deactivate vault record in KRA - response = kra_client.keys.list_keys( - client_key_id, pki.key.KeyClient.KEY_STATUS_ACTIVE) - - for key_info in response.key_infos: - kra_client.keys.modify_key_status( - key_info.get_key_id(), - pki.key.KeyClient.KEY_STATUS_INACTIVE) - - kra_account.logout() - - return True - - -@register() -class vault_find(LDAPSearch): - __doc__ = _('Search for vaults.') - - takes_options = LDAPSearch.takes_options + vault_options + ( - Flag( - 'services?', - doc=_('List all service vaults'), - ), - Flag( - 'users?', - doc=_('List all user vaults'), - ), - ) - - has_output_params = LDAPSearch.has_output_params - - msg_summary = ngettext( - '%(count)d vault matched', - '%(count)d vaults matched', - 0, - ) - - def pre_callback(self, ldap, filter, attrs_list, base_dn, scope, *args, - **options): - assert isinstance(base_dn, DN) - - if not self.api.Command.kra_is_enabled()['result']: - raise errors.InvocationError( - format=_('KRA service is not enabled')) - - if options.get('users') or options.get('services'): - mutex = ['service', 'services', 'shared', 'username', 'users'] - count = sum(bool(options.get(option)) for option in mutex) - if count > 1: - raise errors.MutuallyExclusiveError( - reason=_('Service(s), shared, and user(s) options ' + - 'cannot be specified simultaneously')) - - scope = ldap.SCOPE_SUBTREE - container_dn = DN(self.obj.container_dn, - self.api.env.basedn) - - if options.get('services'): - base_dn = DN(('cn', 'services'), container_dn) - else: - base_dn = DN(('cn', 'users'), container_dn) - else: - base_dn = self.obj.get_dn(None, **options) - - return filter, base_dn, scope - - def post_callback(self, ldap, entries, truncated, *args, **options): - for entry in entries: - self.obj.get_container_attribute(entry, options) - return truncated - - def exc_callback(self, args, options, exc, call_func, *call_args, - **call_kwargs): - if call_func.__name__ == 'find_entries': - if isinstance(exc, errors.NotFound): - # ignore missing containers since they will be created - # automatically on vault creation. - raise errors.EmptyResult(reason=str(exc)) - - raise exc - - -@register() -class vault_mod_internal(LDAPUpdate): - - NO_CLI = True - - takes_options = LDAPUpdate.takes_options + vault_options - - msg_summary = _('Modified vault "%(value)s"') - - def pre_callback(self, ldap, dn, entry_attrs, attrs_list, - *keys, **options): - - assert isinstance(dn, DN) - - if not self.api.Command.kra_is_enabled()['result']: - raise errors.InvocationError( - format=_('KRA service is not enabled')) - - return dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - self.obj.get_container_attribute(entry_attrs, options) - return dn - - -@register() -class vault_show(LDAPRetrieve): - __doc__ = _('Display information about a vault.') - - takes_options = LDAPRetrieve.takes_options + vault_options - - has_output_params = LDAPRetrieve.has_output_params - - def pre_callback(self, ldap, dn, attrs_list, *keys, **options): - assert isinstance(dn, DN) - - if not self.api.Command.kra_is_enabled()['result']: - raise errors.InvocationError( - format=_('KRA service is not enabled')) - - return dn - - def post_callback(self, ldap, dn, entry_attrs, *keys, **options): - self.obj.get_container_attribute(entry_attrs, options) - return dn - - -@register() -class vaultconfig(Object): - __doc__ = _('Vault configuration') - - takes_params = ( - Bytes( - 'transport_cert', - label=_('Transport Certificate'), - ), - ) - - -@register() -class vaultconfig_show(Retrieve): - __doc__ = _('Show vault configuration.') - - takes_options = ( - Str( - 'transport_out?', - doc=_('Output file to store the transport certificate'), - ), - ) - - def execute(self, *args, **options): - - if not self.api.Command.kra_is_enabled()['result']: - raise errors.InvocationError( - format=_('KRA service is not enabled')) - - kra_client = self.api.Backend.kra.get_client() - transport_cert = kra_client.system_certs.get_transport_cert() - return { - 'result': { - 'transport_cert': transport_cert.binary - }, - 'value': None, - } - - -@register() -class vault_archive_internal(PKQuery): - - NO_CLI = True - - takes_options = vault_options + ( - Bytes( - 'session_key', - doc=_('Session key wrapped with transport certificate'), - ), - Bytes( - 'vault_data', - doc=_('Vault data encrypted with session key'), - ), - Bytes( - 'nonce', - doc=_('Nonce'), - ), - ) - - has_output = output.standard_entry - - msg_summary = _('Archived data into vault "%(value)s"') - - def execute(self, *args, **options): - - if not self.api.Command.kra_is_enabled()['result']: - raise errors.InvocationError( - format=_('KRA service is not enabled')) - - wrapped_vault_data = options.pop('vault_data') - nonce = options.pop('nonce') - wrapped_session_key = options.pop('session_key') - - # retrieve vault info - vault = self.api.Command.vault_show(*args, **options)['result'] - - # connect to KRA - kra_client = self.api.Backend.kra.get_client() - - kra_account = pki.account.AccountClient(kra_client.connection) - kra_account.login() - - client_key_id = self.obj.get_key_id(vault['dn']) - - # deactivate existing vault record in KRA - response = kra_client.keys.list_keys( - client_key_id, - pki.key.KeyClient.KEY_STATUS_ACTIVE) - - for key_info in response.key_infos: - kra_client.keys.modify_key_status( - key_info.get_key_id(), - pki.key.KeyClient.KEY_STATUS_INACTIVE) - - # forward wrapped data to KRA - kra_client.keys.archive_encrypted_data( - client_key_id, - pki.key.KeyClient.PASS_PHRASE_TYPE, - wrapped_vault_data, - wrapped_session_key, - None, - nonce, - ) - - kra_account.logout() - - response = { - 'value': args[-1], - 'result': {}, - } - - response['summary'] = self.msg_summary % response - - return response - - -@register() -class vault_retrieve_internal(PKQuery): - - NO_CLI = True - - takes_options = vault_options + ( - Bytes( - 'session_key', - doc=_('Session key wrapped with transport certificate'), - ), - ) - - has_output = output.standard_entry - - msg_summary = _('Retrieved data from vault "%(value)s"') - - def execute(self, *args, **options): - - if not self.api.Command.kra_is_enabled()['result']: - raise errors.InvocationError( - format=_('KRA service is not enabled')) - - wrapped_session_key = options.pop('session_key') - - # retrieve vault info - vault = self.api.Command.vault_show(*args, **options)['result'] - - # connect to KRA - kra_client = self.api.Backend.kra.get_client() - - kra_account = pki.account.AccountClient(kra_client.connection) - kra_account.login() - - client_key_id = self.obj.get_key_id(vault['dn']) - - # find vault record in KRA - response = kra_client.keys.list_keys( - client_key_id, - pki.key.KeyClient.KEY_STATUS_ACTIVE) - - if not len(response.key_infos): - raise errors.NotFound(reason=_('No archived data.')) - - key_info = response.key_infos[0] - - # retrieve encrypted data from KRA - key = kra_client.keys.retrieve_key( - key_info.get_key_id(), - wrapped_session_key) - - kra_account.logout() - - response = { - 'value': args[-1], - 'result': { - 'vault_data': key.encrypted_data, - 'nonce': key.nonce_data, - }, - } - - response['summary'] = self.msg_summary % response - - return response - - -@register() -class vault_add_owner(VaultModMember, LDAPAddMember): - __doc__ = _('Add owners to a vault.') - - takes_options = LDAPAddMember.takes_options + vault_options - - member_attributes = ['owner'] - member_param_label = _('owner %s') - member_count_out = ('%i owner added.', '%i owners added.') - - has_output = ( - output.Entry('result'), - output.Output( - 'failed', - type=dict, - doc=_('Owners that could not be added'), - ), - output.Output( - 'completed', - type=int, - doc=_('Number of owners added'), - ), - ) - - -@register() -class vault_remove_owner(VaultModMember, LDAPRemoveMember): - __doc__ = _('Remove owners from a vault.') - - takes_options = LDAPRemoveMember.takes_options + vault_options - - member_attributes = ['owner'] - member_param_label = _('owner %s') - member_count_out = ('%i owner removed.', '%i owners removed.') - - has_output = ( - output.Entry('result'), - output.Output( - 'failed', - type=dict, - doc=_('Owners that could not be removed'), - ), - output.Output( - 'completed', - type=int, - doc=_('Number of owners removed'), - ), - ) - - -@register() -class vault_add_member(VaultModMember, LDAPAddMember): - __doc__ = _('Add members to a vault.') - - takes_options = LDAPAddMember.takes_options + vault_options - - -@register() -class vault_remove_member(VaultModMember, LDAPRemoveMember): - __doc__ = _('Remove members from a vault.') - - takes_options = LDAPRemoveMember.takes_options + vault_options - - -@register() -class kra_is_enabled(Command): - NO_CLI = True - - has_output = output.standard_value - - def execute(self, *args, **options): - base_dn = DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'), - self.api.env.basedn) - filter = '(&(objectClass=ipaConfigObject)(cn=KRA))' - try: - self.api.Backend.ldap2.find_entries( - base_dn=base_dn, filter=filter, attrs_list=[]) - except errors.NotFound: - result = False - else: - result = True - return dict(result=result, value=pkey_to_value(None, options)) diff --git a/ipalib/plugins/virtual.py b/ipalib/plugins/virtual.py deleted file mode 100644 index 2ba69f651..000000000 --- a/ipalib/plugins/virtual.py +++ /dev/null @@ -1,68 +0,0 @@ -# Authors: -# Rob Crittenden <rcritten@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/>. - -""" -Base classes for non-LDAP backend plugins. -""" -from ipalib import Command -from ipalib import errors -from ipapython.dn import DN -from ipalib.text import _ - -class VirtualCommand(Command): - """ - A command that doesn't use the LDAP backend but wants to use the - LDAP access control system to make authorization decisions. - - The class variable operation is the commonName attribute of the - entry to be tested against. - - In advance, you need to create an entry of the form: - cn=<operation>, api.env.container_virtual, api.env.basedn - - Ex. - cn=request certificate, cn=virtual operations,cn=etc, dc=example, dc=com - """ - operation = None - - def check_access(self, operation=None): - """ - Perform an LDAP query to determine authorization. - - This should be executed before any actual work is done. - """ - if self.operation is None and operation is None: - raise errors.ACIError(info=_('operation not defined')) - - if operation is None: - operation = self.operation - - ldap = self.api.Backend.ldap2 - self.log.debug("IPA: virtual verify %s" % operation) - - operationdn = DN(('cn', operation), self.api.env.container_virtual, self.api.env.basedn) - - try: - if not ldap.can_write(operationdn, "objectclass"): - raise errors.ACIError( - info=_('not allowed to perform operation: %s') % operation) - except errors.NotFound: - raise errors.ACIError(info=_('No such virtual command')) - - return True diff --git a/ipalib/setup.py.in b/ipalib/setup.py.in index 8107c81ff..3bc6b7590 100644 --- a/ipalib/setup.py.in +++ b/ipalib/setup.py.in @@ -59,9 +59,7 @@ def setup_package(): classifiers=[line for line in CLASSIFIERS.split('\n') if line], platforms = ["Linux", "Solaris", "Unix"], package_dir = {'ipalib': ''}, - packages = ["ipalib", - "ipalib.plugins", - ], + packages = ["ipalib"], ) finally: del sys.path[0] |