518 lines
21 KiB
Python
518 lines
21 KiB
Python
# w.c.s. - web application for online forms
|
|
# Copyright (C) 2005-2010 Entr'ouvert
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, see <http://www.gnu.org/licenses/>.
|
|
|
|
from quixote import get_publisher, get_response, get_request, get_session, redirect
|
|
from quixote.directory import Directory
|
|
from quixote.html import TemplateIO, htmltext
|
|
|
|
import wcs.qommon.storage as st
|
|
from qommon import errors
|
|
from qommon import misc, get_cfg
|
|
from qommon.backoffice.listing import pagination_links
|
|
from wcs.roles import Role
|
|
|
|
import qommon.ident
|
|
from qommon.form import *
|
|
from qommon.admin.emails import EmailsDirectory
|
|
from qommon.backoffice.menu import html_top
|
|
from qommon.admin.menu import error_page
|
|
|
|
class UserUI(object):
|
|
def __init__(self, user):
|
|
self.user = user
|
|
|
|
def form(self):
|
|
ident_methods = get_cfg('identification', {}).get('methods', [])
|
|
users_cfg = get_cfg('users', {})
|
|
|
|
form = Form(enctype='multipart/form-data')
|
|
# do not display user attribute fields if the site has been set to get
|
|
# them filled by SAML requests
|
|
if not get_cfg('sp', {}).get('idp-manage-user-attributes', False):
|
|
formdef = get_publisher().user_class.get_formdef()
|
|
if not formdef or not users_cfg.get('field_name'):
|
|
form.add(StringWidget, 'name', title = _('Name'), required = True, size=30,
|
|
value = self.user.name)
|
|
if not formdef or not users_cfg.get('field_email'):
|
|
form.add(EmailWidget, 'email', title = _('Email'), required = False, size=30,
|
|
value = self.user.email)
|
|
if formdef:
|
|
formdef.add_fields_to_form(form, form_data = self.user.form_data)
|
|
form.add(CheckboxWidget, 'is_admin', title = _('Administrator Account'),
|
|
value = self.user.is_admin)
|
|
roles = list(Role.select(order_by='name'))
|
|
if len(roles):
|
|
form.add(WidgetList, 'roles', title = _('Roles'), element_type = SingleSelectWidget,
|
|
value = self.user.roles,
|
|
add_element_label = _('Add Role'),
|
|
element_kwargs = {
|
|
'render_br': False,
|
|
'options': [(None, '---')] + [(x.id, x.name) for x in roles]})
|
|
|
|
for klass in [x for x in qommon.ident.get_method_classes() if x.key in ident_methods]:
|
|
if klass.method_admin_widget:
|
|
value = klass().get_value(self.user)
|
|
form.add(klass.method_admin_widget, 'method_%s' % klass.key, required = False,
|
|
value = value)
|
|
|
|
form.add_submit('submit', _('Submit'))
|
|
form.add_submit('cancel', _('Cancel'))
|
|
return form
|
|
|
|
def submit_form(self, form):
|
|
if not self.user:
|
|
self.user = get_publisher().user_class()
|
|
for f in ('name', 'email', 'is_admin', 'roles'):
|
|
widget = form.get_widget(f)
|
|
if widget:
|
|
setattr(self.user, f, widget.parse())
|
|
if not get_cfg('sp', {}).get('idp-manage-user-attributes', False):
|
|
formdef = get_publisher().user_class.get_formdef()
|
|
if formdef:
|
|
data = formdef.get_data(form)
|
|
self.user.set_attributes_from_formdata(data)
|
|
self.user.form_data = data
|
|
|
|
# user is stored first so it get an id; necessary for some ident
|
|
# methods
|
|
self.user.store()
|
|
|
|
ident_methods = get_cfg('identification', {}).get('methods', [])
|
|
for klass in [x for x in qommon.ident.get_method_classes() if x.key in ident_methods]:
|
|
widget = form.get_widget('method_%s' % klass.key)
|
|
if widget:
|
|
klass().submit(self.user, widget)
|
|
|
|
# XXX: and store!
|
|
# XXX 2: but pay attention to errors set on widget (think
|
|
# "duplicate username") (and the calling method will also
|
|
# have to check this)
|
|
|
|
|
|
class UserPage(Directory):
|
|
_q_exports = ['', 'edit', 'delete', 'token', 'debug']
|
|
|
|
def __init__(self, component):
|
|
self.user = get_publisher().user_class.get(component)
|
|
self.user_ui = UserUI(self.user)
|
|
get_response().breadcrumb.append((component + '/', self.user.display_name))
|
|
|
|
def _q_index(self):
|
|
html_top('users', '%s - %s' % (_('User'), self.user.display_name))
|
|
r = TemplateIO(html=True)
|
|
r += htmltext('<h2>%s - %s</h2>') % (_('User'), self.user.display_name)
|
|
get_response().filter['sidebar'] = self.get_sidebar()
|
|
users_cfg = get_cfg('users', {})
|
|
|
|
r += htmltext('<div class="splitcontent-left">')
|
|
|
|
r += htmltext('<div class="bo-block">')
|
|
r += htmltext('<h3>%s</h3>') % _('Profile')
|
|
|
|
r += htmltext('<div class="form">')
|
|
|
|
if not users_cfg.get('field_name'):
|
|
r += htmltext('<div class="title">%s</div>') % _('Name')
|
|
r += htmltext('<div class="StringWidget content">%s</div>') % self.user.name
|
|
|
|
if not users_cfg.get('field_email') and self.user.email:
|
|
r += htmltext('<div class="title">%s</div>') % _('Email')
|
|
r += htmltext('<div class="StringWidget content">%s</div>') % self.user.email
|
|
|
|
formdef = self.user.get_formdef()
|
|
if formdef:
|
|
if self.user.form_data:
|
|
for field in formdef.fields:
|
|
if not hasattr(field, str('get_view_value')):
|
|
continue
|
|
value = self.user.form_data.get(field.id)
|
|
r += htmltext('<div class="title">')
|
|
r += field.label
|
|
r += htmltext('</div>')
|
|
r += htmltext('<div class="StringWidget content">')
|
|
r += field.get_view_value(value)
|
|
r += htmltext('</div>')
|
|
|
|
r += htmltext('</div>')
|
|
|
|
r += htmltext('</div>') # bo-block
|
|
r += htmltext('</div>') # splitcontent-left
|
|
|
|
r += htmltext('<div class="splitcontent-right">')
|
|
r += htmltext('<div class="bo-block">')
|
|
r += htmltext('<h3>%s</h3>') % _('User Roles')
|
|
|
|
if self.user.roles or self.user.is_admin:
|
|
r += htmltext('<div class="form">')
|
|
r += htmltext('<div class="title">%s</div>') % _('Roles')
|
|
r += htmltext('<div class="StringWidget content"><ul>')
|
|
if self.user.is_admin:
|
|
r += htmltext('<li><strong>%s</strong></li>') % _('Site Administrator')
|
|
for k in self.user.roles or []:
|
|
try:
|
|
r += htmltext('<li>%s</li>') % Role.get(k).name
|
|
except KeyError:
|
|
# removed role ?
|
|
r += htmltext('<li><em>')
|
|
r += _('Unknown role (%s)') % k
|
|
r += htmltext('</em></li>')
|
|
r += htmltext('</ul></div>')
|
|
r += htmltext('</div>')
|
|
|
|
r += htmltext('</div>') # bo-block
|
|
|
|
if self.user.lasso_dump:
|
|
import lasso
|
|
identity = lasso.Identity.newFromDump(self.user.lasso_dump)
|
|
server = misc.get_lasso_server()
|
|
if len(identity.providerIds) and server:
|
|
r += htmltext('<div class="bo-block" id="saml-details">')
|
|
r += htmltext('<h3>%s</h3>') % _('SAML Details')
|
|
r += htmltext('<div class="StringWidget content"><ul>')
|
|
for pid in identity.providerIds:
|
|
provider = server.getProvider(pid)
|
|
label = misc.get_provider_label(provider)
|
|
if label:
|
|
label = '%s (%s)' % (label, pid)
|
|
else:
|
|
label = pid
|
|
federation = identity.getFederation(pid)
|
|
r += htmltext('<li>')
|
|
r += _('Account federated with %s') % label
|
|
r += htmltext('<br />')
|
|
if federation.localNameIdentifier:
|
|
r += _("local: ") + federation.localNameIdentifier.content
|
|
if federation.remoteNameIdentifier:
|
|
r += _("remote: ") + federation.remoteNameIdentifier.content
|
|
r += htmltext('</li>')
|
|
r += htmltext('</ul></div>')
|
|
|
|
if get_cfg('debug', {}).get('debug_mode', False):
|
|
r += htmltext('<h4>%s</h4>') % _('Lasso Identity Dump')
|
|
r += htmltext('<pre>%s</pre>') % self.user.lasso_dump
|
|
r += htmltext('</div>') # bo-block
|
|
|
|
r += htmltext('</div>') # splitcontent-right
|
|
return r.getvalue()
|
|
|
|
def get_sidebar(self):
|
|
ident_methods = get_cfg('identification', {}).get('methods', [])
|
|
r = TemplateIO(html=True)
|
|
r += htmltext('<ul id="sidebar-actions">')
|
|
|
|
if get_cfg('sp', {}).get('idp-manage-user-attributes'):
|
|
r += htmltext('<li><a href="edit">%s</a></li>') % _('Manage Roles')
|
|
else:
|
|
r += htmltext('<li><a href="edit">%s</a></li>') % _('Edit')
|
|
r += htmltext('<li><a href="delete" rel="popup">%s</a></li>') % _('Delete')
|
|
|
|
for method in ident_methods:
|
|
try:
|
|
actions = qommon.ident.get_method_user_directory(
|
|
method, self.user).get_actions()
|
|
except AttributeError:
|
|
continue
|
|
for action_url, action_label in actions:
|
|
r += htmltext('<li><a href="%s/%s">%s</a></li>') % (method, action_url, action_label)
|
|
|
|
debug_cfg = get_cfg('debug', {})
|
|
if debug_cfg.get('logger', False):
|
|
r += htmltext('<li><a href="../../logger/by_user/%s/">') % self.user.id
|
|
r += htmltext('%s</a></li>') % _('Logs')
|
|
|
|
r += htmltext('</ul>')
|
|
return r.getvalue()
|
|
|
|
def debug(self):
|
|
get_response().breadcrumb.append( ('debug', _('Debug')) )
|
|
html_top('users', 'Debug')
|
|
r = TemplateIO(html=True)
|
|
r += htmltext("<h2>Debug - %s</h2>") % self.user.name
|
|
r += htmltext("<pre>")
|
|
r += self.user.lasso_dump
|
|
r += htmltext("</pre>")
|
|
return r.getvalue()
|
|
|
|
def edit(self):
|
|
form = self.user_ui.form()
|
|
if form.get_widget('cancel').parse():
|
|
return redirect('.')
|
|
|
|
if form.get_widget('roles') and form.get_widget('roles').get_widget(
|
|
'add_element').parse():
|
|
form.clear_errors()
|
|
display_form = True
|
|
else:
|
|
display_form = (not form.is_submitted() or form.has_errors())
|
|
|
|
if display_form:
|
|
get_response().breadcrumb.append( ('edit', _('Edit')) )
|
|
html_top('users', title = _('Edit User'))
|
|
r = TemplateIO(html=True)
|
|
r += htmltext('<h2>%s</h2>') % _('Edit User')
|
|
r += form.render()
|
|
return r.getvalue()
|
|
else:
|
|
self.user_ui.submit_form(form)
|
|
return redirect('.')
|
|
|
|
def delete(self):
|
|
form = Form(enctype='multipart/form-data')
|
|
form.widgets.append(HtmlWidget('<p>%s</p>' % _(
|
|
"You are about to irrevocably delete this user.")))
|
|
form.add_submit('delete', _('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('users', title = _('Delete User'))
|
|
r = TemplateIO(html=True)
|
|
r += htmltext('<h2>%s %s</h2>') % (_('Deleting User:'), self.user.name)
|
|
r += form.render()
|
|
return r.getvalue()
|
|
else:
|
|
ident_methods = get_cfg('identification', {}).get('methods', [])
|
|
for klass in [x for x in qommon.ident.get_method_classes() if x.key in ident_methods]:
|
|
if hasattr(klass, str('delete')):
|
|
klass().delete(self.user)
|
|
self.user.remove_self()
|
|
return redirect('..')
|
|
|
|
def _q_lookup(self, component):
|
|
ident_methods = get_cfg('identification', {}).get('methods', [])
|
|
if component in ident_methods:
|
|
get_response().breadcrumb.append((component + '/', None))
|
|
return qommon.ident.get_method_user_directory(component, self.user)
|
|
|
|
class UsersDirectory(Directory):
|
|
_q_exports = ['', 'new']
|
|
|
|
def _q_index(self):
|
|
get_response().breadcrumb.append( ('users/', _('Users')) )
|
|
html_top('users', title = _('Users'))
|
|
r = TemplateIO(html=True)
|
|
|
|
limit = int(get_request().form.get('limit', 20))
|
|
offset = int(get_request().form.get('offset', 0))
|
|
|
|
ident_methods = get_cfg('identification', {}).get('methods', [])
|
|
if not ident_methods:
|
|
r += htmltext('<p>%s</p>') % _('An authentification system must be configured before creating users.')
|
|
elif ident_methods == ['idp'] and len(get_cfg('idp', {}).items()) == 0:
|
|
r += htmltext('<p>%s</p>') % _('SAML support must be setup before creating users.')
|
|
else:
|
|
get_response().filter['sidebar'] = self.get_sidebar(offset, limit)
|
|
|
|
checked_roles = None
|
|
if get_request().form.get('filter'):
|
|
checked_roles = get_request().form.get('role', [])
|
|
if type(checked_roles) in (str, unicode):
|
|
checked_roles = [checked_roles]
|
|
|
|
if checked_roles:
|
|
# optimize query by removing the roles criterias if they are all
|
|
# checked
|
|
possible_roles = ['admin', 'none']
|
|
possible_roles.extend(Role.keys())
|
|
if set(possible_roles) == set(checked_roles):
|
|
checked_roles = None
|
|
|
|
total_count = get_publisher().user_class.count()
|
|
|
|
# declarative criteria to only get checked roles
|
|
criterias = []
|
|
if checked_roles:
|
|
if 'admin' in checked_roles:
|
|
criterias.append(st.Equal('is_admin', True))
|
|
if 'none' in checked_roles:
|
|
criterias.append(st.And([
|
|
st.Equal('is_admin', False), st.Equal('roles', [])]))
|
|
other_roles = [x for x in checked_roles if x not in ('admin', 'none')]
|
|
if other_roles:
|
|
criterias.append(st.Intersects('roles', other_roles))
|
|
criterias = [st.Or(criterias)]
|
|
|
|
query = get_request().form.get('q')
|
|
if query:
|
|
criterias.append(st.Or([st.ILike('name', query), st.ILike('email', query)]))
|
|
|
|
if criterias:
|
|
filtered_count = get_publisher().user_class.count(criterias)
|
|
if filtered_count < offset:
|
|
# reset offset if we are past the number of elements
|
|
offset = 0
|
|
get_request().form['offset'] = 0
|
|
else:
|
|
filtered_count = total_count
|
|
|
|
users = get_publisher().user_class.select(order_by='name',
|
|
clause=criterias, offset=offset, limit=limit)
|
|
|
|
r += htmltext('<div id="listing">')
|
|
r += htmltext('<ul>')
|
|
r += htmltext('<li>%s %s</li>') % (_('Total number of users:'), total_count)
|
|
|
|
if criterias:
|
|
r += htmltext('<li>%s %s</li>') % (_('Number of filtered users:'),
|
|
filtered_count)
|
|
|
|
r += htmltext('</ul>')
|
|
|
|
r += htmltext('<ul class="biglist">')
|
|
for user in users:
|
|
if user.is_admin:
|
|
r += htmltext('<li class="user-is-admin">')
|
|
elif user.roles:
|
|
r += htmltext('<li class="user-has-roles">')
|
|
else:
|
|
r += htmltext('<li class="simple-user">')
|
|
r += htmltext('<strong class="label"><a href="%s/">%s</a></strong>') % (user.id, user.display_name)
|
|
r += htmltext('<p class="details">')
|
|
if user.email:
|
|
r += user.email
|
|
r += htmltext('</p></li>')
|
|
r += htmltext('</ul>')
|
|
|
|
r += pagination_links(offset, limit, filtered_count)
|
|
|
|
r += htmltext('</div>')
|
|
|
|
if get_request().form.get('ajax') == 'true':
|
|
get_response().filter = None
|
|
return r.getvalue()
|
|
|
|
return r.getvalue()
|
|
|
|
def get_sidebar(self, offset=None, limit=None):
|
|
r = TemplateIO(html=True)
|
|
|
|
# if attributes are managed by the identity provider, do not expose
|
|
# the possibility to create users, as only the roles field would
|
|
# be shown, and the creation would fail on missing fields.
|
|
if not get_cfg('sp', {}).get('idp-manage-user-attributes', False):
|
|
r += htmltext("""<ul id="sidebar-actions">
|
|
<li><a class="new-item" href="new">%s</a></li>
|
|
</ul>""") % _('New User')
|
|
|
|
get_response().add_javascript(['jquery.js', 'wcs.listing.js'])
|
|
r += htmltext('<form id="listing-settings">')
|
|
|
|
if offset or limit:
|
|
if not offset:
|
|
offset = 0
|
|
r += htmltext('<input type="hidden" name="offset" value="%s"/>') % offset
|
|
|
|
if limit:
|
|
r += htmltext('<input type="hidden" name="limit" value="%s"/>') % limit
|
|
|
|
r += htmltext('<h3>%s</h3>') % _('Search')
|
|
if get_request().form.get('q'):
|
|
q = get_request().form.get('q')
|
|
if type(q) is not unicode:
|
|
q = unicode(q, get_publisher().site_charset)
|
|
r += htmltext('<input name="q" value="%s">') % q.encode(get_publisher().site_charset)
|
|
else:
|
|
r += htmltext('<input name="q">')
|
|
r += htmltext('<input type="submit" value="%s"/>') % _('Search')
|
|
|
|
r += htmltext('<h3>%s</h3>') % _('Filter on Roles')
|
|
r += htmltext('<input type="hidden" name="filter" value="true"/>')
|
|
r += htmltext('<ul>')
|
|
roles = [('admin', _('Site Administrator'))]
|
|
for role in Role.select():
|
|
roles.append((role.id, role.name))
|
|
roles.append(('none', _('None')))
|
|
|
|
checked_roles = get_request().form.get('role', [])
|
|
if not checked_roles and not get_request().form.get('filter'):
|
|
# take everything as default
|
|
checked_roles = [str(x[0]) for x in roles]
|
|
|
|
for role_id, role_title in roles:
|
|
checked = ''
|
|
if str(role_id) in checked_roles:
|
|
checked = 'checked'
|
|
r += htmltext('<li><label><input type="checkbox" name="role" value="%s"%s/>%s</label></li>') % (
|
|
role_id, checked, role_title)
|
|
r += htmltext('</ul>')
|
|
r += htmltext('<input type="submit" value="%s"/>') % _('Filter')
|
|
r += htmltext('</form>')
|
|
return r.getvalue()
|
|
|
|
def new(self):
|
|
get_response().breadcrumb.append( ('users/', _('Users')) )
|
|
get_response().breadcrumb.append( ('new', _('New')) )
|
|
|
|
ident_methods = get_cfg('identification', {}).get('methods', [])
|
|
if not ident_methods:
|
|
return error_page('users',
|
|
_('An authentification system must be configured before creating users.'))
|
|
if ident_methods == ['idp'] and len(get_cfg('idp', {}).items()) == 0:
|
|
return error_page('users',
|
|
_('SAML support must be setup before creating users.'))
|
|
if get_cfg('sp', {}).get('idp-manage-user-attributes', False):
|
|
raise errors.TraversalError()
|
|
|
|
# XXX: user must be logged in to get here
|
|
user = get_publisher().user_class()
|
|
user_ui = UserUI(user)
|
|
first_user = get_publisher().user_class.count() == 0
|
|
if first_user:
|
|
user.is_admin = first_user
|
|
form = user_ui.form()
|
|
if form.get_widget('cancel').parse():
|
|
return redirect('.')
|
|
|
|
if not form.is_submitted() or form.has_errors():
|
|
html_top('users', title = _('New User'))
|
|
r = TemplateIO(html=True)
|
|
r += htmltext('<h2>%s</h2>') % _('New User')
|
|
r += form.render()
|
|
return r.getvalue()
|
|
else:
|
|
user_ui.submit_form(form)
|
|
if first_user:
|
|
req = get_request()
|
|
if req.user:
|
|
user_ui.user.name_identifiers = req.user.name_identifiers
|
|
user_ui.user.lasso_dump = req.user.lasso_dump
|
|
user_ui.user.store()
|
|
get_session().set_user(user_ui.user.id)
|
|
return redirect('.')
|
|
|
|
def _q_lookup(self, component):
|
|
get_response().breadcrumb.append( ('users/', _('Users')) )
|
|
try:
|
|
return UserPage(component)
|
|
except KeyError:
|
|
raise errors.TraversalError()
|
|
|
|
|
|
EmailsDirectory.register('email_with_token', N_('Identification token'),
|
|
N_('Available variables: token, token_url, sitename'),
|
|
category = N_('Identification'),
|
|
default_subject = N_('Access to [sitename]'),
|
|
default_body = N_('''\
|
|
Hello,
|
|
|
|
An administrator delivered you access to [sitename].
|
|
|
|
Please visit [token_url] to enable it.
|
|
|
|
'''))
|
|
|