Add support for User-Private Groups

This uses a new 389-ds plugin, Managed Entries, to automatically create
a group entry when a user is created. The DNA plugin ensures that the
group has a gidNumber that matches the users uidNumber. When the user is
removed the group is automatically removed as well.

If the managed entries plugin is not available or if a specific, separate
range for gidNumber is passed in at install time then User-Private Groups
will not be configured.

The code checking for the Managed Entries plugin may be removed at some
point. This is there because this plugin is only available in a 389-ds
alpha release currently (1.2.6-a4).
This commit is contained in:
Rob Crittenden 2010-06-25 16:14:46 -04:00
parent 83fd9ef7cc
commit ba59d9d648
7 changed files with 119 additions and 19 deletions

View File

@ -31,6 +31,7 @@ app_DATA = \
preferences.html.template \ preferences.html.template \
referint-conf.ldif \ referint-conf.ldif \
dna-posix.ldif \ dna-posix.ldif \
dna-upg.ldif \
master-entry.ldif \ master-entry.ldif \
memberof-task.ldif \ memberof-task.ldif \
memberof-conf.ldif \ memberof-conf.ldif \
@ -39,6 +40,7 @@ app_DATA = \
schema_compat.uldif \ schema_compat.uldif \
ldapi.ldif \ ldapi.ldif \
wsgi.py \ wsgi.py \
user_private_groups.ldif \
$(NULL) $(NULL)
EXTRA_DIST = \ EXTRA_DIST = \

View File

@ -0,0 +1,16 @@
# add plugin configuration for user private groups
dn: cn=User Private Groups,cn=Distributed Numeric Assignment Plugin,cn=plugins,cn=config
changetype: add
objectclass: top
objectclass: extensibleObject
cn: Posix Accounts
dnaType: uidNumber
dnaType: gidNumber
dnaNextValue: eval($UIDSTART+1)
dnaInterval: 1
dnaMaxValue: eval($UIDSTART+100000)
dnaMagicRegen: 999
dnaFilter: (|(objectclass=posixAccount)(objectClass=posixGroup))
dnaScope: $SUFFIX

View File

@ -0,0 +1,19 @@
dn: cn=UPG Template,$SUFFIX
changetype: add
objectclass: mepTemplateEntry
cn: UPG Template
mepRDNAttr: cn
mepStaticAttr: objectclass: posixGroup
mepMappedAttr: cn: $$uid
mepMappedAttr: gidNumber: $$uidNumber
mepMappedAttr: description: User private group for $$uid
dn: cn=UPG Definition,cn=Managed Entries,cn=plugins,cn=config
changetype: add
objectclass: extensibleObject
cn: UPG Definition
originScope: cn=users,cn=accounts,$SUFFIX
originFilter: objectclass=posixAccount
managedBase: cn=groups,cn=accounts,$SUFFIX
managedTemplate: cn=UPG Template,$SUFFIX

View File

@ -145,6 +145,8 @@ class group_add(LDAPCreate):
def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
if options['posix'] or 'gidnumber' in options: if options['posix'] or 'gidnumber' in options:
entry_attrs['objectclass'].append('posixgroup') entry_attrs['objectclass'].append('posixgroup')
if not 'gidnumber' in options:
entry_attrs['gidnumber'] = 999
return dn return dn
@ -200,6 +202,8 @@ class group_mod(LDAPUpdate):
else: else:
old_entry_attrs['objectclass'].append('posixgroup') old_entry_attrs['objectclass'].append('posixgroup')
entry_attrs['objectclass'] = old_entry_attrs['objectclass'] entry_attrs['objectclass'] = old_entry_attrs['objectclass']
if not 'gidnumber' in options:
entry_attrs['gidnumber'] = 999
return dn return dn
api.register(group_mod) api.register(group_mod)

View File

@ -122,6 +122,8 @@ class user(LDAPObject):
cli_name='uid', cli_name='uid',
label=_('UID'), label=_('UID'),
doc=_('User ID Number (system will assign one if not provided)'), doc=_('User ID Number (system will assign one if not provided)'),
autofill=True,
default=999,
), ),
Str('street?', Str('street?',
cli_name='street', cli_name='street',
@ -169,16 +171,20 @@ class user_add(LDAPCreate):
home_dir = home_dir.replace('//', '/').rstrip('/') home_dir = home_dir.replace('//', '/').rstrip('/')
entry_attrs['homedirectory'] = home_dir entry_attrs['homedirectory'] = home_dir
# we're adding new users to a default group, get its gidNumber if ldap.has_upg():
# get default group name from config # User Private Groups - uidNumber == gidNumber
def_primary_group = config.get('ipadefaultprimarygroup') entry_attrs['gidnumber'] = entry_attrs['uidnumber']
group_dn = self.api.Object['group'].get_dn(def_primary_group) else:
try: # we're adding new users to a default group, get its gidNumber
(group_dn, group_attrs) = ldap.get_entry(group_dn, ['gidnumber']) # get default group name from config
except errors.NotFound: def_primary_group = config.get('ipadefaultprimarygroup')
error_msg = 'Default group for new users not found.' group_dn = self.api.Object['group'].get_dn(def_primary_group)
raise errors.NotFound(reason=error_msg) try:
entry_attrs['gidnumber'] = group_attrs['gidnumber'] (group_dn, group_attrs) = ldap.get_entry(group_dn, ['gidnumber'])
except errors.NotFound:
error_msg = 'Default group for new users not found.'
raise errors.NotFound(reason=error_msg)
entry_attrs['gidnumber'] = group_attrs['gidnumber']
return dn return dn

View File

@ -38,7 +38,8 @@ from ldap.dn import escape_dn_chars
from ipaserver import ipaldap from ipaserver import ipaldap
from ipaserver.install import ldapupdate from ipaserver.install import ldapupdate
from ipaserver.install import httpinstance from ipaserver.install import httpinstance
from ipalib import util, uuid from ipalib import util, uuid, errors
from ipaserver.plugins.ldap2 import ldap2
SERVER_ROOT_64 = "/usr/lib64/dirsrv" SERVER_ROOT_64 = "/usr/lib64/dirsrv"
SERVER_ROOT_32 = "/usr/lib/dirsrv" SERVER_ROOT_32 = "/usr/lib/dirsrv"
@ -114,6 +115,25 @@ def is_ds_running():
ret = False ret = False
return ret return ret
def has_managed_entries(host_name, dm_password):
"""Check to see if the Managed Entries plugin is available"""
ldapuri = 'ldap://%s' % host_name
conn = None
try:
conn = ldap2(shared_instance=False, ldap_uri=ldapuri, base_dn='cn=config')
conn.connect(bind_dn='cn=Directory Manager', bind_pw=dm_password)
(dn, attrs) = conn.get_entry('cn=Managed Entries,cn=plugins',
['*'])
return True
except errors.NotFound:
return False
except errors.ExecutionError, e:
logging.critical("Could not connect to the Directory Server on %s" % host_name)
raise e
finally:
if conn:
conn.disconnect()
INF_TEMPLATE = """ INF_TEMPLATE = """
[General] [General]
@ -179,6 +199,8 @@ class DsInstance(service.Service):
self.step("enabling memberof plugin", self.__add_memberof_module) self.step("enabling memberof plugin", self.__add_memberof_module)
self.step("enabling referential integrity plugin", self.__add_referint_module) self.step("enabling referential integrity plugin", self.__add_referint_module)
self.step("enabling winsync plugin", self.__add_winsync_module) self.step("enabling winsync plugin", self.__add_winsync_module)
if self.uidstart == self.gidstart:
self.step("configuring user private groups", self.__user_private_groups)
self.step("configuring replication version plugin", self.__config_version_module) self.step("configuring replication version plugin", self.__config_version_module)
self.step("enabling IPA enrollment plugin", self.__add_enrollment_module) self.step("enabling IPA enrollment plugin", self.__add_enrollment_module)
self.step("enabling ldapi", self.__enable_ldapi) self.step("enabling ldapi", self.__enable_ldapi)
@ -331,7 +353,11 @@ class DsInstance(service.Service):
self._ldap_mod("unique-attributes.ldif", self.sub_dict) self._ldap_mod("unique-attributes.ldif", self.sub_dict)
def __config_uidgid_gen_first_master(self): def __config_uidgid_gen_first_master(self):
self._ldap_mod("dna-posix.ldif", self.sub_dict) if (self.uidstart == self.gidstart and
has_managed_entries(self.host_name, self.dm_password)):
self._ldap_mod("dna-upg.ldif", self.sub_dict)
else:
self._ldap_mod("dna-posix.ldif", self.sub_dict)
def __add_master_entry_first_master(self): def __add_master_entry_first_master(self):
self._ldap_mod("master-entry.ldif", self.sub_dict) self._ldap_mod("master-entry.ldif", self.sub_dict)
@ -342,6 +368,10 @@ class DsInstance(service.Service):
def __config_version_module(self): def __config_version_module(self):
self._ldap_mod("ipa-version-conf.ldif") self._ldap_mod("ipa-version-conf.ldif")
def __user_private_groups(self):
if has_managed_entries(self.host_name, self.dm_password):
self._ldap_mod("user_private_groups.ldif", self.sub_dict)
def __add_enrollment_module(self): def __add_enrollment_module(self):
self._ldap_mod("enrollment-conf.ldif", self.sub_dict) self._ldap_mod("enrollment-conf.ldif", self.sub_dict)

View File

@ -103,9 +103,12 @@ def _handle_errors(e, **kw):
raise errors.DatabaseError(desc=desc, info=info) raise errors.DatabaseError(desc=desc, info=info)
def load_schema(url): def global_init(url):
""" """
Retrieve the LDAP schema from the provided url. Perform global initialization when the module is loaded.
Retrieve the LDAP schema from the provided url and determine if
User-Private Groups (upg) are configured.
Bind using kerberos credentials. If in the context of the Bind using kerberos credentials. If in the context of the
in-tree "lite" server then use the current ccache. If in the context of in-tree "lite" server then use the current ccache. If in the context of
@ -113,10 +116,11 @@ def load_schema(url):
principal. principal.
""" """
tmpdir = None tmpdir = None
upg = False
if not api.env.in_server or api.env.context not in ['lite', 'server']: if not api.env.in_server or api.env.context not in ['lite', 'server']:
# The schema is only needed on the server side # The schema is only needed on the server side
return return (None, None)
try: try:
if api.env.context == 'server': if api.env.context == 'server':
@ -139,9 +143,17 @@ def load_schema(url):
'cn=schema', _ldap.SCOPE_BASE, 'cn=schema', _ldap.SCOPE_BASE,
attrlist=['attributetypes', 'objectclasses'] attrlist=['attributetypes', 'objectclasses']
)[0] )[0]
try:
upg_entry = conn.search_s(
'cn=UPG Template, %s' % api.env.basedn, _ldap.SCOPE_BASE,
attrlist=['*']
)[0]
upg = True
except _ldap.NO_SUCH_OBJECT, e:
upg = False
conn.unbind_s() conn.unbind_s()
except _ldap.SERVER_DOWN: except _ldap.SERVER_DOWN:
return None return (None, upg)
except _ldap.LDAPError, e: except _ldap.LDAPError, e:
# TODO: raise a more appropriate exception # TODO: raise a more appropriate exception
_handle_errors(e, **{}) _handle_errors(e, **{})
@ -154,13 +166,14 @@ def load_schema(url):
if tmpdir: if tmpdir:
shutil.rmtree(tmpdir) shutil.rmtree(tmpdir)
return _ldap.schema.SubSchema(schema_entry[1]) return (_ldap.schema.SubSchema(schema_entry[1]), upg)
# cache schema when importing module # cache schema and User-Private Groups when importing module
try: try:
_schema = load_schema(api.env.ldap_uri) (_schema, _upg) = global_init(api.env.ldap_uri)
except AttributeError: except AttributeError:
_schema = None _schema = None
_upg = None
def get_syntax(attr, value): def get_syntax(attr, value):
@ -524,6 +537,16 @@ class ldap2(CrudBackend, Encoder):
"""Returns a copy of the current LDAP schema.""" """Returns a copy of the current LDAP schema."""
return copy.deepcopy(self.schema) return copy.deepcopy(self.schema)
def has_upg(self):
"""Returns True/False whether User-Private Groups are enabled.
This is determined based on whether the UPG Template exists.
We determine this at module load so we don't have to test for
it every time.
"""
global _upg
return _upg
@encode_args(1, 2) @encode_args(1, 2)
def get_effective_rights(self, dn, entry_attrs): def get_effective_rights(self, dn, entry_attrs):
"""Returns the rights the currently bound user has for the given DN. """Returns the rights the currently bound user has for the given DN.