refactor login box & form to enable easy pluggability
* vregistry.selectable: get all selectable object of fixed oid with given context
* template headeri, logbox, logform: reorganize a bit the structure
--- a/selectors.py Mon Oct 04 18:55:57 2010 +0200
+++ b/selectors.py Mon Oct 04 18:56:05 2010 +0200
@@ -1182,7 +1182,6 @@
"""
return ~ authenticated_user()
-
class match_user_groups(ExpectedValueSelector):
"""Return a non-zero score if request's user is in at least one of the
groups given as initializer argument. Returned score is the number of groups
@@ -1213,6 +1212,24 @@
return score
+class configuration_values(Selector):
+ """Return 1 if the instance is configured according to
+ the given value(s)"""
+
+ def __init__(self, key, values):
+ self._key = key
+ if isinstance(values, basestring):
+ values = (values,)
+ self._values = frozenset(values)
+
+ @lltrace
+ def __call__(self, cls, req, **kwargs):
+ try:
+ return self._score
+ except AttributeError:
+ self._score = req.vreg.config[self._key] in self._values
+ return self._score
+
# Web request selectors ########################################################
# XXX deprecate
--- a/vregistry.py Mon Oct 04 18:55:57 2010 +0200
+++ b/vregistry.py Mon Oct 04 18:56:05 2010 +0200
@@ -195,6 +195,18 @@
select_object = deprecated('[3.6] use select_or_none instead of select_object'
)(select_or_none)
+ def selectable(self, oid, *args, **kwargs):
+ """return all appobjects having the given oid that are
+ selectable against the given context, in score order
+ """
+ objects = []
+ for appobject in self[oid]:
+ score = appobject.__select__(appobject, *args, **kwargs)
+ if score > 0:
+ objects.append((score, appobject))
+ return [obj(*args, **kwargs)
+ for _score, obj in sorted(objects)]
+
def possible_objects(self, *args, **kwargs):
"""return an iterator on possible objects in this registry for the given
context
--- a/web/data/cubicweb.htmlhelpers.js Mon Oct 04 18:55:57 2010 +0200
+++ b/web/data/cubicweb.htmlhelpers.js Mon Oct 04 18:56:05 2010 +0200
@@ -1,3 +1,14 @@
+/* in CW 3.10, we should move these functions in this namespace */
+cw.htmlhelpers = new Namespace('cw.htmlhelpers');
+
+jQuery.extend(cw.htmlhelpers, {
+ popupLoginBox: function(loginboxid, focusid) {
+ $('#'+loginboxid).toggleClass('hidden');
+ jQuery('#' + focusid +':visible').focus();
+ }
+});
+
+
/**
* .. function:: baseuri()
*
@@ -97,10 +108,11 @@
* toggles visibility of login popup div
*/
// XXX used exactly ONCE in basecomponents
-function popupLoginBox() {
- $('#popupLoginBox').toggleClass('hidden');
- jQuery('#__login:visible').focus();
-}
+popupLoginBox = cw.utils.deprecatedFunction(
+ function() {
+ $('#popupLoginBox').toggleClass('hidden');
+ jQuery('#__login:visible').focus();
+});
/**
* .. function getElementsMatching(tagName, properties, \/* optional \*\/ parent)
--- a/web/data/cubicweb.login.css Mon Oct 04 18:55:57 2010 +0200
+++ b/web/data/cubicweb.login.css Mon Oct 04 18:56:05 2010 +0200
@@ -5,25 +5,25 @@
* :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
*/
-div#popupLoginBox {
+div.popupLoginBox {
position: absolute;
z-index: 400;
right: 0px;
width: 26em;
padding: 0px 1px 1px;
- background: %(listingBorderColor)s;
+ background: %(listingBorderColor)s;
}
-div#popupLoginBox label{
+div.popupLoginBox label{
font-weight: bold;
}
-div#popupLoginBox div#loginContent {
+div.popupLoginBox div.loginContent {
background: #e6e4ce;
padding: 5px 3px 4px;
}
-div#loginBox {
+div.loginBox {
position : absolute;
top: 15%;
left : 50%;
@@ -40,7 +40,7 @@
font-size: 140%;
}
-div#loginTitle {
+div.loginTitle {
color: #fff;
font-weight: bold;
font-size: 140%;
@@ -49,32 +49,32 @@
background: %(headerBgColor)s url("banner.png") repeat-x top left;
}
-div#loginBox div#loginContent form {
+div.loginBox div.loginContent form {
padding-top: 1em;
width: 90%;
margin: auto;
}
-#popupLoginBox table td {
+.popupLoginBox table td {
padding: 0px 3px;
white-space: nowrap;
}
-#loginContent table {
+.loginContent table {
padding: 0px 0.5em;
margin: auto;
}
-#loginBox table td {
+.loginBox table td {
padding: 0px 3px 0.6em;
white-space: nowrap;
}
-#loginBox .loginButton {
+.loginBox .loginButton {
margin-top: 0.6em;
}
-#loginContent input.data {
+.loginContent input.data {
width: 12em;
}
@@ -85,6 +85,6 @@
background: #f0eff0 url("gradient-grey-up.png") left top repeat-x;
}
-#loginContent .formButtonBar {
+.loginContent .formButtonBar {
float: right;
}
--- a/web/views/authentication.py Mon Oct 04 18:55:57 2010 +0200
+++ b/web/views/authentication.py Mon Oct 04 18:56:05 2010 +0200
@@ -37,6 +37,7 @@
class WebAuthInfoRetreiver(Component):
__registry__ = 'webauth'
order = None
+ __abstract__ = True
def authentication_information(self, req):
"""retreive authentication information from the given request, raise
@@ -51,6 +52,18 @@
"""
pass
+ def request_has_auth_info(self, req):
+ """tells from the request if it has enough information
+ to proceed to authentication, would the current session
+ be invalidated
+ """
+ raise NotImplementedError()
+
+ def revalidate_login(self, req):
+ """returns a login string or None, for repository session
+ validation purposes
+ """
+ return None
class LoginPasswordRetreiver(WebAuthInfoRetreiver):
__regid__ = 'loginpwdauth'
@@ -65,6 +78,11 @@
raise NoAuthInfo()
return login, {'password': password}
+ def request_has_auth_info(self, req):
+ return '__login' in req.form
+
+ def revalidate_login(self, req):
+ return req.get_authorization()[0]
class RepositoryAuthenticationManager(AbstractAuthenticationManager):
"""authenticate user associated to a request and check session validity"""
@@ -73,7 +91,7 @@
super(RepositoryAuthenticationManager, self).__init__(vreg)
self.repo = vreg.config.repository(vreg)
self.log_queries = vreg.config['query-log-file']
- self.authinforetreivers = sorted(vreg['webauth'].possible_objects(vreg),
+ self.authinforetrievers = sorted(vreg['webauth'].possible_objects(vreg),
key=lambda x: x.order)
# 2-uple login / password, login is None when no anonymous access
# configured
@@ -88,10 +106,19 @@
raise :exc:`InvalidSession` if session is corrupted for a reason or
another and should be closed
+
+ also invoked while going from anonymous to logged in
"""
# with this authentication manager, session is actually a dbapi
# connection
- login = req.get_authorization()[0]
+ for retriever in self.authinforetrievers:
+ if retriever.request_has_auth_info(req):
+ login = retriever.revalidate_login(req)
+ return self._validate_session(req, session, login)
+ # let's try with the current session
+ return self._validate_session(req, session, None)
+
+ def _validate_session(self, req, session, login):
# check session.login and not user.login, since in case of login by
# email, login and cnx.login are the email while user.login is the
# actual user login
@@ -114,18 +141,19 @@
raise :exc:`cubicweb.AuthenticationError` if authentication failed
(no authentication info found or wrong user/password)
"""
- for retreiver in self.authinforetreivers:
+ for retriever in self.authinforetrievers:
try:
- login, authinfo = retreiver.authentication_information(req)
+ login, authinfo = retriever.authentication_information(req)
except NoAuthInfo:
continue
try:
cnx = self._authenticate(login, authinfo)
except AuthenticationError:
continue # the next one may succeed
- for retreiver_ in self.authinforetreivers:
- retreiver_.authenticated(retreiver, req, cnx, login, authinfo)
+ for retriever_ in self.authinforetrievers:
+ retriever_.authenticated(retriever, req, cnx, login, authinfo)
return cnx, login, authinfo
+
# false if no authentication info found, eg this is not an
# authentication failure
if 'login' in locals():
--- a/web/views/basecomponents.py Mon Oct 04 18:55:57 2010 +0200
+++ b/web/views/basecomponents.py Mon Oct 04 18:56:05 2010 +0200
@@ -28,8 +28,8 @@
from logilab.mtconverter import xml_escape
from rql import parse
-from cubicweb.selectors import (yes, multi_etypes_rset,
- match_form_params, match_context,
+from cubicweb.selectors import (yes, multi_etypes_rset, match_form_params,
+ match_context, configuration_values,
anonymous_user, authenticated_user)
from cubicweb.schema import display_name
from cubicweb.utils import wrap_on_write
@@ -97,24 +97,36 @@
"""if the user is the anonymous user, build a link to login else display a menu
with user'action (preference, logout, etc...)
"""
+ __abstract__ = True
cw_property_defs = VISIBLE_PROP_DEF
# don't want user to hide this component using an cwproperty
site_wide = True
__regid__ = 'loggeduserlink'
-class AnonUserLink(_UserLink):
- __select__ = _UserLink.__select__ & anonymous_user()
+class CookieAnonUserLink(_UserLink):
+ __select__ = _UserLink.__select__ & configuration_values('auth-mode', 'cookie') & anonymous_user()
+ loginboxid = 'popupLoginBox'
+
def call(self):
- if self._cw.vreg.config['auth-mode'] == 'cookie':
- self.w(self._cw._('anonymous'))
- self.w(u''' [<a class="logout" href="javascript: popupLoginBox();">%s</a>]'''
- % (self._cw._('i18n_login_popup')))
- else:
- self.w(self._cw._('anonymous'))
- self.w(u' [<a class="logout" href="%s">%s</a>]'
- % (self._cw.build_url('login'), self._cw._('login')))
+ w = self.w
+ w(self._cw._('anonymous'))
+ w(u"""[<a class="logout" href="javascript: cw.htmlhelpers.popupLoginBox('%s', '__login');">%s</a>]"""
+ % (self.loginboxid, self._cw._('i18n_login_popup')))
+ self.wview('logform', rset=self.cw_rset, id=self.loginboxid,
+ klass='hidden', title=False, showmessage=False)
+class HTTPAnonUserLink(_UserLink):
+ __select__ = _UserLink.__select__ & configuration_values('auth-mode', 'http') & anonymous_user()
+ loginboxid = 'popupLoginBox'
+
+ def call(self):
+ w = self.w
+ w(self._cw._('anonymous'))
+ # this redirects to the 'login' controller which in turn
+ # will raise a 401/Unauthorized
+ w(u' [<a class="logout" href="%s">%s</a>]'
+ % (self._cw.build_url('login'), self._cw._('login')))
class UserLink(_UserLink):
__select__ = _UserLink.__select__ & authenticated_user()
--- a/web/views/basetemplates.py Mon Oct 04 18:55:57 2010 +0200
+++ b/web/views/basetemplates.py Mon Oct 04 18:56:05 2010 +0200
@@ -276,11 +276,12 @@
logo = self._cw.vreg['components'].select_or_none('logo', self._cw,
rset=self.cw_rset)
if logo and logo.cw_propval('visible'):
- self.w(u'<table id="header"><tr>\n')
- self.w(u'<td>')
- logo.render(w=self.w)
- self.w(u'</td>\n')
- self.w(u'</tr></table>\n')
+ w = self.w
+ w(u'<table id="header"><tr>\n')
+ w(u'<td>')
+ logo.render(w=w)
+ w(u'</td>\n')
+ w(u'</tr></table>\n')
# page parts templates ########################################################
@@ -335,35 +336,31 @@
def main_header(self, view):
"""build the top menu with authentification info and the rql box"""
- self.w(u'<table id="header"><tr>\n')
- self.w(u'<td id="firstcolumn">')
+ w = self.w
+ w(u'<table id="header"><tr>\n')
+ w(u'<td id="firstcolumn">')
logo = self._cw.vreg['components'].select_or_none(
'logo', self._cw, rset=self.cw_rset)
if logo and logo.cw_propval('visible'):
- logo.render(w=self.w)
- self.w(u'</td>\n')
+ logo.render(w=w)
+ w(u'</td>\n')
# appliname and breadcrumbs
- self.w(u'<td id="headtext">')
+ w(u'<td id="headtext">')
for cid in self.main_cell_components:
comp = self._cw.vreg['components'].select_or_none(
cid, self._cw, rset=self.cw_rset)
if comp and comp.cw_propval('visible'):
comp.render(w=self.w)
- self.w(u'</td>')
+ w(u'</td>')
# logged user and help
- self.w(u'<td>\n')
- comp = self._cw.vreg['components'].select_or_none(
+ login_components = self._cw.vreg['components'].selectable(
'loggeduserlink', self._cw, rset=self.cw_rset)
- if comp and comp.cw_propval('visible'):
- comp.render(w=self.w)
- self.w(u'</td>')
- # lastcolumn
- self.w(u'<td id="lastcolumn">')
- self.w(u'</td>\n')
- self.w(u'</tr></table>\n')
- if self._cw.session.anonymous_session:
- self.wview('logform', rset=self.cw_rset, id='popupLoginBox',
- klass='hidden', title=False, showmessage=False)
+ for comp in login_components:
+ w(u'<td>\n')
+ if comp.cw_propval('visible'):
+ comp.render(w=w)
+ w(u'</td>')
+ w(u'</tr></table>\n')
def state_header(self):
state = self._cw.search_state
@@ -380,7 +377,6 @@
return self.w(u'<div class="stateMessage">%s</div>' % msg)
-
class HTMLPageFooter(View):
"""default html page footer: include footer actions
"""
@@ -438,12 +434,16 @@
__regid__ = 'logform'
domid = 'loginForm'
needs_css = ('cubicweb.login.css',)
+ onclick = "javascript: cw.htmlhelpers.popupLoginBox('%s', '%s');"
# XXX have to recall fields name since python is mangling __login/__password
__login = ff.StringField('__login', widget=fw.TextInput({'class': 'data'}))
__password = ff.StringField('__password', label=_('password'),
widget=fw.PasswordSingleInput({'class': 'data'}))
form_buttons = [fw.SubmitButton(label=_('log in'),
- attrs={'class': 'loginButton'})]
+ attrs={'class': 'loginButton'}),
+ fw.ResetButton(label=_('cancel'),
+ attrs={'class': 'loginButton',
+ 'onclick': onclick % ('popupLoginBox', '__login')}),]
def form_action(self):
if self.action is None:
@@ -452,32 +452,35 @@
class LogFormView(View):
+ # XXX an awfull lot of hardcoded assumptions there
+ # makes it unobvious to reuse/specialize
__regid__ = 'logform'
__select__ = match_kwargs('id', 'klass')
title = 'log in'
def call(self, id, klass, title=True, showmessage=True):
- self.w(u'<div id="%s" class="%s">' % (id, klass))
+ w = self.w
+ w(u'<div id="%s" class="popupLoginBox %s">' % (id, klass))
if title:
stitle = self._cw.property_value('ui.site-title')
if stitle:
stitle = xml_escape(stitle)
else:
stitle = u' '
- self.w(u'<div id="loginTitle">%s</div>' % stitle)
- self.w(u'<div id="loginContent">\n')
+ w(u'<div class="loginTitle">%s</div>' % stitle)
+ w(u'<div class="loginContent">\n')
if showmessage and self._cw.message:
- self.w(u'<div class="loginMessage">%s</div>\n' % self._cw.message)
+ w(u'<div class="loginMessage">%s</div>\n' % self._cw.message)
config = self._cw.vreg.config
if config['auth-mode'] != 'http':
self.login_form(id) # Cookie authentication
- self.w(u'</div>')
+ w(u'</div>')
if self._cw.https and config.anonymous_user()[0]:
path = xml_escape(config['base-url'] + self._cw.relative_path())
- self.w(u'<div class="loginMessage"><a href="%s">%s</a></div>\n'
- % (path, self._cw._('No account? Try public access at %s') % path))
- self.w(u'</div>\n')
+ w(u'<div class="loginMessage"><a href="%s">%s</a></div>\n'
+ % (path, self._cw._('No account? Try public access at %s') % path))
+ w(u'</div>\n')
def login_form(self, id):
cw = self._cw