One more refactoring in interfaces

* Move actions map parsers classes into their respective interface modules
* Introduce an Interface base class for a futur usage
* Each interfaces must now implement ActionsMapParser and Interface classes
* Standardize interface instantiation
This commit is contained in:
Jerome Lebleu 2014-03-25 18:13:44 +01:00
parent ecd88ce853
commit b3af4ddaea
9 changed files with 679 additions and 605 deletions

View file

@ -45,7 +45,7 @@ if __name__ == '__main__':
api(['yunohost', 'test'], 6787,
{('GET', '/installed'): is_installed}, use_cache)
except MoulinetteError as e:
from moulinette.interface.cli import colorize
from moulinette.interfaces.cli import colorize
print(_('%s: %s' % (colorize(_('Error'), 'red'), e.strerror)))
sys.exit(e.code)
sys.exit(e.errno)
sys.exit(0)

View file

@ -26,10 +26,10 @@ __credits__ = """
"""
__all__ = [
'init', 'api', 'cli',
'MoulinetteError',
'init_interface', 'MoulinetteError',
]
from moulinette.core import MoulinetteError
from moulinette.core import init_interface, MoulinetteError
## Package functions
@ -76,12 +76,10 @@ def api(namespaces, port, routes={}, use_cache=True):
instead of using the cached one
"""
from moulinette.actionsmap import ActionsMap
from moulinette.interface.api import MoulinetteAPI
amap = ActionsMap('api', namespaces, use_cache)
moulinette = MoulinetteAPI(amap, routes)
moulinette = init_interface('api',
kwargs={'routes': routes},
actionsmap={'namespaces': namespaces,
'use_cache': use_cache})
moulinette.run(port)
def cli(namespaces, args, use_cache=True):
@ -97,13 +95,12 @@ def cli(namespaces, args, use_cache=True):
instead of using the cached one
"""
from moulinette.actionsmap import ActionsMap
from moulinette.interface.cli import MoulinetteCLI, colorize
from moulinette.interfaces.cli import colorize
try:
amap = ActionsMap('cli', namespaces, use_cache)
moulinette = MoulinetteCLI(amap)
moulinette = init_interface('cli',
actionsmap={'namespaces': namespaces,
'use_cache': use_cache})
moulinette.run(args)
except MoulinetteError as e:
print(_('%s: %s' % (colorize(_('Error'), 'red'), e.strerror)))

View file

@ -3,19 +3,17 @@
import os
import re
import errno
import logging
import yaml
import argparse
import cPickle as pickle
from collections import OrderedDict
import logging
from moulinette.core import (MoulinetteError, MoulinetteLock,
init_authenticator)
from moulinette.core import (MoulinetteError, MoulinetteLock)
from moulinette.interfaces import BaseActionsMapParser
## Actions map Signals -------------------------------------------------
class _AMapSignals(object):
class ActionsMapSignals(object):
"""Actions map's Signals interface
Allow to easily connect signals of the actions map to handlers. They
@ -93,533 +91,7 @@ class _AMapSignals(object):
def _notimplemented(**kwargs):
raise NotImplementedError("this signal is not handled")
shandler = _AMapSignals()
## Interfaces' Actions map Parser --------------------------------------
class _AMapParser(object):
"""Actions map's base Parser
Each interfaces must implement a parser class derived from this
class. It is used to parse the main parts of the actions map (i.e.
global arguments, categories and actions).
"""
def __init__(self, parent=None):
if parent:
self._o = parent
else:
self._o = self
self._global_conf = {}
self._conf = {}
## Virtual properties
# Each parser classes must implement these properties.
"""The name of the interface for which it is the parser"""
name = None
## Virtual methods
# Each parser classes must implement these methods.
@staticmethod
def format_arg_names(name, full):
"""Format argument name
Format agument name depending on its 'full' parameter and return
a list of strings which will be used as name or option strings
for the argument parser.
Keyword arguments:
- name -- The argument name
- full -- The argument's 'full' parameter
Returns:
A list of option strings
"""
raise NotImplementedError("derived class '%s' must override this method" % \
self.__class__.__name__)
def add_global_parser(self, **kwargs):
"""Add a parser for global arguments
Create and return an argument parser for global arguments.
Returns:
An ArgumentParser based object
"""
raise NotImplementedError("derived class '%s' must override this method" % \
self.__class__.__name__)
def add_category_parser(self, name, **kwargs):
"""Add a parser for a category
Create a new category and return a parser for it.
Keyword arguments:
- name -- The category name
Returns:
A BaseParser based object
"""
raise NotImplementedError("derived class '%s' must override this method" % \
self.__class__.__name__)
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.
Keyword arguments:
- name -- The action name
- tid -- The tuple identifier of the action
Returns:
An ArgumentParser based object
"""
raise NotImplementedError("derived class '%s' must override this method" % \
self.__class__.__name__)
def parse_args(self, args, **kwargs):
"""Parse arguments
Convert argument variables to objects and assign them as
attributes of the namespace.
Keyword arguments:
- args -- Arguments string or dict (TODO)
Returns:
The populated namespace
"""
raise NotImplementedError("derived class '%s' must override this method" % \
self.__class__.__name__)
## Configuration access
@property
def global_conf(self):
"""Return the global configuration of the parser"""
return self._o._global_conf
def get_global_conf(self, name, profile='default'):
"""Get the global value of a configuration
Return the formated global value of the configuration 'name' for
the given profile. If the configuration doesn't provide profile,
the formated default value is returned.
Keyword arguments:
- name -- The configuration name
- profile -- The profile of the configuration
"""
if name == 'authenticator':
value = self.global_conf[name][profile]
else:
value = self.global_conf[name]
return self._format_conf(name, value)
def set_global_conf(self, configuration):
"""Set global configuration
Set the global configuration to use for the parser.
Keyword arguments:
- configuration -- The global configuration
"""
self._o._global_conf.update(self._validate_conf(configuration, True))
def get_conf(self, action, name):
"""Get the value of an action configuration
Return the formated value of configuration 'name' for the action
identified by 'action'. If the configuration for the action is
not set, the default one is returned.
Keyword arguments:
- action -- An action identifier
- name -- The configuration name
"""
try:
value = self._o._conf[action][name]
except KeyError:
return self.get_global_conf(name)
else:
return self._format_conf(name, value)
def set_conf(self, action, configuration):
"""Set configuration for an action
Set the configuration to use for a given action identified by
'action' which is specific to the parser.
Keyword arguments:
- action -- The action identifier
- configuration -- The configuration for the action
"""
self._o._conf[action] = self._validate_conf(configuration)
def _validate_conf(self, configuration, is_global=False):
"""Validate configuration for the parser
Return the validated configuration for the interface's actions
map parser.
Keyword arguments:
- configuration -- The configuration to pre-format
"""
# TODO: Create a class with a validator method for each configuration
conf = {}
# -- 'authenficate'
try:
ifaces = configuration['authenticate']
except KeyError:
pass
else:
if ifaces == 'all':
conf['authenticate'] = ifaces
elif ifaces == False:
conf['authenticate'] = False
elif isinstance(ifaces, list):
# Store only if authentication is needed
conf['authenticate'] = True if self.name in ifaces else False
else:
# TODO: Log error instead and tell valid values
raise MoulinetteError(errno.EINVAL, "Invalid value '%r' for configuration 'authenticate'" % ifaces)
# -- 'authenticator'
try:
auth = configuration['authenticator']
except KeyError:
pass
else:
if not is_global and isinstance(auth, str):
try:
# Store needed authenticator profile
conf['authenticator'] = self.global_conf['authenticator'][auth]
except KeyError:
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 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(errno.EINVAL, "Invalid value '%r' for configuration 'authenticator'" % auth)
# -- 'argument_auth'
try:
arg_auth = configuration['argument_auth']
except KeyError:
pass
else:
if isinstance(arg_auth, bool):
conf['argument_auth'] = arg_auth
else:
# TODO: Log error instead and tell valid values
raise MoulinetteError(errno.EINVAL, "Invalid value '%r' for configuration 'argument_auth'" % arg_auth)
return conf
def _format_conf(self, name, value):
"""Format a configuration value
Return the formated value of the configuration 'name' from its
given value.
Keyword arguments:
- name -- The name of the configuration
- value -- The value to format
"""
if name == 'authenticator' and value:
(identifier, configuration, parameters) = value
# Return global configuration and an authenticator
# instanciator as a 2-tuple
return (configuration,
lambda: init_authenticator(identifier, parameters))
return value
# CLI Actions map Parser
class CLIAMapParser(_AMapParser):
"""Actions map's CLI Parser
"""
def __init__(self, parent=None, parser=None):
super(CLIAMapParser, self).__init__(parent)
self._parser = parser or argparse.ArgumentParser()
self._subparsers = self._parser.add_subparsers()
## Implement virtual properties
name = 'cli'
## Implement virtual methods
@staticmethod
def format_arg_names(name, full):
if name[0] == '-' and full:
return [name, full]
return [name]
def add_global_parser(self, **kwargs):
return self._parser
def add_category_parser(self, name, category_help=None, **kwargs):
"""Add a parser for a category
Keyword arguments:
- category_help -- A brief description for the category
Returns:
A new CLIParser object for the category
"""
parser = self._subparsers.add_parser(name, help=category_help)
return self.__class__(self, parser)
def add_action_parser(self, name, tid, action_help=None, **kwargs):
"""Add a parser for an action
Keyword arguments:
- action_help -- A brief description for the action
Returns:
A new argparse.ArgumentParser object for the action
"""
return self._subparsers.add_parser(name, help=action_help)
def parse_args(self, args, **kwargs):
ret = self._parser.parse_args(args)
# Perform authentication if needed
if self.get_conf(ret._tid, 'authenticate'):
auth_conf, klass = self.get_conf(ret._tid, 'authenticator')
# TODO: Catch errors
auth = shandler.authenticate(klass(), **auth_conf)
if not auth.is_authenticated:
# TODO: Set proper error code
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
return ret
# API Actions map Parser
class _HTTPArgumentParser(object):
"""Argument parser for HTTP requests
Object for parsing HTTP requests into Python objects. It is based
on argparse.ArgumentParser class and implements some of its methods.
"""
def __init__(self):
# Initialize the ArgumentParser object
self._parser = argparse.ArgumentParser(usage='',
prefix_chars='@',
add_help=False)
self._parser.error = self._error
self._positional = [] # list(arg_name)
self._optional = {} # dict({arg_name: option_strings})
def set_defaults(self, **kwargs):
return self._parser.set_defaults(**kwargs)
def get_default(self, dest):
return self._parser.get_default(dest)
def add_argument(self, *args, **kwargs):
action = self._parser.add_argument(*args, **kwargs)
# Append newly created action
if len(action.option_strings) == 0:
self._positional.append(action.dest)
else:
self._optional[action.dest] = action.option_strings
return action
def parse_args(self, args={}, namespace=None):
arg_strings = []
## Append an argument to the current one
def append(arg_strings, value, option_string=None):
# TODO: Process list arguments
if isinstance(value, bool):
# Append the option string only
if option_string is not None:
arg_strings.append(option_string)
elif isinstance(value, str):
if option_string is not None:
arg_strings.append(option_string)
arg_strings.append(value)
else:
arg_strings.append(value)
return arg_strings
# Iterate over positional arguments
for dest in self._positional:
if dest in args:
arg_strings = append(arg_strings, args[dest])
# Iterate over optional arguments
for dest, opt in self._optional.items():
if dest in args:
arg_strings = append(arg_strings, args[dest], opt[0])
return self._parser.parse_args(arg_strings, namespace)
def _error(self, message):
# TODO: Raise a proper exception
raise MoulinetteError(1, message)
class APIAMapParser(_AMapParser):
"""Actions map's API Parser
"""
def __init__(self):
super(APIAMapParser, self).__init__()
self._parsers = {} # dict({(method, path): _HTTPArgumentParser})
@property
def routes(self):
"""Get current routes"""
return self._parsers.keys()
## Implement virtual properties
name = 'api'
## Implement virtual methods
@staticmethod
def format_arg_names(name, full):
if name[0] != '-':
return [name]
if full:
return [full.replace('--', '@', 1)]
if name.startswith('--'):
return [name.replace('--', '@', 1)]
return [name.replace('-', '@', 1)]
def add_global_parser(self, **kwargs):
raise AttributeError("global arguments are not managed")
def add_category_parser(self, name, **kwargs):
return self
def add_action_parser(self, name, tid, api=None, **kwargs):
"""Add a parser for an action
Keyword arguments:
- api -- The action route (e.g. 'GET /' )
Returns:
A new _HTTPArgumentParser object for the route
"""
try:
# Validate action route
m = re.match('(GET|POST|PUT|DELETE) (/\S+)', api)
except TypeError:
raise AttributeError("the action '%s' doesn't provide api access" % name)
if not m:
# TODO: Log error
raise ValueError("the action '%s' doesn't provide api access" % name)
# Check if a parser already exists for the route
key = (m.group(1), m.group(2))
if key in self.routes:
raise AttributeError("a parser for '%s' already exists" % key)
# Create and append parser
parser = _HTTPArgumentParser()
self._parsers[key] = (tid, parser)
# Return the created parser
return parser
def parse_args(self, args, route, **kwargs):
"""Parse arguments
Keyword arguments:
- route -- The action route as a 2-tuple (method, path)
"""
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(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(errno.EACCES, _("This action need authentication"))
if self.get_conf(tid, 'argument_auth') and \
self.get_conf(tid, 'authenticate') == 'all':
ret.auth = auth
return parser.parse_args(args, ret)
"""
The dict of interfaces names and their associated parser class.
"""
actionsmap_parsers = {
'api': APIAMapParser,
'cli': CLIAMapParser
}
shandler = ActionsMapSignals()
## Extra parameters ----------------------------------------------------
@ -850,35 +322,30 @@ class ExtraArgumentParser(object):
class ActionsMap(object):
"""Validate and process actions defined into an actions map
The actions map defines the features and their usage of the main
application. It is composed by categories which contain one or more
action(s). Moreover, the action can have specific argument(s).
The actions map defines the features - and their usage - of an
application which will be available through the moulinette.
It is composed by categories which contain one or more action(s).
Moreover, the action can have specific argument(s).
This class allows to manipulate one or several actions maps
associated to a namespace. If no namespace is given, it will load
all available namespaces.
Keyword arguments:
- interface -- The type of interface which needs the actions map.
Possible values are:
- 'cli' for the command line interface
- 'api' for an API usage (HTTP requests)
- parser -- The BaseActionsMapParser derived class to use for
parsing the actions map
- namespaces -- The list of namespaces to use
- use_cache -- False if it should parse the actions map file
instead of using the cached one.
"""
def __init__(self, interface, namespaces=[], use_cache=True):
def __init__(self, parser, namespaces=[], use_cache=True):
self.use_cache = use_cache
self.interface = interface
if not issubclass(parser, BaseActionsMapParser):
raise MoulinetteError(errno.EINVAL, _("Invalid parser class '%s'" % parser.__name__))
self._parser_class = parser
try:
# Retrieve the interface parser
self._parser_class = actionsmap_parsers[interface]
except KeyError:
raise MoulinetteError(errno.EINVAL, _("Unknown interface '%s'" % interface))
logging.debug("initializing ActionsMap for the '%s' interface" % interface)
logging.debug("initializing ActionsMap for the interface '%s'" % parser.interface)
if len(namespaces) == 0:
namespaces = self.get_namespaces()
@ -903,7 +370,7 @@ class ActionsMap(object):
actionsmaps[n] = yaml.load(f)
# Generate parsers
self.extraparser = ExtraArgumentParser(interface)
self.extraparser = ExtraArgumentParser(parser.interface)
self._parser = self._construct_parser(actionsmaps)
@property
@ -1063,7 +530,7 @@ class ActionsMap(object):
parser.set_defaults(_extra=extras)
# Instantiate parser
top_parser = self._parser_class()
top_parser = self._parser_class(shandler)
# Iterate over actions map namespaces
for n, actionsmap in actionsmaps.items():

View file

@ -12,8 +12,10 @@ 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.
from this class which must overrides virtual properties and methods.
It is used to authenticate and manage session. 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.

View file

@ -131,7 +131,48 @@ class Package(object):
return open('%s/%s' % (self.get_cachedir(**kwargs), filename), mode)
# Authenticators -------------------------------------------------------
# Interfaces & Authenticators management -------------------------------
def init_interface(name, kwargs={}, actionsmap={}):
"""Return a new interface instance
Retrieve the given interface module and return a new instance of its
Interface class. It is initialized with arguments 'kwargs' and
connected to 'actionsmap' if it's an ActionsMap object, otherwise
a new ActionsMap instance will be initialized with arguments
'actionsmap'.
Keyword arguments:
- name -- The interface name
- kwargs -- A dict of arguments to pass to Interface
- actionsmap -- Either an ActionsMap instance or a dict of
arguments to pass to ActionsMap
"""
from moulinette.actionsmap import ActionsMap
try:
mod = import_module('moulinette.interfaces.%s' % name)
except ImportError:
# TODO: List available interfaces
raise MoulinetteError(errno.EINVAL, _("Unknown interface '%s'" % name))
else:
try:
# Retrieve interface classes
parser = mod.ActionsMapParser
interface = mod.Interface
except AttributeError as e:
raise MoulinetteError(errno.EFAULT, _("Invalid interface '%s': %s") % (name, e))
# Instantiate or retrieve ActionsMap
if isinstance(actionsmap, dict):
amap = ActionsMap(actionsmap.pop('parser', parser), **actionsmap)
elif isinstance(actionsmap, ActionsMap):
amap = actionsmap
else:
raise MoulinetteError(errno.EINVAL, _("Invalid actions map '%r'" % actionsmap))
return interface(amap, **kwargs)
def init_authenticator((vendor, name), kwargs={}):
"""Return a new authenticator instance
@ -148,14 +189,24 @@ def init_authenticator((vendor, name), kwargs={}):
try:
mod = import_module('moulinette.authenticators.%s' % vendor)
except ImportError:
# TODO: List available authenticator vendors
# TODO: List available authenticators vendors
raise MoulinetteError(errno.EINVAL, _("Unknown authenticator vendor '%s'" % vendor))
else:
return mod.Authenticator(name, **kwargs)
def clean_session(session_id, profiles=[]):
"""Clean a session cache
Remove cache for the session 'session_id' and for profiles in
'profiles' or for all of them if the list is empty.
Keyword arguments:
- session_id -- The session id to clean
- profiles -- A list of profiles to clean
"""
sessiondir = pkg.get_cachedir('session')
if len(profiles) == 0:
if not profiles:
profiles = os.listdir(sessiondir)
for p in profiles:

View file

@ -0,0 +1,307 @@
# -*- coding: utf-8 -*-
import errno
import logging
from moulinette.core import (init_authenticator, MoulinetteError)
# Base Class -----------------------------------------------------------
class BaseActionsMapParser(object):
"""Actions map's base Parser
Each interfaces must implement an ActionsMapParser class derived
from this class which must overrides virtual properties and methods.
It is used to parse the main parts of the actions map (i.e. global
arguments, categories and actions). It implements methods to set/get
the global and actions configuration.
Keyword arguments:
- shandler -- A actionsmap.ActionsMapSignals instance
- parent -- A parent BaseActionsMapParser derived object
"""
def __init__(self, shandler, parent=None):
if parent:
self.shandler = parent.shandler
self._o = parent
else:
self.shandler = shandler
self._o = self
self._global_conf = {}
self._conf = {}
## Virtual properties
# Each parser classes must implement these properties.
"""The name of the interface for which it is the parser"""
interface = None
## Virtual methods
# Each parser classes must implement these methods.
@staticmethod
def format_arg_names(name, full):
"""Format argument name
Format agument name depending on its 'full' parameter and return
a list of strings which will be used as name or option strings
for the argument parser.
Keyword arguments:
- name -- The argument name
- full -- The argument's 'full' parameter
Returns:
A list of option strings
"""
raise NotImplementedError("derived class '%s' must override this method" % \
self.__class__.__name__)
def add_global_parser(self, **kwargs):
"""Add a parser for global arguments
Create and return an argument parser for global arguments.
Returns:
An ArgumentParser based object
"""
raise NotImplementedError("derived class '%s' must override this method" % \
self.__class__.__name__)
def add_category_parser(self, name, **kwargs):
"""Add a parser for a category
Create a new category and return a parser for it.
Keyword arguments:
- name -- The category name
Returns:
A BaseParser based object
"""
raise NotImplementedError("derived class '%s' must override this method" % \
self.__class__.__name__)
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.
Keyword arguments:
- name -- The action name
- tid -- The tuple identifier of the action
Returns:
An ArgumentParser based object
"""
raise NotImplementedError("derived class '%s' must override this method" % \
self.__class__.__name__)
def parse_args(self, args, **kwargs):
"""Parse arguments
Convert argument variables to objects and assign them as
attributes of the namespace.
Keyword arguments:
- args -- Arguments string or dict (TODO)
Returns:
The populated namespace
"""
raise NotImplementedError("derived class '%s' must override this method" % \
self.__class__.__name__)
## Configuration access
@property
def global_conf(self):
"""Return the global configuration of the parser"""
return self._o._global_conf
def get_global_conf(self, name, profile='default'):
"""Get the global value of a configuration
Return the formated global value of the configuration 'name' for
the given profile. If the configuration doesn't provide profile,
the formated default value is returned.
Keyword arguments:
- name -- The configuration name
- profile -- The profile of the configuration
"""
if name == 'authenticator':
value = self.global_conf[name][profile]
else:
value = self.global_conf[name]
return self._format_conf(name, value)
def set_global_conf(self, configuration):
"""Set global configuration
Set the global configuration to use for the parser.
Keyword arguments:
- configuration -- The global configuration
"""
self._o._global_conf.update(self._validate_conf(configuration, True))
def get_conf(self, action, name):
"""Get the value of an action configuration
Return the formated value of configuration 'name' for the action
identified by 'action'. If the configuration for the action is
not set, the default one is returned.
Keyword arguments:
- action -- An action identifier
- name -- The configuration name
"""
try:
value = self._o._conf[action][name]
except KeyError:
return self.get_global_conf(name)
else:
return self._format_conf(name, value)
def set_conf(self, action, configuration):
"""Set configuration for an action
Set the configuration to use for a given action identified by
'action' which is specific to the parser.
Keyword arguments:
- action -- The action identifier
- configuration -- The configuration for the action
"""
self._o._conf[action] = self._validate_conf(configuration)
def _validate_conf(self, configuration, is_global=False):
"""Validate configuration for the parser
Return the validated configuration for the interface's actions
map parser.
Keyword arguments:
- configuration -- The configuration to pre-format
"""
# TODO: Create a class with a validator method for each configuration
conf = {}
# -- 'authenficate'
try:
ifaces = configuration['authenticate']
except KeyError:
pass
else:
if ifaces == 'all':
conf['authenticate'] = ifaces
elif ifaces == False:
conf['authenticate'] = False
elif isinstance(ifaces, list):
# Store only if authentication is needed
conf['authenticate'] = True if self.interface in ifaces else False
else:
# TODO: Log error instead and tell valid values
raise MoulinetteError(errno.EINVAL, "Invalid value '%r' for configuration 'authenticate'" % ifaces)
# -- 'authenticator'
try:
auth = configuration['authenticator']
except KeyError:
pass
else:
if not is_global and isinstance(auth, str):
try:
# Store needed authenticator profile
conf['authenticator'] = self.global_conf['authenticator'][auth]
except KeyError:
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 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(errno.EINVAL, "Invalid value '%r' for configuration 'authenticator'" % auth)
# -- 'argument_auth'
try:
arg_auth = configuration['argument_auth']
except KeyError:
pass
else:
if isinstance(arg_auth, bool):
conf['argument_auth'] = arg_auth
else:
# TODO: Log error instead and tell valid values
raise MoulinetteError(errno.EINVAL, "Invalid value '%r' for configuration 'argument_auth'" % arg_auth)
return conf
def _format_conf(self, name, value):
"""Format a configuration value
Return the formated value of the configuration 'name' from its
given value.
Keyword arguments:
- name -- The name of the configuration
- value -- The value to format
"""
if name == 'authenticator' and value:
(identifier, configuration, parameters) = value
# Return global configuration and an authenticator
# instanciator as a 2-tuple
return (configuration,
lambda: init_authenticator(identifier, parameters))
return value
class BaseInterface(object):
"""Moulinette's base Interface
Each interfaces must implement an Interface class derived from this
class which must overrides virtual properties and methods.
It is used to provide a user interface for an actions map.
Keyword arguments:
- actionsmap -- The ActionsMap instance to connect to
"""
# TODO: Add common interface methods and try to standardize default ones
def __init__(self, actionsmap):
raise NotImplementedError("derived class '%s' must override this method" % \
self.__class__.__name__)

View file

@ -1,41 +1,89 @@
# -*- coding: utf-8 -*-
import os
import re
import errno
from bottle import run, request, response, Bottle, HTTPResponse
import binascii
import argparse
from json import dumps as json_encode
from bottle import run, request, response, Bottle, HTTPResponse
from moulinette.core import MoulinetteError, clean_session
from moulinette.helpers import YunoHostError, YunoHostLDAP
from moulinette.interfaces import (BaseActionsMapParser, BaseInterface)
# API helpers ----------------------------------------------------------
import os
import binascii
def random_ascii(length=20):
"""Return a random ascii string"""
return binascii.hexlify(os.urandom(length)).decode('ascii')
def random20():
return binascii.hexlify(os.urandom(20)).decode('ascii')
class _HTTPArgumentParser(object):
"""Argument parser for HTTP requests
Object for parsing HTTP requests into Python objects. It is based
on argparse.ArgumentParser class and implements some of its methods.
# HTTP Responses -------------------------------------------------------
"""
def __init__(self):
# Initialize the ArgumentParser object
self._parser = argparse.ArgumentParser(usage='',
prefix_chars='@',
add_help=False)
self._parser.error = self._error
class HTTPOKResponse(HTTPResponse):
def __init__(self, output=''):
super(HTTPOKResponse, self).__init__(output, 200)
self._positional = [] # list(arg_name)
self._optional = {} # dict({arg_name: option_strings})
class HTTPBadRequestResponse(HTTPResponse):
def __init__(self, output=''):
super(HTTPBadRequestResponse, self).__init__(output, 400)
def set_defaults(self, **kwargs):
return self._parser.set_defaults(**kwargs)
class HTTPUnauthorizedResponse(HTTPResponse):
def __init__(self, output=''):
super(HTTPUnauthorizedResponse, self).__init__(output, 401)
def get_default(self, dest):
return self._parser.get_default(dest)
class HTTPErrorResponse(HTTPResponse):
def __init__(self, output=''):
super(HTTPErrorResponse, self).__init__(output, 500)
def add_argument(self, *args, **kwargs):
action = self._parser.add_argument(*args, **kwargs)
# Append newly created action
if len(action.option_strings) == 0:
self._positional.append(action.dest)
else:
self._optional[action.dest] = action.option_strings
# API moulinette interface ---------------------------------------------
return action
def parse_args(self, args={}, namespace=None):
arg_strings = []
## Append an argument to the current one
def append(arg_strings, value, option_string=None):
# TODO: Process list arguments
if isinstance(value, bool):
# Append the option string only
if option_string is not None:
arg_strings.append(option_string)
elif isinstance(value, str):
if option_string is not None:
arg_strings.append(option_string)
arg_strings.append(value)
else:
arg_strings.append(value)
return arg_strings
# Iterate over positional arguments
for dest in self._positional:
if dest in args:
arg_strings = append(arg_strings, args[dest])
# Iterate over optional arguments
for dest, opt in self._optional.items():
if dest in args:
arg_strings = append(arg_strings, args[dest], opt[0])
return self._parser.parse_args(arg_strings, namespace)
def _error(self, message):
# TODO: Raise a proper exception
raise MoulinetteError(1, message)
class _ActionsMapPlugin(object):
"""Actions map Bottle Plugin
@ -142,7 +190,7 @@ class _ActionsMapPlugin(object):
"""
# Retrieve session values
s_id = request.get_cookie('session.id') or random20()
s_id = request.get_cookie('session.id') or random_ascii()
try:
s_secret = self.secrets[s_id]
except KeyError:
@ -150,7 +198,7 @@ class _ActionsMapPlugin(object):
else:
s_hashes = request.get_cookie('session.hashes',
secret=s_secret) or {}
s_hash = random20()
s_hash = random_ascii()
try:
# Attempt to authenticate
@ -166,7 +214,7 @@ class _ActionsMapPlugin(object):
else:
# Update dicts with new values
s_hashes[profile] = s_hash
self.secrets[s_id] = s_secret = random20()
self.secrets[s_id] = s_secret = random_ascii()
response.set_cookie('session.id', s_id, secure=True)
response.set_cookie('session.hashes', s_hashes, secure=True,
@ -238,14 +286,137 @@ class _ActionsMapPlugin(object):
return authenticator(token=(s_id, s_hash))
class MoulinetteAPI(object):
"""Moulinette Application Programming Interface
# HTTP Responses -------------------------------------------------------
Initialize a HTTP server which serves the API to process moulinette
actions.
class HTTPOKResponse(HTTPResponse):
def __init__(self, output=''):
super(HTTPOKResponse, self).__init__(output, 200)
class HTTPBadRequestResponse(HTTPResponse):
def __init__(self, output=''):
super(HTTPBadRequestResponse, self).__init__(output, 400)
class HTTPUnauthorizedResponse(HTTPResponse):
def __init__(self, output=''):
super(HTTPUnauthorizedResponse, self).__init__(output, 401)
class HTTPErrorResponse(HTTPResponse):
def __init__(self, output=''):
super(HTTPErrorResponse, self).__init__(output, 500)
# API Classes Implementation -------------------------------------------
class ActionsMapParser(BaseActionsMapParser):
"""Actions map's Parser for the API
Provide actions map parsing methods for a CLI usage. The parser for
the arguments is represented by a argparse.ArgumentParser object.
"""
def __init__(self, shandler, parent=None):
super(ActionsMapParser, self).__init__(shandler, parent)
self._parsers = {} # dict({(method, path): _HTTPArgumentParser})
@property
def routes(self):
"""Get current routes"""
return self._parsers.keys()
## Implement virtual properties
name = 'api'
## Implement virtual methods
@staticmethod
def format_arg_names(name, full):
if name[0] != '-':
return [name]
if full:
return [full.replace('--', '@', 1)]
if name.startswith('--'):
return [name.replace('--', '@', 1)]
return [name.replace('-', '@', 1)]
def add_global_parser(self, **kwargs):
raise AttributeError("global arguments are not managed")
def add_category_parser(self, name, **kwargs):
return self
def add_action_parser(self, name, tid, api=None, **kwargs):
"""Add a parser for an action
Keyword arguments:
- api -- The action route (e.g. 'GET /' )
Returns:
A new _HTTPArgumentParser object for the route
"""
try:
# Validate action route
m = re.match('(GET|POST|PUT|DELETE) (/\S+)', api)
except TypeError:
raise AttributeError("the action '%s' doesn't provide api access" % name)
if not m:
# TODO: Log error
raise ValueError("the action '%s' doesn't provide api access" % name)
# Check if a parser already exists for the route
key = (m.group(1), m.group(2))
if key in self.routes:
raise AttributeError("a parser for '%s' already exists" % key)
# Create and append parser
parser = _HTTPArgumentParser()
self._parsers[key] = (tid, parser)
# Return the created parser
return parser
def parse_args(self, args, route, **kwargs):
"""Parse arguments
Keyword arguments:
- route -- The action route as a 2-tuple (method, path)
"""
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(tid, 'authenticate'):
auth_conf, klass = self.get_conf(tid, 'authenticator')
# TODO: Catch errors
auth = self.shandler.authenticate(klass(), **auth_conf)
if not auth.is_authenticated:
# TODO: Set proper error code
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 parser.parse_args(args, ret)
class Interface(BaseInterface):
"""Application Programming Interface for the moulinette
Initialize a HTTP server which serves the API connected to a given
actions map.
Keyword arguments:
- actionsmap -- The relevant ActionsMap instance
- actionsmap -- The ActionsMap instance to connect to
- routes -- A dict of additional routes to add in the form of
{(method, path): callback}

View file

@ -2,8 +2,10 @@
import errno
import getpass
import argparse
from moulinette.core import MoulinetteError
from moulinette.interfaces import (BaseActionsMapParser, BaseInterface)
# CLI helpers ----------------------------------------------------------
@ -59,16 +61,93 @@ def pretty_print_dict(d, depth=0):
print((" ") * depth + "%s: %s" % (str(k), v))
# Moulinette Interface -------------------------------------------------
# CLI Classes Implementation -------------------------------------------
class MoulinetteCLI(object):
"""Moulinette command-line Interface
class ActionsMapParser(BaseActionsMapParser):
"""Actions map's Parser for the CLI
Initialize an interface connected to the standard input and output
stream which allows to process moulinette actions.
Provide actions map parsing methods for a CLI usage. The parser for
the arguments is represented by a argparse.ArgumentParser object.
Keyword arguments:
- actionsmap -- The interface relevant ActionsMap instance
- parser -- The argparse.ArgumentParser object to use
"""
def __init__(self, shandler, parent=None, parser=None):
super(ActionsMapParser, self).__init__(shandler, parent)
self._parser = parser or argparse.ArgumentParser()
self._subparsers = self._parser.add_subparsers()
## Implement virtual properties
interface = 'cli'
## Implement virtual methods
@staticmethod
def format_arg_names(name, full):
if name[0] == '-' and full:
return [name, full]
return [name]
def add_global_parser(self, **kwargs):
return self._parser
def add_category_parser(self, name, category_help=None, **kwargs):
"""Add a parser for a category
Keyword arguments:
- category_help -- A brief description for the category
Returns:
A new ActionsMapParser object for the category
"""
parser = self._subparsers.add_parser(name, help=category_help)
return self.__class__(None, self, parser)
def add_action_parser(self, name, tid, action_help=None, **kwargs):
"""Add a parser for an action
Keyword arguments:
- action_help -- A brief description for the action
Returns:
A new argparse.ArgumentParser object for the action
"""
return self._subparsers.add_parser(name, help=action_help)
def parse_args(self, args, **kwargs):
ret = self._parser.parse_args(args)
# Perform authentication if needed
if self.get_conf(ret._tid, 'authenticate'):
auth_conf, klass = self.get_conf(ret._tid, 'authenticator')
# TODO: Catch errors
auth = self.shandler.authenticate(klass(), **auth_conf)
if not auth.is_authenticated:
# TODO: Set proper error code
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
return ret
class Interface(BaseInterface):
"""Command-line Interface for the moulinette
Initialize an interface connected to the standard input/output
stream and to a given actions map.
Keyword arguments:
- actionsmap -- The ActionsMap instance to connect to
"""
def __init__(self, actionsmap):