diff options
author | John Dennis <jdennis@redhat.com> | 2007-11-28 07:49:07 -0500 |
---|---|---|
committer | John Dennis <jdennis@redhat.com> | 2007-11-28 07:49:07 -0500 |
commit | 904b76059cec667a9c155021c8e33ce1dbf2b389 (patch) | |
tree | c2f9d8ed6a2f84427dd494d3814cac77c29a34f0 /ipa-server | |
parent | c939c5d289daaf4c855caa2a6816e7eeba7e2661 (diff) | |
parent | 2e7f629d913d775cfb285ede166d7a0f977782fe (diff) | |
download | freeipa-904b76059cec667a9c155021c8e33ce1dbf2b389.tar.gz freeipa-904b76059cec667a9c155021c8e33ce1dbf2b389.tar.xz freeipa-904b76059cec667a9c155021c8e33ce1dbf2b389.zip |
merged radius work with latest mainline tip
Diffstat (limited to 'ipa-server')
67 files changed, 4866 insertions, 895 deletions
diff --git a/ipa-server/Makefile.am b/ipa-server/Makefile.am index b5da3f566..9638cdab6 100644 --- a/ipa-server/Makefile.am +++ b/ipa-server/Makefile.am @@ -11,6 +11,7 @@ SUBDIRS = \ ipaserver \ ipa-slapi-plugins \ xmlrpc-server \ + ipa-keytab-util \ $(NULL) EXTRA_DIST = \ diff --git a/ipa-server/configure.ac b/ipa-server/configure.ac index e07c04c85..1e62a2f82 100644 --- a/ipa-server/configure.ac +++ b/ipa-server/configure.ac @@ -1,6 +1,6 @@ AC_PREREQ(2.59c) -AC_INIT([freeipa-server], - [0.4], +AC_INIT([ipa-server], + [0.5], [https://hosted.fedoraproject.org/projects/freeipa/newticket]) AC_CONFIG_SRCDIR([ipaserver/ipaldap.py]) @@ -229,12 +229,13 @@ AC_CONFIG_FILES([ ipa-slapi-plugins/ipa-pwd-extop/Makefile xmlrpc-server/Makefile xmlrpc-server/test/Makefile + ipa-keytab-util/Makefile ]) AC_OUTPUT echo " - FreeIPA Server $VERSION + IPA Server $VERSION ======================== prefix: ${prefix} diff --git a/ipa-server/ipa-gui/README.multivalue b/ipa-server/ipa-gui/README.multivalue new file mode 100644 index 000000000..ba315181d --- /dev/null +++ b/ipa-server/ipa-gui/README.multivalue @@ -0,0 +1,27 @@ +The way multi-valued fields work is this: + - A new widget is added to the form. I name it as the attribute + s. + For example, I use cns for the cn attribute. + - If you need a new validator use a ForEach() so that each value is + checked. + - This attribute is populated from the incoming attribute from the + user or group record. The widget can support multiple fields at once + but I'm using it for just one field. In fact, I don't know if it + will work with more the way I'm using it. + - In the GUI an operator can add/remove values to each multi-valued field. + - Naming is very important in the widget. TurboGears automatically + re-assembles the data into a list of dict entries if you name things + properly. For example, the cns (multiple CN entries) looks like: + cns-0.cn=Rob+Crittenden&cns-1.cn=Robert+Crittenden&cns-2.cn=rcrit + - This gets converted to: + [{'cn': u'Rob Crittenden'}, {'cn': u'Robert Crittenden'}, {'cn': u'rcrit'}] + - I take this list of dicts and pull out each value and append it to a new + list that represents the original multi-valued field + - Then the list/dict version is removed (in this case, kw['cns']). + +When adding a new field you have to update: + +1. The form to add the new ExpandingForm() field and perhaps a validator +2. The edit template to add the boilerplate to display the field +3. The show template to be able to display all the fields separately +4. The new template if you want to be able to enter these on new entries +5. The subcontroller so you can do the input and output conversions diff --git a/ipa-server/ipa-gui/ipagui/controllers.py b/ipa-server/ipa-gui/ipagui/controllers.py index 5d0bfee03..d1ee22e01 100644 --- a/ipa-server/ipa-gui/ipagui/controllers.py +++ b/ipa-server/ipa-gui/ipagui/controllers.py @@ -17,6 +17,8 @@ import ipa.ipaclient from subcontrollers.user import UserController from subcontrollers.group import GroupController from subcontrollers.delegation import DelegationController +from subcontrollers.policy import PolicyController +from subcontrollers.ipapolicy import IPAPolicyController ipa.config.init_config() @@ -27,6 +29,8 @@ class Root(controllers.RootController): user = UserController() group = GroupController() delegate = DelegationController() + policy = PolicyController() + ipapolicy = IPAPolicyController() @expose(template="ipagui.templates.welcome") @identity.require(identity.not_anonymous()) diff --git a/ipa-server/ipa-gui/ipagui/forms/Makefile.am b/ipa-server/ipa-gui/ipagui/forms/Makefile.am index 5f07e4cb0..c075b9f47 100644 --- a/ipa-server/ipa-gui/ipagui/forms/Makefile.am +++ b/ipa-server/ipa-gui/ipagui/forms/Makefile.am @@ -4,8 +4,9 @@ appdir = $(IPA_DATA_DIR)/ipagui/forms app_PYTHON = \ __init__.py \ group.py \ + ipapolicy.py \ user.py \ - delegate.py \ + delegate.py \ $(NULL) EXTRA_DIST = \ diff --git a/ipa-server/ipa-gui/ipagui/forms/delegate.py b/ipa-server/ipa-gui/ipagui/forms/delegate.py index 89011f4a2..419df4fc7 100644 --- a/ipa-server/ipa-gui/ipagui/forms/delegate.py +++ b/ipa-server/ipa-gui/ipagui/forms/delegate.py @@ -44,7 +44,7 @@ aci_checkbox_attrs = [(field.name, field.label) for field in aci_attrs] aci_name_to_label = dict(aci_checkbox_attrs) class DelegateFields(): - name = widgets.TextField(name="name", label="Name") + name = widgets.TextField(name="name", label="Delegation Name") source_group_dn = widgets.HiddenField(name="source_group_dn") dest_group_dn = widgets.HiddenField(name="dest_group_dn") diff --git a/ipa-server/ipa-gui/ipagui/forms/group.py b/ipa-server/ipa-gui/ipagui/forms/group.py index 380c904a4..b67156641 100644 --- a/ipa-server/ipa-gui/ipagui/forms/group.py +++ b/ipa-server/ipa-gui/ipagui/forms/group.py @@ -1,14 +1,18 @@ import turbogears from turbogears import validators, widgets +from tg_expanding_form_widget.tg_expanding_form_widget import ExpandingForm class GroupFields(): cn = widgets.TextField(name="cn", label="Name") gidnumber = widgets.TextField(name="gidnumber", label="GID") description = widgets.TextField(name="description", label="Description") - cn_hidden = widgets.HiddenField(name="cn") editprotected_hidden = widgets.HiddenField(name="editprotected") + nsAccountLock = widgets.SingleSelectField(name="nsAccountLock", + label="Group Status", + options = [("", "active"), ("true", "inactive")]) + group_orig = widgets.HiddenField(name="group_orig") member_data = widgets.HiddenField(name="member_data") dn_to_info_json = widgets.HiddenField(name="dn_to_info_json") @@ -37,6 +41,7 @@ class GroupNewForm(widgets.Form): class GroupEditValidator(validators.Schema): + cn = validators.String(not_empty=True) gidnumber = validators.Int(not_empty=False) description = validators.String(not_empty=False) @@ -48,7 +53,7 @@ class GroupEditForm(widgets.Form): params = ['members', 'group_fields'] hidden_fields = [ - GroupFields.cn_hidden, GroupFields.editprotected_hidden, + GroupFields.editprotected_hidden, GroupFields.group_orig, GroupFields.member_data, GroupFields.dn_to_info_json ] diff --git a/ipa-server/ipa-gui/ipagui/forms/ipapolicy.py b/ipa-server/ipa-gui/ipagui/forms/ipapolicy.py new file mode 100644 index 000000000..ec0e8c6f8 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/forms/ipapolicy.py @@ -0,0 +1,59 @@ +import turbogears +from turbogears import validators, widgets + +class IPAPolicyFields(): + # From cn=ipaConfig + ipausersearchfields = widgets.TextField(name="ipausersearchfields", label="User Search Fields") + ipagroupsearchfields = widgets.TextField(name="ipagroupsearchfields", label="Group Search Fields") + ipasearchtimelimit = widgets.TextField(name="ipasearchtimelimit", label="Search Time Limit (sec.)", attrs=dict(size=6,maxlength=6)) + ipasearchrecordslimit = widgets.TextField(name="ipasearchrecordslimit", label="Search Records Limit", attrs=dict(size=6,maxlength=6)) + ipahomesrootdir = widgets.TextField(name="ipahomesrootdir", label="Root for Home Directories") + ipadefaultloginshell = widgets.TextField(name="ipadefaultloginshell", label="Default shell") + ipadefaultprimarygroup = widgets.TextField(name="ipadefaultprimarygroup", label="Default Users group") + ipamaxusernamelength = widgets.TextField(name="ipamaxusernamelength", label="Max. Username Length", attrs=dict(size=3,maxlength=3)) + ipapwdexpadvnotify = widgets.TextField(name="ipapwdexpadvnotify", label="Password Expiration Notification (days)", attrs=dict(size=3,maxlength=3)) + + ipapolicy_orig = widgets.HiddenField(name="ipapolicy_orig") + + # From cn=accounts + krbmaxpwdlife = widgets.TextField(name="krbmaxpwdlife", label="Max. Password Lifetime", attrs=dict(size=3,maxlength=3)) + krbminpwdlife = widgets.TextField(name="krbminpwdlife", label="Min. Password Lifetime", attrs=dict(size=3,maxlength=3)) + krbpwdmindiffchars = widgets.TextField(name="krbpwdmindiffchars", label="Min. number of character classes", attrs=dict(size=3,maxlength=3)) + krbpwdminlength = widgets.TextField(name="krbpwdminlength", label="Min. Length of password", attrs=dict(size=3,maxlength=3)) + krbpwdhistorylength = widgets.TextField(name="krbpwdhistorylength", label="Password History size", attrs=dict(size=3,maxlength=3)) + + password_orig = widgets.HiddenField(name="password_orig") + +class IPAPolicyValidator(validators.Schema): + ipausersearchfields = validators.String(not_empty=True) + ipagroupsearchfields = validators.String(not_empty=True) + ipasearchtimelimit = validators.Number(not_empty=True) + ipasearchrecordslimit = validators.Number(not_empty=True) + ipamaxusernamelength = validators.Number(not_empty=True) + ipapwdexpadvnotify = validators.Number(not_empty=True) + ipahomesrootdir = validators.String(not_empty=True) + ipadefaultloginshell = validators.String(not_empty=True) + ipadefaultprimarygroup = validators.String(not_empty=True) + krbmaxpwdlife = validators.Number(not_empty=True) + krbminpwdlife = validators.Number(not_empty=True) + krbpwdmindiffchars = validators.Number(not_empty=True) + krbpwdminlength = validators.Number(not_empty=True) + krbpwdhistorylength = validators.Number(not_empty=True) + +class IPAPolicyForm(widgets.Form): + params = ['ipapolicy_fields'] + + hidden_fields = [ + IPAPolicyFields.ipapolicy_orig, IPAPolicyFields.password_orig + ] + + validator = IPAPolicyValidator() + + def __init__(self, *args, **kw): + super(IPAPolicyForm,self).__init__(*args, **kw) + (self.template_c, self.template) = widgets.meta.load_kid_template( + "ipagui.templates.ipapolicyeditform") + self.ipapolicy_fields = IPAPolicyFields + + def update_params(self, params): + super(IPAPolicyForm,self).update_params(params) diff --git a/ipa-server/ipa-gui/ipagui/forms/user.py b/ipa-server/ipa-gui/ipagui/forms/user.py index 1a35b4e07..7d3d37193 100644 --- a/ipa-server/ipa-gui/ipagui/forms/user.py +++ b/ipa-server/ipa-gui/ipagui/forms/user.py @@ -1,10 +1,12 @@ import turbogears from turbogears import validators, widgets +from tg_expanding_form_widget.tg_expanding_form_widget import ExpandingForm class UserFields(): givenname = widgets.TextField(name="givenname", label="Given Name") sn = widgets.TextField(name="sn", label="Family Name") cn = widgets.TextField(name="cn", label="Common Names") + cns = ExpandingForm(name="cns", label="Common Names", fields=[cn]) title = widgets.TextField(name="title", label="Title") displayname = widgets.TextField(name="displayname", label="Display Name") initials = widgets.TextField(name="initials", label="Initials") @@ -21,11 +23,16 @@ class UserFields(): mail = widgets.TextField(name="mail", label="E-mail Address") telephonenumber = widgets.TextField(name="telephonenumber", label="Work Number") + telephonenumbers = ExpandingForm(name="telephonenumbers", label="Work Numbers", fields=[telephonenumber]) facsimiletelephonenumber = widgets.TextField(name="facsimiletelephonenumber", label="Fax Number") + facsimiletelephonenumbers = ExpandingForm(name="facsimiletelephonenumbers", label="Fax Numbers", fields=[facsimiletelephonenumber]) mobile = widgets.TextField(name="mobile", label="Cell Number") + mobiles = ExpandingForm(name="mobiles", label="Cell Numbers", fields=[mobile]) pager = widgets.TextField(name="pager", label="Pager Number") + pagers = ExpandingForm(name="pagers", label="Pager Numbers", fields=[pager]) homephone = widgets.TextField(name="homephone", label="Home Number") + homephones = ExpandingForm(name="homephones", label="Home Numbers", fields=[homephone]) street = widgets.TextField(name="street", label="Street Address") l = widgets.TextField(name="l", label="City") @@ -67,7 +74,8 @@ class UserNewValidator(validators.Schema): userpassword_confirm = validators.String(not_empty=False) givenname = validators.String(not_empty=True) sn = validators.String(not_empty=True) - mail = validators.Email(not_empty=True) + cn = validators.ForEach(validators.String(not_empty=True)) + mail = validators.Email(not_empty=False) chained_validators = [ validators.FieldsMatch('userpassword', 'userpassword_confirm') @@ -102,6 +110,7 @@ class UserEditValidator(validators.Schema): userpassword_confirm = validators.String(not_empty=False) givenname = validators.String(not_empty=True) sn = validators.String(not_empty=True) + cn = validators.ForEach(validators.String(not_empty=True)) mail = validators.Email(not_empty=True) uidnumber = validators.Int(not_empty=False) gidnumber = validators.Int(not_empty=False) diff --git a/ipa-server/ipa-gui/ipagui/helpers/userhelper.py b/ipa-server/ipa-gui/ipagui/helpers/userhelper.py index e1ade3a2c..52d37c9e4 100644 --- a/ipa-server/ipa-gui/ipagui/helpers/userhelper.py +++ b/ipa-server/ipa-gui/ipagui/helpers/userhelper.py @@ -13,7 +13,7 @@ def password_expires_in(datestr): if not expdate: return sys.maxint - delta = expdate - datetime.datetime.now() + delta = expdate - datetime.datetime.now(ipautil.GeneralizedTimeZone()) return delta.days def password_is_expired(days): diff --git a/ipa-server/ipa-gui/ipagui/proxyprovider.py b/ipa-server/ipa-gui/ipagui/proxyprovider.py index e8ef69830..bd9cf87a8 100644 --- a/ipa-server/ipa-gui/ipagui/proxyprovider.py +++ b/ipa-server/ipa-gui/ipagui/proxyprovider.py @@ -2,6 +2,11 @@ from turbogears.identity.soprovider import * from turbogears.identity.visitor import * import logging import os +import ipa.ipaclient +from ipaserver import funcs +import ipa.config +import ipa.group +import ipa.user log = logging.getLogger("turbogears.identity") @@ -15,7 +20,25 @@ class IPA_User(object): (principal, realm) = user_name.split('@') self.display_name = principal self.permissions = None - self.groups = None + transport = funcs.IPAServer() + client = ipa.ipaclient.IPAClient(transport) + client.set_krbccache(os.environ["KRB5CCNAME"]) + try: + user = client.get_user_by_principal(user_name, ['dn']) + self.groups = [] + groups = client.get_groups_by_member(user.dn, ['dn', 'cn']) + if isinstance(groups, str): + groups = [groups] + for ginfo in groups: + # cn may be multi-valued, add them all just in case + cn = ginfo.getValue('cn') + if isinstance(cn, str): + cn = [cn] + for c in cn: + self.groups.append(c) + except: + raise + return class ProxyIdentity(object): @@ -57,7 +80,7 @@ class ProxyIdentity(object): def _get_groups(self): try: - return self._groups + return self._user.groups except AttributeError: # Groups haven't been computed yet return None @@ -87,10 +110,14 @@ class ProxyIdentityProvider(SqlObjectIdentityProvider): pass def validate_identity(self, user_name, password, visit_key): - user = IPA_User(user_name) - log.debug( "validate_identity %s" % user_name) - - return ProxyIdentity(visit_key, user) + try: + user = IPA_User(user_name) + log.debug( "validate_identity %s" % user_name) + return ProxyIdentity(visit_key, user) + except: + # Something went wrong in fetching the user. Set to + # anonymous which will deny access. + return ProxyIdentity( None ) def validate_password(self, user, user_name, password): '''Validation has already occurred in the proxy''' diff --git a/ipa-server/ipa-gui/ipagui/static/css/style.css b/ipa-server/ipa-gui/ipagui/static/css/style.css index 28f501352..1a7cbb1fb 100644 --- a/ipa-server/ipa-gui/ipagui/static/css/style.css +++ b/ipa-server/ipa-gui/ipagui/static/css/style.css @@ -383,3 +383,22 @@ ul.checkboxlist li input { #inactive { background-color: silver; } + +/* + * * TableKit css + * + */ + +.sortcol { + cursor: pointer; + padding-right: 20px !important; + background-repeat: no-repeat !important; + background-position: right center !important; + text-decoration: underline; +} +.sortasc { + background-image: url(/static/images/up.gif) !important; +} +.sortdesc { + background-image: url(/static/images/down.gif) !important; +} diff --git a/ipa-server/ipa-gui/ipagui/subcontrollers/Makefile.am b/ipa-server/ipa-gui/ipagui/subcontrollers/Makefile.am index d409bac7d..2f596f2ef 100644 --- a/ipa-server/ipa-gui/ipagui/subcontrollers/Makefile.am +++ b/ipa-server/ipa-gui/ipagui/subcontrollers/Makefile.am @@ -5,6 +5,8 @@ app_PYTHON = \ __init__.py \ group.py \ ipacontroller.py \ + ipapolicy.py \ + policy.py \ user.py \ delegation.py \ $(NULL) diff --git a/ipa-server/ipa-gui/ipagui/subcontrollers/delegation.py b/ipa-server/ipa-gui/ipagui/subcontrollers/delegation.py index 1515b04c1..142d34430 100644 --- a/ipa-server/ipa-gui/ipagui/subcontrollers/delegation.py +++ b/ipa-server/ipa-gui/ipagui/subcontrollers/delegation.py @@ -19,6 +19,7 @@ import ipagui.forms.delegate import ipa.aci import ldap.dn +import operator log = logging.getLogger(__name__) @@ -34,7 +35,7 @@ class DelegationController(IPAController): raise turbogears.redirect("/delegate/list") @expose("ipagui.templates.delegatenew") - @identity.require(identity.not_anonymous()) + @identity.require(identity.in_group("admins")) def new(self): """Display delegate page""" client = self.get_ipaclient() @@ -45,7 +46,7 @@ class DelegationController(IPAController): return dict(form=delegate_form, delegate=delegate) @expose() - @identity.require(identity.not_anonymous()) + @identity.require(identity.in_group("admins")) def create(self, **kw): """Creates a new delegation""" self.restrict_post() @@ -63,11 +64,34 @@ class DelegationController(IPAController): tg_template='ipagui.templates.delegatenew') try: + aci_entry = client.get_aci_entry(aci_fields) + new_aci = ipa.aci.ACI() new_aci.name = kw.get('name') new_aci.source_group = kw.get('source_group_dn') new_aci.dest_group = kw.get('dest_group_dn') new_aci.attrs = kw.get('attrs') + if (new_aci.attrs, str): + new_aci.attrs = [new_aci.attrs] + + # Look for an existing ACI of the same name + aci_str_list = aci_entry.getValues('aci') + if aci_str_list is None: + aci_str_list = [] + if not(isinstance(aci_str_list,list) or isinstance(aci_str_list,tuple)): + aci_str_list = [aci_str_list] + + for aci_str in aci_str_list: + try: + old_aci = ipa.aci.ACI(aci_str) + if old_aci.name == new_aci.name: + turbogears.flash("Delgate add failed: a delegation of that name already exists") + return dict(form=delegate_form, delegate=kw, + tg_template='ipagui.templates.delegatenew') + except SyntaxError: + # ignore aci_str's that ACI can't parse + pass + # not pulling down existing aci attributes aci_entry = client.get_aci_entry(['dn']) @@ -75,7 +99,7 @@ class DelegationController(IPAController): client.update_entry(aci_entry) except ipaerror.IPAError, e: - turbogears.flash("Delgate add failed: " + str(e)) + turbogears.flash("Delgate add failed: " + str(e) + "<br/>" + e.detail[0]['desc']) return dict(form=delegate_form, delegate=kw, tg_template='ipagui.templates.delegatenew') @@ -83,7 +107,7 @@ class DelegationController(IPAController): raise turbogears.redirect('/delegate/list') @expose("ipagui.templates.delegateedit") - @identity.require(identity.not_anonymous()) + @identity.require(identity.in_group("admins")) def edit(self, acistr, tg_errors=None): """Display delegate page""" if tg_errors: @@ -105,12 +129,12 @@ class DelegationController(IPAController): return dict(form=delegate_form, delegate=delegate) except (SyntaxError, ipaerror.IPAError), e: - turbogears.flash("Delegation edit failed: " + str(e)) + turbogears.flash("Delegation edit failed: " + str(e) + "<br/>" + e.detail[0]['desc']) raise turbogears.redirect('/delegate/list') @expose() - @identity.require(identity.not_anonymous()) + @identity.require(identity.in_group("admins")) def update(self, **kw): """Display delegate page""" self.restrict_post() @@ -162,7 +186,7 @@ class DelegationController(IPAController): turbogears.flash("delegate updated") raise turbogears.redirect('/delegate/list') except (SyntaxError, ipaerror.IPAError), e: - turbogears.flash("Delegation update failed: " + str(e)) + turbogears.flash("Delegation update failed: " + str(e) + "<br/>" + e.detail[0]['desc']) return dict(form=delegate_form, delegate=kw, tg_template='ipagui.templates.delegateedit') @@ -175,7 +199,7 @@ class DelegationController(IPAController): try: aci_entry = client.get_aci_entry(aci_fields) except ipaerror.IPAError, e: - turbogears.flash("Delegation list failed: " + str(e)) + turbogears.flash("Delegation list failed: " + str(e) + "<br/>" + e.detail[0]['desc']) raise turbogears.redirect('/') aci_str_list = aci_entry.getValues('aci') @@ -194,6 +218,7 @@ class DelegationController(IPAController): pass group_dn_to_cn = ipa.aci.extract_group_cns(aci_list, client) + aci_list = sorted(aci_list, key=operator.itemgetter(0)) # The list page needs to display field labels, not raw # LDAP attributes for aci in aci_list: @@ -205,7 +230,7 @@ class DelegationController(IPAController): fields=ipagui.forms.delegate.DelegateFields()) @expose() - @identity.require(identity.not_anonymous()) + @identity.require(identity.in_group("admins")) def delete(self, acistr): """Display delegate page""" self.restrict_post() @@ -237,7 +262,7 @@ class DelegationController(IPAController): turbogears.flash("delegate deleted") raise turbogears.redirect('/delegate/list') except (SyntaxError, ipaerror.IPAError), e: - turbogears.flash("Delegation deletion failed: " + str(e)) + turbogears.flash("Delegation deletion failed: " + str(e) + "<br/>" + e.detail[0]['desc']) raise turbogears.redirect('/delegate/list') @expose("ipagui.templates.delegategroupsearch") diff --git a/ipa-server/ipa-gui/ipagui/subcontrollers/group.py b/ipa-server/ipa-gui/ipagui/subcontrollers/group.py index f0574a21c..dbcc77b9a 100644 --- a/ipa-server/ipa-gui/ipagui/subcontrollers/group.py +++ b/ipa-server/ipa-gui/ipagui/subcontrollers/group.py @@ -22,7 +22,7 @@ log = logging.getLogger(__name__) group_new_form = ipagui.forms.group.GroupNewForm() group_edit_form = ipagui.forms.group.GroupEditForm() -group_fields = ['*'] +group_fields = ['*', 'nsAccountLock'] class GroupController(IPAController): @@ -37,7 +37,7 @@ class GroupController(IPAController): raise turbogears.redirect("/group/list") @expose("ipagui.templates.groupnew") - @identity.require(identity.not_anonymous()) + @identity.require(identity.in_group("admins")) def new(self, tg_errors=None): """Displays the new group form""" if tg_errors: @@ -49,7 +49,7 @@ class GroupController(IPAController): return dict(form=group_new_form, group={}) @expose() - @identity.require(identity.not_anonymous()) + @identity.require(identity.in_group("admins")) def create(self, **kw): """Creates a new group""" self.restrict_post() @@ -75,13 +75,16 @@ class GroupController(IPAController): new_group.setValue('description', kw.get('description')) rv = client.add_group(new_group) + + if kw.get('nsAccountLock'): + client.mark_group_inactive(kw.get('cn')) except ipaerror.exception_for(ipaerror.LDAP_DUPLICATE): turbogears.flash("Group with name '%s' already exists" % kw.get('cn')) return dict(form=group_new_form, group=kw, tg_template='ipagui.templates.groupnew') except ipaerror.IPAError, e: - turbogears.flash("Group add failed: " + str(e) + "<br/>" + str(e.detail)) + turbogears.flash("Group add failed: " + str(e) + "<br/>" + e.detail[0]['desc']) return dict(form=group_new_form, group=kw, tg_template='ipagui.templates.groupnew') @@ -90,7 +93,11 @@ class GroupController(IPAController): # on any error, we redirect to the _edit_ group page. # this code does data setup, similar to groupedit() # - group = client.get_entry_by_cn(kw['cn'], group_fields) + if isinstance(kw['cn'], list): + cn0 = kw['cn'][0] + else: + cn0 = kw['cn'] + group = client.get_entry_by_cn(cn0, group_fields) group_dict = group.toDict() member_dicts = [] @@ -166,7 +173,7 @@ class GroupController(IPAController): @expose("ipagui.templates.groupedit") - @identity.require(identity.not_anonymous()) + @identity.require(identity.in_group("admins")) def edit(self, cn, tg_errors=None): """Displays the edit group form""" if tg_errors: @@ -204,20 +211,31 @@ class GroupController(IPAController): raise turbogears.redirect('/group/show', uid=cn) @expose() - @identity.require(identity.not_anonymous()) + @identity.require(identity.in_group("admins")) def update(self, **kw): """Updates an existing group""" self.restrict_post() client = self.get_ipaclient() if kw.get('submit') == 'Cancel Edit': + orig_group_dict = loads(b64decode(kw.get('group_orig'))) + # if cancelling need to use the original group because the one + # in kw may not exist yet. + cn = orig_group_dict.get('cn') + if (isinstance(cn,str)): + cn = [cn] turbogears.flash("Edit group cancelled") - raise turbogears.redirect('/group/show', cn=kw.get('cn')) + raise turbogears.redirect('/group/show', cn=cn[0]) + + if kw.get('editprotected') == '': + # if editprotected set these don't get sent in kw + orig_group_dict = loads(b64decode(kw.get('group_orig'))) + kw['cn'] = orig_group_dict['cn'] + kw['gidnumber'] = orig_group_dict['gidnumber'] # Decode the member data, in case we need to round trip member_dicts = loads(b64decode(kw.get('member_data'))) - tg_errors, kw = self.groupupdatevalidate(**kw) if tg_errors: turbogears.flash("There were validation errors.<br/>" + @@ -242,6 +260,20 @@ class GroupController(IPAController): if new_group.gidnumber != new_gid: group_modified = True new_group.setValue('gidnumber', new_gid) + else: + new_group.setValue('gidnumber', orig_group_dict.get('gidnumber')) + new_group.setValue('cn', orig_group_dict.get('cn')) + if new_group.cn != kw.get('cn'): + group_modified = True + new_group.setValue('cn', kw['cn']) + + if group_modified: + rv = client.update_group(new_group) + # + # If the group update succeeds, but below operations fail, we + if new_group.cn != kw.get('cn'): + group_modified = True + new_group.setValue('cn', kw['cn']) if group_modified: rv = client.update_group(new_group) @@ -252,10 +284,21 @@ class GroupController(IPAController): # kw['group_orig'] = b64encode(dumps(new_group.toDict())) except ipaerror.IPAError, e: - turbogears.flash("Group update failed: " + str(e)) + turbogears.flash("Group update failed: " + str(e) + "<br/>" + e.detail[0]['desc']) return dict(form=group_edit_form, group=kw, members=member_dicts, tg_template='ipagui.templates.groupedit') + if kw.get('nsAccountLock') == '': + kw['nsAccountLock'] = "false" + + modify_no_update = False + if kw.get('nsAccountLock') == "false" and new_group.getValues('nsaccountlock') == "true": + client.mark_group_active(kw.get('cn')) + modify_no_update = True + elif kw.get('nsAccountLock') == "true" and new_group.nsaccountlock != "true": + client.mark_group_inactive(kw.get('cn')) + modify_no_update = True + # # Add members # @@ -268,8 +311,9 @@ class GroupController(IPAController): failed_adds = client.add_members_to_group( utf8_encode_values(dnadds), new_group.dn) kw['dnadd'] = failed_adds + group_modified = True except ipaerror.IPAError, e: - turbogears.flash("Group update failed: " + str(e)) + turbogears.flash("Group update failed: " + str(e) + "<br/>" + e.detail[0]['desc']) return dict(form=group_edit_form, group=kw, members=member_dicts, tg_template='ipagui.templates.groupedit') @@ -285,8 +329,9 @@ class GroupController(IPAController): failed_dels = client.remove_members_from_group( utf8_encode_values(dndels), new_group.dn) kw['dndel'] = failed_dels + group_modified = True except ipaerror.IPAError, e: - turbogears.flash("Group update failed: " + str(e)) + turbogears.flash("Group update failed: " + str(e) + "<br/>" + e.detail[0]['desc']) return dict(form=group_edit_form, group=kw, members=member_dicts, tg_template='ipagui.templates.groupedit') @@ -308,8 +353,15 @@ class GroupController(IPAController): return dict(form=group_edit_form, group=kw, members=member_dicts, tg_template='ipagui.templates.groupedit') - turbogears.flash("%s updated!" % kw['cn']) - raise turbogears.redirect('/group/show', cn=kw['cn']) + if isinstance(kw['cn'], list): + cn0 = kw['cn'][0] + else: + cn0 = kw['cn'] + if group_modified == True or modify_no_update == True: + turbogears.flash("%s updated!" % cn0) + else: + turbogears.flash("No modifications requested.") + raise turbogears.redirect('/group/show', cn=cn0) @expose("ipagui.templates.grouplist") @@ -330,7 +382,7 @@ class GroupController(IPAController): turbogears.flash("These results are truncated.<br />" + "Please refine your search and try again.") except ipaerror.IPAError, e: - turbogears.flash("Find groups failed: " + str(e)) + turbogears.flash("Find groups failed: " + str(e) + "<br/>" + e.detail[0]['desc']) raise turbogears.redirect("/group/list") return dict(groups=groups, criteria=criteria, @@ -374,7 +426,7 @@ class GroupController(IPAController): turbogears.flash("group deleted") raise turbogears.redirect('/group/list') except (SyntaxError, ipaerror.IPAError), e: - turbogears.flash("Group deletion failed: " + str(e) + "<br/>" + str(e.detail)) + turbogears.flash("Group deletion failed: " + str(e) + "<br/>" + e.detail[0]['desc']) raise turbogears.redirect('/group/list') @validate(form=group_new_form) diff --git a/ipa-server/ipa-gui/ipagui/subcontrollers/ipapolicy.py b/ipa-server/ipa-gui/ipagui/subcontrollers/ipapolicy.py new file mode 100644 index 000000000..781ca35d4 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/subcontrollers/ipapolicy.py @@ -0,0 +1,168 @@ +import os +from pickle import dumps, loads +from base64 import b64encode, b64decode +import copy +import logging + +import cherrypy +import turbogears +from turbogears import controllers, expose, flash +from turbogears import validators, validate +from turbogears import widgets, paginate +from turbogears import error_handler +from turbogears import identity + +from ipacontroller import IPAController +from ipa.entity import utf8_encode_values +from ipa import ipaerror +import ipa.entity +import ipagui.forms.ipapolicy + +import ldap.dn + +log = logging.getLogger(__name__) + +ipapolicy_edit_form = ipagui.forms.ipapolicy.IPAPolicyForm() + +class IPAPolicyController(IPAController): + + @expose() + @identity.require(identity.in_group("admins")) + def index(self): + raise turbogears.redirect("/ipapolicy/show") + + @expose("ipagui.templates.ipapolicyshow") + @identity.require(identity.in_group("admins")) + def show(self, tg_errors=None): + """Displays the one policy page""" + client = self.get_ipaclient() + config = client.get_ipa_config() + ipapolicy = config.toDict() + + ppolicy = client.get_password_policy() + password = ppolicy.toDict() + + return dict(ipapolicy=ipapolicy,password=password,fields=ipagui.forms.ipapolicy.IPAPolicyFields()) + + @expose("ipagui.templates.ipapolicyedit") + @identity.require(identity.in_group("admins")) + def edit(self, tg_errors=None): + """Displays the edit IPA policy form""" + if tg_errors: + turbogears.flash("There were validation errors.<br/>" + + "Please see the messages below for details.") + + try: + client = self.get_ipaclient() + config = client.get_ipa_config() + ipapolicy_dict = config.toDict() + + ppolicy = client.get_password_policy() + password_dict = ppolicy.toDict() + + # store a copy of the original policy for the update later + ipapolicy_data = b64encode(dumps(ipapolicy_dict)) + ipapolicy_dict['ipapolicy_orig'] = ipapolicy_data + + # store a copy of the original policy for the update later + password_data = b64encode(dumps(password_dict)) + password_dict['password_orig'] = password_data + + # Combine the 2 dicts to make the form easier + ipapolicy_dict.update(password_dict) + + return dict(form=ipapolicy_edit_form, ipapolicy=ipapolicy_dict) + except ipaerror.IPAError, e: + turbogears.flash("IPA Policy edit failed: " + str(e) + "<br/>" + str(e.detail)) + raise turbogears.redirect('/ipapolicy/show') + + + @expose() + @identity.require(identity.in_group("admins")) + def update(self, **kw): + """Display delegate page""" + self.restrict_post() + client = self.get_ipaclient() + + if kw.get('submit', '').startswith('Cancel'): + turbogears.flash("Edit policy cancelled") + raise turbogears.redirect('/ipapolicy/show') + + tg_errors, kw = self.ipapolicyupdatevalidate(**kw) + if tg_errors: + turbogears.flash("There were validation errors.<br/>" + + "Please see the messages below for details.") + return dict(form=ipapolicy_edit_form, ipapolicy=kw, + tg_template='ipagui.templates.ipapolicyedit') + + policy_modified = False + password_modified = False + + try: + orig_ipapolicy_dict = loads(b64decode(kw.get('ipapolicy_orig'))) + orig_password_dict = loads(b64decode(kw.get('password_orig'))) + + new_ipapolicy = ipa.entity.Entity(orig_ipapolicy_dict) + new_password = ipa.entity.Entity(orig_password_dict) + + if str(new_ipapolicy.ipasearchtimelimit) != str(kw.get('ipasearchtimelimit')): + policy_modified = True + new_ipapolicy.setValue('ipasearchtimelimit', kw.get('ipasearchtimelimit')) + if str(new_ipapolicy.ipasearchrecordslimit) != str(kw.get('ipasearchrecordslimit')): + policy_modified = True + new_ipapolicy.setValue('ipasearchrecordslimit', kw.get('ipasearchrecordslimit')) + if new_ipapolicy.ipausersearchfields != kw.get('ipausersearchfields'): + policy_modified = True + new_ipapolicy.setValue('ipausersearchfields', kw.get('ipausersearchfields')) + if new_ipapolicy.ipagroupsearchfields != kw.get('ipagroupsearchfields'): + policy_modified = True + new_ipapolicy.setValue('ipagroupsearchfields', kw.get('ipagroupsearchfields')) + if str(new_ipapolicy.ipapwdexpadvnotify) != str(kw.get('ipapwdexpadvnotify')): + policy_modified = True + new_ipapolicy.setValue('ipapwdexpadvnotify', kw.get('ipapwdexpadvnotify')) + if str(new_ipapolicy.ipamaxusernamelength) != str(kw.get('ipamaxusernamelength')): + policy_modified = True + new_ipapolicy.setValue('ipamaxusernamelength', kw.get('ipamaxusernamelength')) + if new_ipapolicy.ipahomesrootdir != kw.get('ipahomesrootdir'): + policy_modified = True + new_ipapolicy.setValue('ipahomesrootdir', kw.get('ipahomesrootdir')) + if new_ipapolicy.ipadefaultloginshell != kw.get('ipadefaultloginshell'): + policy_modified = True + new_ipapolicy.setValue('ipadefaultloginshell', kw.get('ipadefaultloginshell')) + if new_ipapolicy.ipadefaultprimarygroup != kw.get('ipadefaultprimarygroup'): + policy_modified = True + new_ipapolicy.setValue('ipadefaultprimarygroup', kw.get('ipadefaultprimarygroup')) + + if policy_modified: + rv = client.update_ipa_config(new_ipapolicy) + + # Now check the password policy for updates + if str(new_password.krbmaxpwdlife) != str(kw.get('krbmaxpwdlife')): + password_modified = True + new_password.setValue('krbmaxpwdlife', str(kw.get('krbmaxpwdlife'))) + if str(new_password.krbminpwdlife) != str(kw.get('krbminpwdlife')): + password_modified = True + new_password.setValue('krbminpwdlife', str(kw.get('krbminpwdlife'))) + if str(new_password.krbpwdhistorylength) != str(kw.get('krbpwdhistorylength')): + password_modified = True + new_password.setValue('krbpwdhistorylength', str(kw.get('krbpwdhistorylength'))) + if str(new_password.krbpwdmindiffchars) != str(kw.get('krbpwdmindiffchars')): + password_modified = True + new_password.setValue('krbpwdmindiffchars', str(kw.get('krbpwdmindiffchars'))) + if str(new_password.krbpwdminlength) != str(kw.get('krbpwdminlength')): + password_modified = True + new_password.setValue('krbpwdminlength', str(kw.get('krbpwdminlength'))) + if password_modified: + rv = client.update_password_policy(new_password) + + turbogears.flash("IPA Policy updated") + raise turbogears.redirect('/ipapolicy/show') + except ipaerror.IPAError, e: + turbogears.flash("Policy update failed: " + str(e) + e.detail[0]['desc']) + return dict(form=ipapolicy_edit_form, ipapolicy=kw, + tg_template='ipagui.templates.ipapolicyedit') + + @validate(form=ipapolicy_edit_form) + @identity.require(identity.not_anonymous()) + def ipapolicyupdatevalidate(self, tg_errors=None, **kw): + return tg_errors, kw diff --git a/ipa-server/ipa-gui/ipagui/subcontrollers/policy.py b/ipa-server/ipa-gui/ipagui/subcontrollers/policy.py new file mode 100644 index 000000000..1f2e45876 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/subcontrollers/policy.py @@ -0,0 +1,32 @@ +import os +from pickle import dumps, loads +from base64 import b64encode, b64decode +import copy +import logging + +import cherrypy +import turbogears +from turbogears import controllers, expose, flash +from turbogears import validators, validate +from turbogears import widgets, paginate +from turbogears import error_handler +from turbogears import identity + +from ipacontroller import IPAController +from ipa.entity import utf8_encode_values +from ipa import ipaerror + +import ldap.dn + +log = logging.getLogger(__name__) + +class PolicyController(IPAController): + + @expose("ipagui.templates.policyindex") + @identity.require(identity.in_group("admins")) + def index(self, tg_errors=None): + """Displays the one policy page""" + + # TODO: return a dict of the items and URLs to display on + # Manage Policy + return dict() diff --git a/ipa-server/ipa-gui/ipagui/subcontrollers/user.py b/ipa-server/ipa-gui/ipagui/subcontrollers/user.py index d328052b1..39343b595 100644 --- a/ipa-server/ipa-gui/ipagui/subcontrollers/user.py +++ b/ipa-server/ipa-gui/ipagui/subcontrollers/user.py @@ -34,26 +34,48 @@ class UserController(IPAController): def __init__(self, *args, **kw): super(UserController,self).__init__(*args, **kw) - self.load_custom_fields() +# self.load_custom_fields() def load_custom_fields(self): - # client = self.get_ipaclient() - # schema = client.get_user_custom_schema() - schema = [ - { 'label': 'See Also', - 'field': 'seeAlso', - 'required': 'true', } , - { 'label': 'O O O', - 'field': 'o', - 'required': 'false', } , - ] + + client = self.get_ipaclient() + schema = client.get_custom_fields() + + # FIXME: Don't load from LDAP every single time it is called + + # FIXME: Is removing the attributes on the fly thread-safe? Do we + # need to lock here? for s in schema: required=False - if (s['required'] == "true"): + if (s['required'].lower() == "true"): required=True field = widgets.TextField(name=s['field'],label=s['label']) validator = validators.String(not_empty=required) + # Don't allow dupes on the new form + try: + for i in range(len(user_new_form.custom_fields)): + if user_new_form.custom_fields[i].name == s['field']: + user_new_form.custom_fields.pop(i) + except: + pass + + # Don't allow dupes on the edit form + try: + for i in range(len(user_edit_form.custom_fields)): + if user_edit_form.custom_fields[i].name == s['field']: + user_edit_form.custom_fields.pop(i) + except: + pass + + # Don't allow dupes in the list of user fields + try: + for i in range(len(ipagui.forms.user.UserFields.custom_fields)): + if ipagui.forms.user.UserFields.custom_fields[i].name == s['field']: + ipagui.forms.user.UserFields.custom_fields.pop(i) + except: + pass + ipagui.forms.user.UserFields.custom_fields.append(field) user_new_form.custom_fields.append(field) user_edit_form.custom_fields.append(field) @@ -61,15 +83,45 @@ class UserController(IPAController): user_new_form.validator.add_field(s['field'], validator) user_edit_form.validator.add_field(s['field'], validator) + def setup_mv_fields(self, field, fieldname): + """Given a field (must be a list) and field name, convert that + field into a list of dictionaries of the form: + [ { fieldname : v1}, { fieldname : v2 }, .. ] + + This is how we pre-fill values for multi-valued fields. + """ + mvlist = [] + if field is not None: + for v in field: + mvlist.append({ fieldname : v } ) + else: + # We need to return an empty value so something can be + # displayed on the edit page. Otherwise only an Add link + # will show, not an empty field. + mvlist.append({ fieldname : '' } ) + return mvlist + + def fix_incoming_fields(self, fields, fieldname, multifieldname): + """This is called by the update() function. It takes the incoming + list of dictionaries and converts it into back into the original + field, then removes the multiple field. + """ + fields[fieldname] = [] + for i in range(len(fields[multifieldname])): + fields[fieldname].append(fields[multifieldname][i][fieldname]) + del(fields[multifieldname]) + + return fields @expose() def index(self): raise turbogears.redirect("/user/list") @expose("ipagui.templates.usernew") - @identity.require(identity.not_anonymous()) + @identity.require(identity.in_any_group("admins","editors")) def new(self, tg_errors=None): """Displays the new user form""" + self.load_custom_fields() if tg_errors: turbogears.flash("There were validation errors.<br/>" + "Please see the messages below for details.") @@ -77,7 +129,7 @@ class UserController(IPAController): return dict(form=user_new_form, user={}) @expose() - @identity.require(identity.not_anonymous()) + @identity.require(identity.in_any_group("admins","editors")) def create(self, **kw): """Creates a new user""" self.restrict_post() @@ -88,6 +140,15 @@ class UserController(IPAController): raise turbogears.redirect('/user/list') tg_errors, kw = self.usercreatevalidate(**kw) + + # Fix incoming multi-valued fields we created for the form + kw = self.fix_incoming_fields(kw, 'cn', 'cns') + kw = self.fix_incoming_fields(kw, 'telephonenumber', 'telephonenumbers') + kw = self.fix_incoming_fields(kw, 'facsimiletelephonenumber', 'facsimiletelephonenumbers') + kw = self.fix_incoming_fields(kw, 'mobile', 'mobiles') + kw = self.fix_incoming_fields(kw, 'pager', 'pagers') + kw = self.fix_incoming_fields(kw, 'homephone', 'homephones') + if tg_errors: turbogears.flash("There were validation errors.<br/>" + "Please see the messages below for details.") @@ -136,21 +197,21 @@ class UserController(IPAController): new_user.setValue('carlicense', kw.get('carlicense')) new_user.setValue('labeleduri', kw.get('labeleduri')) - if kw.get('nsAccountLock'): - new_user.setValue('nsAccountLock', 'true') - for custom_field in user_new_form.custom_fields: new_user.setValue(custom_field.name, kw.get(custom_field.name, '')) rv = client.add_user(new_user) + + if kw.get('nsAccountLock'): + client.mark_user_inactive(kw.get('uid')) except ipaerror.exception_for(ipaerror.LDAP_DUPLICATE): - turbogears.flash("Person with login '%s' already exists" % + turbogears.flash("User with login '%s' already exists" % kw.get('uid')) return dict(form=user_new_form, user=kw, tg_template='ipagui.templates.usernew') except ipaerror.IPAError, e: - turbogears.flash("User add failed: " + str(e)) + turbogears.flash("User add failed: " + str(e) + "<br/>" + e.detail[0]['desc']) return dict(form=user_new_form, user=kw, tg_template='ipagui.templates.usernew') @@ -181,7 +242,7 @@ class UserController(IPAController): try: client.modifyPassword(user_dict['krbprincipalname'], "", kw.get('userpassword')) except ipaerror.IPAError, e: - message = "Person successfully created.<br />" + message = "User successfully created.<br />" message += "There was an error setting the password.<br />" turbogears.flash(message) return dict(form=user_edit_form, user=user_dict, @@ -204,7 +265,7 @@ class UserController(IPAController): failed_adds = dnadds if len(failed_adds) > 0: - message = "Person successfully created.<br />" + message = "User successfully created.<br />" message += "There was an error adding groups.<br />" message += "Failures have been preserved in the add/remove lists." turbogears.flash(message) @@ -243,6 +304,7 @@ class UserController(IPAController): @identity.require(identity.not_anonymous()) def edit(self, uid=None, principal=None, tg_errors=None): """Displays the edit user form""" + self.load_custom_fields() if tg_errors: turbogears.flash("There were validation errors.<br/>" + "Please see the messages below for details.") @@ -259,6 +321,32 @@ class UserController(IPAController): turbogears.flash("User edit failed: No uid or principal provided") raise turbogears.redirect('/') user_dict = user.toDict() + + # Load potential multi-valued fields + if isinstance(user_dict['cn'], str): + user_dict['cn'] = [user_dict['cn']] + user_dict['cns'] = self.setup_mv_fields(user_dict['cn'], 'cn') + + if isinstance(user_dict.get('telephonenumber',''), str): + user_dict['telephonenumber'] = [user_dict.get('telephonenumber'),''] + user_dict['telephonenumbers'] = self.setup_mv_fields(user_dict.get('telephonenumber'), 'telephonenumber') + + if isinstance(user_dict.get('facsimiletelephonenumber',''), str): + user_dict['facsimiletelephonenumber'] = [user_dict.get('facsimiletelephonenumber'),''] + user_dict['facsimiletelephonenumbers'] = self.setup_mv_fields(user_dict.get('facsimiletelephonenumber'), 'facsimiletelephonenumber') + + if isinstance(user_dict.get('mobile',''), str): + user_dict['mobile'] = [user_dict.get('mobile'),''] + user_dict['mobiles'] = self.setup_mv_fields(user_dict.get('mobile'), 'mobile') + + if isinstance(user_dict.get('pager',''), str): + user_dict['pager'] = [user_dict.get('pager'),''] + user_dict['pagers'] = self.setup_mv_fields(user_dict.get('pager'), 'pager') + + if isinstance(user_dict.get('homephone',''), str): + user_dict['homephone'] = [user_dict.get('homephone'),''] + user_dict['homephones'] = self.setup_mv_fields(user_dict.get('homephone'), 'homephone') + # Edit shouldn't fill in the password field. if user_dict.has_key('userpassword'): del(user_dict['userpassword']) @@ -300,7 +388,7 @@ class UserController(IPAController): except ipaerror.IPAError, e: if uid is None: uid = principal - turbogears.flash("User edit failed: " + str(e)) + turbogears.flash("User edit failed: " + str(e) + "<br/>" + e.detail[0]['desc']) raise turbogears.redirect('/user/show', uid=uid) @expose() @@ -314,6 +402,23 @@ class UserController(IPAController): turbogears.flash("Edit user cancelled") raise turbogears.redirect('/user/show', uid=kw.get('uid')) + # Fix incoming multi-valued fields we created for the form + kw = self.fix_incoming_fields(kw, 'cn', 'cns') + kw = self.fix_incoming_fields(kw, 'telephonenumber', 'telephonenumbers') + kw = self.fix_incoming_fields(kw, 'facsimiletelephonenumber', 'facsimiletelephonenumbers') + kw = self.fix_incoming_fields(kw, 'mobile', 'mobiles') + kw = self.fix_incoming_fields(kw, 'pager', 'pagers') + kw = self.fix_incoming_fields(kw, 'homephone', 'homephones') + + # admins and editors can update anybody. A user can only update + # themselves. We need this check because it is very easy to guess + # the edit URI. + if ((not 'admins' in turbogears.identity.current.groups and + not 'editors' in turbogears.identity.current.groups) and + (kw.get('uid') != turbogears.identity.current.display_name)): + turbogears.flash("You do not have permission to update this user.") + raise turbogears.redirect('/user/show', uid=kw.get('uid')) + # Decode the group data, in case we need to round trip user_groups_dicts = loads(b64decode(kw.get('user_groups_data'))) @@ -334,6 +439,14 @@ class UserController(IPAController): try: orig_user_dict = loads(b64decode(kw.get('user_orig'))) + # remove multi-valued fields we created for the form + del(orig_user_dict['cns']) + del(orig_user_dict['telephonenumbers']) + del(orig_user_dict['facsimiletelephonenumbers']) + del(orig_user_dict['mobiles']) + del(orig_user_dict['pagers']) + del(orig_user_dict['homephones']) + new_user = ipa.user.User(orig_user_dict) new_user.setValue('title', kw.get('title')) new_user.setValue('givenname', kw.get('givenname')) @@ -369,12 +482,6 @@ class UserController(IPAController): new_user.setValue('carlicense', kw.get('carlicense')) new_user.setValue('labeleduri', kw.get('labeleduri')) - - if kw.get('nsAccountLock'): - new_user.setValue('nsAccountLock', 'true') - else: - new_user.setValue('nsAccountLock', None) - if kw.get('editprotected') == 'true': if kw.get('userpassword'): password_change = True @@ -400,7 +507,7 @@ class UserController(IPAController): # too much work to figure out unless someone really screams pass except ipaerror.IPAError, e: - turbogears.flash("User update failed: " + str(e)) + turbogears.flash("User update failed: " + str(e) + "<br/>" + e.detail[0]['desc']) return dict(form=user_edit_form, user=kw, user_groups=user_groups_dicts, tg_template='ipagui.templates.useredit') @@ -412,7 +519,7 @@ class UserController(IPAController): if password_change: rv = client.modifyPassword(kw['krbprincipalname'], "", kw.get('userpassword')) except ipaerror.IPAError, e: - turbogears.flash("User password change failed: " + str(e)) + turbogears.flash("User password change failed: " + str(e) + "<br/>" + e.detail[0]['desc']) return dict(form=user_edit_form, user=kw, user_groups=user_groups_dicts, tg_template='ipagui.templates.useredit') @@ -459,6 +566,20 @@ class UserController(IPAController): user_groups=user_groups_dicts, tg_template='ipagui.templates.useredit') + if kw.get('nsAccountLock') == '': + kw['nsAccountLock'] = "false" + + try: + if kw.get('nsAccountLock') == "false" and new_user.getValues('nsaccountlock') == "true": + client.mark_user_active(kw.get('uid')) + elif kw.get('nsAccountLock') == "true" and new_user.nsaccountlock != "true": + client.mark_user_inactive(kw.get('uid')) + except ipaerror.IPAError, e: + turbogears.flash("User status change failed: " + str(e) + "<br/>" + e.detail[0]['desc']) + return dict(form=user_edit_form, user=kw, + user_groups=user_groups_dicts, + tg_template='ipagui.templates.useredit') + turbogears.flash("%s updated!" % kw['uid']) raise turbogears.redirect('/user/show', uid=kw['uid']) @@ -481,7 +602,7 @@ class UserController(IPAController): turbogears.flash("These results are truncated.<br />" + "Please refine your search and try again.") except ipaerror.IPAError, e: - turbogears.flash("User list failed: " + str(e)) + turbogears.flash("User list failed: " + str(e) + "<br/>" + e.detail[0]['desc']) raise turbogears.redirect("/user/list") return dict(users=users, uid=uid, fields=ipagui.forms.user.UserFields()) @@ -492,6 +613,7 @@ class UserController(IPAController): def show(self, uid): """Retrieve a single user for display""" client = self.get_ipaclient() + self.load_custom_fields() try: user = client.get_user_by_uid(uid, user_fields) @@ -523,7 +645,7 @@ class UserController(IPAController): user_groups=user_groups, user_reports=user_reports, user_manager=user_manager, user_secretary=user_secretary) except ipaerror.IPAError, e: - turbogears.flash("User show failed: " + str(e)) + turbogears.flash("User show failed: " + str(e) + "<br/>" + e.detail[0]['desc']) raise turbogears.redirect("/") @expose() @@ -539,7 +661,7 @@ class UserController(IPAController): turbogears.flash("user deleted") raise turbogears.redirect('/user/list') except (SyntaxError, ipaerror.IPAError), e: - turbogears.flash("User deletion failed: " + str(e)) + turbogears.flash("User deletion failed: " + str(e) + "<br/>" + e.detail[0]['desc']) raise turbogears.redirect('/user/list') @validate(form=user_new_form) @@ -661,7 +783,7 @@ class UserController(IPAController): users_counter = users[0] users = users[1:] except ipaerror.IPAError, e: - turbogears.flash("search failed: " + str(e)) + turbogears.flash("search failed: " + str(e) + "<br/>" + e.detail[0]['desc']) return dict(users=users, criteria=criteria, which_select=kw.get('which_select'), diff --git a/ipa-server/ipa-gui/ipagui/templates/Makefile.am b/ipa-server/ipa-gui/ipagui/templates/Makefile.am index 18db5fffc..6626ad8c2 100644 --- a/ipa-server/ipa-gui/ipagui/templates/Makefile.am +++ b/ipa-server/ipa-gui/ipagui/templates/Makefile.am @@ -20,8 +20,13 @@ app_DATA = \ groupnewform.kid \ groupnew.kid \ groupshow.kid \ + ipapolicyeditform.kid \ + ipapolicyedit.kid \ + ipapolicyshow.kid \ loginfailed.kid \ master.kid \ + policyindex.kid \ + policylayout.kid \ usereditform.kid \ useredit.kid \ userlayout.kid \ diff --git a/ipa-server/ipa-gui/ipagui/templates/groupeditform.kid b/ipa-server/ipa-gui/ipagui/templates/groupeditform.kid index cab585fcc..6a5c5adb8 100644 --- a/ipa-server/ipa-gui/ipagui/templates/groupeditform.kid +++ b/ipa-server/ipa-gui/ipagui/templates/groupeditform.kid @@ -25,17 +25,22 @@ from ipagui.helpers import ipahelper <script type="text/javascript" charset="utf-8" src="${tg.url('/static/javascript/dynamicedit.js')}"></script> + <script type="text/javascript" charset="utf-8" + src="${tg.url('/tg_widgets/tg_expanding_form_widget/javascript/expanding_form.js')}"></script> <?python searchurl = tg.url('/group/edit_search') ?> <script type="text/javascript"> function toggleProtectedFields(checkbox) { var gidnumberField = $('form_gidnumber'); + var cnField = $('form_cn'); if (checkbox.checked) { gidnumberField.disabled = false; + cnField.disabled = false; $('form_editprotected').value = 'true'; } else { gidnumberField.disabled = true; + cnField.disabled = true; $('form_editprotected').value = ''; } } @@ -70,11 +75,9 @@ from ipagui.helpers import ipahelper py:content="group_fields.cn.label" />: </th> <td> - <!-- <span py:replace="group_fields.cn.display(value_for(group_fields.cn))" /> + <span py:replace="group_fields.cn.display(value_for(group_fields.cn))" /> <span py:if="tg.errors.get('cn')" class="fielderror" - py:content="tg.errors.get('cn')" /> --> - ${value_for(group_fields.cn)} - + py:content="tg.errors.get('cn')" /> </td> </tr> @@ -88,6 +91,9 @@ from ipagui.helpers import ipahelper <span py:if="tg.errors.get('description')" class="fielderror" py:content="tg.errors.get('description')" /> + <script type="text/javascript"> + document.getElementById('form_cn').disabled = true; + </script> </td> </tr> @@ -106,6 +112,16 @@ from ipagui.helpers import ipahelper </script> </td> </tr> + <tr> + <th> + <label class="fieldlabel" for="${group_fields.nsAccountLock.field_id}" py:content="group_fields.nsAccountLock.label" />: + </th> + <td> + <span py:replace="group_fields.nsAccountLock.display(value_for(group_fields.nsAccountLock))" /> + <span py:if="tg.errors.get('nsAccountLock')" class="fielderror" + py:content="tg.errors.get('nsAccountLock')" /> + </td> + </tr> </table> <div> @@ -160,6 +176,7 @@ from ipagui.helpers import ipahelper div_counter = div_counter + 1 ?> </div> + <!-- a space here to prevent an empty div --> </div> </div> diff --git a/ipa-server/ipa-gui/ipagui/templates/grouplist.kid b/ipa-server/ipa-gui/ipagui/templates/grouplist.kid index 9f9bc4840..9489b3744 100644 --- a/ipa-server/ipa-gui/ipagui/templates/grouplist.kid +++ b/ipa-server/ipa-gui/ipagui/templates/grouplist.kid @@ -20,7 +20,7 @@ </div> <div py:if='(groups != None) and (len(groups) > 0)'> <h2>${len(groups)} results returned:</h2> - <table id="resultstable" class="details sortable resizable"> + <table id="resultstable" class="details sortable resizable" cellspacing="0"> <thead> <tr> <th> @@ -32,7 +32,15 @@ </tr> </thead> <tbody> - <tr py:for="group in groups"> + <tr py:for="group in groups" py:if="group.nsAccountLock != 'true'"> + <td> + <a href="${tg.url('/group/show',cn=group.cn)}">${group.cn}</a> + </td> + <td> + ${group.description} + </td> + </tr> + <tr id="inactive" py:for="group in groups" py:if="group.nsAccountLock == 'true'"> <td> <a href="${tg.url('/group/show',cn=group.cn)}">${group.cn}</a> </td> diff --git a/ipa-server/ipa-gui/ipagui/templates/groupshow.kid b/ipa-server/ipa-gui/ipagui/templates/groupshow.kid index 7a66acdbe..8713742d5 100644 --- a/ipa-server/ipa-gui/ipagui/templates/groupshow.kid +++ b/ipa-server/ipa-gui/ipagui/templates/groupshow.kid @@ -7,12 +7,17 @@ </head> <body> <?python -edit_url = tg.url('/group/edit', cn=group.get('cn')) +cn = group.get('cn') +if isinstance(cn, list): + cn = cn[0] +edit_url = tg.url('/group/edit', cn=cn) +from ipagui.helpers import userhelper ?> <div id="details"> <h1>View Group</h1> - <input class="submitbutton" type="button" + <input py:if="'editors' in tg.identity.groups or 'admins' in tg.identity.groups" + class="submitbutton" type="button" onclick="document.location.href='${edit_url}'" value="Edit Group" /> @@ -38,6 +43,12 @@ edit_url = tg.url('/group/edit', cn=group.get('cn')) </th> <td>${group.get("gidnumber")}</td> </tr> + <tr> + <th> + <label class="fieldlabel" py:content="fields.nsAccountLock.label" />: + </th> + <td>${userhelper.account_status_display(group.get("nsAccountLock"))}</td> + </tr> </table> <h2 class="formsection">Group Members</h2> @@ -51,7 +62,10 @@ edit_url = tg.url('/group/edit', cn=group.get('cn')) member_type = "user" view_url = tg.url('/user/show', uid=member_uid) else: - member_cn = "%s" % member.get('cn') + mem = member.get('cn') + if isinstance(mem, list): + mem = mem[0] + member_cn = "%s" % mem member_desc = "[group]" member_type = "group" view_url = tg.url('/group/show', cn=member_cn) @@ -70,7 +84,8 @@ edit_url = tg.url('/group/edit', cn=group.get('cn')) <br/> <hr /> - <input class="submitbutton" type="button" + <input py:if="'editors' in tg.identity.groups or 'admins' in tg.identity.groups" + class="submitbutton" type="button" onclick="document.location.href='${edit_url}'" value="Edit Group" /> </div> diff --git a/ipa-server/ipa-gui/ipagui/templates/ipapolicyedit.kid b/ipa-server/ipa-gui/ipagui/templates/ipapolicyedit.kid new file mode 100644 index 000000000..5987cc40a --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/ipapolicyedit.kid @@ -0,0 +1,15 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#" + py:extends="'policylayout.kid'"> +<head> + <meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/> + <title>Edit IPA Policy</title> +</head> +<body> + <div> + <h1>Edit IPA Policy</h1> + + ${form.display(action=tg.url('/ipapolicy/update'), value=ipapolicy)} +</div> +</body> +</html> diff --git a/ipa-server/ipa-gui/ipagui/templates/ipapolicyeditform.kid b/ipa-server/ipa-gui/ipagui/templates/ipapolicyeditform.kid new file mode 100644 index 000000000..106657636 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/ipapolicyeditform.kid @@ -0,0 +1,176 @@ +<div xmlns:py="http://purl.org/kid/ns#" + class="simpleroster"> + + <form action="${action}" name="${name}" method="${method}" class="tableform" + onsubmit="preSubmit()" > + + <input type="submit" class="submitbutton" name="submit" + value="Update Policy"/> + <input type="submit" class="submitbutton" name="submit" + value="Cancel Edit" /> + +<?python +from ipagui.helpers import ipahelper +?> + + <script type="text/javascript" charset="utf-8" + src="${tg.url('/static/javascript/dynamicedit.js')}"></script> + + <div py:for="field in hidden_fields" + py:replace="field.display(value_for(field), **params_for(field))" + /> + + <h2 class="formsection">Search</h2> + <table class="formtable" cellpadding="2" cellspacing="0" border="0"> + <tr> + <th> + <label class="fieldlabel" py:content="ipapolicy_fields.ipasearchtimelimit.label" />: + </th> + <td> + <span py:replace="ipapolicy_fields.ipasearchtimelimit.display(value_for(ipapolicy_fields.ipasearchtimelimit))" /> + <span py:if="tg.errors.get('ipasearchtimelimit')" class="fielderror" + py:content="tg.errors.get('ipasearchtimelimit')" /> + </td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="ipapolicy_fields.ipasearchrecordslimit.label" />: + </th> + <td> + <span py:replace="ipapolicy_fields.ipasearchrecordslimit.display(value_for(ipapolicy_fields.ipasearchrecordslimit))" /> + <span py:if="tg.errors.get('ipasearchrecordslimit')" class="fielderror" + py:content="tg.errors.get('ipasearchrecordslimit')" /> + </td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="ipapolicy_fields.ipausersearchfields.label" />: + </th> + <td> + <span py:replace="ipapolicy_fields.ipausersearchfields.display(value_for(ipapolicy_fields.ipausersearchfields))" /> + <span py:if="tg.errors.get('ipausersearchfields')" class="fielderror" + py:content="tg.errors.get('ipausersearchfields')" /> + </td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="ipapolicy_fields.ipagroupsearchfields.label" />: + </th> + <td> + <span py:replace="ipapolicy_fields.ipagroupsearchfields.display(value_for(ipapolicy_fields.ipagroupsearchfields))" /> + <span py:if="tg.errors.get('ipagroupsearchfields')" class="fielderror" + py:content="tg.errors.get('ipagroupsearchfields')" /> + </td> + </tr> + </table> + + <h2 class="formsection">Password Policy</h2> + <table class="formtable" cellpadding="2" cellspacing="0" border="0"> + <tr> + <th> + <label class="fieldlabel" py:content="ipapolicy_fields.ipapwdexpadvnotify.label" />: + </th> + <td> + <span py:replace="ipapolicy_fields.ipapwdexpadvnotify.display(value_for(ipapolicy_fields.ipapwdexpadvnotify))" /> + <span py:if="tg.errors.get('ipapwdexpadvnotify')" class="fielderror" + py:content="tg.errors.get('ipapwdexpadvnotify')" /> + </td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="ipapolicy_fields.krbminpwdlife.label" />: + </th> + <td> + <span py:replace="ipapolicy_fields.krbminpwdlife.display(value_for(ipapolicy_fields.krbminpwdlife))" /> + <span py:if="tg.errors.get('krbminpwdlife')" class="fielderror" + py:content="tg.errors.get('krbminpwdlife')" /> + </td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="ipapolicy_fields.krbmaxpwdlife.label" />: + </th> + <td> + <span py:replace="ipapolicy_fields.krbmaxpwdlife.display(value_for(ipapolicy_fields.krbmaxpwdlife))" /> + <span py:if="tg.errors.get('krbmaxpwdlife')" class="fielderror" + py:content="tg.errors.get('krbmaxpwdlife')" /> + </td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="ipapolicy_fields.krbpwdmindiffchars.label" />: + </th> + <td> + <span py:replace="ipapolicy_fields.krbpwdmindiffchars.display(value_for(ipapolicy_fields.krbpwdmindiffchars))" /> + <span py:if="tg.errors.get('krbpwdmindiffchars')" class="fielderror" + py:content="tg.errors.get('krbpwdmindiffchars')" /> + </td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="ipapolicy_fields.krbpwdminlength.label" />: + </th> + <td> + <span py:replace="ipapolicy_fields.krbpwdminlength.display(value_for(ipapolicy_fields.krbpwdminlength))" /> + <span py:if="tg.errors.get('krbpwdminlength')" class="fielderror" + py:content="tg.errors.get('krbpwdminlength')" /> + </td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="ipapolicy_fields.krbpwdhistorylength.label" />: + </th> + <td> + <span py:replace="ipapolicy_fields.krbpwdhistorylength.display(value_for(ipapolicy_fields.krbpwdhistorylength))" /> + <span py:if="tg.errors.get('krbpwdhistorylength')" class="fielderror" + py:content="tg.errors.get('krbpwdhistorylength')" /> + </td> + </tr> + </table> + + <h2 class="formsection">User Settings</h2> + <table class="formtable" cellpadding="2" cellspacing="0" border="0"> + <tr> + <th> + <label class="fieldlabel" py:content="ipapolicy_fields.ipamaxusernamelength.label" />: + </th> + <td> + <span py:replace="ipapolicy_fields.ipamaxusernamelength.display(value_for(ipapolicy_fields.ipamaxusernamelength))" /> + <span py:if="tg.errors.get('ipamaxusernamelength')" class="fielderror" + py:content="tg.errors.get('ipamaxusernamelength')" /> + </td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="ipapolicy_fields.ipahomesrootdir.label" />: + </th> + <td> + <span py:replace="ipapolicy_fields.ipahomesrootdir.display(value_for(ipapolicy_fields.ipahomesrootdir))" /> + <span py:if="tg.errors.get('ipahomesrootdir')" class="fielderror" + py:content="tg.errors.get('ipahomesrootdir')" /> + </td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="ipapolicy_fields.ipadefaultloginshell.label" />: + </th> + <td> + <span py:replace="ipapolicy_fields.ipadefaultloginshell.display(value_for(ipapolicy_fields.ipadefaultloginshell))" /> + <span py:if="tg.errors.get('ipadefaultloginshell')" class="fielderror" + py:content="tg.errors.get('ipadefaultloginshell')" /> + </td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="ipapolicy_fields.ipadefaultprimarygroup.label" />: + </th> + <td> + <span py:replace="ipapolicy_fields.ipadefaultprimarygroup.display(value_for(ipapolicy_fields.ipadefaultprimarygroup))" /> + <span py:if="tg.errors.get('ipadefaultprimarygroup')" class="fielderror" + py:content="tg.errors.get('ipadefaultprimarygroup')" /> + </td> + </tr> + </table> + </form> + +</div> diff --git a/ipa-server/ipa-gui/ipagui/templates/ipapolicyshow.kid b/ipa-server/ipa-gui/ipagui/templates/ipapolicyshow.kid new file mode 100644 index 000000000..089fb494e --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/ipapolicyshow.kid @@ -0,0 +1,120 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#" + py:extends="'policylayout.kid'"> +<head> +<meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/> +<title>Manage IPA Policy</title> +</head> +<body> + +<?python +from ipagui.helpers import ipahelper +edit_url = tg.url('/ipapolicy/edit') +?> + + <script type="text/javascript" charset="utf-8" src="${tg.url('/static/javascript/tablekit.js')}"></script> + + <h1>Manage IPA Policy</h1> + + <h2 class="formsection">Search</h2> + <table class="formtable" cellpadding="2" cellspacing="0" border="0"> + <tr> + <th> + <label class="fieldlabel" py:content="fields.ipasearchtimelimit.label" />: + </th> + <td>${ipapolicy.get("ipasearchtimelimit")}</td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="fields.ipasearchrecordslimit.label" />: + </th> + <td>${ipapolicy.get("ipasearchrecordslimit")}</td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="fields.ipausersearchfields.label" />: + </th> + <td>${ipapolicy.get("ipausersearchfields")}</td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="fields.ipagroupsearchfields.label" />: + </th> + <td>${ipapolicy.get("ipagroupsearchfields")}</td> + </tr> + </table> + + <h2 class="formsection">Password Policy</h2> + <table class="formtable" cellpadding="2" cellspacing="0" border="0"> + <tr> + <th> + <label class="fieldlabel" py:content="fields.ipapwdexpadvnotify.label" />: + </th> + <td>${ipapolicy.get("ipapwdexpadvnotify")}</td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="fields.krbminpwdlife.label" />: + </th> + <td>${password.get("krbminpwdlife")}</td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="fields.krbmaxpwdlife.label" />: + </th> + <td>${password.get("krbmaxpwdlife")}</td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="fields.krbpwdmindiffchars.label" />: + </th> + <td>${password.get("krbpwdmindiffchars")}</td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="fields.krbpwdminlength.label" />: + </th> + <td>${password.get("krbpwdminlength")}</td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="fields.krbpwdhistorylength.label" />: + </th> + <td>${password.get("krbpwdhistorylength")}</td> + </tr> + </table> + <h2 class="formsection">User Settings</h2> + <table class="formtable" cellpadding="2" cellspacing="0" border="0"> + <tr> + <th> + <label class="fieldlabel" py:content="fields.ipamaxusernamelength.label" />: + </th> + <td>${ipapolicy.get("ipamaxusernamelength")}</td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="fields.ipahomesrootdir.label" />: + </th> + <td>${ipapolicy.get("ipahomesrootdir")}</td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="fields.ipadefaultloginshell.label" />: + </th> + <td>${ipapolicy.get("ipadefaultloginshell")}</td> + </tr> + <tr> + <th> + <label class="fieldlabel" py:content="fields.ipadefaultprimarygroup.label" />: + </th> + <td>${ipapolicy.get("ipadefaultprimarygroup")}</td> + </tr> + </table> +<hr /> + <input class="submitbutton" type="button" + onclick="document.location.href='${edit_url}'" + value="Edit Policy" /> + + +</body> +</html> diff --git a/ipa-server/ipa-gui/ipagui/templates/loginfailed.kid b/ipa-server/ipa-gui/ipagui/templates/loginfailed.kid index 84896be5c..b31db82a7 100644 --- a/ipa-server/ipa-gui/ipagui/templates/loginfailed.kid +++ b/ipa-server/ipa-gui/ipagui/templates/loginfailed.kid @@ -1,35 +1,24 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" - "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" - xmlns:py="http://purl.org/kid/ns#"> - +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#" + py:extends="'master.kid'"> <head> - <meta content="text/html; charset=UTF-8" - http-equiv="content-type" py:replace="''"/> - <title>Login Failure</title> +<meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/> +<title>Permission Denied</title> </head> <body> - <div id="header"> - <div id="logo"> - <a href="${tg.url('/')}"><img - src="${tg.url('/static/images/logo.png')}" - border="0" alt="homepage" - /></a> - </div> - <div id="headerinfo"> - <div id="login"> - <div py:if="tg.config('identity.on') and not defined('logging_in')" id="page -Login"> - <span py:if="tg.identity.anonymous"> - Kerberos login failed. - </span> - <span py:if="not tg.identity.anonymous"> - Logged in as: ${tg.identity.user.display_name} - </span> + <div id="main_content"> + <div id="details"> + <div id="alertbox" py:if="value_of('tg_flash', None)"> + <p py:content="XML(tg_flash)"></p></div> + <h1>Permission Denied</h1> + <div class="instructions"> + <p> + You do not have permission to access this page. + </p> </div> + </div> </div> - </div> - </div> </body> + </html> diff --git a/ipa-server/ipa-gui/ipagui/templates/master.kid b/ipa-server/ipa-gui/ipagui/templates/master.kid index fd527a278..12e54fa1d 100644 --- a/ipa-server/ipa-gui/ipagui/templates/master.kid +++ b/ipa-server/ipa-gui/ipagui/templates/master.kid @@ -70,18 +70,20 @@ <div id="sidebar"> <h2>Tasks</h2> <ul> - <li><a href="${tg.url('/user/new')}">Add Person</a></li> - <li><a href="${tg.url('/user/list')}">Find People</a></li> + <li py:if="'admins' in tg.identity.groups"><a href="${tg.url('/user/new')}">Add User</a></li> + <li><a href="${tg.url('/user/list')}">Find Users</a></li> </ul> <ul> - <li><a href="${tg.url('/group/new')}">Add Group</a></li> + <li py:if="'admins' in tg.identity.groups"><a href="${tg.url('/group/new')}">Add Group</a></li> <li><a href="${tg.url('/group/list')}">Find Groups</a></li> </ul> + <ul py:if="'admins' in tg.identity.groups"> + <li><a href="${tg.url('/policy/index')}">Manage Policy</a></li> + </ul> <ul> - <li><a href="${tg.url('/')}">Manage Policy</a></li> <li><a href="${tg.url('/user/edit/', principal=tg.identity.user.display_name)}">Self Service</a></li> </ul> - <ul> + <ul py:if="'admins' in tg.identity.groups"> <li><a href="${tg.url('/delegate/list')}">Delegations</a></li> </ul> </div> diff --git a/ipa-server/ipa-gui/ipagui/templates/policyindex.kid b/ipa-server/ipa-gui/ipagui/templates/policyindex.kid new file mode 100644 index 000000000..88fa4bcc2 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/policyindex.kid @@ -0,0 +1,31 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#" + py:extends="'policylayout.kid'"> +<head> +<meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/> +<title>Manage Policy</title> +</head> +<body> + +<?python +from ipagui.helpers import ipahelper +?> + + <script type="text/javascript" charset="utf-8" src="${tg.url('/static/javascript/tablekit.js')}"></script> + + <h1>Manage Policy</h1> + + <table> + <tbody> + <tr> + <td> + <a href="${tg.url('/ipapolicy/show')}" + >IPA Policy</a> + </td> + </tr> + </tbody> + </table> + + +</body> +</html> diff --git a/ipa-server/ipa-gui/ipagui/templates/policylayout.kid b/ipa-server/ipa-gui/ipagui/templates/policylayout.kid new file mode 100644 index 000000000..171326539 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/policylayout.kid @@ -0,0 +1,17 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#" + py:extends="'master.kid'"> +<head> +</head> + +<body py:match="item.tag=='{http://www.w3.org/1999/xhtml}body'" py:attrs="item.items()"> + <div id="main_content"> +<div id="details"> + <div id="alertbox" py:if="value_of('tg_flash', None)"><p py:content="XML(tg_flash)"></p></div> + + <div py:replace="[item.text]+item[:]"></div> + </div> +</div> +</body> + +</html> diff --git a/ipa-server/ipa-gui/ipagui/templates/useredit.kid b/ipa-server/ipa-gui/ipagui/templates/useredit.kid index 3f9482a3d..f5cb1b02e 100644 --- a/ipa-server/ipa-gui/ipagui/templates/useredit.kid +++ b/ipa-server/ipa-gui/ipagui/templates/useredit.kid @@ -3,7 +3,7 @@ py:extends="'userlayout.kid'"> <head> <meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/> -<title>Edit Person</title> +<title>Edit User</title> </head> <body> @@ -14,7 +14,7 @@ <span class="small">edit protected fields</span> </input> </div> - <h1>Edit Person</h1> + <h1>Edit User</h1> </div> <?python diff --git a/ipa-server/ipa-gui/ipagui/templates/usereditform.kid b/ipa-server/ipa-gui/ipagui/templates/usereditform.kid index f6da48870..88b778d8c 100644 --- a/ipa-server/ipa-gui/ipagui/templates/usereditform.kid +++ b/ipa-server/ipa-gui/ipagui/templates/usereditform.kid @@ -10,11 +10,11 @@ onsubmit="preSubmit()"> <input type="submit" class="submitbutton" name="submit" - value="Update Person"/> + value="Update User"/> <input type="submit" class="submitbutton" name="submit" value="Cancel Edit" /> <input type="button" class="submitbutton" - value="Delete Person" + value="Delete User" onclick="return confirmDelete();" /> @@ -26,6 +26,8 @@ from ipagui.helpers import ipahelper src="${tg.url('/static/javascript/dynamicedit.js')}"></script> <script type="text/javascript" charset="utf-8" src="${tg.url('/static/javascript/dynamicselect.js')}"></script> + <script type="text/javascript" charset="utf-8" + src="${tg.url('/tg_widgets/tg_expanding_form_widget/javascript/expanding_form.js')}"></script> <?python searchurl = tg.url('/user/edit_search') @@ -141,14 +143,35 @@ from ipagui.helpers import ipahelper <tr> <th> - <label class="fieldlabel" for="${user_fields.cn.field_id}" - py:content="user_fields.cn.label" />: - </th> - <td> - <span py:replace="user_fields.cn.display(value_for(user_fields.cn))" /> - <span py:if="tg.errors.get('cn')" class="fielderror" - py:content="tg.errors.get('cn')" /> - + <label class="fieldlabel" for="${user_fields.cns.field_id}" + py:content="user_fields.cns.label" />: + </th> + <td colspan="3"> + <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${user_fields.cns.field_id}"> + <tbody> + <?python repetition = 0 + cn_index = 0 + cn_error = tg.errors.get('cn') + ?> + <tr py:for="cn in value_for(user_fields.cn)" + id="${user_fields.cns.field_id}_${repetition}" + class="${user_fields.cns.field_class}"> + + <td py:for="field in user_fields.cns.fields"> + <span><input class="textfield" type="text" id="${user_fields.cns.field_id}_${repetition}_cn" name="cns-${repetition}.cn" value="${cn}"/></span> + <span py:if="cn_error and cn_error[cn_index]" class="fielderror" + py:content="tg.errors.get('cn')" /> + </td> + <?python cn_index = cn_index + 1 ?> + <td> + <a + href="javascript:ExpandingForm.removeItem('${user_fields.cns.field_id}_${repetition}')">Remove</a> + </td> + <?python repetition = repetition + 1?> + </tr> + </tbody> + </table> + <a id="${user_fields.cns.field_id}_doclink" href="javascript:ExpandingForm.addItem('${user_fields.cns.field_id}');">Add Common Name</a> </td> </tr> @@ -364,61 +387,170 @@ from ipagui.helpers import ipahelper <tr> <th> - <label class="fieldlabel" for="${user_fields.telephonenumber.field_id}" - py:content="user_fields.telephonenumber.label" />: - </th> - <td> - <span py:replace="user_fields.telephonenumber.display(value_for(user_fields.telephonenumber))" /> - <span py:if="tg.errors.get('telephonenumber')" class="fielderror" - py:content="tg.errors.get('telephonenumber')" /> + <label class="fieldlabel" for="${user_fields.telephonenumbers.field_id}" + py:content="user_fields.telephonenumbers.label" />: + </th> + <td colspan="3"> + <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${user_fields.telephonenumbers.field_id}"> + <tbody> + <?python repetition = 0 + tele_index = 0 + tele_error = tg.errors.get('telephonenumber') + ?> + <tr py:for="tele in value_for(user_fields.telephonenumber)" + id="${user_fields.telephonenumbers.field_id}_${repetition}" + class="${user_fields.telephonenumbers.field_class}"> + + <td py:for="field in user_fields.telephonenumbers.fields"> + <span><input class="textfield" type="text" id="${user_fields.telephonenumbers.field_id}_${repetition}_telephonenumber" name="telephonenumbers-${repetition}.telephonenumber" value="${tele}"/></span> + <span py:if="tele_error and tele_error[tele_index]" class="fielderror" + py:content="tg.errors.get('telephonenumber')" /> + </td> + <?python tele_index = tele_index + 1 ?> + <td> + <a + href="javascript:ExpandingForm.removeItem('${user_fields.telephonenumbers.field_id}_${repetition}')">Remove</a> + </td> + <?python repetition = repetition + 1?> + </tr> + </tbody> + </table> + <a id="${user_fields.telephonenumbers.field_id}_doclink" href="javascript:ExpandingForm.addItem('${user_fields.telephonenumbers.field_id}');">Add Work Number</a> </td> </tr> - <tr> <th> - <label class="fieldlabel" for="${user_fields.facsimiletelephonenumber.field_id}" - py:content="user_fields.facsimiletelephonenumber.label" />: - </th> - <td> - <span py:replace="user_fields.facsimiletelephonenumber.display(value_for(user_fields.facsimiletelephonenumber))" /> - <span py:if="tg.errors.get('facsimiletelephonenumber')" class="fielderror" - py:content="tg.errors.get('facsimiletelephonenumber')" /> + <label class="fieldlabel" for="${user_fields.facsimiletelephonenumbers.field_id}" + py:content="user_fields.facsimiletelephonenumbers.label" />: + </th> + <td colspan="3"> + <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${user_fields.facsimiletelephonenumbers.field_id}"> + <tbody> + <?python repetition = 0 + fax_index = 0 + fax_error = tg.errors.get('facsimiletelephonenumber') + ?> + <tr py:for="fax in value_for(user_fields.facsimiletelephonenumber)" + id="${user_fields.facsimiletelephonenumbers.field_id}_${repetition}" + class="${user_fields.facsimiletelephonenumbers.field_class}"> + + <td py:for="field in user_fields.facsimiletelephonenumbers.fields"> + <span><input class="textfield" type="text" id="${user_fields.facsimiletelephonenumbers.field_id}_${repetition}_facsimiletelephonenumber" name="facsimiletelephonenumbers-${repetition}.facsimiletelephonenumber" value="${fax}"/></span> + <span py:if="fax_error and fax_error[fax_index]" class="fielderror" + py:content="tg.errors.get('facsimiletelephonenumber')" /> + </td> + <?python fax_index = fax_index + 1 ?> + <td> + <a + href="javascript:ExpandingForm.removeItem('${user_fields.facsimiletelephonenumbers.field_id}_${repetition}')">Remove</a> + </td> + <?python repetition = repetition + 1?> + </tr> + </tbody> + </table> + <a id="${user_fields.facsimiletelephonenumbers.field_id}_doclink" href="javascript:ExpandingForm.addItem('${user_fields.facsimiletelephonenumbers.field_id}');">Add Fax Number</a> </td> </tr> <tr> <th> - <label class="fieldlabel" for="${user_fields.mobile.field_id}" - py:content="user_fields.mobile.label" />: - </th> - <td> - <span py:replace="user_fields.mobile.display(value_for(user_fields.mobile))" /> - <span py:if="tg.errors.get('mobile')" class="fielderror" - py:content="tg.errors.get('mobile')" /> + <label class="fieldlabel" for="${user_fields.mobiles.field_id}" + py:content="user_fields.mobiles.label" />: + </th> + <td colspan="3"> + <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${user_fields.mobiles.field_id}"> + <tbody> + <?python repetition = 0 + mobile_index = 0 + mobile_error = tg.errors.get('mobile') + ?> + <tr py:for="mobile in value_for(user_fields.mobile)" + id="${user_fields.mobiles.field_id}_${repetition}" + class="${user_fields.mobiles.field_class}"> + + <td py:for="field in user_fields.mobiles.fields"> + <span><input class="textfield" type="text" id="${user_fields.mobiles.field_id}_${repetition}_mobile" name="mobiles-${repetition}.mobile" value="${mobile}"/></span> + <span py:if="mobile_error and mobile_error[mobile_index]" class="fielderror" + py:content="tg.errors.get('mobile')" /> + </td> + <?python mobile_index = mobile_index + 1 ?> + <td> + <a + href="javascript:ExpandingForm.removeItem('${user_fields.mobiles.field_id}_${repetition}')">Remove</a> + </td> + <?python repetition = repetition + 1?> + </tr> + </tbody> + </table> + <a id="${user_fields.mobiles.field_id}_doclink" href="javascript:ExpandingForm.addItem('${user_fields.mobiles.field_id}');">Add Cell Number</a> </td> </tr> <tr> <th> - <label class="fieldlabel" for="${user_fields.pager.field_id}" - py:content="user_fields.pager.label" />: - </th> - <td> - <span py:replace="user_fields.pager.display(value_for(user_fields.pager))" /> - <span py:if="tg.errors.get('pager')" class="fielderror" - py:content="tg.errors.get('pager')" /> + <label class="fieldlabel" for="${user_fields.pagers.field_id}" + py:content="user_fields.pagers.label" />: + </th> + <td colspan="3"> + <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${user_fields.pagers.field_id}"> + <tbody> + <?python repetition = 0 + pager_index = 0 + pager_error = tg.errors.get('pager') + ?> + <tr py:for="pager in value_for(user_fields.pager)" + id="${user_fields.pagers.field_id}_${repetition}" + class="${user_fields.pagers.field_class}"> + + <td py:for="field in user_fields.pagers.fields"> + <span><input class="textfield" type="text" id="${user_fields.pagers.field_id}_${repetition}_pager" name="pagers-${repetition}.pager" value="${pager}"/></span> + <span py:if="pager_error and pager_error[pager_index]" class="fielderror" + py:content="tg.errors.get('pager')" /> + </td> + <?python pager_index = pager_index + 1 ?> + <td> + <a + href="javascript:ExpandingForm.removeItem('${user_fields.pagers.field_id}_${repetition}')">Remove</a> + </td> + <?python repetition = repetition + 1?> + </tr> + </tbody> + </table> + <a id="${user_fields.pagers.field_id}_doclink" href="javascript:ExpandingForm.addItem('${user_fields.pagers.field_id}');">Add Pager Number</a> </td> </tr> <tr> <th> - <label class="fieldlabel" for="${user_fields.homephone.field_id}" - py:content="user_fields.homephone.label" />: - </th> - <td> - <span py:replace="user_fields.homephone.display(value_for(user_fields.homephone))" /> - <span py:if="tg.errors.get('homephone')" class="fielderror" - py:content="tg.errors.get('homephone')" /> + <label class="fieldlabel" for="${user_fields.homephones.field_id}" + py:content="user_fields.homephones.label" />: + </th> + <td colspan="3"> + <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${user_fields.homephones.field_id}"> + <tbody> + <?python repetition = 0 + homephone_index = 0 + homephone_error = tg.errors.get('homephone') + ?> + <tr py:for="homephone in value_for(user_fields.homephone)" + id="${user_fields.homephones.field_id}_${repetition}" + class="${user_fields.homephones.field_class}"> + + <td py:for="field in user_fields.homephones.fields"> + <span><input class="textfield" type="text" id="${user_fields.homephones.field_id}_${repetition}_homephone" name="homephones-${repetition}.homephone" value="${homephone}"/></span> + <span py:if="homephone_error and homephone_error[homephone_index]" class="fielderror" + py:content="tg.errors.get('homephone')" /> + </td> + <?python homephone_index = homephone_index + 1 ?> + <td> + <a + href="javascript:ExpandingForm.removeItem('${user_fields.homephones.field_id}_${repetition}')">Remove</a> + </td> + <?python repetition = repetition + 1?> + </tr> + </tbody> + </table> + <a id="${user_fields.homephones.field_id}_doclink" href="javascript:ExpandingForm.addItem('${user_fields.homephones.field_id}');">Add Home Phone</a> </td> </tr> </table> @@ -685,6 +817,7 @@ from ipagui.helpers import ipahelper div_counter = div_counter + 1 ?> </div> + <!-- a space here to prevent an empty div --> </div> </div> @@ -714,11 +847,11 @@ from ipagui.helpers import ipahelper <hr/> <input type="submit" class="submitbutton" name="submit" - value="Update Person"/> + value="Update User"/> <input type="submit" class="submitbutton" name="submit" value="Cancel Edit" /> <input type="button" class="submitbutton" - value="Delete Person" + value="Delete User" onclick="return confirmDelete();" /> diff --git a/ipa-server/ipa-gui/ipagui/templates/userlayout.kid b/ipa-server/ipa-gui/ipagui/templates/userlayout.kid index bbeb81399..c4e8104e6 100644 --- a/ipa-server/ipa-gui/ipagui/templates/userlayout.kid +++ b/ipa-server/ipa-gui/ipagui/templates/userlayout.kid @@ -15,8 +15,8 @@ <!-- <div id="sidebar"> <h2>Tools</h2> - <a href="${tg.url('/user/new')}">Add Person</a><br/> - <a href="${tg.url('/user/list')}">Find People</a><br/> + <a href="${tg.url('/user/new')}">Add User</a><br/> + <a href="${tg.url('/user/list')}">Find Users</a><br/> </div> --> </div> </body> diff --git a/ipa-server/ipa-gui/ipagui/templates/userlist.kid b/ipa-server/ipa-gui/ipagui/templates/userlist.kid index fdeeb3169..9ca3753d0 100644 --- a/ipa-server/ipa-gui/ipagui/templates/userlist.kid +++ b/ipa-server/ipa-gui/ipagui/templates/userlist.kid @@ -3,15 +3,15 @@ py:extends="'userlayout.kid'"> <head> <meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/> -<title>Find People</title> +<title>Find Users</title> </head> <body> - <h1>Find People</h1> + <h1>Find Users</h1> <script type="text/javascript" charset="utf-8" src="${tg.url('/static/javascript/tablekit.js')}"></script> <div id="search"> <form action="${tg.url('/user/list')}" method="get"> <input id="uid" type="text" name="uid" value="${uid}" /> - <input class="searchbutton" type="submit" value="Find People"/> + <input class="searchbutton" type="submit" value="Find Users"/> </form> <script type="text/javascript"> document.getElementById("uid").focus(); @@ -23,7 +23,7 @@ <thead> <tr> <th> - Person + User </th> <th> Phone diff --git a/ipa-server/ipa-gui/ipagui/templates/usernew.kid b/ipa-server/ipa-gui/ipagui/templates/usernew.kid index 16f0e66b9..b740bca22 100644 --- a/ipa-server/ipa-gui/ipagui/templates/usernew.kid +++ b/ipa-server/ipa-gui/ipagui/templates/usernew.kid @@ -3,10 +3,10 @@ py:extends="'userlayout.kid'"> <head> <meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/> - <title>Add Person</title> + <title>Add User</title> </head> <body> - <h1>Add Person</h1> + <h1>Add User</h1> ${form.display(action=tg.url("/user/create"), value=user)} </body> diff --git a/ipa-server/ipa-gui/ipagui/templates/usernewform.kid b/ipa-server/ipa-gui/ipagui/templates/usernewform.kid index eeaa87fa4..97be52732 100644 --- a/ipa-server/ipa-gui/ipagui/templates/usernewform.kid +++ b/ipa-server/ipa-gui/ipagui/templates/usernewform.kid @@ -2,8 +2,8 @@ class="simpleroster"> <form action="${action}" name="${name}" method="${method}" class="tableform" onsubmit="preSubmit()"> - -<input type="submit" class="submitbutton" name="submit" value="Add Person"/> + +<input type="submit" class="submitbutton" name="submit" value="Add User"/> <?python from ipagui.helpers import ipahelper @@ -13,6 +13,8 @@ from ipagui.helpers import ipahelper src="${tg.url('/static/javascript/dynamicedit.js')}"></script> <script type="text/javascript" charset="utf-8" src="${tg.url('/static/javascript/dynamicselect.js')}"></script> + <script type="text/javascript" charset="utf-8" + src="${tg.url('/tg_widgets/tg_expanding_form_widget/javascript/expanding_form.js')}"></script> <?python searchurl = tg.url('/user/edit_search') @@ -51,7 +53,7 @@ from ipagui.helpers import ipahelper </script> <div py:for="field in hidden_fields" - py:replace="field.display(value_for(field), **params_for(field))" + py:replace="field.display(value_for(field), **params_for(field))" /> <h2 class="formsection">Identity Details</h2> @@ -107,7 +109,7 @@ from ipagui.helpers import ipahelper var uid = $('form_uid'); var mail = $('form_mail'); - var cn = $('form_cn'); + var cn = $('form_cns_0_cn'); var displayname = $('form_displayname'); var initials = $('form_initials'); @@ -164,14 +166,38 @@ from ipagui.helpers import ipahelper <tr> <th> - <label class="fieldlabel" for="${user_fields.cn.field_id}" - py:content="user_fields.cn.label" />: + <label class="fieldlabel" for="${user_fields.cns.field_id}" + py:content="user_fields.cns.label" />: </th> - <td> - <span py:replace="user_fields.cn.display(value_for(user_fields.cn))" /> - <span py:if="tg.errors.get('cn')" class="fielderror" - py:content="tg.errors.get('cn')" /> + <td colspan="3"> + <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${user_fields.cns.field_id}"> + <tbody> + <?python repetition = 0 + cn_index = 0 + cn_error = tg.errors.get('cn') + values = value_for(user_fields.cn) + if values is None: + values=[''] + ?> + <tr py:for="cn in values" + id="${user_fields.cns.field_id}_${repetition}" + class="${user_fields.cns.field_class}"> + <td py:for="field in user_fields.cns.fields"> + <span><input class="textfield" type="text" id="${user_fields.cns.field_id}_${repetition}_cn" name="cns-${repetition}.cn" value="${cn}"/></span> + <span py:if="cn_error and cn_error[cn_index]" class="fielderror" + py:content="tg.errors.get('cn')" /> + </td> + <?python cn_index = cn_index + 1 ?> + <td> + <a + href="javascript:ExpandingForm.removeItem('${user_fields.cns.field_id}_${repetition}')">Remove</a> + </td> + <?python repetition = repetition + 1?> + </tr> + </tbody> + </table> + <a id="${user_fields.cns.field_id}_doclink" href="javascript:ExpandingForm.addItem('${user_fields.cns.field_id}');">Add Common Name</a> </td> </tr> @@ -337,63 +363,188 @@ from ipagui.helpers import ipahelper <tr> <th> - <label class="fieldlabel" for="${user_fields.telephonenumber.field_id}" - py:content="user_fields.telephonenumber.label" />: - </th> - <td> - <span py:replace="user_fields.telephonenumber.display(value_for(user_fields.telephonenumber))" /> - <span py:if="tg.errors.get('telephonenumber')" class="fielderror" - py:content="tg.errors.get('telephonenumber')" /> - </td> - </tr> - - <tr> - <th> - <label class="fieldlabel" for="${user_fields.facsimiletelephonenumber.field_id}" - py:content="user_fields.facsimiletelephonenumber.label" />: - </th> - <td> - <span py:replace="user_fields.facsimiletelephonenumber.display(value_for(user_fields.facsimiletelephonenumber))" /> - <span py:if="tg.errors.get('facsimiletelephonenumber')" class="fielderror" - py:content="tg.errors.get('facsimiletelephonenumber')" /> - </td> - </tr> - - <tr> - <th> - <label class="fieldlabel" for="${user_fields.mobile.field_id}" - py:content="user_fields.mobile.label" />: - </th> - <td> - <span py:replace="user_fields.mobile.display(value_for(user_fields.mobile))" /> - <span py:if="tg.errors.get('mobile')" class="fielderror" - py:content="tg.errors.get('mobile')" /> - </td> - </tr> - - <tr> - <th> - <label class="fieldlabel" for="${user_fields.pager.field_id}" - py:content="user_fields.pager.label" />: - </th> - <td> - <span py:replace="user_fields.pager.display(value_for(user_fields.pager))" /> - <span py:if="tg.errors.get('pager')" class="fielderror" - py:content="tg.errors.get('pager')" /> + <label class="fieldlabel" for="${user_fields.telephonenumbers.field_id}" + py:content="user_fields.telephonenumbers.label" />: + </th> + <td colspan="3"> + <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${user_fields.telephonenumbers.field_id}"> + <tbody> + <?python repetition = 0 + tele_index = 0 + tele_error = tg.errors.get('telephonenumber') + values = value_for(user_fields.telephonenumber) + if values is None: + values=[''] + ?> + <tr py:for="tele in values" + id="${user_fields.telephonenumbers.field_id}_${repetition}" + class="${user_fields.telephonenumbers.field_class}"> + + <td py:if="user_fields.telephonenumbers.fields is not None" py:for="field in user_fields.telephonenumbers.fields"> + <span><input class="textfield" type="text" id="${user_fields.telephonenumbers.field_id}_${repetition}_telephonenumber" name="telephonenumbers-${repetition}.telephonenumber" value="${tele}"/></span> + <span py:if="tele_error and tele_error[tele_index]" class="fielderror" + py:content="tg.errors.get('telephonenumber')" /> + </td> + <?python tele_index = tele_index + 1 ?> + <td> + <a + href="javascript:ExpandingForm.removeItem('${user_fields.telephonenumbers.field_id}_${repetition}')">Remove</a> + </td> + <?python repetition = repetition + 1?> + </tr> + </tbody> + </table> + <a id="${user_fields.telephonenumbers.field_id}_doclink" href="javascript:ExpandingForm.addItem('${user_fields.telephonenumbers.field_id}');">Add Work Number</a> + </td> + </tr> + <tr> + <th> + <label class="fieldlabel" for="${user_fields.facsimiletelephonenumbers.field_id}" + py:content="user_fields.facsimiletelephonenumbers.label" />: + </th> + <td colspan="3"> + <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${user_fields.facsimiletelephonenumbers.field_id}"> + <tbody> + <?python repetition = 0 + fax_index = 0 + fax_error = tg.errors.get('facsimiletelephonenumber') + values = value_for(user_fields.facsimiletelephonenumber) + if values is None: + values=[''] + ?> + <tr py:for="fax in values" + id="${user_fields.facsimiletelephonenumbers.field_id}_${repetition}" + class="${user_fields.facsimiletelephonenumbers.field_class}"> + + <td py:for="field in user_fields.facsimiletelephonenumbers.fields"> + <span><input class="textfield" type="text" id="${user_fields.facsimiletelephonenumbers.field_id}_${repetition}_facsimiletelephonenumber" name="facsimiletelephonenumbers-${repetition}.facsimiletelephonenumber" value="${fax}"/></span> + <span py:if="fax_error and fax_error[fax_index]" class="fielderror" + py:content="tg.errors.get('facsimiletelephonenumber')" /> + </td> + <?python fax_index = fax_index + 1 ?> + <td> + <a + href="javascript:ExpandingForm.removeItem('${user_fields.facsimiletelephonenumbers.field_id}_${repetition}')">Remove</a> + </td> + <?python repetition = repetition + 1?> + </tr> + </tbody> + </table> + <a id="${user_fields.facsimiletelephonenumbers.field_id}_doclink" href="javascript:ExpandingForm.addItem('${user_fields.facsimiletelephonenumbers.field_id}');">Add Fax Number</a> + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${user_fields.mobiles.field_id}" + py:content="user_fields.mobiles.label" />: + </th> + <td colspan="3"> + <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${user_fields.mobiles.field_id}"> + <tbody> + <?python repetition = 0 + mobile_index = 0 + mobile_error = tg.errors.get('mobile') + values = value_for(user_fields.mobile) + if values is None: + values=[''] + ?> + <tr py:for="mobile in values" + id="${user_fields.mobiles.field_id}_${repetition}" + class="${user_fields.mobiles.field_class}"> + + <td py:for="field in user_fields.mobiles.fields"> + <span><input class="textfield" type="text" id="${user_fields.mobiles.field_id}_${repetition}_mobile" name="mobiles-${repetition}.mobile" value="${mobile}"/></span> + <span py:if="mobile_error and mobile_error[mobile_index]" class="fielderror" + py:content="tg.errors.get('mobile')" /> + </td> + <?python mobile_index = mobile_index + 1 ?> + <td> + <a + href="javascript:ExpandingForm.removeItem('${user_fields.mobiles.field_id}_${repetition}')">Remove</a> + </td> + <?python repetition = repetition + 1?> + </tr> + </tbody> + </table> + <a id="${user_fields.mobiles.field_id}_doclink" href="javascript:ExpandingForm.addItem('${user_fields.mobiles.field_id}');">Add Cell Number</a> + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${user_fields.pagers.field_id}" + py:content="user_fields.pagers.label" />: + </th> + <td colspan="3"> + <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${user_fields.pagers.field_id}"> + <tbody> + <?python repetition = 0 + pager_index = 0 + pager_error = tg.errors.get('pager') + values = value_for(user_fields.pager) + if values is None: + values=[''] + ?> + <tr py:for="pager in values" + id="${user_fields.pagers.field_id}_${repetition}" + class="${user_fields.pagers.field_class}"> + + <td py:for="field in user_fields.pagers.fields"> + <span><input class="textfield" type="text" id="${user_fields.pagers.field_id}_${repetition}_pager" name="pagers-${repetition}.pager" value="${pager}"/></span> + <span py:if="pager_error and pager_error[pager_index]" class="fielderror" + py:content="tg.errors.get('pager')" /> + </td> + <?python pager_index = pager_index + 1 ?> + <td> + <a + href="javascript:ExpandingForm.removeItem('${user_fields.pagers.field_id}_${repetition}')">Remove</a> + </td> + <?python repetition = repetition + 1?> + </tr> + </tbody> + </table> + <a id="${user_fields.pagers.field_id}_doclink" href="javascript:ExpandingForm.addItem('${user_fields.pagers.field_id}');">Add Pager Number</a> + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${user_fields.homephones.field_id}" + py:content="user_fields.homephones.label" />: + </th> + <td colspan="3"> + <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${user_fields.homephones.field_id}"> + <tbody> + <?python repetition = 0 + homephone_index = 0 + homephone_error = tg.errors.get('homephone') + values = value_for(user_fields.homephone) + if values is None: + values=[''] + ?> + <tr py:for="homephone in values" + id="${user_fields.homephones.field_id}_${repetition}" + class="${user_fields.homephones.field_class}"> + + <td py:for="field in user_fields.homephones.fields"> + <span><input class="textfield" type="text" id="${user_fields.homephones.field_id}_${repetition}_homephone" name="homephones-${repetition}.homephone" value="${homephone}"/></span> + <span py:if="homephone_error and homephone_error[homephone_index]" class="fielderror" + py:content="tg.errors.get('homephone')" /> + </td> + <?python homephone_index = homephone_index + 1 ?> + <td> + <a + href="javascript:ExpandingForm.removeItem('${user_fields.homephones.field_id}_${repetition}')">Remove</a> + </td> + <?python repetition = repetition + 1?> + </tr> + </tbody> + </table> + <a id="${user_fields.homephones.field_id}_doclink" href="javascript:ExpandingForm.addItem('${user_fields.homephones.field_id}');">Add Home Phone</a> </td> </tr> - <tr> - <th> - <label class="fieldlabel" for="${user_fields.homephone.field_id}" - py:content="user_fields.homephone.label" />: - </th> - <td> - <span py:replace="user_fields.homephone.display(value_for(user_fields.homephone))" /> - <span py:if="tg.errors.get('homephone')" class="fielderror" - py:content="tg.errors.get('homephone')" /> - </td> - </tr> </table> <h2 class="formsection">Mailing Address</h2> @@ -635,7 +786,7 @@ from ipagui.helpers import ipahelper </div> <hr /> -<input type="submit" class="submitbutton" name="submit" value="Add Person"/> +<input type="submit" class="submitbutton" name="submit" value="Add User"/> </form> diff --git a/ipa-server/ipa-gui/ipagui/templates/usershow.kid b/ipa-server/ipa-gui/ipagui/templates/usershow.kid index cc56340d9..8cc356b89 100644 --- a/ipa-server/ipa-gui/ipagui/templates/usershow.kid +++ b/ipa-server/ipa-gui/ipagui/templates/usershow.kid @@ -3,17 +3,18 @@ py:extends="'userlayout.kid'"> <head> <meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/> - <title>View Person</title> + <title>View User</title> </head> <body> <?python edit_url = tg.url('/user/edit', uid=user.get('uid')) ?> - <h1>View Person</h1> + <h1>View User</h1> - <input class="submitbutton" type="button" + <input py:if="'editors' in tg.identity.groups or 'admins' in tg.identity.groups" + class="submitbutton" type="button" onclick="document.location.href='${edit_url}'" - value="Edit Person" /> + value="Edit User" /> <?python from ipagui.helpers import userhelper @@ -57,7 +58,21 @@ else: <th> <label class="fieldlabel" py:content="fields.cn.label" />: </th> - <td>${user.get("cn")}</td> + <td> + <table cellpadding="2" cellspacing="0" border="0"> + <tbody> + <?python + index = 0 + values = user.get("cn") + if isinstance(values, str): + values = [values] + ?> + <tr py:for="index in range(len(values))"> + <td>${values[index]}</td> + </tr> + </tbody> + </table> + </td> </tr> <tr> <th> @@ -132,31 +147,101 @@ else: <th> <label class="fieldlabel" py:content="fields.telephonenumber.label" />: </th> - <td>${user.get("telephonenumber")}</td> + <td> + <table cellpadding="2" cellspacing="0" border="0"> + <tbody> + <?python + index = 0 + values = user.get("telephonenumber", '') + if isinstance(values, str): + values = [values] + ?> + <tr py:for="index in range(len(values))"> + <td>${values[index]}</td> + </tr> + </tbody> + </table> + </td> </tr> <tr> <th> <label class="fieldlabel" py:content="fields.facsimiletelephonenumber.label" />: </th> - <td>${user.get("facsimiletelephonenumber")}</td> + <td> + <table cellpadding="2" cellspacing="0" border="0"> + <tbody> + <?python + index = 0 + values = user.get("facsimiletelephonenumber", '') + if isinstance(values, str): + values = [values] + ?> + <tr py:for="index in range(len(values))"> + <td>${values[index]}</td> + </tr> + </tbody> + </table> + </td> </tr> <tr> <th> <label class="fieldlabel" py:content="fields.mobile.label" />: </th> - <td>${user.get("mobile")}</td> + <td> + <table cellpadding="2" cellspacing="0" border="0"> + <tbody> + <?python + index = 0 + values = user.get("mobile", '') + if isinstance(values, str): + values = [values] + ?> + <tr py:for="index in range(len(values))"> + <td>${values[index]}</td> + </tr> + </tbody> + </table> + </td> </tr> <tr> <th> <label class="fieldlabel" py:content="fields.pager.label" />: </th> - <td>${user.get("pager")}</td> + <td> + <table cellpadding="2" cellspacing="0" border="0"> + <tbody> + <?python + index = 0 + values = user.get("pager", '') + if isinstance(values, str): + values = [values] + ?> + <tr py:for="index in range(len(values))"> + <td>${values[index]}</td> + </tr> + </tbody> + </table> + </td> </tr> <tr> <th> <label class="fieldlabel" py:content="fields.homephone.label" />: </th> - <td>${user.get("homephone")}</td> + <td> + <table cellpadding="2" cellspacing="0" border="0"> + <tbody> + <?python + index = 0 + values = user.get("homephone", '') + if isinstance(values, str): + values = [values] + ?> + <tr py:for="index in range(len(values))"> + <td>${values[index]}</td> + </tr> + </tbody> + </table> + </td> </tr> </table> @@ -260,7 +345,7 @@ else: </table> <div py:if='len(fields.custom_fields) > 0'> - <div class="formsection" >Custom Fields</div> + <h2 class="formsection">Custom Fields</h2> <table class="formtable" cellpadding="2" cellspacing="0" border="0"> <tr py:for='custom_field in fields.custom_fields'> <th> @@ -289,8 +374,9 @@ else: <br/> <hr /> - <input class="submitbutton" type="button" + <input py:if="'editors' in tg.identity.groups or 'admins' in tg.identity.groups" + class="submitbutton" type="button" onclick="document.location.href='${edit_url}'" - value="Edit Person" /> + value="Edit User" /> </body> </html> diff --git a/ipa-server/ipa-install/Makefile.am b/ipa-server/ipa-install/Makefile.am index 9ecf7e20d..4765cfb54 100644 --- a/ipa-server/ipa-install/Makefile.am +++ b/ipa-server/ipa-install/Makefile.am @@ -6,6 +6,8 @@ SUBDIRS = \ sbin_SCRIPTS = \ ipa-server-install \ + ipa-replica-install \ + ipa-replica-prepare \ $(NULL) appdir = $(IPA_DATA_DIR) diff --git a/ipa-server/ipa-install/ipa-replica-install b/ipa-server/ipa-install/ipa-replica-install new file mode 100644 index 000000000..706dc323d --- /dev/null +++ b/ipa-server/ipa-install/ipa-replica-install @@ -0,0 +1,142 @@ +#! /usr/bin/python -E +# Authors: Karl MacMillan <kmacmillan@mentalrootkit.com> +# +# Copyright (C) 2007 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 or later +# +# 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 +# + +import sys +sys.path.append("/usr/share/ipa") + +import tempfile +from ConfigParser import SafeConfigParser + +from ipa import ipautil + +from ipaserver import dsinstance, replication, installutils, krbinstance, service +from ipaserver import httpinstance, webguiinstance, radiusinstance, ntpinstance + +class ReplicaConfig: + def __init__(self): + self.realm_name = "" + self.master_host_name = "" + self.dirman_password = "" + self.ds_user = "" + self.host_name = "" + self.repl_password = "" + self.dir = "" + +def parse_options(): + from optparse import OptionParser + parser = OptionParser() + parser.add_option("-r", "--read-only", dest="master", action="store_false", + default=True, help="create read-only replica - default is master") + + options, args = parser.parse_args() + + if len(args) != 1: + parser.error("you must provide a file generated by ipa-replica-prepare") + + return options, args[0] + +def get_dirman_password(): + return installutils.read_password("Directory Manager (existing master)") + +def expand_info(filename): + top_dir = tempfile.mkdtemp("ipa") + dir = top_dir + "/realm_info" + ipautil.run(["tar", "xfz", filename, "-C", top_dir]) + + return top_dir, dir + +def read_info(dir, rconfig): + filename = dir + "/realm_info" + fd = open(filename) + config = SafeConfigParser() + config.readfp(fd) + + rconfig.realm_name = config.get("realm", "realm_name") + rconfig.master_host_name = config.get("realm", "master_host_name") + rconfig.ds_user = config.get("realm", "ds_user") + +def get_host_name(): + hostname = installutils.get_fqdn() + try: + installutils.verify_fqdn(hostname) + except RuntimeError, e: + logging.error(str(e)) + sys.exit(1) + + return hostname + +def install_ds(config): + dsinstance.check_existing_installation() + dsinstance.check_ports() + + ds = dsinstance.DsInstance() + ds.create_instance(config.ds_user, config.realm_name, config.host_name, config.dirman_password) + +def install_krb(config): + krb = krbinstance.KrbInstance() + ldappwd_filename = config.dir + "/ldappwd" + krb.create_replica(config.ds_user, config.realm_name, config.host_name, + config.dirman_password, ldappwd_filename) + +def install_http(config): + http = httpinstance.HTTPInstance() + http.create_instance(config.realm_name, config.host_name) + +def main(): + options, filename = parse_options() + top_dir, dir = expand_info(filename) + + config = ReplicaConfig() + read_info(dir, config) + config.host_name = get_host_name() + config.repl_password = "box" + config.dir = dir + + # get the directory manager password + config.dirman_password = get_dirman_password() + + install_ds(config) + + repl = replication.ReplicationManager(config.host_name, config.dirman_password) + repl.setup_replication(config.master_host_name, config.realm_name, options.master) + + install_krb(config) + install_http(config) + + # Create a Web Gui instance + webgui = webguiinstance.WebGuiInstance() + webgui.create_instance() + + # Create a radius instance + radius = radiusinstance.RadiusInstance() + # FIXME: ldap_server should be derived, not hardcoded to localhost, also should it be a URL? + radius.create_instance(config.realm_name, config.host_name, 'localhost') + + # Configure ntpd + ntp = ntpinstance.NTPInstance() + ntp.create_instance() + + + service.restart("dirsrv") + service.restart("krb5kdc") + +main() + + diff --git a/ipa-server/ipa-install/ipa-replica-prepare b/ipa-server/ipa-install/ipa-replica-prepare new file mode 100644 index 000000000..705c731d8 --- /dev/null +++ b/ipa-server/ipa-install/ipa-replica-prepare @@ -0,0 +1,114 @@ +#! /usr/bin/python -E +# Authors: Karl MacMillan <kmacmillan@mentalrootkit.com> +# +# Copyright (C) 2007 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 or later +# +# 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 +# + +import sys +sys.path.append("/usr/share/ipa") + +import logging, tempfile, shutil, os, pwd +from ConfigParser import SafeConfigParser +import krbV + +from ipa import ipautil +from ipaserver import dsinstance +from ipaserver import installutils + +certutil = "/usr/bin/certutil" + +def get_host_name(): + hostname = installutils.get_fqdn() + try: + installutils.verify_fqdn(hostname) + except RuntimeError, e: + logging.error(str(e)) + sys.exit(1) + + return hostname + +def get_realm_name(): + c = krbV.default_context() + return c.default_realm + +def check_ipa_configuration(realm_name): + config_dir = dsinstance.config_dirname(realm_name) + if not ipautil.dir_exists(config_dir): + logging.error("could not find directory instance: %s" % config_dir) + sys.exit(1) + +def create_certdb(ds_dir, dir): + # copy the passwd, noise, and pin files + shutil.copyfile(ds_dir + "/pwdfile.txt", dir + "/pwdfile.txt") + shutil.copyfile(ds_dir + "/noise.txt", dir + "/noise.txt") + shutil.copyfile(ds_dir + "/pin.txt", dir + "/pin.txt") + + # create a new cert db + ipautil.run([certutil, "-N", "-d", dir, "-f", dir + "/pwdfile.txt"]) + + # Add the CA cert + ipautil.run([certutil, "-A", "-d", dir, "-n", "CA certificate", "-t", "CT,CT", "-a", "-i", + ds_dir + "/cacert.asc"]) + +def get_ds_user(ds_dir): + uid = os.stat(ds_dir).st_uid + user = pwd.getpwuid(uid)[0] + + return user + +def copy_files(realm_name, dir): + shutil.copy("/var/kerberos/krb5kdc/ldappwd", dir + "/ldappwd") + + +def save_config(dir, realm_name, host_name, ds_user): + config = SafeConfigParser() + config.add_section("realm") + config.set("realm", "realm_name", realm_name) + config.set("realm", "master_host_name", host_name) + config.set("realm", "ds_user", ds_user) + fd = open(dir + "/realm_info", "w") + config.write(fd) + + +def main(): + realm_name = get_realm_name() + host_name = get_host_name() + ds_dir = dsinstance.config_dirname(realm_name) + ds_user = get_ds_user(ds_dir) + + check_ipa_configuration(realm_name) + + top_dir = tempfile.mkdtemp("ipa") + dir = top_dir + "/realm_info" + os.mkdir(dir, 0700) + + create_certdb(ds_dir, dir) + + copy_files(realm_name, dir) + + save_config(dir, realm_name, host_name, ds_user) + + ipautil.run(["/bin/tar", "cfz", "replica-info-" + realm_name, "-C", top_dir, "realm_info"]) + + shutil.rmtree(dir) + +main() + + + + + diff --git a/ipa-server/ipa-install/ipa-server-install b/ipa-server/ipa-install/ipa-server-install index 2de687fd7..a33a3e892 100644 --- a/ipa-server/ipa-install/ipa-server-install +++ b/ipa-server/ipa-install/ipa-server-install @@ -34,7 +34,6 @@ import socket import errno import logging import pwd -import getpass import subprocess import signal import shutil @@ -51,8 +50,9 @@ import ipaserver.radiusinstance import ipaserver.webguiinstance from ipaserver import service +from ipaserver.installutils import * -from ipa.ipautil import run +from ipa.ipautil import * def parse_options(): parser = OptionParser(version=VERSION) @@ -86,39 +86,6 @@ def parse_options(): return options -def logging_setup(options): - # Always log everything (i.e., DEBUG) to the log - # file. - logging.basicConfig(level=logging.DEBUG, - format='%(asctime)s %(levelname)s %(message)s', - filename='ipaserver-install.log', - filemode='w') - - console = logging.StreamHandler() - # If the debug option is set, also log debug messages to the console - if options.debug: - console.setLevel(logging.DEBUG) - else: - # Otherwise, log critical and error messages - console.setLevel(logging.ERROR) - formatter = logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s') - console.setFormatter(formatter) - logging.getLogger('').addHandler(console) - -def erase_ds_instance_data(serverid): - try: - shutil.rmtree("/etc/dirsrv/slapd-%s" % serverid) - except: - pass - try: - shutil.rmtree("/var/lib/dirsrv/slapd-%s" % serverid) - except: - pass - try: - shutil.rmtree("/var/lock/dirsrv/slapd-%s" % serverid) - except: - pass - def signal_handler(signum, frame): global ds print "\nCleaning up..." @@ -126,59 +93,9 @@ def signal_handler(signum, frame): print "Removing configuration for %s instance" % ds.serverid ds.stop() if ds.serverid: - erase_ds_instance_data (ds.serverid) + ipaserver.dsinstance.erase_ds_instance_data (ds.serverid) sys.exit(1) -def check_existing_installation(): - dirs = glob.glob("/etc/dirsrv/slapd-*") - if not dirs: - return - print "" - print "An existing Directory Server has been detected." - yesno = raw_input("Do you wish to remove it and create a new one? [no]: ") - if not yesno or yesno.lower()[0] != "y": - sys.exit(1) - - try: - run(["/sbin/service", "dirsrv", "stop"]) - except: - pass - for d in dirs: - serverid = os.path.basename(d).split("slapd-", 1)[1] - if serverid: - erase_ds_instance_data(serverid) - -def check_ports(): - ds_unsecure = port_available(389) - ds_secure = port_available(636) - if not ds_unsecure or not ds_secure: - print "IPA requires ports 389 and 636 for the Directory Server." - print "These are currently in use:" - if not ds_unsecure: - print "\t389" - if not ds_secure: - print "\t636" - sys.exit(1) - -def get_fqdn(): - fqdn = "" - try: - fqdn = socket.getfqdn() - except: - try: - fqdn = socket.gethostname() - except: - fqdn = "" - return fqdn - -def verify_fqdn(host_name): - is_ok = True - if len(host_name.split(".")) < 2 or host_name == "localhost.localdomain": - print "Invalid hostname: " + host_name - print "This host name can't be used as a hostname for an IPA Server" - is_ok = False - return is_ok - def read_host_name(host_default): host_ok = False host_name = "" @@ -198,7 +115,9 @@ def read_host_name(host_default): host_name = host_default else: host_name = host_input - if not verify_fqdn(host_name): + try: + verify_fqdn(host_name) + except: host_name = "" continue else: @@ -256,36 +175,6 @@ def read_ip_address(host_name): return ip -def port_available(port): - """Try to bind to a port on the wildcard host - Return 1 if the port is available - Return 0 if the port is in use - """ - rv = 1 - - try: - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - s.bind(('', port)) - s.shutdown(0) - s.close() - except socket.error, e: - if e[0] == errno.EADDRINUSE: - rv = 0 - - if rv: - try: - s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) - s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - s.bind(('', port)) - s.shutdown(0) - s.close() - except socket.error, e: - if e[0] == errno.EADDRINUSE: - rv = 0 - - return rv - def read_ds_user(): print "The server must run as a specific user in a specific group." print "It is strongly recommended that this user should have no privileges" @@ -333,23 +222,6 @@ def read_realm_name(domain_name): realm_name = upper_dom return realm_name -def read_password(user): - correct = False - pwd = "" - while not correct: - pwd = getpass.getpass(user + " password: ") - if not pwd: - continue - pwd_confirm = getpass.getpass("Password (confirm): ") - if pwd != pwd_confirm: - print "Password mismatch!" - print "" - else: - correct = True - #TODO: check validity/length - print "" - return pwd - def read_dm_password(): print "Certain directory server operations require an administrative user." print "This user is referred to as the Directory Manager and has full access" @@ -360,17 +232,6 @@ def read_dm_password(): dm_password = read_password("Directory Manager") return dm_password -def read_master_password(): - print "The Kerberos database is usually encrypted using a master password." - print "Please store this password offline in a secure place." - print "It may be necessary in a recovery situation or to install a replica." - print "Without the master password the encrypted material can't be used by the KDC." - print "If the master password is lost all kerberos related secrets will also be lost." - print "" - #TODO: provide the option of generating a random password - master_password = read_password("Kerberos master") - return master_password - def read_admin_password(): print "The IPA server requires an administrative user, named 'admin'." print "This user is a regular system account used for IPA server administration." @@ -392,6 +253,8 @@ def main(): global ds ds = None + options = parse_options() + if os.getegid() != 0: print "Must be root to setup server" return @@ -399,17 +262,17 @@ def main(): signal.signal(signal.SIGTERM, signal_handler) signal.signal(signal.SIGINT, signal_handler) + standard_logging_setup("ipaserver-install.log", options.debug) + print "==============================================================================" print "This program will setup the FreeIPA Server." print "" print "To accept the default shown in brackets, press the Enter key." print "" - check_existing_installation() - check_ports() + ipaserver.dsinstance.check_existing_installation() + ipaserver.dsinstance.check_ports() - options = parse_options() - logging_setup(options) ds_user = "" realm_name = "" @@ -439,10 +302,13 @@ def main(): host_default = get_fqdn() if options.unattended: - if not verify_fqdn(host_default): + try: + verify_fqdn(host_default) + except RuntimeError, e: + logging.error(str(e) + "\n") return "-Fatal Error-" - else: - host_name = host_default + + host_name = host_default else: host_name = read_host_name(host_default) @@ -504,7 +370,7 @@ def main(): dm_password = options.dm_password if not options.master_password: - master_password = read_master_password() + master_password = ipa_generate_password() else: master_password = options.master_password diff --git a/ipa-server/ipa-install/share/60ipaconfig.ldif b/ipa-server/ipa-install/share/60ipaconfig.ldif new file mode 100644 index 000000000..e15d4a417 --- /dev/null +++ b/ipa-server/ipa-install/share/60ipaconfig.ldif @@ -0,0 +1,37 @@ +## schema file for ipa configuration +## +## IPA Base OID: 2.16.840.1.113730.3.8 +## +## Attributes: 2.16.840.1.113730.3.8.1 +## ObjectClasses: 2.16.840.1.113730.3.8.2 +dn: cn=schema +############################################### +## +## Attributes +## +## ipaUserSearchFields - attribute names to search against when looking for users +attributetypes: ( 2.16.840.1.113730.3.8.1.1 NAME 'ipaUserSearchFields' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26) +## ipaGroupSearchFields - attribute names to search against when looking for groups +attributetypes: ( 2.16.840.1.113730.3.8.1.2 NAME 'ipaGroupSearchFields' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26) +## ipaSearchTimeLimit - search time limit in seconds +attributetypes: ( 2.16.840.1.113730.3.8.1.3 NAME 'ipaSearchTimeLimit' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE) +## ipaSearchRecordsLimit - maximum number of records to return +attributetypes: ( 2.16.840.1.113730.3.8.1.4 NAME 'ipaSearchRecordsLimit' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE) +## ipaCustomFields - custom fields to show in the UI in addition to pre-defined ones +attributetypes: ( 2.16.840.1.113730.3.8.1.5 NAME 'ipaCustomFields' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15) +## ipaHomesRootDir - default posix home directory root dir to use when creating new accounts +attributetypes: ( 2.16.840.1.113730.3.8.1.6 NAME 'ipaHomesRootDir' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE) +## ipaDefaultLoginShell - default posix login shell to use when creating new accounts +attributetypes: ( 2.16.840.1.113730.3.8.1.7 NAME 'ipaDefaultLoginShell' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE) +## ipaDefaultPrimaryGroup - default posix primary group to assign when creating new accounts +attributetypes: ( 2.16.840.1.113730.3.8.1.8 NAME 'ipaDefaultPrimaryGroup' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE) +## ipaMaxUsernameLength - maximum username length to allow in the UI +attributetypes: ( 2.16.840.1.113730.3.8.1.9 NAME 'ipaMaxUsernameLength' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE) +## ipaPwdExpAdvNotify - time in days to send out paswwrod expiration notification before passwpord actually expires +attributetypes: ( 2.16.840.1.113730.3.8.1.10 NAME 'ipaPwdExpAdvNotify' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE) +############################################### +## +## ObjectClasses +## +## ipaGuiConfig - GUI config parameters objectclass +objectClasses: ( 2.16.840.1.113730.3.8.2.1 NAME 'ipaGuiConfig' AUXILIARY MAY ( ipaUserSearchFields $ ipaGroupSearchFields $ ipaSearchTimeLimit $ ipaSearchRecordsLimit $ ipaCustomFields $ ipaHomesRootDir $ ipaDefaultLoginShell $ ipaDefaultPrimaryGroup $ ipaMaxUsernameLength $ ipaPwdExpAdvNotify ) ) diff --git a/ipa-server/ipa-install/share/Makefile.am b/ipa-server/ipa-install/share/Makefile.am index b103d5670..36bb54e83 100644 --- a/ipa-server/ipa-install/share/Makefile.am +++ b/ipa-server/ipa-install/share/Makefile.am @@ -5,6 +5,7 @@ app_DATA = \ 60kerberos.ldif \ 60samba.ldif \ 60radius.ldif \ + 60ipaconfig.ldif \ bootstrap-template.ldif \ default-aci.ldif \ kerberos.ldif \ @@ -22,6 +23,7 @@ app_DATA = \ referint-conf.ldif \ dna-posix.ldif \ master-entry.ldif \ + memberof-task.ldif \ $(NULL) EXTRA_DIST = \ diff --git a/ipa-server/ipa-install/share/bootstrap-template.ldif b/ipa-server/ipa-install/share/bootstrap-template.ldif index df59bc0ec..6232a3f69 100644 --- a/ipa-server/ipa-install/share/bootstrap-template.ldif +++ b/ipa-server/ipa-install/share/bootstrap-template.ldif @@ -8,7 +8,13 @@ dn: cn=accounts,$SUFFIX changetype: add objectClass: top objectClass: nsContainer +objectClass: krbPwdPolicy cn: accounts +krbMinPwdLife: 3600 +krbPwdMinDiffChars: 0 +krbPwdMinLength: 8 +krbPwdHistoryLength: 0 +krbMaxPwdLife: 864000 dn: cn=users,cn=accounts,$SUFFIX changetype: add @@ -22,10 +28,11 @@ objectClass: top objectClass: nsContainer cn: groups -#dn: cn=computers,cn=accounts,$SUFFIX -#objectClass: top -#objectClass: nsContainer -#cn: computers +dn: cn=services,cn=accounts,$SUFFIX +changetype: add +objectClass: top +objectClass: nsContainer +cn: services dn: cn=etc,$SUFFIX changetype: add @@ -101,17 +108,80 @@ uid: ipa_default dn: cn=admins,cn=groups,cn=accounts,$SUFFIX changetype: add objectClass: top -objectClass: groupofuniquenames +objectClass: groupofnames objectClass: posixGroup cn: admins description: Account administrators group gidNumber: 1001 -uniqueMember: uid=admin,cn=sysaccounts,cn=etc,$SUFFIX +member: uid=admin,cn=sysaccounts,cn=etc,$SUFFIX dn: cn=ipausers,cn=groups,cn=accounts,$SUFFIX changetype: add objectClass: top -objectClass: groupofuniquenames +objectClass: groupofnames objectClass: posixGroup gidNumber: 1002 +description: Default group for all users cn: ipausers + +dn: cn=editors,cn=groups,cn=accounts,$SUFFIX +changetype: add +objectClass: top +objectClass: groupofnames +objectClass: posixGroup +gidNumber: 1003 +description: Limited admins who can edit other users +cn: editors + +dn: cn=ipaConfig,cn=etc,$SUFFIX +changetype: add +objectClass: nsContainer +objectClass: top +objectClass: ipaGuiConfig +ipaUserSearchFields: uid,givenName,sn,telephoneNumber,ou,title +ipaGroupSearchFields: cn,description +ipaSearchTimeLimit: 2 +ipaSearchRecordsLimit: 0 +ipaHomesRootDir: /home +ipaDefaultLoginShell: /bin/sh +ipaDefaultPrimaryGroup: ipausers +ipaMaxUsernameLength: 8 +ipaPwdExpAdvNotify: 4 + +dn: cn=account inactivation,cn=accounts,$SUFFIX +description: Lock accounts based on group membership +objectClass: top +objectClass: ldapsubentry +objectClass: cosSuperDefinition +objectClass: cosClassicDefinition +cosTemplateDn: cn=cosTemplates,cn=accounts,$SUFFIX +cosAttribute: nsAccountLock operational +cosSpecifier: memberOf +cn: Account Inactivation + +dn: cn=cosTemplates,cn=accounts,$SUFFIX +objectclass: top +objectclass: nsContainer +cn: cosTemplates + +dn: cn="cn=inactivated,cn=account inactivation,cn=accounts,$SUFFIX", cn=cosTemplates,cn=accounts,$SUFFIX +objectClass: top +objectClass: cosTemplate +objectClass: extensibleobject +nsAccountLock: true +cosPriority: 1 + +dn: cn=inactivated,cn=account inactivation,cn=accounts,$SUFFIX +objectclass: top +objectclass: groupofnames + +dn: cn="cn=activated,cn=account inactivation,cn=accounts,$SUFFIX", cn=cosTemplates,cn=accounts,$SUFFIX +objectClass: top +objectClass: cosTemplate +objectClass: extensibleobject +nsAccountLock: false +cosPriority: 0 + +dn: cn=Activated,cn=Account Inactivation,cn=accounts,$SUFFIX +objectclass: top +objectclass: groupofnames diff --git a/ipa-server/ipa-install/share/default-aci.ldif b/ipa-server/ipa-install/share/default-aci.ldif index 5d19329e8..aac7272c6 100644 --- a/ipa-server/ipa-install/share/default-aci.ldif +++ b/ipa-server/ipa-install/share/default-aci.ldif @@ -4,9 +4,24 @@ changetype: modify replace: aci aci: (targetattr!="userPassword || krbPrincipalKey ||sambaLMPassword || sambaNTPassword")(version 3.0; acl "Enable anonymous access"; allow (read, search, compare) userdn="ldap:///anyone";) aci: (targetattr=*)(version 3.0; acl "Admin can manage any entry"; allow (all) userdn="ldap:///uid=admin,cn=sysaccounts,cn=etc,$SUFFIX";) -aci: (targetattr="krbPrincipalName || krbUPEnabled || krbPrincipalKey || krbTicketPolicyReference || krbPrincipalExpiration || krbPasswordExpiration || krbPwdPolicyReference || krbPrincipalType || krbPwdHistory || krbLastPwdChange || krbPrincipalAliases || krbExtraData")(version 3.0; acl "KDC System Account"; allow (read, search, compare) userdn="ldap:///uid=kdc,cn=sysaccounts,cn=etc,$SUFFIX";) +aci: (targetattr="krbPrincipalName || krbUPEnabled || krbPrincipalKey || krbMKey || krbTicketPolicyReference || krbPrincipalExpiration || krbPasswordExpiration || krbPwdPolicyReference || krbPrincipalType || krbPwdHistory || krbLastPwdChange || krbPrincipalAliases || krbExtraData")(version 3.0; acl "KDC System Account"; allow (read, search, compare) userdn="ldap:///uid=kdc,cn=sysaccounts,cn=etc,$SUFFIX";) aci: (targetattr="krbLastSuccessfulAuth || krbLastFailedAuth || krbLoginFailedCount")(version 3.0; acl "KDC System Account"; allow (read, search, compare, write) userdn="ldap:///uid=kdc,cn=sysaccounts,cn=etc,$SUFFIX";) aci: (targetattr="userPassword || krbPrincipalKey ||sambaLMPassword || sambaNTPassword || krbPasswordExpiration || krbPwdHistory || krbLastPwdChange")(version 3.0; acl "Kpasswd access to passowrd hashes for passowrd changes"; allow (read, write) userdn="ldap:///krbprincipalname=kadmin/changepw@$REALM,cn=$REALM,cn=kerberos,$SUFFIX";) -aci: (targetfilter="(|(objectClass=person)(objectClass=krbPrincipalAux)(objectClass=posixAccount)(objectClass=groupOfUniqueNames)(objectClass=posixGroup)(objectClass=radiusprofile))")(targetattr="*")(version 3.0; acl "Account Admins can manage Users and Groups"; allow (add,delete,read,write) groupdn="ldap:///cn=admins,cn=groups,cn=accounts,$SUFFIX";) +aci: (targetfilter="(|(objectClass=person)(objectClass=krbPrincipalAux)(objectClass=posixAccount)(objectClass=groupOfNames)(objectClass=posixGroup)(objectClass=radiusprofile))")(targetattr="*")(version 3.0; acl "Account Admins can manage Users and Groups"; allow (add,delete,read,write) groupdn="ldap:///cn=admins,cn=groups,cn=accounts,$SUFFIX";) aci: (targetattr = "givenName || sn || cn || displayName || initials || loginShell || homePhone || mobile || pager || facsimileTelephoneNumber || telephoneNumber || street || roomNumber || l || st || postalCode || manager || description || carLicense || labeledURI || inetUserHTTPURL || seeAlso || userPassword")(version 3.0;acl "Self service";allow (write) userdn="ldap:///self";) aci: (target="ldap:///cn=radius,cn=services,cn=etc,$SUFFIX")(version 3.0; acl "Only radius and admin can access radius service data"; deny (all) userdn!="ldap:///uid=admin,cn=sysaccounts,cn=etc,$SUFFIX || ldap:///krbprincipalname=radius/$FQDN@$REALM,cn=$REALM,cn=kerberos,$SUFFIX";) + +dn: cn=ipaConfig,cn=etc,$SUFFIX +changetype: modify +add: aci +aci: (targetattr = "ipaUserSearchFields || ipaGroupSearchFields || ipaSearchTimeLimit || ipaSearchRecordsLimit || ipaCustomFields || ipaHomesRootDir || ipaDefaultLoginShell || ipaDefaultPrimaryGroup || ipaMaxUsernameLength || ipaPwdExpAdvNotify")(version 3.0;acl "Admins can write IPA policy"; allow (write) groupdn="ldap:///cn=admins,cn=groups,cn=accounts,$SUFFIX";) + +dn: cn=accounts,$SUFFIX +changetype: modify +add: aci +aci: (targetattr = "krbMaxPwdLife || krbMinPwdLife || krbPwdMinDiffChars || krbPwdMinLength || krbPwdHistoryLength")(version 3.0;acl "Admins can write password policy"; allow (write) groupdn="ldap:///cn=admins,cn=groups,cn=accounts,$SUFFIX";) + +dn: cn=services,cn=accounts,$SUFFIX +changetype: modify +add: aci +aci: (targetattr="krbPrincipalName || krbUPEnabled || krbPrincipalKey || krbMKey || krbTicketPolicyReference || krbPrincipalExpiration || krbPasswordExpiration || krbPwdPolicyReference || krbPrincipalType || krbPwdHistory || krbLastPwdChange || krbPrincipalAliases || krbExtraData")(version 3.0; acl "KDC System Account"; allow (read, search, compare,write) userdn="ldap:///uid=kdc,cn=sysaccounts,cn=etc,$SUFFIX";) diff --git a/ipa-server/ipa-install/share/indeces.ldif b/ipa-server/ipa-install/share/indeces.ldif index 11dc3c0ec..31cbc30ab 100644 --- a/ipa-server/ipa-install/share/indeces.ldif +++ b/ipa-server/ipa-install/share/indeces.ldif @@ -42,6 +42,14 @@ cn:manager nsSystemIndex:false nsIndexType:eq +dn: cn=secretary,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config +changetype: add +objectClass:top +objectClass:nsIndex +cn:secretary +nsSystemIndex:false +nsIndexType:eq + dn: cn=displayname,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config changetype: add objectClass:top diff --git a/ipa-server/ipa-install/share/kerberos.ldif b/ipa-server/ipa-install/share/kerberos.ldif index d55f39ce4..75057aa3a 100644 --- a/ipa-server/ipa-install/share/kerberos.ldif +++ b/ipa-server/ipa-install/share/kerberos.ldif @@ -14,22 +14,4 @@ objectClass: top cn: kerberos aci: (targetattr="*")(version 3.0; acl "KDC System Account"; allow (all) userdn= "ldap:///uid=kdc,cn=sysaccounts,cn=etc,$SUFFIX";) -#sasl mapping -dn: cn=Full Principal,cn=mapping,cn=sasl,cn=config -changetype: add -objectclass: top -objectclass: nsSaslMapping -cn: Full Principal -nsSaslMapRegexString: \(.*\)@\(.*\) -nsSaslMapBaseDNTemplate: $SUFFIX -nsSaslMapFilterTemplate: (krbPrincipalName=\1@\2) - -dn: cn=Name Only,cn=mapping,cn=sasl,cn=config -changetype: add -objectclass: top -objectclass: nsSaslMapping -cn: Name Only -nsSaslMapRegexString: \(.*\) -nsSaslMapBaseDNTemplate: $SUFFIX -nsSaslMapFilterTemplate: (krbPrincipalName=\1@$REALM) diff --git a/ipa-server/ipa-install/share/memberof-task.ldif b/ipa-server/ipa-install/share/memberof-task.ldif new file mode 100644 index 000000000..fefabba88 --- /dev/null +++ b/ipa-server/ipa-install/share/memberof-task.ldif @@ -0,0 +1,7 @@ +dn: cn=IPA install, cn=memberof task, cn=tasks, cn=config +changetype: add +objectClass: top +objectClass: extensibleObject +cn: IPA install +basedn: $SUFFIX +filter: (objectclass=*) diff --git a/ipa-server/ipa-keytab-util/Makefile.am b/ipa-server/ipa-keytab-util/Makefile.am new file mode 100644 index 000000000..f0680e598 --- /dev/null +++ b/ipa-server/ipa-keytab-util/Makefile.am @@ -0,0 +1,22 @@ +NULL = + +sbin_PROGRAMS = \ + ipa-keytab-util \ + $(NULL) + +ipa_keytab_util_SOURCES = \ + ipa-keytab-util.c \ + $(NULL) + +ipa_keytab_util_LDADD = \ + -lcap \ + $(NULL) + +MAINTAINERCLEANFILES = \ + *~ \ + Makefile.in + +install-exec-hook: + -chown root:apache $(DESTDIR)$(sbindir)/ipa-keytab-util + -chmod o-rwxs $(DESTDIR)$(sbindir)/ipa-keytab-util + -chmod ug+s $(DESTDIR)$(sbindir)/ipa-keytab-util diff --git a/ipa-server/ipa-keytab-util/ipa-keytab-util.c b/ipa-server/ipa-keytab-util/ipa-keytab-util.c new file mode 100644 index 000000000..d080d0cd5 --- /dev/null +++ b/ipa-server/ipa-keytab-util/ipa-keytab-util.c @@ -0,0 +1,304 @@ +/* + * Authors: + * Karl MacMillan <kmacmill@redhat.com> + * + * Copyright (C) 2007 Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#define _GNU_SOURCE /* for asprintf */ +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/capability.h> +#include <sys/prctl.h> +#include <unistd.h> +#include <sys/types.h> +#include <sys/wait.h> + +#define KADMIN_PATH "/usr/kerberos/sbin/kadmin.local" + +struct options +{ + char *princ_name; + char *realm; + int kstdin, kstdout, kstderr; +}; + +void *xmalloc(size_t size) +{ + void *foo = malloc(size); + if (!foo) { + fprintf(stderr, "malloc error of size %jd\n", size); + exit(1); + } + memset(foo, 0, size); + + return foo; +} + +void usage(void) +{ + printf("ipa-keytab-util princ-name realm-name\n"); +} + +struct options *process_args(int argc, char **argv) +{ + struct options* opts; + + opts = xmalloc(sizeof(struct options)); + + if (argc != 3) { + usage(); + exit(1); + } + + opts->princ_name = argv[1]; + opts->realm = argv[2]; + + return opts; +} + +void drop_caps(void) +{ + cap_t caps; + int ret; + + if (geteuid() != 0) + return; + if (getuid() != 0) + return; + + caps = cap_init(); + if (!caps) { + perror("error initializing caps"); + exit(1); + } + ret = cap_clear(caps); + if (ret != 0) { + perror("could not clear capps"); + exit(1); + } + + ret = cap_set_proc(caps); + if (ret != 0) { + perror("could not drop caps"); + exit(1); + } + + cap_free(caps); +} + +pid_t exec_kadmin_local(struct options *opts) +{ + int stdin_pipes[2]; + int stdout_pipes[2]; + int stderr_pipes[2]; + int ret; + pid_t chpid; + char *princ; + + /* create a pair of pipes for stdin / stdout + of the child process. + */ + + if (pipe(stdin_pipes) == -1) { + perror("creating stdin"); + exit(1); + } + + if (pipe(stdout_pipes) == -1) { + perror("creating stdin"); + exit(1); + } + + if (pipe(stderr_pipes) == -1) { + perror("creating stdin"); + exit(1); + } + + chpid = fork(); + if (chpid == -1) { + perror("fork"); + exit(1); + } + + /* CHILD */ + if (chpid == 0) { + /* stdin */ + close(stdin_pipes[1]); + dup2(stdin_pipes[0], 0); + + /* stdout */ + close(stdout_pipes[0]); + dup2(stdout_pipes[1], 1); + + /* stderr */ + close(stderr_pipes[0]); + dup2(stdout_pipes[1], 2); + + /* now exec kadmin.local */ + + ret = asprintf(&princ, "admin@%s", opts->realm); + if (!princ) { + perror("creating bind princ"); + exit(1); + } + ret = execl(KADMIN_PATH, "kadmin.local", "-p", princ, NULL); + free(princ); + if (ret == -1) { + perror("exec"); + exit(1); + } + } else { + close(stdin_pipes[0]); + close(stdout_pipes[1]); + close(stderr_pipes[1]); + + opts->kstdin = stdin_pipes[1]; + opts->kstdout = stdout_pipes[0]; + opts->kstderr = stdout_pipes[0]; + } + + return chpid; +} + +void write_to_kadmin(struct options *opts, char *buf, int len) +{ + int ret; + + ret = write(opts->kstdin, buf, len); + if (ret != len) { + perror("write"); + fprintf(stderr, "write is short %d:%d\n", len, ret); + exit(1); + } + fsync(opts->kstdin); +} + +char *get_temp_filename(void) +{ + char *fname; + /* ok - we have to use mktemp here even w/ the race + * because kadmin.local barfs if the file exists. The + * risk is pretty low and we will try to protect the files + * with selinux. + * + * TODO: generate these files in a safer place than /tmp + */ + fname = strdup("/tmp/ipa-keytab-util-XXXXXX"); + if (!fname) { + fprintf(stderr, "could not allocate temporary file name"); + exit(1); + } + fname = mktemp(fname); + + return fname; +} + +char *create_keytab(struct options *opts) +{ + char *buf, *fname; + int ret; + + fname = get_temp_filename(); + + ret = asprintf(&buf, "ktadd -k %s %s\n", fname, opts->princ_name); + if (ret == -1) { + perror("asprintf"); + exit(1); + } + + write_to_kadmin(opts, buf, ret); + + free(buf); + + write_to_kadmin(opts, "quit\n", sizeof("quit\n")); + + return fname; +} + +void read_keytab(char *fname) +{ + FILE *fd; + char *data; + long flen, ret; + + fd = fopen(fname, "r"); + if (!fd) { + fprintf(stderr, "could not open file %s: ", fname); + perror(NULL); + exit(1); + } + + fseek(fd, 0, SEEK_END); + flen = ftell(fd); + rewind(fd); + + data = xmalloc(flen); + + /* TODO: handle short reads */ + ret = fread(data, 1, flen, fd); + if (ret != flen) { + fprintf(stderr, "short read"); + exit(1); + } + + fclose(fd); + + /* write to stdout */ + ret = fwrite(data, 1, flen, stdout); + if (ret != flen) { + fprintf(stderr, "short write"); + exit(1); + } +} + +void remove_keytab(char *filename) +{ + unlink(filename); +} + +/* TODO: add significantly better authorization */ +int main(int argc, char **argv) +{ + struct options *opts; + pid_t chpid; + int status, ret; + char *fname; + + opts = process_args(argc, argv); + + /* must really be root */ + setuid(0); + + drop_caps(); + + + chpid = exec_kadmin_local(opts); + fname = create_keytab(opts); + + ret = waitpid(-1, &status, 0); + if (WEXITSTATUS(status)) { + fprintf(stderr, "error creating keytab\n"); + exit(1); + } + + read_keytab(fname); + remove_keytab(fname); + + return 0; +} diff --git a/ipa-server/ipa-kpasswd/ipa_kpasswd.c b/ipa-server/ipa-kpasswd/ipa_kpasswd.c index f5540b74c..b0020c04f 100644 --- a/ipa-server/ipa-kpasswd/ipa_kpasswd.c +++ b/ipa-server/ipa-kpasswd/ipa_kpasswd.c @@ -28,26 +28,54 @@ #define TMP_TEMPLATE "/tmp/kpasswd.XXXXXX" #define KPASSWD_PORT 464 +/* blacklist entries are released only BLCAKLIST_TIMEOUT seconds + * after the children performing the noperation has finished. + * this is to avoid races */ + +#define BLACKLIST_TIMEOUT 5 + struct blacklist { struct blacklist *next; char *address; pid_t pid; + time_t expire; }; static struct blacklist *global_blacklist = NULL; int check_blacklist(char *address) { - struct blacklist *bl; + struct blacklist *bl, *prev_bl; + time_t now = time(NULL); if (!global_blacklist) { return 0; } - for (bl = global_blacklist; bl; bl = bl->next) { + prev_bl = NULL; + bl = global_blacklist; + while (bl) { + if (bl->expire && (bl->expire < now)) { + if (prev_bl) { + prev_bl->next = bl->next; + free(bl->address); + free(bl); + bl = prev_bl->next; + } else { + global_blacklist = bl->next; + free(bl->address); + free(bl); + bl = global_blacklist; + } + continue; + } + if (strcmp(address, bl->address) == 0) { return 1; } + + prev_bl = bl; + bl = bl->next; } return 0; @@ -62,6 +90,7 @@ int add_blacklist(pid_t pid, char *address) bl->next = NULL; bl->pid = pid; + bl->expire = 0; bl->address = strdup(address); if (!bl->address) { free(bl); @@ -83,32 +112,24 @@ int add_blacklist(pid_t pid, char *address) int remove_blacklist(pid_t pid) { - struct blacklist *bl, *pbl; + struct blacklist *bl; if (!global_blacklist) { return -1; } - pbl = NULL; bl = global_blacklist; while (bl) { if (pid == bl->pid) { - if (pbl == NULL) { - global_blacklist = bl->next; - } else { - pbl->next = bl->next; - } - free(bl->address); - free(bl); + bl->expire = time(NULL) + BLACKLIST_TIMEOUT; return 0; } - pbl = bl; bl = bl->next; } return -1; } -int debug = 1; +int debug = 0; char *srv_pri_name = "kadmin/changepw"; char *keytab_name = NULL; @@ -255,12 +276,19 @@ int ldap_sasl_interact(LDAP *ld, unsigned flags, void *priv_data, void *sit) return ret; } -int ldap_pwd_change(char *client_name, char *realm_name, krb5_data pwd) +/* from DS ldaprot.h */ +#define LDAP_TAG_PWP_WARNING 0xA0 /* context specific + constructed + 0 */ +#define LDAP_TAG_PWP_SECSLEFT 0x80L /* context specific + primitive */ +#define LDAP_TAG_PWP_GRCLOGINS 0x81L /* context specific + primitive + 1 */ +#define LDAP_TAG_PWP_ERROR 0x81L /* context specific + primitive + 1 */ + +int ldap_pwd_change(char *client_name, char *realm_name, krb5_data pwd, char **errstr) { char *tmp_file = NULL; int version; LDAP *ld = NULL; BerElement *ctrl = NULL; + BerElement *sctrl = NULL; struct berval control; struct berval newpw; char hostname[1024]; @@ -275,7 +303,12 @@ int ldap_pwd_change(char *client_name, char *realm_name, krb5_data pwd) struct berval *retdata = NULL; struct timeval tv; LDAPMessage *entry, *res = NULL; - int ret; + LDAPControl **srvctrl = NULL; + char *exterr1 = NULL; + char *exterr2 = NULL; + char *err; + int msgid; + int ret, rc; tmp_file = strdup(TMP_TEMPLATE); if (!tmp_file) { @@ -399,7 +432,12 @@ int ldap_pwd_change(char *client_name, char *realm_name, krb5_data pwd) if (ret != LDAP_SUCCESS) { syslog(LOG_ERR, "Search for %s failed with error %d", filter, ret); - ret = KRB5_KPASSWD_HARDERROR; + if (ret == LDAP_CONSTRAINT_VIOLATION) { + *errstr = strdup("Password Change Failed"); + ret = KRB5_KPASSWD_SOFTERROR; + } else { + ret = KRB5_KPASSWD_HARDERROR; + } goto done; } free(filter); @@ -409,6 +447,7 @@ int ldap_pwd_change(char *client_name, char *realm_name, krb5_data pwd) userdn = ldap_get_dn(ld, entry); ldap_msgfree(res); + res = NULL; if (!userdn) { syslog(LOG_ERR, "No userdn, can't change password!"); @@ -430,25 +469,164 @@ int ldap_pwd_change(char *client_name, char *realm_name, krb5_data pwd) ret = ber_flatten2(ctrl, &control, 0); if (ret < 0) { syslog(LOG_ERR, "ber flattening failed!"); - ret = -1; + ret = KRB5_KPASSWD_HARDERROR; goto done; } /* perform password change */ - ret = ldap_extended_operation_s(ld, LDAP_EXOP_MODIFY_PASSWD, &control, - NULL, NULL, &retoid, &retdata); - + ret = ldap_extended_operation(ld, + LDAP_EXOP_MODIFY_PASSWD, + &control, NULL, NULL, + &msgid); if (ret != LDAP_SUCCESS) { - syslog(LOG_ERR, "password change failed!"); + syslog(LOG_ERR, "ldap_extended_operation() failed. (%d)", ret); + ret = KRB5_KPASSWD_HARDERROR; + goto done; + } + + tv.tv_sec = 10; + tv.tv_usec = 0; + + ret = ldap_result(ld, msgid, 1, &tv, &res); + if (ret == -1) { + ldap_get_option(ld, LDAP_OPT_ERROR_NUMBER, &rc); + syslog(LOG_ERR, "ldap_result() failed. (%d)", rc); + ret = KRB5_KPASSWD_HARDERROR; + goto done; + } + + ret = ldap_parse_extended_result(ld, res, &retoid, &retdata, 0); + if(ret != LDAP_SUCCESS) { + syslog(LOG_ERR, "ldap_parse_extended_result() failed."); + ldap_msgfree(res); + ret = KRB5_KPASSWD_HARDERROR; + goto done; + } + if (retoid || retdata) { + syslog(LOG_ERR, "ldap_parse_extended_result() returned data, but we don't handle it yet."); + } + + ret = ldap_parse_result(ld, res, &rc, NULL, &err, NULL, &srvctrl, 0); + if(ret != LDAP_SUCCESS) { + syslog(LOG_ERR, "ldap_parse_result() failed."); ret = KRB5_KPASSWD_HARDERROR; goto done; + } + if (rc != LDAP_SUCCESS) { + ret = KRB5_KPASSWD_SOFTERROR; + if (rc != LDAP_CONSTRAINT_VIOLATION) { + ret = KRB5_KPASSWD_HARDERROR; + } + } + if (err) { + syslog(LOG_ERR, "ldap_parse_result(): [%s]", err); + ldap_memfree(err); } - /* TODO: interpret retdata so that we can give back meaningful errors */ + if (srvctrl) { + + LDAPControl *pprc = NULL; + int i; + + for (i = 0; srvctrl[i]; i++) { + if (0 == strcmp(srvctrl[i]->ldctl_oid, LDAP_CONTROL_PASSWORDPOLICYRESPONSE)) { + pprc = srvctrl[i]; + } + } + if (pprc) { + sctrl = ber_init(&pprc->ldctl_value); + } + + if (sctrl) { + /* + * PasswordPolicyResponseValue ::= SEQUENCE { + * warning [0] CHOICE OPTIONAL { + * timeBeforeExpiration [0] INTEGER (0 .. maxInt), + * graceLoginsRemaining [1] INTEGER (0 .. maxInt) } + * error [1] ENUMERATED OPTIONAL { + * passwordExpired (0), + * accountLocked (1), + * changeAfterReset (2), + * passwordModNotAllowed (3), + * mustSupplyOldPassword (4), + * invalidPasswordSyntax (5), + * passwordTooShort (6), + * passwordTooYoung (7), + * passwordInHistory (8) } } + */ + + ber_tag_t rtag, btag; + ber_int_t bint; + rtag = ber_scanf(sctrl, "{t", &btag); + if (btag == LDAP_TAG_PWP_WARNING) { + rtag = ber_scanf(sctrl, "{ti}", &btag, &bint); + if (btag == LDAP_TAG_PWP_SECSLEFT) { + asprintf(&exterr2, " (%d seconds left before password expires)", bint); + } else { + asprintf(&exterr2, " (%d grace logins remaining)", bint); + } + if (!exterr2) { + syslog(LOG_ERR, "exterr2: OOM?"); + } + rtag = ber_scanf(sctrl, "t", &btag); + } + if (btag == LDAP_TAG_PWP_ERROR) { + rtag = ber_scanf(sctrl, "e", &bint); + switch(bint) { + case 0: + asprintf(&exterr1, " Err%d: Password Expired.", bint); + break; + case 1: + asprintf(&exterr1, " Err%d: Account locked.", bint); + break; + case 2: + asprintf(&exterr1, " Err%d: Password changed after reset.", bint); + break; + case 3: + asprintf(&exterr1, " Err%d: Password change not allowed.", bint); + break; + case 4: + asprintf(&exterr1, " Err%d: [Shouldn't happen].", bint); + break; + case 5: + asprintf(&exterr1, " Err%d: Password too simple.", bint); + break; + case 6: + asprintf(&exterr1, " Err%d: Password too short.", bint); + break; + case 7: + asprintf(&exterr1, " Err%d: Too soon to change password.", bint); + break; + case 8: + asprintf(&exterr1, " Err%d: Password reuse not permitted.", bint); + break; + default: + asprintf(&exterr1, " Err%d: Unknown Errorcode.", bint); + break; + } + if (!exterr1) { + syslog(LOG_ERR, "exterr1: OOM?"); + } + } + } + } + + if (ret != LDAP_SUCCESS) { + syslog(LOG_ERR, "password change failed!"); + asprintf(errstr, "Password change, Failed.%s%s", exterr1?exterr1:"", exterr2?exterr2:""); + } else { + syslog(LOG_ERR, "password change succeeded!"); + asprintf(errstr, "Password change, Succeeded.%s%s", exterr1?exterr1:"", exterr2?exterr2:""); + } done: - if (userdn) free(userdn); if (ctrl) ber_free(ctrl, 1); + if (sctrl) ber_free(sctrl, 1); + if (srvctrl) ldap_controls_free(srvctrl); + if (res) ldap_msgfree(res); + if (exterr1) free(exterr1); + if (exterr2) free(exterr2); + if (userdn) free(userdn); if (ld) ldap_unbind_ext_s(ld, NULL, NULL); if (ldap_uri) free(ldap_uri); if (tmp_file) { @@ -482,6 +660,7 @@ void handle_krb_packets(uint8_t *buf, ssize_t buflen, *replen = 0; + result_string = NULL; auth_context = NULL; krep.length = 0; krep.data = NULL; @@ -508,14 +687,14 @@ void handle_krb_packets(uint8_t *buf, ssize_t buflen, } break; default: - result_string = "Invalid remopte IP address"; + result_string = strdup("Invalid remopte IP address"); result_err = KRB5_KPASSWD_MALFORMED; syslog(LOG_ERR, "%s", result_string); goto done; } if (buflen < 4) { - result_string = "Request truncated"; + result_string = strdup("Request truncated"); result_err = KRB5_KPASSWD_MALFORMED; syslog(LOG_ERR, "%s", result_string); goto done; @@ -524,7 +703,7 @@ void handle_krb_packets(uint8_t *buf, ssize_t buflen, reqlen = (buf[0] << 8) + buf[1]; if (reqlen != buflen) { - result_string = "Unmatching request length"; + result_string = strdup("Unmatching request length"); result_err = KRB5_KPASSWD_MALFORMED; syslog(LOG_ERR, "%s", result_string); goto done; @@ -533,7 +712,7 @@ void handle_krb_packets(uint8_t *buf, ssize_t buflen, verno = (buf[2] << 8) + buf[3]; if (verno != 1) { - result_string = "Unsupported version"; + result_string = strdup("Unsupported version"); result_err = KRB5_KPASSWD_BAD_VERSION; syslog(LOG_ERR, "%s", result_string); goto done; @@ -541,7 +720,7 @@ void handle_krb_packets(uint8_t *buf, ssize_t buflen, kreq.length = (buf[4] << 8) + buf[5]; if (kreq.length > (buflen - 6)) { - result_string = "Request truncated"; + result_string = strdup("Request truncated"); result_err = KRB5_KPASSWD_MALFORMED; syslog(LOG_ERR, "%s", result_string); goto done; @@ -550,7 +729,7 @@ void handle_krb_packets(uint8_t *buf, ssize_t buflen, krberr = krb5_init_context(&context); if (krberr) { - result_string = "Failed to init kerberos context"; + result_string = strdup("Failed to init kerberos context"); result_err = KRB5_KPASSWD_HARDERROR; syslog(LOG_ERR, "%s", result_string); goto done; @@ -558,7 +737,7 @@ void handle_krb_packets(uint8_t *buf, ssize_t buflen, krberr = krb5_get_default_realm(context, &realm_name); if (krberr) { - result_string = "Failed to get default realm name"; + result_string = strdup("Failed to get default realm name"); result_err = KRB5_KPASSWD_HARDERROR; syslog(LOG_ERR, "%s", result_string); goto done; @@ -566,7 +745,7 @@ void handle_krb_packets(uint8_t *buf, ssize_t buflen, krberr = krb5_auth_con_init(context, &auth_context); if (krberr) { - result_string = "Unable to init auth context"; + result_string = strdup("Unable to init auth context"); result_err = KRB5_KPASSWD_HARDERROR; syslog(LOG_ERR, "%s: %s", result_string, krb5_get_error_message(context, krberr)); @@ -576,7 +755,7 @@ void handle_krb_packets(uint8_t *buf, ssize_t buflen, krberr = krb5_auth_con_setflags(context, auth_context, KRB5_AUTH_CONTEXT_DO_SEQUENCE); if (krberr) { - result_string = "Unable to init auth context"; + result_string = strdup("Unable to init auth context"); result_err = KRB5_KPASSWD_HARDERROR; syslog(LOG_ERR, "%s: %s", result_string, krb5_get_error_message(context, krberr)); @@ -587,7 +766,7 @@ void handle_krb_packets(uint8_t *buf, ssize_t buflen, strlen(realm_name), realm_name, "kadmin", "changepw", NULL); if (krberr) { - result_string = "Unable to build principal"; + result_string = strdup("Unable to build principal"); result_err = KRB5_KPASSWD_HARDERROR; syslog(LOG_ERR, "%s: %s", result_string, krb5_get_error_message(context, krberr)); @@ -596,7 +775,7 @@ void handle_krb_packets(uint8_t *buf, ssize_t buflen, krberr = krb5_kt_resolve(context, keytab_name, &keytab); if (krberr) { - result_string = "Unable to retrieve keytab"; + result_string = strdup("Unable to retrieve keytab"); result_err = KRB5_KPASSWD_HARDERROR; syslog(LOG_ERR, "%s: %s", result_string, krb5_get_error_message(context, krberr)); @@ -606,7 +785,7 @@ void handle_krb_packets(uint8_t *buf, ssize_t buflen, krberr = krb5_rd_req(context, &auth_context, &kreq, kprincpw, keytab, NULL, &ticket); if (krberr) { - result_string = "Unable to read request"; + result_string = strdup("Unable to read request"); result_err = KRB5_KPASSWD_AUTHERROR; syslog(LOG_ERR, "%s: %s", result_string, krb5_get_error_message(context, krberr)); @@ -618,7 +797,7 @@ void handle_krb_packets(uint8_t *buf, ssize_t buflen, * the password have been successfully changed */ krberr = krb5_mk_rep(context, auth_context, &krep); if (krberr) { - result_string = "Failed to to build reply"; + result_string = strdup("Failed to to build reply"); result_err = KRB5_KPASSWD_HARDERROR; syslog(LOG_ERR, "%s: %s", result_string, krb5_get_error_message(context, krberr)); @@ -627,7 +806,7 @@ void handle_krb_packets(uint8_t *buf, ssize_t buflen, /* verify that this is an AS_REQ ticket */ if (!(ticket->enc_part2->flags & TKT_FLG_INITIAL)) { - result_string = "Ticket must be derived from a password"; + result_string = strdup("Ticket must be derived from a password"); result_err = KRB5_KPASSWD_AUTHERROR; syslog(LOG_ERR, "%s", result_string); goto kpreply; @@ -636,7 +815,7 @@ void handle_krb_packets(uint8_t *buf, ssize_t buflen, krberr = krb5_unparse_name(context, ticket->enc_part2->client, &client_name); if (krberr) { - result_string = "Unable to parse client name"; + result_string = strdup("Unable to parse client name"); result_err = KRB5_KPASSWD_HARDERROR; syslog(LOG_ERR, "%s", result_string); goto kpreply; @@ -644,7 +823,7 @@ void handle_krb_packets(uint8_t *buf, ssize_t buflen, krberr = krb5_auth_con_setaddrs(context, auth_context, NULL, &rkaddr); if (krberr) { - result_string = "Failed to set client address"; + result_string = strdup("Failed to set client address"); result_err = KRB5_KPASSWD_HARDERROR; syslog(LOG_ERR, "%s: %s", result_string, krb5_get_error_message(context, krberr)); @@ -659,24 +838,22 @@ void handle_krb_packets(uint8_t *buf, ssize_t buflen, * requires the local address (from kadmin code) */ krberr = krb5_rd_priv(context, auth_context, &kenc, &kdec, &replay); if (krberr) { - result_string = "Failed to decrypt password"; + result_string = strdup("Failed to decrypt password"); result_err = KRB5_KPASSWD_HARDERROR; syslog(LOG_ERR, "%s: %s", result_string, krb5_get_error_message(context, krberr)); goto kpreply; } - if (debug > 0) { + if (debug > 100) { syslog(LOG_ERR, "Client %s trying to set password [%*s]", client_name, kdec.length, kdec.data); } /* Actually try to change the password */ - result_err = ldap_pwd_change(client_name, realm_name, kdec); - if (result_err != KRB5_KPASSWD_SUCCESS) { - result_string = "Generic error occurred while changing password"; - } else { - result_string = ""; + result_err = ldap_pwd_change(client_name, realm_name, kdec, &result_string); + if (result_string == NULL) { + result_string = strdup("Server Error"); } /* make sure password is cleared off before we free the memory */ @@ -700,7 +877,7 @@ kpreply: /* we listen on ANYADDR, use this retrieve the right address */ krberr = krb5_os_localaddr(context, &lkaddr); if (krberr) { - result_string = "Failed to retrieve local address"; + result_string = strdup("Failed to retrieve local address"); syslog(LOG_ERR, "%s: %s", result_string, krb5_get_error_message(context, krberr)); goto done; @@ -708,7 +885,7 @@ kpreply: krberr = krb5_auth_con_setaddrs(context, auth_context, lkaddr[0], NULL); if (krberr) { - result_string = "Failed to set local address"; + result_string = strdup("Failed to set local address"); syslog(LOG_ERR, "%s: %s", result_string, krb5_get_error_message(context, krberr)); goto done; @@ -716,7 +893,7 @@ kpreply: krberr = krb5_mk_priv(context, auth_context, &kdec, &kenc, &replay); if (krberr) { - result_string = "Failed to encrypt reply message"; + result_string = strdup("Failed to encrypt reply message"); syslog(LOG_ERR, "%s: %s", result_string, krb5_get_error_message(context, krberr)); /* encryption was unsuccessful, let's return a krb error */ @@ -734,7 +911,7 @@ kpreply: krb5err.susec = 0; krberr = krb5_timeofday(context, &krb5err.stime); if (krberr) { - result_string = "Failed to set time of day"; + result_string = strdup("Failed to set time of day"); syslog(LOG_ERR, "%s: %s", result_string, krb5_get_error_message(context, krberr)); goto done; @@ -744,7 +921,7 @@ kpreply: krb5err.e_data = kdec; krberr = krb5_mk_error(context, &krb5err, &kenc); if (krberr) { - result_string = "Failed to build error message"; + result_string = strdup("Failed to build error message"); syslog(LOG_ERR, "%s: %s", result_string, krb5_get_error_message(context, krberr)); goto done; @@ -774,6 +951,7 @@ kpreply: *replen = replylen; done: + if (result_string) free(result_string); if (auth_context) krb5_auth_con_free(context, auth_context); if (kprincpw) krb5_free_principal(context, kprincpw); if (krep.length) free(krep.data); @@ -787,6 +965,7 @@ pid_t handle_conn(int fd, int type) { int mfd, tcp; pid_t pid; + char addrto6[INET6_ADDRSTRLEN+1]; char address[INET6_ADDRSTRLEN+1]; uint8_t request[1500]; ssize_t reqlen; @@ -809,17 +988,38 @@ pid_t handle_conn(int fd, int type) return -1; } } else { - mfd = fd; + /* read first to empty the buffer on udp connections */ + reqlen = recvfrom(fd, request, sizeof(request), 0, + (struct sockaddr *)&from, &fromlen); + if (reqlen <= 0) { + syslog(LOG_ERR, "Error receiving request (%d) %s", + errno, strerror(errno)); + return -1; + } + } (void) getnameinfo((struct sockaddr *)&from, fromlen, - address, INET6_ADDRSTRLEN+1, + addrto6, INET6_ADDRSTRLEN+1, NULL, 0, NI_NUMERICHOST); if (debug > 0) { - syslog(LOG_ERR, "Connection from %s", address); + syslog(LOG_ERR, "Connection from %s", addrto6); } + if (strchr(addrto6, ':') == NULL) { + char *prefix6 = "::ffff:"; + /* this is an IPv4 formatted addr + * convert to IPv6 mapped addr */ + memcpy(address, prefix6, 7); + memcpy(&address[7], addrto6, INET6_ADDRSTRLEN-7); + } else { + /* regular IPv6 address, copy as is */ + memcpy(address, addrto6, INET6_ADDRSTRLEN); + } + /* make sure we have termination */ + address[INET6_ADDRSTRLEN] = '\0'; + /* Check blacklist for requests from the same IP until operations * are finished on the active client. * the password change may be slow and pam_krb5 sends up to 3 UDP @@ -834,15 +1034,17 @@ pid_t handle_conn(int fd, int type) return 0; } - reqlen = recvfrom(mfd, request, sizeof(request), 0, - (struct sockaddr *)&from, &fromlen); - if (reqlen <= 0) { - syslog(LOG_ERR, "Error receiving request (%d) %s", - errno, strerror(errno)); - if (tcp) close(mfd); - return -1; + /* now read data if it was a TCP connection */ + if (tcp) { + reqlen = recvfrom(mfd, request, sizeof(request), 0, + (struct sockaddr *)&from, &fromlen); + if (reqlen <= 0) { + syslog(LOG_ERR, "Error receiving request (%d) %s", + errno, strerror(errno)); + close(mfd); + return -1; + } } - #if 1 /* handle kerberos and ldap operations in childrens */ pid = fork(); @@ -860,6 +1062,7 @@ pid_t handle_conn(int fd, int type) #endif /* children */ + if (debug > 0) syslog(LOG_ERR, "Servicing %s", address); /* TCP packets prepend the lenght as a 32bit network order field, * this information seem to be just redundant, so let's simply @@ -874,13 +1077,13 @@ pid_t handle_conn(int fd, int type) if (tcp) { sendret = sendto(mfd, reply, replen, 0, NULL, 0); } else { - sendret = sendto(mfd, reply, replen, 0, (struct sockaddr *)&from, fromlen); + sendret = sendto(fd, reply, replen, 0, (struct sockaddr *)&from, fromlen); } if (sendret == -1) { syslog(LOG_ERR, "Error sending reply (%d)", errno); } } - close(mfd); + if (tcp) close(mfd); exit(0); } @@ -895,7 +1098,7 @@ int main(int argc, char *argv[]) int pfdtype[4]; int nfds; int ret; - char *key; + char *env; /* log to syslog */ openlog("kpasswd", LOG_PID, LOG_DAEMON); @@ -935,15 +1138,21 @@ int main(int argc, char *argv[]) exit(0); } - key = getenv("KRB5_KTNAME"); - if (!key) { - key = DEFAULT_KEYTAB; + /* source evn vars */ + env = getenv("KRB5_KTNAME"); + if (!env) { + env = DEFAULT_KEYTAB; } - keytab_name = strdup(key); + keytab_name = strdup(env); if (!keytab_name) { syslog(LOG_ERR, "Out of memory!"); } + env = getenv("IPA_KPASSWD_DEBUG"); + if (env) { + debug = strtol(env, NULL, 0); + } + /* set hints */ memset(&hints, 0, sizeof(hints)); hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG; @@ -1026,6 +1235,8 @@ int main(int argc, char *argv[]) /* check for children exiting */ cid = waitpid(-1, &cstatus, WNOHANG); if (cid != -1 && cid != 0) { + if (debug > 0) + syslog(LOG_ERR, "pid %d completed operations!\n", cid); remove_blacklist(cid); } } diff --git a/ipa-server/freeipa-server.spec b/ipa-server/ipa-server.spec index 918e17c3b..81cf7d3d2 100755 --- a/ipa-server/freeipa-server.spec +++ b/ipa-server/ipa-server.spec @@ -1,7 +1,7 @@ -Name: freeipa-server -Version: 0.4.1 +Name: ipa-server +Version: 0.5.0 Release: 1%{?dist} -Summary: FreeIPA authentication server +Summary: Ipa authentication server Group: System Environment/Base License: GPL @@ -9,17 +9,41 @@ URL: http://www.freeipa.org Source0: %{name}-%{version}.tgz BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) -BuildRequires: fedora-ds-base-devel openldap-devel krb5-devel nss-devel mozldap-devel openssl-devel - -Requires: python fedora-ds-base krb5-server krb5-server-ldap nss-tools openldap-clients httpd mod_python mod_auth_kerb python-ldap freeipa-python ntp cyrus-sasl-gssapi nss TurboGears python-krbV acl freeipa-admintools +BuildRequires: fedora-ds-base-devel >= 1.1 +BuildRequires: mozldap-devel +BuildRequires: openssl-devel +BuildRequires: openldap-devel +BuildRequires: krb5-devel +BuildRequires: nss-devel + +Requires: ipa-python +Requires: ipa-admintools +Requires: fedora-ds-base >= 1.1 +Requires: openldap-clients +Requires: nss +Requires: nss-tools +Requires: krb5-server +Requires: krb5-server-ldap +Requires: cyrus-sasl-gssapi +Requires: ntp +Requires: httpd +Requires: mod_python +Requires: mod_auth_kerb Requires: mod_nss >= 1.0.7-2 -Requires: freeradius >= 1.1.7 +Requires: python-ldap +Requires: python +Requires: python-krbV +Requires: TurboGears +Requires: python-tgexpandingformwidget +Requires: acl +Requires: freeradius +Requires: pyasn1 %define httpd_conf /etc/httpd/conf.d %define plugin_dir %{_libdir}/dirsrv/plugins %description -FreeIPA is a server for identity, policy, and audit. +Ipa is a server for identity, policy, and audit. %prep %setup -q @@ -47,8 +71,11 @@ rm -rf %{buildroot} %files %defattr(-,root,root,-) %{_sbindir}/ipa-server-install +%{_sbindir}/ipa-replica-install +%{_sbindir}/ipa-replica-prepare %{_sbindir}/ipa_kpasswd %{_sbindir}/ipa-webgui +%attr(4750,root,apache) %{_sbindir}/ipa-keytab-util %attr(755,root,root) %{_initrddir}/ipa-kpasswd %attr(755,root,root) %{_initrddir}/ipa-webgui @@ -61,7 +88,17 @@ rm -rf %{buildroot} %changelog -* Thu Nov 1 2007 Karl MacMillan <kmacmill@redhat.com> - 0.4.1-1 +* Wed Nov 21 2007 Karl MacMillan <kmacmill@mentalrootkit.com> - 0.5.0-1 +- Preverse mode on ipa-keytab-util +- Version bump for relase and rpm name change + +* Thu Nov 15 2007 Rob Crittenden <rcritten@redhat.com> - 0.4.1-2 +- Broke invididual Requires and BuildRequires onto separate lines and + reordered them +- Added python-tgexpandingformwidget as a dependency +- Require at least fedora-ds-base 1.1 + +* Thu Nov 1 2007 Karl MacMillan <kmacmill@redhat.com> - 0.4.1-1 - Version bump for release * Wed Oct 31 2007 Karl MacMillan <kmacmill@redhat.com> - 0.4.0-6 @@ -98,5 +135,3 @@ rm -rf %{buildroot} * Fri Jul 27 2007 Karl MacMillan <kmacmill@redhat.com> - 0.1.0-1 - Initial rpm version - - diff --git a/ipa-server/freeipa-server.spec.in b/ipa-server/ipa-server.spec.in index d40aaf32a..cfe9b8d79 100644 --- a/ipa-server/freeipa-server.spec.in +++ b/ipa-server/ipa-server.spec.in @@ -1,7 +1,7 @@ -Name: freeipa-server +Name: ipa-server Version: VERSION Release: 1%{?dist} -Summary: FreeIPA authentication server +Summary: Ipa authentication server Group: System Environment/Base License: GPL @@ -9,17 +9,41 @@ URL: http://www.freeipa.org Source0: %{name}-%{version}.tgz BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) -BuildRequires: fedora-ds-base-devel openldap-devel krb5-devel nss-devel mozldap-devel openssl-devel - -Requires: python fedora-ds-base krb5-server krb5-server-ldap nss-tools openldap-clients httpd mod_python mod_auth_kerb python-ldap freeipa-python ntp cyrus-sasl-gssapi nss TurboGears python-krbV acl freeipa-admintools rpm +BuildRequires: fedora-ds-base-devel >= 1.1 +BuildRequires: mozldap-devel +BuildRequires: openssl-devel +BuildRequires: openldap-devel +BuildRequires: krb5-devel +BuildRequires: nss-devel + +Requires: ipa-python +Requires: ipa-admintools +Requires: fedora-ds-base >= 1.1 +Requires: openldap-clients +Requires: nss +Requires: nss-tools +Requires: krb5-server +Requires: krb5-server-ldap +Requires: cyrus-sasl-gssapi +Requires: ntp +Requires: httpd +Requires: mod_python +Requires: mod_auth_kerb Requires: mod_nss >= 1.0.7-2 -Requires: freeradius >= 1.1.7 +Requires: python-ldap +Requires: python +Requires: python-krbV +Requires: TurboGears +Requires: python-tgexpandingformwidget +Requires: acl +Requires: freeradius +Requires: pyasn1 %define httpd_conf /etc/httpd/conf.d %define plugin_dir %{_libdir}/dirsrv/plugins %description -FreeIPA is a server for identity, policy, and audit. +Ipa is a server for identity, policy, and audit. %prep %setup -q @@ -47,8 +71,11 @@ rm -rf %{buildroot} %files %defattr(-,root,root,-) %{_sbindir}/ipa-server-install +%{_sbindir}/ipa-replica-install +%{_sbindir}/ipa-replica-prepare %{_sbindir}/ipa_kpasswd %{_sbindir}/ipa-webgui +%attr(4750,root,apache) %{_sbindir}/ipa-keytab-util %attr(755,root,root) %{_initrddir}/ipa-kpasswd %attr(755,root,root) %{_initrddir}/ipa-webgui @@ -61,7 +88,17 @@ rm -rf %{buildroot} %changelog -* Thu Nov 1 2007 Karl MacMillan <kmacmill@redhat.com> - 0.4.1-1 +* Wed Nov 21 2007 Karl MacMillan <kmacmill@mentalrootkit.com> - 0.5.0-1 +- Preverse mode on ipa-keytab-util +- Version bump for relase and rpm name change + +* Thu Nov 15 2007 Rob Crittenden <rcritten@redhat.com> - 0.4.1-2 +- Broke invididual Requires and BuildRequires onto separate lines and + reordered them +- Added python-tgexpandingformwidget as a dependency +- Require at least fedora-ds-base 1.1 + +* Thu Nov 1 2007 Karl MacMillan <kmacmill@redhat.com> - 0.4.1-1 - Version bump for release * Wed Oct 31 2007 Karl MacMillan <kmacmill@redhat.com> - 0.4.0-6 @@ -98,5 +135,3 @@ rm -rf %{buildroot} * Fri Jul 27 2007 Karl MacMillan <kmacmill@redhat.com> - 0.1.0-1 - Initial rpm version - - diff --git a/ipa-server/ipa-slapi-plugins/ipa-memberof/ipa-memberof.c b/ipa-server/ipa-slapi-plugins/ipa-memberof/ipa-memberof.c index b23a04ae6..706b81325 100644 --- a/ipa-server/ipa-slapi-plugins/ipa-memberof/ipa-memberof.c +++ b/ipa-server/ipa-slapi-plugins/ipa-memberof/ipa-memberof.c @@ -70,7 +70,7 @@ #include "string.h" #include "nspr.h" -#define IPA_GROUP_ATTR "uniquemember" +#define IPA_GROUP_ATTR "member" #define IPA_MEMBEROF_ATTR "memberof" #define IPA_GROUP_ATTR_IS_DN 1 #define IPA_GROUP_ATTR_TYPE "uid" diff --git a/ipa-server/ipa-slapi-plugins/ipa-pwd-extop/ipa_pwd_extop.c b/ipa-server/ipa-slapi-plugins/ipa-pwd-extop/ipa_pwd_extop.c index e4e9d4615..558fdaea0 100644 --- a/ipa-server/ipa-slapi-plugins/ipa-pwd-extop/ipa_pwd_extop.c +++ b/ipa-server/ipa-slapi-plugins/ipa-pwd-extop/ipa_pwd_extop.c @@ -58,6 +58,7 @@ #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> +#include <unistd.h> #include <prio.h> #include <ssl.h> @@ -127,10 +128,16 @@ struct krb5p_keysalt { static void *ipapwd_plugin_id; -krb5_keyblock kmkey; -struct krb5p_keysalt *keysalts; -int n_keysalts; +const char *ipa_realm_dn = NULL; +const char *ipa_realm = NULL; +struct krb5p_keysalt *ipa_keysalts = NULL; +int ipa_n_keysalts = 0; + +Slapi_Mutex *ipa_globals = NULL; +krb5_keyblock *ipa_kmkey = NULL; + +static int ipapwd_getMasterKey(const char *realm_dn); /* Novell key-format scheme: @@ -164,7 +171,7 @@ int n_keysalts; struct kbvals { ber_int_t kvno; - struct berval *bval; + const struct berval *bval; }; static int cmpkbvals(const void *p1, const void *p2) @@ -183,19 +190,33 @@ static inline void encode_int16(unsigned int val, unsigned char *p) p[0] = (val ) & 0xff; } -static Slapi_Value **encrypt_encode_key(krb5_context krbctx, Slapi_Entry *e, const char *newPasswd) +#define IPA_CHANGETYPE_NORMAL 0 +#define IPA_CHANGETYPE_ADMIN 1 +#define IPA_CHANGETYPE_DSMGR 2 + +struct ipapwd_data { + Slapi_Entry *target; + const char *dn; + const char *password; + time_t timeNow; + time_t lastPwChange; + time_t expireTime; + int changetype; + int pwHistoryLen; +}; + +static Slapi_Value **encrypt_encode_key(krb5_context krbctx, struct ipapwd_data *data) { + krb5_keyblock *kmkey; const char *krbPrincipalName; - const char *krbLastPwdChange; uint32_t krbMaxTicketLife; Slapi_Attr *krbPrincipalKey = NULL; struct kbvals *kbvals = NULL; - time_t lastpwchange; time_t time_now; int kvno; int num_versions, num_keys; int krbTicketFlags; - BerElement *be; + BerElement *be = NULL; struct berval *bval = NULL; Slapi_Value **svals = NULL; int svals_no; @@ -204,55 +225,37 @@ static Slapi_Value **encrypt_encode_key(krb5_context krbctx, Slapi_Entry *e, con krb5_data pwd; int ret, i; + slapi_lock_mutex(ipa_globals); + kmkey = ipa_kmkey; + slapi_unlock_mutex(ipa_globals); + time_now = time(NULL); - krbPrincipalName = slapi_entry_attr_get_charptr(e, "krbPrincipalName"); + krbPrincipalName = slapi_entry_attr_get_charptr(data->target, "krbPrincipalName"); if (!krbPrincipalName) { slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "no krbPrincipalName present in this entry\n"); return NULL; } - krbMaxTicketLife = slapi_entry_attr_get_uint(e, "krbMaxTicketLife"); + krbMaxTicketLife = slapi_entry_attr_get_uint(data->target, "krbMaxTicketLife"); if (krbMaxTicketLife == 0) { /* FIXME: retrieve the default from config (max_life from kdc.conf) */ krbMaxTicketLife = 86400; /* just set the default 24h for now */ } - krbLastPwdChange = slapi_entry_attr_get_charptr(e, "krbLastPwdChange"); - if (!krbLastPwdChange) { - lastpwchange = -1; - } else { - struct tm tm; - - memset(&tm, 0, sizeof(struct tm)); - ret = sscanf(krbLastPwdChange, - "%04u%02u%02u%02u%02u%02u", - &tm.tm_year, &tm.tm_mon, &tm.tm_mday, - &tm.tm_hour, &tm.tm_min, &tm.tm_sec); - - if (ret == 6) { - tm.tm_year -= 1900; - tm.tm_mon -= 1; - lastpwchange = timegm(&tm); - } else { - /* FIXME: report an error ? */ - lastpwchange = -1; - } - } - /* FIXME: warn if lastpwchange == -1 ? */ - kvno = 0; num_keys = 0; num_versions = 1; /* retrieve current kvno and and keys */ - ret = slapi_entry_attr_find(e, "krbPrincipalKey", &krbPrincipalKey); + ret = slapi_entry_attr_find(data->target, "krbPrincipalKey", &krbPrincipalKey); if (ret == 0) { int i, n, count, idx; ber_int_t tkvno; Slapi_ValueSet *svs; Slapi_Value *sv; ber_tag_t tag, tmp; + const struct berval *cbval; slapi_attr_get_valueset(krbPrincipalKey, &svs); count = slapi_valueset_count(svs); @@ -260,7 +263,7 @@ static Slapi_Value **encrypt_encode_key(krb5_context krbctx, Slapi_Entry *e, con kbvals = (struct kbvals *)calloc(count, sizeof(struct kbvals)); } n = 0; - for (i = 0; count > 0 && i < count; i++) { + for (i = 0, idx = 0; count > 0 && i < count; i++) { if (i == 0) { idx = slapi_valueset_first_value(svs, &sv); } else { @@ -271,13 +274,13 @@ static Slapi_Value **encrypt_encode_key(krb5_context krbctx, Slapi_Entry *e, con "Array of stored keys shorter than expected\n"); break; } - bval = slapi_value_get_berval(sv); - if (!bval) { + cbval = slapi_value_get_berval(sv); + if (!cbval) { slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", "Error retrieving berval from Slapi_Value\n"); continue; } - be = ber_init(bval); + be = ber_init(cbval); if (!be) { slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", "ber_init() failed!\n"); @@ -293,7 +296,7 @@ static Slapi_Value **encrypt_encode_key(krb5_context krbctx, Slapi_Entry *e, con } kbvals[n].kvno = tkvno; - kbvals[n].bval = bval; + kbvals[n].bval = cbval; n++; if (tkvno > kvno) { @@ -305,8 +308,8 @@ static Slapi_Value **encrypt_encode_key(krb5_context krbctx, Slapi_Entry *e, con num_keys = n; /* now verify how many keys we need to keep around */ - if (num_keys > 0 && lastpwchange != -1) { - if (time_now > lastpwchange + krbMaxTicketLife) { + if (num_keys) { + if (time_now > data->lastPwChange + krbMaxTicketLife) { /* the last password change was long ago, * at most only the last entry need to be kept */ num_versions = 2; @@ -345,6 +348,8 @@ static Slapi_Value **encrypt_encode_key(krb5_context krbctx, Slapi_Entry *e, con } } + if (kbvals) free(kbvals); + krberr = krb5_parse_name(krbctx, krbPrincipalName, &princ); if (krberr) { slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", @@ -353,10 +358,10 @@ static Slapi_Value **encrypt_encode_key(krb5_context krbctx, Slapi_Entry *e, con goto enc_error; } - krbTicketFlags = slapi_entry_attr_get_int(e, "krbTicketFlags"); + krbTicketFlags = slapi_entry_attr_get_int(data->target, "krbTicketFlags"); - pwd.data = (char *)newPasswd; - pwd.length = strlen(newPasswd); + pwd.data = (char *)data->password; + pwd.length = strlen(data->password); be = ber_alloc_t( LBER_USE_DER ); @@ -381,7 +386,7 @@ static Slapi_Value **encrypt_encode_key(krb5_context krbctx, Slapi_Entry *e, con goto enc_error; } - for (i = 0; i < n_keysalts; i++) { + for (i = 0; i < ipa_n_keysalts; i++) { krb5_keyblock key; krb5_data salt; krb5_octet *ptr; @@ -392,7 +397,7 @@ static Slapi_Value **encrypt_encode_key(krb5_context krbctx, Slapi_Entry *e, con salt.data = NULL; - switch (keysalts[i].salt_type) { + switch (ipa_keysalts[i].salt_type) { case KRB5_KDB_SALTTYPE_ONLYREALM: @@ -473,12 +478,12 @@ static Slapi_Value **encrypt_encode_key(krb5_context krbctx, Slapi_Entry *e, con default: slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", - "Invalid salt type [%d]\n", keysalts[i].salt_type); + "Invalid salt type [%d]\n", ipa_keysalts[i].salt_type); goto enc_error; } /* need to build the key now to manage the AFS salt.length special case */ - krberr = krb5_c_string_to_key(krbctx, keysalts[i].enc_type, &pwd, &salt, &key); + krberr = krb5_c_string_to_key(krbctx, ipa_keysalts[i].enc_type, &pwd, &salt, &key); if (krberr) { slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "krb5_c_string_to_key failed [%s]\n", @@ -490,7 +495,7 @@ static Slapi_Value **encrypt_encode_key(krb5_context krbctx, Slapi_Entry *e, con salt.length = strlen(salt.data); } - krberr = krb5_c_encrypt_length(krbctx, kmkey.enctype, key.length, &len); + krberr = krb5_c_encrypt_length(krbctx, kmkey->enctype, key.length, &len); if (krberr) { slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "krb5_c_string_to_key failed [%s]\n", @@ -516,7 +521,7 @@ static Slapi_Value **encrypt_encode_key(krb5_context krbctx, Slapi_Entry *e, con cipher.ciphertext.length = len; cipher.ciphertext.data = (char *)ptr+2; - krberr = krb5_c_encrypt(krbctx, &kmkey, 0, 0, &plain, &cipher); + krberr = krb5_c_encrypt(krbctx, kmkey, 0, 0, &plain, &cipher); if (krberr) { slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "krb5_c_encrypt failed [%s]\n", @@ -531,12 +536,12 @@ static Slapi_Value **encrypt_encode_key(krb5_context krbctx, Slapi_Entry *e, con if (salt.length) { ret = ber_printf(be, "{t[{t[i]t[o]}]", (ber_tag_t)(LBER_CONSTRUCTED | LBER_CLASS_CONTEXT | 0), - (ber_tag_t)(LBER_CONSTRUCTED | LBER_CLASS_CONTEXT | 0), keysalts[i].salt_type, + (ber_tag_t)(LBER_CONSTRUCTED | LBER_CLASS_CONTEXT | 0), ipa_keysalts[i].salt_type, (ber_tag_t)(LBER_CONSTRUCTED | LBER_CLASS_CONTEXT | 1), salt.data, salt.length); } else { ret = ber_printf(be, "{t[{t[i]}]", (ber_tag_t)(LBER_CONSTRUCTED | LBER_CLASS_CONTEXT | 0), - (ber_tag_t)(LBER_CONSTRUCTED | LBER_CLASS_CONTEXT | 0), keysalts[i].salt_type); + (ber_tag_t)(LBER_CONSTRUCTED | LBER_CLASS_CONTEXT | 0), ipa_keysalts[i].salt_type); } if (ret == -1) { slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", @@ -793,25 +798,563 @@ done: return ret; } +/* searches the directory and finds the policy closest to the DN */ +/* return 0 on success, -1 on error or if no policy is found */ +static int ipapwd_getPolicy(const char *dn, Slapi_Entry *target, Slapi_Entry **e) +{ + const char *krbPwdPolicyReference; + const char *pdn; + const Slapi_DN *psdn; + Slapi_Backend *be; + Slapi_PBlock *pb; + char *attrs[] = { "krbMaxPwdLife", "krbMinPwdLife", + "krbPwdMinDiffChars", "krbPwdMinLength", + "krbPwdHistoryLength", NULL}; + Slapi_Entry **es = NULL; + Slapi_Entry *pe = NULL; + char **edn; + int ret, res, dist, rdnc, scope, i; + Slapi_DN *sdn; + + sdn = slapi_sdn_new_dn_byref(dn); + + slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", + "ipapwd_getPolicy: Searching policy for [%s]\n", dn); + + krbPwdPolicyReference = slapi_entry_attr_get_charptr(target, "krbPwdPolicyReference"); + if (krbPwdPolicyReference) { + pdn = krbPwdPolicyReference; + scope = LDAP_SCOPE_BASE; + } else { + /* Find ancestor base DN */ + be = slapi_be_select(sdn); + psdn = slapi_be_getsuffix(be, 0); + pdn = slapi_sdn_get_dn(psdn); + scope = LDAP_SCOPE_SUBTREE; + } + + *e = NULL; + + pb = slapi_pblock_new(); + slapi_search_internal_set_pb (pb, + pdn, scope, + "(objectClass=krbPwdPolicy)", + attrs, 0, + NULL, /* Controls */ + NULL, /* UniqueID */ + ipapwd_plugin_id, + 0); /* Flags */ + + /* do search the tree */ + ret = slapi_search_internal_pb(pb); + slapi_pblock_get(pb, SLAPI_PLUGIN_INTOP_RESULT, &res); + if (ret == -1 || res != LDAP_SUCCESS) { + slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", + "ipapwd_getPolicy: Couldn't find policy, err (%d)\n", + res?res:ret); + ret = -1; + goto done; + } + + /* get entries */ + slapi_pblock_get(pb, SLAPI_PLUGIN_INTOP_SEARCH_ENTRIES, &es); + if (!es) { + slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", + "ipapwd_getPolicy: No entries ?!"); + ret = -1; + goto done; + } + + /* count entries */ + for (i = 0; es[i]; i++) /* count */ ; + + /* if there is only one, return that */ + if (i == 1) { + *e = slapi_entry_dup(es[0]); + + ret = 0; + goto done; + } + + /* count number of RDNs in DN */ + edn = ldap_explode_dn(dn, 0); + if (!edn) { + slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", + "ipapwd_getPolicy: ldap_explode_dn(dn) failed ?!"); + ret = -1; + goto done; + } + for (rdnc = 0; edn[rdnc]; rdnc++) /* count */ ; + ldap_value_free(edn); + + pe = NULL; + dist = -1; + + /* find closest entry */ + for (i = 0; es[i]; i++) { + const Slapi_DN *esdn; + + esdn = slapi_entry_get_sdn_const(es[i]); + if (0 == slapi_sdn_compare(esdn, sdn)) { + pe = es[i]; + dist = 0; + break; + } + if (slapi_sdn_issuffix(sdn, esdn)) { + const char *dn1; + char **e1; + int c1; + + dn1 = slapi_sdn_get_dn(esdn); + if (!dn1) continue; + e1 = ldap_explode_dn(dn1, 0); + if (!e1) continue; + for (c1 = 0; e1[c1]; c1++) /* count */ ; + ldap_value_free(e1); + if ((dist == -1) || + ((rdnc - c1) < dist)) { + dist = rdnc - c1; + pe = es[i]; + } + } + if (dist == 0) break; /* found closest */ + } + + if (pe == NULL) { + ret = -1; + goto done; + } + + *e = slapi_entry_dup(pe); + ret = 0; +done: + slapi_free_search_results_internal(pb); + slapi_pblock_destroy(pb); + slapi_sdn_free(&sdn); + return ret; +} + +#define GENERALIZED_TIME_LENGTH 15 + +static int ipapwd_sv_pw_cmp(const void *pv1, const void *pv2) +{ + const char *pw1 = slapi_value_get_string(*((Slapi_Value **)pv1)); + const char *pw2 = slapi_value_get_string(*((Slapi_Value **)pv2)); + + return strncmp(pw1, pw2, GENERALIZED_TIME_LENGTH); +} + +static Slapi_Value **ipapwd_setPasswordHistory(Slapi_Mods *smods, struct ipapwd_data *data) +{ + Slapi_Value **pH = NULL; + Slapi_Attr *passwordHistory = NULL; + char timestr[GENERALIZED_TIME_LENGTH+1]; + char *histr, *old_pw; + struct tm utctime; + int ret, pc; + + old_pw = slapi_entry_attr_get_charptr(data->target, "userPassword"); + if (!old_pw) { + /* no old password to store, just return */ + return NULL; + } + + if (!gmtime_r(&(data->timeNow), &utctime)) { + slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "failed to retrieve current date (buggy gmtime_r ?)\n"); + return NULL; + } + strftime(timestr, GENERALIZED_TIME_LENGTH+1, "%Y%m%d%H%M%SZ", &utctime); + + histr = slapi_ch_smprintf("%s%s", timestr, old_pw); + if (!histr) { + slapi_log_error(SLAPI_LOG_PLUGIN, "ipa_pwd_extop", + "ipapwd_checkPassword: Out of Memory\n"); + return NULL; + } + + /* retrieve current history */ + ret = slapi_entry_attr_find(data->target, "passwordHistory", &passwordHistory); + if (ret == 0) { + int ret, hint, count, i; + Slapi_Value *pw; + + hint = 0; + count = 0; + ret = slapi_attr_get_numvalues(passwordHistory, &count); + /* if we have one */ + if (count > 0) { + pH = calloc(count + 2, sizeof(Slapi_Value *)); + if (!pH) { + slapi_log_error(SLAPI_LOG_PLUGIN, "ipa_pwd_extop", + "ipapwd_checkPassword: Out of Memory\n"); + free(histr); + return NULL; + } + + i = 0; + hint = slapi_attr_first_value(passwordHistory, &pw); + while (hint != -1) { + pH[i] = slapi_value_dup(pw); + i++; + hint = slapi_attr_next_value(passwordHistory, hint, &pw); + } + + qsort(pH, i, sizeof(Slapi_Value *), ipapwd_sv_pw_cmp); + + if (count > data->pwHistoryLen) { + count = data->pwHistoryLen; + } + + if (count == data->pwHistoryLen) { + /* replace oldest */ + slapi_value_free(&pH[0]); + i = 0; + } + + pc = i; + } + + } + + if (pH == NULL) { + pH = calloc(2, sizeof(Slapi_Value *)); + if (!pH) { + slapi_log_error(SLAPI_LOG_PLUGIN, "ipa_pwd_extop", + "ipapwd_checkPassword: Out of Memory\n"); + free(histr); + return NULL; + } + pc = 0; + } + + /* add new history value */ + pH[pc] = slapi_value_new_string(histr); + + free(histr); + + return pH; +} + +static Slapi_Value *ipapwd_strip_pw_date(Slapi_Value *pw) +{ + char *pwstr; + + pwstr = slapi_value_get_string(pw); + if (strlen(pwstr) <= GENERALIZED_TIME_LENGTH) { + /* not good, must be garbage, we never set histories without time */ + return NULL; + } + + return slapi_value_new_string(&pwstr[GENERALIZED_TIME_LENGTH]); +} + +#define IPAPWD_POLICY_MASK 0x0FF +#define IPAPWD_POLICY_ERROR 0x100 +#define IPAPWD_POLICY_OK 0 + +/* 90 days default pwd max lifetime */ +#define IPAPWD_DEFAULT_PWDLIFE (90 * 24 *3600) +#define IPAPWD_DEFAULT_MINLEN 0 + +/* check password strenght and history */ +static int ipapwd_CheckPolicy(struct ipapwd_data *data) +{ + const char *krbPrincipalExpiration; + const char *krbLastPwdChange; + int krbMaxPwdLife = IPAPWD_DEFAULT_PWDLIFE; + int krbPwdMinLength = IPAPWD_DEFAULT_MINLEN; + int krbPwdMinDiffChars = 0; + int krbMinPwdLife = 0; + int pwdCharLen = 0; + Slapi_Entry *policy = NULL; + Slapi_Attr *passwordHistory = NULL; + struct tm tm; + int tmp, ret; + + /* check account is not expired */ + krbPrincipalExpiration = slapi_entry_attr_get_charptr(data->target, "krbPrincipalExpiration"); + if (krbPrincipalExpiration) { + /* if expiration date set check it */ + memset(&tm, 0, sizeof(struct tm)); + ret = sscanf(krbPrincipalExpiration, + "%04u%02u%02u%02u%02u%02u", + &tm.tm_year, &tm.tm_mon, &tm.tm_mday, + &tm.tm_hour, &tm.tm_min, &tm.tm_sec); + + if (ret == 6) { + tm.tm_year -= 1900; + tm.tm_mon -= 1; + + if (data->timeNow > timegm(&tm)) { + slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", "Account Expired"); + return IPAPWD_POLICY_ERROR | LDAP_PWPOLICY_PWDMODNOTALLOWED; + } + } + /* FIXME: else error out ? */ + } + + if (data->changetype != IPA_CHANGETYPE_NORMAL) { + /* We must skip policy checks (Admin change) but + * force a password change on the next login. + * But not if Directory Manager */ + if (data->changetype == IPA_CHANGETYPE_ADMIN) { + data->expireTime = data->timeNow; + } + + /* skip policy checks */ + goto no_policy; + } + + krbLastPwdChange = slapi_entry_attr_get_charptr(data->target, "krbLastPwdChange"); + /* if no previous change, it means this is probably a new account + * or imported, log and just ignore */ + if (krbLastPwdChange) { + + memset(&tm, 0, sizeof(struct tm)); + ret = sscanf(krbLastPwdChange, + "%04u%02u%02u%02u%02u%02u", + &tm.tm_year, &tm.tm_mon, &tm.tm_mday, + &tm.tm_hour, &tm.tm_min, &tm.tm_sec); + + if (ret == 6) { + tm.tm_year -= 1900; + tm.tm_mon -= 1; + data->lastPwChange = timegm(&tm); + } + /* FIXME: *else* report an error ? */ + } else { + slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", "Warning: Last Password Change Time is not available"); + } + + /* find the entry with the password policy */ + ret = ipapwd_getPolicy(data->dn, data->target, &policy); + if (ret) { + slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", "No password policy"); + goto no_policy; + } + + /* Check min age */ + krbMinPwdLife = slapi_entry_attr_get_int(policy, "krbMinPwdLife"); + /* if no default then treat it as no limit */ + if (krbMinPwdLife != 0) { + + if (data->timeNow < data->lastPwChange + krbMinPwdLife) { + slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", + "ipapwd_checkPassword: Too soon to change password\n"); + slapi_entry_free(policy); + return IPAPWD_POLICY_ERROR | LDAP_PWPOLICY_PWDTOOYOUNG; + } + } + + /* Retrieve min length */ + tmp = slapi_entry_attr_get_int(policy, "krbPwdMinLength"); + if (tmp != 0) { + krbPwdMinLength = tmp; + } + + /* check complexity */ + /* FIXME: this code is partially based on Directory Server code, + * the plan is to merge this code later making it available + * trough a pulic DS API for slapi plugins */ + krbPwdMinDiffChars = slapi_entry_attr_get_int(policy, "krbPwdMinDiffChars"); + if (krbPwdMinDiffChars != 0) { + int num_digits = 0; + int num_alphas = 0; + int num_uppers = 0; + int num_lowers = 0; + int num_specials = 0; + int num_8bit = 0; + int num_repeated = 0; + int max_repeated = 0; + int num_categories = 0; + char *p, *pwd; + + pwd = strdup(data->password); + + /* check character types */ + p = pwd; + while ( p && *p ) + { + if ( ldap_utf8isdigit( p ) ) { + num_digits++; + } else if ( ldap_utf8isalpha( p ) ) { + num_alphas++; + if ( slapi_utf8isLower( (unsigned char *)p ) ) { + num_lowers++; + } else { + num_uppers++; + } + } else { + /* check if this is an 8-bit char */ + if ( *p & 128 ) { + num_8bit++; + } else { + num_specials++; + } + } + + /* check for repeating characters. If this is the + first char of the password, no need to check */ + if ( pwd != p ) { + int len = ldap_utf8len( p ); + char *prev_p = ldap_utf8prev( p ); + + if ( len == ldap_utf8len( prev_p ) ) + { + if ( memcmp( p, prev_p, len ) == 0 ) + { + num_repeated++; + if ( max_repeated < num_repeated ) { + max_repeated = num_repeated; + } + } else { + num_repeated = 0; + } + } else { + num_repeated = 0; + } + } + + p = ldap_utf8next( p ); + } + + free(pwd); + p = pwd = NULL; + + /* tally up the number of character categories */ + if ( num_digits > 0 ) + ++num_categories; + if ( num_uppers > 0 ) + ++num_categories; + if ( num_lowers > 0 ) + ++num_categories; + if ( num_specials > 0 ) + ++num_categories; + if ( num_8bit > 0 ) + ++num_categories; + + /* FIXME: the kerberos plicy schema does not define separated threshold values, + * so just treat anything as a category, we will fix this when we merge + * with DS policies */ + + if (max_repeated > 1) + --num_categories; + + if (num_categories < krbPwdMinDiffChars) { + slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", + "ipapwd_checkPassword: Password not complex enough\n"); + slapi_entry_free(policy); + return IPAPWD_POLICY_ERROR | LDAP_PWPOLICY_INVALIDPWDSYNTAX; + } + } + + /* Check password history */ + ret = slapi_entry_attr_find(data->target, "passwordHistory", &passwordHistory); + if (ret == 0) { + int ret, hint, count, i; + Slapi_Value **pH; + Slapi_Value *pw; + + hint = 0; + count = 0; + i = 0; + ret = slapi_attr_get_numvalues(passwordHistory, &count); + /* check history only if we have one */ + if (count > 0) { + pH = calloc(count + 1, sizeof(Slapi_Value *)); + if (!pH) { + slapi_log_error(SLAPI_LOG_PLUGIN, "ipa_pwd_extop", + "ipapwd_checkPassword: Out of Memory\n"); + slapi_entry_free(policy); + return LDAP_OPERATIONS_ERROR; + } + + hint = slapi_attr_first_value(passwordHistory, &pw); + while (hint != -1) { + pH[i] = ipapwd_strip_pw_date(pw); + if (pH[i]) i++; + hint = slapi_attr_next_value(passwordHistory, hint, &pw); + } + + pw = slapi_value_new_string(data->password); + if (!pw) { + slapi_log_error(SLAPI_LOG_PLUGIN, "ipa_pwd_extop", + "ipapwd_checkPassword: Out of Memory\n"); + slapi_entry_free(policy); + free(pH); + return LDAP_OPERATIONS_ERROR; + } + + ret = slapi_pw_find_sv(pH, pw); + + slapi_value_free(&pw); + + for (i = 0; pH[i]; i++) { + slapi_value_free(&pH[i]); + } + free(pH); + + if (ret == 0) { + slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", + "ipapwd_checkPassword: Password in history\n"); + slapi_entry_free(policy); + return IPAPWD_POLICY_ERROR | LDAP_PWPOLICY_PWDINHISTORY; + } + } + } + + /* Calculate max age */ + tmp = slapi_entry_attr_get_int(policy, "krbMaxPwdLife"); + if (tmp != 0) { + krbMaxPwdLife = tmp; + } + + /* Retrieve History Len */ + data->pwHistoryLen = slapi_entry_attr_get_int(policy, "krbPwdHistoryLength"); + + slapi_entry_free(policy); + +no_policy: + + /* check min lenght */ + pwdCharLen = ldap_utf8characters(data->password); + + if (pwdCharLen < krbPwdMinLength) { + slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", + "ipapwd_checkPassword: Password too short\n"); + return IPAPWD_POLICY_ERROR | LDAP_PWPOLICY_PWDTOOSHORT; + } + + if (data->expireTime == 0) { + data->expireTime = data->timeNow + krbMaxPwdLife; + } + + return IPAPWD_POLICY_OK; +} + + /* Searches the dn in directory, * If found : fills in slapi_entry structure and returns 0 * If NOT found : returns the search result as LDAP_NO_SUCH_OBJECT */ -static int -ipapwd_getEntry( const char *dn, Slapi_Entry **e2 ) { - int search_result = 0; - Slapi_DN *sdn; +static int ipapwd_getEntry(const char *dn, Slapi_Entry **e2, char **attrlist) +{ + Slapi_DN *sdn; + int search_result = 0; + slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", "=> ipapwd_getEntry\n"); sdn = slapi_sdn_new_dn_byref(dn); - if ((search_result = slapi_search_internal_get_entry( sdn, NULL, e2, + if ((search_result = slapi_search_internal_get_entry( sdn, attrlist, e2, ipapwd_plugin_id)) != LDAP_SUCCESS ){ - slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", "ipapwd_getEntry: No such entry-(%s), err (%d)\n", - dn, search_result); + slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", + "ipapwd_getEntry: No such entry-(%s), err (%d)\n", + dn, search_result); } slapi_sdn_free( &sdn ); - slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", "<= ipapwd_getEntry: %d\n", search_result); + slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", + "<= ipapwd_getEntry: %d\n", search_result); return search_result; } @@ -822,35 +1365,44 @@ ipapwd_getEntry( const char *dn, Slapi_Entry **e2 ) { static int ipapwd_apply_mods(const char *dn, Slapi_Mods *mods) { Slapi_PBlock *pb; - int ret=0; + int ret; slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", "=> ipapwd_apply_mods\n"); - if (mods && (slapi_mods_get_num_mods(mods) > 0)) - { - pb = slapi_pblock_new(); - slapi_modify_internal_set_pb (pb, dn, - slapi_mods_get_ldapmods_byref(mods), - NULL, /* Controls */ - NULL, /* UniqueID */ - ipapwd_plugin_id, /* PluginID */ - 0); /* Flags */ - - ret = slapi_modify_internal_pb (pb); + if (!mods || (slapi_mods_get_num_mods(mods) == 0)) { + return -1; + } - slapi_pblock_get(pb, SLAPI_PLUGIN_INTOP_RESULT, &ret); + pb = slapi_pblock_new(); + slapi_modify_internal_set_pb (pb, dn, + slapi_mods_get_ldapmods_byref(mods), + NULL, /* Controls */ + NULL, /* UniqueID */ + ipapwd_plugin_id, /* PluginID */ + 0); /* Flags */ - if (ret != LDAP_SUCCESS){ - slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", "WARNING: modify error %d on entry '%s'\n", + ret = slapi_modify_internal_pb (pb); + if (ret) { + slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", + "WARNING: modify error %d on entry '%s'\n", ret, dn); - } + } else { + + slapi_pblock_get(pb, SLAPI_PLUGIN_INTOP_RESULT, &ret); + + if (ret != LDAP_SUCCESS){ + slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", + "WARNING: modify error %d on entry '%s'\n", + ret, dn); + } else { + slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", + "<= ipapwd_apply_mods: Successful\n"); + } + } slapi_pblock_destroy(pb); - } - - slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", "<= ipapwd_apply_mods: %d\n", ret); - - return ret; + + return ret; } /* ascii hex output of bytes in "in" @@ -867,22 +1419,22 @@ static void hexbuf(char *out, const uint8_t *in) } } -/* Modify the userPassword attribute field of the entry */ -static int ipapwd_userpassword(Slapi_Entry *targetEntry, const char *newPasswd) +/* Modify the Password attributes of the entry */ +static int ipapwd_SetPassword(struct ipapwd_data *data) { - char *dn = NULL; int ret = 0, i = 0; Slapi_Mods *smods; Slapi_Value **svals; - time_t curtime; + Slapi_Value **pwvals; struct tm utctime; - char timestr[16]; + char timestr[GENERALIZED_TIME_LENGTH+1]; krb5_context krbctx; krb5_error_code krberr; char lm[33], nt[33]; struct ntlm_keys ntlm; int ntlm_flags = 0; Slapi_Value *sambaSamAccount; + char *userpwd; krberr = krb5_init_context(&krbctx); if (krberr) { @@ -890,13 +1442,12 @@ static int ipapwd_userpassword(Slapi_Entry *targetEntry, const char *newPasswd) return LDAP_OPERATIONS_ERROR; } - slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", "=> ipapwd_userpassword\n"); + slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", "=> ipapwd_SetPassword\n"); smods = slapi_mods_new(); - dn = slapi_entry_get_ndn( targetEntry ); /* generate kerberos keys to be put into krbPrincipalKey */ - svals = encrypt_encode_key(krbctx, targetEntry, newPasswd); + svals = encrypt_encode_key(krbctx, data); if (!svals) { slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "key encryption/encoding failed\n"); krb5_free_context(krbctx); @@ -908,32 +1459,35 @@ static int ipapwd_userpassword(Slapi_Entry *targetEntry, const char *newPasswd) slapi_mods_add_mod_values(smods, LDAP_MOD_REPLACE, "krbPrincipalKey", svals); /* change Last Password Change field with the current date */ - curtime = time(NULL); - if (!gmtime_r(&curtime, &utctime)) { + if (!gmtime_r(&(data->timeNow), &utctime)) { slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "failed to retrieve current date (buggy gmtime_r ?)\n"); + free(svals); return LDAP_OPERATIONS_ERROR; } - if (utctime.tm_year > 8099 || utctime.tm_mon > 11 || utctime.tm_mday > 31 || - utctime.tm_hour > 23 || utctime.tm_min > 59 || utctime.tm_sec > 59) { - slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "retrieved a bad date (buggy gmtime_r ?)\n"); + strftime(timestr, GENERALIZED_TIME_LENGTH+1, "%Y%m%d%H%M%SZ", &utctime); + slapi_mods_add_string(smods, LDAP_MOD_REPLACE, "krbLastPwdChange", timestr); + + /* set Password Expiration date */ + if (!gmtime_r(&(data->expireTime), &utctime)) { + slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "failed to convert expiration date\n"); + free(svals); return LDAP_OPERATIONS_ERROR; } - - snprintf(timestr, 16, "%04d%02d%02d%02d%02d%02dZ", utctime.tm_year+1900, utctime.tm_mon+1, - utctime.tm_mday, utctime.tm_hour, utctime.tm_min, utctime.tm_sec); - - slapi_mods_add_string(smods, LDAP_MOD_REPLACE, "krbLastPwdChange", timestr); - /* TODO: krbPasswordExpiration, (krbMaxTicketLife, krbMaxRenewableAge, krbTicketFlags ?) */ + strftime(timestr, GENERALIZED_TIME_LENGTH+1, "%Y%m%d%H%M%SZ", &utctime); + slapi_mods_add_string(smods, LDAP_MOD_REPLACE, "krbPasswordExpiration", timestr); sambaSamAccount = slapi_value_new_string("sambaSamAccount"); - if (slapi_entry_attr_has_syntax_value(targetEntry, "objectClass", sambaSamAccount)) { + if (slapi_entry_attr_has_syntax_value(data->target, "objectClass", sambaSamAccount)) { /* TODO: retrieve if we want to store the LM hash or not */ ntlm_flags = KTF_LM_HASH | KTF_NT_HASH; } slapi_value_free(&sambaSamAccount); if (ntlm_flags) { - if (encode_ntlm_keys((char *)newPasswd, ntlm_flags, &ntlm) != 0) { + char *password = strdup(data->password); + if (encode_ntlm_keys(password, ntlm_flags, &ntlm) != 0) { + free(svals); + free(password); return LDAP_OPERATIONS_ERROR; } if (ntlm_flags & KTF_LM_HASH) { @@ -946,24 +1500,47 @@ static int ipapwd_userpassword(Slapi_Entry *targetEntry, const char *newPasswd) nt[32] = '\0'; slapi_mods_add_string(smods, LDAP_MOD_REPLACE, "sambaNTPassword", nt); } + free(password); + } + + /* use the default configured encoding */ + userpwd = slapi_encode(data->password, NULL); + if (!userpwd) { + slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "failed to make userPassword hash\n"); + free(svals); + return LDAP_OPERATIONS_ERROR; + } + + slapi_mods_add_string(smods, LDAP_MOD_REPLACE, "userPassword", userpwd); + + /* set password history */ + pwvals = ipapwd_setPasswordHistory(smods, data); + if (pwvals) { + slapi_mods_add_mod_values(smods, LDAP_MOD_REPLACE, "passwordHistory", pwvals); } - /* TODO !!! + /* FIXME: * instead of replace we should use a delete/add so that we are * completely sure nobody else modified the entry meanwhile and * fail if that's the case */ /* commit changes */ - ret = ipapwd_apply_mods(dn, smods); + ret = ipapwd_apply_mods(data->dn, smods); slapi_mods_free(&smods); - slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", "<= ipapwd_userpassword: %d\n", ret); + slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", "<= ipapwd_SetPassword: %d\n", ret); for (i = 0; svals[i]; i++) { slapi_value_free(&svals[i]); } free(svals); + if (pwvals) { + for (i = 0; pwvals[i]; i++) { + slapi_value_free(&pwvals[i]); + } + free(pwvals); + } return ret; } @@ -1011,8 +1588,7 @@ static int ipapwd_generate_basic_passwd( int passlen, char **genpasswd ) #endif /* Password Modify Extended operation plugin function */ -int -ipapwd_extop( Slapi_PBlock *pb ) +static int ipapwd_extop(Slapi_PBlock *pb) { char *oid = NULL; char *bindDN = NULL; @@ -1027,10 +1603,22 @@ ipapwd_extop( Slapi_PBlock *pb ) struct berval *extop_value = NULL; BerElement *ber = NULL; Slapi_Entry *targetEntry=NULL; - /* Slapi_DN sdn; */ + char *attrlist[] = {"*", "passwordHistory", NULL }; + struct ipapwd_data pwdata; - slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", "=> ipa_pwd_extop\n"); + slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", "=> ipapwd_extop\n"); + /* make sure we have the master key */ + if (ipa_kmkey == NULL) { + ret = ipapwd_getMasterKey(ipa_realm_dn); + if (ret != LDAP_SUCCESS) { + errMesg = "Fatal Internal Error Retrieving Master Key"; + rc = LDAP_OPERATIONS_ERROR; + slapi_log_error( SLAPI_LOG_PLUGIN, "ipa_pwd_extop", errMesg ); + goto free_and_return; + } + } + /* Before going any further, we'll make sure that the right extended operation plugin * has been called: i.e., the OID shipped whithin the extended operation request must * match this very plugin's OID: EXOP_PASSWD_OID. */ @@ -1116,47 +1704,40 @@ ipapwd_extop( Slapi_PBlock *pb ) tag = ber_peek_tag( ber, &len); } - /* identify userID field by tags */ if (tag == LDAP_EXTOP_PASSMOD_TAG_USERID ) { - if ( ber_scanf( ber, "a", &dn) == LBER_ERROR ) - { - slapi_ch_free_string(&dn); - slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "ber_scanf failed :{\n"); - errMesg = "ber_scanf failed at userID parse.\n"; - rc = LDAP_PROTOCOL_ERROR; - goto free_and_return; + if (ber_scanf(ber, "a", &dn) == LBER_ERROR) { + slapi_ch_free_string(&dn); + slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "ber_scanf failed :{\n"); + errMesg = "ber_scanf failed at userID parse.\n"; + rc = LDAP_PROTOCOL_ERROR; + goto free_and_return; } - tag = ber_peek_tag( ber, &len); + tag = ber_peek_tag(ber, &len); } - /* identify oldPasswd field by tags */ if (tag == LDAP_EXTOP_PASSMOD_TAG_OLDPWD ) { - if ( ber_scanf( ber, "a", &oldPasswd ) == LBER_ERROR ) - { - slapi_ch_free_string(&oldPasswd); - slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "ber_scanf failed :{\n"); - errMesg = "ber_scanf failed at oldPasswd parse.\n"; - rc = LDAP_PROTOCOL_ERROR; - goto free_and_return; + if (ber_scanf(ber, "a", &oldPasswd) == LBER_ERROR) { + slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "ber_scanf failed :{\n"); + errMesg = "ber_scanf failed at oldPasswd parse.\n"; + rc = LDAP_PROTOCOL_ERROR; + goto free_and_return; } - tag = ber_peek_tag( ber, &len); + tag = ber_peek_tag(ber, &len); } /* identify newPasswd field by tags */ - if (tag == LDAP_EXTOP_PASSMOD_TAG_NEWPWD ) + if (tag == LDAP_EXTOP_PASSMOD_TAG_NEWPWD ) { - if ( ber_scanf( ber, "a", &newPasswd ) == LBER_ERROR ) - { - slapi_ch_free_string(&newPasswd); - slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "ber_scanf failed :{\n"); - errMesg = "ber_scanf failed at newPasswd parse.\n"; - rc = LDAP_PROTOCOL_ERROR; - goto free_and_return; + if (ber_scanf(ber, "a", &newPasswd) == LBER_ERROR) { + slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "ber_scanf failed :{\n"); + errMesg = "ber_scanf failed at newPasswd parse.\n"; + rc = LDAP_PROTOCOL_ERROR; + goto free_and_return; } } @@ -1167,7 +1748,7 @@ parse_req_done: /* Get Bind DN */ - slapi_pblock_get( pb, SLAPI_CONN_DN, &bindDN ); + slapi_pblock_get(pb, SLAPI_CONN_DN, &bindDN); /* If the connection is bound anonymously, we must refuse to process this operation. */ if (bindDN == NULL || *bindDN == '\0') { @@ -1209,7 +1790,7 @@ parse_req_done: slapi_pblock_set( pb, SLAPI_ORIGINAL_TARGET, dn ); /* Now we have the DN, look for the entry */ - ret = ipapwd_getEntry(dn, &targetEntry); + ret = ipapwd_getEntry(dn, &targetEntry, attrlist); /* If we can't find the entry, then that's an error */ if (ret) { /* Couldn't find the entry, fail */ @@ -1259,48 +1840,186 @@ parse_req_done: the bind operation (or used sasl or client cert auth or OS creds) */ slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", "oldPasswd provided, but we will ignore it"); } + + memset(&pwdata, 0, sizeof(pwdata)); + pwdata.target = targetEntry; + pwdata.dn = dn; + pwdata.password = newPasswd; + pwdata.timeNow = time(NULL); + pwdata.changetype = IPA_CHANGETYPE_NORMAL; + + /* determine type of password change */ + if (strcasecmp(dn, bindDN) != 0) { + char **bindexp; + + pwdata.changetype = IPA_CHANGETYPE_ADMIN; + bindexp = ldap_explode_dn(bindDN, 0); + if (bindexp) { + /* special case kpasswd and Directory Manager */ + if ((strncasecmp(bindexp[0], "krbprincipalname=kadmin/changepw@", 33) == 0) && + (strcasecmp(&(bindexp[0][33]), ipa_realm) == 0)) { + pwdata.changetype = IPA_CHANGETYPE_NORMAL; + } + if ((strcasecmp(bindexp[0], "cn=Directory Manager") == 0) && + bindexp[1] == NULL) { + pwdata.changetype = IPA_CHANGETYPE_DSMGR; + } + ldap_value_free(bindexp); + } + } - /* Now we're ready to make actual password change */ - ret = ipapwd_userpassword(targetEntry, newPasswd); + /* check the policy */ + ret = ipapwd_CheckPolicy(&pwdata); + if (ret) { + errMesg = "Password Fails to meet minimum strength criteria"; + if (ret & IPAPWD_POLICY_ERROR) { + slapi_pwpolicy_make_response_control(pb, -1, -1, ret & IPAPWD_POLICY_MASK); + rc = LDAP_CONSTRAINT_VIOLATION; + } else { + errMesg = "Internal error"; + rc = ret; + } + goto free_and_return; + } + + /* Now we're ready to set the kerberos key material */ + ret = ipapwd_SetPassword(&pwdata); if (ret != LDAP_SUCCESS) { /* Failed to modify the password, e.g. because insufficient access allowed */ - errMesg = "Failed to update password\n"; - rc = ret; + errMesg = "Failed to update password"; + if (ret > 0) { + rc = ret; + } else { + rc = LDAP_OPERATIONS_ERROR; + } goto free_and_return; } - slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", "<= ipa_pwd_extop: %d\n", rc); + slapi_log_error(SLAPI_LOG_TRACE, "ipa_pwd_extop", "<= ipapwd_extop: %d\n", rc); /* Free anything that we allocated above */ - free_and_return: +free_and_return: slapi_ch_free_string(&oldPasswd); slapi_ch_free_string(&newPasswd); /* Either this is the same pointer that we allocated and set above, * or whoever used it should have freed it and allocated a new * value that we need to free here */ - slapi_pblock_get( pb, SLAPI_ORIGINAL_TARGET, &dn ); + slapi_pblock_get(pb, SLAPI_ORIGINAL_TARGET, &dn); slapi_ch_free_string(&dn); - slapi_pblock_set( pb, SLAPI_ORIGINAL_TARGET, NULL ); + slapi_pblock_set(pb, SLAPI_ORIGINAL_TARGET, NULL); slapi_ch_free_string(&authmethod); - if ( targetEntry != NULL ){ - slapi_entry_free (targetEntry); - } + if (targetEntry) slapi_entry_free(targetEntry); + if (ber) ber_free(ber, 1); - if ( ber != NULL ){ - ber_free(ber, 1); - ber = NULL; + slapi_log_error(SLAPI_LOG_PLUGIN, "ipa_pwd_extop", errMesg ? errMesg : "success"); + slapi_send_ldap_result(pb, rc, NULL, errMesg, 0, NULL); + + return SLAPI_PLUGIN_EXTENDED_SENT_RESULT; + +} /* ipapwd_extop */ + +/* Novell key-format scheme: + KrbMKey ::= SEQUENCE { + kvno [0] UInt32, + key [1] MasterKey + } + + MasterKey ::= SEQUENCE { + keytype [0] Int32, + keyvalue [1] OCTET STRING + } +*/ + +static int ipapwd_getMasterKey(const char *realm_dn) +{ + krb5_keyblock *kmkey; + Slapi_Attr *a; + Slapi_Value *v; + Slapi_Entry *realm_entry; + BerElement *be; + ber_tag_t tag, tmp; + ber_int_t ttype; + const struct berval *bval; + struct berval *mkey; + + kmkey = malloc(sizeof(krb5_keyblock)); + if (!kmkey) { + slapi_log_error( SLAPI_LOG_FATAL, "ipapwd_start", "Out of memory!\n"); + return LDAP_OPERATIONS_ERROR; } - - slapi_log_error( SLAPI_LOG_PLUGIN, "ipa_pwd_extop", - errMesg ? errMesg : "success" ); - slapi_send_ldap_result( pb, rc, NULL, errMesg, 0, NULL ); - - return( SLAPI_PLUGIN_EXTENDED_SENT_RESULT ); + if (ipapwd_getEntry(realm_dn, &realm_entry, NULL) != LDAP_SUCCESS) { + slapi_log_error( SLAPI_LOG_FATAL, "ipapwd_start", "No realm Entry?\n"); + free(kmkey); + return LDAP_OPERATIONS_ERROR; + } + + if (slapi_entry_attr_find(realm_entry, "krbMKey", &a) == -1) { + slapi_log_error( SLAPI_LOG_FATAL, "ipapwd_start", "No master key??\n"); + free(kmkey); + slapi_entry_free(realm_entry); + return LDAP_OPERATIONS_ERROR; + } -}/* ipa_pwd_extop */ + /* there should be only one value here */ + if (slapi_attr_first_value(a, &v) == -1) { + slapi_log_error( SLAPI_LOG_FATAL, "ipapwd_start", "No master key values??\n"); + free(kmkey); + slapi_entry_free(realm_entry); + return LDAP_OPERATIONS_ERROR; + } + + bval = slapi_value_get_berval(v); + if (!bval) { + slapi_log_error( SLAPI_LOG_FATAL, "ipapwd_start", "Error retrieving master key berval\n"); + free(kmkey); + slapi_entry_free(realm_entry); + return LDAP_OPERATIONS_ERROR; + } + + be = ber_init(bval); + if (!bval) { + slapi_log_error( SLAPI_LOG_FATAL, "ipapwd_start", "ber_init() failed!\n"); + free(kmkey); + slapi_entry_free(realm_entry); + return LDAP_OPERATIONS_ERROR; + } + + tag = ber_scanf(be, "{i{iO}}", &tmp, &ttype, &mkey); + if (tag == LBER_ERROR) { + slapi_log_error(SLAPI_LOG_TRACE, "ipapwd_start", + "Bad Master key encoding ?!\n"); + free(kmkey); + ber_free(be, 1); + slapi_entry_free(realm_entry); + return LDAP_OPERATIONS_ERROR; + } + + kmkey->magic = KV5M_KEYBLOCK; + kmkey->enctype = ttype; + kmkey->length = mkey->bv_len; + kmkey->contents = malloc(mkey->bv_len); + if (!kmkey->contents) { + slapi_log_error( SLAPI_LOG_FATAL, "ipapwd_start", "Out of memory!\n"); + ber_bvfree(mkey); + ber_free(be, 1); + free(kmkey); + slapi_entry_free(realm_entry); + return LDAP_OPERATIONS_ERROR; + } + memcpy(kmkey->contents, mkey->bv_val, mkey->bv_len); + + slapi_lock_mutex(ipa_globals); + ipa_kmkey = kmkey; + slapi_unlock_mutex(ipa_globals); + + slapi_entry_free(realm_entry); + ber_bvfree(mkey); + ber_free(be, 1); + return LDAP_SUCCESS; +} static char *ipapwd_oid_list[] = { @@ -1310,7 +2029,7 @@ static char *ipapwd_oid_list[] = { static char *ipapwd_name_list[] = { - "ipa_pwd_extop", + "ipapwd_extop", NULL }; @@ -1336,29 +2055,35 @@ const char *krb_sup_encs[] = { /* Init data structs */ /* TODO: read input from tree */ -int ipapwd_start( Slapi_PBlock *pb ) +static int ipapwd_start( Slapi_PBlock *pb ) { int krberr, i; krb5_context krbctx; + char *realm; + char *realm_dn; char *config_dn; + char *partition_dn; Slapi_Entry *config_entry; - const char *stash_file; - int fd; - ssize_t r; - uint16_t e; - unsigned int l; - unsigned char *o; + int ret; + struct krb5p_keysalt *keysalts; + int n_keysalts; + ipa_globals = slapi_new_mutex(); + krberr = krb5_init_context(&krbctx); if (krberr) { - slapi_log_error(SLAPI_LOG_FATAL, "ipa_pwd_extop", "krb5_init_context failed\n"); + slapi_log_error(SLAPI_LOG_FATAL, "ipapwd_start", "krb5_init_context failed\n"); + return LDAP_OPERATIONS_ERROR; + } + if (krb5_get_default_realm(krbctx, &realm)) { + krb5_free_context(krbctx); return LDAP_OPERATIONS_ERROR; } - for (i = 0; krb_sup_encs[i]; i++) /* count */ ; keysalts = (struct krb5p_keysalt *)malloc(sizeof(struct krb5p_keysalt) * (i + 1)); if (!keysalts) { krb5_free_context(krbctx); + free(realm); return LDAP_OPERATIONS_ERROR; } @@ -1373,6 +2098,7 @@ int ipapwd_start( Slapi_PBlock *pb ) if (!enc) { slapi_log_error( SLAPI_LOG_PLUGIN, "ipapwd_start", "Allocation error\n"); krb5_free_context(krbctx); + free(realm); return LDAP_OPERATIONS_ERROR; } salt = strchr(enc, ':'); @@ -1409,76 +2135,56 @@ int ipapwd_start( Slapi_PBlock *pb ) free(enc); } + krb5_free_context(krbctx); + /*retrieve the master key from the stash file */ if (slapi_pblock_get(pb, SLAPI_TARGET_DN, &config_dn) != 0) { slapi_log_error( SLAPI_LOG_FATAL, "ipapwd_start", "No config DN?\n"); krb5_free_context(krbctx); + free(keysalts); + free(realm); return LDAP_OPERATIONS_ERROR; } - if (ipapwd_getEntry(config_dn, &config_entry) != LDAP_SUCCESS) { + if (ipapwd_getEntry(config_dn, &config_entry, NULL) != LDAP_SUCCESS) { slapi_log_error( SLAPI_LOG_FATAL, "ipapwd_start", "No config Entry?\n"); krb5_free_context(krbctx); + free(keysalts); + free(realm); return LDAP_OPERATIONS_ERROR; } - stash_file = slapi_entry_attr_get_charptr(config_entry, "nsslapd-pluginarg0"); - if (!stash_file) { - slapi_log_error( SLAPI_LOG_FATAL, "ipapwd_start", "Missing Master key stash file path configuration entry (nsslapd-pluginarg0)!\n"); + partition_dn = slapi_entry_attr_get_charptr(config_entry, "nsslapd-realmtree"); + if (!partition_dn) { + slapi_log_error( SLAPI_LOG_FATAL, "ipapwd_start", "Missing partition configuration entry (nsslapd-targetSubtree)!\n"); krb5_free_context(krbctx); + slapi_entry_free(config_entry); + free(keysalts); + free(realm); return LDAP_OPERATIONS_ERROR; } - fd = open(stash_file, O_RDONLY); - if (fd == -1) { - slapi_log_error( SLAPI_LOG_FATAL, "ipapwd_start", "Missing Master key stash file!\n"); - krb5_free_context(krbctx); + realm_dn = slapi_ch_smprintf("cn=%s,cn=kerberos,%s", realm, partition_dn); + if (!realm_dn) { + slapi_log_error( SLAPI_LOG_FATAL, "ipapwd_start", "Out of memory ?\n"); + free(keysalts); + free(realm); return LDAP_OPERATIONS_ERROR; } - r = read(fd, &e, 2); /* read enctype a local endian 16bit value */ - if (r != 2) { - slapi_log_error( SLAPI_LOG_FATAL, "ipapwd_start", "Error reading Master key stash file!\n"); - krb5_free_context(krbctx); - return LDAP_OPERATIONS_ERROR; - } + ipa_realm = realm; + ipa_realm_dn = realm_dn; + ipa_keysalts = keysalts; + ipa_n_keysalts = n_keysalts; - r = read(fd, &l, sizeof(l)); /* read the key length, a horrible sizeof(int) local endian value */ - if (r != sizeof(l)) { - slapi_log_error( SLAPI_LOG_FATAL, "ipapwd_start", "Error reading Master key stash file!\n"); - krb5_free_context(krbctx); - return LDAP_OPERATIONS_ERROR; + ret = ipapwd_getMasterKey(ipa_realm_dn); + if (ret) { + slapi_log_error( SLAPI_LOG_PLUGIN, "ipapwd_start", "Couldn't init master key at start delaying ..."); + ret = LDAP_SUCCESS; } - if (l == 0 || l > 1024) { /* the maximum key size should be 32 bytes, lets's not accept more than 1k anyway */ - slapi_log_error( SLAPI_LOG_FATAL, "ipapwd_start", "Invalid key lenght, Master key stash file corrupted?\n"); - krb5_free_context(krbctx); - return LDAP_OPERATIONS_ERROR; - } - - o = malloc(l); - if (!o) { - slapi_log_error( SLAPI_LOG_FATAL, "ipapwd_start", "Memory allocation problem!\n"); - krb5_free_context(krbctx); - return LDAP_OPERATIONS_ERROR; - } - - r = read(fd, o, l); - if (r != l) { - slapi_log_error( SLAPI_LOG_FATAL, "ipapwd_start", "Error reading Master key stash file!\n"); - krb5_free_context(krbctx); - return LDAP_OPERATIONS_ERROR; - } - - close(fd); - - kmkey.magic = KV5M_KEYBLOCK; - kmkey.enctype = e; - kmkey.length = l; - kmkey.contents = o; - - krb5_free_context(krbctx); - return LDAP_SUCCESS; + slapi_entry_free(config_entry); + return ret; } /* Initialization function */ diff --git a/ipa-server/ipa-slapi-plugins/ipa-pwd-extop/pwd-extop-conf.ldif b/ipa-server/ipa-slapi-plugins/ipa-pwd-extop/pwd-extop-conf.ldif index 689baecb3..efd80ccf5 100644 --- a/ipa-server/ipa-slapi-plugins/ipa-pwd-extop/pwd-extop-conf.ldif +++ b/ipa-server/ipa-slapi-plugins/ipa-pwd-extop/pwd-extop-conf.ldif @@ -12,4 +12,5 @@ nsslapd-pluginid: Multi-hash Change Password Extended Operation nsslapd-pluginversion: 1.0 nsslapd-pluginvendor: RedHat nsslapd-plugindescription: Support saving passwords in multiple formats for different consumers (krb5, samba, freeradius, etc.) -nsslapd-pluginarg0: /var/kerberos/krb5kdc/.k5.$REALM +nsslapd-plugin-depends-on-type: database +nsslapd-realmTree: $SUFFIX diff --git a/ipa-server/ipaserver/Makefile.am b/ipa-server/ipaserver/Makefile.am index 25b856878..c2d164b96 100644 --- a/ipa-server/ipaserver/Makefile.am +++ b/ipa-server/ipaserver/Makefile.am @@ -12,6 +12,8 @@ app_PYTHON = \ radiusinstance.py \ webguiinstance.py \ service.py \ + installutils.py \ + replication.py \ $(NULL) EXTRA_DIST = \ diff --git a/ipa-server/ipaserver/dsinstance.py b/ipa-server/ipaserver/dsinstance.py index ce3c154f0..79a57182f 100644 --- a/ipa-server/ipaserver/dsinstance.py +++ b/ipa-server/ipaserver/dsinstance.py @@ -24,10 +24,14 @@ import tempfile import shutil import logging import pwd +import glob +import sys from ipa.ipautil import * import service +import installutils + SERVER_ROOT_64 = "/usr/lib64/dirsrv" SERVER_ROOT_32 = "/usr/lib/dirsrv" @@ -46,6 +50,61 @@ def find_server_root(): else: return SERVER_ROOT_32 +def realm_to_serverid(realm_name): + return "-".join(realm_name.split(".")) + +def config_dirname(realm_name): + return "/etc/dirsrv/slapd-" + realm_to_serverid(realm_name) + "/" + +def schema_dirname(realm_name): + return config_dirname(realm_name) + "/schema/" + +def erase_ds_instance_data(serverid): + try: + shutil.rmtree("/etc/dirsrv/slapd-%s" % serverid) + except: + pass + try: + shutil.rmtree("/var/lib/dirsrv/slapd-%s" % serverid) + except: + pass + try: + shutil.rmtree("/var/lock/dirsrv/slapd-%s" % serverid) + except: + pass + +def check_existing_installation(): + dirs = glob.glob("/etc/dirsrv/slapd-*") + if not dirs: + return + print "" + print "An existing Directory Server has been detected." + yesno = raw_input("Do you wish to remove it and create a new one? [no]: ") + if not yesno or yesno.lower()[0] != "y": + sys.exit(1) + + try: + run(["/sbin/service", "dirsrv", "stop"]) + except: + pass + for d in dirs: + serverid = os.path.basename(d).split("slapd-", 1)[1] + if serverid: + erase_ds_instance_data(serverid) + +def check_ports(): + ds_unsecure = installutils.port_available(389) + ds_secure = installutils.port_available(636) + if not ds_unsecure or not ds_secure: + print "IPA requires ports 389 and 636 for the Directory Server." + print "These are currently in use:" + if not ds_unsecure: + print "\t389" + if not ds_secure: + print "\t636" + sys.exit(1) + + INF_TEMPLATE = """ [General] FullMachineName= $FQHN @@ -69,20 +128,25 @@ class DsInstance(service.Service): self.dm_password = None self.sub_dict = None - def create_instance(self, ds_user, realm_name, host_name, dm_password): + def create_instance(self, ds_user, realm_name, host_name, dm_password, ro_replica=False): self.ds_user = ds_user self.realm_name = realm_name.upper() - self.serverid = "-".join(self.realm_name.split(".")) + self.serverid = realm_to_serverid(self.realm_name) self.suffix = realm_to_suffix(self.realm_name) self.host_name = host_name self.dm_password = dm_password self.__setup_sub_dict() + + if ro_replica: + self.start_creation(15, "Configuring directory server:") + else: + self.start_creation(15, "Configuring directory server:") - self.start_creation(14, "Configuring directory server:") self.__create_ds_user() self.__create_instance() self.__add_default_schemas() - self.__add_memberof_module() + if not ro_replica: + self.__add_memberof_module() self.__add_referint_module() self.__add_dna_module() self.__create_indeces() @@ -94,9 +158,11 @@ class DsInstance(service.Service): except: # TODO: roll back here? logging.critical("Failed to restart the ds instance") - self.__config_uidgid_gen_first_master() self.__add_default_layout() - self.__add_master_entry_first_master() + if not ro_replica: + self.__config_uidgid_gen_first_master() + self.__add_master_entry_first_master() + self.__init_memberof() self.step("configuring directoy to start on boot") @@ -104,18 +170,10 @@ class DsInstance(service.Service): self.done_creation() - def config_dirname(self): - if not self.serverid: - raise RuntimeError("serverid not set") - return "/etc/dirsrv/slapd-" + self.serverid + "/" - - def schema_dirname(self): - return self.config_dirname() + "/schema/" - def __setup_sub_dict(self): server_root = find_server_root() self.sub_dict = dict(FQHN=self.host_name, SERVERID=self.serverid, - PASSWORD=self.dm_password, SUFFIX=self.suffix, + PASSWORD=self.dm_password, SUFFIX=self.suffix.lower(), REALM=self.realm_name, USER=self.ds_user, SERVER_ROOT=server_root) @@ -161,11 +219,13 @@ class DsInstance(service.Service): def __add_default_schemas(self): self.step("adding default schema") shutil.copyfile(SHARE_DIR + "60kerberos.ldif", - self.schema_dirname() + "60kerberos.ldif") + schema_dirname(self.realm_name) + "60kerberos.ldif") shutil.copyfile(SHARE_DIR + "60samba.ldif", - self.schema_dirname() + "60samba.ldif") + schema_dirname(self.realm_name) + "60samba.ldif") shutil.copyfile(SHARE_DIR + "60radius.ldif", - self.schema_dirname() + "60radius.ldif") + schema_dirname(self.realm_name) + "60radius.ldif") + shutil.copyfile(SHARE_DIR + "60ipaconfig.ldif", + schema_dirname(self.realm_name) + "60ipaconfig.ldif") def __add_memberof_module(self): self.step("enabling memboerof plugin") @@ -177,6 +237,16 @@ class DsInstance(service.Service): logging.critical("Failed to load memberof-conf.ldif: %s" % str(e)) memberof_fd.close() + def __init_memberof(self): + self.step("initializing group membership") + memberof_txt = template_file(SHARE_DIR + "memberof-task.ldif", self.sub_dict) + memberof_fd = write_tmp_file(memberof_txt) + try: + ldap_mod(memberof_fd, "cn=Directory Manager", self.dm_password) + except subprocess.CalledProcessError, e: + logging.critical("Failed to load memberof-conf.ldif: %s" % str(e)) + memberof_fd.close() + def __add_referint_module(self): self.step("enabling referential integrity plugin") referint_txt = template_file(SHARE_DIR + "referint-conf.ldif", self.sub_dict) @@ -219,7 +289,7 @@ class DsInstance(service.Service): def __enable_ssl(self): self.step("configuring ssl for ds instance") - dirname = self.config_dirname() + dirname = config_dirname(self.realm_name) args = ["/usr/share/ipa/ipa-server-setupssl", self.dm_password, dirname, self.host_name] try: @@ -257,7 +327,7 @@ class DsInstance(service.Service): def __certmap_conf(self): self.step("configuring certmap.conf") - dirname = self.config_dirname() + dirname = config_dirname(self.realm_name) certmap_conf = template_file(SHARE_DIR+"certmap.conf.template", self.sub_dict) certmap_fd = open(dirname+"certmap.conf", "w+") certmap_fd.write(certmap_conf) @@ -265,7 +335,7 @@ class DsInstance(service.Service): def change_admin_password(self, password): logging.debug("Changing admin password") - dirname = self.config_dirname() + dirname = config_dirname(self.realm_name) if dir_exists("/usr/lib64/mozldap"): app = "/usr/lib64/mozldap/ldappasswd" else: diff --git a/ipa-server/ipaserver/httpinstance.py b/ipa-server/ipaserver/httpinstance.py index 0433025b2..60d33eedc 100644 --- a/ipa-server/ipaserver/httpinstance.py +++ b/ipa-server/ipaserver/httpinstance.py @@ -50,13 +50,17 @@ def update_file(filename, orig, subst): else: sys.stdout.write(p.sub(subst, line)) fileinput.close() + return 0 + else: + print "File %s doesn't exist." % filename + return 1 class HTTPInstance(service.Service): def __init__(self): service.Service.__init__(self, "httpd") def create_instance(self, realm, fqdn): - self.sub_dict = { "REALM" : realm } + self.sub_dict = { "REALM" : realm, "FQDN": fqdn } self.fqdn = fqdn self.realm = realm @@ -137,4 +141,5 @@ class HTTPInstance(service.Service): def __set_mod_nss_port(self): self.step("Setting mod_nss port to 443") - update_file(NSS_CONF, '8443', '443') + if update_file(NSS_CONF, '8443', '443') != 0: + print "Updating %s failed." % NSS_CONF diff --git a/ipa-server/ipaserver/installutils.py b/ipa-server/ipaserver/installutils.py new file mode 100644 index 000000000..a403e8154 --- /dev/null +++ b/ipa-server/ipaserver/installutils.py @@ -0,0 +1,108 @@ +# Authors: Simo Sorce <ssorce@redhat.com> +# +# Copyright (C) 2007 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 or later +# +# 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 +# + +import logging +import socket +import errno +import getpass + +def get_fqdn(): + fqdn = "" + try: + fqdn = socket.getfqdn() + except: + try: + fqdn = socket.gethostname() + except: + fqdn = "" + return fqdn + +def verify_fqdn(host_name): + if len(host_name.split(".")) < 2 or host_name == "localhost.localdomain": + raise RuntimeError("Invalid hostname: " + host_name) + +def port_available(port): + """Try to bind to a port on the wildcard host + Return 1 if the port is available + Return 0 if the port is in use + """ + rv = 1 + + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind(('', port)) + s.shutdown(0) + s.close() + except socket.error, e: + if e[0] == errno.EADDRINUSE: + rv = 0 + + if rv: + try: + s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind(('', port)) + s.shutdown(0) + s.close() + except socket.error, e: + if e[0] == errno.EADDRINUSE: + rv = 0 + + return rv + +def standard_logging_setup(log_filename, debug=False): + # Always log everything (i.e., DEBUG) to the log + # file. + logging.basicConfig(level=logging.DEBUG, + format='%(asctime)s %(levelname)s %(message)s', + filename=log_filename, + filemode='w') + + console = logging.StreamHandler() + # If the debug option is set, also log debug messages to the console + if debug: + console.setLevel(logging.DEBUG) + else: + # Otherwise, log critical and error messages + console.setLevel(logging.ERROR) + formatter = logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s') + console.setFormatter(formatter) + logging.getLogger('').addHandler(console) + +def read_password(user): + correct = False + pwd = "" + while not correct: + pwd = getpass.getpass(user + " password: ") + if not pwd: + continue + if len(pwd) < 8: + print "Password must be at least 8 characters long" + continue + pwd_confirm = getpass.getpass("Password (confirm): ") + if pwd != pwd_confirm: + print "Password mismatch!" + print "" + else: + correct = True + print "" + return pwd + + diff --git a/ipa-server/ipaserver/ipaldap.py b/ipa-server/ipaserver/ipaldap.py index 69f040eb5..ef8becec5 100644 --- a/ipa-server/ipaserver/ipaldap.py +++ b/ipa-server/ipaserver/ipaldap.py @@ -176,25 +176,90 @@ def wrapper(f,name): return f(*args, **kargs) return inner +class LDIFConn(ldif.LDIFParser): + def __init__( + self, + input_file, + ignored_attr_types=None,max_entries=0,process_url_schemes=None + ): + """ + See LDIFParser.__init__() + + Additional Parameters: + all_records + List instance for storing parsed records + """ + self.dndict = {} # maps dn to Entry + self.dnlist = [] # contains entries in order read + myfile = input_file + if isinstance(input_file,str) or isinstance(input_file,unicode): + myfile = open(input_file, "r") + ldif.LDIFParser.__init__(self,myfile,ignored_attr_types,max_entries,process_url_schemes) + self.parse() + if isinstance(input_file,str) or isinstance(input_file,unicode): + myfile.close() + + def handle(self,dn,entry): + """ + Append single record to dictionary of all records. + """ + if not dn: + dn = '' + newentry = Entry((dn, entry)) + self.dndict[IPAdmin.normalizeDN(dn)] = newentry + self.dnlist.append(newentry) + + def get(self,dn): + ndn = IPAdmin.normalizeDN(dn) + return self.dndict.get(ndn, Entry(None)) + class IPAdmin(SimpleLDAPObject): CFGSUFFIX = "o=NetscapeRoot" DEFAULT_USER_ID = "nobody" + + def getDseAttr(self,attrname): + conffile = self.confdir + '/dse.ldif' + dseldif = LDIFConn(conffile) + cnconfig = dseldif.get("cn=config") + if cnconfig: + return cnconfig.getValue(attrname) + return None def __initPart2(self): if self.binddn and len(self.binddn) and not hasattr(self,'sroot'): try: ent = self.getEntry('cn=config', ldap.SCOPE_BASE, '(objectclass=*)', - [ 'nsslapd-instancedir', 'nsslapd-errorlog' ]) - instdir = ent.getValue('nsslapd-instancedir') - self.sroot, self.inst = re.match(r'(.*)[\/]slapd-(\w+)$', instdir).groups() + [ 'nsslapd-instancedir', 'nsslapd-errorlog', + 'nsslapd-certdir', 'nsslapd-schemadir' ]) self.errlog = ent.getValue('nsslapd-errorlog') - except (ldap.INSUFFICIENT_ACCESS, ldap.CONNECT_ERROR, - ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND)): + self.confdir = None + if self.isLocal: + self.confdir = ent.getValue('nsslapd-certdir') + if not self.confdir or not os.access(self.confdir + '/dse.ldif', os.R_OK): + self.confdir = ent.getValue('nsslapd-schemadir') + if self.confdir: + self.confdir = os.path.dirname(self.confdir) + instdir = ent.getValue('nsslapd-instancedir') + if not instdir: + # get instance name from errorlog + self.inst = re.match(r'(.*)[\/]slapd-([\w-]+)/errors', self.errlog).group(2) + if self.confdir: + instdir = self.getDseAttr('nsslapd-instancedir') + else: + if self.isLocal: + print instdir + self.sroot, self.inst = re.match(r'(.*)[\/]slapd-([\w-]+)$', instdir).groups() + instdir = re.match(r'(.*/slapd-.*)/errors', self.errlog).group(1) + #self.sroot, self.inst = re.match(r'(.*)[\/]slapd-([\w-]+)$', instdir).groups() + ent = self.getEntry('cn=config,cn=ldbm database,cn=plugins,cn=config', + ldap.SCOPE_BASE, '(objectclass=*)', + [ 'nsslapd-directory' ]) + self.dbdir = os.path.dirname(ent.getValue('nsslapd-directory')) + except (ldap.INSUFFICIENT_ACCESS, ldap.CONNECT_ERROR): pass # usually means -# print "ignored exception" except ldap.LDAPError, e: print "caught exception ", e - raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, None, e) + raise def __localinit__(self): """If a CA certificate is provided then it is assumed that we are @@ -209,7 +274,7 @@ class IPAdmin(SimpleLDAPObject): else: SimpleLDAPObject.__init__(self,'ldap://%s:%d' % (self.host,self.port)) - def __init__(self,host,port,cacert,bindcert,bindkey,proxydn=None,debug=None): + def __init__(self,host,port=389,cacert=None,bindcert=None,bindkey=None,proxydn=None,debug=None): """We just set our instance variables and wrap the methods - the real work is done in __localinit__ and __initPart2 - these are separated out this way so that we can call them from places other than @@ -223,7 +288,7 @@ class IPAdmin(SimpleLDAPObject): ldap.set_option(ldap.OPT_X_TLS_KEYFILE,bindkey) self.__wrapmethods() - self.port = port or 389 + self.port = port self.host = host self.cacert = cacert self.bindcert = bindcert @@ -272,6 +337,12 @@ class IPAdmin(SimpleLDAPObject): self.principal = principal self.proxydn = None + def do_simple_bind(self, binddn="cn=directory manager", bindpw=""): + self.binddn = binddn + self.bindpwd = bindpw + self.simple_bind_s(binddn, bindpw) + self.__initPart2() + def getEntry(self,*args): """This wraps the search function. It is common to just get one entry""" @@ -283,8 +354,9 @@ class IPAdmin(SimpleLDAPObject): try: res = self.search(*args) type, obj = self.result(res) - - # res = self.search_ext(args[0], args[1], filterstr=args[2], attrlist=args[3], serverctrls=sctrl) + except ldap.NO_SUCH_OBJECT: + raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND, + "no such entry for " + str(args)) except ldap.LDAPError, e: raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, None, e) @@ -377,6 +449,23 @@ class IPAdmin(SimpleLDAPObject): raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, None, e) return "Success" + def updateRDN(self, dn, newrdn): + """Wrap the modrdn function.""" + + sctrl = self.__get_server_controls__() + + if dn == newrdn: + # no need to report an error + return "Success" + + try: + if sctrl is not None: + self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) + self.modrdn_s(dn, newrdn, delold=1) + except ldap.LDAPError, e: + raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, None, e) + return "Success" + def updateEntry(self,dn,olduser,newuser): """This wraps the mod function. It assumes that the entry is already populated with all of the desired objectclasses and attributes""" @@ -521,7 +610,7 @@ class IPAdmin(SimpleLDAPObject): print "Export task %s for file %s completed successfully" % (cn,file) return rc - def waitForEntry(self, dn, timeout=7200, attr='', quiet=False): + def waitForEntry(self, dn, timeout=7200, attr='', quiet=True): scope = ldap.SCOPE_BASE filter = "(objectclass=*)" attrlist = [] @@ -543,7 +632,8 @@ class IPAdmin(SimpleLDAPObject): entry = self.getEntry(dn, scope, filter, attrlist) except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND): pass # found entry, but no attr - except ldap.NO_SUCH_OBJECT: pass # no entry yet + except ldap.NO_SUCH_OBJECT: + pass # no entry yet except ldap.LDAPError, e: # badness print "\nError reading entry", dn, e break @@ -557,7 +647,7 @@ class IPAdmin(SimpleLDAPObject): print "\nwaitForEntry timeout for %s for %s" % (self,dn) elif entry and not quiet: print "\nThe waited for entry is:", entry - else: + elif not entry: print "\nError: could not read entry %s from %s" % (dn,self) return entry diff --git a/ipa-server/ipaserver/krbinstance.py b/ipa-server/ipaserver/krbinstance.py index c4ebde50c..c83002f73 100644 --- a/ipa-server/ipaserver/krbinstance.py +++ b/ipa-server/ipaserver/krbinstance.py @@ -26,29 +26,32 @@ import logging import fileinput import re import sys -from random import Random -from time import gmtime import os import pwd import socket import time +import shutil import service from ipa.ipautil import * +from ipa import ipaerror + +import ipaldap + +import ldap +from ldap import LDAPError +from ldap import ldapobject + +from pyasn1.type import univ, namedtype +import pyasn1.codec.ber.encoder +import pyasn1.codec.ber.decoder +import struct +import base64 def host_to_domain(fqdn): s = fqdn.split(".") return ".".join(s[1:]) -def generate_kdc_password(): - rndpwd = '' - r = Random() - r.seed(gmtime()) - for x in range(12): -# rndpwd += chr(r.randint(32,126)) - rndpwd += chr(r.randint(65,90)) #stricter set for testing - return rndpwd - def ldap_mod(fd, dn, pwd): args = ["/usr/bin/ldapmodify", "-h", "127.0.0.1", "-xv", "-D", dn, "-w", pwd, "-f", fd.name] run(args) @@ -79,18 +82,26 @@ class KrbInstance(service.Service): self.kdc_password = None self.sub_dict = None - def create_instance(self, ds_user, realm_name, host_name, admin_password, master_password): + def __common_setup(self, ds_user, realm_name, host_name, admin_password): self.ds_user = ds_user - self.fqdn = host_name - self.ip = socket.gethostbyname(host_name) + self.fqdn = host_name self.realm = realm_name.upper() self.host = host_name.split(".")[0] - self.domain = host_to_domain(host_name) - self.admin_password = admin_password - self.master_password = master_password - + self.ip = socket.gethostbyname(host_name) + self.domain = host_to_domain(host_name) self.suffix = realm_to_suffix(self.realm) - self.kdc_password = generate_kdc_password() + self.kdc_password = ipa_generate_password() + self.admin_password = admin_password + + self.__setup_sub_dict() + + # get a connection to the DS + try: + self.conn = ipaldap.IPAdmin(self.fqdn) + self.conn.do_simple_bind(bindpw=self.admin_password) + except ipaerror.exception_for(ipaerror.LDAP_DATABASE_ERROR), e: + logging.critical("Could not connect to DS") + raise e try: self.stop() @@ -98,22 +109,7 @@ class KrbInstance(service.Service): # It could have been not running pass - self.start_creation(10, "Configuring Kerberos KDC") - - self.__configure_kdc_account_password() - - self.__setup_sub_dict() - - self.__configure_ldap() - - self.__create_instance() - - self.__create_ds_keytab() - - self.__export_kadmin_changepw_keytab() - - self.__add_pwd_extop_module() - + def __common_post_setup(self): try: self.step("starting the KDC") self.start() @@ -129,8 +125,49 @@ class KrbInstance(service.Service): self.step("starting ipa-kpasswd") service.start("ipa-kpasswd") + + def create_instance(self, ds_user, realm_name, host_name, admin_password, master_password): + self.master_password = master_password + + self.__common_setup(ds_user, realm_name, host_name, admin_password) + + self.start_creation(11, "Configuring Kerberos KDC") + + self.__configure_kdc_account_password() + self.__configure_sasl_mappings() + self.__add_krb_entries() + self.__create_instance() + self.__create_ds_keytab() + self.__export_kadmin_changepw_keytab() + self.__add_pwd_extop_module() + + self.__common_post_setup() + + self.done_creation() + + + def create_replica(self, ds_user, realm_name, host_name, admin_password, ldap_passwd_filename): + + self.__common_setup(ds_user, realm_name, host_name, admin_password) + + self.start_creation(9, "Configuring Kerberos KDC") + self.__copy_ldap_passwd(ldap_passwd_filename) + self.__configure_sasl_mappings() + self.__write_stash_from_ds() + self.__create_instance(replica=True) + self.__create_ds_keytab() + self.__export_kadmin_changepw_keytab() + + self.__common_post_setup() + self.done_creation() + + def __copy_ldap_passwd(self, filename): + shutil.copy(filename, "/var/kerberos/krb5kdc/ldappwd") + os.chmod("/var/kerberos/krb5kdc/ldappwd", 0600) + + def __configure_kdc_account_password(self): self.step("setting KDC account password") hexpwd = '' @@ -139,6 +176,7 @@ class KrbInstance(service.Service): pwd_fd = open("/var/kerberos/krb5kdc/ldappwd", "w") pwd_fd.write("uid=kdc,cn=sysaccounts,cn=etc,"+self.suffix+"#{HEX}"+hexpwd+"\n") pwd_fd.close() + os.chmod("/var/kerberos/krb5kdc/ldappwd", 0600) def __setup_sub_dict(self): self.sub_dict = dict(FQDN=self.fqdn, @@ -149,9 +187,60 @@ class KrbInstance(service.Service): HOST=self.host, REALM=self.realm) - def __configure_ldap(self): - self.step("adding kerberos configuration to the directory") - #TODO: test that the ldif is ok with any random charcter we may use in the password + def __configure_sasl_mappings(self): + self.step("adding sasl mappings to the directory") + # we need to remove any existing SASL mappings in the directory as otherwise they + # they may conflict. There is no way to define the order they are used in atm. + + # FIXME: for some reason IPAdmin dies here, so we switch + # it out for a regular ldapobject. + conn = self.conn + self.conn = ldapobject.SimpleLDAPObject("ldap://127.0.0.1/") + self.conn.bind("cn=directory manager", self.admin_password) + try: + msgid = self.conn.search("cn=mapping,cn=sasl,cn=config", ldap.SCOPE_ONELEVEL, "(objectclass=nsSaslMapping)") + res = self.conn.result(msgid) + for r in res[1]: + mid = self.conn.delete_s(r[0]) + #except LDAPError, e: + # logging.critical("Error during SASL mapping removal: %s" % str(e)) + except Exception, e: + print type(e) + print dir(e) + raise e + + self.conn = conn + + entry = ipaldap.Entry("cn=Full Principal,cn=mapping,cn=sasl,cn=config") + entry.setValues("objectclass", "top", "nsSaslMapping") + entry.setValues("cn", "Full Principal") + entry.setValues("nsSaslMapRegexString", '\(.*\)@\(.*\)') + entry.setValues("nsSaslMapBaseDNTemplate", self.suffix) + entry.setValues("nsSaslMapFilterTemplate", '(krbPrincipalName=\\1@\\2)') + + try: + self.conn.add_s(entry) + except ldap.ALREADY_EXISTS: + logging.critical("failed to add Full Principal Sasl mapping") + raise e + + entry = ipaldap.Entry("cn=Name Only,cn=mapping,cn=sasl,cn=config") + entry.setValues("objectclass", "top", "nsSaslMapping") + entry.setValues("cn", "Name Only") + entry.setValues("nsSaslMapRegexString", '\(.*\)') + entry.setValues("nsSaslMapBaseDNTemplate", self.suffix) + entry.setValues("nsSaslMapFilterTemplate", '(krbPrincipalName=\\1@%s)' % self.realm) + + try: + self.conn.add_s(entry) + except ldap.ALREADY_EXISTS: + logging.critical("failed to add Name Only Sasl mapping") + raise e + + def __add_krb_entries(self): + self.step("adding kerberos entries to the DS") + + #TODO: test that the ldif is ok with any random charcter we may use in the password kerberos_txt = template_file(SHARE_DIR + "kerberos.ldif", self.sub_dict) kerberos_fd = write_tmp_file(kerberos_txt) try: @@ -169,7 +258,7 @@ class KrbInstance(service.Service): logging.critical("Failed to load default-aci.ldif: %s" % str(e)) aci_fd.close() - def __create_instance(self): + def __create_instance(self, replica=False): self.step("configuring KDC") kdc_conf = template_file(SHARE_DIR+"kdc.conf.template", self.sub_dict) kdc_fd = open("/var/kerberos/krb5kdc/kdc.conf", "w+") @@ -197,12 +286,34 @@ class KrbInstance(service.Service): krb_fd.write(krb_realm) krb_fd.close() - #populate the directory with the realm structure - args = ["/usr/kerberos/sbin/kdb5_ldap_util", "-D", "uid=kdc,cn=sysaccounts,cn=etc,"+self.suffix, "-w", self.kdc_password, "create", "-s", "-P", self.master_password, "-r", self.realm, "-subtrees", self.suffix, "-sscope", "sub"] + if not replica: + #populate the directory with the realm structure + args = ["/usr/kerberos/sbin/kdb5_ldap_util", "-D", "uid=kdc,cn=sysaccounts,cn=etc,"+self.suffix, "-w", self.kdc_password, "create", "-s", "-P", self.master_password, "-r", self.realm, "-subtrees", self.suffix, "-sscope", "sub"] + try: + run(args) + except subprocess.CalledProcessError, e: + print "Failed to populate the realm structure in kerberos", e + + def __write_stash_from_ds(self): + self.step("writing stash file from DS") try: - run(args) - except subprocess.CalledProcessError, e: - print "Failed to populate the realm structure in kerberos", e + entry = self.conn.getEntry("cn=%s, cn=kerberos, %s" % (self.realm, self.suffix), ldap.SCOPE_SUBTREE) + except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND), e: + logging.critical("Could not find master key in DS") + raise e + + krbMKey = pyasn1.codec.ber.decoder.decode(entry.krbmkey) + keytype = int(krbMKey[0][1][0]) + keydata = str(krbMKey[0][1][1]) + + format = '=hi%ss' % len(keydata) + s = struct.pack(format, keytype, len(keydata), keydata) + try: + fd = open("/var/kerberos/krb5kdc/.k5."+self.realm, "w") + fd.write(s) + except os.error, e: + logging.critical("failed to write stash file") + raise e #add the password extop module def __add_pwd_extop_module(self): @@ -215,12 +326,31 @@ class KrbInstance(service.Service): logging.critical("Failed to load pwd-extop-conf.ldif: %s" % str(e)) extop_fd.close() - #add an ACL to let the DS user read the master key - args = ["/usr/bin/setfacl", "-m", "u:"+self.ds_user+":r", "/var/kerberos/krb5kdc/.k5."+self.realm] + #get the Master Key from the stash file try: - run(args) - except subprocess.CalledProcessError, e: - logging.critical("Failed to set the ACL on the master key: %s" % str(e)) + stash = open("/var/kerberos/krb5kdc/.k5."+self.realm, "r") + keytype = struct.unpack('h', stash.read(2))[0] + keylen = struct.unpack('i', stash.read(4))[0] + keydata = stash.read(keylen) + except os.error: + logging.critical("Failed to retrieve Master Key from Stash file: %s") + #encode it in the asn.1 attribute + MasterKey = univ.Sequence() + MasterKey.setComponentByPosition(0, univ.Integer(keytype)) + MasterKey.setComponentByPosition(1, univ.OctetString(keydata)) + krbMKey = univ.Sequence() + krbMKey.setComponentByPosition(0, univ.Integer(0)) #we have no kvno + krbMKey.setComponentByPosition(1, MasterKey) + asn1key = pyasn1.codec.ber.encoder.encode(krbMKey) + + entry = ipaldap.Entry("cn="+self.realm+",cn=kerberos,"+self.suffix) + dn = "cn="+self.realm+",cn=kerberos,"+self.suffix + mod = [(ldap.MOD_ADD, 'krbMKey', str(asn1key))] + try: + self.conn.modify_s(dn, mod) + except ldap.TYPE_OR_VALUE_EXISTS, e: + logging.critical("failed to add master key to kerberos database\n") + raise e def __create_ds_keytab(self): self.step("creating a keytab for the directory") diff --git a/ipa-server/ipaserver/radiusinstance.py b/ipa-server/ipaserver/radiusinstance.py index dd14bf200..3b89018f0 100644 --- a/ipa-server/ipaserver/radiusinstance.py +++ b/ipa-server/ipaserver/radiusinstance.py @@ -26,6 +26,7 @@ import shutil import logging import pwd import time +import sys from ipa.ipautil import * from ipa import radius_util @@ -147,8 +148,7 @@ class RadiusInstance(service.Service): retry += 1 if retry > 15: print "Error timed out waiting for kadmin to finish operations\n" - sys.exit() - + sys.exit(1) try: pent = pwd.getpwnam(radius_util.RADIUS_USER) os.chown(radius_util.RADIUS_IPA_KEYTAB_FILEPATH, pent.pw_uid, pent.pw_gid) diff --git a/ipa-server/ipaserver/replication.py b/ipa-server/ipaserver/replication.py new file mode 100644 index 000000000..580ec27bf --- /dev/null +++ b/ipa-server/ipaserver/replication.py @@ -0,0 +1,316 @@ +# Authors: Karl MacMillan <kmacmillan@mentalrootkit.com> +# +# Copyright (C) 2007 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 or later +# +# 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 +# + +import time, logging + +import ipaldap, ldap, dsinstance +from ipa import ipaerror + +DIRMAN_CN = "cn=directory manager" +PORT = 389 +TIMEOUT = 120 + +class ReplicationManager: + """Manage replicatin agreements between DS servers""" + def __init__(self, hostname, dirman_passwd): + self.hostname = hostname + self.dirman_passwd = dirman_passwd + self.conn = ipaldap.IPAdmin(hostname) + self.conn.do_simple_bind(bindpw=dirman_passwd) + + self.repl_man_passwd = dirman_passwd + + # these are likely constant, but you could change them + # at runtime if you really want + self.repl_man_dn = "cn=replication manager,cn=config" + self.repl_man_cn = "replication manager" + self.suffix = "" + + def find_replication_dns(self, conn): + filt = "(objectlcass=nsds5ReplicationAgreement)" + try: + ents = conn.search_s("cn=mapping tree,cn-config", ldap.SCOPE_SUBTREE, filt, ["cn"]) + except ldap.NO_SUCH_OBJECT: + return [] + return [ent.dn for ent in ents] + + def add_replication_manager(self, conn, passwd=None): + """ + Create a pseudo user to use for replication. If no password + is provided the directory manager password will be used. + """ + + if passwd: + self.repl_man_passwd = passwd + + ent = ipaldap.Entry(self.repl_man_dn) + ent.setValues("objectclass", "top", "person") + ent.setValues("cn", self.repl_man_cn) + ent.setValues("userpassword", self.repl_man_passwd) + ent.setValues("sn", "replication manager pseudo user") + + try: + conn.add_s(ent) + except ldap.ALREADY_EXISTS: + # should we set the password here? + pass + + def delete_replication_manager(self, conn, dn="cn=replication manager,cn=config"): + try: + conn.delete_s(dn) + except ldap.NO_SUCH_OBJECT: + pass + + def get_replica_type(self, master): + if master: + return "3" + else: + return "2" + + def replica_dn(self): + return 'cn=replica, cn="%s", cn=mapping tree, cn=config' % self.suffix + + + def local_replica_config(self, conn, master, replica_id): + dn = self.replica_dn() + + try: + conn.getEntry(dn, ldap.SCOPE_BASE) + # replication is already configured + return + except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND): + pass + + replica_type = self.get_replica_type(master) + + entry = ipaldap.Entry(dn) + entry.setValues('objectclass', "top", "nsds5replica", "extensibleobject") + entry.setValues('cn', "replica") + entry.setValues('nsds5replicaroot', self.suffix) + entry.setValues('nsds5replicaid', str(replica_id)) + entry.setValues('nsds5replicatype', replica_type) + entry.setValues('nsds5flags', "1") + entry.setValues('nsds5replicabinddn', [self.repl_man_dn]) + entry.setValues('nsds5replicalegacyconsumer', "off") + + conn.add_s(entry) + + def setup_changelog(self, conn): + dn = "cn=changelog5, cn=config" + dirpath = conn.dbdir + "/cldb" + entry = ipaldap.Entry(dn) + entry.setValues('objectclass', "top", "extensibleobject") + entry.setValues('cn', "changelog5") + entry.setValues('nsslapd-changelogdir', dirpath) + try: + conn.add_s(entry) + except ldap.ALREADY_EXISTS: + return + + def setup_chaining_backend(self, conn): + chaindn = "cn=chaining database, cn=plugins, cn=config" + benamebase = "chaindb" + urls = [self.to_ldap_url(conn)] + cn = "" + benum = 1 + done = False + while not done: + try: + cn = benamebase + str(benum) # e.g. localdb1 + dn = "cn=" + cn + ", " + chaindn + entry = ipaldap.Entry(dn) + entry.setValues('objectclass', 'top', 'extensibleObject', 'nsBackendInstance') + entry.setValues('cn', cn) + entry.setValues('nsslapd-suffix', self.suffix) + entry.setValues('nsfarmserverurl', urls) + entry.setValues('nsmultiplexorbinddn', self.repl_man_dn) + entry.setValues('nsmultiplexorcredentials', self.repl_man_passwd) + + self.conn.add_s(entry) + done = True + except ldap.ALREADY_EXISTS: + benum += 1 + except ldap.LDAPError, e: + print "Could not add backend entry " + dn, e + raise + + return cn + + def to_ldap_url(self, conn): + return "ldap://%s:%d/" % (conn.host, conn.port) + + def setup_chaining_farm(self, conn): + try: + conn.modify_s(self.suffix, [(ldap.MOD_ADD, 'aci', + [ "(targetattr = \"*\")(version 3.0; acl \"Proxied authorization for database links\"; allow (proxy) userdn = \"ldap:///%s\";)" % self.repl_man_dn ])]) + except ldap.TYPE_OR_VALUE_EXISTS: + logging.debug("proxy aci already exists in suffix %s on %s" % (self.suffix, conn.host)) + + def get_mapping_tree_entry(self): + try: + entry = self.conn.getEntry("cn=mapping tree,cn=config", ldap.SCOPE_ONELEVEL, + "(cn=\"%s\")" % (self.suffix)) + except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND), e: + logging.debug("failed to find mappting tree entry for %s" % self.suffix) + raise e + + return entry + + + def enable_chain_on_update(self, bename): + mtent = self.get_mapping_tree_entry() + dn = mtent.dn + + plgent = self.conn.getEntry("cn=Multimaster Replication Plugin,cn=plugins,cn=config", + ldap.SCOPE_BASE, "(objectclass=*)", ['nsslapd-pluginPath']) + path = plgent.getValue('nsslapd-pluginPath') + + mod = [(ldap.MOD_REPLACE, 'nsslapd-state', 'backend'), + (ldap.MOD_ADD, 'nsslapd-backend', bename), + (ldap.MOD_ADD, 'nsslapd-distribution-plugin', path), + (ldap.MOD_ADD, 'nsslapd-distribution-funct', 'repl_chain_on_update')] + + try: + self.conn.modify_s(dn, mod) + except ldap.TYPE_OR_VALUE_EXISTS: + logging.debug("chainOnUpdate already enabled for %s" % self.suffix) + + + def setup_chain_on_update(self, other_conn): + chainbe = self.setup_chaining_backend(other_conn) + self.enable_chain_on_update(chainbe) + + + def agreement_dn(self, conn): + cn = "meTo%s%d" % (conn.host, PORT) + dn = "cn=%s, %s" % (cn, self.replica_dn()) + + return (cn, dn) + + + def setup_agreement(self, a, b): + cn, dn = self.agreement_dn(b) + try: + a.getEntry(dn, ldap.SCOPE_BASE) + return + except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND): + pass + + entry = ipaldap.Entry(dn) + entry.setValues('objectclass', "top", "nsds5replicationagreement") + entry.setValues('cn', cn) + entry.setValues('nsds5replicahost', b.host) + entry.setValues('nsds5replicaport', str(PORT)) + entry.setValues('nsds5replicatimeout', str(TIMEOUT)) + entry.setValues('nsds5replicabinddn', self.repl_man_dn) + entry.setValues('nsds5replicacredentials', self.repl_man_passwd) + entry.setValues('nsds5replicabindmethod', 'simple') + entry.setValues('nsds5replicaroot', self.suffix) + entry.setValues('nsds5replicaupdateschedule', '0000-2359 0123456') + entry.setValues('description', "me to %s%d" % (b.host, PORT)) + + a.add_s(entry) + + entry = a.waitForEntry(entry) + + + def check_repl_init(self, conn, agmtdn): + done = False + hasError = 0 + attrlist = ['cn', 'nsds5BeginReplicaRefresh', 'nsds5replicaUpdateInProgress', + 'nsds5ReplicaLastInitStatus', 'nsds5ReplicaLastInitStart', + 'nsds5ReplicaLastInitEnd'] + entry = conn.getEntry(agmtdn, ldap.SCOPE_BASE, "(objectclass=*)", attrlist) + if not entry: + print "Error reading status from agreement", agmtdn + hasError = 1 + else: + refresh = entry.nsds5BeginReplicaRefresh + inprogress = entry.nsds5replicaUpdateInProgress + status = entry.nsds5ReplicaLastInitStatus + if not refresh: # done - check status + if not status: + print "No status yet" + elif status.find("replica busy") > -1: + print "Update failed - replica busy - status", status + done = True + hasError = 2 + elif status.find("Total update succeeded") > -1: + print "Update succeeded" + done = True + elif inprogress.lower() == 'true': + print "Update in progress yet not in progress" + else: + print "Update failed: status", status + hasError = 1 + done = True + else: + print "Update in progress" + + return done, hasError + + + def wait_for_repl_init(self, conn, agmtdn): + done = False + haserror = 0 + while not done and not haserror: + time.sleep(1) # give it a few seconds to get going + done, haserror = self.check_repl_init(conn, agmtdn) + return haserror + + def start_replication(self, other_conn): + print "starting replication" + cn, dn = self.agreement_dn(self.conn) + + mod = [(ldap.MOD_ADD, 'nsds5BeginReplicaRefresh', 'start')] + other_conn.modify_s(dn, mod) + + return self.wait_for_repl_init(other_conn, dn) + + + def basic_replication_setup(self, conn, master, replica_id): + self.add_replication_manager(conn) + self.local_replica_config(conn, master, replica_id) + if master: + self.setup_changelog(conn) + + def setup_replication(self, other_hostname, realm_name, master=True): + """ + NOTES: + - the directory manager password needs to be the same on + both directories. + """ + other_conn = ipaldap.IPAdmin(other_hostname) + other_conn.do_simple_bind(bindpw=self.dirman_passwd) + self.suffix = ipaldap.IPAdmin.normalizeDN(dsinstance.realm_to_suffix(realm_name)) + + self.basic_replication_setup(self.conn, master, 1) + self.basic_replication_setup(other_conn, True, 2) + + self.setup_agreement(other_conn, self.conn) + if master: + self.setup_agreement(self.conn, other_conn) + else: + self.setup_chaining_farm(other_conn) + self.setup_chain_on_update(other_conn) + + return self.start_replication(other_conn) + + + diff --git a/ipa-server/xmlrpc-server/funcs.py b/ipa-server/xmlrpc-server/funcs.py index de9b265e3..04b053240 100644 --- a/ipa-server/xmlrpc-server/funcs.py +++ b/ipa-server/xmlrpc-server/funcs.py @@ -30,12 +30,15 @@ import xmlrpclib import copy import attrs from ipa import ipaerror +from urllib import quote,unquote from ipa import radius_util import string from types import * import os import re +import logging +import subprocess try: from threading import Lock @@ -48,6 +51,12 @@ _LDAPPool = None ACIContainer = "cn=accounts" DefaultUserContainer = "cn=users,cn=accounts" DefaultGroupContainer = "cn=groups,cn=accounts" +DefaultServiceContainer = "cn=services,cn=accounts" + +# FIXME: need to check the ipadebug option in ipa.conf +#logging.basicConfig(level=logging.DEBUG, +# format='%(asctime)s %(levelname)s %(message)s', +# stream=sys.stderr) # # Apache runs in multi-process mode so each process will have its own @@ -78,7 +87,10 @@ class IPAConnPool: conn = ipaserver.ipaldap.IPAdmin(host,port,None,None,None,debug) # This will bind the connection - conn.set_krbccache(krbccache, cprinc.name) + try: + conn.set_krbccache(krbccache, cprinc.name) + except ldap.UNWILLING_TO_PERFORM, e: + raise ipaerror.gen_exception(ipaerror.CONNECTION_UNWILLING) return conn @@ -418,17 +430,30 @@ class IPAServer: # FIXME: This should be dynamic and can include just about anything + # Get our configuration + config = self.get_ipa_config(opts) + # Let us add in some missing attributes if user.get('homedirectory') is None: - user['homedirectory'] = '/home/%s' % user.get('uid') + user['homedirectory'] = '%s/%s' % (config.get('ipahomesrootdir'), user.get('uid')) + user['homedirectory'] = user['homedirectory'].replace('//', '/') + user['homedirectory'] = user['homedirectory'].rstrip('/') + if user.get('loginshell') is None: + user['loginshell'] = config.get('ipadefaultloginshell') if user.get('gecos') is None: user['gecos'] = user['uid'] - # FIXME: This can be removed once the DS plugin is installed - user['uidnumber'] = '501' + # If uidnumber is blank the the FDS dna_plugin will automatically + # assign the next value. So we don't have to do anything with it. - # FIXME: What is the default group for users? - user['gidnumber'] = '501' + group_dn="cn=%s,%s,%s" % (config.get('ipadefaultprimarygroup'), DefaultGroupContainer, self.basedn) + try: + default_group = self.get_entry_by_dn(group_dn, ['dn','gidNumber'], opts) + if default_group: + user['gidnumber'] = default_group.get('gidnumber') + except ipaerror.exception_for(ipaerror.LDAP_DATABASE_ERROR): + # Fake an LDAP error so we can return something useful to the user + raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND, "No default group for new users can be found.") if user.get('krbprincipalname') is None: user['krbprincipalname'] = "%s@%s" % (user.get('uid'), self.realm) @@ -453,10 +478,40 @@ class IPAServer: conn = self.getConnection(opts) try: res = conn.addEntry(entry) + self.add_user_to_group(user.get('uid'), group_dn, opts) finally: self.releaseConnection(conn) return res + def get_custom_fields (self, opts=None): + """Get the list of custom user fields. + + A schema is a list of dict's of the form: + label: The label dispayed to the user + field: the attribute name + required: true/false + + It is displayed to the user in the order of the list. + """ + + config = self.get_ipa_config(opts) + + fields = config.get('ipacustomfields') + + if fields is None or fields == '': + return [] + + fl = fields.split('$') + schema = [] + for x in range(len(fl)): + vals = fl[x].split(',') + if len(vals) != 3: + # Raise? + print "Invalid field, skipping" + d = dict(label=unquote(vals[0]), field=unquote(vals[1]), required=unquote(vals[2])) + schema.append(d) + + return schema # radius support # clients @@ -696,6 +751,37 @@ class IPAServer: return fields + def set_custom_fields (self, schema, opts=None): + """Set the list of custom user fields. + + A schema is a list of dict's of the form: + label: The label dispayed to the user + field: the attribute name + required: true/false + + It is displayed to the user in the order of the list. + """ + config = self.get_ipa_config(opts) + + # The schema is stored as: + # label,field,required$label,field,required$... + # quote() from urilib is used to ensure that it is easy to unparse + + stored_schema = "" + for i in range(len(schema)): + entry = schema[i] + entry = quote(entry.get('label')) + "," + quote(entry.get('field')) + "," + quote(entry.get('required')) + + if stored_schema != "": + stored_schema = stored_schema + "$" + entry + else: + stored_schema = entry + + new_config = copy.deepcopy(config) + new_config['ipacustomfields'] = stored_schema + + return self.update_entry(config, new_config, opts) + def get_all_users (self, args=None, opts=None): """Return a list containing a User object for each existing user. @@ -714,18 +800,21 @@ class IPAServer: return users - def find_users (self, criteria, sattrs=None, searchlimit=0, timelimit=-1, + def find_users (self, criteria, sattrs=None, searchlimit=-1, timelimit=-1, opts=None): """Returns a list: counter followed by the results. If the results are truncated, counter will be set to -1.""" - # TODO - retrieve from config - timelimit = 2 + config = self.get_ipa_config(opts) + if timelimit < 0: + timelimit = float(config.get('ipasearchtimelimit')) + if searchlimit < 0: + searchlimit = float(config.get('ipasearchrecordslimit')) # Assume the list of fields to search will come from a central # configuration repository. A good format for that would be # a comma-separated list of fields - search_fields_conf_str = "uid,givenName,sn,telephoneNumber,ou,title" + search_fields_conf_str = config.get('ipausersearchfields') search_fields = string.split(search_fields_conf_str, ",") criteria = self.__safe_filter(criteria) @@ -797,29 +886,115 @@ class IPAServer: return new_dict def update_user (self, oldentry, newentry, opts=None): - """Thin wrapper around update_entry""" - return self.update_entry(oldentry, newentry, opts) + """Wrapper around update_entry with user-specific handling. - def mark_user_deleted (self, uid, opts=None): - """Mark a user as inactive in LDAP. We aren't actually deleting - users here, just making it so they can't log in, etc.""" - user = self.get_user_by_uid(uid, ['dn', 'uid', 'nsAccountlock'], opts) + If you want to change the RDN of a user you must use + this function. update_entry will fail. + """ - # Are we doing an add or replace operation? - if user.has_key('nsaccountlock'): - if user['nsaccountlock'] == "true": - return "already marked as deleted" - has_key = True - else: - has_key = False + newrdn = 0 + + if oldentry.get('uid') != newentry.get('uid'): + # RDN change + conn = self.getConnection(opts) + try: + res = conn.updateRDN(oldentry.get('dn'), "uid=" + newentry.get('uid')) + newdn = oldentry.get('dn') + newdn = newdn.replace("uid=%s" % oldentry.get('uid'), "uid=%s" % newentry.get('uid')) + + # Now fix up the dns and uids so they aren't seen as having + # changed. + oldentry['dn'] = newdn + newentry['dn'] = newdn + oldentry['uid'] = newentry['uid'] + newrdn = 1 + finally: + self.releaseConnection(conn) - conn = self.getConnection(opts) try: - res = conn.inactivateEntry(user['dn'], has_key) - finally: - self.releaseConnection(conn) + rv = self.update_entry(oldentry, newentry, opts) + return rv + except ipaerror.exception_for(ipaerror.LDAP_EMPTY_MODLIST): + # This means that there was just an rdn change, nothing else. + if newrdn == 1: + return "Success" + else: + raise + + def mark_entry_active (self, dn, opts=None): + """Mark an entry as active in LDAP.""" + + # This can be tricky. The entry itself can be marked inactive + # by being in the inactivated group. It can also be inactivated by + # being the member of an inactive group. + # + # First we try to remove the entry from the inactivated group. Then + # if it is still inactive we have to add it to the activated group + # which will override the group membership. + + logging.debug("IPA: activating entry %s" % dn) + + res = "" + # First, check the entry status + entry = self.get_entry_by_dn(dn, ['dn', 'nsAccountlock'], opts) + + if entry.get('nsaccountlock', 'false') == "false": + logging.debug("IPA: already active") + raise ipaerror.gen_exception(ipaerror.LDAP_EMPTY_MODLIST) + + group = self.get_entry_by_cn("inactivated", None, opts) + res = self.remove_member_from_group(entry.get('dn'), group.get('dn'), opts) + + # Now they aren't a member of inactivated directly, what is the status + # now? + entry = self.get_entry_by_dn(dn, ['dn', 'nsAccountlock'], opts) + + if entry.get('nsaccountlock', 'false') == "false": + # great, we're done + logging.debug("IPA: removing from inactivated did it.") + return res + + # So still inactive, add them to activated + group = self.get_entry_by_cn("activated", None, opts) + res = self.add_member_to_group(dn, group.get('dn'), opts) + logging.debug("IPA: added to activated.") + return res + def mark_entry_inactive (self, dn, opts=None): + """Mark an entry as inactive in LDAP.""" + + logging.debug("IPA: inactivating entry %s" % dn) + + entry = self.get_entry_by_dn(dn, ['dn', 'nsAccountlock', 'memberOf'], opts) + + if entry.get('nsaccountlock', 'false') == "true": + logging.debug("IPA: already marked as inactive") + raise ipaerror.gen_exception(ipaerror.LDAP_EMPTY_MODLIST) + + # First see if they are in the activated group as this will override + # the our inactivation. + group = self.get_entry_by_cn("activated", None, opts) + self.remove_member_from_group(dn, group.get('dn'), opts) + + # Now add them to inactivated + group = self.get_entry_by_cn("inactivated", None, opts) + res = self.add_member_to_group(dn, group.get('dn'), opts) + + return res + + def mark_user_active(self, uid, opts=None): + """Mark a user as active""" + + user = self.get_user_by_uid(uid, ['dn', 'uid'], opts) + return self.mark_entry_active(user.get('dn')) + + def mark_user_inactive(self, uid, opts=None): + """Mark a user as inactive""" + + user = self.get_user_by_uid(uid, ['dn', 'uid'], opts) + return self.mark_entry_inactive(user.get('dn')) + def delete_user (self, uid, opts=None): """Delete a user. Not to be confused with inactivate_user. This makes the entry go away completely. @@ -877,7 +1052,7 @@ class IPAServer: """ member_dn = self.__safe_filter(member_dn) - filter = "(&(objectClass=posixGroup)(uniqueMember=%s))" % member_dn + filter = "(&(objectClass=posixGroup)(member=%s))" % member_dn try: return self.__get_list(self.basedn, filter, sattrs, opts) @@ -900,12 +1075,11 @@ class IPAServer: entry = ipaserver.ipaldap.Entry(dn) # some required objectclasses - entry.setValues('objectClass', 'top', 'groupofuniquenames', 'posixGroup', + entry.setValues('objectClass', 'top', 'groupofnames', 'posixGroup', 'inetUser') - # FIXME, need a gidNumber generator - if group.get('gidnumber') is None: - entry.setValues('gidNumber', '501') + # No need to explicitly set gidNumber. The dna_plugin will do this + # for us if the value isn't provided by the user. # fill in our new entry with everything sent by the user for g in group: @@ -917,16 +1091,22 @@ class IPAServer: finally: self.releaseConnection(conn) - def find_groups (self, criteria, sattrs=None, searchlimit=0, timelimit=-1, + def find_groups (self, criteria, sattrs=None, searchlimit=-1, timelimit=-1, opts=None): """Return a list containing a User object for each existing group that matches the criteria. """ + config = self.get_ipa_config(opts) + if timelimit < 0: + timelimit = float(config.get('ipasearchtimelimit')) + if searchlimit < 0: + searchlimit = float(config.get('ipasearchrecordslimit')) + # Assume the list of fields to search will come from a central # configuration repository. A good format for that would be # a comma-separated list of fields - search_fields_conf_str = "cn,description" + search_fields_conf_str = config.get('ipagroupsearchfields') search_fields = string.split(search_fields_conf_str, ",") criteria = self.__safe_filter(criteria) @@ -1001,12 +1181,12 @@ class IPAServer: # check to make sure member_dn exists member_entry = self.__get_base_entry(member_dn, "(objectClass=*)", ['dn','uid'], opts) - if new_group.get('uniquemember') is not None: - if ((isinstance(new_group.get('uniquemember'), str)) or (isinstance(new_group.get('uniquemember'), unicode))): - new_group['uniquemember'] = [new_group['uniquemember']] - new_group['uniquemember'].append(member_dn) + if new_group.get('member') is not None: + if ((isinstance(new_group.get('member'), str)) or (isinstance(new_group.get('member'), unicode))): + new_group['member'] = [new_group['member']] + new_group['member'].append(member_dn) else: - new_group['uniquemember'] = member_dn + new_group['member'] = member_dn try: ret = self.__update_entry(old_group, new_group, opts) @@ -1045,11 +1225,11 @@ class IPAServer: raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND) new_group = copy.deepcopy(old_group) - if new_group.get('uniquemember') is not None: - if ((isinstance(new_group.get('uniquemember'), str)) or (isinstance(new_group.get('uniquemember'), unicode))): - new_group['uniquemember'] = [new_group['uniquemember']] + if new_group.get('member') is not None: + if ((isinstance(new_group.get('member'), str)) or (isinstance(new_group.get('member'), unicode))): + new_group['member'] = [new_group['member']] try: - new_group['uniquemember'].remove(member_dn) + new_group['member'].remove(member_dn) except ValueError: # member is not in the group # FIXME: raise more specific error? @@ -1198,8 +1378,56 @@ class IPAServer: return failed def update_group (self, oldentry, newentry, opts=None): - """Thin wrapper around update_entry""" - return self.update_entry(oldentry, newentry, opts) + """Wrapper around update_entry with group-specific handling. + + If you want to change the RDN of a group you must use + this function. update_entry will fail. + """ + + newrdn = 0 + + oldcn=oldentry.get('cn') + newcn=newentry.get('cn') + if isinstance(oldcn, str): + oldcn = [oldcn] + if isinstance(newcn, str): + newcn = [newcn] + + oldcn.sort() + newcn.sort() + if oldcn != newcn: + # RDN change + conn = self.getConnection(opts) + try: + res = conn.updateRDN(oldentry.get('dn'), "cn=" + newcn[0]) + newdn = oldentry.get('dn') + + # Ick. Need to find the exact cn used in the old DN so we'll + # walk the list of cns and skip the obviously bad ones: + for c in oldentry.get('dn').split("cn="): + if c and c != "groups" and not c.startswith("accounts"): + newdn = newdn.replace("cn=%s" % c, "uid=%s" % newentry.get('cn')[0]) + break + + # Now fix up the dns and cns so they aren't seen as having + # changed. + oldentry['dn'] = newdn + newentry['dn'] = newdn + oldentry['cn'] = newentry['cn'] + newrdn = 1 + finally: + self.releaseConnection(conn) + + try: + rv = self.update_entry(oldentry, newentry, opts) + return rv + except ipaerror.exception_for(ipaerror.LDAP_EMPTY_MODLIST): + if newrdn == 1: + # This means that there was just the rdn change, no other + # attributes + return "Success" + else: + raise def delete_group (self, group_dn, opts=None): """Delete a group @@ -1234,12 +1462,12 @@ class IPAServer: if group_dn is None: raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND) - if new_group.get('uniquemember') is not None: - if ((isinstance(new_group.get('uniquemember'), str)) or (isinstance(new_group.get('uniquemember'), unicode))): - new_group['uniquemember'] = [new_group['uniquemember']] - new_group['uniquemember'].append(group_dn['dn']) + if new_group.get('member') is not None: + if ((isinstance(new_group.get('member'), str)) or (isinstance(new_group.get('member'), unicode))): + new_group['member'] = [new_group['member']] + new_group['member'].append(group_dn['dn']) else: - new_group['uniquemember'] = group_dn['dn'] + new_group['member'] = group_dn['dn'] try: ret = self.__update_entry(old_group, new_group, opts) @@ -1261,10 +1489,10 @@ class IPAServer: """Do a memberOf search of groupdn and return the attributes in attr_list (an empty list returns everything).""" - # TODO - retrieve from config - timelimit = 2 + config = self.get_ipa_config(opts) + timelimit = float(config.get('ipasearchtimelimit')) - searchlimit = 0 + searchlimit = float(config.get('ipasearchrecordslimit')) groupdn = self.__safe_filter(groupdn) filter = "(memberOf=%s)" % groupdn @@ -1288,6 +1516,134 @@ class IPAServer: return entries + def mark_group_active(self, cn, opts=None): + """Mark a group as active""" + + group = self.get_entry_by_cn(cn, ['dn', 'cn'], opts) + return self.mark_entry_active(group.get('dn')) + + def mark_group_inactive(self, cn, opts=None): + """Mark a group as inactive""" + + group = self.get_entry_by_cn(cn, ['dn', 'uid'], opts) + return self.mark_entry_inactive(group.get('dn')) + + def __is_service_unique(self, name, opts): + """Return 1 if the uid is unique in the tree, 0 otherwise.""" + name = self.__safe_filter(name) + filter = "(&(krbprincipalname=%s)(objectclass=krbPrincipal))" % name + + try: + entry = self.__get_sub_entry(self.basedn, filter, ['dn','krbprincipalname'], opts) + return 0 + except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND): + return 1 + + def add_service_principal(self, name, opts=None): + service_container = DefaultServiceContainer + + princ_name = name + "@" + self.realm + + conn = self.getConnection(opts) + if self.__is_service_unique(name, opts) == 0: + raise ipaerror.gen_exception(ipaerror.LDAP_DUPLICATE) + + dn = "krbprincipalname=%s,%s,%s" % (ldap.dn.escape_dn_chars(princ_name), + service_container,self.basedn) + entry = ipaserver.ipaldap.Entry(dn) + + entry.setValues('objectclass', 'krbPrincipal', 'krbPrincipalAux', 'krbTicketPolicyAux') + entry.setValues('krbprincipalname', princ_name) + + try: + res = conn.addEntry(entry) + finally: + self.releaseConnection(conn) + return res + + + def get_keytab(self, name, opts=None): + """get a keytab""" + + princ_name = name + "@" + self.realm + + conn = self.getConnection(opts) + + if conn.principal != "admin@" + self.realm: + raise ipaerror.gen_exception(ipaerror.CONNECTION_GSSAPI_CREDENTIALS) + + try: + try: + princs = conn.getList(self.basedn, self.scope, "krbprincipalname=" + princ_name, None) + except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND): + return None + finally: + self.releaseConnection(conn) + + + # This is ugly - call out to a C wrapper around kadmin.local + p = subprocess.Popen(["/usr/sbin/ipa-keytab-util", princ_name, self.realm], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout,stderr = p.communicate() + + if p.returncode != 0: + return None + + return stdout + + + +# Configuration support + def get_ipa_config(self, opts=None): + """Retrieve the IPA configuration""" + try: + config = self.get_entry_by_cn("ipaconfig", None, opts) + except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND): + raise ipaerror.gen_exception(ipaerror.LDAP_NO_CONFIG) + + return config + + def update_ipa_config(self, oldconfig, newconfig, opts=None): + """Update the IPA configuration""" + + # The LDAP routines want strings, not ints, so convert a few + # things. Otherwise it sees a string -> int conversion as a change. + try: + newconfig['krbmaxpwdlife'] = str(newconfig.get('krbmaxpwdlife')) + newconfig['krbminpwdlife'] = str(newconfig.get('krbminpwdlife')) + newconfig['krbpwdmindiffchars'] = str(newconfig.get('krbpwdmindiffchars')) + newconfig['krbpwdminlength'] = str(newconfig.get('krbpwdminlength')) + newconfig['krbpwdhistorylength'] = str(newconfig.get('krbpwdhistorylength')) + except KeyError: + # These should all be there but if not, let things proceed + pass + return self.update_entry(oldconfig, newconfig, opts) + + def get_password_policy(self, opts=None): + """Retrieve the IPA password policy""" + try: + policy = self.get_entry_by_cn("accounts", None, opts) + except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND): + raise ipaerror.gen_exception(ipaerror.LDAP_NO_CONFIG) + + return policy + + def update_password_policy(self, oldpolicy, newpolicy, opts=None): + """Update the IPA configuration""" + + # The LDAP routines want strings, not ints, so convert a few + # things. Otherwise it sees a string -> int conversion as a change. + try: + newpolicy['krbmaxpwdlife'] = str(newpolicy.get('krbmaxpwdlife')) + newpolicy['krbminpwdlife'] = str(newpolicy.get('krbminpwdlife')) + newpolicy['krbpwdhistorylength'] = str(newpolicy.get('krbpwdhistorylength')) + newpolicy['krbpwdmindiffchars'] = str(newpolicy.get('krbpwdmindiffchars')) + newpolicy['krbpwdminlength'] = str(newpolicy.get('krbpwdminlength')) + except KeyError: + # These should all be there but if not, let things proceed + pass + + return self.update_entry(oldpolicy, newpolicy, opts) def ldap_search_escape(match): """Escapes out nasty characters from the ldap search. diff --git a/ipa-server/xmlrpc-server/ipa.conf b/ipa-server/xmlrpc-server/ipa.conf index 2931b86dd..fbf26b67c 100644 --- a/ipa-server/xmlrpc-server/ipa.conf +++ b/ipa-server/xmlrpc-server/ipa.conf @@ -2,12 +2,18 @@ ProxyRequests Off -# Make all requests use SSL except for Kerberos authentication errors RewriteEngine on +# Redirect to the fully-qualified hostname. Not redirecting to secure +# port so configuration files can be retrieved without requiring SSL. +RewriteCond %{HTTP_HOST} !^$FQDN$$ [NC] +RewriteRule ^/(.*) http://$FQDN/$$1 [L,R=301] + +# Redirect to the secure port if not displaying an error or retrieving +# configuration. RewriteCond %{SERVER_PORT} !^443$$ RewriteCond %{REQUEST_URI} !^/(errors|config)/ -RewriteRule ^/(.*) https://%{SERVER_NAME}/$$1 [L,R,NC] +RewriteRule ^/(.*) https://$FQDN/$$1 [L,R=301,NC] <Proxy *> AuthType Kerberos diff --git a/ipa-server/xmlrpc-server/ipaxmlrpc.py b/ipa-server/xmlrpc-server/ipaxmlrpc.py index ef48f4aa0..bda39932e 100644 --- a/ipa-server/xmlrpc-server/ipaxmlrpc.py +++ b/ipa-server/xmlrpc-server/ipaxmlrpc.py @@ -141,8 +141,8 @@ class ModXMLRPCRequestHandler(object): if req.subprocess_env.get("KRB5CCNAME") is not None: opts['krbccache'] = req.subprocess_env.get("KRB5CCNAME") else: - sys.stderr.write("IPA: did not receive a Kerberos credentials cache. Expect problems") - sys.stderr.flush() + response = dumps(Fault(5, "Did not receive Kerberos credentials.")) + return response if pythonopts.get("IPADebug"): opts['ipadebug'] = pythonopts.get("IPADebug") @@ -277,17 +277,17 @@ class ModXMLRPCRequestHandler(object): def handle_request(self,req): """Handle a single XML-RPC request""" - # The LDAP connection pool is not thread-safe. Avoid problems and - # force the forked model for now. - if not apache.mpm_query(apache.AP_MPMQ_IS_FORKED): - raise Fault(3, "Apache must use the forked model") - # XMLRPC uses POST only. Reject anything else if req.method != 'POST': req.allow_methods(['POST'],1) raise apache.SERVER_RETURN, apache.HTTP_METHOD_NOT_ALLOWED - response = self._marshaled_dispatch(req.read(), req) + # The LDAP connection pool is not thread-safe. Avoid problems and + # force the forked model for now. + if apache.mpm_query(apache.AP_MPMQ_IS_THREADED): + response = dumps(Fault(3, "Apache must use the forked model")) + else: + response = self._marshaled_dispatch(req.read(), req) req.content_type = "text/xml" req.set_content_length(len(response)) @@ -326,12 +326,16 @@ def handler(req, profiling=False): h.register_function(f.get_user_by_email) h.register_function(f.get_users_by_manager) h.register_function(f.add_user) - h.register_function(f.get_add_schema) + h.register_function(f.get_custom_fields) + h.register_function(f.set_custom_fields) h.register_function(f.get_all_users) h.register_function(f.find_users) h.register_function(f.update_user) h.register_function(f.delete_user) - h.register_function(f.mark_user_deleted) + h.register_function(f.mark_user_active) + h.register_function(f.mark_user_inactive) + h.register_function(f.mark_group_active) + h.register_function(f.mark_group_inactive) h.register_function(f.modifyPassword) h.register_function(f.get_groups_by_member) h.register_function(f.add_group) @@ -351,6 +355,12 @@ def handler(req, profiling=False): h.register_function(f.delete_group) h.register_function(f.attrs_to_labels) h.register_function(f.group_members) + h.register_function(f.get_ipa_config) + h.register_function(f.update_ipa_config) + h.register_function(f.get_password_policy) + h.register_function(f.update_password_policy) + h.register_function(f.add_service_principal) + h.register_function(f.get_keytab) h.register_function(f.get_radius_client_by_ip_addr) h.register_function(f.add_radius_client) h.register_function(f.update_radius_client) diff --git a/ipa-server/xmlrpc-server/unauthorized.html b/ipa-server/xmlrpc-server/unauthorized.html index 98e037e58..23a8d5c7d 100644 --- a/ipa-server/xmlrpc-server/unauthorized.html +++ b/ipa-server/xmlrpc-server/unauthorized.html @@ -7,7 +7,7 @@ Unable to verify your Kerberos credentials. Please make sure that you have valid Kerberos tickets (obtainable via kinit), and that you have <a href="/errors/ssbrowser.html">configured your browser correctly</a>. If you are still unable to access -the idm wiki, please contact the helpdesk on for additional assistance. +the IPA Web interface, please contact the helpdesk on for additional assistance. </p> </ul> </body> |