diff options
author | Karl MacMillan <kmacmill@redhat.com> | 2007-12-06 17:21:48 -0500 |
---|---|---|
committer | Karl MacMillan <kmacmill@redhat.com> | 2007-12-06 17:21:48 -0500 |
commit | ca118de76cb036acb31eae41970b962497d18838 (patch) | |
tree | 90f82e999620ce23e944564d4a7e3e4f94cd9e72 | |
parent | cd93c81a13aefa75c778ea641c67c505a79ac8c1 (diff) | |
parent | 86d80f12ca6c0add3d19e8351633dbcfe3a62e9e (diff) | |
download | freeipa.git-ca118de76cb036acb31eae41970b962497d18838.tar.gz freeipa.git-ca118de76cb036acb31eae41970b962497d18838.tar.xz freeipa.git-ca118de76cb036acb31eae41970b962497d18838.zip |
Merge.
31 files changed, 866 insertions, 122 deletions
diff --git a/ipa-admintools/ipa-findgroup b/ipa-admintools/ipa-findgroup index 9f809aa0..d84a2c62 100644 --- a/ipa-admintools/ipa-findgroup +++ b/ipa-admintools/ipa-findgroup @@ -65,6 +65,9 @@ def main(): if counter == 0: print "No entries found for", args[1] return 2 + elif counter == -1: + print "These results are truncated." + print "Please revine your search and try again." for ent in groups: try: diff --git a/ipa-admintools/ipa-finduser b/ipa-admintools/ipa-finduser index 6dc4d56c..81e8898a 100644 --- a/ipa-admintools/ipa-finduser +++ b/ipa-admintools/ipa-finduser @@ -90,6 +90,9 @@ def main(): if counter == 0: print "No entries found for", args[1] return 2 + elif counter == -1: + print "These results are truncated." + print "Please revine your search and try again." for ent in users: attr = ent.attrList() diff --git a/ipa-client/ipa-install/ipa-client-install b/ipa-client/ipa-install/ipa-client-install index fccdac3b..bc8e7c9c 100644 --- a/ipa-client/ipa-install/ipa-client-install +++ b/ipa-client/ipa-install/ipa-client-install @@ -131,8 +131,10 @@ def main(): opts = [{'name':'comment', 'type':'comment', 'value':'File modified by ipa-client-install'}, {'name':'empty', 'type':'empty'}] - defopts.append({'name':'server', 'type':'option', 'value':ds.getServerName()}) - defopts.append({'name':'realm', 'type':'option', 'value':ds.getRealmName()}) + + #[defaults] + defopts = [{'name':'server', 'type':'option', 'value':ds.getServerName()}, + {'name':'realm', 'type':'option', 'value':ds.getRealmName()}] opts.append({'name':'defaults', 'type':'section', 'value':defopts}) opts.append({'name':'empty', 'type':'empty'}) diff --git a/ipa-python/ipaclient.py b/ipa-python/ipaclient.py index c551f043..426f6681 100644 --- a/ipa-python/ipaclient.py +++ b/ipa-python/ipaclient.py @@ -116,9 +116,6 @@ class IPAClient: user_dict = user.toDict() - # dn is set on the server-side - del user_dict['dn'] - # convert to a regular dict before sending result = self.transport.add_user(user_dict, user_container) return result @@ -385,6 +382,20 @@ class IPAClient: def add_service_principal(self, princ_name): return self.transport.add_service_principal(princ_name) + def find_service_principal(self, criteria, sattrs=None, searchlimit=0, timelimit=-1): + """Return a list: counter followed by a Entity object for each host that + matches the criteria. If the results are truncated, counter will + be set to -1""" + result = self.transport.find_service_principal(criteria, sattrs, searchlimit, timelimit) + counter = result[0] + + hosts = [counter] + for attrs in result[1:]: + if attrs is not None: + hosts.append(entity.Entity(attrs)) + + return hosts + def get_keytab(self, princ_name): return self.transport.get_keytab(princ_name) diff --git a/ipa-python/ipaerror.py b/ipa-python/ipaerror.py index 2f9a9836..e3496336 100644 --- a/ipa-python/ipaerror.py +++ b/ipa-python/ipaerror.py @@ -177,3 +177,8 @@ CONFIG_DEFAULT_GROUP = gen_error_code( CONFIGURATION_CATEGORY, 0x0002, "You cannot remove the default users group.") + +CONFIG_INVALID_OC = gen_error_code( + CONFIGURATION_CATEGORY, + 0x0003, + "Invalid object class.") diff --git a/ipa-python/ipautil.py b/ipa-python/ipautil.py index 9b8412d2..2dc9b0c9 100644 --- a/ipa-python/ipautil.py +++ b/ipa-python/ipautil.py @@ -27,9 +27,7 @@ from random import Random from time import gmtime import os import stat -import socket -from string import lower import re import xmlrpclib import datetime @@ -79,7 +77,7 @@ def run(args, stdin=None): logging.info(stderr) if p.returncode != 0: - raise self.CalledProcessError(p.returncode, ' '.join(args)) + raise CalledProcessError(p.returncode, ' '.join(args)) def file_exists(filename): try: @@ -118,24 +116,24 @@ class CIDict(dict): self.update(default or {}) def __getitem__(self,key): - return super(CIDict,self).__getitem__(lower(key)) + return super(CIDict,self).__getitem__(string.lower(key)) def __setitem__(self,key,value): - lower_key = lower(key) + lower_key = string.lower(key) self._keys[lower_key] = key - return super(CIDict,self).__setitem__(lower(key),value) + return super(CIDict,self).__setitem__(string.lower(key),value) def __delitem__(self,key): - lower_key = lower(key) + lower_key = string.lower(key) del self._keys[lower_key] - return super(CIDict,self).__delitem__(lower(key)) + return super(CIDict,self).__delitem__(string.lower(key)) def update(self,dict): for key in dict.keys(): self[key] = dict[key] def has_key(self,key): - return super(CIDict, self).has_key(lower(key)) + return super(CIDict, self).has_key(string.lower(key)) def get(self,key,failobj=None): try: diff --git a/ipa-python/rpcclient.py b/ipa-python/rpcclient.py index d7ff9740..de32e9be 100644 --- a/ipa-python/rpcclient.py +++ b/ipa-python/rpcclient.py @@ -703,6 +703,24 @@ class RPCClient: return ipautil.unwrap_binary_data(result) + def find_service_principal (self, criteria, sattrs=None, searchlimit=0, timelimit=-1): + """Return a list: counter followed by a Entity object for each host that + matches the criteria. If the results are truncated, counter will + be set to -1""" + + server = self.setup_server() + try: + # None values are not allowed in XML-RPC + if sattrs is None: + sattrs = "__NONE__" + result = server.find_service_principal(criteria, sattrs, searchlimit, timelimit) + except xmlrpclib.Fault, fault: + raise ipaerror.gen_exception(fault.faultCode, fault.faultString) + except socket.error, (value, msg): + raise xmlrpclib.Fault(value, msg) + + return ipautil.unwrap_binary_data(result) + def get_keytab(self, princ_name): server = self.setup_server() diff --git a/ipa-server/ipa-gui/ipagui/controllers.py b/ipa-server/ipa-gui/ipagui/controllers.py index d1ee22e0..70a29246 100644 --- a/ipa-server/ipa-gui/ipagui/controllers.py +++ b/ipa-server/ipa-gui/ipagui/controllers.py @@ -19,6 +19,7 @@ from subcontrollers.group import GroupController from subcontrollers.delegation import DelegationController from subcontrollers.policy import PolicyController from subcontrollers.ipapolicy import IPAPolicyController +from subcontrollers.principal import PrincipalController ipa.config.init_config() @@ -31,6 +32,7 @@ class Root(controllers.RootController): delegate = DelegationController() policy = PolicyController() ipapolicy = IPAPolicyController() + principal = PrincipalController() @expose(template="ipagui.templates.welcome") @identity.require(identity.not_anonymous()) diff --git a/ipa-server/ipa-gui/ipagui/forms/ipapolicy.py b/ipa-server/ipa-gui/ipagui/forms/ipapolicy.py index 6cd967a9..1d48f8f3 100644 --- a/ipa-server/ipa-gui/ipagui/forms/ipapolicy.py +++ b/ipa-server/ipa-gui/ipagui/forms/ipapolicy.py @@ -1,5 +1,6 @@ import turbogears from turbogears import validators, widgets +from tg_expanding_form_widget.tg_expanding_form_widget import ExpandingForm class IPAPolicyFields(object): # From cn=ipaConfig @@ -12,12 +13,16 @@ class IPAPolicyFields(object): 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)) + ipauserobjectclasses = widgets.TextField(name="ipauserobjectclasses", label="Default User Object Classes", attrs=dict(size=50)) + userobjectclasses = ExpandingForm(name="userobjectclasses", label="Default User Object Classes", fields=[ipauserobjectclasses]) + ipagroupobjectclasses = widgets.TextField(name="ipagroupobjectclasses", label="Default Group Object Classes", attrs=dict(size=50)) + groupobjectclasses = ExpandingForm(name="groupobjectclasses", label="Default User Object Classes", fields=[ipagroupobjectclasses]) 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)) + krbmaxpwdlife = widgets.TextField(name="krbmaxpwdlife", label="Max. Password Lifetime (days)", attrs=dict(size=3,maxlength=3)) + krbminpwdlife = widgets.TextField(name="krbminpwdlife", label="Min. Password Lifetime (hours)", 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)) @@ -34,6 +39,9 @@ class IPAPolicyValidator(validators.Schema): ipahomesrootdir = validators.String(not_empty=True) ipadefaultloginshell = validators.String(not_empty=True) ipadefaultprimarygroup = validators.String(not_empty=True) + ipauserobjectclasses = validators.ForEach(validators.String(not_empty=True)) + ipagroupobjectclasses = validators.ForEach(validators.String(not_empty=True)) + krbmaxpwdlife = validators.Number(not_empty=True) krbminpwdlife = validators.Number(not_empty=True) krbpwdmindiffchars = validators.Number(not_empty=True) diff --git a/ipa-server/ipa-gui/ipagui/forms/principal.py b/ipa-server/ipa-gui/ipagui/forms/principal.py new file mode 100644 index 00000000..a830c8a3 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/forms/principal.py @@ -0,0 +1,39 @@ +import turbogears +from turbogears import validators, widgets +from tg_expanding_form_widget.tg_expanding_form_widget import ExpandingForm + +class PrincipalFields(object): + hostname = widgets.TextField(name="hostname", label="Host Name") + service = widgets.SingleSelectField(name="service", + label="Service Type", + options = [ + ("cifs", "cifs"), + ("dhcp", "dhcp"), + ("dns", "dns"), + ("host", "host"), + ("HTTP", "HTTP"), + ("ldap", "ldap"), + ("other", "other"), + ("rpc", "rpc"), + ("snmp", "snmp") + ], + attrs=dict(onchange="toggleOther(this.id)")) + other = widgets.TextField(name="other", label="Other Service", attrs=dict(size=10)) + +class PrincipalNewValidator(validators.Schema): + hostname = validators.String(not_empty=True) + service = validators.String(not_empty=True) + other = validators.String(not_empty=False) + +class PrincipalNewForm(widgets.Form): + params = ['principal_fields'] + + validator = PrincipalNewValidator() + + def __init__(self, *args, **kw): + super(PrincipalNewForm,self).__init__(*args, **kw) + (self.template_c, self.template) = widgets.meta.load_kid_template("ipagui.templates.principalnewform") + self.principal_fields = PrincipalFields + + def update_params(self, params): + super(PrincipalNewForm,self).update_params(params) diff --git a/ipa-server/ipa-gui/ipagui/helpers/ipahelper.py b/ipa-server/ipa-gui/ipagui/helpers/ipahelper.py index 9ea6b48a..e5c2bd37 100644 --- a/ipa-server/ipa-gui/ipagui/helpers/ipahelper.py +++ b/ipa-server/ipa-gui/ipagui/helpers/ipahelper.py @@ -7,3 +7,34 @@ def javascript_string_escape(input): return re.sub(r'[\'\"\\]', lambda match: "\\%s" % match.group(), input) + +def setup_mv_fields(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: + for v in field: + if v: + mvlist.append({ fieldname : v } ) + if len(mvlist) == 0: + # 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(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 diff --git a/ipa-server/ipa-gui/ipagui/proxyprovider.py b/ipa-server/ipa-gui/ipagui/proxyprovider.py index bd9cf87a..485a0f3b 100644 --- a/ipa-server/ipa-gui/ipagui/proxyprovider.py +++ b/ipa-server/ipa-gui/ipagui/proxyprovider.py @@ -83,7 +83,7 @@ class ProxyIdentity(object): return self._user.groups except AttributeError: # Groups haven't been computed yet - return None + return [] groups= property(_get_groups) def logout(self): diff --git a/ipa-server/ipa-gui/ipagui/subcontrollers/ipapolicy.py b/ipa-server/ipa-gui/ipagui/subcontrollers/ipapolicy.py index a82b9888..d8237331 100644 --- a/ipa-server/ipa-gui/ipagui/subcontrollers/ipapolicy.py +++ b/ipa-server/ipa-gui/ipagui/subcontrollers/ipapolicy.py @@ -17,6 +17,7 @@ from ipa.entity import utf8_encode_values from ipa import ipaerror import ipa.entity import ipagui.forms.ipapolicy +from ipagui.helpers import ipahelper import ldap.dn @@ -71,6 +72,15 @@ class IPAPolicyController(IPAController): # Combine the 2 dicts to make the form easier ipapolicy_dict.update(password_dict) + # Load potential multi-valued fields + if isinstance(ipapolicy_dict.get('ipauserobjectclasses',''), str): + ipapolicy_dict['ipauserobjectclasses'] = [ipapolicy_dict.get('ipauserobjectclasses')] + ipapolicy_dict['userobjectclasses'] = ipahelper.setup_mv_fields(ipapolicy_dict.get('ipauserobjectclasses'), 'ipauserobjectclasses') + + if isinstance(ipapolicy_dict.get('ipagroupobjectclasses',''), str): + ipapolicy_dict['ipagroupobjectclasses'] = [ipapolicy_dict.get('ipagroupobjectclasses')] + ipapolicy_dict['groupobjectclasses'] = ipahelper.setup_mv_fields(ipapolicy_dict.get('ipagroupobjectclasses'), 'ipagroupobjectclasses') + 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)) @@ -88,6 +98,10 @@ class IPAPolicyController(IPAController): turbogears.flash("Edit policy cancelled") raise turbogears.redirect('/ipapolicy/show') + # Fix incoming multi-valued fields we created for the form + kw = ipahelper.fix_incoming_fields(kw, 'ipauserobjectclasses', 'userobjectclasses') + kw = ipahelper.fix_incoming_fields(kw, 'ipagroupobjectclasses', 'groupobjectclasses') + tg_errors, kw = self.ipapolicyupdatevalidate(**kw) if tg_errors: turbogears.flash("There were validation errors.<br/>" + @@ -132,6 +146,12 @@ class IPAPolicyController(IPAController): if new_ipapolicy.ipadefaultprimarygroup != kw.get('ipadefaultprimarygroup'): policy_modified = True new_ipapolicy.setValue('ipadefaultprimarygroup', kw.get('ipadefaultprimarygroup')) + if new_ipapolicy.ipauserobjectclasses != kw.get('ipauserobjectclasses'): + policy_modified = True + new_ipapolicy.setValue('ipauserobjectclasses', kw.get('ipauserobjectclasses')) + if new_ipapolicy.ipagroupobjectclasses != kw.get('ipagroupobjectclasses'): + policy_modified = True + new_ipapolicy.setValue('ipagroupobjectclasses', kw.get('ipagroupobjectclasses')) if policy_modified: rv = client.update_ipa_config(new_ipapolicy) diff --git a/ipa-server/ipa-gui/ipagui/subcontrollers/principal.py b/ipa-server/ipa-gui/ipagui/subcontrollers/principal.py new file mode 100644 index 00000000..1b2ad694 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/subcontrollers/principal.py @@ -0,0 +1,153 @@ +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 ipagui.forms.principal + +import ldap.dn + +log = logging.getLogger(__name__) + +principal_new_form = ipagui.forms.principal.PrincipalNewForm() +principal_fields = ['*'] + +class PrincipalController(IPAController): + + @expose() + @identity.require(identity.in_group("admins")) + def index(self, tg_errors=None): + raise turbogears.redirect("/principal/list") + + @expose("ipagui.templates.principalnew") + @identity.require(identity.in_group("admins")) + def new(self, tg_errors=None): + """Displays the new service principal form""" + if tg_errors: + turbogears.flash("There were validation errors.<br/>" + + "Please see the messages below for details.") + + client = self.get_ipaclient() + + return dict(form=principal_new_form, principal={}) + + @expose() + @identity.require(identity.in_group("admins")) + def create(self, **kw): + """Creates a service principal group""" + self.restrict_post() + client = self.get_ipaclient() + + if kw.get('submit') == 'Cancel': + turbogears.flash("Add principal cancelled") + raise turbogears.redirect('/') + + tg_errors, kw = self.principalcreatevalidate(**kw) + if tg_errors: + turbogears.flash("There were validation errors.<br/>" + + "Please see the messages below for details.") + return dict(form=principal_new_form, principal=kw, + tg_template='ipagui.templates.principalnew') + + principal_name = "" + hostname = kw.get('hostname') + # + # Create the principal itself + # + try: + if kw.get('service') == "other": + service = kw.get('other') + if not service: + turbogears.flash("Service type must be provided") + return dict(form=principal_new_form, principal=kw, + tg_template='ipagui.templates.principalnew') + else: + service = kw.get('service') + + # The realm is added by add_service_principal + principal_name = utf8_encode_values(service + "/" + kw.get('hostname')) + + rv = client.add_service_principal(principal_name) + except ipaerror.exception_for(ipaerror.LDAP_DUPLICATE): + turbogears.flash("Service principal '%s' already exists" % + principal_name) + return dict(form=principal_new_form, principal=kw, + tg_template='ipagui.templates.principalnew') + except ipaerror.IPAError, e: + turbogears.flash("Service principal add failed: " + str(e) + "<br/>" + e.detail[0]['desc']) + return dict(form=principal_new_form, principal=kw, + tg_template='ipagui.templates.principalnew') + + turbogears.flash("%s added!" % principal_name) + raise turbogears.redirect('/principal/list', hostname=hostname) + + @expose("ipagui.templates.principallist") + @identity.require(identity.not_anonymous()) + def list(self, **kw): + """Searches for service principals and displays list of results""" + client = self.get_ipaclient() + + principals = None + counter = 0 + hostname = kw.get('hostname') + if hostname != None and len(hostname) > 0: + try: + principals = client.find_service_principal(hostname.encode('utf-8'), principal_fields, 0, 2) + counter = principals[0] + principals = principals[1:] + + if counter == -1: + turbogears.flash("These results are truncated.<br />" + + "Please refine your search and try again.") + + # For each entry break out service type and hostname + for i in range(len(principals)): + (service,host) = principals[i].krbprincipalname.split('/') + h = host.split('@') + principals[i].setValue('service', service) + principals[i].setValue('hostname', h[0]) + + except ipaerror.IPAError, e: + turbogears.flash("principal list failed: " + str(e) + "<br/>" + e.detail[0]['desc']) + raise turbogears.redirect("/principal/list") + + return dict(principals=principals, hostname=hostname, fields=ipagui.forms.principal.PrincipalFields()) + + @expose() + @identity.require(identity.not_anonymous()) + def show(self, **kw): + """Returns the keytab for a given principal""" + client = self.get_ipaclient() + + principal = kw.get('principal') + if principal != None and len(principal) > 0: + try: + p = principal.split('@') + keytab = client.get_keytab(p[0].encode('utf-8')) + + cherrypy.response.headers['Content-Type'] = "application/x-download" + cherrypy.response.headers['Content-Disposition'] = 'attachment; filename=krb5.keytab' + cherrypy.response.headers['Content-Length'] = len(keytab) + cherrypy.response.body = keytab + return cherrypy.response.body + except ipaerror.IPAError, e: + turbogears.flash("keytab retrieval failed: " + str(e) + "<br/>" + e.detail[0]['desc']) + raise turbogears.redirect("/principal/list") + raise turbogears.redirect("/principal/list") + + @validate(form=principal_new_form) + @identity.require(identity.not_anonymous()) + def principalcreatevalidate(self, tg_errors=None, **kw): + return tg_errors, kw diff --git a/ipa-server/ipa-gui/ipagui/subcontrollers/user.py b/ipa-server/ipa-gui/ipagui/subcontrollers/user.py index 39343b59..eda0966b 100644 --- a/ipa-server/ipa-gui/ipagui/subcontrollers/user.py +++ b/ipa-server/ipa-gui/ipagui/subcontrollers/user.py @@ -18,6 +18,7 @@ from ipa.entity import utf8_encode_values from ipa import ipaerror import ipagui.forms.user import ipa.config +from ipagui.helpers import ipahelper log = logging.getLogger(__name__) @@ -83,36 +84,6 @@ 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") @@ -142,12 +113,12 @@ class UserController(IPAController): 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') + kw = ipahelper.fix_incoming_fields(kw, 'cn', 'cns') + kw = ipahelper.fix_incoming_fields(kw, 'telephonenumber', 'telephonenumbers') + kw = ipahelper.fix_incoming_fields(kw, 'facsimiletelephonenumber', 'facsimiletelephonenumbers') + kw = ipahelper.fix_incoming_fields(kw, 'mobile', 'mobiles') + kw = ipahelper.fix_incoming_fields(kw, 'pager', 'pagers') + kw = ipahelper.fix_incoming_fields(kw, 'homephone', 'homephones') if tg_errors: turbogears.flash("There were validation errors.<br/>" + @@ -325,27 +296,27 @@ class UserController(IPAController): # 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') + user_dict['cns'] = ipahelper.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') + user_dict['telephonenumber'] = [user_dict.get('telephonenumber')] + user_dict['telephonenumbers'] = ipahelper.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') + user_dict['facsimiletelephonenumber'] = [user_dict.get('facsimiletelephonenumber')] + user_dict['facsimiletelephonenumbers'] = ipahelper.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') + user_dict['mobile'] = [user_dict.get('mobile')] + user_dict['mobiles'] = ipahelper.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') + user_dict['pager'] = [user_dict.get('pager')] + user_dict['pagers'] = ipahelper.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') + user_dict['homephone'] = [user_dict.get('homephone')] + user_dict['homephones'] = ipahelper.setup_mv_fields(user_dict.get('homephone'), 'homephone') # Edit shouldn't fill in the password field. if user_dict.has_key('userpassword'): @@ -403,12 +374,12 @@ class UserController(IPAController): 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') + kw = ipahelper.fix_incoming_fields(kw, 'cn', 'cns') + kw = ipahelper.fix_incoming_fields(kw, 'telephonenumber', 'telephonenumbers') + kw = ipahelper.fix_incoming_fields(kw, 'facsimiletelephonenumber', 'facsimiletelephonenumbers') + kw = ipahelper.fix_incoming_fields(kw, 'mobile', 'mobiles') + kw = ipahelper.fix_incoming_fields(kw, 'pager', 'pagers') + kw = ipahelper.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 diff --git a/ipa-server/ipa-gui/ipagui/templates/delegateform.kid b/ipa-server/ipa-gui/ipagui/templates/delegateform.kid index 62cc710f..4eb846d5 100644 --- a/ipa-server/ipa-gui/ipagui/templates/delegateform.kid +++ b/ipa-server/ipa-gui/ipagui/templates/delegateform.kid @@ -180,4 +180,10 @@ </script> </form> + + + <script type="text/javascript"> + document.getElementById("form_name").focus(); + </script> + </div> diff --git a/ipa-server/ipa-gui/ipagui/templates/groupnewform.kid b/ipa-server/ipa-gui/ipagui/templates/groupnewform.kid index 2b6e2ebb..04a9a9e7 100644 --- a/ipa-server/ipa-gui/ipagui/templates/groupnewform.kid +++ b/ipa-server/ipa-gui/ipagui/templates/groupnewform.kid @@ -98,6 +98,10 @@ from ipagui.helpers import ipahelper </form> <script type="text/javascript"> + document.getElementById("form_cn").focus(); + </script> + + <script type="text/javascript"> /* * This section restores the contents of the add and remove lists * dynamically if we have to refresh the page diff --git a/ipa-server/ipa-gui/ipagui/templates/ipapolicyeditform.kid b/ipa-server/ipa-gui/ipagui/templates/ipapolicyeditform.kid index 10665763..9584e445 100644 --- a/ipa-server/ipa-gui/ipagui/templates/ipapolicyeditform.kid +++ b/ipa-server/ipa-gui/ipagui/templates/ipapolicyeditform.kid @@ -15,6 +15,8 @@ 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> <div py:for="field in hidden_fields" py:replace="field.display(value_for(field), **params_for(field))" @@ -170,7 +172,80 @@ from ipagui.helpers import ipahelper py:content="tg.errors.get('ipadefaultprimarygroup')" /> </td> </tr> + <tr> + <th> + <label class="fieldlabel" for="${ipapolicy_fields.userobjectclasses.field_id}" + py:content="ipapolicy_fields.userobjectclasses.label" />: + </th> + <td colspan="3"> + <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${ipapolicy_fields.userobjectclasses.field_id}"> + <tbody> + <?python repetition = 0 + fld_index = 0 + fld_error = tg.errors.get('ipauserobjectclasses') + ?> + <tr py:for="fld in value_for(ipapolicy_fields.ipauserobjectclasses)" + id="${ipapolicy_fields.userobjectclasses.field_id}_${repetition}" + class="${ipapolicy_fields.userobjectclasses.field_class}"> + + <td py:for="field in ipapolicy_fields.userobjectclasses.fields"> + <span><input class="textfield" type="text" id="${ipapolicy_fields.userobjectclasses.field_id}_${repetition}_ipauserobjectclasses" name="userobjectclasses-${repetition}.ipauserobjectclasses" value="${fld}"/></span> + <span py:if="fld_error and fld_error[fld_index]" class="fielderror" + py:content="tg.errors.get('ipauserobjectclasses')" /> + </td> + <?python fld_index = fld_index + 1 ?> + <td> + <a + href="javascript:ExpandingForm.removeItem('${ipapolicy_fields.userobjectclasses.field_id}_${repetition}')">Remove</a> + </td> + <?python repetition = repetition + 1?> + </tr> + </tbody> + </table> + <a id="${ipapolicy_fields.userobjectclasses.field_id}_doclink" href="javascript:ExpandingForm.addItem('${ipapolicy_fields.userobjectclasses.field_id}');">Add User Object Class</a> + </td> + </tr> + <tr> + <th> + <label class="fieldlabel" for="${ipapolicy_fields.groupobjectclasses.field_id}" + py:content="ipapolicy_fields.groupobjectclasses.label" />: + </th> + <td colspan="3"> + <table class="formtable" cellpadding="2" cellspacing="0" border="0" id="${ipapolicy_fields.groupobjectclasses.field_id}"> + <tbody> + <?python repetition = 0 + fld_index = 0 + fld_error = tg.errors.get('ipagroupobjectclasses') + ?> + <tr py:for="fld in value_for(ipapolicy_fields.ipagroupobjectclasses)" + id="${ipapolicy_fields.groupobjectclasses.field_id}_${repetition}" + class="${ipapolicy_fields.groupobjectclasses.field_class}"> + + <td py:for="field in ipapolicy_fields.groupobjectclasses.fields"> + <span><input class="textfield" type="text" id="${ipapolicy_fields.groupobjectclasses.field_id}_${repetition}_ipagroupobjectclasses" name="groupobjectclasses-${repetition}.ipagroupobjectclasses" value="${fld}"/></span> + <span py:if="fld_error and fld_error[fld_index]" class="fielderror" + py:content="tg.errors.get('ipagroupobjectclasses')" /> + </td> + <?python fld_index = fld_index + 1 ?> + <td> + <a + href="javascript:ExpandingForm.removeItem('${ipapolicy_fields.groupobjectclasses.field_id}_${repetition}')">Remove</a> + </td> + <?python repetition = repetition + 1?> + </tr> + </tbody> + </table> + <a id="${ipapolicy_fields.groupobjectclasses.field_id}_doclink" href="javascript:ExpandingForm.addItem('${ipapolicy_fields.groupobjectclasses.field_id}');">Add Group Object Class</a> + </td> + </tr> </table> + + <hr/> + + <input type="submit" class="submitbutton" name="submit" + value="Update Policy"/> + <input type="submit" class="submitbutton" name="submit" + value="Cancel Edit" /> </form> </div> diff --git a/ipa-server/ipa-gui/ipagui/templates/ipapolicyshow.kid b/ipa-server/ipa-gui/ipagui/templates/ipapolicyshow.kid index 089fb494..50c7d6d8 100644 --- a/ipa-server/ipa-gui/ipagui/templates/ipapolicyshow.kid +++ b/ipa-server/ipa-gui/ipagui/templates/ipapolicyshow.kid @@ -15,6 +15,9 @@ 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> + <input class="submitbutton" type="button" + onclick="document.location.href='${edit_url}'" + value="Edit Policy" /> <h2 class="formsection">Search</h2> <table class="formtable" cellpadding="2" cellspacing="0" border="0"> @@ -109,6 +112,46 @@ edit_url = tg.url('/ipapolicy/edit') </th> <td>${ipapolicy.get("ipadefaultprimarygroup")}</td> </tr> + <tr> + <th> + <label class="fieldlabel" py:content="fields.ipauserobjectclasses.label" />: + </th> + <td> + <table cellpadding="2" cellspacing="0" border="0"> + <tbody> + <?python + index = 0 + values = ipapolicy.get("ipauserobjectclasses", '') + 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.ipagroupobjectclasses.label" />: + </th> + <td> + <table cellpadding="2" cellspacing="0" border="0"> + <tbody> + <?python + index = 0 + values = ipapolicy.get("ipagroupobjectclasses", '') + if isinstance(values, str): + values = [values] + ?> + <tr py:for="index in range(len(values))"> + <td>${values[index]}</td> + </tr> + </tbody> + </table> + </td> + </tr> </table> <hr /> <input class="submitbutton" type="button" diff --git a/ipa-server/ipa-gui/ipagui/templates/master.kid b/ipa-server/ipa-gui/ipagui/templates/master.kid index 12e54fa1..e1149754 100644 --- a/ipa-server/ipa-gui/ipagui/templates/master.kid +++ b/ipa-server/ipa-gui/ipagui/templates/master.kid @@ -78,10 +78,14 @@ <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('/principal/new')}">Add Service Principal</a></li> + <li><a href="${tg.url('/principal/list')}">Find Service Principal</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('/user/edit/', principal=tg.identity.user.display_name)}">Self Service</a></li> + <li py:if="not tg.identity.anonymous"><a href="${tg.url('/user/edit/', principal=tg.identity.user.display_name)}">Self Service</a></li> </ul> <ul py:if="'admins' in tg.identity.groups"> <li><a href="${tg.url('/delegate/list')}">Delegations</a></li> diff --git a/ipa-server/ipa-gui/ipagui/templates/principallayout.kid b/ipa-server/ipa-gui/ipagui/templates/principallayout.kid new file mode 100644 index 00000000..1e7bef2e --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/principallayout.kid @@ -0,0 +1,19 @@ +<!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/principallist.kid b/ipa-server/ipa-gui/ipagui/templates/principallist.kid new file mode 100644 index 00000000..dcd9dd4b --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/principallist.kid @@ -0,0 +1,64 @@ +<!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="'principallayout.kid'"> +<head> +<meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/> +<title>Find Service Principals</title> +</head> +<body> + <h1>Find Service Principals</h1> + <script type="text/javascript" charset="utf-8" src="${tg.url('/static/javascript/tablekit.js')}"></script> + <div id="search"> + <form action="${tg.url('/principal/list')}" method="get"> + <input id="hostname" type="text" name="hostname" value="${hostname}" /> + <input class="searchbutton" type="submit" value="Find Hosts"/> + </form> + <script type="text/javascript"> + document.getElementById("hostname").focus(); + </script> + </div> + <div py:if='(principals != None) and (len(principals) > 0)'> + <h2>${len(principals)} results returned:</h2> + <table id="resultstable" class="details sortable resizable" cellspacing="0"> + <thead> + <tr> + <th> + Hostname + </th> + <th> + Service + </th> + </tr> + </thead> + <tbody> + <tr py:for="principal in principals"> + <td> + <a href="${tg.url('/principal/show',principal=principal.krbprincipalname)}" + >${principal.hostname}</a> + </td> + <td> + ${principal.service} + </td> + </tr> + </tbody> + </table> + </div> + <div id="alertbox" py:if='(principals != None) and (len(principals) == 0)'> + <p id="alertbox">No results found for "${hostname}"</p> + </div> + + <div class="instructions" py:if='principals == None'> + <p> + Exact matches are listed first, followed by partial matches. If your search + is too broad, you will get a warning that the search returned too many + results. Try being more specific. + </p> + <p> + The results that come back are sortable. Simply click on a column + header to sort on that header. A triangle will indicate the sorted + column, along with its direction. Clicking and dragging between headers + will allow you to resize the header. + </p> + </div> +</body> +</html> diff --git a/ipa-server/ipa-gui/ipagui/templates/principalnew.kid b/ipa-server/ipa-gui/ipagui/templates/principalnew.kid new file mode 100644 index 00000000..b4ba3179 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/principalnew.kid @@ -0,0 +1,13 @@ +<!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="'principallayout.kid'"> +<head> + <meta content="text/html; charset=utf-8" http-equiv="Content-Type" py:replace="''"/> + <title>Add Service Principal</title> +</head> +<body> + <h1>Add Service Principal</h1> + + ${form.display(action=tg.url('/principal/create'), value=principal)} +</body> +</html> diff --git a/ipa-server/ipa-gui/ipagui/templates/principalnewform.kid b/ipa-server/ipa-gui/ipagui/templates/principalnewform.kid new file mode 100644 index 00000000..25b54621 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/principalnewform.kid @@ -0,0 +1,102 @@ +<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="Add Principal"/> + +<?python +from ipagui.helpers import ipahelper +?> + + <script type="text/javascript" charset="utf-8" + src="${tg.url('/static/javascript/dynamicedit.js')}"></script> + + <?python searchurl = tg.url('/principal/edit_search') ?> + + <script type="text/javascript"> + function toggleOther(field) { + otherField = document.getElementById('form_other'); + var e=document.getElementById(field).value; + if ( e == "other") { + otherField.disabled = false; + } else { + otherField.disabled =true; + } + } + + function doSearch() { + $('searchresults').update("Searching..."); + new Ajax.Updater('searchresults', + '${searchurl}', + { asynchronous:true, + parameters: { criteria: $('criteria').value }, + evalScripts: true }); + return false; + } + </script> + + <div py:for="field in hidden_fields" + py:replace="field.display(value_for(field), **params_for(field))" + /> + + <h2 class="formsection">Service Principal Details</h2> + <table class="formtable" cellpadding="2" cellspacing="0" border="0"> + <tr> + <th> + <label class="fieldlabel" for="${principal_fields.hostname.field_id}" + py:content="principal_fields.hostname.label" />: + </th> + <td> + <span py:replace="principal_fields.hostname.display(value_for(principal_fields.hostname))" /> + <span py:if="tg.errors.get('hostname')" class="fielderror" + py:content="tg.errors.get('hostname')" /> + + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${principal_fields.service.field_id}" + py:content="principal_fields.service.label" />: + </th> + <td> + <span py:replace="principal_fields.service.display(value_for(principal_fields.service))" /> + <span py:if="tg.errors.get('service')" class="fielderror" + py:content="tg.errors.get('service')" /> + + </td> + </tr> + + <tr> + <th> + <label class="fieldlabel" for="${principal_fields.other.field_id}" + py:content="principal_fields.other.label" />: + </th> + <td> + <span py:replace="principal_fields.other.display(value_for(principal_fields.other))" /> + <span py:if="tg.errors.get('other')" class="fielderror" + py:content="tg.errors.get('other')" /> + <script type="text/javascript"> + var e=document.getElementById('form_service').value; + if ( e != "other") { + document.getElementById('form_other').disabled = true; + } + </script> + + </td> + </tr> + + </table> + +<hr /> + + <input type="submit" class="submitbutton" name="submit" value="Add Principal"/> + + </form> + + <script type="text/javascript"> + document.getElementById("form_hostname").focus(); + </script> + +</div> diff --git a/ipa-server/ipa-gui/ipagui/templates/usernewform.kid b/ipa-server/ipa-gui/ipagui/templates/usernewform.kid index 97be5273..83dc6463 100644 --- a/ipa-server/ipa-gui/ipagui/templates/usernewform.kid +++ b/ipa-server/ipa-gui/ipagui/templates/usernewform.kid @@ -791,6 +791,10 @@ from ipagui.helpers import ipahelper </form> <script type="text/javascript"> + document.getElementById("form_title").focus(); + </script> + + <script type="text/javascript"> /* * This section restores the contents of the add and remove lists * dynamically if we have to refresh the page diff --git a/ipa-server/ipa-gui/ipagui/templates/usershow.kid b/ipa-server/ipa-gui/ipagui/templates/usershow.kid index 61498872..05372123 100644 --- a/ipa-server/ipa-gui/ipagui/templates/usershow.kid +++ b/ipa-server/ipa-gui/ipagui/templates/usershow.kid @@ -11,7 +11,7 @@ edit_url = tg.url('/user/edit', uid=user.get('uid')) ?> <h1>View User</h1> - <input py:if="'editors' in tg.identity.groups or 'admins' in tg.identity.groups" + <input py:if="'editors' in tg.identity.groups or 'admins' in tg.identity.groups or tg.identity.display_name == user.get('uid')" class="submitbutton" type="button" onclick="document.location.href='${edit_url}'" value="Edit User" /> @@ -374,7 +374,7 @@ else: <br/> <hr /> - <input py:if="'editors' in tg.identity.groups or 'admins' in tg.identity.groups" + <input py:if="'editors' in tg.identity.groups or 'admins' in tg.identity.groups or tg.identity.display_name == user.get('uid')" class="submitbutton" type="button" onclick="document.location.href='${edit_url}'" value="Edit User" /> diff --git a/ipa-server/ipa-install/share/60ipaconfig.ldif b/ipa-server/ipa-install/share/60ipaconfig.ldif index e15d4a41..b9371e77 100644 --- a/ipa-server/ipa-install/share/60ipaconfig.ldif +++ b/ipa-server/ipa-install/share/60ipaconfig.ldif @@ -27,11 +27,15 @@ attributetypes: ( 2.16.840.1.113730.3.8.1.7 NAME 'ipaDefaultLoginShell' EQUALITY 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 +## ipaPwdExpAdvNotify - time in days to send out paswword 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) +# ipaUserObjectClasses - required objectclasses for users +attributetypes: ( 2.16.840.1.113730.3.8.1.11 NAME 'ipaUserObjectClasses' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27) +# ipaGroupObjectClasses - required objectclasses for groups +attributetypes: ( 2.16.840.1.113730.3.8.1.12 NAME 'ipaGroupObjectClasses' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27) ############################################### ## ## 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 ) ) +objectClasses: ( 2.16.840.1.113730.3.8.2.1 NAME 'ipaGuiConfig' AUXILIARY MAY ( ipaUserSearchFields $ ipaGroupSearchFields $ ipaSearchTimeLimit $ ipaSearchRecordsLimit $ ipaCustomFields $ ipaHomesRootDir $ ipaDefaultLoginShell $ ipaDefaultPrimaryGroup $ ipaMaxUsernameLength $ ipaPwdExpAdvNotify $ ipaUserObjectClasses $ ipaGroupObjectClasses) ) diff --git a/ipa-server/ipa-install/share/bootstrap-template.ldif b/ipa-server/ipa-install/share/bootstrap-template.ldif index 4063c4e4..fb124a79 100644 --- a/ipa-server/ipa-install/share/bootstrap-template.ldif +++ b/ipa-server/ipa-install/share/bootstrap-template.ldif @@ -14,7 +14,7 @@ krbMinPwdLife: 3600 krbPwdMinDiffChars: 0 krbPwdMinLength: 8 krbPwdHistoryLength: 0 -krbMaxPwdLife: 864000 +krbMaxPwdLife: 7776000 dn: cn=users,cn=accounts,$SUFFIX changetype: add diff --git a/ipa-server/ipaserver/dsinstance.py b/ipa-server/ipaserver/dsinstance.py index 8735ebda..08b86035 100644 --- a/ipa-server/ipaserver/dsinstance.py +++ b/ipa-server/ipaserver/dsinstance.py @@ -18,16 +18,15 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # -import subprocess -import string -import tempfile import shutil import logging import pwd import glob import sys +import os + +from ipa import ipautil -from ipa.ipautil import * import service import installutils @@ -36,7 +35,7 @@ SERVER_ROOT_32 = "/usr/lib/dirsrv" 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) + ipautil.run(args) def realm_to_suffix(realm_name): s = realm_name.split(".") @@ -44,7 +43,7 @@ def realm_to_suffix(realm_name): return ",".join(terms) def find_server_root(): - if dir_exists(SERVER_ROOT_64): + if ipautil.dir_exists(SERVER_ROOT_64): return SERVER_ROOT_64 else: return SERVER_ROOT_32 @@ -83,7 +82,7 @@ def check_existing_installation(): sys.exit(1) try: - run(["/sbin/service", "dirsrv", "stop"]) + ipautil.run(["/sbin/service", "dirsrv", "stop"]) except: pass for d in dirs: @@ -185,25 +184,25 @@ class DsInstance(service.Service): logging.debug("adding ds user %s" % self.ds_user) args = ["/usr/sbin/useradd", "-c", "DS System User", "-d", "/var/lib/dirsrv", "-M", "-r", "-s", "/sbin/nologin", self.ds_user] try: - run(args) + ipautil.run(args) logging.debug("done adding user") except ipautil.CalledProcessError, e: logging.critical("failed to add user %s" % e) def __create_instance(self): self.step("creating directory server instance") - inf_txt = template_str(INF_TEMPLATE, self.sub_dict) + inf_txt = ipautil.template_str(INF_TEMPLATE, self.sub_dict) logging.debug(inf_txt) - inf_fd = write_tmp_file(inf_txt) + inf_fd = ipautil.write_tmp_file(inf_txt) logging.debug("writing inf template") - if file_exists("/usr/sbin/setup-ds.pl"): + if ipautil.file_exists("/usr/sbin/setup-ds.pl"): args = ["/usr/sbin/setup-ds.pl", "--silent", "--logfile", "-", "-f", inf_fd.name] logging.debug("calling setup-ds.pl") else: args = ["/usr/bin/ds_newinst.pl", inf_fd.name] logging.debug("calling ds_newinst.pl") try: - run(args) + ipautil.run(args) logging.debug("completed creating ds instance") except ipautil.CalledProcessError, e: logging.critical("failed to restart ds instance %s" % e) @@ -217,19 +216,19 @@ class DsInstance(service.Service): def __add_default_schemas(self): self.step("adding default schema") - shutil.copyfile(SHARE_DIR + "60kerberos.ldif", + shutil.copyfile(ipautil.SHARE_DIR + "60kerberos.ldif", schema_dirname(self.realm_name) + "60kerberos.ldif") - shutil.copyfile(SHARE_DIR + "60samba.ldif", + shutil.copyfile(ipautil.SHARE_DIR + "60samba.ldif", schema_dirname(self.realm_name) + "60samba.ldif") - shutil.copyfile(SHARE_DIR + "60radius.ldif", + shutil.copyfile(ipautil.SHARE_DIR + "60radius.ldif", schema_dirname(self.realm_name) + "60radius.ldif") - shutil.copyfile(SHARE_DIR + "60ipaconfig.ldif", + shutil.copyfile(ipautil.SHARE_DIR + "60ipaconfig.ldif", schema_dirname(self.realm_name) + "60ipaconfig.ldif") def __add_memberof_module(self): self.step("enabling memboerof plugin") - memberof_txt = template_file(SHARE_DIR + "memberof-conf.ldif", self.sub_dict) - memberof_fd = write_tmp_file(memberof_txt) + memberof_txt = ipautil.template_file(ipautil.SHARE_DIR + "memberof-conf.ldif", self.sub_dict) + memberof_fd = ipautil.write_tmp_file(memberof_txt) try: ldap_mod(memberof_fd, "cn=Directory Manager", self.dm_password) except ipautil.CalledProcessError, e: @@ -238,8 +237,8 @@ class DsInstance(service.Service): 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) + memberof_txt = ipautil.template_file(ipautil.SHARE_DIR + "memberof-task.ldif", self.sub_dict) + memberof_fd = ipautil.write_tmp_file(memberof_txt) try: ldap_mod(memberof_fd, "cn=Directory Manager", self.dm_password) except ipautil.CalledProcessError, e: @@ -248,8 +247,8 @@ class DsInstance(service.Service): def __add_referint_module(self): self.step("enabling referential integrity plugin") - referint_txt = template_file(SHARE_DIR + "referint-conf.ldif", self.sub_dict) - referint_fd = write_tmp_file(referint_txt) + referint_txt = ipautil.template_file(ipautil.SHARE_DIR + "referint-conf.ldif", self.sub_dict) + referint_fd = ipautil.write_tmp_file(referint_txt) try: ldap_mod(referint_fd, "cn=Directory Manager", self.dm_password) except ipautil.CalledProcessError, e: @@ -258,8 +257,8 @@ class DsInstance(service.Service): def __add_dna_module(self): self.step("enabling distributed numeric assignment plugin") - dna_txt = template_file(SHARE_DIR + "dna-conf.ldif", self.sub_dict) - dna_fd = write_tmp_file(dna_txt) + dna_txt = ipautil.template_file(ipautil.SHARE_DIR + "dna-conf.ldif", self.sub_dict) + dna_fd = ipautil.write_tmp_file(dna_txt) try: ldap_mod(dna_fd, "cn=Directory Manager", self.dm_password) except ipautil.CalledProcessError, e: @@ -268,8 +267,8 @@ class DsInstance(service.Service): def __config_uidgid_gen_first_master(self): self.step("configuring Posix uid/gid generation as first master") - dna_txt = template_file(SHARE_DIR + "dna-posix.ldif", self.sub_dict) - dna_fd = write_tmp_file(dna_txt) + dna_txt = ipautil.template_file(ipautil.SHARE_DIR + "dna-posix.ldif", self.sub_dict) + dna_fd = ipautil.write_tmp_file(dna_txt) try: ldap_mod(dna_fd, "cn=Directory Manager", self.dm_password) except ipautil.CalledProcessError, e: @@ -278,8 +277,8 @@ class DsInstance(service.Service): def __add_master_entry_first_master(self): self.step("adding master entry as first master") - master_txt = template_file(SHARE_DIR + "master-entry.ldif", self.sub_dict) - master_fd = write_tmp_file(master_txt) + master_txt = ipautil.template_file(ipautil.SHARE_DIR + "master-entry.ldif", self.sub_dict) + master_fd = ipautil.write_tmp_file(master_txt) try: ldap_mod(master_fd, "cn=Directory Manager", self.dm_password) except ipautil.CalledProcessError, e: @@ -292,20 +291,20 @@ class DsInstance(service.Service): args = ["/usr/share/ipa/ipa-server-setupssl", self.dm_password, dirname, self.host_name] try: - run(args) + ipautil.run(args) logging.debug("done configuring ssl for ds instance") except ipautil.CalledProcessError, e: logging.critical("Failed to configure ssl in ds instance %s" % e) def __add_default_layout(self): self.step("adding default layout") - txt = template_file(SHARE_DIR + "bootstrap-template.ldif", self.sub_dict) - inf_fd = write_tmp_file(txt) + txt = ipautil.template_file(ipautil.SHARE_DIR + "bootstrap-template.ldif", self.sub_dict) + inf_fd = ipautil.write_tmp_file(txt) logging.debug("adding default dfrom ipa.ipautil import *s layout") args = ["/usr/bin/ldapmodify", "-xv", "-D", "cn=Directory Manager", "-w", self.dm_password, "-f", inf_fd.name] try: - run(args) + ipautil.run(args) logging.debug("done adding default ds layout") except ipautil.CalledProcessError, e: print "Failed to add default ds layout", e @@ -313,13 +312,13 @@ class DsInstance(service.Service): def __create_indeces(self): self.step("creating indeces") - txt = template_file(SHARE_DIR + "indeces.ldif", self.sub_dict) - inf_fd = write_tmp_file(txt) + txt = ipautil.template_file(ipautil.SHARE_DIR + "indeces.ldif", self.sub_dict) + inf_fd = ipautil.write_tmp_file(txt) logging.debug("adding/updating indeces") args = ["/usr/bin/ldapmodify", "-xv", "-D", "cn=Directory Manager", "-w", self.dm_password, "-f", inf_fd.name] try: - run(args) + ipautil.run(args) logging.debug("done adding/updating indeces") except ipautil.CalledProcessError, e: logging.critical("Failed to add/update indeces %s" % str(e)) @@ -327,7 +326,7 @@ class DsInstance(service.Service): def __certmap_conf(self): self.step("configuring certmap.conf") dirname = config_dirname(self.realm_name) - certmap_conf = template_file(SHARE_DIR+"certmap.conf.template", self.sub_dict) + certmap_conf = ipautil.template_file(ipautil.SHARE_DIR + "certmap.conf.template", self.sub_dict) certmap_fd = open(dirname+"certmap.conf", "w+") certmap_fd.write(certmap_conf) certmap_fd.close() @@ -335,7 +334,7 @@ class DsInstance(service.Service): def change_admin_password(self, password): logging.debug("Changing admin password") dirname = config_dirname(self.realm_name) - if dir_exists("/usr/lib64/mozldap"): + if ipautil.dir_exists("/usr/lib64/mozldap"): app = "/usr/lib64/mozldap/ldappasswd" else: app = "/usr/lib/mozldap/ldappasswd" @@ -344,7 +343,7 @@ class DsInstance(service.Service): "-P", dirname+"/cert8.db", "-ZZZ", "-s", password, "uid=admin,cn=sysaccounts,cn=etc,"+self.suffix] try: - run(args) + ipautil.run(args) logging.debug("ldappasswd done") except ipautil.CalledProcessError, e: print "Unable to set admin password", e diff --git a/ipa-server/xmlrpc-server/funcs.py b/ipa-server/xmlrpc-server/funcs.py index 032f6b42..7be75ddc 100644 --- a/ipa-server/xmlrpc-server/funcs.py +++ b/ipa-server/xmlrpc-server/funcs.py @@ -329,6 +329,32 @@ class IPAServer: return (exact_match_filter, partial_match_filter) + def __get_schema(self, opts=None): + """Retrieves the current LDAP schema from the LDAP server.""" + + schema_entry = self.__get_base_entry("", "objectclass=*", ['dn','subschemasubentry'], opts) + schema_cn = schema_entry.get('subschemasubentry') + schema = self.__get_base_entry(schema_cn, "objectclass=*", ['*'], opts) + + return schema + + def __get_objectclasses(self, opts=None): + """Returns a list of available objectclasses that the LDAP + server supports. This parses out the syntax, attributes, etc + and JUST returns a lower-case list of the names.""" + + schema = self.__get_schema(opts) + + objectclasses = schema.get('objectclasses') + + # Convert this list into something more readable + result = [] + for i in range(len(objectclasses)): + oc = objectclasses[i].lower().split(" ") + result.append(oc[3].replace("'","")) + + return result + # Higher-level API def get_aci_entry(self, sattrs, opts=None): @@ -423,6 +449,19 @@ class IPAServer: if self.__is_user_unique(user['uid'], opts) == 0: raise ipaerror.gen_exception(ipaerror.LDAP_DUPLICATE) + # dn is set here, not by the user + try: + del user['dn'] + except KeyError: + pass + + # No need to set empty fields, and they can cause issues when they + # get to LDAP, like: + # TypeError: ('expected a string in the list', None) + for k in user.keys(): + if not user[k] or len(user[k]) == 0 or (len(user[k]) == 1 and '' in user[k]): + del user[k] + dn="uid=%s,%s,%s" % (ldap.dn.escape_dn_chars(user['uid']), user_container,self.basedn) entry = ipaserver.ipaldap.Entry(dn) @@ -476,8 +515,16 @@ class IPAServer: conn = self.getConnection(opts) try: - res = conn.addEntry(entry) - self.add_user_to_group(user.get('uid'), group_dn, opts) + try: + res = conn.addEntry(entry) + except TypeError, e: + raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, "There is a problem with one of the data types.") + except Exception, e: + raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, e) + try: + self.add_user_to_group(user.get('uid'), group_dn, opts) + except Exception, e: + raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, "The user was created but adding to group %s failed" % group_dn) finally: self.releaseConnection(conn) return res @@ -1332,7 +1379,78 @@ class IPAServer: finally: self.releaseConnection(conn) return res - + + def find_service_principal(self, criteria, sattrs, 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.""" + + config = self.get_ipa_config(opts) + if timelimit < 0: + timelimit = float(config.get('ipasearchtimelimit')) + if searchlimit < 0: + searchlimit = float(config.get('ipasearchrecordslimit')) + + search_fields = ["krbprincipalname"] + + criteria = self.__safe_filter(criteria) + criteria_words = re.split(r'\s+', criteria) + criteria_words = filter(lambda value:value!="", criteria_words) + if len(criteria_words) == 0: + return [0] + + (exact_match_filter, partial_match_filter) = self.__generate_match_filters( + search_fields, criteria_words) + + # + # further constrain search to just the objectClass + # TODO - need to parameterize this into generate_match_filters, + # and work it into the field-specification search feature + # + exact_match_filter = "(&(objectclass=krbPrincipalAux)(!(objectClass=person))(!(krbprincipalname=kadmin/*))%s)" % exact_match_filter + partial_match_filter = "(&(objectclass=krbPrincipalAux)(!(objectClass=person))(!(krbprincipalname=kadmin/*))%s)" % partial_match_filter + print exact_match_filter + print partial_match_filter + + conn = self.getConnection(opts) + try: + try: + exact_results = conn.getListAsync(self.basedn, self.scope, + exact_match_filter, sattrs, 0, None, None, timelimit, + searchlimit) + except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND): + exact_results = [0] + + try: + partial_results = conn.getListAsync(self.basedn, self.scope, + partial_match_filter, sattrs, 0, None, None, timelimit, + searchlimit) + except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND): + partial_results = [0] + finally: + self.releaseConnection(conn) + + exact_counter = exact_results[0] + partial_counter = partial_results[0] + + exact_results = exact_results[1:] + partial_results = partial_results[1:] + + # Remove exact matches from the partial_match list + exact_dns = set(map(lambda e: e.dn, exact_results)) + partial_results = filter(lambda e: e.dn not in exact_dns, + partial_results) + + if (exact_counter == -1) or (partial_counter == -1): + counter = -1 + else: + counter = len(exact_results) + len(partial_results) + + entries = [counter] + for e in exact_results + partial_results: + entries.append(self.convert_entry(e)) + + return entries def get_keytab(self, name, opts=None): """get a keytab""" @@ -1397,6 +1515,19 @@ class IPAServer: except: raise + # Run through the list of User and Group object classes to make + # sure they are all valid. This doesn't handle dependencies but it + # will at least catch typos. + classes = self.__get_objectclasses(opts) + oc = newconfig['ipauserobjectclasses'] + for i in range(len(oc)): + if not oc[i].lower() in classes: + raise ipaerror.gen_exception(ipaerror.CONFIG_INVALID_OC) + oc = newconfig['ipagroupobjectclasses'] + for i in range(len(oc)): + if not oc[i].lower() in classes: + raise ipaerror.gen_exception(ipaerror.CONFIG_INVALID_OC) + return self.update_entry(oldconfig, newconfig, opts) def get_password_policy(self, opts=None): @@ -1406,6 +1537,10 @@ class IPAServer: except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND): raise ipaerror.gen_exception(ipaerror.LDAP_NO_CONFIG) + # convert some values for display purposes + policy['krbmaxpwdlife'] = str(int(policy.get('krbmaxpwdlife')) / 86400) + policy['krbminpwdlife'] = str(int(policy.get('krbminpwdlife')) / 3600) + return policy def update_password_policy(self, oldpolicy, newpolicy, opts=None): @@ -1414,11 +1549,18 @@ class IPAServer: # 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')) + for k in oldpolicy.iterkeys(): + if k.startswith("krb", 0, 3): + oldpolicy[k] = str(oldpolicy[k]) + for k in newpolicy.iterkeys(): + if k.startswith("krb", 0, 3): + newpolicy[k] = str(newpolicy[k]) + + # Convert hours and days to seconds + oldpolicy['krbmaxpwdlife'] = str(int(oldpolicy.get('krbmaxpwdlife')) * 86400) + oldpolicy['krbminpwdlife'] = str(int(oldpolicy.get('krbminpwdlife')) * 3600) + newpolicy['krbmaxpwdlife'] = str(int(newpolicy.get('krbmaxpwdlife')) * 86400) + newpolicy['krbminpwdlife'] = str(int(newpolicy.get('krbminpwdlife')) * 3600) except KeyError: # These should all be there but if not, let things proceed pass diff --git a/ipa-server/xmlrpc-server/ipaxmlrpc.py b/ipa-server/xmlrpc-server/ipaxmlrpc.py index c6f0ec2c..31cfbae6 100644 --- a/ipa-server/xmlrpc-server/ipaxmlrpc.py +++ b/ipa-server/xmlrpc-server/ipaxmlrpc.py @@ -360,6 +360,7 @@ def handler(req, profiling=False): 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.find_service_principal) h.register_function(f.get_keytab) h.handle_request(req) finally: |