diff --git a/.gitignore b/.gitignore index cecc2c1..4c7c380 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,4 @@ coverage.xml # Sphinx documentation docs/_build/ +ldapcherry-dev.ini diff --git a/.travis.yml b/.travis.yml index fd0f42a..557f5e3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,31 +1,40 @@ sudo: required -dist: trusty +dist: xenial language: python -#env: -# - TRAVIS="yes" - before_install: - '[ "$TEST_PEP8" == "1" ] || sudo ./tests/test_env/deploy.sh' -python: - - "2.7" - install: - - pip install -e . - - "if [[ $TEST_PEP8 == '1' ]]; then pip install pep8; fi" + - "pip install -e . -r $REQ_FILE" + - "if [[ $TEST_PEP8 == '1' ]]; then pip install pycodestyle; fi" - pip install passlib - pip install coveralls -# command to run tests -# -#script: -# - coverage run --source=ldapcherry setup.py test -script: "if [[ $TEST_PEP8 == '1' ]]; then pep8 --repeat --show-source --exclude=.venv,.tox,dist,docs,build,*.egg,tests,misc,setup.py . scripts/ldapcherryd; else coverage run --source=ldapcherry setup.py test; fi" +script: "if [[ $TEST_PEP8 == '1' ]]; then pycodestyle --repeat --show-source --exclude=.venv,.tox,dist,docs,build,*.egg,tests,misc,setup.py .; else coverage run --source=ldapcherry setup.py test; fi" matrix: include: - python: "2.7" - env: TEST_PEP8=1 + env: + TEST_PEP8=1 + REQ_FILE=requirements.txt + - python: "2.7" + env: + TEST_PEP8=0 + REQ_FILE=requirements-el7.txt + - python: "2.7" + env: + TEST_PEP8=0 + REQ_FILE=requirements-stretch.txt + - python: "2.7" + env: + TEST_PEP8=0 + REQ_FILE=requirements.txt + - python: "3.6" + env: + TEST_PEP8=0 + REQ_FILE=requirements.txt + after_success: - coveralls after_failure: diff --git a/ChangeLog.rst b/ChangeLog.rst index 2597668..883f28a 100644 --- a/ChangeLog.rst +++ b/ChangeLog.rst @@ -1,6 +1,39 @@ Dev *** +Version 1.1.1 +************* + +* [fix ] fix double escaping issues introduced in 1.0.0 +* [fix ] fix missing url escaping in links with querystring parameters (delete and modify page mostly) +* [fix ] fix log level not being honored in the backends +* [impr] clarify the role of 'key: True' of attributes.yml in the documentation +* [impr] add a few more comments in the .ini file to explain better the \*_filter_tmpl and group_attr parameters +* [impr] add debug log to help debug ldap search filters + +Version 1.1.0 +************* + +* [feat] add stdout as a valid log method (useful when running with docker) + +Version 1.0.1 +************* + +* [fix ] fix error handling when adding user that already exists + +Version 1.0.0 +************* + +* [sec ] fix XSS injection in the url redirect in the login page (thanks to jthiltges) +* [fix ] fix configuration consistency check for attribute file (error if a given backend is not declared in main .ini file but in attributes) +* [fix ] remove a few deprecation warnings +* [fix ] fix potential issue with group names containing non-ascii characters +* [feat] support for python 3 +* [feat] support for python-ldap 3.X.X +* [impr] better log error message if inconsistency between role, attribute and main .ini file for backends +* [impr] more systematic use of html and url escaping in the html rendering to prevent against content injection (thanks to jthiltges) +* [impr] more testing for various versions of python and python-ldap + Version 0.5.2 ************* diff --git a/README.rst b/README.rst index 3685447..e7c407c 100644 --- a/README.rst +++ b/README.rst @@ -30,6 +30,21 @@ Nice and simple application to manage users and groups in multiple directory ser ---- +******** + Demo +******** + +A demo is accessible here: https://ldapcherry.kakwalab.ovh + +The credentials are: + +* as administrator: admin/admin +* as user: user/user + +Please take note that it's not possible to modify/delete the 'admin' and 'user' users. + +Also take note that the service will be reseted once per day. + **************** Presentation **************** @@ -75,7 +90,7 @@ The default backend plugins permit to manage Ldap and Active Directory. $ export DATAROOTDIR=/usr/share/ # install ldapcherry - $ python setup.py + $ python setup.py install # edit configuration files $ vi /etc/ldapcherry/ldapcherry.ini diff --git a/conf/ldapcherry.ini b/conf/ldapcherry.ini index 12c12fa..c5de286 100644 --- a/conf/ldapcherry.ini +++ b/conf/ldapcherry.ini @@ -28,6 +28,14 @@ request.show_tracebacks = False ## error and ldapcherry log file #log.error_file = '/tmp/ldapcherry_error.log' +##################################### +# configuration to log to stdout # +##################################### +## logger stdout for access log +#log.access_handler = 'stdout' +## logger stdout for error and ldapcherry log +#log.error_handler = 'stdout' + ##################################### # configuration to log in syslog # ##################################### @@ -98,16 +106,24 @@ ldap.timeout = 1 ldap.groupdn = 'ou=group,dc=example,dc=org' # users dn ldap.userdn = 'ou=people,dc=example,dc=org' -# ldapsearch filter to get a user + +# ldapsearch filter to get one specific user +# %(username)s is content of the attribute marked 'key: True' in the attributes.file config file ldap.user_filter_tmpl = '(uid=%(username)s)' # ldapsearch filter to get groups of a user +# %(username)s is content of the attribute marked 'key: True' in the attributes.file config file ldap.group_filter_tmpl = '(member=uid=%(username)s,ou=People,dc=example,dc=org)' # filter to search users +# %(searchstring)s is the content passed through the search box ldap.search_filter_tmpl = '(|(uid=%(searchstring)s*)(sn=%(searchstring)s*))' # ldap group attributes and how to fill them +# 'member' is the name of the attribute +# for the template, any of the user's ldap attributes can be user ldap.group_attr.member = "%(dn)s" +# same with memverUid and the uid user's attribute #ldap.group_attr.memberUid = "%(uid)s" + # object classes of a user entry ldap.objectclasses = 'top, person, posixAccount, inetOrgPerson' # dn entry attribute for an ldap user diff --git a/docs/backends.rst b/docs/backends.rst index 878fa7a..7679597 100644 --- a/docs/backends.rst +++ b/docs/backends.rst @@ -47,48 +47,55 @@ Configuration The ldap backend exposes the following parameters: -+--------------------------+----------+------------------------------------+--------------------------+--------------------------------------------+ -| Parameter | Section | Description | Values | Comment | -+==========================+==========+====================================+==========================+============================================+ -| uri | backends | The ldap uri to access | ldap uri | * use ldap:// for clear/starttls | -| | | | | * use ldaps:// for ssl | -| | | | | * custom port: ldap://: | -+--------------------------+----------+------------------------------------+--------------------------+--------------------------------------------+ -| ca | backends | Path to the CA file | file path | optional | -+--------------------------+----------+------------------------------------+--------------------------+--------------------------------------------+ -| starttls | backends | Use starttls | 'on' or 'off' | optional | -+--------------------------+----------+------------------------------------+--------------------------+--------------------------------------------+ -| checkcert | backends | Check the server certificat | 'on' or 'off' | optional | -+--------------------------+----------+------------------------------------+--------------------------+--------------------------------------------+ -| binddn | backends | The bind dn to use | ldap dn | This dn must have read/write permissions | -+--------------------------+----------+------------------------------------+--------------------------+--------------------------------------------+ -| password | backends | The password of the bind dn | password | | -+--------------------------+----------+------------------------------------+--------------------------+--------------------------------------------+ -| timeout | backends | Ldap connexion timeout | integer (second) | | -+--------------------------+----------+------------------------------------+--------------------------+--------------------------------------------+ -| password | backends | The password of the bind dn | password | | -+--------------------------+----------+------------------------------------+--------------------------+--------------------------------------------+ -| groupdn | backends | The ldap dn where groups are | ldap dn | | -+--------------------------+----------+------------------------------------+--------------------------+--------------------------------------------+ -| userdn | backends | The ldap dn where users are | ldap dn | | -+--------------------------+----------+------------------------------------+--------------------------+--------------------------------------------+ -| user_filter_tmpl | backends | The search filter template | ldap search filter | The user identifier is passed through | -| | | to recover a given user | template | the **username** variable (*%(username)s*).| -+--------------------------+----------+------------------------------------+--------------------------+--------------------------------------------+ -| group_filter_tmpl | backends | The search filter template to | ldap search filter | The following variables are usable: | -| | | recover the groups of a given user | template | * **username**: the user key attribute | -| | | | | * **userdn**: the user ldap dn | -+--------------------------+----------+------------------------------------+--------------------------+--------------------------------------------+ -| group_attr. | backends | Member attribute template value | template | * is the member attribute | -| | | | | in groups dn entries | -| | | | | * every user attributes are exposed | -| | | | | in the template | -| | | | | * multiple attributes can be set | -+--------------------------+----------+------------------------------------+--------------------------+--------------------------------------------+ -| objectclasses | backends | list of object classes for users | comma separated list | | -+--------------------------+----------+------------------------------------+--------------------------+--------------------------------------------+ -| dn_user_attr | backends | attribute used in users dn | dn attribute | | -+--------------------------+----------+------------------------------------+--------------------------+--------------------------------------------+ ++--------------------------+----------+------------------------------------+--------------------------+------------------------------------------------+ +| Parameter | Section | Description | Values | Comment | ++==========================+==========+====================================+==========================+================================================+ +| uri | backends | The ldap uri to access | ldap uri | * use ldap:// for clear/starttls | +| | | | | * use ldaps:// for ssl | +| | | | | * custom port: ldap://: | ++--------------------------+----------+------------------------------------+--------------------------+------------------------------------------------+ +| ca | backends | Path to the CA file | file path | optional | ++--------------------------+----------+------------------------------------+--------------------------+------------------------------------------------+ +| starttls | backends | Use starttls | 'on' or 'off' | optional | ++--------------------------+----------+------------------------------------+--------------------------+------------------------------------------------+ +| checkcert | backends | Check the server certificat | 'on' or 'off' | optional | ++--------------------------+----------+------------------------------------+--------------------------+------------------------------------------------+ +| binddn | backends | The bind dn to use | ldap dn | This dn must have read/write permissions | ++--------------------------+----------+------------------------------------+--------------------------+------------------------------------------------+ +| password | backends | The password of the bind dn | password | | ++--------------------------+----------+------------------------------------+--------------------------+------------------------------------------------+ +| timeout | backends | Ldap connexion timeout | integer (second) | | ++--------------------------+----------+------------------------------------+--------------------------+------------------------------------------------+ +| password | backends | The password of the bind dn | password | | ++--------------------------+----------+------------------------------------+--------------------------+------------------------------------------------+ +| groupdn | backends | The ldap dn where groups are | ldap dn | | ++--------------------------+----------+------------------------------------+--------------------------+------------------------------------------------+ +| userdn | backends | The ldap dn where users are | ldap dn | | ++--------------------------+----------+------------------------------------+--------------------------+------------------------------------------------+ +| user_filter_tmpl | backends | The search filter template | ldap search filter | The user identifier is passed through | +| | | to recover a given user | template | the **username** variable (*%(username)s*) | +| | | | | | +| | | | | **username** is the content of the | +| | | | | the attribute marked by '**key: True**' | +| | | | | in the **attributes.yml** file | ++--------------------------+----------+------------------------------------+--------------------------+------------------------------------------------+ +| group_filter_tmpl | backends | The search filter template to | ldap search filter | The following variables are usable: | +| | | recover the groups of a given user | template | | +| | | recover the groups of a given user | template | * **username**: the user's key attribute | +| | | | | * **userdn**: the user's ldap dn | ++--------------------------+----------+------------------------------------+--------------------------+------------------------------------------------+ +| group_attr. | backends | Member attribute template value | template | * is the member attribute | +| | | | | in groups dn entries | +| | | | | * every user attributes are exposed | +| | | | | in the template | +| | | | | * multiple attributes | +| | | | | can be set (ex: group_attr.member | +| | | | | (ex: group_attr.member, group_attr.usermemb) | ++--------------------------+----------+------------------------------------+--------------------------+------------------------------------------------+ +| objectclasses | backends | list of object classes for users | comma separated list | | ++--------------------------+----------+------------------------------------+--------------------------+------------------------------------------------+ +| dn_user_attr | backends | attribute used in users dn | dn attribute | | ++--------------------------+----------+------------------------------------+--------------------------+------------------------------------------------+ Example @@ -96,46 +103,58 @@ Example .. sourcecode:: ini - [backends] - - # name of the module - ldap.module = 'ldapcherry.backend.backendLdap' - # display name of the ldap - ldap.display_name = 'My Ldap Directory' - - # uri of the ldap directory - ldap.uri = 'ldap://ldap.ldapcherry.org' - # ca to use for ssl/tls connexion - #ldap.ca = '/etc/dnscherry/TEST-cacert.pem' - # use start tls - #ldap.starttls = 'off' - # check server certificate (for tls) - #ldap.checkcert = 'off' - # bind dn to the ldap - ldap.binddn = 'cn=dnscherry,dc=example,dc=org' - # password of the bind dn - ldap.password = 'password' - # timeout of ldap connexion (in second) - ldap.timeout = 1 - - # groups dn - ldap.groupdn = 'ou=group,dc=example,dc=org' - # users dn - ldap.userdn = 'ou=people,dc=example,dc=org' - # ldapsearch filter to get a user - ldap.user_filter_tmpl = '(uid=%(username)s)' - # ldapsearch filter to get groups of a user - ldap.group_filter_tmpl = '(member=uid=%(username)s,ou=People,dc=example,dc=org)' - # filter to search users - ldap.search_filter_tmpl = '(|(uid=%(searchstring)s*)(sn=%(searchstring)s*))' + [backends] - # ldap group attributes and how to fill them - ldap.group_attr.member = "%(dn)s" - #ldap.group_attr.memberUid = "%(uid)s" - # object classes of a user entry - ldap.objectclasses = 'top, person, posixAccount, inetOrgPerson' - # dn entry attribute for an ldap user - ldap.dn_user_attr = 'uid' + ##################################### + # configuration of ldap backend # + ##################################### + + # name of the module + ldap.module = 'ldapcherry.backend.backendLdap' + # display name of the ldap + ldap.display_name = 'My Ldap Directory' + + # uri of the ldap directory + ldap.uri = 'ldap://ldap.ldapcherry.org' + # ca to use for ssl/tls connexion + #ldap.ca = '/etc/dnscherry/TEST-cacert.pem' + # use start tls + #ldap.starttls = 'off' + # check server certificate (for tls) + #ldap.checkcert = 'off' + # bind dn to the ldap + ldap.binddn = 'cn=dnscherry,dc=example,dc=org' + # password of the bind dn + ldap.password = 'password' + # timeout of ldap connexion (in second) + ldap.timeout = 1 + + # groups dn + ldap.groupdn = 'ou=group,dc=example,dc=org' + # users dn + ldap.userdn = 'ou=people,dc=example,dc=org' + + # ldapsearch filter to get one specific user + # %(username)s is content of the attribute marked 'key: True' in the attributes.file config file + ldap.user_filter_tmpl = '(uid=%(username)s)' + # ldapsearch filter to get groups of a user + # %(username)s is content of the attribute marked 'key: True' in the attributes.file config file + ldap.group_filter_tmpl = '(member=uid=%(username)s,ou=People,dc=example,dc=org)' + # filter to search users + # %(searchstring)s is the content passed through the search box + ldap.search_filter_tmpl = '(|(uid=%(searchstring)s*)(sn=%(searchstring)s*))' + + # ldap group attributes and how to fill them + # 'member' is the name of the attribute + # for the template, any of the user's ldap attributes can be user + ldap.group_attr.member = "%(dn)s" + # same with memverUid and the uid user's attribute + #ldap.group_attr.memberUid = "%(uid)s" + + # object classes of a user entry + ldap.objectclasses = 'top, person, posixAccount, inetOrgPerson' + # dn entry attribute for an ldap user + ldap.dn_user_attr = 'uid' Active Directory Backend diff --git a/docs/deploy.rst b/docs/deploy.rst index 1c8a4c0..5859d0c 100644 --- a/docs/deploy.rst +++ b/docs/deploy.rst @@ -112,7 +112,12 @@ If **type** is set to **stringlist** the parameter **values** must be filled wit Key attribute: ^^^^^^^^^^^^^^ -One attribute must be used as a unique key across all backends: +One attribute must be used as a unique key across all backends. + +It acts as a reconciliation key. + +It also marks which attribute must be used within ldapcherry (ex: querysting parameter in links) +to point to one given user. To set the key attribute, you must set **key** to **True** on this attribute. @@ -445,16 +450,16 @@ Logging LdapCherry has two loggers, one for errors and applicative actions (login, del/add, logout...) and one for access logs. -Each logger can be configured to log to syslog, file or be disabled. +Each logger can be configured to log to **syslog**, **file**, **stdout** or be disabled. Logging parameters: +--------------------+---------+---------------------------------+-------------------------------------------------+----------------------------------------+ | Parameter | Section | Description | Values | Comment | +====================+=========+=================================+=================================================+========================================+ -| log.access_handler | global | Logger type for access log | 'syslog', 'file', 'none' | | +| log.access_handler | global | Logger type for access log | 'syslog', 'file', 'stdout', 'none' | | +--------------------+---------+---------------------------------+-------------------------------------------------+----------------------------------------+ -| log.error_handler | global | Logger type for applicative log | 'syslog', 'file', 'none' | | +| log.error_handler | global | Logger type for applicative log | 'syslog', 'file', 'stdout', 'none' | | +--------------------+---------+---------------------------------+-------------------------------------------------+----------------------------------------+ | log.access_file | global | log file for access log | path to log file | only used if log.access_handler='file' | +--------------------+---------+---------------------------------+-------------------------------------------------+----------------------------------------+ @@ -477,6 +482,14 @@ Example: log.level = 'info' +.. warning:: + + 'debug' should not be used in production. + + It tends to log a lot. + More significantly can represent a security issue, + as things like passwords will be logged 'clear text'. + Custom javascript ~~~~~~~~~~~~~~~~~ diff --git a/docs/install.rst b/docs/install.rst index 448644d..896a576 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -1,9 +1,35 @@ Install ======= +Using pip +--------- + +Install the dependencies from the operating system's repos, and then install ldapcherry using `pip`: + +.. sourcecode:: bash + + # RHEL/CentOS + $ sudo yum install epel-release && sudo yum install python-pip python-devel gcc openldap-devel + + # Debian/Ubuntu + $ sudo apt install python-dev python-pip libldap2-dev libsasl2-dev libssl-dev + + $ pip install ldapcherry + From the sources ---------------- +Install the dependencies from the operating system's repos: + +.. sourcecode:: bash + + # RHEL/CentOS + $ sudo yum install epel-release && sudo yum install python36-pip python36-devel python36-PyYAML gcc openldap-devel + + # Debian/Ubuntu + $ sudo apt install python-dev python-pip libldap2-dev libsasl2-dev libssl-dev + + Download the latest release from `GitHub `_. .. sourcecode:: bash diff --git a/goodies/gen-dev-conf.sh b/goodies/gen-dev-conf.sh new file mode 100755 index 0000000..432da6f --- /dev/null +++ b/goodies/gen-dev-conf.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +ROOT=$(readlink -f $(dirname $0)/../) + +cp $ROOT/conf/ldapcherry.ini $ROOT/ldapcherry-dev.ini + +sed -i "s|/etc/ldapcherry/|$ROOT/conf/|" $ROOT/ldapcherry-dev.ini +sed -i "s|/usr/share/ldapcherry/|$ROOT/resources/|" $ROOT/ldapcherry-dev.ini +sed -i "s|^ldap\.|#ldap.|" $ROOT/ldapcherry-dev.ini +sed -i "s|#demo\.|ldap.|" $ROOT/ldapcherry-dev.ini + +GROUPS='cn=nagios admins\\,ou=Group\\,dc=example\\,dc=org, cn=users\\,ou=Group\\,dc=example\\,dc=org' +sed -i "s|ldap.admin.groups.*|ldap.admin.groups = '$GROUPS'|" $ROOT/ldapcherry-dev.ini + + +sed -i "s|^min_length.*|min_length = 3|" $ROOT/ldapcherry-dev.ini +sed -i "s|^min_upper.*|min_upper = 0|" $ROOT/ldapcherry-dev.ini +sed -i "s|^min_digit.*|min_digit = 0|" $ROOT/ldapcherry-dev.ini diff --git a/ldapcherry/__init__.py b/ldapcherry/__init__.py index 60ce654..4a3b584 100644 --- a/ldapcherry/__init__.py +++ b/ldapcherry/__init__.py @@ -15,10 +15,8 @@ import logging.handlers from operator import itemgetter from socket import error as socket_error -import base64 -import cgi -from exceptions import * +from ldapcherry.exceptions import * from ldapcherry.lclogging import * from ldapcherry.roles import Roles from ldapcherry.attributes import Attributes @@ -31,7 +29,13 @@ from mako.template import Template from mako import lookup from mako import exceptions -from sets import Set + + +if sys.version < '3': + from sets import Set as set + from urllib import quote_plus +else: + from urllib.parse import quote_plus SESSION_KEY = '_cp_username' @@ -56,36 +60,6 @@ def _handle_exception(self, e): traceback=True ) - def _escape_list(self, data): - ret = [] - for i in data: - ret.append(cgi.escape(i, True)) - return ret - - def _escape_dict(self, data): - for d in data: - if isinstance(data[d], list): - data[d] = self._escape_list(data[d]) - elif isinstance(data[d], dict): - data[d] = self._escape_dict(data[d]) - elif isinstance(data[d], Set): - data[d] = Set(self._escape_list(data[d])) - else: - data[d] = cgi.escape(data[d], True) - return data - - def _escape(self, data, dtype): - if data is None: - return None - elif dtype == 'search_list': - for d in data: - data[d] = self._escape_dict(data[d]) - elif dtype == 'attr_list': - data = self._escape_dict(data) - elif dtype == 'lonely_groups': - data = self._escape_dict(data) - return data - def _get_param(self, section, key, config, default=None): """ Get configuration parameter "key" from config @str section: the section of the config file @@ -144,10 +118,10 @@ def _check_backends(self): backends = self.backends_params.keys() for b in self.roles.get_backends(): if b not in backends: - raise MissingBackend(b) - for b in self.roles.get_backends(): + raise MissingBackend(b, 'role') + for b in self.attributes.get_backends(): if b not in backends: - raise MissingBackend(b) + raise MissingBackend(b, 'attribute') def _init_backends(self, config): """ Init all backends @@ -168,7 +142,7 @@ def _init_backends(self, config): try: self.backends_display_names[backend] = \ self.backends_params[backend]['display_name'] - except: + except Exception as e: self.backends_display_names[backend] = backend self.backends_params[backend]['display_name'] = backend params = self.backends_params[backend] @@ -178,7 +152,7 @@ def _init_backends(self, config): except Exception as e: raise MissingParameter('backends', backend + '.module') try: - bc = __import__(module, globals(), locals(), ['Backend'], -1) + bc = __import__(module, globals(), locals(), ['Backend'], 0) except Exception as e: self._handle_exception(e) raise BackendModuleLoadingFail(module) @@ -187,7 +161,7 @@ def _init_backends(self, config): key = self.attributes.get_backend_key(backend) self.backends[backend] = bc.Backend( params, - cherrypy.log, + cherrypy.log.error, backend, attrslist, key, @@ -219,8 +193,8 @@ def _init_ppolicy(self, config): 'ldapcherry.ppolicy' ) try: - pp = __import__(module, globals(), locals(), ['PPolicy'], -1) - except: + pp = __import__(module, globals(), locals(), ['PPolicy'], 0) + except Exception as e: raise BackendModuleLoadingFail(module) if 'ppolicy' in config: ppcfg = config['ppolicy'] @@ -238,7 +212,7 @@ def _init_auth(self, config): elif self.auth_mode == 'custom': # load custom auth module auth_module = self._get_param('auth', 'auth.module', config) - auth = __import__(auth_module, globals(), locals(), ['Auth'], -1) + auth = __import__(auth_module, globals(), locals(), ['Auth'], 0) self.auth = auth.Auth(config['auth'], cherrypy.log) else: raise WrongParamValue( @@ -279,6 +253,15 @@ def _set_access_log(self, config, level): handler.setFormatter(syslog_formatter) cherrypy.log.access_log.addHandler(handler) + # if stdout, open a logger on stdout + elif access_handler == 'stdout': + cherrypy.log.access_log.handlers = [] + handler = logging.StreamHandler(sys.stdout) + formatter = logging.Formatter( + 'ldapcherry.access - %(levelname)s - %(message)s' + ) + handler.setFormatter(formatter) + cherrypy.log.access_log.addHandler(handler) # if file, we keep the default elif access_handler == 'file': pass @@ -324,6 +307,15 @@ def _set_error_log(self, config, level, debug=False): handler.setFormatter(syslog_formatter) cherrypy.log.error_log.addHandler(handler) + # if stdout, open a logger on stdout + elif error_handler == 'stdout': + cherrypy.log.error_log.handlers = [] + handler = logging.StreamHandler(sys.stdout) + formatter = logging.Formatter( + 'ldapcherry.app - %(levelname)s - %(message)s' + ) + handler.setFormatter(formatter) + cherrypy.log.error_log.addHandler(handler) # if file, we keep the default elif error_handler == 'file': pass @@ -338,6 +330,7 @@ def _set_error_log(self, config, level, debug=False): cherrypy.log.error_log.setLevel(level) if debug: + cherrypy.log.error_log.handlers = [] handler = logging.StreamHandler(sys.stderr) handler.setLevel(logging.DEBUG) cherrypy.log.error_log.addHandler(handler) @@ -387,7 +380,8 @@ def _load_templates(self, config): ) # preload templates self.temp_lookup = lookup.TemplateLookup( - directories=self.template_dir, input_encoding='utf-8' + directories=self.template_dir, input_encoding='utf-8', + default_filters=['unicode', 'h'] ) # load each template self.temp = {} @@ -573,7 +567,7 @@ def _check_session(self): def _check_auth(self, must_admin, redir_login=True): """ check if a user is autheticated and, optionnaly an administrator - if user not authentifaced -> redirection to login page (with base64 + if user not authenticated -> redirect to login page (with escaped URL of the originaly requested page (redirection after login) if user authenticated, not admin and must_admin enabled -> 403 error @boolean must_admin: flag "user must be an administrator to access @@ -588,13 +582,13 @@ def _check_auth(self, must_admin, redir_login=True): qs = '' else: qs = '?' + cherrypy.request.query_string - # base64 of the requested URL - b64requrl = base64.b64encode(cherrypy.url() + qs) + # Escaped version of the requested URL + quoted_requrl = quote_plus(cherrypy.url() + qs) if not username: - # return to login page (with base64 of the url in query string + # return to login page (with quoted url in query string) if redir_login: raise cherrypy.HTTPRedirect( - "/signin?url=%(url)s" % {'url': b64requrl}, + "/signin?url=%(url)s" % {'url': quoted_requrl}, ) else: raise cherrypy.HTTPError( @@ -606,7 +600,7 @@ def _check_auth(self, must_admin, redir_login=True): or not cherrypy.session['connected']: if redir_login: raise cherrypy.HTTPRedirect( - "/signin?url=%(url)s" % {'url': b64requrl}, + "/signin?url=%(url)s" % {'url': quoted_requrl}, ) else: raise cherrypy.HTTPError( @@ -631,7 +625,7 @@ def _check_auth(self, must_admin, redir_login=True): else: if redir_login: raise cherrypy.HTTPRedirect( - "/signin?url=%(url)s" % {'url': b64requrl}, + "/signin?url=%(url)s" % {'url': quoted_requrl}, ) else: raise cherrypy.HTTPError( @@ -671,6 +665,7 @@ def _adduser(self, params): self._add_notification( 'User already exists in backend "' + b + '"' ) + return if not added: raise e @@ -694,7 +689,7 @@ def _adduser(self, params): roles.append(r) groups = self.roles.get_groups(roles) for b in groups: - self.backends[b].add_to_groups(username, Set(groups[b])) + self.backends[b].add_to_groups(username, set(groups[b])) cherrypy.log.error( msg="user '" + username + "' made member of " + @@ -822,10 +817,10 @@ def _modify(self, params): if b not in g: g[b] = [] tmp = \ - Set(groups_add[b]) - \ - Set(groups_keep[b]) - \ - Set(groups_current[b]) - \ - Set(lonely_groups[b]) + set(groups_add[b]) - \ + set(groups_keep[b]) - \ + set(groups_current[b]) - \ + set(lonely_groups[b]) cherrypy.log.error( msg="user '" + username + "' added to groups: " + str(list(tmp)) + " in backend '" + b + "'", @@ -839,11 +834,11 @@ def _modify(self, params): g[b] = [] tmp = \ ( - (Set(groups_rm[b]) | Set(groups_remove[b])) - - (Set(groups_keep[b]) | Set(groups_add[b])) + (set(groups_rm[b]) | set(groups_remove[b])) - + (set(groups_keep[b]) | set(groups_add[b])) ) & \ ( - Set(groups_current[b]) | Set(lonely_groups[b]) + set(groups_current[b]) | set(lonely_groups[b]) ) cherrypy.log.error( msg="user '" + username + "' removed from groups: " + @@ -919,7 +914,7 @@ def login(self, login, password, url=None): if url is None: redirect = "/" else: - redirect = base64.b64decode(url) + redirect = url raise cherrypy.HTTPRedirect(redirect) else: message = "login failed for user '%(user)s'" % { @@ -932,7 +927,7 @@ def login(self, login, password, url=None): if url is None: qs = '' else: - qs = '?url=' + url + qs = '?url=' + quote_plus(url) raise cherrypy.HTTPRedirect("/signin" + qs) @cherrypy.expose @@ -969,7 +964,7 @@ def index(self): return self.temp['index.tmpl'].render( is_admin=is_admin, attrs_list=attrs_list, - searchresult=self._escape(user_attrs, 'attr_list'), + searchresult=user_attrs, notifications=self._empty_notification(), ) @@ -985,7 +980,7 @@ def searchuser(self, searchstring=None): res = None attrs_list = self.attributes.get_search_attributes() return self.temp['searchuser.tmpl'].render( - searchresult=self._escape(res, 'search_list'), + searchresult=res, attrs_list=attrs_list, is_admin=is_admin, custom_js=self.custom_js, @@ -997,7 +992,7 @@ def searchuser(self, searchstring=None): def checkppolicy(self, **params): """ search user page """ self._check_auth(must_admin=False, redir_login=False) - keys = params.keys() + keys = list(params.keys()) if len(keys) != 1: cherrypy.response.status = 400 return "bad argument" @@ -1022,7 +1017,7 @@ def searchadmin(self, searchstring=None): res = None attrs_list = self.attributes.get_search_attributes() return self.temp['searchadmin.tmpl'].render( - searchresult=self._escape(res, 'search_list'), + searchresult=res, attrs_list=attrs_list, is_admin=is_admin, custom_js=self.custom_js, @@ -1085,7 +1080,7 @@ def delete(self, user): is_admin = self._check_admin() try: referer = cherrypy.request.headers['Referer'] - except: + except Exception as e: referer = '/' self._deleteuser(user) self._add_notification('User Deleted') @@ -1104,7 +1099,7 @@ def modify(self, user=None, **params): self._add_notification("User modified") try: referer = cherrypy.request.headers['Referer'] - except: + except Exception as e: referer = '/' raise cherrypy.HTTPRedirect(referer) @@ -1143,7 +1138,7 @@ def modify(self, user=None, **params): try: form = self.temp['form.tmpl'].render( attributes=self.attributes.attributes, - values=self._escape(user_attrs, 'attr_list'), + values=user_attrs, modify=True, keyattr=key, autofill=False @@ -1161,10 +1156,7 @@ def modify(self, user=None, **params): form=form, roles=roles, is_admin=is_admin, - standalone_groups=self._escape( - standalone_groups, - 'lonely_groups' - ), + standalone_groups=standalone_groups, backends_display_names=self.backends_display_names, custom_js=self.custom_js, notifications=self._empty_notification(), @@ -1178,7 +1170,7 @@ def modify(self, user=None, **params): @cherrypy.expose @exception_decorator - def default(self, attr='', **params): + def default(self, attr='', *args, **params): cherrypy.response.status = 404 self._check_auth(must_admin=False) is_admin = self._check_admin() @@ -1219,7 +1211,7 @@ def selfmodify(self, **params): form = self.temp['form.tmpl'].render( attributes=self.attributes.get_selfattributes(), - values=self._escape(user_attrs, 'attr_list'), + values=user_attrs, modify=True, autofill=False ) diff --git a/ldapcherry/attributes.py b/ldapcherry/attributes.py index 01fa396..a544569 100644 --- a/ldapcherry/attributes.py +++ b/ldapcherry/attributes.py @@ -12,9 +12,11 @@ from ldapcherry.pyyamlwrapper import loadNoDump from ldapcherry.pyyamlwrapper import DumplicatedKey from ldapcherry.exceptions import * -from sets import Set import yaml +if sys.version < '3': + from sets import Set as set + # List of available types for form types = ['string', 'textfield', 'email', 'int', 'stringlist', 'fix', 'password'] @@ -24,14 +26,14 @@ class Attributes: def __init__(self, attributes_file): self.attributes_file = attributes_file - self.backends = Set([]) + self.backends = set([]) self.self_attributes = {} self.backend_attributes = {} self.displayed_attributes = {} self.key = None try: stream = open(attributes_file, 'r') - except: + except Exception as e: raise MissingAttributesFile(attributes_file) try: self.attributes = loadNoDump(stream) @@ -69,7 +71,7 @@ def __init__(self, attributes_file): raise MissingUserKey() def _is_email(self, email): - pattern = '[\.\w]{1,}[@]\w+[.]\w+' + pattern = r'[\+\.\w]+@[-\.\w]+\.\w+' if re.match(pattern, email): return True else: @@ -128,7 +130,9 @@ def get_backends(self): def get_backend_attributes(self, backend): if backend not in self.backends: raise WrongBackend(backend) - return self.backend_attributes[backend].keys() + ret = list(self.backend_attributes[backend].keys()) + ret.sort() + return ret def get_backend_key(self, backend): if backend not in self.backends: diff --git a/ldapcherry/backend/backendAD.py b/ldapcherry/backend/backendAD.py index 37c194c..9483390 100644 --- a/ldapcherry/backend/backendAD.py +++ b/ldapcherry/backend/backendAD.py @@ -15,6 +15,7 @@ from ldapcherry.exceptions import UserDoesntExist, GroupDoesntExist import os import re +import sys class CaFileDontExist(Exception): @@ -129,11 +130,11 @@ def __init__(self, config, logger, name, attrslist, key): self.dn_user_attr = 'cn' self.key = 'sAMAccountName' self.objectclasses = [ - 'top', - 'person', - 'organizationalPerson', - 'user', - 'posixAccount', + self._byte_p23('top'), + self._byte_p23('person'), + self._byte_p23('organizationalPerson'), + self._byte_p23('user'), + self._byte_p23('posixAccount'), ] self.group_attrs = { 'member': "%(dn)s" @@ -142,16 +143,25 @@ def __init__(self, config, logger, name, attrslist, key): self.attrlist = [] self.group_attrs_keys = [] for a in attrslist: - self.attrlist.append(self._str(a)) + self.attrlist.append(self._byte_p2(a)) - if 'cn' not in self.attrlist: + if self._byte_p2('cn') not in self.attrlist: raise MissingAttr() - if 'unicodePwd' not in self.attrlist: + if self._byte_p2('unicodePwd') not in self.attrlist: raise MissingAttr() + if sys.version < '3': + @staticmethod + def _tobyte(in_int): + return str(in_int) + else: + @staticmethod + def _tobyte(in_int): + return in_int.to_bytes(4, byteorder='big') + def _search_group(self, searchfilter, groupdn): - searchfilter = self._str(searchfilter) + searchfilter = self._byte_p2(searchfilter) ldap_client = self._bind() try: r = ldap_client.search_s( @@ -183,22 +193,24 @@ def _set_password(self, name, password, by_cn=True): ldap_client = self._bind() if by_cn: - dn = self._str('CN=%(cn)s,%(user_dn)s' % { + dn = self._byte_p2('CN=%(cn)s,%(user_dn)s' % { 'cn': name, 'user_dn': self.userdn }) else: - dn = self._str(name) + dn = self._byte_p2(name) attrs = {} - attrs['unicodePwd'] = self._str(password_value) + attrs['unicodePwd'] = self._modlist(self._byte_p2(password_value)) ldif = modlist.modifyModlist({'unicodePwd': 'tmp'}, attrs) ldap_client.modify_s(dn, ldif) del(attrs['unicodePwd']) - attrs['UserAccountControl'] = str(NORMAL_ACCOUNT) + attrs['UserAccountControl'] = self._modlist( + self._tobyte(NORMAL_ACCOUNT) + ) ldif = modlist.modifyModlist({'UserAccountControl': 'tmp'}, attrs) ldap_client.modify_s(dn, ldif) @@ -212,7 +224,7 @@ def set_attrs(self, username, attrs): if 'unicodePwd' in attrs: password = attrs['unicodePwd'] del(attrs['unicodePwd']) - userdn = self._get_user(self._str(username), NO_ATTR) + userdn = self._get_user(self._byte_p2(username), NO_ATTR) self._set_password(userdn, password, False) super(Backend, self).set_attrs(username, attrs) @@ -226,7 +238,7 @@ def del_from_groups(self, username, groups): def get_groups(self, username): username = ldap.filter.escape_filter_chars(username) - userdn = self._get_user(self._str(username), NO_ATTR) + userdn = self._get_user(self._byte_p2(username), NO_ATTR) searchfilter = self.group_filter_tmpl % { 'userdn': userdn, @@ -246,7 +258,7 @@ def get_groups(self, username): ) for entry in groups: - ret.append(entry[1]['cn'][0]) + ret.append(self._uni(entry[1]['cn'][0])) return ret def auth(self, username, password): @@ -256,8 +268,8 @@ def auth(self, username, password): ldap_client = self._connect() try: ldap_client.simple_bind_s( - self._str(binddn), - self._str(password) + self._byte_p2(binddn), + self._byte_p2(password) ) except ldap.INVALID_CREDENTIALS: ldap_client.unbind_s() diff --git a/ldapcherry/backend/backendDemo.py b/ldapcherry/backend/backendDemo.py index db3559b..6e0e0b4 100644 --- a/ldapcherry/backend/backendDemo.py +++ b/ldapcherry/backend/backendDemo.py @@ -7,12 +7,15 @@ # This is a demo backend -from sets import Set + +import sys import ldapcherry.backend from ldapcherry.exceptions import UserDoesntExist, \ GroupDoesntExist, MissingParameter, \ UserAlreadyExists import re +if sys.version < '3': + from sets import Set as set class Backend(ldapcherry.backend.Backend): @@ -37,13 +40,17 @@ def __init__(self, config, logger, name, attrslist, key): self.backend_name = name admin_user = self.get_param('admin.user', 'admin') admin_password = self.get_param('admin.password', 'admin') - admin_groups = Set(re.split('\W+', self.get_param('admin.groups'))) + admin_groups = set( + self._basic_splitter(self.get_param('admin.groups')) + ) basic_user = self.get_param('basic.user', 'user') basic_password = self.get_param('basic.password', 'user') - basic_groups = Set(re.split('\W+', self.get_param('basic.groups'))) + basic_groups = set( + self._basic_splitter(self.get_param('basic.groups')) + ) pwd_attr = self.get_param('pwd_attr') - self.search_attrs = Set( - re.split('\W+', self.get_param('search_attributes')), + self.search_attrs = set( + re.split(r'\W+', self.get_param('search_attributes')), ) self.pwd_attr = pwd_attr self.admin_user = admin_user @@ -60,6 +67,11 @@ def __init__(self, config, logger, name, attrslist, key): 'groups': basic_groups, } + @staticmethod + def _basic_splitter(in_str): + return [re.sub(r'(? 3.0.0 and python 3, it gets tricky, + # some parts of python-ldap takes string, specially the filters/escaper. + # + # so we have now: + # *_byte_p2 (unicode -> bytes conversion for python 2) + # *_byte_p3 (unicode -> bytes conversion for python 3) + # *_byte_p23 (unicode -> bytes conversion for python AND 3) + def _byte_p23(self, s): """unicode -> bytes conversion""" if s is None: return None return s.encode('utf-8') - def _uni(self, s): - """bytes -> unicode conversion""" - if s is None: - return None - return s.decode('utf-8', 'ignore') + if sys.version < '3': + def _byte_p2(self, s): + """unicode -> bytes conversion (python 2)""" + if s is None: + return None + return s.encode('utf-8') + + def _byte_p3(self, s): + """pass through (does something in python 3)""" + return s + + def _uni(self, s): + """bytes -> unicode conversion""" + if s is None: + return None + return s.decode('utf-8', 'ignore') + + def attrs_pretreatment(self, attrs): + attrs_srt = {} + for a in attrs: + attrs_srt[self._byte_p2(a)] = self._modlist( + self._byte_p2(attrs[a]) + ) + return attrs_srt + else: + def _byte_p2(self, s): + """pass through (does something in python 2)""" + return s + + def _byte_p3(self, s): + """unicode -> bytes conversion""" + if s is None: + return None + return s.encode('utf-8') + + def _uni(self, s): + """bytes -> unicode conversion""" + if s is None: + return None + if type(s) is not str: + return s.decode('utf-8', 'ignore') + else: + return s + + def attrs_pretreatment(self, attrs): + attrs_srt = {} + for a in attrs: + attrs_srt[self._byte_p2(a)] = self._modlist( + self._byte_p3(attrs[a]) + ) + return attrs_srt def auth(self, username, password): """Authentication of a user""" - binddn = self._get_user(self._str(username), NO_ATTR) + binddn = self._get_user(self._byte_p2(username), NO_ATTR) if binddn is not None: ldap_client = self._connect() try: ldap_client.simple_bind_s( - self._str(binddn), - self._str(password) + self._byte_p2(binddn), + self._byte_p2(password) ) except ldap.INVALID_CREDENTIALS: ldap_client.unbind_s() @@ -346,30 +415,35 @@ def auth(self, username, password): else: return False - def attrs_pretreatment(self, attrs): - attrs_str = {} - for a in attrs: - attrs_str[self._str(a)] = self._str(attrs[a]) - return attrs_str + if PYTHON_LDAP_MAJOR_VERSION == '2': + @staticmethod + def _modlist(in_attr): + return in_attr + + else: + @staticmethod + def _modlist(in_attr): + return [in_attr] def add_user(self, attrs): """add a user""" ldap_client = self._bind() # encoding crap - attrs_str = self.attrs_pretreatment(attrs) + attrs_srt = self.attrs_pretreatment(attrs) - attrs_str['objectClass'] = self.objectclasses + attrs_srt[self._byte_p2('objectClass')] = self.objectclasses # construct is DN dn = \ - self._str(self.dn_user_attr) + \ - '=' + \ - ldap.dn.escape_dn_chars( - self._str(attrs[self.dn_user_attr]) + self._byte_p2(self.dn_user_attr) + \ + self._byte_p2('=') + \ + self._byte_p2(ldap.dn.escape_dn_chars( + attrs[self.dn_user_attr] + ) ) + \ - ',' + \ - self._str(self.userdn) - # gen the ldif fir add_s and add the user - ldif = modlist.addModlist(attrs_str) + self._byte_p2(',') + \ + self._byte_p2(self.userdn) + # gen the ldif first add_s and add the user + ldif = modlist.addModlist(attrs_srt) try: ldap_client.add_s(dn, ldif) except ldap.ALREADY_EXISTS as e: @@ -383,7 +457,7 @@ def del_user(self, username): """delete a user""" ldap_client = self._bind() # recover the user dn - dn = self._str(self._get_user(self._str(username), NO_ATTR)) + dn = self._byte_p2(self._get_user(self._byte_p2(username), NO_ATTR)) # delete if dn is not None: ldap_client.delete_s(dn) @@ -393,17 +467,17 @@ def del_user(self, username): ldap_client.unbind_s() def set_attrs(self, username, attrs): - """ Set user attributes""" + """ set user attributes""" ldap_client = self._bind() - tmp = self._get_user(self._str(username), ALL_ATTRS) + tmp = self._get_user(self._byte_p2(username), ALL_ATTRS) if tmp is None: raise UserDoesntExist(username, self.backend_name) - dn = self._str(tmp[0]) + dn = self._byte_p2(tmp[0]) old_attrs = tmp[1] for attr in attrs: - bcontent = self._str(attrs[attr]) - battr = self._str(attr) - new = {battr: bcontent} + bcontent = self._byte_p2(attrs[attr]) + battr = self._byte_p2(attr) + new = {battr: self._modlist(self._byte_p3(bcontent))} # if attr is dn entry, use rename if attr.lower() == self.dn_user_attr.lower(): ldap_client.rename_s( @@ -420,39 +494,42 @@ def set_attrs(self, username, attrs): if type(old_attrs[attr]) is list: tmp = [] for value in old_attrs[attr]: - tmp.append(self._str(value)) + tmp.append(self._byte_p2(value)) bold_value = tmp else: - bold_value = self._str(old_attrs[attr]) + bold_value = self._modlist( + self._byte_p3(old_attrs[attr]) + ) old = {battr: bold_value} # attribute is not set, just add it else: old = {} ldif = modlist.modifyModlist(old, new) - try: - ldap_client.modify_s(dn, ldif) - except Exception as e: - ldap_client.unbind_s() - self._exception_handler(e) + if ldif: + try: + ldap_client.modify_s(dn, ldif) + except Exception as e: + ldap_client.unbind_s() + self._exception_handler(e) ldap_client.unbind_s() def add_to_groups(self, username, groups): ldap_client = self._bind() # recover dn of the user and his attributes - tmp = self._get_user(self._str(username), ALL_ATTRS) + tmp = self._get_user(self._byte_p2(username), ALL_ATTRS) dn = tmp[0] attrs = tmp[1] attrs['dn'] = dn self._normalize_group_attrs(attrs) - dn = self._str(tmp[0]) + dn = self._byte_p2(tmp[0]) # add user to all groups for group in groups: - group = self._str(group) + group = self._byte_p2(group) # iterate on group membership attributes for attr in self.group_attrs: # fill the content template - content = self._str(self.group_attrs[attr] % attrs) + content = self._byte_p2(self.group_attrs[attr] % attrs) self._logger( severity=logging.DEBUG, msg="%(backend)s: adding user '%(user)s'" @@ -466,11 +543,14 @@ def add_to_groups(self, username, groups): 'backend': self.backend_name } ) - ldif = modlist.modifyModlist({}, {attr: content}) + ldif = modlist.modifyModlist( + {}, + {attr: self._modlist(self._byte_p3(content))} + ) try: ldap_client.modify_s(group, ldif) # if already member, not a big deal, just log it and continue - except ldap.TYPE_OR_VALUE_EXISTS as e: + except (ldap.TYPE_OR_VALUE_EXISTS, ldap.ALREADY_EXISTS) as e: self._logger( severity=logging.INFO, msg="%(backend)s: user '%(user)s'" @@ -494,19 +574,19 @@ def del_from_groups(self, username, groups): # it follows the same logic than add_to_groups # but with MOD_DELETE ldap_client = self._bind() - tmp = self._get_user(self._str(username), ALL_ATTRS) + tmp = self._get_user(self._byte_p2(username), ALL_ATTRS) if tmp is None: raise UserDoesntExist(username, self.backend_name) dn = tmp[0] attrs = tmp[1] attrs['dn'] = dn self._normalize_group_attrs(attrs) - dn = self._str(tmp[0]) + dn = self._byte_p2(tmp[0]) for group in groups: - group = self._str(group) + group = self._byte_p2(group) for attr in self.group_attrs: - content = self._str(self.group_attrs[attr] % attrs) - ldif = [(ldap.MOD_DELETE, attr, content)] + content = self._byte_p2(self.group_attrs[attr] % attrs) + ldif = [(ldap.MOD_DELETE, attr, self._byte_p3(content))] try: ldap_client.modify_s(group, ldif) except ldap.NO_SUCH_ATTRIBUTE as e: @@ -529,7 +609,9 @@ def del_from_groups(self, username, groups): def search(self, searchstring): """Search users""" # escape special char to avoid injection - searchstring = ldap.filter.escape_filter_chars(self._str(searchstring)) + searchstring = ldap.filter.escape_filter_chars( + self._byte_p2(searchstring) + ) # fill the search string template searchfilter = self.search_filter_tmpl % { 'searchstring': searchstring @@ -554,7 +636,7 @@ def search(self, searchstring): def get_user(self, username): """Gest a specific user""" ret = {} - tmp = self._get_user(self._str(username), ALL_ATTRS) + tmp = self._get_user(self._byte_p2(username), ALL_ATTRS) if tmp is None: raise UserDoesntExist(username, self.backend_name) attrs_tmp = tmp[1] @@ -568,7 +650,7 @@ def get_user(self, username): def get_groups(self, username): """Get all groups of a user""" - username = ldap.filter.escape_filter_chars(self._str(username)) + username = ldap.filter.escape_filter_chars(self._byte_p2(username)) userdn = self._get_user(username, NO_ATTR) searchfilter = self.group_filter_tmpl % { @@ -579,5 +661,5 @@ def get_groups(self, username): groups = self._search(searchfilter, NO_ATTR, self.groupdn) ret = [] for entry in groups: - ret.append(entry[0]) + ret.append(self._uni(entry[0])) return ret diff --git a/scripts/ldapcherryd b/ldapcherry/cli.py similarity index 99% rename from scripts/ldapcherryd rename to ldapcherry/cli.py index 1547caf..626ef2e 100755 --- a/scripts/ldapcherryd +++ b/ldapcherry/cli.py @@ -95,14 +95,14 @@ def new_as_dict(self, raw=True, vars=None): # Always start the engine; this will start all other services try: engine.start() - except: + except Exception as e: # Assume the error has been logged already via bus.log. sys.exit(1) else: engine.block() -if __name__ == '__main__': +def main(): from optparse import OptionParser p = OptionParser() @@ -142,3 +142,7 @@ def new_as_dict(self, raw=True, vars=None): start(options.config, options.daemonize, options.environment, options.fastcgi, options.scgi, options.pidfile, options.cgi, options.debug) + + +if __name__ == '__main__': + main() diff --git a/ldapcherry/exceptions.py b/ldapcherry/exceptions.py index 0bd39b5..cb3ac54 100644 --- a/ldapcherry/exceptions.py +++ b/ldapcherry/exceptions.py @@ -46,11 +46,12 @@ def __init__(self, role): class MissingBackend(Exception): - def __init__(self, backend): + def __init__(self, backend, type_conf): self.backend = backend self.log = \ - "backend '%(backend)s' does not exist in main config file" % \ - {'backend': backend} + "backend '%(backend)s' does not exist in main config file " \ + "but is still declared in '%(type_conf)s' file" % \ + {'backend': backend, 'type_conf': type_conf} class WrongBackend(Exception): diff --git a/ldapcherry/roles.py b/ldapcherry/roles.py index 0bd1dd6..a12fd70 100644 --- a/ldapcherry/roles.py +++ b/ldapcherry/roles.py @@ -9,12 +9,14 @@ import sys import copy -from sets import Set from ldapcherry.pyyamlwrapper import loadNoDump from ldapcherry.pyyamlwrapper import DumplicatedKey from ldapcherry.exceptions import * import yaml +if sys.version < '3': + from sets import Set as set + class CustomDumper(yaml.SafeDumper): "A custom YAML dumper that never emits aliases" @@ -27,10 +29,10 @@ class Roles: def __init__(self, role_file): self.role_file = role_file - self.backends = Set([]) + self.backends = set([]) try: stream = open(role_file, 'r') - except: + except Exception as e: raise MissingRolesFile(role_file) try: self.roles_raw = loadNoDump(stream) @@ -51,11 +53,12 @@ def _merge_groups(self, backends_list): for backends in backends_list: for b in backends: if b not in ret: - ret[b] = Set([]) + ret[b] = set([]) for group in backends[b]: ret[b].add(group) for b in ret: ret[b] = list(ret[b]) + ret[b].sort() return ret def _flatten(self, roles=None, groups=None): @@ -134,8 +137,8 @@ def _nest(self): if roleid not in self.graph: self.graph[roleid] = { - 'parent_roles': Set([]), - 'sub_roles': Set([]) + 'parent_roles': set([]), + 'sub_roles': set([]) } # Create the nested groups @@ -147,7 +150,7 @@ def _nest(self): if b not in self.group2roles: self.group2roles[b] = {} if g not in self.group2roles[b]: - self.group2roles[b][g] = Set([]) + self.group2roles[b][g] = set([]) self.group2roles[b][g].add(roleid) parent_roles[roleid] = [] @@ -223,7 +226,7 @@ def _check_member( # add groups of the role to usedgroups for b in self.roles[role]['backends_groups']: if b not in usedgroups: - usedgroups[b] = Set([]) + usedgroups[b] = set([]) for g in self.roles[role]['backends_groups'][b]: usedgroups[b].add(g) @@ -254,11 +257,11 @@ def get_groups_to_remove(self, current_roles, roles_to_remove): """get groups to remove from list of roles to remove and current roles """ - current_roles = Set(current_roles) + current_roles = set(current_roles) ret = {} - roles_to_remove = Set(roles_to_remove) - tmp = Set([]) + roles_to_remove = set(roles_to_remove) + tmp = set([]) # get sub roles of the role to remove that the user belongs to # if we remove a role, there is no reason to keep the sub roles for r in roles_to_remove: @@ -267,7 +270,7 @@ def get_groups_to_remove(self, current_roles, roles_to_remove): tmp.add(sr) roles_to_remove = roles_to_remove.union(tmp) - roles = current_roles.difference(Set(roles_to_remove)) + roles = current_roles.difference(set(roles_to_remove)) groups_roles = self._get_groups(roles) groups_roles_to_remove = self._get_groups(roles_to_remove) @@ -284,12 +287,12 @@ def _get_groups(self, roles): for b in self.flatten[r]['backends_groups']: groups = self.flatten[r]['backends_groups'][b] if b not in ret: - ret[b] = Set(groups) - ret[b] = ret[b].union(Set(groups)) + ret[b] = set(groups) + ret[b] = ret[b].union(set(groups)) return ret def _get_subroles(self, role): - ret = Set([]) + ret = set([]) for sr in self.graph[role]['sub_roles']: tmp = self._get_subroles(sr) tmp.add(sr) @@ -298,10 +301,10 @@ def _get_subroles(self, role): def get_roles(self, groups): """get list of roles and list of standalone groups""" - roles = Set([]) - parentroles = Set([]) - notroles = Set([]) - tmp = Set([]) + roles = set([]) + parentroles = set([]) + notroles = set([]) + tmp = set([]) usedgroups = {} unusedgroups = {} ret = {} @@ -316,7 +319,7 @@ def get_roles(self, groups): for g in groups[b]: if b not in usedgroups or g not in usedgroups[b]: if b not in unusedgroups: - unusedgroups[b] = Set([]) + unusedgroups[b] = set([]) unusedgroups[b].add(g) ret['roles'] = roles diff --git a/ldapcherry/version.py b/ldapcherry/version.py index 75f8af2..4ce55d1 100644 --- a/ldapcherry/version.py +++ b/ldapcherry/version.py @@ -5,4 +5,4 @@ # ldapCherry # Copyright (c) 2014 Carpentier Pierre-Francois -version = '0.5.2' +version = '1.1.1' diff --git a/requirements-el7.txt b/requirements-el7.txt new file mode 100644 index 0000000..109ab48 --- /dev/null +++ b/requirements-el7.txt @@ -0,0 +1,4 @@ +CherryPy>=3.0.0 +PyYAML +Mako +python-ldap==2.4.15 diff --git a/requirements-stretch.txt b/requirements-stretch.txt new file mode 100644 index 0000000..dcdcf36 --- /dev/null +++ b/requirements-stretch.txt @@ -0,0 +1,4 @@ +CherryPy>=3.0.0 +PyYAML +Mako +python-ldap==2.4.28 diff --git a/resources/templates/adduser.tmpl b/resources/templates/adduser.tmpl index 7b690fc..cd8e31f 100644 --- a/resources/templates/adduser.tmpl +++ b/resources/templates/adduser.tmpl @@ -6,14 +6,14 @@
-