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'"))
# Execute the action
cli(['yunohost', 'test'], args, use_cache)
except MoulinetteError as e:
print(e.colorize())
sys.exit(e.code)
ret = cli(['yunohost', 'test'], args, use_cache)
except YunoHostError as e:
print(colorize(_("Error: "), 'red') + e.message)
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'):
sys.path.append(basedir +'/src')
from moulinette import init, api
from moulinette import init, api, MoulinetteError
## Callbacks for additional routes
@ -40,6 +40,12 @@ if __name__ == '__main__':
sys.argv.remove('--debug')
# TODO: Add log argument
# Rune the server
api(['yunohost', 'test'], 6787, {('GET', '/installed'): is_installed}, use_cache)
try:
# 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)

View file

@ -29,7 +29,7 @@ __all__ = [
'MoulinetteError',
]
from .core import MoulinetteError
from moulinette.core import MoulinetteError
## Package functions
@ -50,7 +50,7 @@ def init(**kwargs):
"""
import sys
import __builtin__
from .core import Package, install_i18n
from moulinette.core import Package, install_i18n
__builtin__.__dict__['pkg'] = Package(**kwargs)
# Initialize internationalization
@ -76,8 +76,8 @@ def api(namespaces, port, routes={}, use_cache=True):
instead of using the cached one
"""
from .actionsmap import ActionsMap
from .interface.api import MoulinetteAPI
from moulinette.actionsmap import ActionsMap
from moulinette.interface.api import MoulinetteAPI
amap = ActionsMap('api', namespaces, use_cache)
moulinette = MoulinetteAPI(amap, routes)
@ -97,10 +97,15 @@ def cli(namespaces, args, use_cache=True):
instead of using the cached one
"""
from .actionsmap import ActionsMap
from .interface.cli import MoulinetteCLI
from moulinette.actionsmap import ActionsMap
from moulinette.interface.cli import MoulinetteCLI, colorize
try:
amap = ActionsMap('cli', namespaces, use_cache)
moulinette = MoulinetteCLI(amap)
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 -*-
import argparse
import yaml
import re
import os
import re
import errno
import yaml
import argparse
import cPickle as pickle
from collections import OrderedDict
import logging
from . import __version__
from .core import MoulinetteError, MoulinetteLock, init_authenticator
from moulinette.core import (MoulinetteError, MoulinetteLock,
init_authenticator)
## Actions map Signals -------------------------------------------------
@ -48,7 +49,7 @@ class _AMapSignals(object):
"""The list of available signals"""
signals = { 'authenticate', 'prompt' }
def authenticate(self, authenticator, name, help, vendor=None):
def authenticate(self, authenticator, help):
"""Process the authentication
Attempt to authenticate to the given authenticator and return
@ -57,10 +58,8 @@ class _AMapSignals(object):
action).
Keyword arguments:
- authenticator -- The authenticator to use
- name -- The authenticator name in the actions map
- authenticator -- The authenticator object to use
- help -- A help message for the authenticator
- vendor -- Not expected (TODO: Remove it)
Returns:
The authenticator object
@ -68,7 +67,7 @@ class _AMapSignals(object):
"""
if authenticator.is_authenticated:
return authenticator
return self._authenticate(authenticator, name, help)
return self._authenticate(authenticator, help)
def prompt(self, message, is_password=False, confirm=False):
"""Prompt for a value
@ -172,18 +171,14 @@ class _AMapParser(object):
raise NotImplementedError("derived class '%s' must override this method" % \
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
Create a new action and return an argument parser for it. 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).
Create a new action and return an argument parser for it.
Keyword arguments:
- name -- The action name
- tid -- The tuple identifier of the action
- conf -- A dict of configuration for the action
Returns:
An ArgumentParser based object
@ -288,6 +283,7 @@ class _AMapParser(object):
- configuration -- The configuration to pre-format
"""
# TODO: Create a class with a validator method for each configuration
conf = {}
# -- 'authenficate'
@ -305,7 +301,7 @@ class _AMapParser(object):
conf['authenticate'] = True if self.name in ifaces else False
else:
# 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'
try:
@ -315,26 +311,31 @@ class _AMapParser(object):
else:
if not is_global and isinstance(auth, str):
try:
# Store parameters of the required authenticator
# Store needed authenticator profile
conf['authenticator'] = self.global_conf['authenticator'][auth]
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):
if len(auth) == 0:
logging.warning('no authenticator defined in global configuration')
else:
auths = {}
for auth_name, auth_conf in auth.items():
# Add authenticator name
auths[auth_name] = ({ 'name': auth_name,
'vendor': auth_conf.get('vendor'),
'help': auth_conf.get('help', None)
},
# Add authenticator profile as a 3-tuple
# (identifier, configuration, parameters) with
# - identifier: the authenticator vendor and its
# 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', {}))
conf['authenticator'] = auths
else:
# 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'
try:
@ -346,7 +347,7 @@ class _AMapParser(object):
conf['argument_auth'] = arg_auth
else:
# 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
@ -362,14 +363,12 @@ class _AMapParser(object):
"""
if name == 'authenticator' and value:
auth_conf, auth_params = value
(identifier, configuration, parameters) = value
# Return authenticator configuration and an instanciator for
# it as a 2-tuple
return (auth_conf,
lambda: init_authenticator(auth_conf['name'],
auth_conf['vendor'],
**auth_params))
# Return global configuration and an authenticator
# instanciator as a 2-tuple
return (configuration,
lambda: init_authenticator(identifier, parameters))
return value
@ -415,7 +414,7 @@ class CLIAMapParser(_AMapParser):
parser = self._subparsers.add_parser(name, help=category_help)
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
Keyword arguments:
@ -425,8 +424,6 @@ class CLIAMapParser(_AMapParser):
A new argparse.ArgumentParser object for the action
"""
if conf:
self.set_conf(tid, conf)
return self._subparsers.add_parser(name, help=action_help)
def parse_args(self, args, **kwargs):
@ -440,7 +437,7 @@ class CLIAMapParser(_AMapParser):
auth = shandler.authenticate(klass(), **auth_conf)
if not auth.is_authenticated:
# 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 \
self.get_conf(ret._tid, 'authenticate') == 'all':
ret.auth = auth
@ -521,7 +518,6 @@ class APIAMapParser(_AMapParser):
"""Actions map's API Parser
"""
def __init__(self):
super(APIAMapParser, self).__init__()
@ -556,7 +552,7 @@ class APIAMapParser(_AMapParser):
def add_category_parser(self, name, **kwargs):
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
Keyword arguments:
@ -582,9 +578,7 @@ class APIAMapParser(_AMapParser):
# Create and append parser
parser = _HTTPArgumentParser()
self._parsers[key] = parser
if conf:
self.set_conf(key, conf)
self._parsers[key] = (tid, parser)
# Return the created parser
return parser
@ -596,25 +590,27 @@ class APIAMapParser(_AMapParser):
- route -- The action route as a 2-tuple (method, path)
"""
# Retrieve the parser for the route
if route not in self.routes:
raise MoulinetteError(22, "No parser for '%s %s' found" % key)
try:
# Retrieve the tid and the parser for the route
tid, parser = self._parsers[route]
except KeyError:
raise MoulinetteError(errno.EINVAL, "No parser found for route '%s'" % route)
ret = argparse.Namespace()
# Perform authentication if needed
if self.get_conf(route, 'authenticate'):
auth_conf, klass = self.get_conf(route, 'authenticator')
if self.get_conf(tid, 'authenticate'):
auth_conf, klass = self.get_conf(tid, 'authenticator')
# TODO: Catch errors
auth = shandler.authenticate(klass(), **auth_conf)
if not auth.is_authenticated:
# TODO: Set proper error code
raise MoulinetteError(1, _("This action need authentication"))
if self.get_conf(route, 'argument_auth') and \
self.get_conf(route, 'authenticate') == 'all':
raise MoulinetteError(errno.EACCES, _("This action need authentication"))
if self.get_conf(tid, 'argument_auth') and \
self.get_conf(tid, 'authenticate') == 'all':
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.
@ -758,7 +754,7 @@ class PatternParameter(_ExtraParameter):
pattern, message = (arguments[0], arguments[1])
if not re.match(pattern, arg_value or ''):
raise MoulinetteError(22, message)
raise MoulinetteError(errno.EINVAL, message)
return arg_value
@staticmethod
@ -776,7 +772,7 @@ The list of available extra parameters classes. It will keep to this list
order on argument parsing.
"""
extraparameters_list = {AskParameter, PasswordParameter, PatternParameter}
extraparameters_list = [AskParameter, PasswordParameter, PatternParameter]
# Extra parameters argument Parser
@ -880,7 +876,7 @@ class ActionsMap(object):
# Retrieve the interface parser
self._parser_class = actionsmap_parsers[interface]
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)
@ -931,7 +927,7 @@ class ActionsMap(object):
try:
auth = self.parser.get_global_conf('authenticator', profile)[1]
except KeyError:
raise MoulinetteError(167, _("Unknown authenticator profile '%s'") % profile)
raise MoulinetteError(errno.EINVAL, _("Unknown authenticator profile '%s'") % profile)
else:
return auth()
@ -977,7 +973,7 @@ class ActionsMap(object):
fromlist=[func_name])
func = getattr(mod, func_name)
except (AttributeError, ImportError):
raise MoulinetteError(168, _('Function is not defined'))
raise MoulinetteError(errno.ENOSYS, _('Function is not defined'))
else:
# Process the action
return func(**arguments)
@ -1056,11 +1052,13 @@ class ActionsMap(object):
for argn, argp in arguments.items():
names = top_parser.format_arg_names(argn,
argp.pop('full', None))
extra = argp.pop('extra', None)
arg = parser.add_argument(*names, **argp)
if extra:
extras[arg.dest] = _get_extra(arg.dest, extra)
try:
extra = argp.pop('extra')
arg_dest = (parser.add_argument(*names, **argp)).dest
extras[arg_dest] = _get_extra(arg_dest, extra)
except KeyError:
# No extra parameters
parser.add_argument(*names, **argp)
if extras:
parser.set_defaults(_extra=extras)
@ -1095,6 +1093,7 @@ class ActionsMap(object):
actions = cp.pop('actions')
except KeyError:
# Invalid category without actions
logging.warning("no actions found in category '%s'" % cn)
continue
# Get category parser
@ -1102,13 +1101,18 @@ class ActionsMap(object):
# -- Parse actions
for an, ap in actions.items():
conf = ap.pop('configuration', None)
args = ap.pop('arguments', {})
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:
# Get action parser
parser = cat_parser.add_action_parser(an, tid, conf, **ap)
parser = cat_parser.add_action_parser(an, tid, **ap)
except AttributeError:
# No parser for the action
continue
@ -1119,5 +1123,6 @@ class ActionsMap(object):
# Store action identifier and add arguments
parser.set_defaults(_tid=tid)
_add_arguments(parser, args)
_set_conf(cat_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 sys
import time
import errno
import gettext
import logging
from .helpers import colorize
from importlib import import_module
# Package manipulation -------------------------------------------------
@ -132,189 +133,25 @@ class Package(object):
# Authenticators -------------------------------------------------------
import ldap
import gnupg
def init_authenticator((vendor, name), kwargs={}):
"""Return a new authenticator instance
class AuthenticationError(Exception):
pass
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.
Retrieve the given authenticator vendor and return a new instance of
its Authenticator class for the given profile.
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:
# Extract id and hash from token
s_id, s_hash = token
except TypeError:
if not password:
raise MoulinetteError(22, _("Invalid format for token"))
mod = import_module('moulinette.authenticators.%s' % vendor)
except ImportError:
# TODO: List available authenticator vendors
raise MoulinetteError(errno.EINVAL, _("Unknown authenticator vendor '%s'" % vendor))
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 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)
return mod.Authenticator(name, **kwargs)
def clean_session(session_id, profiles=[]):
sessiondir = pkg.get_cachedir('session')
@ -330,44 +167,9 @@ def clean_session(session_id, profiles=[]):
# Moulinette core classes ----------------------------------------------
class MoulinetteError(Exception):
"""Moulinette base exception
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 MoulinetteError(OSError):
"""Moulinette base exception"""
pass
class MoulinetteLock(object):
@ -409,7 +211,7 @@ class MoulinetteLock(object):
break
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)
# Wait before checking again
time.sleep(self.interval)

View file

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

View file

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