diff options
author | Rob Crittenden <rcritten@redhat.com> | 2007-12-05 15:17:11 -0500 |
---|---|---|
committer | Rob Crittenden <rcritten@redhat.com> | 2007-12-05 15:17:11 -0500 |
commit | 15b7dc6ff9c202dee00f1403139c206b5969c0f3 (patch) | |
tree | edcf5a1f50c8b0508674a1c255285c80e2f495a5 | |
parent | c397041bfa66b3d44d65af27eabc289c70423f21 (diff) | |
download | freeipa-15b7dc6ff9c202dee00f1403139c206b5969c0f3.tar.gz freeipa-15b7dc6ff9c202dee00f1403139c206b5969c0f3.tar.xz freeipa-15b7dc6ff9c202dee00f1403139c206b5969c0f3.zip |
Add UI for service principal creation and keytab retrieval
-rw-r--r-- | ipa-python/ipaclient.py | 14 | ||||
-rw-r--r-- | ipa-python/rpcclient.py | 18 | ||||
-rw-r--r-- | ipa-server/ipa-gui/ipagui/controllers.py | 2 | ||||
-rw-r--r-- | ipa-server/ipa-gui/ipagui/forms/principal.py | 39 | ||||
-rw-r--r-- | ipa-server/ipa-gui/ipagui/subcontrollers/principal.py | 153 | ||||
-rw-r--r-- | ipa-server/ipa-gui/ipagui/templates/master.kid | 6 | ||||
-rw-r--r-- | ipa-server/ipa-gui/ipagui/templates/principallayout.kid | 19 | ||||
-rw-r--r-- | ipa-server/ipa-gui/ipagui/templates/principallist.kid | 64 | ||||
-rw-r--r-- | ipa-server/ipa-gui/ipagui/templates/principalnew.kid | 13 | ||||
-rw-r--r-- | ipa-server/ipa-gui/ipagui/templates/principalnewform.kid | 98 | ||||
-rw-r--r-- | ipa-server/xmlrpc-server/funcs.py | 73 | ||||
-rw-r--r-- | ipa-server/xmlrpc-server/ipaxmlrpc.py | 1 |
12 files changed, 498 insertions, 2 deletions
diff --git a/ipa-python/ipaclient.py b/ipa-python/ipaclient.py index c551f0435..0a4d64f11 100644 --- a/ipa-python/ipaclient.py +++ b/ipa-python/ipaclient.py @@ -385,6 +385,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/rpcclient.py b/ipa-python/rpcclient.py index d7ff97405..de32e9beb 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 d1ee22e01..70a29246a 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/principal.py b/ipa-server/ipa-gui/ipagui/forms/principal.py new file mode 100644 index 000000000..a830c8a34 --- /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/subcontrollers/principal.py b/ipa-server/ipa-gui/ipagui/subcontrollers/principal.py new file mode 100644 index 000000000..1b2ad6942 --- /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/templates/master.kid b/ipa-server/ipa-gui/ipagui/templates/master.kid index 12e54fa1d..e11497546 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 000000000..1e7bef2e7 --- /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 000000000..dcd9dd4b8 --- /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 000000000..b4ba3179c --- /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 000000000..166b18811 --- /dev/null +++ b/ipa-server/ipa-gui/ipagui/templates/principalnewform.kid @@ -0,0 +1,98 @@ +<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> + +</div> diff --git a/ipa-server/xmlrpc-server/funcs.py b/ipa-server/xmlrpc-server/funcs.py index 9e9ad27a6..d046b5181 100644 --- a/ipa-server/xmlrpc-server/funcs.py +++ b/ipa-server/xmlrpc-server/funcs.py @@ -1358,7 +1358,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""" diff --git a/ipa-server/xmlrpc-server/ipaxmlrpc.py b/ipa-server/xmlrpc-server/ipaxmlrpc.py index c6f0ec2ce..31cfbae69 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: |