Add UI for service principal creation and keytab retrieval

This commit is contained in:
Rob Crittenden 2007-12-05 15:17:11 -05:00
parent c397041bfa
commit 15b7dc6ff9
12 changed files with 498 additions and 2 deletions

View File

@ -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)

View File

@ -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()

View File

@ -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())

View File

@ -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)

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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"""

View File

@ -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: