From d0587cbdd5bc5e07a6e8519deb07adaace643740 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Mon, 14 Sep 2009 17:04:08 -0400 Subject: Enrollment for a host in an IPA domain This will create a host service principal and may create a host entry (for admins). A keytab will be generated, by default in /etc/krb5.keytab If no kerberos credentails are available then enrollment over LDAPS is used if a password is provided. This change requires that openldap be used as our C LDAP client. It is much easier to do SSL using openldap than mozldap (no certdb required). Otherwise we'd have to write a slew of extra code to create a temporary cert database, import the CA cert, ... --- ipaserver/install/dsinstance.py | 4 ++ ipaserver/plugins/join.py | 120 ++++++++++++++++++++++++++++++++++++++++ ipaserver/plugins/ldap2.py | 42 +++++++++++++- 3 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 ipaserver/plugins/join.py (limited to 'ipaserver') diff --git a/ipaserver/install/dsinstance.py b/ipaserver/install/dsinstance.py index eb0356289..ea9f26da2 100644 --- a/ipaserver/install/dsinstance.py +++ b/ipaserver/install/dsinstance.py @@ -172,6 +172,7 @@ class DsInstance(service.Service): self.step("enabling memberof plugin", self.__add_memberof_module) self.step("enabling referential integrity plugin", self.__add_referint_module) self.step("enabling winsync plugin", self.__add_winsync_module) + self.step("enabling IPA enrollment plugin", self.__add_enrollment_module) self.step("enabling ldapi", self.__enable_ldapi) self.step("configuring uniqueness plugin", self.__set_unique_attrs) self.step("creating indices", self.__create_indices) @@ -316,6 +317,9 @@ class DsInstance(service.Service): def __add_winsync_module(self): self._ldap_mod("ipa-winsync-conf.ldif") + def __add_enrollment_module(self): + self._ldap_mod("enrollment-conf.ldif", self.sub_dict) + def __enable_ssl(self): dirname = config_dirname(self.serverid) dsdb = certs.CertDB(dirname) diff --git a/ipaserver/plugins/join.py b/ipaserver/plugins/join.py new file mode 100644 index 000000000..b63000d89 --- /dev/null +++ b/ipaserver/plugins/join.py @@ -0,0 +1,120 @@ +# Authors: +# Rob Crittenden +# +# 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 + +""" +Joining an IPA domain +""" + +from ipalib import api, util +from ipalib import Command, Str, Int +from ipalib import errors +import krbV +import os, subprocess +from ipapython import ipautil +import tempfile +import sha +import stat +import shutil + +def get_realm(): + krbctx = krbV.default_context() + + return unicode(krbctx.default_realm) + +def validate_host(ugettext, cn): + """ + Require at least one dot in the hostname (to support localhost.localdomain) + """ + dots = len(cn.split('.')) + if dots < 2: + return 'Fully-qualified hostname required' + return None + +class join(Command): + """Join an IPA domain""" + + requires_root = True + + takes_args = ( + Str('cn', + validate_host, + cli_name='hostname', + doc="The hostname to register as", + create_default=lambda **kw: unicode(util.get_fqdn()), + autofill=True, + #normalizer=lamda value: value.lower(), + ), + ) + takes_options= ( + Str('realm', + doc="The IPA realm", + create_default=lambda **kw: get_realm(), + autofill=True, + ), + Str('nshardwareplatform?', + cli_name='platform', + doc='Hardware platform of the host (e.g. Lenovo T61)', + ), + Str('nsosversion?', + cli_name='os', + doc='Operating System and version of the host (e.g. Fedora 9)', + ), + ) + + def execute(self, hostname, **kw): + """ + Execute the machine join operation. + + Returns the entry as it will be created in LDAP. + + :param hostname: The name of the host joined + :param kw: Keyword arguments for the other attributes. + """ + assert 'cn' not in kw + ldap = self.api.Backend.ldap2 + + host = None + try: + # First see if the host exists + kw = {'fqdn': hostname, 'all': True} + (dn, attrs_list) = api.Command['host_show'](**kw) + + # If no principal name is set yet we need to try to add + # one. + if 'krbprincipalname' not in attrs_list: + service = "host/%s@%s" % (hostname, api.env.realm) + (d, a) = api.Command['host_mod'](hostname, krbprincipalname=service) + + # It exists, can we write the password attributes? + allowed = ldap.can_write(dn, 'krblastpwdchange') + if not allowed: + raise errors.ACIError(info="Insufficient 'write' privilege to the 'krbLastPwdChange' attribute of entry '%s'." % dn) + + kw = {'fqdn': hostname, 'all': True} + (dn, attrs_list) = api.Command['host_show'](**kw) + except errors.NotFound: + (dn, attrs_list) = api.Command['host_add'](hostname) + + return (dn, attrs_list) + + def output_for_cli(self, textui, result, args, **options): + textui.print_plain("Welcome to the %s realm" % options['realm']) + textui.print_plain("Your keytab is in %s" % result.get('keytab')) + +api.register(join) diff --git a/ipaserver/plugins/ldap2.py b/ipaserver/plugins/ldap2.py index c854dac28..0deded937 100644 --- a/ipaserver/plugins/ldap2.py +++ b/ipaserver/plugins/ldap2.py @@ -31,15 +31,18 @@ import os import socket import string +import krbV import ldap as _ldap import ldap.filter as _ldap_filter import ldap.sasl as _ldap_sasl +from ldap.controls import LDAPControl # for backward compatibility from ldap.functions import explode_dn from ipalib import api, errors from ipalib.crud import CrudBackend from ipalib.encoder import Encoder, encode_args, decode_retval +from ipalib.request import context # attribute syntax to python type mapping, 'SYNTAX OID': type # everything not in this dict is considered human readable unicode @@ -140,7 +143,11 @@ _schema = _load_schema(api.env.ldap_uri) def _get_syntax(attr, value): schema = api.Backend.ldap2._schema - return schema.get_obj(_ldap.schema.AttributeType, attr).syntax + obj = schema.get_obj(_ldap.schema.AttributeType, attr) + if obj is not None: + return obj.syntax + else: + return None # ldap backend class @@ -215,6 +222,9 @@ class ldap2(CrudBackend, Encoder): if ccache is not None: os.environ['KRB5CCNAME'] = ccache conn.sasl_interactive_bind_s('', _sasl_auth) + principal = krbV.CCache(name=ccache, + context=krbV.default_context()).principal().name + setattr(context, "principal", principal) else: # no kerberos ccache, use simple bind conn.simple_bind_s(bind_dn, bind_pw) @@ -485,6 +495,36 @@ class ldap2(CrudBackend, Encoder): """Returns a copy of the current LDAP schema.""" return copy.deepcopy(self._schema) + @encode_args(1, 2) + def get_effective_rights(self, dn, entry_attrs): + """Returns the rights the currently bound user has for the given DN. + + Returns 2 attributes, the attributeLevelRights for the given list of + attributes and the entryLevelRights for the entry itself. + """ + principal = getattr(context, 'principal') + (binddn, attrs) = self.find_entry_by_attr("krbprincipalname", principal, "posixAccount") + sctrl = [LDAPControl("1.3.6.1.4.1.42.2.27.9.5.2", True, "dn: " + binddn.encode('UTF-8'))] + self.conn.set_option(_ldap.OPT_SERVER_CONTROLS, sctrl) + (dn, attrs) = self.get_entry(dn, entry_attrs) + # remove the control so subsequent operations don't include GER + self.conn.set_option(_ldap.OPT_SERVER_CONTROLS, []) + return (dn, attrs) + + @encode_args(1, 2) + def can_write(self, dn, attr): + """Returns True/False if the currently bound user has write permissions + on the attribute. This only operates on a single attribute at a time. + """ + (dn, attrs) = self.get_effective_rights(dn, [attr]) + if 'attributelevelrights' in attrs: + attr_rights = attrs.get('attributelevelrights')[0].decode('UTF-8') + (attr, rights) = attr_rights.split(':') + if 'w' in rights: + return True + + return False + @encode_args(1, 2) def update_entry_rdn(self, dn, new_rdn, del_old=True): """ -- cgit