summaryrefslogtreecommitdiffstats
path: root/ipa-server
diff options
context:
space:
mode:
authorJohn Dennis <jdennis@redhat.com>2007-11-28 07:49:07 -0500
committerJohn Dennis <jdennis@redhat.com>2007-11-28 07:49:07 -0500
commit904b76059cec667a9c155021c8e33ce1dbf2b389 (patch)
treec2f9d8ed6a2f84427dd494d3814cac77c29a34f0 /ipa-server
parentc939c5d289daaf4c855caa2a6816e7eeba7e2661 (diff)
parent2e7f629d913d775cfb285ede166d7a0f977782fe (diff)
downloadfreeipa-904b76059cec667a9c155021c8e33ce1dbf2b389.tar.gz
freeipa-904b76059cec667a9c155021c8e33ce1dbf2b389.tar.xz
freeipa-904b76059cec667a9c155021c8e33ce1dbf2b389.zip
merged radius work with latest mainline tip
Diffstat (limited to 'ipa-server')
-rw-r--r--ipa-server/Makefile.am1
-rw-r--r--ipa-server/configure.ac7
-rw-r--r--ipa-server/ipa-gui/README.multivalue27
-rw-r--r--ipa-server/ipa-gui/ipagui/controllers.py4
-rw-r--r--ipa-server/ipa-gui/ipagui/forms/Makefile.am3
-rw-r--r--ipa-server/ipa-gui/ipagui/forms/delegate.py2
-rw-r--r--ipa-server/ipa-gui/ipagui/forms/group.py9
-rw-r--r--ipa-server/ipa-gui/ipagui/forms/ipapolicy.py59
-rw-r--r--ipa-server/ipa-gui/ipagui/forms/user.py11
-rw-r--r--ipa-server/ipa-gui/ipagui/helpers/userhelper.py2
-rw-r--r--ipa-server/ipa-gui/ipagui/proxyprovider.py39
-rw-r--r--ipa-server/ipa-gui/ipagui/static/css/style.css19
-rw-r--r--ipa-server/ipa-gui/ipagui/subcontrollers/Makefile.am2
-rw-r--r--ipa-server/ipa-gui/ipagui/subcontrollers/delegation.py45
-rw-r--r--ipa-server/ipa-gui/ipagui/subcontrollers/group.py84
-rw-r--r--ipa-server/ipa-gui/ipagui/subcontrollers/ipapolicy.py168
-rw-r--r--ipa-server/ipa-gui/ipagui/subcontrollers/policy.py32
-rw-r--r--ipa-server/ipa-gui/ipagui/subcontrollers/user.py190
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/Makefile.am5
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/groupeditform.kid25
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/grouplist.kid12
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/groupshow.kid23
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/ipapolicyedit.kid15
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/ipapolicyeditform.kid176
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/ipapolicyshow.kid120
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/loginfailed.kid43
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/master.kid12
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/policyindex.kid31
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/policylayout.kid17
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/useredit.kid4
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/usereditform.kid229
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/userlayout.kid4
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/userlist.kid8
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/usernew.kid4
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/usernewform.kid281
-rw-r--r--ipa-server/ipa-gui/ipagui/templates/usershow.kid112
-rw-r--r--ipa-server/ipa-install/Makefile.am2
-rw-r--r--ipa-server/ipa-install/ipa-replica-install142
-rw-r--r--ipa-server/ipa-install/ipa-replica-prepare114
-rw-r--r--ipa-server/ipa-install/ipa-server-install172
-rw-r--r--ipa-server/ipa-install/share/60ipaconfig.ldif37
-rw-r--r--ipa-server/ipa-install/share/Makefile.am2
-rw-r--r--ipa-server/ipa-install/share/bootstrap-template.ldif84
-rw-r--r--ipa-server/ipa-install/share/default-aci.ldif19
-rw-r--r--ipa-server/ipa-install/share/indeces.ldif8
-rw-r--r--ipa-server/ipa-install/share/kerberos.ldif18
-rw-r--r--ipa-server/ipa-install/share/memberof-task.ldif7
-rw-r--r--ipa-server/ipa-keytab-util/Makefile.am22
-rw-r--r--ipa-server/ipa-keytab-util/ipa-keytab-util.c304
-rw-r--r--ipa-server/ipa-kpasswd/ipa_kpasswd.c349
-rwxr-xr-xipa-server/ipa-server.spec (renamed from ipa-server/freeipa-server.spec)57
-rw-r--r--ipa-server/ipa-server.spec.in (renamed from ipa-server/freeipa-server.spec.in)55
-rw-r--r--ipa-server/ipa-slapi-plugins/ipa-memberof/ipa-memberof.c2
-rw-r--r--ipa-server/ipa-slapi-plugins/ipa-pwd-extop/ipa_pwd_extop.c1138
-rw-r--r--ipa-server/ipa-slapi-plugins/ipa-pwd-extop/pwd-extop-conf.ldif3
-rw-r--r--ipa-server/ipaserver/Makefile.am2
-rw-r--r--ipa-server/ipaserver/dsinstance.py112
-rw-r--r--ipa-server/ipaserver/httpinstance.py9
-rw-r--r--ipa-server/ipaserver/installutils.py108
-rw-r--r--ipa-server/ipaserver/ipaldap.py118
-rw-r--r--ipa-server/ipaserver/krbinstance.py228
-rw-r--r--ipa-server/ipaserver/radiusinstance.py4
-rw-r--r--ipa-server/ipaserver/replication.py316
-rw-r--r--ipa-server/xmlrpc-server/funcs.py462
-rw-r--r--ipa-server/xmlrpc-server/ipa.conf10
-rw-r--r--ipa-server/xmlrpc-server/ipaxmlrpc.py30
-rw-r--r--ipa-server/xmlrpc-server/unauthorized.html2
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>
+ &nbsp; <!-- 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>
+ &nbsp; <!-- 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) &gt; 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>