mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2025-02-25 18:55:28 -06:00
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:
986
ipaserver/plugins/aci.py
Normal file
986
ipaserver/plugins/aci.py
Normal 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),
|
||||
)
|
||||
802
ipaserver/plugins/automember.py
Normal file
802
ipaserver/plugins/automember.py
Normal 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))
|
||||
841
ipaserver/plugins/automount.py
Normal file
841
ipaserver/plugins/automount.py
Normal 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
|
||||
2397
ipaserver/plugins/baseldap.py
Normal file
2397
ipaserver/plugins/baseldap.py
Normal file
File diff suppressed because it is too large
Load Diff
663
ipaserver/plugins/baseuser.py
Normal file
663
ipaserver/plugins/baseuser.py
Normal 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
143
ipaserver/plugins/batch.py
Normal 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
562
ipaserver/plugins/caacl.py
Normal 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
835
ipaserver/plugins/cert.py
Normal 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))
|
||||
335
ipaserver/plugins/certprofile.py
Normal file
335
ipaserver/plugins/certprofile.py
Normal 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
358
ipaserver/plugins/config.py
Normal 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.')
|
||||
|
||||
226
ipaserver/plugins/delegation.py
Normal file
226
ipaserver/plugins/delegation.py
Normal 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
4396
ipaserver/plugins/dns.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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')
|
||||
|
||||
137
ipaserver/plugins/domainlevel.py
Normal file
137
ipaserver/plugins/domainlevel.py
Normal 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
690
ipaserver/plugins/group.py
Normal 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),
|
||||
)
|
||||
|
||||
7
ipaserver/plugins/hbac.py
Normal file
7
ipaserver/plugins/hbac.py
Normal file
@@ -0,0 +1,7 @@
|
||||
#
|
||||
# Copyright (C) 2016 FreeIPA Contributors see COPYING for license
|
||||
#
|
||||
|
||||
from ipalib.text import _
|
||||
|
||||
__doc__ = _('Host-based access control commands')
|
||||
605
ipaserver/plugins/hbacrule.py
Normal file
605
ipaserver/plugins/hbacrule.py
Normal 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.')
|
||||
|
||||
152
ipaserver/plugins/hbacsvc.py
Normal file
152
ipaserver/plugins/hbacsvc.py
Normal 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.')
|
||||
|
||||
176
ipaserver/plugins/hbacsvcgroup.py
Normal file
176
ipaserver/plugins/hbacsvcgroup.py
Normal 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.')
|
||||
|
||||
499
ipaserver/plugins/hbactest.py
Normal file
499
ipaserver/plugins/hbactest.py
Normal 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
1284
ipaserver/plugins/host.py
Normal file
File diff suppressed because it is too large
Load Diff
316
ipaserver/plugins/hostgroup.py
Normal file
316
ipaserver/plugins/hostgroup.py
Normal 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)
|
||||
|
||||
769
ipaserver/plugins/idrange.py
Normal file
769
ipaserver/plugins/idrange.py
Normal 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
1123
ipaserver/plugins/idviews.py
Normal file
File diff suppressed because it is too large
Load Diff
859
ipaserver/plugins/internal.py
Normal file
859
ipaserver/plugins/internal.py
Normal 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 <database path></code> </li> <li>Create a CSR with subject <em>CN=<${cn_name}>,O=<realm></em>, for example:<br/> <code># certutil -R -d <database path> -a -g <key size> -s 'CN=${cn},O=${realm}'</code> </li> <li> Copy and paste the CSR (from <em>-----BEGIN NEW CERTIFICATE REQUEST-----</em> to <em>-----END NEW CERTIFICATE REQUEST-----</em>) into the text area below: </li> </ol>"),
|
||||
"requested": _("Certificate requested"),
|
||||
"restore_certificate": _("Restore Certificate for ${entity} ${primary_key}"),
|
||||
"restore_certificate_simple": _("Restore Certificate"),
|
||||
"restore_confirmation": _("To confirm your intention to restore this certificate, click the \"Restore\" button."),
|
||||
"restored": _("Certificate restored"),
|
||||
"revocation_reason": _("Revocation reason"),
|
||||
"revoke_certificate": _("Revoke Certificate for ${entity} ${primary_key}"),
|
||||
"revoke_certificate_simple": _("Revoke Certificate"),
|
||||
"revoke_confirmation": _("To confirm your intention to revoke this certificate, select a reason from the pull-down list, and click the \"Revoke\" button."),
|
||||
"revoked": _("Certificate Revoked"),
|
||||
"serial_number": _("Serial Number"),
|
||||
"serial_number_hex": _("Serial Number (hex)"),
|
||||
"sha1_fingerprint": _("SHA1 Fingerprint"),
|
||||
"status": _("Status"),
|
||||
"superseded": _("Superseded"),
|
||||
"unspecified": _("Unspecified"),
|
||||
"valid": _("Valid Certificate Present"),
|
||||
"validity": _("Validity"),
|
||||
"view_certificate": _("Certificate for ${entity} ${primary_key}"),
|
||||
"view_certificate_btn": _("View Certificate"),
|
||||
},
|
||||
"config": {
|
||||
"group": _("Group Options"),
|
||||
"search": _("Search Options"),
|
||||
"selinux": _("SELinux Options"),
|
||||
"service": _("Service Options"),
|
||||
"user": _("User Options"),
|
||||
},
|
||||
"delegation": {
|
||||
},
|
||||
"dnsconfig": {
|
||||
"forward_first": _("Forward first"),
|
||||
"forward_none": _("Forwarding disabled"),
|
||||
"forward_only": _("Forward only"),
|
||||
"options": _("Options"),
|
||||
},
|
||||
"dnsrecord": {
|
||||
"data": _("Data"),
|
||||
"deleted_no_data": _("DNS record was deleted because it contained no data."),
|
||||
"other": _("Other Record Types"),
|
||||
"ptr_redir_address_err": _("Address not valid, can't redirect"),
|
||||
"ptr_redir_create": _("Create dns record"),
|
||||
"ptr_redir_creating": _("Creating record."),
|
||||
"ptr_redir_creating_err": _("Record creation failed."),
|
||||
"ptr_redir_record": _("Checking if record exists."),
|
||||
"ptr_redir_record_err": _("Record not found."),
|
||||
"ptr_redir_title": _("Redirection to PTR record"),
|
||||
"ptr_redir_zone": _("Zone found: ${zone}"),
|
||||
"ptr_redir_zone_err": _("Target reverse zone not found."),
|
||||
"ptr_redir_zones": _("Fetching DNS zones."),
|
||||
"ptr_redir_zones_err": _("An error occurred while fetching dns zones."),
|
||||
"redirection_dnszone": _("You will be redirected to DNS Zone."),
|
||||
"standard": _("Standard Record Types"),
|
||||
"title": _("Records for DNS Zone"),
|
||||
"type": _("Record Type"),
|
||||
},
|
||||
"dnszone": {
|
||||
"identity": _("DNS Zone Settings"),
|
||||
"add_permission":_("Add Permission"),
|
||||
"add_permission_confirm":_("Are you sure you want to add permission for DNS Zone ${object}?"),
|
||||
"remove_permission": _("Remove Permission"),
|
||||
"remove_permission_confirm": _("Are you sure you want to remove permission for DNS Zone ${object}?"),
|
||||
"skip_dns_check": _("Skip DNS check"),
|
||||
"skip_overlap_check": _("Skip overlap check"),
|
||||
"soamname_change_message": _("Do you want to check if new authoritative nameserver address is in DNS"),
|
||||
"soamname_change_title": _("Authoritative nameserver change"),
|
||||
},
|
||||
"domainlevel": {
|
||||
"label": _("Domain Level"),
|
||||
"label_singular": _("Domain Level"),
|
||||
"ipadomainlevel": _("Level"),
|
||||
"set": _("Set Domain Level"),
|
||||
},
|
||||
"group": {
|
||||
"details": _("Group Settings"),
|
||||
"external": _("External"),
|
||||
"make_external": _("Change to external group"),
|
||||
"make_posix": _("Change to POSIX group"),
|
||||
"normal": _("Normal"),
|
||||
"posix": _("POSIX"),
|
||||
"type": _("Group Type"),
|
||||
},
|
||||
"hbacrule": {
|
||||
"any_host": _("Any Host"),
|
||||
"any_service": _("Any Service"),
|
||||
"anyone": _("Anyone"),
|
||||
"host": _("Accessing"),
|
||||
"ipaenabledflag": _("Rule status"),
|
||||
"service": _("Via Service"),
|
||||
"specified_hosts": _("Specified Hosts and Groups"),
|
||||
"specified_services": _("Specified Services and Groups"),
|
||||
"specified_users": _("Specified Users and Groups"),
|
||||
"user": _("Who"),
|
||||
},
|
||||
"hbacsvc": {
|
||||
},
|
||||
"hbacsvcgroup": {
|
||||
"services": _("Services"),
|
||||
},
|
||||
"hbactest": {
|
||||
"access_denied": _("Access Denied"),
|
||||
"access_granted": _("Access Granted"),
|
||||
"include_disabled": _("Include Disabled"),
|
||||
"include_enabled": _("Include Enabled"),
|
||||
"label": _("HBAC Test"),
|
||||
"matched": _("Matched"),
|
||||
"missing_values": _("Missing values: "),
|
||||
"new_test": _("New Test"),
|
||||
"rules": _("Rules"),
|
||||
"run_test": _("Run Test"),
|
||||
"specify_external": _("Specify external ${entity}"),
|
||||
"unmatched": _("Unmatched"),
|
||||
},
|
||||
"host": {
|
||||
"certificate": _("Host Certificate"),
|
||||
"cn": _("Host Name"),
|
||||
"delete_key_unprovision": _("Delete Key, Unprovision"),
|
||||
"details": _("Host Settings"),
|
||||
"enrolled": _("Enrolled"),
|
||||
"enrollment": _("Enrollment"),
|
||||
"fqdn": _("Fully Qualified Host Name"),
|
||||
"generate_otp": _("Generate OTP"),
|
||||
"generated_otp": _("Generated OTP"),
|
||||
"keytab": _("Kerberos Key"),
|
||||
"keytab_missing": _("Kerberos Key Not Present"),
|
||||
"keytab_present": _("Kerberos Key Present, Host Provisioned"),
|
||||
"password": _("One-Time-Password"),
|
||||
"password_missing": _("One-Time-Password Not Present"),
|
||||
"password_present": _("One-Time-Password Present"),
|
||||
"password_reset_button": _("Reset OTP"),
|
||||
"password_reset_title": _("Reset One-Time-Password"),
|
||||
"password_set_button": _("Set OTP"),
|
||||
"password_set_success": _("OTP set"),
|
||||
"password_set_title": _("Set One-Time-Password"),
|
||||
"status": _("Status"),
|
||||
"unprovision": _("Unprovision"),
|
||||
"unprovision_confirmation": _("Are you sure you want to unprovision this host?"),
|
||||
"unprovision_title": _("Unprovisioning ${entity}"),
|
||||
"unprovisioned": _("Host unprovisioned"),
|
||||
},
|
||||
"hostgroup": {
|
||||
"identity": _("Host Group Settings"),
|
||||
},
|
||||
"idoverrideuser": {
|
||||
"anchor_label": _("User to override"),
|
||||
"anchor_tooltip": _("Enter trusted or IPA user login. Note: search doesn't list users from trusted domains."),
|
||||
"anchor_tooltip_ad": _("Enter trusted user login."),
|
||||
},
|
||||
"idoverridegroup": {
|
||||
"anchor_label": _("Group to override"),
|
||||
"anchor_tooltip": _("Enter trusted or IPA group name. Note: search doesn't list groups from trusted domains."),
|
||||
"anchor_tooltip_ad": _("Enter trusted group name."),
|
||||
},
|
||||
"idview": {
|
||||
"appliesto_tab": _("${primary_key} applies to:"),
|
||||
"appliedtohosts": _("Applied to hosts"),
|
||||
"appliedtohosts_title": _("Applied to hosts"),
|
||||
"apply_hostgroups": _("Apply to host groups"),
|
||||
"apply_hostgroups_title": _("Apply ID View ${primary_key} on hosts of ${entity}"),
|
||||
"apply_hosts": _("Apply to hosts"),
|
||||
"apply_hosts_title": _("Apply ID view ${primary_key} on ${entity}"),
|
||||
"ipaassignedidview": _("Assigned ID View"),
|
||||
"overrides_tab": _("${primary_key} overrides:"),
|
||||
"unapply_hostgroups": _("Un-apply from host groups"),
|
||||
"unapply_hostgroups_all_title": _("Un-apply ID Views from hosts of hostgroups"),
|
||||
"unapply_hostgroups_title": _("Un-apply ID View ${primary_key} from hosts of ${entity}"),
|
||||
"unapply_hosts": _("Un-apply"),
|
||||
"unapply_hosts_all": _("Un-apply from hosts"),
|
||||
"unapply_hosts_all_title": _("Un-apply ID Views from hosts"),
|
||||
"unapply_hosts_confirm": _("Are you sure you want to un-apply ID view from selected entries?"),
|
||||
"unapply_hosts_title": _("Un-apply ID View ${primary_key} from hosts"),
|
||||
},
|
||||
"krbtpolicy": {
|
||||
"identity": _("Kerberos Ticket Policy"),
|
||||
},
|
||||
"netgroup": {
|
||||
"any_host": _("Any Host"),
|
||||
"anyone": _("Anyone"),
|
||||
"external": _("External"),
|
||||
"host": _("Host"),
|
||||
"hostgroups": _("Host Groups"),
|
||||
"hosts": _("Hosts"),
|
||||
"identity": _("Netgroup Settings"),
|
||||
"specified_hosts": _("Specified Hosts and Groups"),
|
||||
"specified_users": _("Specified Users and Groups"),
|
||||
"user": _("User"),
|
||||
"usergroups": _("User Groups"),
|
||||
"users": _("Users"),
|
||||
},
|
||||
"otptoken": {
|
||||
"add_token": _("Add OTP Token"),
|
||||
"app_link": _("You can use <a href=\"${link}\" target=\"_blank\">FreeOTP<a/> as a software OTP token application."),
|
||||
"config_title": _("Configure your token"),
|
||||
"config_instructions": _("Configure your token by scanning the QR code below. Click on the QR code if you see this on the device you want to configure."),
|
||||
"details": _("OTP Token Settings"),
|
||||
"disable": _("Disable token"),
|
||||
"enable": _("Enable token"),
|
||||
"show_qr": _("Show QR code"),
|
||||
"show_uri": _("Show configuration uri"),
|
||||
"type_hotp": _("Counter-based (HOTP)"),
|
||||
"type_totp": _("Time-based (TOTP)"),
|
||||
},
|
||||
"permission": {
|
||||
"add_custom_attr": _("Add custom attribute"),
|
||||
"attribute": _("Attribute"),
|
||||
"filter": _("Filter"),
|
||||
"identity": _("Permission settings"),
|
||||
"managed": _("Attribute breakdown"),
|
||||
"target": _("Target"),
|
||||
},
|
||||
"privilege": {
|
||||
"identity": _("Privilege Settings"),
|
||||
},
|
||||
"pwpolicy": {
|
||||
"identity": _("Password Policy"),
|
||||
},
|
||||
"idrange": {
|
||||
"details": _("Range Settings"),
|
||||
"ipabaseid": _("Base ID"),
|
||||
"ipabaserid": _("Primary RID base"),
|
||||
"ipaidrangesize": _("Range size"),
|
||||
"ipanttrusteddomainsid": _("Domain SID"),
|
||||
"ipasecondarybaserid": _("Secondary RID base"),
|
||||
"type": _("Range type"),
|
||||
"type_ad": _("Active Directory domain"),
|
||||
"type_ad_posix": _("Active Directory domain with POSIX attributes"),
|
||||
"type_detect": _("Detect"),
|
||||
"type_local": _("Local domain"),
|
||||
"type_ipa": _("IPA trust"),
|
||||
"type_winsync": _("Active Directory winsync"),
|
||||
},
|
||||
"radiusproxy": {
|
||||
"details": _("RADIUS Proxy Server Settings"),
|
||||
},
|
||||
"realmdomains": {
|
||||
"identity": _("Realm Domains"),
|
||||
"check_dns": _("Check DNS"),
|
||||
"check_dns_confirmation": _("Do you also want to perform DNS check?"),
|
||||
"force_update": _("Force Update"),
|
||||
},
|
||||
"role": {
|
||||
"identity": _("Role Settings"),
|
||||
},
|
||||
"selfservice": {
|
||||
},
|
||||
"selinuxusermap": {
|
||||
"any_host": _("Any Host"),
|
||||
"anyone": _("Anyone"),
|
||||
"host": _("Host"),
|
||||
"specified_hosts": _("Specified Hosts and Groups"),
|
||||
"specified_users": _("Specified Users and Groups"),
|
||||
"user": _("User"),
|
||||
},
|
||||
"service": {
|
||||
"certificate": _("Service Certificate"),
|
||||
"delete_key_unprovision": _("Delete Key, Unprovision"),
|
||||
"details": _("Service Settings"),
|
||||
"host": _("Host Name"),
|
||||
"missing": _("Kerberos Key Not Present"),
|
||||
"provisioning": _("Provisioning"),
|
||||
"service": _("Service"),
|
||||
"status": _("Status"),
|
||||
"unprovision": _("Unprovision"),
|
||||
"unprovision_confirmation": _("Are you sure you want to unprovision this service?"),
|
||||
"unprovision_title": _("Unprovisioning ${entity}"),
|
||||
"unprovisioned": _("Service unprovisioned"),
|
||||
"valid": _("Kerberos Key Present, Service Provisioned"),
|
||||
},
|
||||
"sshkeystore": {
|
||||
"keys": _("SSH public keys"),
|
||||
"set_dialog_help": _("SSH public key:"),
|
||||
"set_dialog_title": _("Set SSH key"),
|
||||
"show_set_key": _("Show/Set key"),
|
||||
"status_mod_ns": _("Modified: key not set"),
|
||||
"status_mod_s": _("Modified"),
|
||||
"status_new_ns": _("New: key not set"),
|
||||
"status_new_s": _("New: key set"),
|
||||
},
|
||||
"stageuser": {
|
||||
"activate_confirm": _("Are you sure you want to activate selected users?"),
|
||||
"activate_one_confirm": _("Are you sure you want to activate ${object}?"),
|
||||
"activate_success": _("${count} user(s) activated"),
|
||||
"label": _("Stage users"),
|
||||
"preserved_label": _("Preserved users"),
|
||||
"undel_confirm": _("Are you sure you want to restore selected users?"),
|
||||
"undel_success": _("${count} user(s) restored"),
|
||||
"user_categories": _("User categories"),
|
||||
},
|
||||
"sudocmd": {
|
||||
"groups": _("Groups"),
|
||||
},
|
||||
"sudocmdgroup": {
|
||||
"commands": _("Commands"),
|
||||
},
|
||||
"sudorule": {
|
||||
"allow": _("Allow"),
|
||||
"any_command": _("Any Command"),
|
||||
"any_group": _("Any Group"),
|
||||
"any_host": _("Any Host"),
|
||||
"anyone": _("Anyone"),
|
||||
"command": _("Run Commands"),
|
||||
"deny": _("Deny"),
|
||||
"external": _("External"),
|
||||
"host": _("Access this host"),
|
||||
"ipaenabledflag": _("Rule status"),
|
||||
"option_added": _("Option added"),
|
||||
"option_removed": _("${count} option(s) removed"),
|
||||
"options": _("Options"),
|
||||
"runas": _("As Whom"),
|
||||
"specified_commands": _("Specified Commands and Groups"),
|
||||
"specified_groups": _("Specified Groups"),
|
||||
"specified_hosts": _("Specified Hosts and Groups"),
|
||||
"specified_users": _("Specified Users and Groups"),
|
||||
"user": _("Who"),
|
||||
},
|
||||
"topology": {
|
||||
"segment_details": _("Segment details"),
|
||||
"replication_config": _("Replication configuration"),
|
||||
"insufficient_domain_level" : _("Managed topology requires minimal domain level ${domainlevel}"),
|
||||
},
|
||||
"trust": {
|
||||
"account": _("Account"),
|
||||
"admin_account": _("Administrative account"),
|
||||
"blacklists": _("SID blacklists"),
|
||||
"details": _("Trust Settings"),
|
||||
"domain": _("Domain"),
|
||||
"establish_using": _("Establish using"),
|
||||
"fetch_domains": _("Fetch domains"),
|
||||
"ipantflatname": _("Domain NetBIOS name"),
|
||||
"ipanttrusteddomainsid": _("Domain Security Identifier"),
|
||||
"preshared_password": _("Pre-shared password"),
|
||||
"trustdirection": _("Trust direction"),
|
||||
"truststatus": _("Trust status"),
|
||||
"trusttype": _("Trust type"),
|
||||
},
|
||||
"trustconfig": {
|
||||
"options": _("Options"),
|
||||
},
|
||||
"user": {
|
||||
"account": _("Account Settings"),
|
||||
"account_status": _("Account Status"),
|
||||
"activeuser_label": _("Active users"),
|
||||
"contact": _("Contact Settings"),
|
||||
"delete_mode": _("Delete mode"),
|
||||
"employee": _("Employee Information"),
|
||||
"error_changing_status": _("Error changing account status"),
|
||||
"krbpasswordexpiration": _("Password expiration"),
|
||||
"mailing": _("Mailing Address"),
|
||||
"misc": _("Misc. Information"),
|
||||
"mode_delete": _("delete"),
|
||||
"mode_preserve": _("preserve"),
|
||||
"noprivate": _("No private group"),
|
||||
"status_confirmation": _("Are you sure you want to ${action} the user?<br/>The change will take effect immediately."),
|
||||
"status_link": _("Click to ${action}"),
|
||||
"unlock": _("Unlock"),
|
||||
"unlock_confirm": _("Are you sure you want to unlock user ${object}?"),
|
||||
},
|
||||
},
|
||||
"password": {
|
||||
"current_password": _("Current Password"),
|
||||
"current_password_required": _("Current password is required"),
|
||||
"expires_in": _("Your password expires in ${days} days."),
|
||||
"first_otp": _("First OTP"),
|
||||
"invalid_password": _("The password or username you entered is incorrect."),
|
||||
"new_password": _("New Password"),
|
||||
"new_password_required": _("New password is required"),
|
||||
"otp": _("OTP"),
|
||||
"otp_info": _("<i class=\"fa fa-info-circle\"></i> <strong>One-Time-Password(OTP):</strong> Generate new OTP code for each OTP field."),
|
||||
"otp_long": _("One-Time-Password"),
|
||||
"otp_sync_fail": _("Token synchronization failed"),
|
||||
"otp_sync_invalid": _("The username, password or token codes are not correct"),
|
||||
"otp_sync_success":_("Token was synchronized"),
|
||||
"password": _("Password"),
|
||||
"password_and_otp": _("Password or Password+One-Time-Password"),
|
||||
"password_change_complete": _("Password change complete"),
|
||||
"password_must_match": _("Passwords must match"),
|
||||
"reset_failure": _("Password reset was not successful."),
|
||||
"reset_password": _("Reset Password"),
|
||||
"reset_password_sentence": _("Reset your password."),
|
||||
"second_otp": _("Second OTP"),
|
||||
"token_id": _("Token ID"),
|
||||
"verify_password": _("Verify Password"),
|
||||
},
|
||||
"search": {
|
||||
"delete_confirm": _("Are you sure you want to delete selected entries?"),
|
||||
"deleted": _("${count} item(s) deleted"),
|
||||
"disable_confirm": _("Are you sure you want to disable selected entries?"),
|
||||
"disabled": _("${count} item(s) disabled"),
|
||||
"enable_confirm": _("Are you sure you want to enable selected entries?"),
|
||||
"enabled": _("${count} item(s) enabled"),
|
||||
"partial_delete": _("Some entries were not deleted"),
|
||||
"placeholder": _("Search"),
|
||||
"quick_links": _("Quick Links"),
|
||||
"select_all": _("Select All"),
|
||||
"truncated": _("Query returned more results than the configured size limit. Displaying the first ${counter} results."),
|
||||
"unselect_all": _("Unselect All"),
|
||||
},
|
||||
"status": {
|
||||
"disable": _("Disable"),
|
||||
"disabled": _("Disabled"),
|
||||
"enable": _("Enable"),
|
||||
"enabled": _("Enabled"),
|
||||
"label": _("Status"),
|
||||
"working": _("Working"),
|
||||
},
|
||||
"tabs": {
|
||||
"audit": _("Audit"),
|
||||
"authentication": _("Authentication"),
|
||||
"automember": _("Automember"),
|
||||
"automount": _("Automount"),
|
||||
"cert": _("Certificates"),
|
||||
"dns": _("DNS"),
|
||||
"hbac": _("Host Based Access Control"),
|
||||
"identity": _("Identity"),
|
||||
"ipaserver": _("IPA Server"),
|
||||
"network_services": _("Network Services"),
|
||||
"policy": _("Policy"),
|
||||
"role": _("Role Based Access Control"),
|
||||
"sudo": _("Sudo"),
|
||||
"topology": _("Topology"),
|
||||
"trust": _("Trusts"),
|
||||
},
|
||||
"true": _("True"),
|
||||
"widget": {
|
||||
"first": _("First"),
|
||||
"last": _("Last"),
|
||||
"next": _("Next"),
|
||||
"page": _("Page"),
|
||||
"prev": _("Prev"),
|
||||
"undo": _("Undo"),
|
||||
"undo_title": _("Undo this change."),
|
||||
"undo_all": _("Undo All"),
|
||||
"undo_all_title": _("Undo all changes in this field."),
|
||||
"validation": {
|
||||
"error": _("Text does not match field pattern"),
|
||||
"datetime": _("Must be an UTC date/time value (e.g., \"2014-01-20 17:58:01Z\")"),
|
||||
"decimal": _("Must be a decimal number"),
|
||||
"format": _("Format error"),
|
||||
"integer": _("Must be an integer"),
|
||||
"ip_address": _('Not a valid IP address'),
|
||||
"ip_v4_address": _('Not a valid IPv4 address'),
|
||||
"ip_v6_address": _('Not a valid IPv6 address'),
|
||||
"max_value": _("Maximum value is ${value}"),
|
||||
"min_value": _("Minimum value is ${value}"),
|
||||
"net_address": _("Not a valid network address (examples: 2001:db8::/64, 192.0.2.0/24)"),
|
||||
"parse": _("Parse error"),
|
||||
"port": _("'${port}' is not a valid port"),
|
||||
"required": _("Required field"),
|
||||
"unsupported": _("Unsupported value"),
|
||||
},
|
||||
},
|
||||
}
|
||||
has_output = (
|
||||
Output('texts', dict, doc=_('Dict of I18N messages')),
|
||||
)
|
||||
def execute(self, **options):
|
||||
return dict(texts=json_serialize(self.messages))
|
||||
@@ -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,
|
||||
|
||||
243
ipaserver/plugins/krbtpolicy.py
Normal file
243
ipaserver/plugins/krbtpolicy.py
Normal 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))
|
||||
920
ipaserver/plugins/migration.py
Normal file
920
ipaserver/plugins/migration.py
Normal 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
138
ipaserver/plugins/misc.py
Normal 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),
|
||||
)
|
||||
387
ipaserver/plugins/netgroup.py
Normal file
387
ipaserver/plugins/netgroup.py
Normal 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
7
ipaserver/plugins/otp.py
Normal file
@@ -0,0 +1,7 @@
|
||||
#
|
||||
# Copyright (C) 2016 FreeIPA Contributors see COPYING for license
|
||||
#
|
||||
|
||||
from ipalib.text import _
|
||||
|
||||
__doc__ = _('One time password commands')
|
||||
121
ipaserver/plugins/otpconfig.py
Normal file
121
ipaserver/plugins/otpconfig.py
Normal 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.')
|
||||
464
ipaserver/plugins/otptoken.py
Normal file
464
ipaserver/plugins/otptoken.py
Normal 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
139
ipaserver/plugins/passwd.py
Normal 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,
|
||||
)
|
||||
|
||||
1395
ipaserver/plugins/permission.py
Normal file
1395
ipaserver/plugins/permission.py
Normal file
File diff suppressed because it is too large
Load Diff
70
ipaserver/plugins/ping.py
Normal file
70
ipaserver/plugins/ping.py
Normal 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
105
ipaserver/plugins/pkinit.py
Normal 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)
|
||||
|
||||
251
ipaserver/plugins/privilege.py
Normal file
251
ipaserver/plugins/privilege.py
Normal 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'),
|
||||
),
|
||||
)
|
||||
611
ipaserver/plugins/pwpolicy.py
Normal file
611
ipaserver/plugins/pwpolicy.py
Normal 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
|
||||
175
ipaserver/plugins/radiusproxy.py
Normal file
175
ipaserver/plugins/radiusproxy.py
Normal 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.')
|
||||
340
ipaserver/plugins/realmdomains.py
Normal file
340
ipaserver/plugins/realmdomains.py
Normal 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
252
ipaserver/plugins/role.py
Normal 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
660
ipaserver/plugins/schema.py
Normal 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)
|
||||
224
ipaserver/plugins/selfservice.py
Normal file
224
ipaserver/plugins/selfservice.py
Normal 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),
|
||||
)
|
||||
|
||||
569
ipaserver/plugins/selinuxusermap.py
Normal file
569
ipaserver/plugins/selinuxusermap.py
Normal 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
260
ipaserver/plugins/server.py
Normal 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
|
||||
889
ipaserver/plugins/service.py
Normal file
889
ipaserver/plugins/service.py
Normal 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
|
||||
550
ipaserver/plugins/servicedelegation.py
Normal file
550
ipaserver/plugins/servicedelegation.py
Normal 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',
|
||||
}
|
||||
33
ipaserver/plugins/session.py
Normal file
33
ipaserver/plugins/session.py
Normal 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)
|
||||
745
ipaserver/plugins/stageuser.py
Normal file
745
ipaserver/plugins/stageuser.py
Normal 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")
|
||||
7
ipaserver/plugins/sudo.py
Normal file
7
ipaserver/plugins/sudo.py
Normal file
@@ -0,0 +1,7 @@
|
||||
#
|
||||
# Copyright (C) 2016 FreeIPA Contributors see COPYING for license
|
||||
#
|
||||
|
||||
from ipalib.text import _
|
||||
|
||||
__doc__ = _('commands for controlling sudo configuration')
|
||||
203
ipaserver/plugins/sudocmd.py
Normal file
203
ipaserver/plugins/sudocmd.py
Normal 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.')
|
||||
|
||||
195
ipaserver/plugins/sudocmdgroup.py
Normal file
195
ipaserver/plugins/sudocmdgroup.py
Normal 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.')
|
||||
|
||||
998
ipaserver/plugins/sudorule.py
Normal file
998
ipaserver/plugins/sudorule.py
Normal 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))
|
||||
503
ipaserver/plugins/topology.py
Normal file
503
ipaserver/plugins/topology.py
Normal 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
1725
ipaserver/plugins/trust.py
Normal file
File diff suppressed because it is too large
Load Diff
1151
ipaserver/plugins/user.py
Normal file
1151
ipaserver/plugins/user.py
Normal file
File diff suppressed because it is too large
Load Diff
1215
ipaserver/plugins/vault.py
Normal file
1215
ipaserver/plugins/vault.py
Normal file
File diff suppressed because it is too large
Load Diff
68
ipaserver/plugins/virtual.py
Normal file
68
ipaserver/plugins/virtual.py
Normal 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
|
||||
Reference in New Issue
Block a user