Some refactoring (again) in authenticators and exceptions

* Move authenticators classes into distinct modules
* Standardize MoulinetteError which is now a child class of IOError
* Add methods from helpers.py to LDAP authenticator class
* Review authenticator and action configuration storage
* Small changes in error printing on MoulinetteError raising
This commit is contained in:
Jerome Lebleu 2014-03-25 00:51:39 +01:00
parent c95469073a
commit ecd88ce853
9 changed files with 477 additions and 318 deletions

View file

@ -35,11 +35,8 @@ if __name__ == '__main__':
raise YunoHostError(17, _("YunoHost is not correctly installed, please execute 'yunohost tools postinstall'")) raise YunoHostError(17, _("YunoHost is not correctly installed, please execute 'yunohost tools postinstall'"))
# Execute the action # Execute the action
cli(['yunohost', 'test'], args, use_cache) ret = cli(['yunohost', 'test'], args, use_cache)
except MoulinetteError as e:
print(e.colorize())
sys.exit(e.code)
except YunoHostError as e: except YunoHostError as e:
print(colorize(_("Error: "), 'red') + e.message) print(colorize(_("Error: "), 'red') + e.message)
sys.exit(e.code) sys.exit(e.code)
sys.exit(0) sys.exit(ret)

View file

@ -9,7 +9,7 @@ basedir = os.path.abspath(os.path.dirname(__file__) +'/../')
if os.path.isdir(basedir +'/src'): if os.path.isdir(basedir +'/src'):
sys.path.append(basedir +'/src') sys.path.append(basedir +'/src')
from moulinette import init, api from moulinette import init, api, MoulinetteError
## Callbacks for additional routes ## Callbacks for additional routes
@ -40,6 +40,12 @@ if __name__ == '__main__':
sys.argv.remove('--debug') sys.argv.remove('--debug')
# TODO: Add log argument # TODO: Add log argument
# Rune the server try:
api(['yunohost', 'test'], 6787, {('GET', '/installed'): is_installed}, use_cache) # Run the server
api(['yunohost', 'test'], 6787,
{('GET', '/installed'): is_installed}, use_cache)
except MoulinetteError as e:
from moulinette.interface.cli import colorize
print(_('%s: %s' % (colorize(_('Error'), 'red'), e.strerror)))
sys.exit(e.code)
sys.exit(0) sys.exit(0)

View file

@ -29,7 +29,7 @@ __all__ = [
'MoulinetteError', 'MoulinetteError',
] ]
from .core import MoulinetteError from moulinette.core import MoulinetteError
## Package functions ## Package functions
@ -50,7 +50,7 @@ def init(**kwargs):
""" """
import sys import sys
import __builtin__ import __builtin__
from .core import Package, install_i18n from moulinette.core import Package, install_i18n
__builtin__.__dict__['pkg'] = Package(**kwargs) __builtin__.__dict__['pkg'] = Package(**kwargs)
# Initialize internationalization # Initialize internationalization
@ -76,8 +76,8 @@ def api(namespaces, port, routes={}, use_cache=True):
instead of using the cached one instead of using the cached one
""" """
from .actionsmap import ActionsMap from moulinette.actionsmap import ActionsMap
from .interface.api import MoulinetteAPI from moulinette.interface.api import MoulinetteAPI
amap = ActionsMap('api', namespaces, use_cache) amap = ActionsMap('api', namespaces, use_cache)
moulinette = MoulinetteAPI(amap, routes) moulinette = MoulinetteAPI(amap, routes)
@ -97,10 +97,15 @@ def cli(namespaces, args, use_cache=True):
instead of using the cached one instead of using the cached one
""" """
from .actionsmap import ActionsMap from moulinette.actionsmap import ActionsMap
from .interface.cli import MoulinetteCLI from moulinette.interface.cli import MoulinetteCLI, colorize
try:
amap = ActionsMap('cli', namespaces, use_cache) amap = ActionsMap('cli', namespaces, use_cache)
moulinette = MoulinetteCLI(amap) moulinette = MoulinetteCLI(amap)
moulinette.run(args) moulinette.run(args)
except MoulinetteError as e:
print(_('%s: %s' % (colorize(_('Error'), 'red'), e.strerror)))
return e.errno
return 0

View file

@ -1,16 +1,17 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import argparse
import yaml
import re
import os import os
import re
import errno
import yaml
import argparse
import cPickle as pickle import cPickle as pickle
from collections import OrderedDict from collections import OrderedDict
import logging import logging
from . import __version__ from moulinette.core import (MoulinetteError, MoulinetteLock,
from .core import MoulinetteError, MoulinetteLock, init_authenticator init_authenticator)
## Actions map Signals ------------------------------------------------- ## Actions map Signals -------------------------------------------------
@ -48,7 +49,7 @@ class _AMapSignals(object):
"""The list of available signals""" """The list of available signals"""
signals = { 'authenticate', 'prompt' } signals = { 'authenticate', 'prompt' }
def authenticate(self, authenticator, name, help, vendor=None): def authenticate(self, authenticator, help):
"""Process the authentication """Process the authentication
Attempt to authenticate to the given authenticator and return Attempt to authenticate to the given authenticator and return
@ -57,10 +58,8 @@ class _AMapSignals(object):
action). action).
Keyword arguments: Keyword arguments:
- authenticator -- The authenticator to use - authenticator -- The authenticator object to use
- name -- The authenticator name in the actions map
- help -- A help message for the authenticator - help -- A help message for the authenticator
- vendor -- Not expected (TODO: Remove it)
Returns: Returns:
The authenticator object The authenticator object
@ -68,7 +67,7 @@ class _AMapSignals(object):
""" """
if authenticator.is_authenticated: if authenticator.is_authenticated:
return authenticator return authenticator
return self._authenticate(authenticator, name, help) return self._authenticate(authenticator, help)
def prompt(self, message, is_password=False, confirm=False): def prompt(self, message, is_password=False, confirm=False):
"""Prompt for a value """Prompt for a value
@ -172,18 +171,14 @@ class _AMapParser(object):
raise NotImplementedError("derived class '%s' must override this method" % \ raise NotImplementedError("derived class '%s' must override this method" % \
self.__class__.__name__) self.__class__.__name__)
def add_action_parser(self, name, tid, conf=None, **kwargs): def add_action_parser(self, name, tid, **kwargs):
"""Add a parser for an action """Add a parser for an action
Create a new action and return an argument parser for it. It Create a new action and return an argument parser for it.
should set the configuration 'conf' for the action which can be
identified by the tuple identifier 'tid' - it is usually in the
form of (namespace, category, action).
Keyword arguments: Keyword arguments:
- name -- The action name - name -- The action name
- tid -- The tuple identifier of the action - tid -- The tuple identifier of the action
- conf -- A dict of configuration for the action
Returns: Returns:
An ArgumentParser based object An ArgumentParser based object
@ -288,6 +283,7 @@ class _AMapParser(object):
- configuration -- The configuration to pre-format - configuration -- The configuration to pre-format
""" """
# TODO: Create a class with a validator method for each configuration
conf = {} conf = {}
# -- 'authenficate' # -- 'authenficate'
@ -305,7 +301,7 @@ class _AMapParser(object):
conf['authenticate'] = True if self.name in ifaces else False conf['authenticate'] = True if self.name in ifaces else False
else: else:
# TODO: Log error instead and tell valid values # TODO: Log error instead and tell valid values
raise MoulinetteError(22, "Invalid value '%r' for configuration 'authenticate'" % ifaces) raise MoulinetteError(errno.EINVAL, "Invalid value '%r' for configuration 'authenticate'" % ifaces)
# -- 'authenticator' # -- 'authenticator'
try: try:
@ -315,26 +311,31 @@ class _AMapParser(object):
else: else:
if not is_global and isinstance(auth, str): if not is_global and isinstance(auth, str):
try: try:
# Store parameters of the required authenticator # Store needed authenticator profile
conf['authenticator'] = self.global_conf['authenticator'][auth] conf['authenticator'] = self.global_conf['authenticator'][auth]
except KeyError: except KeyError:
raise MoulinetteError(22, "Authenticator '%s' is not defined in global configuration" % auth) raise MoulinetteError(errno.EINVAL, "Undefined authenticator '%s' in global configuration" % auth)
elif is_global and isinstance(auth, dict): elif is_global and isinstance(auth, dict):
if len(auth) == 0: if len(auth) == 0:
logging.warning('no authenticator defined in global configuration') logging.warning('no authenticator defined in global configuration')
else: else:
auths = {} auths = {}
for auth_name, auth_conf in auth.items(): for auth_name, auth_conf in auth.items():
# Add authenticator name # Add authenticator profile as a 3-tuple
auths[auth_name] = ({ 'name': auth_name, # (identifier, configuration, parameters) with
'vendor': auth_conf.get('vendor'), # - identifier: the authenticator vendor and its
'help': auth_conf.get('help', None) # profile name as a 2-tuple
}, # - configuration: a dict of additional global
# configuration (i.e. 'help')
# - parameters: a dict of arguments for the
# authenticator profile
auths[auth_name] = ((auth_conf.get('vendor'), auth_name),
{ 'help': auth_conf.get('help', None) },
auth_conf.get('parameters', {})) auth_conf.get('parameters', {}))
conf['authenticator'] = auths conf['authenticator'] = auths
else: else:
# TODO: Log error instead and tell valid values # TODO: Log error instead and tell valid values
raise MoulinetteError(22, "Invalid value '%r' for configuration 'authenticator'" % auth) raise MoulinetteError(errno.EINVAL, "Invalid value '%r' for configuration 'authenticator'" % auth)
# -- 'argument_auth' # -- 'argument_auth'
try: try:
@ -346,7 +347,7 @@ class _AMapParser(object):
conf['argument_auth'] = arg_auth conf['argument_auth'] = arg_auth
else: else:
# TODO: Log error instead and tell valid values # TODO: Log error instead and tell valid values
raise MoulinetteError(22, "Invalid value '%r' for configuration 'argument_auth'" % arg_auth) raise MoulinetteError(errno.EINVAL, "Invalid value '%r' for configuration 'argument_auth'" % arg_auth)
return conf return conf
@ -362,14 +363,12 @@ class _AMapParser(object):
""" """
if name == 'authenticator' and value: if name == 'authenticator' and value:
auth_conf, auth_params = value (identifier, configuration, parameters) = value
# Return authenticator configuration and an instanciator for # Return global configuration and an authenticator
# it as a 2-tuple # instanciator as a 2-tuple
return (auth_conf, return (configuration,
lambda: init_authenticator(auth_conf['name'], lambda: init_authenticator(identifier, parameters))
auth_conf['vendor'],
**auth_params))
return value return value
@ -415,7 +414,7 @@ class CLIAMapParser(_AMapParser):
parser = self._subparsers.add_parser(name, help=category_help) parser = self._subparsers.add_parser(name, help=category_help)
return self.__class__(self, parser) return self.__class__(self, parser)
def add_action_parser(self, name, tid, conf=None, action_help=None, **kwargs): def add_action_parser(self, name, tid, action_help=None, **kwargs):
"""Add a parser for an action """Add a parser for an action
Keyword arguments: Keyword arguments:
@ -425,8 +424,6 @@ class CLIAMapParser(_AMapParser):
A new argparse.ArgumentParser object for the action A new argparse.ArgumentParser object for the action
""" """
if conf:
self.set_conf(tid, conf)
return self._subparsers.add_parser(name, help=action_help) return self._subparsers.add_parser(name, help=action_help)
def parse_args(self, args, **kwargs): def parse_args(self, args, **kwargs):
@ -440,7 +437,7 @@ class CLIAMapParser(_AMapParser):
auth = shandler.authenticate(klass(), **auth_conf) auth = shandler.authenticate(klass(), **auth_conf)
if not auth.is_authenticated: if not auth.is_authenticated:
# TODO: Set proper error code # TODO: Set proper error code
raise MoulinetteError(1, _("This action need authentication")) raise MoulinetteError(errno.EACCES, _("This action need authentication"))
if self.get_conf(ret._tid, 'argument_auth') and \ if self.get_conf(ret._tid, 'argument_auth') and \
self.get_conf(ret._tid, 'authenticate') == 'all': self.get_conf(ret._tid, 'authenticate') == 'all':
ret.auth = auth ret.auth = auth
@ -521,7 +518,6 @@ class APIAMapParser(_AMapParser):
"""Actions map's API Parser """Actions map's API Parser
""" """
def __init__(self): def __init__(self):
super(APIAMapParser, self).__init__() super(APIAMapParser, self).__init__()
@ -556,7 +552,7 @@ class APIAMapParser(_AMapParser):
def add_category_parser(self, name, **kwargs): def add_category_parser(self, name, **kwargs):
return self return self
def add_action_parser(self, name, tid, conf=None, api=None, **kwargs): def add_action_parser(self, name, tid, api=None, **kwargs):
"""Add a parser for an action """Add a parser for an action
Keyword arguments: Keyword arguments:
@ -582,9 +578,7 @@ class APIAMapParser(_AMapParser):
# Create and append parser # Create and append parser
parser = _HTTPArgumentParser() parser = _HTTPArgumentParser()
self._parsers[key] = parser self._parsers[key] = (tid, parser)
if conf:
self.set_conf(key, conf)
# Return the created parser # Return the created parser
return parser return parser
@ -596,25 +590,27 @@ class APIAMapParser(_AMapParser):
- route -- The action route as a 2-tuple (method, path) - route -- The action route as a 2-tuple (method, path)
""" """
# Retrieve the parser for the route try:
if route not in self.routes: # Retrieve the tid and the parser for the route
raise MoulinetteError(22, "No parser for '%s %s' found" % key) tid, parser = self._parsers[route]
except KeyError:
raise MoulinetteError(errno.EINVAL, "No parser found for route '%s'" % route)
ret = argparse.Namespace() ret = argparse.Namespace()
# Perform authentication if needed # Perform authentication if needed
if self.get_conf(route, 'authenticate'): if self.get_conf(tid, 'authenticate'):
auth_conf, klass = self.get_conf(route, 'authenticator') auth_conf, klass = self.get_conf(tid, 'authenticator')
# TODO: Catch errors # TODO: Catch errors
auth = shandler.authenticate(klass(), **auth_conf) auth = shandler.authenticate(klass(), **auth_conf)
if not auth.is_authenticated: if not auth.is_authenticated:
# TODO: Set proper error code # TODO: Set proper error code
raise MoulinetteError(1, _("This action need authentication")) raise MoulinetteError(errno.EACCES, _("This action need authentication"))
if self.get_conf(route, 'argument_auth') and \ if self.get_conf(tid, 'argument_auth') and \
self.get_conf(route, 'authenticate') == 'all': self.get_conf(tid, 'authenticate') == 'all':
ret.auth = auth ret.auth = auth
return self._parsers[route].parse_args(args, ret) return parser.parse_args(args, ret)
""" """
The dict of interfaces names and their associated parser class. The dict of interfaces names and their associated parser class.
@ -758,7 +754,7 @@ class PatternParameter(_ExtraParameter):
pattern, message = (arguments[0], arguments[1]) pattern, message = (arguments[0], arguments[1])
if not re.match(pattern, arg_value or ''): if not re.match(pattern, arg_value or ''):
raise MoulinetteError(22, message) raise MoulinetteError(errno.EINVAL, message)
return arg_value return arg_value
@staticmethod @staticmethod
@ -776,7 +772,7 @@ The list of available extra parameters classes. It will keep to this list
order on argument parsing. order on argument parsing.
""" """
extraparameters_list = {AskParameter, PasswordParameter, PatternParameter} extraparameters_list = [AskParameter, PasswordParameter, PatternParameter]
# Extra parameters argument Parser # Extra parameters argument Parser
@ -880,7 +876,7 @@ class ActionsMap(object):
# Retrieve the interface parser # Retrieve the interface parser
self._parser_class = actionsmap_parsers[interface] self._parser_class = actionsmap_parsers[interface]
except KeyError: except KeyError:
raise MoulinetteError(22, _("Invalid interface '%s'" % interface)) raise MoulinetteError(errno.EINVAL, _("Unknown interface '%s'" % interface))
logging.debug("initializing ActionsMap for the '%s' interface" % interface) logging.debug("initializing ActionsMap for the '%s' interface" % interface)
@ -931,7 +927,7 @@ class ActionsMap(object):
try: try:
auth = self.parser.get_global_conf('authenticator', profile)[1] auth = self.parser.get_global_conf('authenticator', profile)[1]
except KeyError: except KeyError:
raise MoulinetteError(167, _("Unknown authenticator profile '%s'") % profile) raise MoulinetteError(errno.EINVAL, _("Unknown authenticator profile '%s'") % profile)
else: else:
return auth() return auth()
@ -977,7 +973,7 @@ class ActionsMap(object):
fromlist=[func_name]) fromlist=[func_name])
func = getattr(mod, func_name) func = getattr(mod, func_name)
except (AttributeError, ImportError): except (AttributeError, ImportError):
raise MoulinetteError(168, _('Function is not defined')) raise MoulinetteError(errno.ENOSYS, _('Function is not defined'))
else: else:
# Process the action # Process the action
return func(**arguments) return func(**arguments)
@ -1056,11 +1052,13 @@ class ActionsMap(object):
for argn, argp in arguments.items(): for argn, argp in arguments.items():
names = top_parser.format_arg_names(argn, names = top_parser.format_arg_names(argn,
argp.pop('full', None)) argp.pop('full', None))
extra = argp.pop('extra', None) try:
extra = argp.pop('extra')
arg = parser.add_argument(*names, **argp) arg_dest = (parser.add_argument(*names, **argp)).dest
if extra: extras[arg_dest] = _get_extra(arg_dest, extra)
extras[arg.dest] = _get_extra(arg.dest, extra) except KeyError:
# No extra parameters
parser.add_argument(*names, **argp)
if extras: if extras:
parser.set_defaults(_extra=extras) parser.set_defaults(_extra=extras)
@ -1095,6 +1093,7 @@ class ActionsMap(object):
actions = cp.pop('actions') actions = cp.pop('actions')
except KeyError: except KeyError:
# Invalid category without actions # Invalid category without actions
logging.warning("no actions found in category '%s'" % cn)
continue continue
# Get category parser # Get category parser
@ -1102,13 +1101,18 @@ class ActionsMap(object):
# -- Parse actions # -- Parse actions
for an, ap in actions.items(): for an, ap in actions.items():
conf = ap.pop('configuration', None)
args = ap.pop('arguments', {}) args = ap.pop('arguments', {})
tid = (n, cn, an) tid = (n, cn, an)
try:
conf = ap.pop('configuration')
_set_conf = lambda p: p.set_conf(tid, conf)
except KeyError:
# No action configuration
_set_conf = lambda p: False
try: try:
# Get action parser # Get action parser
parser = cat_parser.add_action_parser(an, tid, conf, **ap) parser = cat_parser.add_action_parser(an, tid, **ap)
except AttributeError: except AttributeError:
# No parser for the action # No parser for the action
continue continue
@ -1119,5 +1123,6 @@ class ActionsMap(object):
# Store action identifier and add arguments # Store action identifier and add arguments
parser.set_defaults(_tid=tid) parser.set_defaults(_tid=tid)
_add_arguments(parser, args) _add_arguments(parser, args)
_set_conf(cat_parser)
return top_parser return top_parser

View file

@ -0,0 +1,146 @@
# -*- coding: utf-8 -*-
import errno
import gnupg
import logging
from moulinette.core import MoulinetteError
# Base Class -----------------------------------------------------------
class BaseAuthenticator(object):
"""Authenticator base representation
Each authenticators must implement an Authenticator class derived
from this class. It implements base methods to authenticate with a
password or a session token.
Authenticators configurations are identified by a profile name which
must be given on instantiation - with the corresponding vendor
configuration of the authenticator.
Keyword arguments:
- name -- The authenticator profile name
"""
def __init__(self, name):
self._name = name
@property
def name(self):
"""Return the name of the authenticator instance"""
return self._name
## Virtual properties
# Each authenticator classes must implement these properties.
"""The vendor name of the authenticator"""
vendor = None
@property
def is_authenticated(self):
"""Either the instance is authenticated or not"""
raise NotImplementedError("derived class '%s' must override this property" % \
self.__class__.__name__)
## Virtual methods
# Each authenticator classes must implement these methods.
def authenticate(password=None):
"""Attempt to authenticate
Attempt to authenticate with given password. It should raise an
AuthenticationError exception if authentication fails.
Keyword arguments:
- password -- A clear text password
"""
raise NotImplementedError("derived class '%s' must override this method" % \
self.__class__.__name__)
## Authentication methods
def __call__(self, password=None, token=None):
"""Attempt to authenticate
Attempt to authenticate either with password or with session
token if 'password' is None. If the authentication succeed, the
instance is returned and the session is registered for the token
if 'token' and 'password' are given.
The token is composed by the session identifier and a session
hash - to use for encryption - as a 2-tuple.
Keyword arguments:
- password -- A clear text password
- token -- The session token in the form of (id, hash)
Returns:
The authenticated instance
"""
if self.is_authenticated:
return self
store_session = True if password and token else False
if token:
try:
# Extract id and hash from token
s_id, s_hash = token
except TypeError:
if not password:
raise MoulinetteError(errno.EINVAL, _("Invalid format for token"))
else:
# TODO: Log error
store_session = False
else:
if password is None:
# Retrieve session
password = self._retrieve_session(s_id, s_hash)
try:
# Attempt to authenticate
self.authenticate(password)
except MoulinetteError:
raise
except Exception as e:
logging.error("authentication (name: '%s', type: '%s') fails: %s" % \
(self.name, self.vendor, e))
raise MoulinetteError(errno.EACCES, _("Unable to authenticate"))
# Store session
if store_session:
self._store_session(s_id, s_hash, password)
return self
## Private methods
def _open_sessionfile(self, session_id, mode='r'):
"""Open a session file for this instance in given mode"""
return pkg.open_cachefile('%s.asc' % session_id, mode,
subdir='session/%s' % self.name)
def _store_session(self, session_id, session_hash, password):
"""Store a session and its associated password"""
gpg = gnupg.GPG()
gpg.encoding = 'utf-8'
with self._open_sessionfile(session_id, 'w') as f:
f.write(str(gpg.encrypt(password, None, symmetric=True,
passphrase=session_hash)))
def _retrieve_session(self, session_id, session_hash):
"""Retrieve a session and return its associated password"""
try:
with self._open_sessionfile(session_id, 'r') as f:
enc_pwd = f.read()
except IOError:
# TODO: Set proper error code
raise MoulinetteError(167, _("Unable to retrieve session"))
else:
gpg = gnupg.GPG()
gpg.encoding = 'utf-8'
return str(gpg.decrypt(enc_pwd, passphrase=session_hash))

View file

@ -0,0 +1,192 @@
# -*- coding: utf-8 -*-
# TODO: Use Python3 to remove this fix!
from __future__ import absolute_import
import ldap
import ldap.modlist as modlist
from moulinette.core import MoulinetteError
from moulinette.authenticators import BaseAuthenticator
# LDAP Class Implementation --------------------------------------------
class Authenticator(BaseAuthenticator):
"""LDAP Authenticator
Initialize a LDAP connexion for the given arguments. It attempts to
authenticate a user if 'user_rdn' is given - by associating user_rdn
and base_dn - and provides extra methods to manage opened connexion.
Keyword arguments:
- uri -- The LDAP server URI
- base_dn -- The base dn
- user_rdn -- The user rdn to authenticate
"""
def __init__(self, name, uri, base_dn, user_rdn=None):
super(Authenticator, self).__init__(name)
self.uri = uri
self.basedn = base_dn
if user_rdn:
self.userdn = '%s,%s' % (user_rdn, base_dn)
self.con = None
else:
# Initialize anonymous usage
self.userdn = ''
self.authenticate(None)
## Implement virtual properties
vendor = 'ldap'
@property
def is_authenticated(self):
try:
# Retrieve identity
who = self.con.whoami_s()
except:
return False
else:
if who[3:] == self.userdn:
return True
return False
## Implement virtual methods
def authenticate(self, password):
try:
con = ldap.initialize(self.uri)
if self.userdn:
con.simple_bind_s(self.userdn, password)
else:
con.simple_bind_s()
except ldap.INVALID_CREDENTIALS:
raise MoulinetteError(errno.EACCES, _("Invalid password"))
else:
self.con = con
## Additional LDAP methods
# TODO: Review these methods
def search(self, base=None, filter='(objectClass=*)', attrs=['dn']):
"""
Search in LDAP base
Keyword arguments:
base -- Base to search into
filter -- LDAP filter
attrs -- Array of attributes to fetch
Returns:
Boolean | Dict
"""
if not base:
base = self.basedn
try:
result = self.con.search_s(base, ldap.SCOPE_SUBTREE, filter, attrs)
except:
raise MoulinetteError(169, _('An error occured during LDAP search'))
if result:
result_list = []
for dn, entry in result:
if attrs != None:
if 'dn' in attrs:
entry['dn'] = [dn]
result_list.append(entry)
return result_list
else:
return False
def add(self, rdn, attr_dict):
"""
Add LDAP entry
Keyword arguments:
rdn -- DN without domain
attr_dict -- Dictionnary of attributes/values to add
Returns:
Boolean | MoulinetteError
"""
dn = rdn + ',' + self.basedn
ldif = modlist.addModlist(attr_dict)
try:
self.con.add_s(dn, ldif)
except:
raise MoulinetteError(169, _('An error occured during LDAP entry creation'))
else:
return True
def remove(self, rdn):
"""
Remove LDAP entry
Keyword arguments:
rdn -- DN without domain
Returns:
Boolean | MoulinetteError
"""
dn = rdn + ',' + self.basedn
try:
self.con.delete_s(dn)
except:
raise MoulinetteError(169, _('An error occured during LDAP entry deletion'))
else:
return True
def update(self, rdn, attr_dict, new_rdn=False):
"""
Modify LDAP entry
Keyword arguments:
rdn -- DN without domain
attr_dict -- Dictionnary of attributes/values to add
new_rdn -- New RDN for modification
Returns:
Boolean | MoulinetteError
"""
dn = rdn + ',' + self.basedn
actual_entry = self.search(base=dn, attrs=None)
ldif = modlist.modifyModlist(actual_entry[0], attr_dict, ignore_oldexistent=1)
try:
if new_rdn:
self.con.rename_s(dn, new_rdn)
dn = new_rdn + ',' + self.basedn
self.con.modify_ext_s(dn, ldif)
except:
raise MoulinetteError(169, _('An error occured during LDAP entry update'))
else:
return True
def validate_uniqueness(self, value_dict):
"""
Check uniqueness of values
Keyword arguments:
value_dict -- Dictionnary of attributes/values to check
Returns:
Boolean | MoulinetteError
"""
for attr, value in value_dict.items():
if not self.search(filter=attr + '=' + value):
continue
else:
raise MoulinetteError(17, _('Attribute already exists') + ' "' + attr + '=' + value + '"')
return True

View file

@ -3,10 +3,11 @@
import os import os
import sys import sys
import time import time
import errno
import gettext import gettext
import logging import logging
from .helpers import colorize from importlib import import_module
# Package manipulation ------------------------------------------------- # Package manipulation -------------------------------------------------
@ -132,189 +133,25 @@ class Package(object):
# Authenticators ------------------------------------------------------- # Authenticators -------------------------------------------------------
import ldap def init_authenticator((vendor, name), kwargs={}):
import gnupg """Return a new authenticator instance
class AuthenticationError(Exception): Retrieve the given authenticator vendor and return a new instance of
pass its Authenticator class for the given profile.
class _BaseAuthenticator(object):
def __init__(self, name):
self._name = name
@property
def name(self):
"""Return the name of the authenticator instance"""
return self._name
## Virtual properties
# Each authenticator classes must implement these properties.
"""The vendor of the authenticator"""
vendor = None
@property
def is_authenticated(self):
"""Either the instance is authenticated or not"""
raise NotImplementedError("derived class '%s' must override this property" % \
self.__class__.__name__)
## Virtual methods
# Each authenticator classes must implement these methods.
def authenticate(password=None):
"""Attempt to authenticate
Attempt to authenticate with given password. It should raise an
AuthenticationError exception if authentication fails.
Keyword arguments: Keyword arguments:
- password -- A clear text password - vendor -- The authenticator vendor name
- name -- The authenticator profile name
- kwargs -- A dict of arguments for the authenticator profile
""" """
raise NotImplementedError("derived class '%s' must override this method" % \
self.__class__.__name__)
## Authentication methods
def __call__(self, password=None, token=None):
"""Attempt to authenticate
Attempt to authenticate either with password or with session
token if 'password' is None. If the authentication succeed, the
instance is returned and the session is registered for the token
if 'token' and 'password' are given.
The token is composed by the session identifier and a session
hash - to use for encryption - as a 2-tuple.
Keyword arguments:
- password -- A clear text password
- token -- The session token in the form of (id, hash)
Returns:
The authenticated instance
"""
if self.is_authenticated:
return self
store_session = True if password and token else False
if token:
try: try:
# Extract id and hash from token mod = import_module('moulinette.authenticators.%s' % vendor)
s_id, s_hash = token except ImportError:
except TypeError: # TODO: List available authenticator vendors
if not password: raise MoulinetteError(errno.EINVAL, _("Unknown authenticator vendor '%s'" % vendor))
raise MoulinetteError(22, _("Invalid format for token"))
else: else:
# TODO: Log error return mod.Authenticator(name, **kwargs)
store_session = False
else:
if password is None:
# Retrieve session
password = self._retrieve_session(s_id, s_hash)
try:
# Attempt to authenticate
self.authenticate(password)
except AuthenticationError as e:
raise MoulinetteError(13, str(e))
except Exception as e:
logging.error("authentication (name: '%s', type: '%s') fails: %s" % \
(self.name, self.vendor, e))
raise MoulinetteError(13, _("Unable to authenticate"))
# Store session
if store_session:
self._store_session(s_id, s_hash, password)
return self
## Private methods
def _open_sessionfile(self, session_id, mode='r'):
"""Open a session file for this instance in given mode"""
return pkg.open_cachefile('%s.asc' % session_id, mode,
subdir='session/%s' % self.name)
def _store_session(self, session_id, session_hash, password):
"""Store a session and its associated password"""
gpg = gnupg.GPG()
gpg.encoding = 'utf-8'
with self._open_sessionfile(session_id, 'w') as f:
f.write(str(gpg.encrypt(password, None, symmetric=True,
passphrase=session_hash)))
def _retrieve_session(self, session_id, session_hash):
"""Retrieve a session and return its associated password"""
try:
with self._open_sessionfile(session_id, 'r') as f:
enc_pwd = f.read()
except IOError:
# TODO: Set proper error code
raise MoulinetteError(167, _("Unable to retrieve session"))
else:
gpg = gnupg.GPG()
gpg.encoding = 'utf-8'
return str(gpg.decrypt(enc_pwd, passphrase=session_hash))
class LDAPAuthenticator(_BaseAuthenticator):
def __init__(self, uri, base_dn, user_rdn=None, **kwargs):
super(LDAPAuthenticator, self).__init__(**kwargs)
self.uri = uri
self.basedn = base_dn
if user_rdn:
self.userdn = '%s,%s' % (user_rdn, base_dn)
self.con = None
else:
# Initialize anonymous usage
self.userdn = ''
self.authenticate(None)
## Implement virtual properties
vendor = 'ldap'
@property
def is_authenticated(self):
try:
# Retrieve identity
who = self.con.whoami_s()
except:
return False
else:
if who[3:] == self.userdn:
return True
return False
## Implement virtual methods
def authenticate(self, password):
try:
con = ldap.initialize(self.uri)
if self.userdn:
con.simple_bind_s(self.userdn, password)
else:
con.simple_bind_s()
except ldap.INVALID_CREDENTIALS:
raise AuthenticationError(_("Invalid password"))
else:
self.con = con
def init_authenticator(_name, _vendor, **kwargs):
if _vendor == 'ldap':
return LDAPAuthenticator(name=_name, **kwargs)
def clean_session(session_id, profiles=[]): def clean_session(session_id, profiles=[]):
sessiondir = pkg.get_cachedir('session') sessiondir = pkg.get_cachedir('session')
@ -330,44 +167,9 @@ def clean_session(session_id, profiles=[]):
# Moulinette core classes ---------------------------------------------- # Moulinette core classes ----------------------------------------------
class MoulinetteError(Exception): class MoulinetteError(OSError):
"""Moulinette base exception """Moulinette base exception"""
pass
Keyword arguments:
- code -- Integer error code
- message -- Error message to display
"""
def __init__(self, code, message):
self.code = code
self.message = message
errorcode_desc = {
1 : _('Fail'),
13 : _('Permission denied'),
17 : _('Already exists'),
22 : _('Invalid arguments'),
87 : _('Too many users'),
111 : _('Connection refused'),
122 : _('Quota exceeded'),
125 : _('Operation canceled'),
167 : _('Not found'),
168 : _('Undefined'),
169 : _('LDAP operation error')
}
if code in errorcode_desc:
self.desc = errorcode_desc[code]
else:
self.desc = _('Error %s' % code)
def __str__(self, colorized=False):
desc = self.desc
if colorized:
desc = colorize(self.desc, 'red')
return _('%s: %s' % (desc, self.message))
def colorize(self):
return self.__str__(colorized=True)
class MoulinetteLock(object): class MoulinetteLock(object):
@ -409,7 +211,7 @@ class MoulinetteLock(object):
break break
if (time.time() - start_time) > self.timeout: if (time.time() - start_time) > self.timeout:
raise MoulinetteError(1, _("An instance is already running for '%s'") \ raise MoulinetteError(errno.EBUSY, _("An instance is already running for '%s'") \
% self.namespace) % self.namespace)
# Wait before checking again # Wait before checking again
time.sleep(self.interval) time.sleep(self.interval)

View file

@ -1,10 +1,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import errno
from bottle import run, request, response, Bottle, HTTPResponse from bottle import run, request, response, Bottle, HTTPResponse
from json import dumps as json_encode from json import dumps as json_encode
from ..core import MoulinetteError, clean_session from moulinette.core import MoulinetteError, clean_session
from ..helpers import YunoHostError, YunoHostLDAP from moulinette.helpers import YunoHostError, YunoHostLDAP
# API helpers ---------------------------------------------------------- # API helpers ----------------------------------------------------------
@ -159,10 +160,9 @@ class _ActionsMapPlugin(object):
if len(s_hashes) > 0: if len(s_hashes) > 0:
try: self.logout(profile) try: self.logout(profile)
except: pass except: pass
# TODO: Replace by proper exception if e.errno == errno.EACCES:
if e.code == 13: raise HTTPUnauthorizedResponse(e.strerror)
raise HTTPUnauthorizedResponse(e.message) raise HTTPErrorResponse(e.strerror)
raise HTTPErrorResponse(e.message)
else: else:
# Update dicts with new values # Update dicts with new values
s_hashes[profile] = s_hash s_hashes[profile] = s_hash
@ -210,14 +210,14 @@ class _ActionsMapPlugin(object):
try: try:
ret = self.actionsmap.process(arguments, route=_route) ret = self.actionsmap.process(arguments, route=_route)
except MoulinetteError as e: except MoulinetteError as e:
raise HTTPErrorResponse(e.message) raise HTTPErrorResponse(e.strerror)
else: else:
return ret return ret
## Signals handlers ## Signals handlers
def _do_authenticate(self, authenticator, name, help): def _do_authenticate(self, authenticator, help):
"""Process the authentication """Process the authentication
Handle the actionsmap._AMapSignals.authenticate signal. Handle the actionsmap._AMapSignals.authenticate signal.
@ -227,12 +227,12 @@ class _ActionsMapPlugin(object):
try: try:
s_secret = self.secrets[s_id] s_secret = self.secrets[s_id]
s_hash = request.get_cookie('session.hashes', s_hash = request.get_cookie('session.hashes',
secret=s_secret)[name] secret=s_secret)[authenticator.name]
except KeyError: except KeyError:
if name == 'default': if authenticator.name == 'default':
msg = _("Needing authentication") msg = _("Needing authentication")
else: else:
msg = _("Needing authentication to profile '%s'") % name msg = _("Needing authentication to profile '%s'") % authenticator.name
raise HTTPUnauthorizedResponse(msg) raise HTTPUnauthorizedResponse(msg)
else: else:
return authenticator(token=(s_id, s_hash)) return authenticator(token=(s_id, s_hash))
@ -287,7 +287,12 @@ class MoulinetteAPI(object):
- _port -- Port number to run on - _port -- Port number to run on
""" """
try:
run(self._app, port=_port) run(self._app, port=_port)
except IOError as e:
if e.args[0] == errno.EADDRINUSE:
raise MoulinetteError(errno.EADDRINUSE, _("A server is already running"))
raise
## Routes handlers ## Routes handlers

View file

@ -1,8 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import errno
import getpass import getpass
from ..core import MoulinetteError from moulinette.core import MoulinetteError
# CLI helpers ---------------------------------------------------------- # CLI helpers ----------------------------------------------------------
@ -64,7 +65,7 @@ class MoulinetteCLI(object):
"""Moulinette command-line Interface """Moulinette command-line Interface
Initialize an interface connected to the standard input and output Initialize an interface connected to the standard input and output
stream which allows to process moulinette action. stream which allows to process moulinette actions.
Keyword arguments: Keyword arguments:
- actionsmap -- The interface relevant ActionsMap instance - actionsmap -- The interface relevant ActionsMap instance
@ -90,7 +91,7 @@ class MoulinetteCLI(object):
try: try:
ret = self.actionsmap.process(args, timeout=5) ret = self.actionsmap.process(args, timeout=5)
except KeyboardInterrupt, EOFError: except KeyboardInterrupt, EOFError:
raise MoulinetteError(125, _("Interrupted")) raise MoulinetteError(errno.EINTR, _("Interrupted"))
if isinstance(ret, dict): if isinstance(ret, dict):
pretty_print_dict(ret) pretty_print_dict(ret)
@ -100,7 +101,7 @@ class MoulinetteCLI(object):
## Signals handlers ## Signals handlers
def _do_authenticate(self, authenticator, name, help): def _do_authenticate(self, authenticator, help):
"""Process the authentication """Process the authentication
Handle the actionsmap._AMapSignals.authenticate signal. Handle the actionsmap._AMapSignals.authenticate signal.
@ -124,6 +125,6 @@ class MoulinetteCLI(object):
if confirm: if confirm:
if prompt(_('Retype %s: ') % message) != value: if prompt(_('Retype %s: ') % message) != value:
raise MoulinetteError(22, _("Values don't match")) raise MoulinetteError(errno.EINVAL, _("Values don't match"))
return value return value