Example #1
0
 def testWrongGetBackendAttributes(self):
     inv = Attributes('./tests/cfg/attributes.yml')
     try:
         ret = inv.get_backend_attributes('notabackend')
     except WrongBackend:
         return
     else:
         raise AssertionError("expected an exception")
Example #2
0
    def reload(self, config=None, debug=False):
        """ load/reload configuration
        @dict: configuration of ldapcherry
        """
        try:
            # log configuration handling
            # get log level
            # (if not in configuration file, log level is set to debug)
            level = get_loglevel(
                self._get_param(
                    'global',
                    'log.level',
                    config,
                    'debug',
                ))
            # configure access log
            self._set_access_log(config, level)
            # configure error log
            self._set_error_log(config, level, debug)

            # load template files
            self._load_templates(config)

            # loading the auth configuration
            self._init_auth(config)

            # Loading the attributes
            self.attributes_file = \
                self._get_param('attributes', 'attributes.file', config)
            cherrypy.log.error(msg="loading attributes file '%(file)s'" %
                               {'file': self.attributes_file},
                               severity=logging.DEBUG)

            self.notifications = {}

            self.attributes = Attributes(self.attributes_file)

            cherrypy.log.error(msg="init directories backends",
                               severity=logging.DEBUG)
            self._init_backends(config)
            self._check_backends()

            # loading the ppolicy
            self._init_ppolicy(config)

            # loading custom javascript
            self._init_custom_js(config)

            cherrypy.log.error(msg="application started",
                               severity=logging.INFO)

        except Exception as e:
            self._handle_exception(e)
            cherrypy.log.error(msg="application failed to start",
                               severity=logging.ERROR)
            exit(1)
Example #3
0
 def testValidateError(self):
     inv = Attributes('./tests/cfg/attributes.yml')
     attrs = {'email': 'notamail', 'uidNumber': 'not an integer', 'shell': '/bin/not in list', 'logscript': 'not fixed'}
     for attrid in attrs:
         try:
             inv.check_attr(attrid, attrs[attrid])
         except WrongAttrValue:
             pass
         else:
             raise AssertionError("expected an exception")
Example #4
0
 def testDuplicatePassword(self):
     try:
         inv = Attributes('./tests/cfg/attribute_pwderror.yml')
     except PasswordAttributesCollision:
         return
     else:
         raise AssertionError("expected an exception")
Example #5
0
 def testMissingMandatory(self):
     try:
         inv = Attributes('./tests/cfg/attributes_missing_mandatory.yml')
     except MissingKey:
         return
     else:
         raise AssertionError("expected an exception")
Example #6
0
 def testNoFile(self):
     try:
         inv = Attributes('./tests/cfg/dontexist')
     except MissingAttributesFile:
         return
     else:
         raise AssertionError("expected an exception")
Example #7
0
 def testWrongType(self):
     try:
         inv = Attributes('./tests/cfg/attributes_wrong_type.yml')
     except WrongAttributeType:
         return
     else:
         raise AssertionError("expected an exception")
Example #8
0
    def reload(self, config=None):
        """ load/reload configuration
        @dict: configuration of ldapcherry
        """
        try:
            # log configuration handling
            # get log level
            # (if not in configuration file, log level is set to debug)
            level = get_loglevel(
                self._get_param(
                    'global',
                    'log.level',
                    config,
                    'debug',
                    )
                )
            # configure access log
            self._set_access_log(config, level)
            # configure error log
            self._set_error_log(config, level)

            # load template files
            self._load_templates(config)

            # loading the auth configuration
            self._init_auth(config)

            # Loading the attributes
            self.attributes_file = \
                self._get_param('attributes', 'attributes.file', config)
            cherrypy.log.error(
                msg="loading attributes file '%(file)s'" %
                    {'file': self.attributes_file},
                severity=logging.DEBUG
            )

            self.notifications = {}

            self.attributes = Attributes(self.attributes_file)

            cherrypy.log.error(
                msg="init directories backends",
                severity=logging.DEBUG
            )
            self._init_backends(config)
            self._check_backends()

            # loading the ppolicy
            self._init_ppolicy(config)

            # loading custom javascript
            self._init_custom_js(config)

            cherrypy.log.error(
                msg="application started",
                severity=logging.INFO
            )

        except Exception as e:
            self._handle_exception(e)
            cherrypy.log.error(
                msg="application failed to start",
                severity=logging.ERROR
            )
            exit(1)
Example #9
0
class LdapCherry(object):

    def _handle_exception(self, e):
        if hasattr(e, 'log'):
            cherrypy.log.error(
                msg=e.log,
                severity=logging.ERROR
            )
        else:
            cherrypy.log.error(
                msg="uncaught exception: [%(e)s]" % {'e': str(e)},
                severity=logging.ERROR
            )
        # log the traceback as 'debug'
        cherrypy.log.error(
            msg='',
            severity=logging.DEBUG,
            traceback=True
            )

    def _get_param(self, section, key, config, default=None):
        """ Get configuration parameter "key" from config
        @str section: the section of the config file
        @str key: the key to get
        @dict config: the configuration (dictionnary)
        @str default: the default value if parameter "key" is not present
        @rtype: str (value of config['key'] if present default otherwith
        """
        if section in config and key in config[section]:
            return config[section][key]
        if default is not None:
            return default
        else:
            raise MissingParameter(section, key)

    def _get_groups(self, username):
        """ Get groups of a user
        @str username: name of the user
        @rtype: dict, format { '<backend>': [<list of groups>] }
        """
        ret = {}
        for b in self.backends:
            ret[b] = self.backends[b].get_groups(username)
        cherrypy.log.error(
            msg="user '" + username + "' groups: " + str(ret),
            severity=logging.DEBUG,
        )
        return ret

    def _get_roles(self, username):
        """ Get roles of a user
        @str username: name of the user
        @rtype: dict, format { 'roles': [<list of roles>],
            'unusedgroups': [<list of groups not matching roles>] }
        """
        groups = self._get_groups(username)
        user_roles = self.roles.get_roles(groups)
        cherrypy.log.error(
            msg="user '" + username + "' roles: " + str(user_roles),
            severity=logging.DEBUG,
        )
        return user_roles

    def _is_admin(self, username):
        """ Check if a user is an ldapcherry administrator
        @str username: name of the user
        @rtype: bool, True if administrator, False otherwise
        """
        roles = self._get_roles(username)
        return self.roles.is_admin(roles['roles'])

    def _check_backends(self):
        """ Check that every backend in roles and attributes
        is declared in main configuration
        """
        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():
            if b not in backends:
                raise MissingBackend(b)

    def _init_backends(self, config):
        """ Init all backends
        @dict: configuration of ldapcherry
        """
        self.backends_params = {}
        self.backends = {}
        self.backends_display_names = {}
        for entry in config['backends']:
            # split at the first dot
            backend, sep, param = entry.partition('.')
            value = config['backends'][entry]
            if backend not in self.backends_params:
                self.backends_params[backend] = {}
            self.backends_params[backend][param] = value
        for backend in self.backends_params:
            # get the backend display_name
            try:
                self.backends_display_names[backend] = \
                    self.backends_params[backend]['display_name']
            except:
                self.backends_display_names[backend] = backend
                self.backends_params[backend]['display_name'] = backend
            params = self.backends_params[backend]
            # Loading the backend module
            try:
                module = params['module']
            except Exception as e:
                raise MissingParameter('backends', backend + '.module')
            try:
                bc = __import__(module, globals(), locals(), ['Backend'], -1)
            except Exception as e:
                self._handle_exception(e)
                raise BackendModuleLoadingFail(module)
            try:
                attrslist = self.attributes.get_backend_attributes(backend)
                key = self.attributes.get_backend_key(backend)
                self.backends[backend] = bc.Backend(
                    params,
                    cherrypy.log,
                    backend,
                    attrslist,
                    key,
                    )
            except MissingParameter as e:
                raise
            except Exception as e:
                self._handle_exception(e)
                raise BackendModuleInitFail(module)

    def _init_custom_js(self, config):
        self.custom_js = []
        if '/custom' not in config:
            return
        directory = self._get_param(
            '/custom',
            'tools.staticdir.dir',
            config,
            )
        for file in os.listdir(directory):
            if file.endswith(".js"):
                self.custom_js.append(file)

    def _init_ppolicy(self, config):
        module = self._get_param(
            'ppolicy',
            'ppolicy.module',
            config,
            'ldapcherry.ppolicy'
        )
        try:
            pp = __import__(module, globals(), locals(), ['PPolicy'], -1)
        except:
            raise BackendModuleLoadingFail(module)
        if 'ppolicy' in config:
            ppcfg = config['ppolicy']
        else:
            ppcfg = {}
        self.ppolicy = pp.PPolicy(ppcfg, cherrypy.log)

    def _init_auth(self, config):
        """ Init authentication
        @dict: configuration of ldapcherry
        """
        self.auth_mode = self._get_param('auth', 'auth.mode', config)
        if self.auth_mode in ['and', 'or', 'none']:
            pass
        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)
            self.auth = auth.Auth(config['auth'], cherrypy.log)
        else:
            raise WrongParamValue(
                'auth.mode',
                'auth',
                ['and', 'or', 'none', 'custom'],
                )

        self.roles_file = self._get_param('roles', 'roles.file', config)
        cherrypy.log.error(
            msg="loading roles file '%(file)s'" % {'file': self.roles_file},
            severity=logging.DEBUG
        )
        self.roles = Roles(self.roles_file)

    def _set_access_log(self, config, level):
        """ Configure access logs
        """
        access_handler = self._get_param(
            'global',
            'log.access_handler',
            config,
            'syslog',
            )

        # log format for syslog
        syslog_formatter = logging.Formatter(
            "ldapcherry[%(process)d]: %(message)s"
            )

        # replace access log handler by a syslog handler
        if access_handler == 'syslog':
            cherrypy.log.access_log.handlers = []
            handler = logging.handlers.SysLogHandler(
                address='/dev/log',
                facility='user',
                )
            handler.setFormatter(syslog_formatter)
            cherrypy.log.access_log.addHandler(handler)

        # if file, we keep the default
        elif access_handler == 'file':
            pass

        # replace access log handler by a null handler
        elif access_handler == 'none':
            cherrypy.log.access_log.handlers = []
            handler = logging.NullHandler()
            cherrypy.log.access_log.addHandler(handler)

        # set log level
        cherrypy.log.access_log.setLevel(level)

    def _set_error_log(self, config, level):
        """ Configure error logs
        """
        error_handler = self._get_param(
            'global',
            'log.error_handler',
            config,
            'syslog'
            )

        # log format for syslog
        syslog_formatter = logging.Formatter(
            "ldapcherry[%(process)d]: %(message)s",
            )

        # replacing the error handler by a syslog handler
        if error_handler == 'syslog':
            cherrypy.log.error_log.handlers = []

            # redefining log.error method because cherrypy does weird
            # things like adding the date inside the message
            # or adding space even if context is empty
            # (by the way, what's the use of "context"?)
            cherrypy.log.error = syslog_error

            handler = logging.handlers.SysLogHandler(
                address='/dev/log',
                facility='user',
                )
            handler.setFormatter(syslog_formatter)
            cherrypy.log.error_log.addHandler(handler)

        # if file, we keep the default
        elif error_handler == 'file':
            pass

        # replacing the error handler by a null handler
        elif error_handler == 'none':
            cherrypy.log.error_log.handlers = []
            handler = logging.NullHandler()
            cherrypy.log.error_log.addHandler(handler)

        # set log level
        cherrypy.log.error_log.setLevel(level)

    def _auth(self, user, password):
        """ authenticate a user
        @str user: login of the user
        @str password: password of the user
        @rtype: dict, {'connected': <boolean, True if connection succeded>,
            'isadmin': <True if user is ldapcherry administrator>}
        """
        if self.auth_mode == 'none':
            return {'connected': True, 'isadmin': True}
        elif self.auth_mode == 'and':
            ret1 = True
            for b in self.backends:
                ret1 = self.backends[b].auth(user, password) and ret1
        elif self.auth_mode == 'or':
            ret1 = False
            for b in self.backends:
                ret1 = self.backends[b].auth(user, password) or ret1
        elif self.auth_mode == 'custom':
            ret1 = self.auth.auth(user, password)
        else:
            raise Exception()
        if not ret1:
            return {'connected': False, 'isadmin': False}
        else:
            isadmin = self._is_admin(user)
            return {'connected': True, 'isadmin': isadmin}

    def _load_templates(self, config):
        """ load templates
        @dict: configuration of ldapcherry
        """
        # definition of the template directory
        self.template_dir = self._get_param(
            'resources',
            'templates.dir',
            config
            )
        cherrypy.log.error(
            msg="loading templates from dir '%(dir)s'" %
                {'dir': self.template_dir},
            severity=logging.DEBUG
        )
        # preload templates
        self.temp_lookup = lookup.TemplateLookup(
            directories=self.template_dir, input_encoding='utf-8'
            )
        # load each template
        self.temp = {}
        for t in ('index.tmpl', 'error.tmpl', 'login.tmpl',
                  'searchadmin.tmpl', 'searchuser.tmpl', 'adduser.tmpl',
                  'roles.tmpl', 'groups.tmpl', 'form.tmpl', 'selfmodify.tmpl',
                  'modify.tmpl', 'service_unavailable.tmpl'
                  ):
            self.temp[t] = self.temp_lookup.get_template(t)

    def reload(self, config=None):
        """ load/reload configuration
        @dict: configuration of ldapcherry
        """
        try:
            # log configuration handling
            # get log level
            # (if not in configuration file, log level is set to debug)
            level = get_loglevel(
                self._get_param(
                    'global',
                    'log.level',
                    config,
                    'debug',
                    )
                )
            # configure access log
            self._set_access_log(config, level)
            # configure error log
            self._set_error_log(config, level)

            # load template files
            self._load_templates(config)

            # loading the auth configuration
            self._init_auth(config)

            # Loading the attributes
            self.attributes_file = \
                self._get_param('attributes', 'attributes.file', config)
            cherrypy.log.error(
                msg="loading attributes file '%(file)s'" %
                    {'file': self.attributes_file},
                severity=logging.DEBUG
            )

            self.notifications = {}

            self.attributes = Attributes(self.attributes_file)

            cherrypy.log.error(
                msg="init directories backends",
                severity=logging.DEBUG
            )
            self._init_backends(config)
            self._check_backends()

            # loading the ppolicy
            self._init_ppolicy(config)

            # loading custom javascript
            self._init_custom_js(config)

            cherrypy.log.error(
                msg="application started",
                severity=logging.INFO
            )

        except Exception as e:
            self._handle_exception(e)
            cherrypy.log.error(
                msg="application failed to start",
                severity=logging.ERROR
            )
            exit(1)

    def _add_notification(self, message):
        """ add a notification in the notification queue of a user
        """
        sess = cherrypy.session
        username = sess.get(SESSION_KEY, None)
        if username not in self.notifications:
            self.notifications[username] = []
        self.notifications[username].append(message)

    def _empty_notification(self):
        """ empty and return list of message notification
        """
        sess = cherrypy.session
        username = sess.get(SESSION_KEY, None)
        if username in self.notifications:
            ret = self.notifications[username]
        else:
            ret = []
        self.notifications[username] = []
        return ret

    def _merge_user_attrs(self, attrs_backend, attrs_out, backend_name):
        """ merge attributes from one backend search to the attributes dict
        output

        """
        for attr in attrs_backend:
            if attr in self.attributes.backend_attributes[backend_name]:
                attrid = self.attributes.backend_attributes[backend_name][attr]
                if attrid not in attrs_out:
                    attrs_out[attrid] = attrs_backend[attr]

    def _search(self, searchstring):
        """ search users
        @str searchstring: search string
        @rtype: dict, {<user>: {<attr>: <value>}}
        """
        if searchstring is None:
            return {}
        ret = {}
        for b in self.backends:
            tmp = self.backends[b].search(searchstring)
            for u in tmp:
                if u not in ret:
                    ret[u] = {}
                self._merge_user_attrs(tmp[u], ret[u], b)
        return ret

    def _get_user(self, username):
        """ get user attributes
        @str username: user to get
        @rtype: dict, {<attr>: <value>}
        """
        if username is None:
            return {}
        ret = {}
        for b in self.backends:
            try:
                tmp = self.backends[b].get_user(username)
            except UserDoesntExist as e:
                self._handle_exception(e)
                tmp = {}
            self._merge_user_attrs(tmp, ret, b)

        cherrypy.log.error(
            msg="user '" + username + "' attributes " + str(ret),
            severity=logging.DEBUG
        )
        return ret

    def _parse_params(self, params):
        """ get user attributes
        @dict params: form parameters
        @rtype: dict, {<type>: {<attr>: <value>}}
        """
        ret = {'attrs': {}, 'roles': {}, 'groups': {}}
        for p in params:
            # each form attributes is prefixed with type, ex: attr.uidNumber
            # separate this prefix from the attribute name
            p_type, sep, param = p.partition('.')
            if p_type == 'attr':
                ret['attrs'][param] = params[p]
            elif p_type == 'role':
                ret['roles'][param] = params[p]
            elif p_type == 'group':
                # with groups there is a second prefix
                # corresponding to the backend
                backend, sep, value = param.partition('.')
                if backend not in ret['groups']:
                    ret['groups'][backend] = []
                ret['groups'][backend].append(value)
        return ret

    def _check_admin(self):
        """ check in the session database if current user
        is an ldapcherry administrator
        @rtype: boolean, True if administrator, False otherwise
        """
        if self.auth_mode == 'none':
            return True
        return cherrypy.session['isadmin']

    def _check_session(self):
        if self.auth_mode == 'none':
            return 'anonymous'
        return cherrypy.session.get(SESSION_KEY)

    def _check_auth(self, must_admin):
        """ check if a user is autheticated and, optionnaly an administrator
        if user not authentifaced -> redirection to login page (with base64
            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
            this page"
        @rtype str: login of the user
        """
        if self.auth_mode == 'none':
            return 'anonymous'
        username = self._check_session()

        if cherrypy.request.query_string == '':
            qs = ''
        else:
            qs = '?' + cherrypy.request.query_string
        # base64 of the requested URL
        b64requrl = base64.b64encode(cherrypy.url() + qs)
        if not username:
            # return to login page (with base64 of the url in query string
            raise cherrypy.HTTPRedirect(
                "/signin?url=%(url)s" % {'url': b64requrl},
                )

        if 'connected' not in cherrypy.session \
                or not cherrypy.session['connected']:
            raise cherrypy.HTTPRedirect(
                "/signin?url=%(url)s" % {'url': b64requrl},
                )
        if cherrypy.session['connected'] and \
                not cherrypy.session['isadmin']:
            if must_admin:
                # user is not an administrator, so he gets 403 Forbidden
                raise cherrypy.HTTPError(
                    "403 Forbidden",
                    "You are not allowed to access this resource.",
                    )
            else:
                return username
        if cherrypy.session['connected'] and \
                cherrypy.session['isadmin']:
            return username
        else:
            raise cherrypy.HTTPRedirect(
                "/signin?url=%(url)s" % {'url': b64requrl},
                )

    def _adduser(self, params):
        cherrypy.log.error(
            msg="add user form attributes: " + str(params),
            severity=logging.DEBUG
        )
        badd = {}

        for attr in self.attributes.get_attributes():
            if self.attributes.attributes[attr]['type'] == 'password':
                pwd1 = attr + '1'
                pwd2 = attr + '2'
                if params['attrs'][pwd1] != params['attrs'][pwd2]:
                    raise PasswordMissMatch()
                if not self._checkppolicy(params['attrs'][pwd1])['match']:
                    raise PPolicyError()
                params['attrs'][attr] = params['attrs'][pwd1]
            if attr in params['attrs']:
                self.attributes.check_attr(attr, params['attrs'][attr])
                backends = self.attributes.get_backends_attributes(attr)
                for b in backends:
                    if b not in badd:
                        badd[b] = {}
                    badd[b][backends[b]] = params['attrs'][attr]
        for b in badd:
            self.backends[b].add_user(badd[b])

        key = self.attributes.get_key()
        username = params['attrs'][key]
        sess = cherrypy.session
        admin = str(sess.get(SESSION_KEY, None))

        cherrypy.log.error(
            msg="user '" + username + "' added by '" + admin + "'",
            severity=logging.INFO
        )
        cherrypy.log.error(
            msg="user '" + username + "' attributes: " + str(badd),
            severity=logging.DEBUG
        )

        roles = []
        for r in self.roles.get_allroles():
            if r in params['roles']:
                roles.append(r)
        groups = self.roles.get_groups(roles)
        for b in groups:
            self.backends[b].add_to_groups(username, Set(groups[b]))

        cherrypy.log.error(
            msg="user '" + username + "' made member of " +
                str(roles) + " by '" + admin + "'",
            severity=logging.INFO
        )
        cherrypy.log.error(
            msg="user '" + username + "' groups: " + str(groups),
            severity=logging.DEBUG
        )

    def _modify_attrs(self, params, attr_list, username):
        badd = {}
        for attr in attr_list:
            if self.attributes.attributes[attr]['type'] == 'password':
                pwd1 = attr + '1'
                pwd2 = attr + '2'
                if pwd1 in params['attrs']:
                    if params['attrs'][pwd1] != params['attrs'][pwd2]:
                        raise PasswordMissMatch()
                    if params['attrs'][pwd1] != '' and \
                            not self._checkppolicy(
                                params['attrs'][pwd1]
                                )['match']:
                        raise PPolicyError()
                    params['attrs'][attr] = params['attrs'][pwd1]
            if attr in params['attrs'] and params['attrs'][attr] != '':
                self.attributes.check_attr(attr, params['attrs'][attr])
                backends = self.attributes.get_backends_attributes(attr)
                for b in backends:
                    if b not in badd:
                        badd[b] = {}
                    badd[b][backends[b]] = params['attrs'][attr]
        for b in badd:
            self.backends[b].set_attrs(username, badd[b])
        return badd

    def _selfmodify(self, params):
        cherrypy.log.error(
            msg="modify user form attributes: " + str(params),
            severity=logging.DEBUG
        )
        sess = cherrypy.session
        username = str(sess.get(SESSION_KEY, None))
        badd = self._modify_attrs(
            params,
            self.attributes.get_selfattributes(),
            username,
            )
        cherrypy.log.error(
            msg="user '" + username + "' modified his attributes",
            severity=logging.INFO
        )
        cherrypy.log.error(
            msg="user '" + username + "' attributes: " + str(badd),
            severity=logging.DEBUG
        )

    def _modify(self, params):
        cherrypy.log.error(
            msg="modify user form attributes: " + str(params),
            severity=logging.DEBUG
        )
        key = self.attributes.get_key()
        username = params['attrs'][key]

        badd = self._modify_attrs(
            params,
            self.attributes.get_attributes(),
            username
            )

        sess = cherrypy.session
        admin = str(sess.get(SESSION_KEY, None))

        cherrypy.log.error(
            msg="user '" + username + "' modified by '" + admin + "'",
            severity=logging.INFO
        )
        cherrypy.log.error(
            msg="user '" + username + "' attributes: " + str(badd),
            severity=logging.DEBUG
        )

        tmp = self._get_roles(username)
        roles_current = tmp['roles']
        lonely_groups = tmp['unusedgroups']
        roles_member = []
        roles_not_member = []

        groups_keep = {}
        groups_remove = {}

        for b in lonely_groups:
            for g in lonely_groups[b]:
                if b in params['groups'] and g in params['groups'][b]:
                    if b not in groups_keep:
                        groups_keep[b] = []
                    groups_keep[b].append(g)

                else:
                    if b not in groups_remove:
                        groups_remove[b] = []
                    groups_remove[b].append(g)

        for r in self.roles.get_allroles():
            if r in params['roles']:
                roles_member.append(r)
            else:
                roles_not_member.append(r)

        groups_current = self.roles.get_groups(roles_current)
        groups_rm = self.roles.get_groups(roles_not_member)
        groups_add = self.roles.get_groups(roles_member)

        for b in groups_add:
            for g in [groups_add, groups_keep,
                      groups_current, lonely_groups]:
                if b not in g:
                    g[b] = []
            tmp = \
                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 + "'",
                severity=logging.DEBUG
            )
            self.backends[b].add_to_groups(username, tmp)
        for b in groups_rm:
            for g in [groups_remove, groups_rm, groups_add,
                      groups_keep, groups_current, lonely_groups]:
                if b not in g:
                    g[b] = []
            tmp = \
                (
                    (Set(groups_rm[b]) | Set(groups_remove[b])) -
                    (Set(groups_keep[b]) | Set(groups_add[b]))
                ) & \
                (
                    Set(groups_current[b]) | Set(lonely_groups[b])
                )
            cherrypy.log.error(
                msg="user '" + username + "' removed from groups: " +
                    str(list(tmp)) + " in backend '" + b + "'",
                severity=logging.DEBUG
            )
            self.backends[b].del_from_groups(username, tmp)

        cherrypy.log.error(
            msg="user '" + username + "' made member of " +
                str(roles_member) + " by '" + admin + "'",
            severity=logging.INFO
        )

    def _deleteuser(self, username):
        sess = cherrypy.session
        admin = str(sess.get(SESSION_KEY, None))

        for b in self.backends:
            self.backends[b].del_user(username)
            cherrypy.log.error(
                msg="user '" + username + "' deleted from backend '" + b + "'",
                severity=logging.DEBUG
            )

        cherrypy.log.error(
            msg="User '" + username + "' deleted by '" + admin + "'",
            severity=logging.INFO
        )

    def _checkppolicy(self, password):
        return self.ppolicy.check(password)

    @cherrypy.expose
    @exception_decorator
    def signin(self, url=None):
        """simple signin page
        """
        return self.temp['login.tmpl'].render(url=url)

    @cherrypy.expose
    @exception_decorator
    def login(self, login, password, url=None):
        """login page
        """
        auth = self._auth(login, password)
        cherrypy.session['isadmin'] = auth['isadmin']
        cherrypy.session['connected'] = auth['connected']

        if auth['connected']:
            if auth['isadmin']:
                message = \
                    "login success for user '%(user)s' as administrator" % {
                        'user': login
                    }
            else:
                message = \
                    "login success for user '%(user)s' as normal user" % {
                        'user': login
                    }
            cherrypy.log.error(
                msg=message,
                severity=logging.INFO
            )
            cherrypy.session[SESSION_KEY] = cherrypy.request.login = login
            if url is None:
                redirect = "/"
            else:
                redirect = base64.b64decode(url)
            raise cherrypy.HTTPRedirect(redirect)
        else:
            message = "login failed for user '%(user)s'" % {
                'user': login
            }
            cherrypy.log.error(
                msg=message,
                severity=logging.WARNING
            )
            if url is None:
                qs = ''
            else:
                qs = '?url=' + url
            raise cherrypy.HTTPRedirect("/signin" + qs)

    @cherrypy.expose
    @exception_decorator
    def logout(self):
        """ logout page
        """
        sess = cherrypy.session
        username = sess.get(SESSION_KEY, None)
        sess[SESSION_KEY] = None
        if username:
            cherrypy.request.login = None

        cherrypy.log.error(
            msg="user '%(user)s' logout" % {'user': username},
            severity=logging.INFO
        )
        raise cherrypy.HTTPRedirect("/signin")

    @cherrypy.expose
    @exception_decorator
    def index(self):
        """main page rendering
        """
        self._check_auth(must_admin=False)
        is_admin = self._check_admin()
        return self.temp['index.tmpl'].render(
            is_admin=is_admin,
            notifications=self._empty_notification(),
            )

    @cherrypy.expose
    @exception_decorator
    def searchuser(self, searchstring=None):
        """ search user page """
        self._check_auth(must_admin=False)
        is_admin = self._check_admin()
        if searchstring is not None:
            res = self._search(searchstring)
        else:
            res = None
        attrs_list = self.attributes.get_search_attributes()
        return self.temp['searchuser.tmpl'].render(
            searchresult=res,
            attrs_list=attrs_list,
            is_admin=is_admin,
            custom_js=self.custom_js,
            notifications=self._empty_notification(),
            )

    @cherrypy.expose
    @exception_decorator
    def checkppolicy(self, **params):
        """ search user page """
        self._check_auth(must_admin=False)
        keys = params.keys()
        if len(keys) != 1:
            cherrypy.response.status = 400
            return "bad argument"
        password = params[keys[0]]
        is_admin = self._check_admin()
        ret = self._checkppolicy(password)
        if ret['match']:
            cherrypy.response.status = 200
        else:
            cherrypy.response.status = 200
        return json.dumps(ret, separators=(',', ':'))

    @cherrypy.expose
    @exception_decorator
    def searchadmin(self, searchstring=None):
        """ search user page """
        self._check_auth(must_admin=True)
        is_admin = self._check_admin()
        if searchstring is not None:
            res = self._search(searchstring)
        else:
            res = None
        attrs_list = self.attributes.get_search_attributes()
        return self.temp['searchadmin.tmpl'].render(
            searchresult=res,
            attrs_list=attrs_list,
            is_admin=is_admin,
            custom_js=self.custom_js,
            notifications=self._empty_notification(),
            )

    @cherrypy.expose
    @exception_decorator
    def adduser(self, **params):
        """ add user page """
        self._check_auth(must_admin=True)
        is_admin = self._check_admin()

        if cherrypy.request.method.upper() == 'POST':
            params = self._parse_params(params)
            self._adduser(params)
            self._add_notification("User added")

        graph = {}
        for r in self.roles.graph:
            s = list(self.roles.graph[r]['sub_roles'])
            p = list(self.roles.graph[r]['parent_roles'])
            graph[r] = {'sub_roles': s, 'parent_roles': p}
        graph_js = json.dumps(graph, separators=(',', ':'))
        display_names = {}
        for r in self.roles.flatten:
            display_names[r] = self.roles.flatten[r]['display_name']
        roles_js = json.dumps(display_names, separators=(',', ':'))
        form = self.temp['form.tmpl'].render(
            attributes=self.attributes.attributes,
            values=None,
            modify=False,
            autofill=True
            )
        roles = self.temp['roles.tmpl'].render(
            roles=self.roles.flatten,
            graph=self.roles.graph,
            graph_js=graph_js,
            roles_js=roles_js,
            current_roles=None,
            )
        return self.temp['adduser.tmpl'].render(
            form=form,
            roles=roles,
            is_admin=is_admin,
            custom_js=self.custom_js,
            notifications=self._empty_notification(),
            )

    @cherrypy.expose
    @exception_decorator
    def delete(self, user):
        """ remove user page """
        self._check_auth(must_admin=True)
        is_admin = self._check_admin()
        try:
            referer = cherrypy.request.headers['Referer']
        except:
            referer = '/'
        self._deleteuser(user)
        self._add_notification('User Deleted')
        raise cherrypy.HTTPRedirect(referer)

    @cherrypy.expose
    @exception_decorator
    def modify(self, user=None, **params):
        """ modify user page """
        self._check_auth(must_admin=True)
        is_admin = self._check_admin()

        if cherrypy.request.method.upper() == 'POST':
            params = self._parse_params(params)
            self._modify(params)
            self._add_notification("User modified")
            try:
                referer = cherrypy.request.headers['Referer']
            except:
                referer = '/'
            raise cherrypy.HTTPRedirect(referer)

        graph = {}
        for r in self.roles.graph:
            s = list(self.roles.graph[r]['sub_roles'])
            p = list(self.roles.graph[r]['parent_roles'])
            graph[r] = {'sub_roles': s, 'parent_roles': p}
        graph_js = json.dumps(graph, separators=(',', ':'))
        display_names = {}
        for r in self.roles.flatten:
            display_names[r] = self.roles.flatten[r]['display_name']
        user_attrs = self._get_user(user)
        if user_attrs == {}:
            cherrypy.response.status = 400
            return self.temp['error.tmpl'].render(
                is_admin=is_admin,
                alert='warning',
                message="User '" + user + "' does not exist"
                )
        tmp = self._get_roles(user)
        user_roles = tmp['roles']
        user_lonely_groups = tmp['unusedgroups']
        roles_js = json.dumps(display_names, separators=(',', ':'))
        key = self.attributes.get_key()
        form = self.temp['form.tmpl'].render(
            attributes=self.attributes.attributes,
            values=user_attrs,
            modify=True,
            keyattr=key,
            autofill=False
            )
        roles = self.temp['roles.tmpl'].render(
            roles=self.roles.flatten,
            graph=self.roles.graph,
            graph_js=graph_js,
            roles_js=roles_js,
            current_roles=user_roles,
            )
        return self.temp['modify.tmpl'].render(
            form=form,
            roles=roles,
            is_admin=is_admin,
            standalone_groups=user_lonely_groups,
            backends_display_names=self.backends_display_names,
            custom_js=self.custom_js,
            notifications=self._empty_notification(),
            )

    @cherrypy.expose
    @exception_decorator
    def selfmodify(self, **params):
        """ self modify user page """
        self._check_auth(must_admin=False)
        is_admin = self._check_admin()
        sess = cherrypy.session
        user = str(sess.get(SESSION_KEY, None))
        if self.auth_mode == 'none':
            return self.temp['error.tmpl'].render(
                is_admin=is_admin,
                alert='warning',
                message="Not accessible with authentication disabled."
                )
        if cherrypy.request.method.upper() == 'POST':
            params = self._parse_params(params)
            self._selfmodify(params)
            self._add_notification(
                "Self modification done"
            )
        user_attrs = self._get_user(user)
        if user_attrs == {}:
            return self.temp['error.tmpl'].render(
                is_admin=is_admin,
                alert='warning',
                message="User doesn't exist"
                )
        form = self.temp['form.tmpl'].render(
            attributes=self.attributes.get_selfattributes(),
            values=user_attrs,
            modify=True,
            autofill=False
            )
        return self.temp['selfmodify.tmpl'].render(
            form=form,
            is_admin=is_admin,
            notifications=self._empty_notification(),
            )
Example #10
0
 def testValidate(self):
     inv = Attributes('./tests/cfg/attributes.yml')
     attrs = {'cn': 'test', 'email': '*****@*****.**', 'uidNumber': 4242, 'shell': '/bin/bash', 'logscript': 'login1.bat'}
     for attrid in attrs:
         inv.check_attr(attrid, attrs[attrid])
Example #11
0
 def testGetSelfAttributes(self):
     inv = Attributes('./tests/cfg/attributes.yml')
     ret = inv.get_selfattributes()
     expected = {'password': {'backends': {'ad': 'unicodePwd', 'ldap': 'userPassword'}, 'display_name': 'Password', 'description': 'Password of the user', 'weight': 31, 'self': True, 'type': 'password'}, 'shell': {'backends': {'ad': 'SHELL', 'ldap': 'shell'}, 'display_name': 'Shell', 'description': 'Shell of the user', 'weight': 80, 'values': ['/bin/bash', '/bin/zsh', '/bin/sh'], 'self': True, 'type': 'stringlist'}}
     assert ret == expected
Example #12
0
 def testGetKey(self):
     inv = Attributes('./tests/cfg/attributes.yml')
     ret = inv.get_key()
     expected = 'uid'
     assert ret == expected
Example #13
0
 def testGetBackendAttributes(self):
     inv = Attributes('./tests/cfg/attributes.yml')
     ret = inv.get_backend_attributes('ldap')
     expected = ['shell', 'cn', 'userPassword', 'uidNumber', 'gidNumber', 'sn', 'home', 'givenName', 'email', 'uid']
     assert ret == expected
Example #14
0
 def testGetSearchAttributes(self):
     inv = Attributes('./tests/cfg/attributes.yml')
     ret = inv.get_search_attributes()
     expected = {'first-name': {'backends': {'ad': 'givenName', 'ldap': 'givenName'}, 'display_name': 'First Name', 'description': 'First name of the user', 'weight': 20, 'search_displayed': True, 'type': 'string'}, 'cn': {'autofill': {'function': 'cn', 'args': ['$first-name', '$name']}, 'backends': {'ad': 'cn', 'ldap': 'cn'}, 'display_name': 'Display Name', 'description': 'Firt Name and Display Name', 'weight': 30, 'search_displayed': True, 'type': 'string'}, 'name': {'backends': {'ad': 'sn', 'ldap': 'sn'}, 'display_name': 'Name', 'description': 'Family name of the user', 'weight': 10, 'search_displayed': True, 'type': 'string'}, 'uid': {'display_name': 'UID', 'description': 'UID of the user', 'weight': 50, 'autofill': {'function': 'uid', 'args': ['$first-name', '$last-name']}, 'backends': {'ad': 'UID', 'ldap': 'uid'}, 'key': True, 'search_displayed': True, 'type': 'string'}}
     assert ret == expected
Example #15
0
 def testGetSelfAttributes(self):
     inv = Attributes('./tests/cfg/attributes.yml')
     ret = inv.get_backends()
     expected = Set(['ldap', 'ad'])
     assert ret == expected
Example #16
0
from ldapcherry.attributes import Attributes
from ldapcherry.exceptions import DumplicateRoleKey, MissingKey, DumplicateRoleContent, MissingRolesFile
from ldapcherry.pyyamlwrapper import DumplicatedKey, RelationError
from yaml import load, dump
import yaml

try:
    from yaml import CLoader as Loader, CDumper as Dumper
except ImportError:
    from yaml import Loader, Dumper


class CustomDumper(yaml.SafeDumper):
    "A custom YAML dumper that never emits aliases"

    def ignore_aliases(self, _data):
        return True


try:
    inv = Attributes('./tests/cfg/attributes.yml')
    #inv = Attributes('./tests/cfg/attributes_wrong_type.yml')
except Exception as e:
    print e.log

print inv.backend_attributes
Example #17
0
class LdapCherry(object):
    def _handle_exception(self, e):
        if hasattr(e, 'log'):
            cherrypy.log.error(msg=e.log, severity=logging.ERROR)
        else:
            cherrypy.log.error(msg="uncaught exception: [%(e)s]" %
                               {'e': str(e)},
                               severity=logging.ERROR)
        # log the traceback as 'debug'
        cherrypy.log.error(msg='', severity=logging.DEBUG, traceback=True)

    def _get_param(self, section, key, config, default=None):
        """ Get configuration parameter "key" from config
        @str section: the section of the config file
        @str key: the key to get
        @dict config: the configuration (dictionnary)
        @str default: the default value if parameter "key" is not present
        @rtype: str (value of config['key'] if present default otherwith
        """
        if section in config and key in config[section]:
            return config[section][key]
        if default is not None:
            return default
        else:
            raise MissingParameter(section, key)

    def _get_groups(self, username):
        """ Get groups of a user
        @str username: name of the user
        @rtype: dict, format { '<backend>': [<list of groups>] }
        """
        ret = {}
        for b in self.backends:
            ret[b] = self.backends[b].get_groups(username)
        cherrypy.log.error(
            msg="user '" + username + "' groups: " + str(ret),
            severity=logging.DEBUG,
        )
        return ret

    def _get_roles(self, username):
        """ Get roles of a user
        @str username: name of the user
        @rtype: dict, format { 'roles': [<list of roles>],
            'unusedgroups': [<list of groups not matching roles>] }
        """
        groups = self._get_groups(username)
        user_roles = self.roles.get_roles(groups)
        cherrypy.log.error(
            msg="user '" + username + "' roles: " + str(user_roles),
            severity=logging.DEBUG,
        )
        return user_roles

    def _is_admin(self, username):
        """ Check if a user is an ldapcherry administrator
        @str username: name of the user
        @rtype: bool, True if administrator, False otherwise
        """
        roles = self._get_roles(username)
        return self.roles.is_admin(roles['roles'])

    def _check_backends(self):
        """ Check that every backend in roles and attributes
        is declared in main configuration
        """
        backends = self.backends_params.keys()
        for b in self.roles.get_backends():
            if b not in backends:
                raise MissingBackend(b, 'role')
        for b in self.attributes.get_backends():
            if b not in backends:
                raise MissingBackend(b, 'attribute')

    def _init_backends(self, config):
        """ Init all backends
        @dict: configuration of ldapcherry
        """
        self.backends_params = {}
        self.backends = {}
        self.backends_display_names = {}
        for entry in config['backends']:
            # split at the first dot
            backend, sep, param = entry.partition('.')
            value = config['backends'][entry]
            if backend not in self.backends_params:
                self.backends_params[backend] = {}
            self.backends_params[backend][param] = value
        for backend in self.backends_params:
            # get the backend display_name
            try:
                self.backends_display_names[backend] = \
                    self.backends_params[backend]['display_name']
            except Exception as e:
                self.backends_display_names[backend] = backend
                self.backends_params[backend]['display_name'] = backend
            params = self.backends_params[backend]
            # Loading the backend module
            try:
                module = params['module']
            except Exception as e:
                raise MissingParameter('backends', backend + '.module')
            try:
                bc = __import__(module, globals(), locals(), ['Backend'], 0)
            except Exception as e:
                self._handle_exception(e)
                raise BackendModuleLoadingFail(module)
            try:
                attrslist = self.attributes.get_backend_attributes(backend)
                key = self.attributes.get_backend_key(backend)
                self.backends[backend] = bc.Backend(
                    params,
                    cherrypy.log.error,
                    backend,
                    attrslist,
                    key,
                )
            except MissingParameter as e:
                raise
            except Exception as e:
                self._handle_exception(e)
                raise BackendModuleInitFail(module)

    def _init_autofill(self, config):
        self.backends_autofill = {}
        for entry in config['autofill']:
            # split at the first dot
            backend, sep, field = entry.partition('.')
            if backend not in self.backends_autofill:
                self.backends_autofill[backend] = {}
            self.backends_autofill[backend][field] = config['autofill'][entry]

    def _init_custom_js(self, config):
        self.custom_js = []
        if '/custom' not in config:
            return
        directory = self._get_param(
            '/custom',
            'tools.staticdir.dir',
            config,
        )
        for file in os.listdir(directory):
            if file.endswith(".js"):
                self.custom_js.append(file)

    def _init_ppolicy(self, config):
        module = self._get_param('ppolicy', 'ppolicy.module', config,
                                 'ldapcherry.ppolicy')
        try:
            pp = __import__(module, globals(), locals(), ['PPolicy'], 0)
        except Exception as e:
            raise BackendModuleLoadingFail(module)
        if 'ppolicy' in config:
            ppcfg = config['ppolicy']
        else:
            ppcfg = {}
        self.ppolicy = pp.PPolicy(ppcfg, cherrypy.log)

    def _init_auth(self, config):
        """ Init authentication
        @dict: configuration of ldapcherry
        """
        self.auth_mode = self._get_param('auth', 'auth.mode', config)
        if self.auth_mode in ['and', 'or', 'none']:
            pass
        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'], 0)
            self.auth = auth.Auth(config['auth'], cherrypy.log)
        else:
            raise WrongParamValue(
                'auth.mode',
                'auth',
                ['and', 'or', 'none', 'custom'],
            )

        self.roles_file = self._get_param('roles', 'roles.file', config)
        cherrypy.log.error(msg="loading roles file '%(file)s'" %
                           {'file': self.roles_file},
                           severity=logging.DEBUG)
        self.roles = Roles(self.roles_file)

    def _set_access_log(self, config, level):
        """ Configure access logs
        """
        access_handler = self._get_param(
            'global',
            'log.access_handler',
            config,
            'syslog',
        )

        # log format for syslog
        syslog_formatter = logging.Formatter(
            "ldapcherry[%(process)d]: %(message)s")

        # replace access log handler by a syslog handler
        if access_handler == 'syslog':
            cherrypy.log.access_log.handlers = []
            handler = logging.handlers.SysLogHandler(
                address='/dev/log',
                facility='user',
            )
            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

        # replace access log handler by a null handler
        elif access_handler == 'none':
            cherrypy.log.access_log.handlers = []
            handler = logging.NullHandler()
            cherrypy.log.access_log.addHandler(handler)

        # set log level
        cherrypy.log.access_log.setLevel(level)

    def _set_error_log(self, config, level, debug=False):
        """ Configure error logs
        """
        error_handler = self._get_param('global', 'log.error_handler', config,
                                        'syslog')

        # log format for syslog
        syslog_formatter = logging.Formatter(
            "ldapcherry[%(process)d]: %(message)s", )

        # replacing the error handler by a syslog handler
        if error_handler == 'syslog':
            cherrypy.log.error_log.handlers = []

            # redefining log.error method because cherrypy does weird
            # things like adding the date inside the message
            # or adding space even if context is empty
            # (by the way, what's the use of "context"?)
            cherrypy.log.error = syslog_error

            handler = logging.handlers.SysLogHandler(
                address='/dev/log',
                facility='user',
            )
            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

        # replacing the error handler by a null handler
        elif error_handler == 'none':
            cherrypy.log.error_log.handlers = []
            handler = logging.NullHandler()
            cherrypy.log.error_log.addHandler(handler)

        # set log level
        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)
            cherrypy.log.error_log.setLevel(logging.DEBUG)

    def _auth(self, user, password):
        """ authenticate a user
        @str user: login of the user
        @str password: password of the user
        @rtype: dict, {'connected': <boolean, True if connection succeded>,
            'isadmin': <True if user is ldapcherry administrator>}
        """
        if self.auth_mode == 'none':
            return {'connected': True, 'isadmin': True}
        elif self.auth_mode == 'and':
            ret1 = True
            for b in self.backends:
                ret1 = self.backends[b].auth(user, password) and ret1
        elif self.auth_mode == 'or':
            ret1 = False
            for b in self.backends:
                ret1 = self.backends[b].auth(user, password) or ret1
        elif self.auth_mode == 'custom':
            ret1 = self.auth.auth(user, password)
        else:
            raise Exception()
        if not ret1:
            return {'connected': False, 'isadmin': False}
        else:
            isadmin = self._is_admin(user)
            return {'connected': True, 'isadmin': isadmin}

    def _load_templates(self, config):
        """ load templates
        @dict: configuration of ldapcherry
        """
        # definition of the template directory
        self.template_dir = self._get_param('resources', 'templates.dir',
                                            config)
        cherrypy.log.error(msg="loading templates from dir '%(dir)s'" %
                           {'dir': self.template_dir},
                           severity=logging.DEBUG)
        # preload templates
        self.temp_lookup = lookup.TemplateLookup(
            directories=self.template_dir,
            input_encoding='utf-8',
            default_filters=['unicode', 'h'])
        # load each template
        self.temp = {}
        for t in (
                'index.tmpl',
                'error.tmpl',
                'login.tmpl',
                '404.tmpl',
                'searchadmin.tmpl',
                'searchuser.tmpl',
                'adduser.tmpl',
                'roles.tmpl',
                'groups.tmpl',
                'form.tmpl',
                'selfmodify.tmpl',
                'modify.tmpl',
                'service_unavailable.tmpl',
                'reset_password_req.tmpl',
                'reset_password_set.tmpl',
        ):
            self.temp[t] = self.temp_lookup.get_template(t)

    def reload(self, config=None, debug=False):
        """ load/reload configuration
        @dict: configuration of ldapcherry
        """
        try:
            # log configuration handling
            # get log level
            # (if not in configuration file, log level is set to debug)
            level = get_loglevel(
                self._get_param(
                    'global',
                    'log.level',
                    config,
                    'debug',
                ))
            # configure access log
            self._set_access_log(config, level)
            # configure error log
            self._set_error_log(config, level, debug)

            # load template files
            self._load_templates(config)

            # loading the auth configuration
            self._init_auth(config)

            # Loading the attributes
            self.attributes_file = \
                self._get_param('attributes', 'attributes.file', config)
            cherrypy.log.error(msg="loading attributes file '%(file)s'" %
                               {'file': self.attributes_file},
                               severity=logging.DEBUG)

            self.notifications = {}

            self.attributes = Attributes(self.attributes_file)

            cherrypy.log.error(msg="init directories backends",
                               severity=logging.DEBUG)
            self._init_backends(config)
            self._check_backends()

            # init server-side autofill
            self._init_autofill(config)

            # init password reset class
            self.resetpassword = ResetPassword(config['resetpassword'],
                                               self.backends,
                                               cherrypy.log.error)

            # loading the ppolicy
            self._init_ppolicy(config)

            # loading custom javascript
            self._init_custom_js(config)

            cherrypy.log.error(msg="application started",
                               severity=logging.INFO)

        except Exception as e:
            self._handle_exception(e)
            cherrypy.log.error(msg="application failed to start",
                               severity=logging.ERROR)
            exit(1)

    def _add_notification(self, message):
        """ add a notification in the notification queue of a user
        """
        sess = cherrypy.session
        username = sess.get(SESSION_KEY, None)
        if username not in self.notifications:
            self.notifications[username] = []
        self.notifications[username].append(message)

    def _empty_notification(self):
        """ empty and return list of message notification
        """
        sess = cherrypy.session
        username = sess.get(SESSION_KEY, None)
        if username in self.notifications:
            ret = self.notifications[username]
        else:
            ret = []
        self.notifications[username] = []
        return ret

    def _merge_user_attrs(self, attrs_backend, attrs_out, backend_name):
        """ merge attributes from one backend search to the attributes dict
        output

        """
        for attr in attrs_backend:
            if attr in self.attributes.backend_attributes[backend_name]:
                attrid = self.attributes.backend_attributes[backend_name][attr]
                if attrid not in attrs_out:
                    attrs_out[attrid] = attrs_backend[attr]

    def _search(self, searchstring):
        """ search users
        @str searchstring: search string
        @rtype: dict, {<user>: {<attr>: <value>}}
        """
        if searchstring is None:
            return {}
        ret = {}
        for b in self.backends:
            tmp = self.backends[b].search(searchstring)
            for u in tmp:
                if u not in ret:
                    ret[u] = {}
                self._merge_user_attrs(tmp[u], ret[u], b)
        return ret

    def _get_user(self, username):
        """ get user attributes
        @str username: user to get
        @rtype: dict, {<attr>: <value>}
        """
        if username is None:
            return {}
        ret = {}
        for b in self.backends:
            try:
                tmp = self.backends[b].get_user(username)
            except UserDoesntExist as e:
                self._handle_exception(e)
                tmp = {}
            self._merge_user_attrs(tmp, ret, b)

        cherrypy.log.error(msg="user '" + username + "' attributes " +
                           str(ret),
                           severity=logging.DEBUG)
        return ret

    def _parse_params(self, params):
        """ get user attributes
        @dict params: form parameters
        @rtype: dict, {<type>: {<attr>: <value>}}
        """
        ret = {'attrs': {}, 'roles': {}, 'groups': {}}
        for p in params:
            # each form attributes is prefixed with type, ex: attr.uidNumber
            # separate this prefix from the attribute name
            p_type, sep, param = p.partition('.')
            if p_type == 'attr':
                ret['attrs'][param] = params[p]
            elif p_type == 'role':
                ret['roles'][param] = params[p]
            elif p_type == 'group':
                # with groups there is a second prefix
                # corresponding to the backend
                backend, sep, value = param.partition('.')
                if backend not in ret['groups']:
                    ret['groups'][backend] = []
                ret['groups'][backend].append(value)
        return ret

    def _check_admin(self):
        """ check in the session database if current user
        is an ldapcherry administrator
        @rtype: boolean, True if administrator, False otherwise
        """
        if self.auth_mode == 'none':
            return True
        return cherrypy.session['isadmin']

    def _check_session(self):
        if self.auth_mode == 'none':
            return 'anonymous'
        return cherrypy.session.get(SESSION_KEY)

    def _check_auth(self, must_admin, redir_login=True):
        """ check if a user is autheticated and, optionnaly an administrator
        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
            this page"
        @rtype str: login of the user
        """
        if self.auth_mode == 'none':
            return 'anonymous'
        username = self._check_session()

        if cherrypy.request.query_string == '':
            qs = ''
        else:
            qs = '?' + cherrypy.request.query_string
        # Escaped version of the requested URL
        quoted_requrl = quote_plus(cherrypy.url() + qs)
        if not username:
            # return to login page (with quoted url in query string)
            if redir_login:
                raise cherrypy.HTTPRedirect(
                    "/signin?url=%(url)s" % {'url': quoted_requrl}, )
            else:
                raise cherrypy.HTTPError(
                    "403 Forbidden",
                    "You must be logged in to access this ressource.",
                )

        if 'connected' not in cherrypy.session \
                or not cherrypy.session['connected']:
            if redir_login:
                raise cherrypy.HTTPRedirect(
                    "/signin?url=%(url)s" % {'url': quoted_requrl}, )
            else:
                raise cherrypy.HTTPError(
                    "403 Forbidden",
                    "You must be logged in to access this ressource.",
                )

        if cherrypy.session['connected'] and \
                not cherrypy.session['isadmin']:
            if must_admin:
                # user is not an administrator, so he gets 403 Forbidden
                raise cherrypy.HTTPError(
                    "403 Forbidden",
                    "You are not allowed to access this resource.",
                )
            else:
                return username

        if cherrypy.session['connected'] and \
                cherrypy.session['isadmin']:
            return username
        else:
            if redir_login:
                raise cherrypy.HTTPRedirect(
                    "/signin?url=%(url)s" % {'url': quoted_requrl}, )
            else:
                raise cherrypy.HTTPError(
                    "403 Forbidden",
                    "You must be logged in to access this ressource.",
                )

    def _autofill_attrs(self, backend, attrs):
        """ Copy attributes dict and add autofill values """
        ret = attrs.copy()
        if backend in self.backends_autofill:
            for key in self.backends_autofill[backend]:
                ret[key] = self.backends_autofill[backend][key] % attrs
        return ret

    def _adduser(self, params):
        cherrypy.log.error(msg="add user form attributes: " + str(params),
                           severity=logging.DEBUG)
        badd = {}

        for attr in self.attributes.get_attributes():
            if self.attributes.attributes[attr]['type'] == 'password':
                pwd1 = attr + '1'
                pwd2 = attr + '2'
                if params['attrs'][pwd1] != params['attrs'][pwd2]:
                    raise PasswordMissMatch()
                if params['attrs'][pwd1] != '' and \
                   not self._checkppolicy(params['attrs'][pwd1])['match']:
                    raise PPolicyError()
                params['attrs'][attr] = params['attrs'][pwd1]
            if attr in params['attrs'] and self.attributes.attributes[attr][
                    'type'] != 'readonly':
                self.attributes.check_attr(attr, params['attrs'][attr])
                backends = self.attributes.get_backends_attributes(attr)
                for b in backends:
                    if b not in badd:
                        badd[b] = {}
                    badd[b][backends[b]] = params['attrs'][attr]
        added = False
        for b in badd:
            try:
                self.backends[b].add_user(self._autofill_attrs(b, badd[b]))
                added = True
            except UserAlreadyExists as e:
                self._add_notification('User already exists in backend "' + b +
                                       '"')
                return
        if not added:
            raise e

        key = self.attributes.get_key()
        username = params['attrs'][key]
        sess = cherrypy.session
        admin = sess.get(SESSION_KEY, 'unknown')

        cherrypy.log.error(msg="user '" + username + "' added by '" + admin +
                           "'",
                           severity=logging.INFO)
        cherrypy.log.error(msg="user '" + username + "' attributes: " +
                           str(badd),
                           severity=logging.DEBUG)

        roles = []
        for r in self.roles.get_allroles():
            if r in params['roles']:
                roles.append(r)
        groups = self.roles.get_groups(roles)
        for b in groups:
            self.backends[b].add_to_groups(username, set(groups[b]))

        cherrypy.log.error(msg="user '" + username + "' made member of " +
                           str(roles) + " by '" + admin + "'",
                           severity=logging.INFO)
        cherrypy.log.error(msg="user '" + username + "' groups: " +
                           str(groups),
                           severity=logging.DEBUG)

    def _modify_attrs(self, params, attr_list, username, autofill=True):
        badd = {}
        for attr in attr_list:
            if self.attributes.attributes[attr]['type'] == 'password':
                pwd1 = attr + '1'
                pwd2 = attr + '2'
                if pwd1 in params['attrs']:
                    if params['attrs'][pwd1] != params['attrs'][pwd2]:
                        raise PasswordMissMatch()
                    if params['attrs'][pwd1] != '' and \
                            not self._checkppolicy(
                                params['attrs'][pwd1]
                                )['match']:
                        raise PPolicyError()
                    params['attrs'][attr] = params['attrs'][pwd1]
            if attr in params['attrs'] and params['attrs'][attr] != '' \
               and self.attributes.attributes[attr]['type'] != 'readonly' \
               and not ('immutable' in self.attributes.attributes[attr] and self.attributes.attributes[attr]['immutable']):
                self.attributes.check_attr(attr, params['attrs'][attr])
                backends = self.attributes.get_backends_attributes(attr)
                for b in backends:
                    if b not in badd:
                        badd[b] = {}
                    badd[b][backends[b]] = params['attrs'][attr]
        for b in badd:
            try:
                if autofill:
                    badd[b] = self._autofill_attrs(b, badd[b])
                self.backends[b].set_attrs(username, badd[b])
            except UserDoesntExist as e:
                self._add_notification('User does not exist in backend "' + b +
                                       '"')

        return badd

    def _selfmodify(self, params):
        cherrypy.log.error(msg="modify user form attributes: " + str(params),
                           severity=logging.DEBUG)
        sess = cherrypy.session
        username = sess.get(SESSION_KEY, None)
        badd = self._modify_attrs(
            params,
            self.attributes.get_selfattributes(),
            username,
            autofill=False,
        )
        cherrypy.log.error(msg="user '" + username +
                           "' modified his attributes",
                           severity=logging.INFO)
        cherrypy.log.error(msg="user '" + username + "' attributes: " +
                           str(badd),
                           severity=logging.DEBUG)

    def _modify(self, params):
        cherrypy.log.error(msg="modify user form attributes: " + str(params),
                           severity=logging.DEBUG)
        key = self.attributes.get_key()
        username = params['attrs'][key]

        badd = self._modify_attrs(
            params,
            self.attributes.get_attributes(),
            username,
            autofill=True,
        )

        sess = cherrypy.session
        admin = sess.get(SESSION_KEY, 'unknown')

        cherrypy.log.error(msg="user '" + username + "' modified by '" +
                           admin + "'",
                           severity=logging.INFO)
        cherrypy.log.error(msg="user '" + username + "' attributes: " +
                           str(badd),
                           severity=logging.DEBUG)

        tmp = self._get_roles(username)
        roles_current = tmp['roles']
        lonely_groups = tmp['unusedgroups']
        roles_member = []
        roles_not_member = []

        groups_keep = {}
        groups_remove = {}

        for b in lonely_groups:
            for g in lonely_groups[b]:
                if b in params['groups'] and g in params['groups'][b]:
                    if b not in groups_keep:
                        groups_keep[b] = []
                    groups_keep[b].append(g)

                else:
                    if b not in groups_remove:
                        groups_remove[b] = []
                    groups_remove[b].append(g)

        for r in self.roles.get_allroles():
            if r in params['roles']:
                roles_member.append(r)
            else:
                roles_not_member.append(r)

        groups_current = self.roles.get_groups(roles_current)
        groups_rm = self.roles.get_groups(roles_not_member)
        groups_add = self.roles.get_groups(roles_member)

        for b in groups_add:
            for g in [groups_add, groups_keep, groups_current, lonely_groups]:
                if b not in g:
                    g[b] = []
            tmp = \
                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 + "'",
                               severity=logging.DEBUG)
            self.backends[b].add_to_groups(username, tmp)
        for b in groups_rm:
            for g in [
                    groups_remove, groups_rm, groups_add, groups_keep,
                    groups_current, lonely_groups
            ]:
                if b not in g:
                    g[b] = []
            tmp = \
                (
                    (set(groups_rm[b]) | set(groups_remove[b])) -
                    (set(groups_keep[b]) | set(groups_add[b]))
                ) & \
                (
                    set(groups_current[b]) | set(lonely_groups[b])
                )
            cherrypy.log.error(msg="user '" + username +
                               "' removed from groups: " + str(list(tmp)) +
                               " in backend '" + b + "'",
                               severity=logging.DEBUG)
            self.backends[b].del_from_groups(username, tmp)

        cherrypy.log.error(msg="user '" + username + "' made member of " +
                           str(roles_member) + " by '" + admin + "'",
                           severity=logging.INFO)

    def _deleteuser(self, username):
        sess = cherrypy.session
        admin = sess.get(SESSION_KEY, 'unknown')

        for b in self.backends:
            try:
                self.backends[b].del_user(username)
            except UserDoesntExist as e:
                cherrypy.log.error(msg="User '" + username +
                                   "' didn't exist in backend '" + b + "'",
                                   severity=logging.INFO)
            cherrypy.log.error(msg="user '" + username +
                               "' deleted from backend '" + b + "'",
                               severity=logging.DEBUG)

        cherrypy.log.error(msg="User '" + username + "' deleted by '" + admin +
                           "'",
                           severity=logging.INFO)

    def _checkppolicy(self, password):
        return self.ppolicy.check(password)

    @cherrypy.expose
    @exception_decorator
    def signin(self, url=None):
        """simple signin page
        """
        return self.temp['login.tmpl'].render(url=url, signin=True)

    @cherrypy.expose
    @exception_decorator
    def login(self, login, password, url=None):
        """login page
        """
        auth = self._auth(login, password)
        cherrypy.session['isadmin'] = auth['isadmin']
        cherrypy.session['connected'] = auth['connected']

        if auth['connected']:
            if auth['isadmin']:
                message = \
                    "login success for user '%(user)s' as administrator" % {
                        'user': login
                    }
            else:
                message = \
                    "login success for user '%(user)s' as normal user" % {
                        'user': login
                    }
            cherrypy.log.error(msg=message, severity=logging.INFO)
            cherrypy.session[SESSION_KEY] = cherrypy.request.login = login
            if url is None:
                redirect = "/"
            else:
                redirect = url
            raise cherrypy.HTTPRedirect(redirect)
        else:
            message = "login failed for user '%(user)s'" % {'user': login}
            cherrypy.log.error(msg=message, severity=logging.WARNING)
            if url is None:
                qs = ''
            else:
                qs = '?url=' + quote_plus(url)
            raise cherrypy.HTTPRedirect("/signin" + qs)

    @cherrypy.expose
    @exception_decorator
    def logout(self):
        """ logout page
        """
        sess = cherrypy.session
        username = sess.get(SESSION_KEY, None)
        sess[SESSION_KEY] = None
        if username:
            cherrypy.request.login = None

        cherrypy.log.error(msg="user '%(user)s' logout" % {'user': username},
                           severity=logging.INFO)
        raise cherrypy.HTTPRedirect("/signin")

    @cherrypy.expose
    @exception_decorator
    def reset_password(self,
                       login=None,
                       token=None,
                       password1=None,
                       password2=None):
        """ send email with a token to reset password
        """
        if not login and not token:
            """
            stage 1: show form
            """
            return self.temp['reset_password_req.tmpl'].render(signin=True,
                                                               confirm=False)
        elif login:
            """
            stage 2: check user and send token
            """
            self.resetpassword.send_token(login)
            return self.temp['reset_password_req.tmpl'].render(signin=True,
                                                               confirm=True)
        elif token and not password1:
            """
            stage 3: form to change password
            """
            return self.temp['reset_password_set.tmpl'].render(signin=True,
                                                               token=token,
                                                               changed=False)
        elif token and password1 and password2:
            """
            stage 4: change password
            """
            self.resetpassword.change_password(token, password1)
            return self.temp['reset_password_set.tmpl'].render(signin=True,
                                                               changed=True)

    @cherrypy.expose
    @exception_decorator
    def index(self):
        """main page rendering
        """
        self._check_auth(must_admin=False)
        is_admin = self._check_admin()
        sess = cherrypy.session
        user = sess.get(SESSION_KEY, None)
        if self.auth_mode == 'none':
            user_attrs = None
        else:
            user_attrs = self._get_user(user)
        attrs_list = self.attributes.get_search_attributes()
        return self.temp['index.tmpl'].render(
            is_admin=is_admin,
            attrs_list=attrs_list,
            searchresult=user_attrs,
            notifications=self._empty_notification(),
        )

    @cherrypy.expose
    @exception_decorator
    def searchuser(self, searchstring=None):
        """ search user page """
        self._check_auth(must_admin=False)
        is_admin = self._check_admin()
        if searchstring is not None and len(searchstring) > 2:
            res = self._search(searchstring)
        else:
            res = None
        attrs_list = self.attributes.get_search_attributes()
        return self.temp['searchuser.tmpl'].render(
            searchresult=res,
            attrs_list=attrs_list,
            is_admin=is_admin,
            custom_js=self.custom_js,
            notifications=self._empty_notification(),
        )

    @cherrypy.expose
    @exception_decorator
    def checkppolicy(self, **params):
        """ check if password matches policy (Ajax) """
        keys = list(params.keys())
        if len(keys) != 1:
            cherrypy.response.status = 400
            return "bad argument"
        password = params[keys[0]]
        ret = self._checkppolicy(password)
        if ret['match']:
            cherrypy.response.status = 200
            return "OK"
        else:
            cherrypy.response.status = '400 %s' % (ret['reason'])
            return ret['reason']

    @cherrypy.expose
    @exception_decorator
    def checkusername(self, **params):
        """ check if username is available (Ajax) """
        self._check_auth(must_admin=False, redir_login=False)
        keys = list(params.keys())
        if len(keys) != 1:
            cherrypy.response.status = 400
            return "bad argument"
        username = params[keys[0]]
        found = False
        for b in self.backends:
            try:
                tmp = self.backends[b].get_user(username)
            except UserDoesntExist:
                pass
            else:
                found = True
                break
        if found:
            cherrypy.response.status = "400 Username '{}' is already in use.".format(
                username)
            return "username in use"
        else:
            cherrypy.response.status = 200
            return "OK"

    @cherrypy.expose
    @exception_decorator
    def searchadmin(self, searchstring=None):
        """ search user page """
        self._check_auth(must_admin=True)
        is_admin = self._check_admin()
        """
        if searchstring is not None:
            res = self._search(searchstring)
        else:
            res = None
        """
        res = self._search(searchstring or '')
        attrs_list = self.attributes.get_search_attributes()
        return self.temp['searchadmin.tmpl'].render(
            searchstring=searchstring or '',
            searchresult=res,
            attrs_list=attrs_list,
            is_admin=is_admin,
            custom_js=self.custom_js,
            notifications=self._empty_notification(),
        )

    @cherrypy.expose
    @exception_decorator
    def adduser(self, **params):
        """ add user page """
        self._check_auth(must_admin=True)
        is_admin = self._check_admin()

        if cherrypy.request.method.upper() == 'POST':
            params = self._parse_params(params)
            self._adduser(params)
            self._add_notification("User added")

        graph = {}
        for r in self.roles.graph:
            s = list(self.roles.graph[r]['sub_roles'])
            p = list(self.roles.graph[r]['parent_roles'])
            graph[r] = {'sub_roles': s, 'parent_roles': p}
        graph_js = json.dumps(graph, separators=(',', ':'))
        display_names = {}
        for r in self.roles.flatten:
            display_names[r] = self.roles.flatten[r]['display_name']
        roles_js = json.dumps(display_names, separators=(',', ':'))
        try:
            form = self.temp['form.tmpl'].render(
                attributes=self.attributes.attributes,
                values=None,
                modify=False,
                autofill=True)
            roles = self.temp['roles.tmpl'].render(
                roles=self.roles.flatten,
                graph=self.roles.graph,
                graph_js=graph_js,
                roles_js=roles_js,
                current_roles=None,
            )
            return self.temp['adduser.tmpl'].render(
                form=form,
                roles=roles,
                is_admin=is_admin,
                custom_js=self.custom_js,
                notifications=self._empty_notification(),
            )
        except NameError:
            raise TemplateRenderError(
                exceptions.text_error_template().render())

    @cherrypy.expose
    @exception_decorator
    def delete(self, user):
        """ remove user page """
        self._check_auth(must_admin=True)
        is_admin = self._check_admin()
        try:
            referer = cherrypy.request.headers['Referer']
        except Exception as e:
            referer = '/'
        self._deleteuser(user)
        self._add_notification('User Deleted')
        raise cherrypy.HTTPRedirect(referer)

    @cherrypy.expose
    @exception_decorator
    def modify(self, user=None, **params):
        """ modify user page """
        self._check_auth(must_admin=True)
        is_admin = self._check_admin()

        if cherrypy.request.method.upper() == 'POST':
            params = self._parse_params(params)
            self._modify(params)
            self._add_notification("User modified")
            try:
                referer = cherrypy.request.headers['Referer']
            except Exception as e:
                referer = '/'
            raise cherrypy.HTTPRedirect(referer)

        graph = {}
        for r in self.roles.graph:
            s = list(self.roles.graph[r]['sub_roles'])
            p = list(self.roles.graph[r]['parent_roles'])
            graph[r] = {'sub_roles': s, 'parent_roles': p}
        graph_js = json.dumps(graph, separators=(',', ':'))
        display_names = {}
        for r in self.roles.flatten:
            display_names[r] = self.roles.flatten[r]['display_name']

        if user is None:
            cherrypy.response.status = 400
            return self.temp['error.tmpl'].render(is_admin=is_admin,
                                                  alert='warning',
                                                  message="No user requested")

        user_attrs = self._get_user(user)
        if user_attrs == {}:
            cherrypy.response.status = 400
            return self.temp['error.tmpl'].render(is_admin=is_admin,
                                                  alert='warning',
                                                  message="User '" + user +
                                                  "' does not exist")
        tmp = self._get_roles(user)
        user_roles = tmp['roles']
        standalone_groups = tmp['unusedgroups']
        roles_js = json.dumps(display_names, separators=(',', ':'))
        key = self.attributes.get_key()

        try:
            form = self.temp['form.tmpl'].render(
                attributes=self.attributes.attributes,
                values=user_attrs,
                modify=True,
                keyattr=key,
                autofill=False)

            roles = self.temp['roles.tmpl'].render(
                roles=self.roles.flatten,
                graph=self.roles.graph,
                graph_js=graph_js,
                roles_js=roles_js,
                current_roles=user_roles,
            )

            glued_template = self.temp['modify.tmpl'].render(
                form=form,
                roles=roles,
                is_admin=is_admin,
                standalone_groups=standalone_groups,
                backends_display_names=self.backends_display_names,
                custom_js=self.custom_js,
                notifications=self._empty_notification(),
            )
        except NameError:
            raise TemplateRenderError(
                exceptions.text_error_template().render())

        return glued_template

    @cherrypy.expose
    @exception_decorator
    def default(self, attr='', *args, **params):
        cherrypy.response.status = 404
        self._check_auth(must_admin=False)
        is_admin = self._check_admin()
        return self.temp['404.tmpl'].render(
            is_admin=is_admin,
            notifications=self._empty_notification(),
        )

    @cherrypy.expose
    @exception_decorator
    def selfmodify(self, **params):
        """ self modify user page """
        self._check_auth(must_admin=False)
        is_admin = self._check_admin()
        sess = cherrypy.session
        user = sess.get(SESSION_KEY, None)
        if self.auth_mode == 'none':
            return self.temp['error.tmpl'].render(
                is_admin=is_admin,
                alert='warning',
                message="Not accessible with authentication disabled.")
        if cherrypy.request.method.upper() == 'POST':
            params = self._parse_params(params)
            self._selfmodify(params)
            self._add_notification("Self modification done")
        user_attrs = self._get_user(user)

        try:
            if user_attrs == {}:
                return self.temp['error.tmpl'].render(
                    is_admin=is_admin,
                    alert='warning',
                    message="User doesn't exist")

            form = self.temp['form.tmpl'].render(
                attributes=self.attributes.get_selfattributes(),
                values=user_attrs,
                modify=True,
                autofill=False)
            return self.temp['selfmodify.tmpl'].render(
                form=form,
                is_admin=is_admin,
                notifications=self._empty_notification(),
            )
        except NameError:
            raise TemplateRenderError(
                exceptions.text_error_template().render())
Example #18
0
 def testNominal(self):
     inv = Attributes('./tests/cfg/attributes.yml')
     inv.get_attributes()
     return True