wcs/wcs/admin/users.py

531 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 django.utils import six
from django.utils.encoding import force_text
from quixote import get_publisher, get_response, get_request, get_session, redirect
from quixote.directory import Directory
from quixote.html import TemplateIO, htmltext
from wcs.qommon import _, force_str
import wcs.qommon.storage as st
from wcs.qommon import errors
from wcs.qommon import misc, get_cfg
from wcs.qommon.backoffice.listing import pagination_links
from wcs.roles import Role
from wcs.qommon import ident
from wcs.qommon.ident.idp import is_idp_managing_user_attributes, is_idp_managing_user_roles
from wcs.qommon.form import *
from wcs.qommon.admin.emails import EmailsDirectory
from wcs.qommon.backoffice.menu import html_top
from wcs.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 is_idp_managing_user_attributes():
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) and not is_idp_managing_user_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, '---', None)] + [(x.id, x.name, x.id) for x in roles if not x.is_internal()]})
for klass in [x for x in 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 is_idp_managing_user_attributes():
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 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']
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('<div id="appbar">')
r += htmltext('<h2>%s</h2>') % self.user.display_name
r += htmltext('<span class="actions">')
r += self.get_actions()
r += htmltext('</span>')
r += htmltext('</div>')
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">')
if value is None:
r += htmltext('<i>%s</i>') % _('Not set')
else:
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>') % _('Roles')
if self.user.roles or self.user.is_admin:
r += htmltext('<div class="form">')
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_actions(self):
ident_methods = get_cfg('identification', {}).get('methods', [])
r = TemplateIO(html=True)
if is_idp_managing_user_attributes() and not is_idp_managing_user_roles():
r += htmltext('<a href="edit">%s</a>') % _('Manage Roles')
elif not (is_idp_managing_user_attributes() and is_idp_managing_user_roles()):
r += htmltext('<a href="edit">%s</a>') % _('Edit')
r += htmltext('<a href="delete" rel="popup">%s</a>') % _('Delete')
for method in ident_methods:
try:
actions = ident.get_method_user_directory(
method, self.user).get_actions()
except AttributeError:
continue
for action_url, action_label in actions:
r += htmltext('<a href="%s/%s">%s</a>') % (method, action_url, action_label)
debug_cfg = get_cfg('debug', {})
if debug_cfg.get('logger', True):
r += htmltext('<a href="../../settings/logs/by_user/%s/">') % self.user.id
r += htmltext('%s</a>') % _('Logs')
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', _('Delete'))
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 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 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',
get_publisher().get_site_option('default-page-size') or 20))
offset = int(get_request().form.get('offset', 0))
checked_roles = None
if get_request().form.get('filter'):
checked_roles = get_request().form.get('role', [])
if isinstance(checked_roles, six.string_types):
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)
if user.email and False:
r += htmltext('<p class="details">')
r += user.email
r += htmltext('</p>')
r += htmltext('</li>')
r += htmltext('</ul>')
r += pagination_links(offset, limit, filtered_count)
r += htmltext('</div>')
if get_request().form.get('ajax') == 'true':
get_response().filter = {'raw': True}
return r.getvalue()
ident_methods = get_cfg('identification', {}).get('methods', [])
r2 = TemplateIO(html=True)
r2 += htmltext('<div id="appbar">')
r2 += htmltext('<h2>%s</h2>') % _('Users')
info_notice = None
if not ident_methods:
info_notice = _('An authentification system must be configured before creating users.')
elif ident_methods == ['idp'] and len(get_cfg('idp', {}).items()) == 0:
info_notice = _('SAML support must be setup before creating users.')
else:
# 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 is_idp_managing_user_attributes():
r2 += htmltext('<span class="actions">')
r2 += htmltext('<a class="new-item" href="new">%s</a>') % _('New User')
r2 += htmltext('</span>')
get_response().filter['sidebar'] = self.get_sidebar(offset, limit)
r2 += htmltext('</div>')
if info_notice:
r2 += htmltext('<div class="infonotice"><p>%s</p></div>') % info_notice
r2 += r.getvalue()
return r2.getvalue()
def get_sidebar(self, offset=None, limit=None):
r = TemplateIO(html=True)
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')
r += htmltext('<input name="q" value="%s">') % force_str(q)
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 is_idp_managing_user_attributes():
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.
'''))