wcs/wcs/admin/users.py

569 lines
22 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
from quixote import get_request
from quixote import get_response
from quixote import get_session
from quixote import redirect
from quixote.directory import Directory
from quixote.html import TemplateIO
from quixote.html import htmltext
import wcs.qommon.storage as st
from wcs.qommon import N_
from wcs.qommon import _
from wcs.qommon import errors
from wcs.qommon import force_str
from wcs.qommon import get_cfg
from wcs.qommon import ident
from wcs.qommon import misc
from wcs.qommon.admin.emails import EmailsDirectory
from wcs.qommon.admin.menu import error_page
from wcs.qommon.backoffice.listing import pagination_links
from wcs.qommon.backoffice.menu import html_top
from wcs.qommon.form import CheckboxWidget
from wcs.qommon.form import EmailWidget
from wcs.qommon.form import Form
from wcs.qommon.form import HtmlWidget
from wcs.qommon.form import SingleSelectWidget
from wcs.qommon.form import StringWidget
from wcs.qommon.form import WidgetList
from wcs.qommon.ident.idp import is_idp_managing_user_attributes
from wcs.qommon.ident.idp import is_idp_managing_user_roles
class UserUI:
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(get_publisher().role_class.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', {})
if self.user.deleted_timestamp:
r += htmltext('<div class="warningnotice">')
r += _('Marked as deleted on %(date)s.') % {
'date': misc.localstrftime(self.user.deleted_timestamp)
}
r += htmltext('</div>')
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">')
if not self.user.is_active:
r += htmltext('<div class="infonotice">%s</div>') % _('This user is not active.')
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>') % get_publisher().role_class.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, str):
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(get_publisher().role_class.keys())
if set(possible_roles) == set(checked_roles):
checked_roles = None
criterias = [st.Null('deleted_timestamp')]
total_count = get_publisher().user_class.count(criterias)
# declarative criteria to only get checked roles
if checked_roles:
roles_criterias = []
if 'admin' in checked_roles:
roles_criterias.append(st.Equal('is_admin', True))
if 'none' in checked_roles:
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:
roles_criterias.append(st.Intersects('roles', other_roles))
criterias.append(st.Or(roles_criterias))
query = get_request().form.get('q')
if query:
criterias.append(st.Or([st.ILike('name', query), st.ILike('email', query)]))
if len(criterias) > 1:
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 len(criterias) > 1:
r += htmltext('<li>%s %s</li>') % (_('Number of filtered users:'), filtered_count)
r += htmltext('</ul>')
r += htmltext('<ul class="biglist">')
for user in users:
user_classes = []
if not user.is_active:
user_classes.append('user-inactive')
if user.is_admin:
user_classes.append('user-is-admin')
elif user.roles:
user_classes.append('user-has-roles')
else:
user_classes.append('simple-user')
r += htmltext('<li class="%s">' % ' '.join(user_classes))
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(['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 get_publisher().role_class.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.
'''
),
)