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>
This commit is contained in:
Jan Cholasta
2016-04-28 10:30:05 +02:00
parent ec841e5d7a
commit 6e44557b60
103 changed files with 65 additions and 80 deletions

986
ipaserver/plugins/aci.py Normal file
View File

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

View File

@@ -0,0 +1,802 @@
# 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))

View File

@@ -0,0 +1,841 @@
# 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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,663 @@
# 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']

143
ipaserver/plugins/batch.py Normal file
View File

@@ -0,0 +1,143 @@
# 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)

562
ipaserver/plugins/caacl.py Normal file
View File

@@ -0,0 +1,562 @@
#
# 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.'))

835
ipaserver/plugins/cert.py Normal file
View File

@@ -0,0 +1,835 @@
# 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))

View File

@@ -0,0 +1,335 @@
#
# 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

358
ipaserver/plugins/config.py Normal file
View File

@@ -0,0 +1,358 @@
# 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.')

View File

@@ -0,0 +1,226 @@
# 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),
)

4396
ipaserver/plugins/dns.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -244,19 +244,21 @@ import json
from lxml import etree
import time
import pki
from pki.client import PKIConnection
import pki.crypto as cryptoutil
from pki.kra import KRAClient
import six
from six.moves import urllib
from ipalib import Backend
from ipalib import Backend, api
from ipapython.dn import DN
import ipapython.cookie
from ipapython import dogtag
from ipapython import ipautil
if api.env.in_server:
import pki
from pki.client import PKIConnection
import pki.crypto as cryptoutil
from pki.kra import KRAClient
if six.PY3:
unicode = str
@@ -1269,7 +1271,7 @@ def select_any_master(ldap2, service='CA'):
#-------------------------------------------------------------------------------
from ipalib import Registry, api, errors, SkipPluginModule
from ipalib import Registry, errors, SkipPluginModule
if api.env.ra_plugin != 'dogtag':
# In this case, abort loading this plugin module...
raise SkipPluginModule(reason='dogtag not selected as RA plugin')

View File

@@ -0,0 +1,137 @@
#
# 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'])}

690
ipaserver/plugins/group.py Normal file
View File

@@ -0,0 +1,690 @@
# 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),
)

View File

@@ -0,0 +1,7 @@
#
# Copyright (C) 2016 FreeIPA Contributors see COPYING for license
#
from ipalib.text import _
__doc__ = _('Host-based access control commands')

View File

@@ -0,0 +1,605 @@
# 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.')

View File

@@ -0,0 +1,152 @@
# 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.')

View File

@@ -0,0 +1,176 @@
# 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.')

View File

@@ -0,0 +1,499 @@
# 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

1284
ipaserver/plugins/host.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,316 @@
# 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)

View File

@@ -0,0 +1,769 @@
# 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

1123
ipaserver/plugins/idviews.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,859 @@
# 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 &lt;database path&gt;</code> </li> <li>Create a CSR with subject <em>CN=&lt;${cn_name}&gt;,O=&lt;realm&gt;</em>, for example:<br/> <code># certutil -R -d &lt;database path&gt; -a -g &lt;key size&gt; -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))

View File

@@ -49,6 +49,8 @@ def validate_host(ugettext, cn):
class join(Command):
"""Join an IPA domain"""
NO_CLI = True
takes_args = (
Str('cn',
validate_host,

View File

@@ -0,0 +1,243 @@
# 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))

View File

@@ -0,0 +1,920 @@
# 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)

138
ipaserver/plugins/misc.py Normal file
View File

@@ -0,0 +1,138 @@
# 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),
)

View File

@@ -0,0 +1,387 @@
# 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')

7
ipaserver/plugins/otp.py Normal file
View File

@@ -0,0 +1,7 @@
#
# Copyright (C) 2016 FreeIPA Contributors see COPYING for license
#
from ipalib.text import _
__doc__ = _('One time password commands')

View File

@@ -0,0 +1,121 @@
# 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.')

View File

@@ -0,0 +1,464 @@
# 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']

139
ipaserver/plugins/passwd.py Normal file
View File

@@ -0,0 +1,139 @@
# 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,
)

File diff suppressed because it is too large Load Diff

70
ipaserver/plugins/ping.py Normal file
View File

@@ -0,0 +1,70 @@
# 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))

105
ipaserver/plugins/pkinit.py Normal file
View File

@@ -0,0 +1,105 @@
# 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)

View File

@@ -0,0 +1,251 @@
# 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'),
),
)

View File

@@ -0,0 +1,611 @@
# 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

View File

@@ -0,0 +1,175 @@
# 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.')

View File

@@ -0,0 +1,340 @@
# 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.')

252
ipaserver/plugins/role.py Normal file
View File

@@ -0,0 +1,252 @@
# 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'),
),
)

660
ipaserver/plugins/schema.py Normal file
View File

@@ -0,0 +1,660 @@
#
# 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)

View File

@@ -0,0 +1,224 @@
# 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),
)

View File

@@ -0,0 +1,569 @@
# 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.')

260
ipaserver/plugins/server.py Normal file
View File

@@ -0,0 +1,260 @@
#
# 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

View File

@@ -0,0 +1,889 @@
# 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

View File

@@ -0,0 +1,550 @@
#
# 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',
}

View File

@@ -0,0 +1,33 @@
#
# 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)

View File

@@ -0,0 +1,745 @@
# 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")

View File

@@ -0,0 +1,7 @@
#
# Copyright (C) 2016 FreeIPA Contributors see COPYING for license
#
from ipalib.text import _
__doc__ = _('commands for controlling sudo configuration')

View File

@@ -0,0 +1,203 @@
# 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.')

View File

@@ -0,0 +1,195 @@
# 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.')

View File

@@ -0,0 +1,998 @@
# 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))

View File

@@ -0,0 +1,503 @@
#
# 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
},
)

1725
ipaserver/plugins/trust.py Normal file

File diff suppressed because it is too large Load Diff

1151
ipaserver/plugins/user.py Normal file

File diff suppressed because it is too large Load Diff

1215
ipaserver/plugins/vault.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,68 @@
# 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