mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2024-12-26 17:01:14 -06:00
4b6b710ba6
ticket #158
655 lines
20 KiB
Python
655 lines
20 KiB
Python
# 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; version 2 only
|
|
#
|
|
# 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, write to the Free Software
|
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
"""
|
|
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, rolegroups.
|
|
|
|
For example, if you have taskgroups that allow adding and modifying users you
|
|
could create a rolegroup, useradmin. You would assign users to the useradmin
|
|
rolegroup 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
|
|
targetted. This can include a list of attributes, an area of that LDAP
|
|
tree or an LDAP filter.
|
|
|
|
The permissions define what the ACI is allowed to do, they 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 editabe.
|
|
|
|
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 rolegroups.
|
|
|
|
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:
|
|
|
|
Add an ACI so that the group "secretaries" can update the address on any user:
|
|
ipa aci-add --attrs=streetAddress --memberof=ipausers --group=secretaries --permissions=write "Secretaries write addresses"
|
|
|
|
Show the new ACI:
|
|
ipa aci-show "Secretaries write addresses"
|
|
|
|
Add an ACI that allows members of the "addusers" taskgroup to add new users:
|
|
ipa aci-add --type=user --taskgroup=addusers --permissions=add "Add new users"
|
|
|
|
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 ipalib import api, crud, errors
|
|
from ipalib import Object, Command
|
|
from ipalib import Flag, Int, List, Str, StrEnum
|
|
from ipalib.aci import ACI
|
|
from ipalib import output
|
|
from ipalib import _, ngettext
|
|
import logging
|
|
|
|
_type_map = {
|
|
'user': 'ldap:///uid=*,%s,%s' % (api.env.container_user, api.env.basedn),
|
|
'group': 'ldap:///cn=*,%s,%s' % (api.env.container_group, api.env.basedn),
|
|
'host': 'ldap:///fqdn=*,%s,%s' % (api.env.container_host, api.env.basedn)
|
|
}
|
|
|
|
_valid_permissions_values = [
|
|
u'read', u'write', u'add', u'delete', u'all'
|
|
]
|
|
|
|
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(current, aciname, kw):
|
|
"""
|
|
Given a name and a set of keywords construct an ACI.
|
|
"""
|
|
# Do some quick and dirty validation.
|
|
t1 = 'type' in kw
|
|
t2 = 'filter' in kw
|
|
t3 = 'subtree' in kw
|
|
t4 = 'targetgroup' in kw
|
|
t5 = 'attrs' in kw
|
|
t6 = 'memberof' in kw
|
|
if t1 + t2 + t3 + t4 > 1:
|
|
raise errors.ValidationError(name='target', error=_('type, filter, subtree and targetgroup are mutually exclusive'))
|
|
|
|
if t1 + t2 + t3 + t4 + t5 + t6 == 0:
|
|
raise errors.ValidationError(name='target', error=_('at least one of: type, filter, subtree, targetgroup, attrs or memberof are required'))
|
|
|
|
group = 'group' in kw
|
|
taskgroup = 'taskgroup' in kw
|
|
selfaci = 'selfaci' in kw and kw['selfaci'] == True
|
|
if group + taskgroup + selfaci > 1:
|
|
raise errors.ValidationError(name='target', error=_('group, taskgroup and self are mutually exclusive'))
|
|
elif group + taskgroup + selfaci == 0:
|
|
raise errors.ValidationError(name='target', error=_('One of group, taskgroup or self is required'))
|
|
|
|
# Grab the dn of the group we're granting access to. This group may be a
|
|
# taskgroup or a user group.
|
|
entry_attrs = []
|
|
if taskgroup:
|
|
try:
|
|
entry_attrs = api.Command['taskgroup_show'](kw['taskgroup'])['result']
|
|
except errors.NotFound:
|
|
# The task group doesn't exist, let's be helpful and add it
|
|
tgkw = {'description': aciname}
|
|
entry_attrs = api.Command['taskgroup_add'](
|
|
kw['taskgroup'], **tgkw
|
|
)['result']
|
|
elif group:
|
|
# Not so friendly with groups. This will raise
|
|
try:
|
|
entry_attrs = api.Command['group_show'](kw['group'])['result']
|
|
except errors.NotFound:
|
|
raise errors.NotFound(reason=_("Group '%s' does not exist") % kw['group'])
|
|
|
|
a = ACI(current)
|
|
a.name = 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 'attrs' in kw:
|
|
a.set_target_attr(kw['attrs'])
|
|
if 'memberof' in kw:
|
|
entry_attrs = api.Command['group_show'](kw['memberof'])['result']
|
|
a.set_target_filter('memberOf=%s' % dn)
|
|
if 'filter' in kw:
|
|
a.set_target_filter(kw['filter'])
|
|
if 'type' in kw:
|
|
target = _type_map[kw['type']]
|
|
a.set_target(target)
|
|
if 'targetgroup' in kw:
|
|
# Purposely no try here so we'll raise a NotFound
|
|
entry_attrs = api.Command['group_show'](kw['targetgroup'])['result']
|
|
target = 'ldap:///%s' % dn
|
|
a.set_target(target)
|
|
if 'subtree' in kw:
|
|
# See if the subtree is a full URI
|
|
target = kw['subtree']
|
|
if not target.startswith('ldap:///'):
|
|
target = 'ldap:///%s' % target
|
|
a.set_target(target)
|
|
|
|
return a
|
|
|
|
def _aci_to_kw(ldap, a):
|
|
"""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['aciname'] = a.name
|
|
kw['permissions'] = tuple(a.permissions)
|
|
if 'targetattr' in a.target:
|
|
kw['attrs'] = tuple(a.target['targetattr']['expression'])
|
|
if 'targetfilter' in a.target:
|
|
target = a.target['targetfilter']['expression']
|
|
if target.startswith('memberOf'):
|
|
kw['memberof'] = target
|
|
else:
|
|
kw['filter'] = 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'] = target
|
|
else:
|
|
# See if the target is a group. If so we set the
|
|
# targetgroup attr, otherwise we consider it a subtree
|
|
if api.env.container_group in target:
|
|
kw['targetgroup'] = target
|
|
else:
|
|
kw['subtree'] = target
|
|
|
|
groupdn = a.bindrule['expression']
|
|
groupdn = groupdn.replace('ldap:///','')
|
|
if groupdn == 'self':
|
|
kw['selfaci'] = True
|
|
else:
|
|
(dn, entry_attrs) = ldap.get_entry(groupdn, ['cn'])
|
|
if api.env.container_taskgroup in dn:
|
|
kw['taskgroup'] = entry_attrs['cn'][0]
|
|
else:
|
|
kw['group'] = entry_attrs['cn'][0]
|
|
|
|
return kw
|
|
|
|
def _convert_strings_to_acis(acistrs):
|
|
acis = []
|
|
for a in acistrs:
|
|
try:
|
|
acis.append(ACI(a))
|
|
except SyntaxError, e:
|
|
logging.warn("Failed to parse: %s" % a)
|
|
return acis
|
|
|
|
def _find_aci_by_name(acis, aciname):
|
|
for a in acis:
|
|
if a.name.lower() == aciname.lower():
|
|
return a
|
|
raise errors.NotFound(reason=_('ACI with name "%s" not found') % aciname)
|
|
|
|
def _normalize_permissions(permissions):
|
|
valid_permissions = []
|
|
permissions = permissions.split(',')
|
|
for p in permissions:
|
|
p = p.strip().lower()
|
|
if p in _valid_permissions_values and p not in valid_permissions:
|
|
valid_permissions.append(p)
|
|
return ','.join(valid_permissions)
|
|
|
|
|
|
class aci(Object):
|
|
"""
|
|
ACI object.
|
|
"""
|
|
|
|
label = _('ACIs')
|
|
|
|
takes_params = (
|
|
Str('aciname',
|
|
cli_name='name',
|
|
label=_('ACI name'),
|
|
primary_key=True,
|
|
),
|
|
Str('taskgroup?',
|
|
cli_name='taskgroup',
|
|
label=_('Taskgroup'),
|
|
doc=_('Taskgroup ACI grants access to'),
|
|
),
|
|
Str('group?',
|
|
cli_name='group',
|
|
label=_('User group'),
|
|
doc=_('User group ACI grants access to'),
|
|
),
|
|
List('permissions',
|
|
cli_name='permissions',
|
|
label=_('Permissions'),
|
|
doc=_('comma-separated list of permissions to grant' \
|
|
'(read, write, add, delete, all)'),
|
|
normalizer=_normalize_permissions,
|
|
),
|
|
List('attrs?',
|
|
cli_name='attrs',
|
|
label=_('Attributes'),
|
|
doc=_('Comma-separated list of attributes'),
|
|
),
|
|
StrEnum('type?',
|
|
cli_name='type',
|
|
label=_('Type'),
|
|
doc=_('type of IPA object (user, group, host)'),
|
|
values=(u'user', u'group', u'host'),
|
|
),
|
|
Str('memberof?',
|
|
cli_name='memberof',
|
|
label=_('Member of'), # FIXME: Does this label make sense?
|
|
doc=_('Member of a group'),
|
|
),
|
|
Str('filter?',
|
|
cli_name='filter',
|
|
label=_('Filter'),
|
|
doc=_('Legal LDAP filter (e.g. ou=Engineering)'),
|
|
),
|
|
Str('subtree?',
|
|
cli_name='subtree',
|
|
label=_('Subtree'),
|
|
doc=_('Subtree to apply ACI to'),
|
|
),
|
|
Str('targetgroup?',
|
|
cli_name='targetgroup',
|
|
label=_('Target group'),
|
|
doc=_('Group to apply ACI to'),
|
|
),
|
|
Flag('selfaci?',
|
|
cli_name='self',
|
|
label=_('Target your own entry (self)'),
|
|
doc=_('Apply ACI to your own entry (self)'),
|
|
),
|
|
)
|
|
|
|
api.register(aci)
|
|
|
|
|
|
class aci_add(crud.Create):
|
|
"""
|
|
Create new ACI.
|
|
"""
|
|
has_output = aci_output
|
|
msg_summary = _('Created ACI "%(value)s"')
|
|
|
|
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(None, aciname, kw)
|
|
|
|
(dn, entry_attrs) = ldap.get_entry(self.api.env.basedn, ['aci'])
|
|
|
|
acis = _convert_strings_to_acis(entry_attrs.get('aci', []))
|
|
for a in acis:
|
|
if a.isequal(newaci):
|
|
raise errors.DuplicateEntry()
|
|
|
|
newaci_str = unicode(newaci)
|
|
entry_attrs['aci'].append(newaci_str)
|
|
|
|
ldap.update_entry(dn, entry_attrs)
|
|
|
|
return dict(
|
|
result=newaci_str,
|
|
value=newaci.name
|
|
)
|
|
|
|
def output_for_cli(self, textui, result, aciname, **options):
|
|
"""
|
|
Display the newly created ACI and a success message.
|
|
"""
|
|
result = result['result']
|
|
textui.print_name(self.name)
|
|
textui.print_plain(result)
|
|
textui.print_dashed('Created ACI "%s".' % aciname)
|
|
|
|
api.register(aci_add)
|
|
|
|
|
|
class aci_del(crud.Delete):
|
|
"""
|
|
Delete ACI.
|
|
"""
|
|
has_output = output.standard_delete
|
|
msg_summary = _('Deleted ACI "%(value)s"')
|
|
|
|
def execute(self, aciname, **kw):
|
|
"""
|
|
Execute the aci-delete operation.
|
|
|
|
:param aciname: The name of the ACI being added.
|
|
:param kw: unused
|
|
"""
|
|
assert 'aciname' not in kw
|
|
ldap = self.api.Backend.ldap2
|
|
|
|
(dn, entry_attrs) = ldap.get_entry(self.api.env.basedn, ['aci'])
|
|
|
|
acistrs = entry_attrs.get('aci', [])
|
|
acis = _convert_strings_to_acis(acistrs)
|
|
aci = _find_aci_by_name(acis, aciname)
|
|
for a in acistrs:
|
|
candidate = ACI(a)
|
|
if aci.isequal(candidate):
|
|
acistrs.remove(a)
|
|
break
|
|
|
|
entry_attrs['aci'] = acistrs
|
|
|
|
ldap.update_entry(dn, entry_attrs)
|
|
|
|
return dict(
|
|
result=True,
|
|
value=aciname,
|
|
)
|
|
|
|
api.register(aci_del)
|
|
|
|
|
|
class aci_mod(crud.Update):
|
|
"""
|
|
Modify ACI.
|
|
"""
|
|
has_output = aci_output
|
|
msg_summary = _('Modified ACI "%(value)s"')
|
|
|
|
def execute(self, aciname, **kw):
|
|
ldap = self.api.Backend.ldap2
|
|
|
|
(dn, entry_attrs) = ldap.get_entry(self.api.env.basedn, ['aci'])
|
|
|
|
acis = _convert_strings_to_acis(entry_attrs.get('aci', []))
|
|
aci = _find_aci_by_name(acis, 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.
|
|
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
|
|
for k in kw.keys():
|
|
newkw[k] = kw[k]
|
|
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(None, aciname, newkw)
|
|
|
|
self.api.Command['aci_del'](aciname)
|
|
|
|
result = self.api.Command['aci_add'](aciname, **newkw)['result']
|
|
|
|
return dict(
|
|
result=result,
|
|
value=aciname,
|
|
)
|
|
|
|
def output_for_cli(self, textui, result, aciname, **options):
|
|
"""
|
|
Display the updated ACI and a success message.
|
|
"""
|
|
result = result['result']
|
|
textui.print_name(self.name)
|
|
textui.print_plain(result)
|
|
textui.print_dashed('Modified ACI "%s".' % aciname)
|
|
|
|
api.register(aci_mod)
|
|
|
|
|
|
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.
|
|
"""
|
|
has_output = (
|
|
ListOfACI('result'),
|
|
output.Output('count', int, 'Number of entries returned'),
|
|
output.summary,
|
|
)
|
|
msg_summary = ngettext('%(count)d ACI matched', '%(count)d ACIs matched', 0)
|
|
|
|
def execute(self, term, **kw):
|
|
ldap = self.api.Backend.ldap2
|
|
|
|
(dn, entry_attrs) = ldap.get_entry(self.api.env.basedn, ['aci'])
|
|
|
|
acis = _convert_strings_to_acis(entry_attrs.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 'aciname' in kw:
|
|
for a in acis:
|
|
if a.name != kw['aciname']:
|
|
results.remove(a)
|
|
acis = list(results)
|
|
|
|
if 'attrs' in kw:
|
|
for a in acis:
|
|
alist1 = sorted(
|
|
[t.lower() for t in a.target['targetattr']['expression']]
|
|
)
|
|
alist2 = sorted([t.lower() for t in kw['attrs']])
|
|
if alist1 != alist2:
|
|
results.remove(a)
|
|
acis = list(results)
|
|
|
|
if 'taskgroup' in kw:
|
|
try:
|
|
self.api.Command['taskgroup_show'](
|
|
kw['taskgroup']
|
|
)
|
|
except errors.NotFound:
|
|
pass
|
|
else:
|
|
for a in acis:
|
|
if a.bindrule['expression'] != ('ldap:///%s' % dn):
|
|
results.remove(a)
|
|
acis = list(results)
|
|
|
|
if 'permissions' in kw:
|
|
for a in acis:
|
|
alist1 = sorted(a.permissions)
|
|
alist2 = sorted(kw['permissions'])
|
|
if alist1 != alist2:
|
|
results.remove(a)
|
|
acis = list(results)
|
|
|
|
if 'memberof' in kw:
|
|
try:
|
|
self.api.Command['group_show'](
|
|
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)
|
|
# uncomment next line if you add more search criteria
|
|
# acis = list(results)
|
|
|
|
# TODO: searching by: type, filter, subtree
|
|
|
|
return dict(
|
|
result=[unicode(aci) for aci in results],
|
|
count=len(results),
|
|
)
|
|
|
|
def output_for_cli(self, textui, result, term, **options):
|
|
"""
|
|
Display the search results
|
|
"""
|
|
textui.print_name(self.name)
|
|
for aci in result['result']:
|
|
textui.print_plain(aci)
|
|
textui.print_plain('')
|
|
textui.print_count(
|
|
result['count'], '%i ACI matched.', '%i ACIs matched.'
|
|
)
|
|
|
|
api.register(aci_find)
|
|
|
|
|
|
class aci_show(crud.Retrieve):
|
|
"""
|
|
Display a single ACI given an ACI name.
|
|
"""
|
|
has_output = (
|
|
output.Output('result', unicode, 'A string representing the ACI'),
|
|
output.value,
|
|
output.summary,
|
|
)
|
|
|
|
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, entry_attrs) = ldap.get_entry(self.api.env.basedn, ['aci'])
|
|
|
|
acis = _convert_strings_to_acis(entry_attrs.get('aci', []))
|
|
|
|
return dict(
|
|
result=unicode(_find_aci_by_name(acis, aciname)),
|
|
value=aciname,
|
|
)
|
|
|
|
def output_for_cli(self, textui, result, aciname, **options):
|
|
"""
|
|
Display the requested ACI
|
|
"""
|
|
result = result['result']
|
|
textui.print_name(self.name)
|
|
textui.print_plain(result)
|
|
|
|
api.register(aci_show)
|