This repository has been archived on 2023-02-21. You can view files and clone it, but cannot push or open issues or pull requests.
larpe/larpe/trunk/larpe/admin/hosts.ptl

1343 lines
60 KiB
Plaintext

import os
import urllib
import urlparse
from quixote import redirect, get_request, get_response, get_publisher
from quixote.directory import Directory
import lasso
from qommon.admin.menu import html_top, command_icon
from qommon import get_cfg
from qommon.form import *
from qommon.misc import http_get_page, get_abs_path
from larpe import site_authentication
from larpe import errors
from larpe import misc
from larpe.hosts import Host
from larpe.admin.apache import Location
from larpe.admin.liberty_utils import *
from larpe.admin.apache import write_apache2_vhosts
from larpe.admin.forms_prefill import FormsDirectory
from larpe.Defaults import DATA_DIR
from larpe.plugins import site_authentication_plugins
def check_basic_configuration(form):
get_publisher().reload_cfg()
# Check reversed_hostname and reversed_directory
reversed_hostname = form.get_widget('reversed_hostname').parse()
reversed_directory = form.get_widget('reversed_directory').parse()
if reversed_hostname == get_publisher().cfg['proxy_hostname'] and not reversed_directory:
form.set_error('reversed_hostname',
_('You must either choose a different hostname from Larpe or specify a reversed directory'))
def convert_label_to_name(label):
'''Build host name from host label'''
name = label.lower()
invalid_characters = [' ', "'"]
for char in invalid_characters:
name = name.replace(char, '_')
return name
class DictWidget(Widget):
def render_content [html] (self):
self.render_br = False
if self.value['enabled'] is True:
htmltag('input', xml_end=True, type='checkbox', name=self.name + '_enabled', checked='checked')
else:
htmltag('input', xml_end=True, type='checkbox', name=self.name + '_enabled')
' ' + self.name + '  '
htmltag('input', xml_end=True, type='text', name=self.name, value=self.value['value'], size='35', **self.attrs)
def _parse(self, request):
enabled = request.form.get(self.name + '_enabled')
value = request.form.get(self.name)
self.value = { 'enabled': enabled, 'value': value }
class ConfigurationAssistant(Directory):
_q_exports = ['start', 'check_new_address', 'modify_site_address_and_name',
'authentication_and_logout_adresses', 'check_auto_detected_configuration',
'credentials', 'send_authentication_request', 'check_authentication',
'see_authentication_response', 'see_response_html_page',
'see_bad_response_html_page', 'authentication_success_criteria',
'modify_authentication_request', 'auth_request_post_parameters',
'auth_request_http_headers', 'sso_init_link', 'metadatas',
'check_full_configuration', 'advanced_options']
def __init__(self, host):
self.host = host
def html_top [html] (self, title):
html_top('hosts', title)
'<h2>%s</h2>' % title
def start [html] (self):
# Check the global domain name has been previously set
get_publisher().reload_cfg()
if not get_cfg('domain_names') or not get_cfg('domain_names')[0]:
get_response().breadcrumb.append(('start', _('Basic configuration')))
self.html_top(_('Need domain name configuration'))
return htmltext(_('''Before configuring hosts, you must
<a href="../../../settings/domain_names">setup a global domain name</a> in
%(settings)s menu.''') % { 'settings': _('Settings') })
form = self.form_start()
if form.get_widget('cancel').parse():
return redirect('../..')
connection_failure = None
if form.is_submitted() and not form.has_errors():
try:
self.submit_start_form(form)
except Exception, e:
connection_failure = e
else:
if form.get_widget('terminate').parse():
return redirect('..')
return redirect('check_new_address')
get_response().breadcrumb.append(('start', _('Basic configuration')))
self.html_top(_('Step 1 - Basic configuration'))
if connection_failure:
'<div class="errornotice">%s</div>' % connection_failure
form.render()
def form_start(self):
form = Form(enctype='multipart/form-data')
form.add(UrlWidget, 'orig_site', title = _('Original site root address'), required = True,
size = 50, value = self.host.orig_site,
hint = _('If your site address is http://test.org/index.php, put http://test.org/ here'))
get_publisher().reload_cfg()
if get_cfg('proxy', {}).get('enabled'):
form.add(CheckboxWidget, 'use_proxy', title = _('Use a proxy'),
hint = _("Uncheck it if Larpe doesn't need to use the proxy to connect to this site"),
value = self.host.use_proxy)
else:
form.add(HtmlWidget, htmltext('<p>%s</p>' % \
_('''If Larpe needs to use a proxy to connect to this site, you must first configure
it in <a href="../../../settings/proxy">global proxy parameters</a>.''')))
form.add_submit('cancel', _('Cancel'))
form.add_submit('submit', _('Next'))
form.add_submit('terminate', _('Terminate'))
return form
def submit_start_form(self, form):
fields = ['orig_site']
use_proxy = get_cfg('proxy', {}).get('enabled')
if use_proxy:
fields += ['use_proxy']
for f in fields:
setattr(self.host, f, form.get_widget(f).parse())
# If no proxy is setup yet, set use_proxy to False for new hosts in case a proxy is later configured
if not use_proxy:
self.host.use_proxy = False
# Remove what is after the last '/'
#self.host.orig_site = '/'.join(self.host.orig_site.split('/')[:-1])
html_page = self.get_data_after_redirects(self.host.orig_site)
if not self.host.label:
# Look for html title in original site index page
regexp = re.compile("""<title.*?>(.*?)</title>""", re.DOTALL | re.IGNORECASE)
title = regexp.findall(html_page)
if title:
self.host.label = title[0]
else:
self.host.label = 'Untitled'
# If another site already uses this site title, add trailings "_" until we find an available name
existing_label = True
while existing_label is True:
for any_host in Host.select():
if any_host.id != self.host.id and self.host.label == any_host.label:
self.host.label += '_'
break
else:
existing_label = False
# Fill host.name attribute
self.host.name = convert_label_to_name(self.host.label)
if not self.host.scheme:
# Get tokens from orig site url
orig_scheme, rest = urllib.splittype(self.host.orig_site)
orig_host, rest = urllib.splithost(rest)
get_publisher().reload_cfg()
# Set url scheme (HTTP or HTTPS)
# TODO: Handle the option "Both"
if get_cfg('sites_url_scheme'):
self.host.scheme = get_cfg('sites_url_scheme')
else:
self.host.scheme = orig_scheme
if not self.host.reversed_hostname:
# Build a new domain name
short_name = orig_host.split('.')[0]
if short_name == 'www':
short_name = orig_host.split('.')[1]
self.host.reversed_hostname = '%s.%s' % (short_name, get_cfg('domain_names')[0])
# If another site already uses this domain name, add some trailing "_" until we find an available name
existing_domain = True
while existing_domain is True:
for any_host in Host.select():
if any_host.id != self.host.id and self.host.reversed_hostname == any_host.reversed_hostname:
self.host.reversed_hostname += '-'
break
else:
existing_domain = False
self.host.reversed_directory = None
if not self.host.new_url:
# New url for this host
self.host.new_url = '%s://%s%s/' % (self.host.scheme, self.host.reversed_hostname, get_request().environ['SCRIPT_NAME'])
# FIXME: Check if the new domain name already exists
# New url for this host
# self.host.new_url = '%s://%s%s/' % (self.host.scheme, self.host.reversed_hostname, get_request().environ['SCRIPT_NAME'])
# if self.host.reversed_directory is not None:
# self.host.new_url += '%s/' % self.host.reversed_directory
self.host.store()
write_apache2_vhosts()
# XXX: Should use the FancyURLopener class instead when it supports proxies
def get_data_after_redirects(self, start_url):
if not start_url:
return ''
status = 302
location = None
while status // 100 == 3:
if location is None:
url = start_url
elif location.startswith('http'):
# Location is an absolute path
url = location
else:
# Location is a relative path
url = urlparse.urljoin(start_url, location)
response, status, data, auth_headers = http_get_page(url, use_proxy=self.host.use_proxy)
location = response.getheader('Location', None)
return data
def create_dirs(self):
# Hack : sites must use the configuration which is stored in main Larpe directory,
# but they need to have a directory named with their hostname, which will contain the
# main domain name for Larpe so they know where is the main configuration
hostname_dir = get_abs_path(os.path.join('..', self.host.reversed_hostname))
if not os.path.exists(hostname_dir):
os.mkdir(hostname_dir)
# Load the configuration from the main directory
get_publisher().reload_cfg()
# Write it in the site directory
get_publisher().write_cfg(hostname_dir)
# Storage directories
if not self.host.reversed_directory:
reversed_dir = 'default'
else:
reversed_dir = self.host.reversed_directory
self.host.site_dir = \
os.path.join(get_publisher().app_dir, 'sp', self.host.reversed_hostname, reversed_dir)
user_dir = os.path.join(self.host.site_dir, 'users')
token_dir = os.path.join(self.host.site_dir, 'tokens')
filter_dir = os.path.join(self.host.site_dir, 'filters')
for dir in (self.host.site_dir, user_dir, token_dir, filter_dir):
if not os.path.isdir(dir):
os.makedirs(dir)
def generate_ssl_keys(self):
# Generate SSL keys
private_key_path = os.path.join(self.host.site_dir, 'private_key.pem')
public_key_path = os.path.join(self.host.site_dir, 'public_key.pem')
if not os.path.isfile(private_key_path) or not os.path.isfile(public_key_path):
set_provider_keys(private_key_path, public_key_path)
self.host.private_key = private_key_path
self.host.public_key = public_key_path
def generate_metadatas(self):
metadata_cfg = {}
# Organization name
self.host.organization_name = self.host.label
metadata_cfg['organization_name'] = self.host.organization_name
# Base URL
base_url = '%s://%s%s/liberty/%s/liberty' % (self.host.scheme,
self.host.reversed_hostname,
get_request().environ['SCRIPT_NAME'],
self.host.name)
metadata_cfg['base_url'] = base_url
self.host.base_url = base_url
if lasso.SAML2_SUPPORT:
saml2_base_url = '%s://%s%s/liberty/%s/saml' % (self.host.scheme,
self.host.reversed_hostname,
get_request().environ['SCRIPT_NAME'],
self.host.name)
metadata_cfg['saml2_base_url'] = saml2_base_url
self.host.saml2_base_url = saml2_base_url
# Provider Id
provider_id = '%s/metadata' % base_url
metadata_cfg['provider_id'] = provider_id
self.host.provider_id = provider_id
if lasso.SAML2_SUPPORT:
saml2_provider_id = '%s/metadata' % saml2_base_url
metadata_cfg['saml2_provider_id'] = saml2_provider_id
self.host.saml2_provider_id = saml2_provider_id
# Read public key
public_key = ''
if self.host.public_key is not None and os.path.exists(self.host.public_key):
metadata_cfg['signing_public_key'] = open(self.host.public_key).read()
# Write metadatas
metadata_path = os.path.join(self.host.site_dir, 'metadata.xml')
open(metadata_path, 'w').write(get_metadata(metadata_cfg))
self.host.metadata = metadata_path
if lasso.SAML2_SUPPORT:
saml2_metadata_path = os.path.join(self.host.site_dir, 'saml2_metadata.xml')
open(saml2_metadata_path, 'w').write(get_saml2_metadata(metadata_cfg))
self.host.saml2_metadata = saml2_metadata_path
def check_new_address [html] (self):
form = Form(enctype='multipart/form-data')
form.add_submit('cancel', _('Previous'))
form.add_submit('submit', _('Next'))
form.add_submit('terminate', _('Terminate'))
if form.get_widget('cancel').parse():
return redirect('start')
if form.is_submitted():
self.create_dirs()
if self.host.private_key is None:
self.generate_ssl_keys()
self.generate_metadatas()
self.host.store()
if form.get_widget('terminate').parse():
return redirect('..')
return redirect('authentication_and_logout_adresses')
get_response().breadcrumb.append(('check_new_address', _('Check site address and name')))
self.html_top(_('Step 2 - Check the new site address works'))
'<h3>%s</h3>' % _('DNS configuration')
'<p>%s</p>' % _('''Before opening the following link, ensure you have configured your DNS
for this address. If you don't have a DNS server and you just want to test Larpe, add this
domain name in the file "/etc/hosts".''')
'<p>%s</p>' % _('''Then you can open this link in a new window or tab and see if your site
is displayed. If it's ok, you can click the "%(next)s" button. Otherwise, click the "%(previous)s"
button and check your settings.''') % {'next': _('Next'), 'previous': _('Previous')}
'<h3>%s</h3>' % _('Site adress and name')
'<p>%s' % _('The new address of this site is ')
'<a href="%s">%s</a><br/>' % (self.host.new_url, self.host.new_url)
'%s</p>' % _('The name of this site is "%s".') % self.host.label
'<p>%s</p>' % htmltext(_('''You can also <a href="modify_site_address_and_name">
modify the address or the name of this site</a>'''))
form.render()
def modify_site_address_and_name [html] (self):
form = self.form_modify_site_address_and_name()
if form.get_widget('cancel').parse():
return redirect('check_new_address')
if form.is_submitted() and not form.has_errors():
label = form.get_widget('label').parse()
name = convert_label_to_name(label)
for any_host in Host.select():
if any_host.id != self.host.id and name == any_host.name:
form.set_error('label', _('An host with the same name already exists'))
break
if form.is_submitted() and not form.has_errors():
self.submit_modify_site_address_and_name_form(form)
return redirect('check_new_address')
get_response().breadcrumb.append(('modify_site_address_and_name', _('Modify site address and name')))
self.html_top(_('Modify site address and name'))
form.render()
def form_modify_site_address_and_name(self):
form = Form(enctype='multipart/form-data')
form.add(UrlWidget, 'new_url', title = _('Address'), required = True,
size = 50, value = self.host.new_url)
form.add(StringWidget, 'label', title = _('Name'), required = True,
size = 50, value = self.host.label)
form.add_submit('submit', _('Submit'))
form.add_submit('cancel', _('Cancel'))
return form
def submit_modify_site_address_and_name_form(self, form):
fields = ['new_url', 'label']
for f in fields:
setattr(self.host, f, form.get_widget(f).parse())
# Split url to retrieve components
tokens = urlparse.urlparse(self.host.new_url)
self.host.scheme = tokens[0]
self.host.reversed_hostname = tokens[1]
self.host.reversed_directory = tokens[2]
if self.host.reversed_directory.startswith('/'):
self.host.reversed_directory = self.host.reversed_directory[1:]
# Fill host.name attribute
self.host.name = convert_label_to_name(self.host.label)
self.host.store()
write_apache2_vhosts()
def authentication_and_logout_adresses [html] (self):
form = self.form_authentication_and_logout_adresses()
if form.get_widget('cancel').parse():
return redirect('check_new_address')
if form.is_submitted() and not form.has_errors():
self.submit_authentication_and_logout_adresses_form(form)
if form.get_widget('terminate').parse():
return redirect('..')
return redirect('check_auto_detected_configuration')
get_response().breadcrumb.append(('authentication_and_logout_adresses', _('Authentication and logout')))
self.html_top(_('Step 3 - Configure authentication and logout pages'))
form.render()
def form_authentication_and_logout_adresses(self):
form = Form(enctype='multipart/form-data')
form.add(ValidUrlWidget, 'auth_url', title = _('Authentication form page address'),
hint = _('Address of a page on the site which contains the authentication form'),
required = True, size = 50, value = self.host.auth_url)
form.add(ValidUrlWidget, 'logout_url', title = _('Logout address'), required = False,
hint = _('Address of the logout link on the site'),
size = 50, value = self.host.logout_url)
form.add_submit('cancel', _('Previous'))
form.add_submit('submit', _('Next'))
form.add_submit('terminate', _('Terminate'))
return form
def submit_authentication_and_logout_adresses_form(self, form):
fields = ['auth_url', 'logout_url']
for f in fields:
setattr(self.host, f, form.get_widget(f).parse())
self.host.auth_form_url = self.host.auth_url
if not self.host.http_headers:
self.host.http_headers = {
'Content-Type': { 'enabled': True, 'value': 'application/x-www-form-urlencoded', 'immutable': False },
'X-Forwarded-For': { 'enabled': True, 'value': _('(computed automatically)'), 'immutable': True },
'X-Forwarded-Host': { 'enabled': True, 'value': self.host.reversed_hostname, 'immutable': False },
}
self.auto_detect_configuration()
self.host.store()
write_apache2_vhosts()
def check_auto_detected_configuration [html] (self):
form = Form(enctype='multipart/form-data')
form.add_submit('cancel', _('Previous'))
form.add_submit('submit', _('Next'))
form.add_submit('terminate', _('Terminate'))
if form.get_widget('cancel').parse():
return redirect('authentication_and_logout_adresses')
if form.is_submitted():
if form.get_widget('terminate').parse():
return redirect('..')
return redirect('credentials')
get_response().breadcrumb.append(('check_auto_detected_configuration', _('Auto detected configuration')))
self.html_top(_('Step 4 - Check automatically detected configuration for the authentication form'))
host_attrs = (
('auth_check_url', _('Address where the authentication form must be sent')),
('login_field_name', _('Name of the login field')),
('password_field_name', _('Name of the password field')),
)
html_fields = ''
success = True
for attr, name in host_attrs:
color = 'black'
if attr in ('auth_check_url', 'login_field_name', 'password_field_name') and \
not getattr(self.host, str(attr)):
color = 'red'
success = False
html_fields += '<div style="margin-bottom: 0.1em; font-weight: bold; color: %s;">%s</div>' % (color, name)
html_fields += '<div style="margin-left: 1em; margin-bottom: 0.3em; color: %s;">%s</div>' % \
(color, getattr(self.host, str(attr)))
if getattr(self.host, str(attr)) == '':
html_fields += '<br />'
'<p>'
if success:
htmltext(_('''\
The following authentication form parameters have been detected. If they look right, you can go to the next step.
If you think they are wrong, go back and check your settings then try again.
'''))
else:
htmltext(_('''\
The following authentication form parameters in red haven't been correctly detected. Go back and check
your settings then try again.
'''))
'</p>'
html_fields
form.render()
def credentials [html] (self):
form = self.form_credentials()
if form.get_widget('cancel').parse():
return redirect('check_auto_detected_configuration')
if form.is_submitted() and not form.has_errors():
self.submit_credentials_form(form)
if form.get_widget('terminate').parse():
return redirect('..')
return redirect('send_authentication_request')
get_response().breadcrumb.append(('credentials', _('Credentials')))
self.html_top(_('Step 5 - Fill in a valid username/password for this site'))
form.render()
def form_credentials(self):
form = Form(enctype='multipart/form-data')
form.add(StringWidget, 'username', title = _('Username'), required = True,
size = 30, value = self.host.valid_username)
form.add(PasswordWidget, 'password', title = _('Password'), required = True,
size = 30, value = self.host.valid_password)
for name, values in self.host.select_fields.iteritems():
options = []
if values:
for value in values:
options.append(value)
form.add(SingleSelectWidget, name, title = name.capitalize(),
value = values[0], options = options)
form.add_submit('cancel', _('Previous'))
form.add_submit('submit', _('Next'))
form.add_submit('terminate', _('Terminate'))
return form
def submit_credentials_form(self, form):
self.host.valid_username = form.get_widget('username').parse()
self.host.valid_password = form.get_widget('password').parse()
self.host.valid_select = {}
for name, values in self.host.select_fields.iteritems():
if form.get_widget(name):
self.host.valid_select[name] = form.get_widget(name).parse()
self.host.store()
def send_authentication_request(self):
site_auth = site_authentication.get_site_authentication(self.host)
# Request with good credentials
self.host.auth_request_status, self.host.auth_request_data = site_auth.local_auth_check_dispatch(
self.host.valid_username, self.host.valid_password, self.host.valid_select)
self.host.auth_request_success, self.host.auth_request_return_content = \
site_auth.check_auth(self.host.auth_request_status, self.host.auth_request_data)
# Request with bad credentials
self.host.auth_bad_request_status, self.host.auth_bad_request_data = site_auth.local_auth_check_dispatch(
'this_is_a_bad_login', 'this_is_a_bad_password', {})
self.host.auth_bad_request_success, self.host.auth_bad_request_return_content = \
site_auth.check_auth(self.host.auth_bad_request_status, self.host.auth_bad_request_data)
self.host.store()
return redirect('check_authentication')
def check_authentication [html] (self):
form = Form(enctype='multipart/form-data')
form.add_submit('cancel', _('Previous'))
form.add_submit('submit', _('Next'))
form.add_submit('terminate', _('Terminate'))
if form.get_widget('cancel').parse():
return redirect('credentials')
if form.is_submitted():
if form.get_widget('terminate').parse():
return redirect('..')
return redirect('sso_init_link')
get_response().breadcrumb.append(('check_authentication', _('Check authentication')))
self.html_top(_('Step 6 - Check the authentication process'))
if self.host.auth_request_success:
good_cred_status = 'Success <span style="color: green">[OK]</span>'
else:
good_cred_status = 'Failed <span style="color: red">[KO]</span'
if not self.host.auth_bad_request_success:
bad_cred_status = 'Failed <span style="color: green">[OK]</span>'
else:
bad_cred_status = 'Success <span style="color: red">[KO]</span>'
'<p>Results of authentication requests :</p>\n'
'<ul>\n'
'\t<li>With good credentials : %s</li>' % good_cred_status
'\t<li>With bad credentials : %s</li>' % bad_cred_status
'</ul>\n'
if self.host.auth_request_success and not self.host.auth_bad_request_success :
'<p>%s</p>\n' % _('Authentication succeeded ! You can go to the next step.')
else:
'<p>%s</p>\n' % _('Authentication has failed. To resolve this problem, you can :')
'<ul>\n'
'\t<li><a href="send_authentication_request">%s</a></li>\n' % \
_('Try authentication again')
'\t<li><a href="see_authentication_response">%s</a></li>\n' % \
_('See the response of the authentication requests')
'\t<li><a href="modify_authentication_request">%s</a></li>\n' % \
_('Modify the parameters of the authentication requests')
'\t<li><a href="authentication_success_criteria">%s</a></li>\n' % \
_('Change the way Larpe detects the authentication is successful or not')
'\t<li>%s</li>\n' % _('Go back and change your username and/or password')
'</ul>\n'
form.render()
def see_authentication_response [html] (self):
get_response().breadcrumb.append(('see_authentication_response', _('Authentication response')))
self.html_top(_('Authentication response'))
'<h3>%s</h3>' % _('Response of the request with good credentials')
'<div style="margin-bottom: 0.1em; font-weight: bold; color: black;">%s</div>' % \
str(_('HTTP status code'))
'<div style="margin-left: 1em; margin-bottom: 0.3em; color: black;">%s (%s)</div>' % \
(self.host.auth_request_status, status_reasons[self.host.auth_request_status])
'<dl>'
'<dt><a href="see_bad_response_html_page">%s</a></dt>' % _('See HTML page')
'</dl>'
'<h3>%s</h3>' % _('Response of the request with bad credentials')
'<div style="margin-bottom: 0.1em; font-weight: bold; color: black;">%s</div>' % \
str(_('HTTP status code'))
'<div style="margin-left: 1em; margin-bottom: 0.3em; color: black;">%s (%s)</div>' % \
(self.host.auth_bad_request_status, status_reasons[self.host.auth_bad_request_status])
'<dl>'
'<dt><a href="see_response_html_page">%s</a></dt>' % _('See HTML page')
'</dl>'
'<div class="buttons"><a href="check_authentication"><input type="button" value="%s" /></a></div><br />' % _('Back')
def see_response_html_page (self):
return self.host.auth_request_data
def see_bad_response_html_page (self):
return self.host.auth_bad_request_data
def authentication_success_criteria [html] (self):
form = self.form_authentication_success_criteria()
if form.get_widget('cancel').parse():
return redirect('check_authentication')
if form.is_submitted() and not form.has_errors():
self.submit_authentication_success_criteria_form(form)
return redirect('check_authentication')
get_response().breadcrumb.append(('authentication_success_criteria', _('Authentication success criteria')))
self.html_top(_('Authentication success criteria'))
form.render()
def form_authentication_success_criteria(self):
form = Form(enctype='multipart/form-data')
form.add(RadiobuttonsWidget, 'auth_system', title = _('Authentication system of the original site'),
options=[
('password', _('Check the existence of a password field'), 'password'),
('match_text', _('Match some text to detect an authentication failure'), 'match_text'),
],
sort=False,
delim=htmltext('<br />'),
value = self.host.auth_system)
form.add(RegexStringWidget, 'auth_match_text', title = _('Text to match in case of authentication failure'),
required = False, size = 50, value = self.host.auth_match_text)
form.add_submit('submit', _('Submit'))
form.add_submit('cancel', _('Cancel'))
return form
def submit_authentication_success_criteria_form(self, form):
for f in ('auth_system', 'auth_match_text'):
value = form.get_widget(f).parse()
setattr(self.host, f, value)
self.host.store()
def modify_authentication_request [html] (self):
get_response().breadcrumb.append(('modify_authentication_request', _('Authentication request')))
self.html_top(_('Modify the parameters of the authentication requests'))
'<dl>'
'<dt><a href="auth_request_post_parameters">%s</a></dt> <dd>%s</dd>' % (
_('Modify POST parameters'), _('Configure the form attributes that will be sent within the authentication POST requests'))
'<dt><a href="auth_request_http_headers">%s</a></dt> <dd>%s</dd>' % (
_('Modify HTTP headers'), _('Configure the HTTP headers of the authentication requests made by Larpe'))
'</dl>'
'<div class="buttons"><a href="check_authentication"><input type="button" value="%s" /></a></div><br />' % _('Back')
def auth_request_post_parameters [html] (self):
form = self.form_auth_request_post_parameters()
if form.get_widget('cancel').parse():
return redirect('modify_authentication_request')
if form.is_submitted() and not form.has_errors():
self.submit_auth_request_post_parameters_form(form)
return redirect('modify_authentication_request')
get_response().breadcrumb.append(('auth_request_post_parameters', _('POST parameters')))
self.html_top(_('Configure POST parameters'))
'<p>%s</p>' % _('''Here are the detected form fields that will be sent as parameters of the
authentication POST request. You can desactivate some or all of them, or change their value.''')
form.render()
def form_auth_request_post_parameters(self):
form = Form(enctype='multipart/form-data')
for name, value in self.host.post_parameters.iteritems():
if value['immutable']:
form.add(DictWidget, name, value, disabled = 'disabled')
else:
form.add(DictWidget, name, value)
form.add_submit('submit', _('Submit'))
form.add_submit('cancel', _('Cancel'))
return form
def submit_auth_request_post_parameters_form(self, form):
for name, old_value in self.host.post_parameters.iteritems():
value = form.get_widget(name).parse()
if value['enabled'] == 'on':
old_value['enabled'] = True
else:
old_value['enabled'] = False
if old_value['immutable'] is False:
old_value['value'] = value['value']
self.host.post_parameters[name] = old_value
self.host.store()
def auth_request_http_headers [html] (self):
form = self.form_auth_request_http_headers()
if form.get_widget('cancel').parse():
return redirect('modify_authentication_request')
if form.is_submitted() and not form.has_errors():
self.submit_auth_request_http_headers_form(form)
return redirect('modify_authentication_request')
get_response().breadcrumb.append(('auth_request_http_headers', _('HTTP headers')))
self.html_top(_('Configure HTTP headers'))
'<p>%s</p>' % _('''Here are the HTTP headers that will be sent within the authentication
POST request. You can desactivate some or all of them, or change their value.''')
form.render()
def form_auth_request_http_headers(self):
form = Form(enctype='multipart/form-data')
for name, value in self.host.http_headers.iteritems():
if value['immutable']:
form.add(DictWidget, name, value, disabled = 'disabled')
else:
form.add(DictWidget, name, value)
form.add(HtmlWidget, htmltext('<p>%s</p>' % \
_('The headers "Host", "Accept-Encoding" and "Content-Length" will also automatically be sent.')))
if get_cfg('proxy', {}).get('enabled') and self.host.use_proxy:
form.add(HtmlWidget, htmltext('<p>%s</p>' % \
_('''As Larpe uses a proxy for this site, the headers "Proxy-Authorization",
"Proxy-Connection" and "Keep-Alive" will be sent as well.''')))
form.add_submit('submit', _('Submit'))
form.add_submit('cancel', _('Cancel'))
return form
def submit_auth_request_http_headers_form(self, form):
for name, old_value in self.host.http_headers.iteritems():
value = form.get_widget(name).parse()
if value['enabled'] == 'on':
old_value['enabled'] = True
else:
old_value['enabled'] = False
if old_value['immutable'] is False:
old_value['value'] = value['value']
self.host.http_headers[name] = old_value
self.host.store()
def generate_apache_filters(self):
self.host.apache_python_paths = []
self.host.apache_output_python_filters = []
site_auth = site_authentication.get_site_authentication(self.host)
output_filters = site_auth.output_filters
replace_login_form = self.host.auth_form_places == 'form_everywhere' and \
self.host.auth_form_action
if replace_login_form and not 'output_replace_form' in output_filters:
output_filters.append('output_replace_form')
if output_filters:
location = Location(self.host)
conf = { 'auth_form_action': self.host.auth_form_action,
'name': self.host.name,
'larpe_dir': get_publisher().app_dir,
'logout_url': location.new_logout_url,
'login_url': location.new_auth_url }
# Set Python filter path for Apache configuration
python_path = os.path.join(self.host.site_dir, 'filters')
if python_path not in self.host.apache_python_paths:
self.host.apache_python_paths.append(python_path)
for filter in output_filters:
python_file = open(
os.path.join(self.host.site_dir, 'filters', filter + ".py"),
'w')
python_file.write(
open(os.path.join(DATA_DIR, "filters", filter + ".py")).read() % conf
)
if not filter in self.host.apache_output_python_filters:
self.host.apache_output_python_filters.append(filter)
def sso_init_link [html] (self):
form = self.form_sso_init_link()
if form.get_widget('cancel').parse():
return redirect('check_authentication')
if form.is_submitted() and not form.has_errors():
self.submit_sso_init_link_form(form)
if form.get_widget('terminate').parse():
return redirect('..')
return redirect('metadatas')
get_response().breadcrumb.append(('sso_init_link', _('SSO initiation')))
self.html_top(_('Step 7 - Configure how a Single Sign On can be initiated'))
'<p>%s\n' % _('Most sites use one of the following 2 ways to allow users to initialise an authentication :')
'\t<ol>\n'
'\t\t<li>%s</li>\n' % \
_('''The site has a single authentication page. It redirects users to this page when
they click a "Login" button or try to access a page which require users to be authenticated.''')
'\t\t<li>%s</li>\n' % \
_('''The site includes an authentication form in most or all of his pages. Users can
authenticate on any of these pages, and don't need to be redirected to a separate authentication page.''')
'\t</ol>\n'
'</p>\n'
'<p>%s</p>' % _('Select the way your site works :')
form.render()
def form_sso_init_link(self):
form = Form(enctype='multipart/form-data')
form.add(RadiobuttonsWidget, 'auth_form_places',
options=[
('form_once', _('The site has a single authentication page'), 'form_once'),
('form_everywhere', _('The site includes an authentication form in most or all pages'), 'form_everywhere'),
],
sort=False, required = True, delim=htmltext('<br />'), value = self.host.auth_form_places)
form.add_submit('cancel', _('Previous'))
form.add_submit('submit', _('Next'))
form.add_submit('terminate', _('Terminate'))
return form
def submit_sso_init_link_form(self, form):
fields = [ 'auth_form_places', ]
for f in fields:
setattr(self.host, f, form.get_widget(f).parse())
self.host.auth_form_url = self.host.auth_url
self.generate_apache_filters()
self.host.store()
write_apache2_vhosts()
def metadatas [html] (self):
form = Form(enctype='multipart/form-data')
form.add_submit('cancel', _('Previous'))
form.add_submit('submit', _('Next'))
form.add_submit('terminate', _('Terminate'))
if form.get_widget('cancel').parse():
return redirect('sso_init_link')
if form.is_submitted():
if form.get_widget('terminate').parse():
return redirect('..')
return redirect('advanced_options')
get_response().breadcrumb.append(('metadatas', _('Metadatas')))
self.html_top(_('Step 8 - Metadatas of %(site_name)s' % {'site_name': self.host.name}))
'<p>%s</p>' % \
_('''Download the metadatas and the public key for this site and
upload them on your identity provider in order to use Liberty Alliance features.''')
'<dl>'
if hasattr(self.host, str('base_url')):
if lasso.SAML2_SUPPORT:
saml2_metadata_url = '%s/metadata.xml' % self.host.saml2_base_url
'<dt><a href="%s">%s</a></dt> <dd>%s</dd>' % (
saml2_metadata_url,
_('SAML 2.0 Metadata'),
_('Download SAML 2.0 metadata file'))
metadata_url = '%s/metadata.xml' % self.host.base_url
'<dt><a href="%s">%s</a></dt> <dd>%s</dd>' % (
metadata_url,
_('ID-FF 1.2 Metadata'),
_('Download ID-FF 1.2 metadata file'))
else:
'<p>%s</p>' % _('No metadata has been generated for this host.')
if hasattr(self.host, str('base_url')) and self.host.public_key and os.path.exists(self.host.public_key):
public_key_url = '%s/public_key' % self.host.base_url
'<dt><a href="%s">%s</a></dt> <dd>%s</dd>' % (
public_key_url,
_('Public key'),
_('Download SSL Public Key file'))
else:
'<p>%s</p>' % _('No public key has been generated for this host.')
'</dl>'
form.render()
def advanced_options [html] (self):
form = self.form_advanced_options()
if form.get_widget('cancel').parse():
return redirect('metadatas')
if not form.is_submitted() or form.has_errors():
get_response().breadcrumb.append(('advanced_options', _('Advanced options')))
self.html_top(_('Step 9 - Advanced options'))
'<p>%s</p>' % _('Configure advanced options to setup the last details of your site.')
'<p>%s</p>' % _('''If you don't know what to configure here, just click %(next)s and
come here later if needed.''') % {'next': _('Next')}
form.render()
else:
self.submit_advanced_options_form(form)
if form.get_widget('terminate').parse():
return redirect('..')
return redirect('check_full_configuration')
def form_advanced_options(self):
form = Form(enctype='multipart/form-data')
form.add(CheckboxWidget, 'redirect_root_to_login',
title=_('Redirect the root URL of the site to the login page.'),
value = self.host.redirect_root_to_login)
form.add(UrlOrAbsPathWidget, 'return_url', title = _('Return address'),
hint = _('Where the user will be redirected after a successful authentication'),
required = False, size = 50, value = self.host.return_url)
form.add(UrlOrAbsPathWidget, 'root_url', title = _('Error address'),
hint = _('Where the user will be redirected after a disconnection or an error'),
required = False, size = 50, value = self.host.root_url)
form.add(UrlOrAbsPathWidget, 'initiate_sso_url', title = _('URL which must initiate the SSO'),
hint = _('''Address which must initiate the SSO. If empty, defaults to the previously
specified "%s"''') % _('Authentication form page address'),
required = False, size = 50, value = self.host.initiate_sso_url)
form.add(CheckboxWidget, 'proxy-html', title = _('Apache HTML proxy'),
hint = _('''Converts urls in the HTML pages according to the host new domain name.
Disabled by default because it makes some sites not work correctly.'''),
value = 'proxy-html' in self.host.apache_output_filters)
form.add_submit('cancel', _('Previous'))
form.add_submit('submit', _('Next'))
form.add_submit('terminate', _('Terminate'))
return form
def submit_advanced_options_form(self, form):
old_redirect_root_to_login = self.host.redirect_root_to_login
for f in ('redirect_root_to_login', 'return_url', 'root_url', 'initiate_sso_url'):
value = form.get_widget(f).parse()
setattr(self.host, f, value)
f = 'proxy-html'
value = form.get_widget(f).parse()
if value is True and f not in self.host.apache_output_filters:
self.host.apache_output_filters.append(f)
if value is False and f in self.host.apache_output_filters:
self.host.apache_output_filters.remove(f)
self.host.store()
if self.host.initiate_sso_url or self.host.redirect_root_to_login is not old_redirect_root_to_login:
write_apache2_vhosts()
def check_full_configuration [html] (self):
form = Form(enctype='multipart/form-data')
form.add_submit('cancel', _('Previous'))
form.add_submit('submit', _('Finish'))
if form.get_widget('cancel').parse():
return redirect('advanced_options')
if form.is_submitted():
return redirect('../..')
get_response().breadcrumb.append(('check_full_configuration', _('Check everything works')))
self.html_top(_('Step 10 - Check everything works'))
'<p>%s</p>' % \
_('''Now you can fully test your site, start from the home page, initiate a
Single Sign On, federate your identities and do a Single Logout.''')
'<p>%s' % _('The address of your site is : ')
'<a href="%s">%s</a>' % (self.host.new_url, self.host.new_url)
'</p>'
'<p>%s</p>' % \
_('''If everything works, click the "%(finish)s" button, otherwise you can go
back and check your settings.''') % { 'finish': _('Finish') }
form.render()
def auto_detect_configuration(self):
# Reset previous detected values
self.host.auth_form = None
self.host.auth_check_url = None
self.host.login_field_name = None
self.host.password_field_name = None
if not self.host.post_parameters:
self.host.post_parameters = {}
self.parse_page(self.host.auth_form_url)
def parse_page(self, page_url):
# Get the authentication page
try:
response, status, page, auth_header = http_get_page(page_url, use_proxy=self.host.use_proxy)
except Exception, msg:
print msg
return
if page is None:
return
#raise FormError, ('auth_check_url', '%s : %s' % (_('Failed to get page'), self.host.auth_form_url))
# Default authentication mode
self.host.auth_mode = 'form'
self.host.site_authentication_plugin = site_authentication_plugins.auto_detect(page)
self.parse_frames(page)
self.parse_forms(page)
if self.host.auth_form is not None:
self.parse_form_action()
input_fields = self.parse_input_fields()
self.parse_login_field(input_fields)
self.parse_password_field()
self.parse_select_fields()
self.parse_other_fields()
def parse_frames(self, page):
'''If there are frames, parse them recursively'''
regexp = re.compile("""<frame.*?src=["'](.*?)["'][^>]*?>""", re.DOTALL | re.IGNORECASE)
found_frames = regexp.findall(page)
if found_frames:
for frame_url in found_frames:
if frame_url.startswith('http'):
frame_full_url = frame_url
else:
page_url_tokens = frame_url.split('/')
page_url_tokens[-1] = frame_url
frame_full_url = '/'.join(page_url_tokens)
self.parse_page(frame_full_url)
def parse_forms(self, page):
'''Search for an authentication form'''
# Get all forms
regexp = re.compile("""<form.*?</form>""", re.DOTALL | re.IGNORECASE)
found_forms = regexp.findall(page)
if not found_forms:
return
#raise FormError, ('auth_check_url', '%s : %s' % (_('Failed to find any form'), self.host.auth_form_url))
# Get the first form with a password field
for found_form in found_forms:
regexp = re.compile("""<input[^>]*?type=["']?password["']?[^>]*?>""", re.DOTALL | re.IGNORECASE)
if regexp.search(found_form) is not None:
self.host.auth_form = found_form
break
def parse_form_action(self):
'''Get the action url of the form'''
regexp = re.compile("""<form.*?action=["']?(.*?)["']?[\s>].*?>""", re.DOTALL | re.IGNORECASE)
self.host.auth_form_action = regexp.findall(self.host.auth_form)[0]
# FIXME: Find a Python module which unescapes html entities
self.host.auth_check_url = self.host.auth_form_action.replace('&amp;', '&')
if not self.host.auth_check_url.startswith('http'):
if self.host.auth_check_url.startswith('/'):
if self.host.orig_site.startswith('https'):
orig_site_root = 'https://%s' % urllib.splithost(self.host.orig_site[6:])[0]
else:
orig_site_root = 'http://%s' % urllib.splithost(self.host.orig_site[5:])[0]
self.host.auth_check_url = orig_site_root + self.host.auth_check_url
else:
auth_form_url_tokens = self.host.auth_form_url.split('/')
auth_form_url_tokens[-1] = self.host.auth_check_url
self.host.auth_check_url = '/'.join(auth_form_url_tokens)
def parse_input_fields(self):
'''Get all input fields'''
regexp = re.compile("""<input[^>]*?>""", re.DOTALL | re.IGNORECASE)
return regexp.findall(self.host.auth_form)
def parse_login_field(self, input_fields):
'''Get login field name'''
try:
regexp = re.compile("""<input[^>]*?type=["']?text["']?[^>]*?>""", re.DOTALL | re.IGNORECASE)
text_fields = regexp.findall(self.host.auth_form)
login_field = ''
if text_fields:
login_field = text_fields[0]
else:
for field in input_fields:
if re.search("""type=["']?""", field, re.DOTALL | re.IGNORECASE) is None:
login_field = field
break
regexp = re.compile("""name=["']?(.*?)["']?[\s/>]""", re.DOTALL | re.IGNORECASE)
self.host.login_field_name = regexp.findall(login_field)[0]
if not self.host.post_parameters.has_key(self.host.login_field_name):
self.host.post_parameters[self.host.login_field_name] = \
{ 'enabled': True, 'value': _('(filled by users)'), 'immutable': True }
self.host.store()
except IndexError, e:
self.host.login_field_name = None
print 'Error handling login field : %s' % e
def parse_password_field(self):
'''Get password field name'''
try:
regexp = re.compile("""<input[^>]*?type=["']?password["']?[^>]*?>""", re.DOTALL | re.IGNORECASE)
password_field = regexp.findall(self.host.auth_form)[0]
regexp = re.compile("""name=["']?(.*?)["']?[\s/>]""", re.DOTALL | re.IGNORECASE)
self.host.password_field_name = regexp.findall(password_field)[0]
if not self.host.post_parameters.has_key(self.host.password_field_name):
self.host.post_parameters[self.host.password_field_name] = \
{ 'enabled': True, 'value': _('(filled by users)'), 'immutable': True }
except IndexError, e:
self.host.password_field_name = None
print 'Error handling password field : %s' % e
def parse_select_fields(self):
'''Add select fields to host attributes'''
# First added for Imuse (Rennes)
regexp = re.compile("""<select.*?</select>""", re.DOTALL | re.IGNORECASE)
self.host.select_fields = {}
for field in regexp.findall(self.host.auth_form):
try:
regexp = re.compile("""name=["']?(.*?)["']?[\s/>]""", re.DOTALL | re.IGNORECASE)
name = regexp.findall(field)[0]
regexp = re.compile("""<option[^>]*?>.*?</option>""", re.DOTALL | re.IGNORECASE)
options = regexp.findall(field)
values = []
for option in options:
regexp = re.compile("""<option[^>]*?>(.*?)</option>""", re.DOTALL | re.IGNORECASE)
option_label = regexp.findall(option)
regexp = re.compile("""value=["']?(.*?)["']?[\s/>]""", re.DOTALL | re.IGNORECASE)
option_value = regexp.findall(option)
if option_label:
if not option_value:
option_value = option_label
values.append((option_value[0], option_label[0]))
else:
print >> sys.stderr, 'W: Could not parse select options'
self.host.select_fields[name] = values
if not self.host.post_parameters.has_key(name):
self.host.post_parameters[name] = \
{ 'enabled': True, 'value': _('(filled by users)'), 'immutable': True }
except IndexError, e:
continue
def parse_other_fields(self):
'''Get the default value of all other fields'''
self.host.other_fields = {}
# Get hidden fields
regexp = re.compile("""<input[^>]*?type=["']?hidden["']?[^>]*?>""", re.DOTALL | re.IGNORECASE)
other_fields = regexp.findall(self.host.auth_form)
# Only get first submit field
regexp = re.compile("""<input[^>]*?type=["']?submit["']?[^>]*?>""", re.DOTALL | re.IGNORECASE)
found = regexp.findall(self.host.auth_form)
if found:
if other_fields:
other_fields.append(found[0])
else:
other_fields = found[0]
for field in other_fields:
try:
regexp = re.compile("""name=["']?(.*?)["']?[\s/>]""", re.DOTALL | re.IGNORECASE)
name = regexp.findall(field)[0]
regexp = re.compile("""value=["'](.*?)["'][\s/>]""", re.DOTALL | re.IGNORECASE)
value = regexp.findall(field)[0]
self.host.other_fields[name] = value
if not self.host.post_parameters.has_key(name):
self.host.post_parameters[name] = { 'enabled': True, 'value': value, 'immutable': False }
except IndexError, e:
continue
class HostPage(Directory):
_q_exports = ['', 'delete']
def __init__(self, host_id):
self.host = Host.get(host_id)
get_response().breadcrumb.append((host_id + '/', self.host.label))
def _q_lookup(self, component):
if component == 'configuration_assistant':
return ConfigurationAssistant(self.host)
elif component == 'forms_prefill':
return FormsDirectory(self.host)
def _q_index [html] (self):
get_publisher().reload_cfg()
html_top('hosts', title = self.host.label)
'<h2>%s</h2>' % _('Configuration assistant')
'<dl>'
'<dt><a href="configuration_assistant/start">%s</a></dt> <dd>%s</dd>' % (
_('Address of the original site'), _('Configure the root address of the site'))
'<dt><a href="configuration_assistant/check_new_address">%s</a></dt> <dd>%s</dd>' % (
_('New address and name'), _('Configure the new address and name of this site'))
'<dt><a href="configuration_assistant/authentication_and_logout_adresses">%s</a></dt> <dd>%s</dd>' % (
_('Authentication and logout addresses'), _('Configure the authentication and logout addresses of the original site'))
'<dt><a href="configuration_assistant/check_auto_detected_configuration">%s</a></dt> <dd>%s</dd>' % (
_('Check auto detected configuration'), _('Check the automatically detected configuration is right'))
'<dt><a href="configuration_assistant/credentials">%s</a></dt> <dd>%s</dd>' % (
_('Credentials'), _('Configure some valid credentials to authenticate on the original site'))
'<dt><a href="configuration_assistant/send_authentication_request">%s</a></dt> <dd>%s</dd>' % (
_('Retry authentication'), _('Retry sending an authentication request to the site to check if your new parameters work well'))
'<dt><a href="configuration_assistant/see_authentication_response">%s</a></dt> <dd>%s</dd>' % (
_('Check authentication response'), _('Check the response from the latest authentication request'))
'<dt><a href="configuration_assistant/authentication_success_criteria">%s</a></dt> <dd>%s</dd>' % (
_('Configure authentication success criteria'), _('Specify how Larpe knows if the authentication has succeeded or not'))
'<dt><a href="configuration_assistant/modify_authentication_request">%s</a></dt> <dd>%s</dd>' % (
_('Modify authentication request'), _('Modify POST fields or HTTP headers of the authentication request'))
'<dt><a href="configuration_assistant/sso_init_link">%s</a></dt> <dd>%s</dd>' % (
_('Configure how a Single Sign On can be initiated'), _('Configure how a Single Sign On can be initiated'))
'<dt><a href="configuration_assistant/metadatas">%s</a></dt> <dd>%s</dd>' % (
_('Metadatas and key'), _('Download SAML 2.0 or ID-FF metadatas and SSL public key'))
'<dt><a href="configuration_assistant/advanced_options">%s</a></dt> <dd>%s</dd>' % (
_('Adavanced options'), _('Configure advanced options to setup the last details of your site'))
'</dl>'
'<h2>%s</h2>' % _('Form prefilling with ID-WSF')
'<dl>'
'<dt><a href="forms_prefill/">%s</a></dt> <dd>%s</dd>' % (
_('Forms'), _('Configure the forms to prefill'))
'</dl>'
def delete [html] (self):
form = Form(enctype='multipart/form-data')
form.widgets.append(HtmlWidget('<p>%s</p>' % _(
'You are about to irrevocably delete this host.')))
form.add_submit('submit', _('Submit'))
form.add_submit('cancel', _('Cancel'))
if form.get_widget('cancel').parse():
return redirect('..')
if not form.is_submitted() or form.has_errors():
get_response().breadcrumb.append(('delete', _('Delete')))
html_top('hosts', title = _('Delete Host'))
'<h2>%s : %s</h2>' % (_('Delete Host'), self.host.label)
form.render()
else:
self.host.remove_self()
write_apache2_vhosts()
return redirect('..')
class HostsDirectory(Directory):
_q_exports = ['', 'new']
def _q_index [html] (self):
get_response().breadcrumb.append(('hosts/', _('Hosts')))
html_top('hosts', title = _('Hosts'))
"""<ul id="nav-hosts-admin">
<li><a href="new">%s</a></li>
</ul>""" % _('New Host')
'<ul class="biglist">'
for host in Host.select(lambda x: x.name != 'larpe', order_by = 'label'):
if not host.name:
continue
if not hasattr(host, str('scheme')):
host.scheme = str('http')
'<li>'
'<strong class="label">%s</strong>' % host.label
if hasattr(host, str('new_url')) and host.new_url:
url = host.new_url
else:
# Compat with older Larpe versions
url = '%s://%s%s/' % (host.scheme, host.reversed_hostname, get_request().environ['SCRIPT_NAME'])
if host.reversed_directory is not None:
url += '%s/' % host.reversed_directory
'<br /><a href="%s">%s</a>' % (url, url)
'<p class="commands">'
command_icon('%s/' % host.id, 'edit')
command_icon('%s/delete' % host.id, 'remove')
'</p></li>'
'</ul>'
def new [html] (self):
if not os.path.isdir(os.path.join(get_publisher().app_dir, str('idp'))):
html_top('hosts', title = _('New Host'))
html = '<h2>%s</h2>' % _('New Host')
html += 'You must <a href="%s/admin/settings/liberty_idp/">' % misc.get_root_url()
html += 'configure an Identity Provider</a> first<br /><br />'
html += '<a href="."><input type="button" value="%s" /></a>' % _('Back')
return html
get_response().breadcrumb.append(('hosts/', _('Hosts')))
get_response().breadcrumb.append(('new', _('New')) )
host = Host()
host.store()
return redirect('%s/configuration_assistant/start' % host.id)
def _q_lookup(self, component):
get_response().breadcrumb.append(('hosts/', _('Hosts')))
return HostPage(component)