mirror of
https://github.com/YunoHost/moulinette.git
synced 2024-09-03 20:06:31 +02:00
Add support for Authenticators in the API
* Update the API to support authenticators and session token * Add a function to clean sessions * Make the API fit in better with actions map * Minor changes on test namespace
This commit is contained in:
parent
5da9f6add8
commit
3fae8bf1ff
7 changed files with 320 additions and 178 deletions
|
@ -41,5 +41,5 @@ if __name__ == '__main__':
|
||||||
# TODO: Add log argument
|
# TODO: Add log argument
|
||||||
|
|
||||||
# Rune the server
|
# Rune the server
|
||||||
api(['yunohost'], 6787, {('GET', '/installed'): is_installed}, use_cache)
|
api(['yunohost', 'test'], 6787, {('GET', '/installed'): is_installed}, use_cache)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
|
@ -19,6 +19,13 @@ _global:
|
||||||
parameters:
|
parameters:
|
||||||
uri: ldap://localhost:389
|
uri: ldap://localhost:389
|
||||||
base_dn: dc=yunohost,dc=org
|
base_dn: dc=yunohost,dc=org
|
||||||
|
test-profile:
|
||||||
|
vendor: ldap
|
||||||
|
help: Admin Password (profile)
|
||||||
|
parameters:
|
||||||
|
uri: ldap://localhost:389
|
||||||
|
base_dn: dc=yunohost,dc=org
|
||||||
|
user_rdn: cn=admin
|
||||||
argument_auth: true
|
argument_auth: true
|
||||||
|
|
||||||
#############################
|
#############################
|
||||||
|
@ -34,6 +41,11 @@ test:
|
||||||
api: GET /test/auth
|
api: GET /test/auth
|
||||||
configuration:
|
configuration:
|
||||||
authenticate: all
|
authenticate: all
|
||||||
|
auth-profile:
|
||||||
|
api: GET /test/auth-profile
|
||||||
|
configuration:
|
||||||
|
authenticate: all
|
||||||
|
authenticator: test-profile
|
||||||
auth-cli:
|
auth-cli:
|
||||||
api: GET /test/auth-cli
|
api: GET /test/auth-cli
|
||||||
configuration:
|
configuration:
|
||||||
|
|
|
@ -1,12 +1,19 @@
|
||||||
|
|
||||||
def test_non_auth():
|
def test_non_auth():
|
||||||
print('non-auth')
|
return {'action': 'non-auth'}
|
||||||
|
|
||||||
def test_auth(auth):
|
def test_auth(auth):
|
||||||
print('[default] / all / auth: %r' % auth)
|
return {'action': 'auth',
|
||||||
|
'authenticator': 'default', 'authenticate': 'all'}
|
||||||
|
|
||||||
|
def test_auth_profile(auth):
|
||||||
|
return {'action': 'auth-profile',
|
||||||
|
'authenticator': 'test-profile', 'authenticate': 'all'}
|
||||||
|
|
||||||
def test_auth_cli():
|
def test_auth_cli():
|
||||||
print('[default] / cli')
|
return {'action': 'auth-cli',
|
||||||
|
'authenticator': 'default', 'authenticate': ['cli']}
|
||||||
|
|
||||||
def test_anonymous():
|
def test_anonymous():
|
||||||
print('[ldap-anonymous] / all')
|
return {'action': 'anonymous',
|
||||||
|
'authenticator': 'ldap-anonymous', 'authenticate': 'all'}
|
||||||
|
|
|
@ -69,21 +69,20 @@ def api(namespaces, port, routes={}, use_cache=True):
|
||||||
|
|
||||||
Keyword arguments:
|
Keyword arguments:
|
||||||
- namespaces -- The list of namespaces to use
|
- namespaces -- The list of namespaces to use
|
||||||
- port -- Port to run on
|
- port -- Port number to run on
|
||||||
- routes -- A dict of additional routes to add in the form of
|
- routes -- A dict of additional routes to add in the form of
|
||||||
{(method, uri): callback}
|
{(method, uri): callback}
|
||||||
- use_cache -- False if it should parse the actions map file
|
- use_cache -- False if it should parse the actions map file
|
||||||
instead of using the cached one
|
instead of using the cached one
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from bottle import run
|
|
||||||
from .actionsmap import ActionsMap
|
from .actionsmap import ActionsMap
|
||||||
from .interface.api import MoulinetteAPI
|
from .interface.api import MoulinetteAPI
|
||||||
|
|
||||||
amap = ActionsMap('api', namespaces, use_cache)
|
amap = ActionsMap('api', namespaces, use_cache)
|
||||||
moulinette = MoulinetteAPI(amap, routes)
|
moulinette = MoulinetteAPI(amap, routes)
|
||||||
|
|
||||||
run(moulinette.app, port=port)
|
moulinette.run(port)
|
||||||
|
|
||||||
def cli(namespaces, args, use_cache=True):
|
def cli(namespaces, args, use_cache=True):
|
||||||
"""Command line interface
|
"""Command line interface
|
||||||
|
|
|
@ -48,7 +48,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):
|
def authenticate(self, authenticator, name, help, vendor=None):
|
||||||
"""Process the authentication
|
"""Process the authentication
|
||||||
|
|
||||||
Attempt to authenticate to the given authenticator and return
|
Attempt to authenticate to the given authenticator and return
|
||||||
|
@ -60,6 +60,7 @@ class _AMapSignals(object):
|
||||||
- authenticator -- The authenticator to use
|
- authenticator -- The authenticator to use
|
||||||
- name -- The authenticator name in the actions map
|
- 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
|
||||||
|
@ -227,14 +228,10 @@ class _AMapParser(object):
|
||||||
- profile -- The profile of the configuration
|
- profile -- The profile of the configuration
|
||||||
|
|
||||||
"""
|
"""
|
||||||
try:
|
|
||||||
if name == 'authenticator':
|
if name == 'authenticator':
|
||||||
value = self.global_conf[name][profile]
|
value = self.global_conf[name][profile]
|
||||||
else:
|
else:
|
||||||
value = self.global_conf[name]
|
value = self.global_conf[name]
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return self._format_conf(name, value)
|
return self._format_conf(name, value)
|
||||||
|
|
||||||
def set_global_conf(self, configuration):
|
def set_global_conf(self, configuration):
|
||||||
|
@ -366,13 +363,13 @@ class _AMapParser(object):
|
||||||
"""
|
"""
|
||||||
if name == 'authenticator' and value:
|
if name == 'authenticator' and value:
|
||||||
auth_conf, auth_params = value
|
auth_conf, auth_params = value
|
||||||
auth_vendor = auth_conf.pop('vendor')
|
|
||||||
|
|
||||||
# Return authenticator configuration and an instanciator for
|
# Return authenticator configuration and an instanciator for
|
||||||
# it as a 2-tuple
|
# it as a 2-tuple
|
||||||
return (auth_conf,
|
return (auth_conf,
|
||||||
lambda: init_authenticator(auth_conf['name'],
|
lambda: init_authenticator(auth_conf['name'],
|
||||||
auth_vendor, **auth_params))
|
auth_conf['vendor'],
|
||||||
|
**auth_params))
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
@ -486,7 +483,7 @@ class _HTTPArgumentParser(object):
|
||||||
|
|
||||||
return action
|
return action
|
||||||
|
|
||||||
def parse_args(self, args):
|
def parse_args(self, args={}, namespace=None):
|
||||||
arg_strings = []
|
arg_strings = []
|
||||||
|
|
||||||
## Append an argument to the current one
|
## Append an argument to the current one
|
||||||
|
@ -514,7 +511,7 @@ class _HTTPArgumentParser(object):
|
||||||
for dest, opt in self._optional.items():
|
for dest, opt in self._optional.items():
|
||||||
if dest in args:
|
if dest in args:
|
||||||
arg_strings = append(arg_strings, args[dest], opt[0])
|
arg_strings = append(arg_strings, args[dest], opt[0])
|
||||||
return self._parser.parse_args(arg_strings)
|
return self._parser.parse_args(arg_strings, namespace)
|
||||||
|
|
||||||
def _error(self, message):
|
def _error(self, message):
|
||||||
# TODO: Raise a proper exception
|
# TODO: Raise a proper exception
|
||||||
|
@ -596,16 +593,28 @@ class APIAMapParser(_AMapParser):
|
||||||
"""Parse arguments
|
"""Parse arguments
|
||||||
|
|
||||||
Keyword arguments:
|
Keyword arguments:
|
||||||
- route -- The action route (e.g. 'GET /' )
|
- route -- The action route as a 2-tuple (method, path)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Retrieve the parser for the route
|
# Retrieve the parser for the route
|
||||||
if route not in self.routes:
|
if route not in self.routes:
|
||||||
raise MoulinetteError(22, "No parser for '%s %s' found" % key)
|
raise MoulinetteError(22, "No parser for '%s %s' found" % key)
|
||||||
|
ret = argparse.Namespace()
|
||||||
|
|
||||||
# TODO: Implement authentication
|
# Perform authentication if needed
|
||||||
|
if self.get_conf(route, 'authenticate'):
|
||||||
|
auth_conf, klass = self.get_conf(route, 'authenticator')
|
||||||
|
|
||||||
return self._parsers[route].parse_args(args)
|
# 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':
|
||||||
|
ret.auth = auth
|
||||||
|
|
||||||
|
return self._parsers[route].parse_args(args, ret)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
The dict of interfaces names and their associated parser class.
|
The dict of interfaces names and their associated parser class.
|
||||||
|
@ -906,6 +915,26 @@ class ActionsMap(object):
|
||||||
"""Return the instance of the interface's actions map parser"""
|
"""Return the instance of the interface's actions map parser"""
|
||||||
return self._parser
|
return self._parser
|
||||||
|
|
||||||
|
def get_authenticator(self, profile='default'):
|
||||||
|
"""Get an authenticator instance
|
||||||
|
|
||||||
|
Retrieve the authenticator for the given profile and return a
|
||||||
|
new instance.
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
- profile -- An authenticator profile name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A new _BaseAuthenticator derived instance
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
auth = self.parser.get_global_conf('authenticator', profile)[1]
|
||||||
|
except KeyError:
|
||||||
|
raise MoulinetteError(167, _("Unknown authenticator profile '%s'") % profile)
|
||||||
|
else:
|
||||||
|
return auth()
|
||||||
|
|
||||||
def connect(self, signal, handler):
|
def connect(self, signal, handler):
|
||||||
"""Connect a signal to a handler
|
"""Connect a signal to a handler
|
||||||
|
|
||||||
|
|
|
@ -316,6 +316,17 @@ def init_authenticator(_name, _vendor, **kwargs):
|
||||||
if _vendor == 'ldap':
|
if _vendor == 'ldap':
|
||||||
return LDAPAuthenticator(name=_name, **kwargs)
|
return LDAPAuthenticator(name=_name, **kwargs)
|
||||||
|
|
||||||
|
def clean_session(session_id, profiles=[]):
|
||||||
|
sessiondir = pkg.get_cachedir('session')
|
||||||
|
if len(profiles) == 0:
|
||||||
|
profiles = os.listdir(sessiondir)
|
||||||
|
|
||||||
|
for p in profiles:
|
||||||
|
try:
|
||||||
|
os.unlink(os.path.join(sessiondir, p, '%s.asc' % session_id))
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
# Moulinette core classes ----------------------------------------------
|
# Moulinette core classes ----------------------------------------------
|
||||||
|
|
||||||
|
|
|
@ -1,147 +1,118 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from bottle import Bottle, request, response, HTTPResponse
|
from bottle import run, request, response, Bottle, HTTPResponse
|
||||||
from beaker.middleware import SessionMiddleware
|
from json import dumps as json_encode
|
||||||
|
|
||||||
from ..core import MoulinetteError
|
from ..core import MoulinetteError, clean_session
|
||||||
from ..helpers import YunoHostError, YunoHostLDAP
|
from ..helpers import YunoHostError, YunoHostLDAP
|
||||||
|
|
||||||
|
# API helpers ----------------------------------------------------------
|
||||||
|
|
||||||
|
import os
|
||||||
|
import binascii
|
||||||
|
|
||||||
|
def random20():
|
||||||
|
return binascii.hexlify(os.urandom(20)).decode('ascii')
|
||||||
|
|
||||||
|
|
||||||
|
# HTTP Responses -------------------------------------------------------
|
||||||
|
|
||||||
|
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 moulinette interface ---------------------------------------------
|
# API moulinette interface ---------------------------------------------
|
||||||
|
|
||||||
class _APIAuthPlugin(object):
|
|
||||||
"""
|
|
||||||
Manage the authentication for the API access.
|
|
||||||
|
|
||||||
"""
|
|
||||||
name = 'apiauth'
|
|
||||||
api = 2
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
# TODO: Add options (e.g. session type, content type, ...)
|
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
|
||||||
def app(self):
|
|
||||||
"""Get Bottle application with session integration"""
|
|
||||||
if hasattr(self, '_app'):
|
|
||||||
return self._app
|
|
||||||
raise Exception(_("The APIAuth Plugin is not installed yet."))
|
|
||||||
|
|
||||||
def setup(self, app):
|
|
||||||
"""
|
|
||||||
Setup the plugin and install the session into the app
|
|
||||||
|
|
||||||
Keyword argument:
|
|
||||||
app -- The associated application object
|
|
||||||
|
|
||||||
"""
|
|
||||||
app.route('/login', name='login', method='POST', callback=self.login)
|
|
||||||
app.route('/logout', name='logout', method='GET', callback=self.logout)
|
|
||||||
|
|
||||||
session_opts = {
|
|
||||||
'session.type': 'file',
|
|
||||||
'session.cookie_expires': True,
|
|
||||||
'session.data_dir': pkg.get_cachedir('session'),
|
|
||||||
'session.secure': True
|
|
||||||
}
|
|
||||||
self._app = SessionMiddleware(app, session_opts)
|
|
||||||
|
|
||||||
def apply(self, callback, context):
|
|
||||||
"""
|
|
||||||
Check authentication before executing the route callback
|
|
||||||
|
|
||||||
Keyword argument:
|
|
||||||
callback -- The route callback
|
|
||||||
context -- An instance of Route
|
|
||||||
|
|
||||||
"""
|
|
||||||
# Check the authentication
|
|
||||||
if self._is_authenticated:
|
|
||||||
if context.name == 'login':
|
|
||||||
self.logout()
|
|
||||||
else:
|
|
||||||
return callback
|
|
||||||
|
|
||||||
# Process login route
|
|
||||||
if context.name == 'login':
|
|
||||||
password = request.POST.get('password', None)
|
|
||||||
if password is not None and self.login(password):
|
|
||||||
raise HTTPResponse(status=200)
|
|
||||||
else:
|
|
||||||
raise HTTPResponse(_("Wrong password"), 401)
|
|
||||||
|
|
||||||
# Deny access to the requested route
|
|
||||||
raise HTTPResponse(_("Unauthorized"), 401)
|
|
||||||
|
|
||||||
def login(self, password):
|
|
||||||
"""
|
|
||||||
Attempt to log in with the given password
|
|
||||||
|
|
||||||
Keyword argument:
|
|
||||||
password -- Cleartext password
|
|
||||||
|
|
||||||
"""
|
|
||||||
try: YunoHostLDAP(password=password)
|
|
||||||
except YunoHostError:
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
session = self._beaker_session
|
|
||||||
session['authenticated'] = True
|
|
||||||
session.save()
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def logout(self):
|
|
||||||
"""
|
|
||||||
Log out and delete the session
|
|
||||||
|
|
||||||
"""
|
|
||||||
# TODO: Delete the cached session file
|
|
||||||
session = self._beaker_session
|
|
||||||
session.delete()
|
|
||||||
|
|
||||||
|
|
||||||
## Private methods
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _beaker_session(self):
|
|
||||||
"""Get Beaker session"""
|
|
||||||
return request.environ.get('beaker.session')
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _is_authenticated(self):
|
|
||||||
"""Check authentication"""
|
|
||||||
# TODO: Clear the session path on password changing to avoid invalid access
|
|
||||||
if 'authenticated' in self._beaker_session:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
class _ActionsMapPlugin(object):
|
class _ActionsMapPlugin(object):
|
||||||
"""
|
"""Actions map Bottle Plugin
|
||||||
Process action for the request using the actions map.
|
|
||||||
|
Process relevant action for the request using the actions map and
|
||||||
|
manage authentication.
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
- actionsmap -- An ActionsMap instance
|
||||||
|
|
||||||
"""
|
"""
|
||||||
name = 'actionsmap'
|
name = 'actionsmap'
|
||||||
api = 2
|
api = 2
|
||||||
|
|
||||||
def __init__(self, actionsmap):
|
def __init__(self, actionsmap):
|
||||||
|
# Connect signals to handlers
|
||||||
|
actionsmap.connect('authenticate', self._do_authenticate)
|
||||||
|
|
||||||
self.actionsmap = actionsmap
|
self.actionsmap = actionsmap
|
||||||
|
# TODO: Save and load secrets?
|
||||||
|
self.secrets = {}
|
||||||
|
|
||||||
def setup(self, app):
|
def setup(self, app):
|
||||||
|
"""Setup plugin on the application
|
||||||
|
|
||||||
|
Add routes according to the actions map to the application.
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
- app -- The application instance
|
||||||
|
|
||||||
|
"""
|
||||||
|
## Login wrapper
|
||||||
|
def _login(callback):
|
||||||
|
def wrapper():
|
||||||
|
kwargs = {}
|
||||||
|
try:
|
||||||
|
kwargs['password'] = request.POST['password']
|
||||||
|
except KeyError:
|
||||||
|
raise HTTPBadRequestResponse(_("Missing password parameter"))
|
||||||
|
try:
|
||||||
|
kwargs['profile'] = request.POST['profile']
|
||||||
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
return callback(**kwargs)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
## Logout wrapper
|
||||||
|
def _logout(callback):
|
||||||
|
def wrapper():
|
||||||
|
kwargs = {}
|
||||||
|
try:
|
||||||
|
kwargs['profile'] = request.POST.get('profile')
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
return callback(**kwargs)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
# Append authentication routes
|
||||||
|
app.route('/login', name='login', method='POST',
|
||||||
|
callback=self.login, skip=['actionsmap'], apply=_login)
|
||||||
|
app.route('/logout', name='logout', method='GET',
|
||||||
|
callback=self.logout, skip=['actionsmap'], apply=_logout)
|
||||||
|
|
||||||
|
# Append routes from the actions map
|
||||||
|
for (m, p) in self.actionsmap.parser.routes:
|
||||||
|
app.route(p, method=m, callback=self.process)
|
||||||
|
|
||||||
def apply(self, callback, context):
|
def apply(self, callback, context):
|
||||||
"""
|
"""Apply plugin to the route callback
|
||||||
Process the relevant action for the request
|
|
||||||
|
|
||||||
Keyword argument:
|
Install a wrapper which replace callback and process the
|
||||||
|
relevant action for the route.
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
callback -- The route callback
|
callback -- The route callback
|
||||||
context -- An instance of Route
|
context -- An instance of Route
|
||||||
|
|
||||||
"""
|
"""
|
||||||
method = request.method
|
|
||||||
uri = context.rule
|
|
||||||
|
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
# Bring arguments together
|
# Bring arguments together
|
||||||
params = kwargs
|
params = kwargs
|
||||||
|
@ -151,14 +122,127 @@ class _ActionsMapPlugin(object):
|
||||||
params[k] = v
|
params[k] = v
|
||||||
|
|
||||||
# Process the action
|
# Process the action
|
||||||
return self.actionsmap.process(params, route=(method, uri))
|
return callback((request.method, context.rule), params)
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
class MoulinetteAPI(object):
|
## Routes callbacks
|
||||||
|
|
||||||
|
def login(self, password, profile='default'):
|
||||||
|
"""Log in to an authenticator profile
|
||||||
|
|
||||||
|
Attempt to authenticate to a given authenticator profile and
|
||||||
|
register it with the current session - a new one will be created
|
||||||
|
if needed.
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
- password -- A clear text password
|
||||||
|
- profile -- The authenticator profile name to log in
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Initialize a HTTP server which serves the API to access to the
|
# Retrieve session values
|
||||||
moulinette actions.
|
s_id = request.get_cookie('session.id') or random20()
|
||||||
|
try:
|
||||||
|
s_secret = self.secrets[s_id]
|
||||||
|
except KeyError:
|
||||||
|
s_hashes = {}
|
||||||
|
else:
|
||||||
|
s_hashes = request.get_cookie('session.hashes',
|
||||||
|
secret=s_secret) or {}
|
||||||
|
s_hash = random20()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Attempt to authenticate
|
||||||
|
auth = self.actionsmap.get_authenticator(profile)
|
||||||
|
auth(password, token=(s_id, s_hash))
|
||||||
|
except MoulinetteError as e:
|
||||||
|
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)
|
||||||
|
else:
|
||||||
|
# Update dicts with new values
|
||||||
|
s_hashes[profile] = s_hash
|
||||||
|
self.secrets[s_id] = s_secret = random20()
|
||||||
|
|
||||||
|
response.set_cookie('session.id', s_id, secure=True)
|
||||||
|
response.set_cookie('session.hashes', s_hashes, secure=True,
|
||||||
|
secret=s_secret)
|
||||||
|
raise HTTPOKResponse()
|
||||||
|
|
||||||
|
def logout(self, profile=None):
|
||||||
|
"""Log out from an authenticator profile
|
||||||
|
|
||||||
|
Attempt to unregister a given profile - or all by default - from
|
||||||
|
the current session.
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
- profile -- The authenticator profile name to log out
|
||||||
|
|
||||||
|
"""
|
||||||
|
s_id = request.get_cookie('session.id')
|
||||||
|
try:
|
||||||
|
del self.secrets[s_id]
|
||||||
|
except KeyError:
|
||||||
|
raise HTTPUnauthorizedResponse(_("You are not logged in"))
|
||||||
|
else:
|
||||||
|
# TODO: Clean the session for profile only
|
||||||
|
# Delete cookie and clean the session
|
||||||
|
response.set_cookie('session.hashes', '', max_age=-1)
|
||||||
|
clean_session(s_id)
|
||||||
|
raise HTTPOKResponse()
|
||||||
|
|
||||||
|
def process(self, _route, arguments={}):
|
||||||
|
"""Process the relevant action for the route
|
||||||
|
|
||||||
|
Call the actions map in order to process the relevant action for
|
||||||
|
the route with the given arguments and process the returned
|
||||||
|
value.
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
- _route -- The action route as a 2-tuple (method, path)
|
||||||
|
- arguments -- A dict of arguments for the route
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
ret = self.actionsmap.process(arguments, route=_route)
|
||||||
|
except MoulinetteError as e:
|
||||||
|
raise HTTPErrorResponse(e.message)
|
||||||
|
else:
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
## Signals handlers
|
||||||
|
|
||||||
|
def _do_authenticate(self, authenticator, name, help):
|
||||||
|
"""Process the authentication
|
||||||
|
|
||||||
|
Handle the actionsmap._AMapSignals.authenticate signal.
|
||||||
|
|
||||||
|
"""
|
||||||
|
s_id = request.get_cookie('session.id')
|
||||||
|
try:
|
||||||
|
s_secret = self.secrets[s_id]
|
||||||
|
s_hash = request.get_cookie('session.hashes',
|
||||||
|
secret=s_secret)[name]
|
||||||
|
except KeyError:
|
||||||
|
if name == 'default':
|
||||||
|
msg = _("Needing authentication")
|
||||||
|
else:
|
||||||
|
msg = _("Needing authentication to profile '%s'") % name
|
||||||
|
raise HTTPUnauthorizedResponse(msg)
|
||||||
|
else:
|
||||||
|
return authenticator(token=(s_id, s_hash))
|
||||||
|
|
||||||
|
|
||||||
|
class MoulinetteAPI(object):
|
||||||
|
"""Moulinette Application Programming Interface
|
||||||
|
|
||||||
|
Initialize a HTTP server which serves the API to process moulinette
|
||||||
|
actions.
|
||||||
|
|
||||||
Keyword arguments:
|
Keyword arguments:
|
||||||
- actionsmap -- The relevant ActionsMap instance
|
- actionsmap -- The relevant ActionsMap instance
|
||||||
|
@ -166,43 +250,47 @@ class MoulinetteAPI(object):
|
||||||
{(method, path): callback}
|
{(method, path): callback}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, actionsmap, routes={}):
|
def __init__(self, actionsmap, routes={}):
|
||||||
# Initialize app and default routes
|
|
||||||
# TODO: Return OK to 'OPTIONS' xhr requests (l173)
|
# TODO: Return OK to 'OPTIONS' xhr requests (l173)
|
||||||
app = Bottle()
|
app = Bottle(autojson=False)
|
||||||
app.route(['/api', '/api/<category:re:[a-z]+>'], method='GET',
|
|
||||||
callback=self.doc, skip=['apiauth'])
|
|
||||||
|
|
||||||
# Append routes from the actions map
|
## Wrapper which sets proper header
|
||||||
amap = _ActionsMapPlugin(actionsmap)
|
|
||||||
for (m, p) in actionsmap.parser.routes:
|
|
||||||
app.route(p, method=m, callback=self._error, apply=amap)
|
|
||||||
|
|
||||||
# Append additional routes
|
|
||||||
# TODO: Add an option to skip auth for the route
|
|
||||||
for (m, p), c in routes.items():
|
|
||||||
app.route(p, method=m, callback=c)
|
|
||||||
|
|
||||||
# Define and install a plugin which sets proper header
|
|
||||||
def apiheader(callback):
|
def apiheader(callback):
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
response.content_type = 'application/json'
|
response.content_type = 'application/json'
|
||||||
response.set_header('Access-Control-Allow-Origin', '*')
|
response.set_header('Access-Control-Allow-Origin', '*')
|
||||||
return callback(*args, **kwargs)
|
return json_encode(callback(*args, **kwargs))
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
# Install plugins
|
||||||
app.install(apiheader)
|
app.install(apiheader)
|
||||||
|
app.install(_ActionsMapPlugin(actionsmap))
|
||||||
|
|
||||||
# Install authentication plugin
|
# Append default routes
|
||||||
apiauth = _APIAuthPlugin()
|
app.route(['/api', '/api/<category:re:[a-z]+>'], method='GET',
|
||||||
app.install(apiauth)
|
callback=self.doc, skip=['actionsmap'])
|
||||||
|
|
||||||
self._app = apiauth.app
|
# Append additional routes
|
||||||
|
# TODO: Add optional authentication to those routes?
|
||||||
|
for (m, p), c in routes.items():
|
||||||
|
app.route(p, method=m, callback=c, skip=['actionsmap'])
|
||||||
|
|
||||||
@property
|
self._app = app
|
||||||
def app(self):
|
|
||||||
"""Get Bottle application"""
|
def run(self, _port):
|
||||||
return self._app
|
"""Run the moulinette
|
||||||
|
|
||||||
|
Start a server instance on the given port to serve moulinette
|
||||||
|
actions.
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
- _port -- Port number to run on
|
||||||
|
|
||||||
|
"""
|
||||||
|
run(self._app, port=_port)
|
||||||
|
|
||||||
|
|
||||||
|
## Routes handlers
|
||||||
|
|
||||||
def doc(self, category=None):
|
def doc(self, category=None):
|
||||||
"""
|
"""
|
||||||
|
@ -220,8 +308,4 @@ class MoulinetteAPI(object):
|
||||||
with open('%s/../doc/%s.json' % (pkg.datadir, category)) as f:
|
with open('%s/../doc/%s.json' % (pkg.datadir, category)) as f:
|
||||||
return f.read()
|
return f.read()
|
||||||
except IOError:
|
except IOError:
|
||||||
return 'unknown'
|
return None
|
||||||
|
|
||||||
def _error(self, *args, **kwargs):
|
|
||||||
# TODO: Raise or return an error
|
|
||||||
print('error')
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue