# -*- mode: python -*- # -*- coding: utf-8 -*- ############################################################################## # # Gestion scolarite IUT # # Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. # # 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, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Emmanuel Viennet emmanuel.viennet@viennet.net # ############################################################################## """ Gestion des utilisateurs (table SQL pour Zope User Folder) """ import string import re import time import md5 import base64 import jaxml from sco_zope import * # pylint: disable=unused-wildcard-import # --------------- import sco_utils as scu import notesdb as ndb from notes_log import log from scolog import logdb from scolars import format_prenom, format_nom import sco_import_users import sco_excel from TrivialFormulator import TrivialFormulator, TF, tf_error_message from gen_tables import GenTable import scolars import sco_cache import sco_users from sco_permissions import ( ScoEditAllEvals, ScoEditAllNotes, ScoImplement, ScoSuperAdmin, ScoUsersAdmin, ScoUsersView, ScoView, ) from sco_exceptions import ( AccessDenied, ScoException, ScoValueError, ScoInvalidDateError, ScoLockedFormError, ScoGenError, ) # --------------- # cache global: chaque instance, repérée par son URL, a un cache # qui est recréé à la demande # On cache ici la liste des utilisateurs, pour une duree limitee # (une minute). CACHE_userlist = {} # --------------- class ZScoUsers( ObjectManager, PropertyManager, RoleManager, Item, Persistent, Implicit ): "ZScousers object" meta_type = "ZScoUsers" security = ClassSecurityInfo() # This is the list of the methods associated to 'tabs' in the ZMI # Be aware that The first in the list is the one shown by default, so if # the 'View' tab is the first, you will never see your tabs by cliquing # on the object. manage_options = ( ({"label": "Contents", "action": "manage_main"},) + PropertyManager.manage_options # add the 'Properties' tab + ({"label": "View", "action": "index_html"},) + Item.manage_options # add the 'Undo' & 'Owner' tab + RoleManager.manage_options # add the 'Security' tab ) # no permissions, only called from python def __init__(self, id, title): "initialise a new instance" self.id = id self.title = title # Connexion to SQL database of users: # Ugly but necessary during transition out of Zope: _db_cnx_string = "dbname=SCOUSERS port=5432" security.declareProtected("Change DTML Documents", "GetUsersDBConnexion") GetUsersDBConnexion = ndb.GetUsersDBConnexion # -------------------------------------------------------------------- # # Users (top level) # # -------------------------------------------------------------------- # used to view content of the object security.declareProtected(ScoUsersView, "index_html") def index_html(self, REQUEST, all=0, with_olds=0, format="html"): "gestion utilisateurs..." all = int(all) with_olds = int(with_olds) # Controle d'acces authuser = REQUEST.AUTHENTICATED_USER user_name = str(authuser) # log('user: %s roles: %s'%(user_name,authuser.getRolesInContext(self))) user = self._user_list(args={"user_name": user_name}) if not user: zope_roles = authuser.getRolesInContext(self) if ("Manager" in zope_roles) or ("manage" in zope_roles): dept = "" # special case for zope admin else: raise AccessDenied("Vous n'avez pas la permission de voir cette page") else: dept = user[0]["dept"] H = [self.sco_header(REQUEST, page_title="Gestion des utilisateurs")] H.append("
Ajouter un utilisateur' ) H.append( ' Importer des utilisateurs
' ) if all: checked = "checked" else: checked = "" if with_olds: olds_checked = "checked" else: olds_checked = "" H.append( """""" % (REQUEST.URL0, checked, olds_checked) ) L = self.list_users( dept, all=all, with_olds=with_olds, format=format, REQUEST=REQUEST, with_links=authuser.has_permission(ScoUsersAdmin, self), ) if format != "html": return L H.append(L) F = self.sco_footer(REQUEST) return "\n".join(H) + F _userEditor = ndb.EditableTable( "sco_users", "user_id", ( "user_id", "user_name", "passwd", "roles", "date_modif_passwd", "nom", "prenom", "email", "dept", "passwd_temp", "status", "date_expiration", ), output_formators={ "date_modif_passwd": ndb.DateISOtoDMY, "date_expiration": ndb.DateISOtoDMY, }, input_formators={ "date_modif_passwd": ndb.DateDMYtoISO, "date_expiration": ndb.DateDMYtoISO, }, sortkey="nom", filter_nulls=False, ) def _user_list(self, **kw): # list info sur utilisateur(s) cnx = self.GetUsersDBConnexion() users = self._userEditor.list(cnx, **kw) for u in users: if u["status"] == "old": u["status_txt"] = "(ancien)" else: u["status_txt"] = "" return users def _user_edit(self, user_name, vals): # edit user cnx = self.GetUsersDBConnexion() vals["user_name"] = user_name self._userEditor.edit(cnx, vals) self.get_userlist_cache().inval_cache() # > self.acl_users.cache_removeUser(user_name) # exUserFolder's caches self.acl_users.xcache_removeUser(user_name) # Ensure that if status is "old", login is disabled # note that operation is reversible without having to re-enter a password # We change the roles (to avoid dealing with passwd hash, controled by exUserFolder) u = self._user_list(args={"user_name": user_name})[0] if u["status"] == "old" and u["roles"] and u["roles"][0] != "-": roles = ["-" + r for r in u["roles"].split(",")] cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) self.acl_users.scodoc_editUser(cursor, user_name, roles=roles) self.get_userlist_cache().inval_cache() elif not u["status"] and u["roles"] and u["roles"][0] == "-": roles = [r[1:] for r in u["roles"].split(",") if (r and r[0] == "-")] cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) self.acl_users.scodoc_editUser(cursor, user_name, roles=roles) self.get_userlist_cache().inval_cache() def _user_delete(self, user_name): # delete user cnx = self.GetUsersDBConnexion() user_id = self._user_list(args={"user_name": user_name})[0]["user_id"] self._userEditor.delete(cnx, user_id) self.get_userlist_cache().inval_cache() # > def _all_roles(self): "ensemble de tous les roles attribués ou attribuables" roles = set(self.DeptUsersRoles()) cnx = self.GetUsersDBConnexion() L = self._userEditor.list(cnx, {}) for l in L: roles.update([x.strip() for x in l["roles"].split(",")]) return [r for r in roles if r and r[0] != "-"] security.declareProtected(ScoUsersAdmin, "user_info") def user_info(self, user_name=None, user=None, format=None, REQUEST=None): """Donne infos sur l'utilisateur (qui peut ne pas etre dans notre base). Si user_name est specifie, interroge la BD. Sinon, user doit etre un dict. """ if user_name: infos = self._user_list(args={"user_name": user_name}) else: infos = [user.copy()] user_name = user["user_name"] if not infos: # special case: user is not in our database return { "user_name": user_name, "nom": user_name, "prenom": "", "email": "", "dept": "", "nomprenom": user_name, "prenomnom": user_name, "prenom_fmt": "", "nom_fmt": user_name, "nomcomplet": user_name, "nomplogin": user_name, "nomnoacc": scu.suppress_accents(user_name), "passwd_temp": 0, "status": "", "date_expiration": None, } else: info = infos[0] # always conceal password ! del info["passwd"] # always conceal password ! # if info["prenom"]: p = format_prenom(info["prenom"]) else: p = "" if info["nom"]: n = format_nom( info["nom"], uppercase=False ) # strcapitalize(strlower(info['nom'])) else: n = user_name prenom_abbrv = scu.abbrev_prenom(p) # nomprenom est le nom capitalisé suivi de l'initiale du prénom info["nomprenom"] = (n + " " + prenom_abbrv).strip() # prenomnom est l'initiale du prénom suivie du nom info["prenomnom"] = (prenom_abbrv + " " + n).strip() # nom_fmt et prenom_fmt: minuscule capitalisé info["nom_fmt"] = n info["prenom_fmt"] = scolars.format_prenom(p) # nomcomplet est le prenom et le nom complets info["nomcomplet"] = info["prenom_fmt"] + " " + info["nom_fmt"] # nomplogin est le nom en majuscules suivi du prénom et du login # e.g. Dupont Pierre (dupont) info["nomplogin"] = "%s %s (%s)" % (scu.strupper(n), p, info["user_name"]) # nomnoacc est le nom en minuscules sans accents info["nomnoacc"] = scu.suppress_accents(scu.strlower(info["nom"])) return scu.sendResult(REQUEST, info, name="user", format=format) def _can_handle_passwd(self, authuser, user_name, allow_admindepts=False): """true if authuser can see or change passwd of user_name. If allow_admindepts, allow Admin from all depts (so they can view users from other depts and add roles to them). authuser is a Zope user object. user_name is a string. """ # Is authuser a zope admin ? zope_roles = authuser.getRolesInContext(self) if ("Manager" in zope_roles) or ("manage" in zope_roles): return True # Anyone can change its own passwd (or see its informations) if str(authuser) == user_name: return True # has permission ? if not authuser.has_permission(ScoUsersAdmin, self): return False # Ok, now check that authuser can manage users from this departement # Get user info user = self._user_list(args={"user_name": user_name}) if not user: return False # we don't have infos on this user ! # Get authuser info auth_name = str(authuser) authuser_info = self._user_list(args={"user_name": auth_name}) if not authuser_info: return False # not admin, and not in out database auth_dept = authuser_info[0]["dept"] if not auth_dept: return True # if no dept, can access users from all depts ! if auth_dept == user[0]["dept"] or allow_admindepts: return True else: return False def do_change_password(self, user_name, password): user = self._user_list(args={"user_name": user_name}) assert len(user) == 1, "database inconsistency: len(user)=%d" % len(user) # should not occur, already tested in _can_handle_passwd cnx = self.GetUsersDBConnexion() # en mode autocommit cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor.execute( "update sco_users set date_modif_passwd=now(), passwd_temp=0 where user_name=%(user_name)s", {"user_name": user_name}, ) # Laisse le exUserFolder modifier les donnees: self.acl_users.scodoc_editUser( cursor, user_name, password=password, roles=[user[0]["roles"]] ) log("change_password: change ok for %s" % user_name) self.get_userlist_cache().inval_cache() # > security.declareProtected(ScoView, "change_password") def change_password(self, user_name, password, password2, REQUEST): "change a password" # ScoUsersAdmin: modif tous les passwd de SON DEPARTEMENT # sauf si pas de dept (admin global) H = [] F = self.sco_footer(REQUEST) # Check access permission if not self._can_handle_passwd(REQUEST.AUTHENTICATED_USER, user_name): # access denied log( "change_password: access denied (authuser=%s, user_name=%s, ip=%s)" % (REQUEST.AUTHENTICATED_USER, user_name, REQUEST.REMOTE_ADDR) ) raise AccessDenied( "vous n'avez pas la permission de changer ce mot de passe" ) # check password if password != password2: H.append( """Les deux mots de passes saisis sont différents !
""" % user_name ) else: if not sco_users.is_valid_password(password): H.append( """ce mot de passe n\'est pas assez compliqué !
(oui, il faut un mot de passe vraiment compliqué !)
Ne notez pas ce mot de passe, mais mémorisez le !
Rappel: il est interdit de communiquer son mot de passe à un tiers, même si c'est un collègue de confiance !
Si vous n'êtes pas administrateur, le système va vous redemander votre login et nouveau mot de passe au prochain accès.
" ) return ( """Vous n'avez pas la permission de changer ce mot de passe
" + F ) # H.append( """