mirror of
https://github.com/YunoHost/moulinette.git
synced 2024-09-03 20:06:31 +02:00
Rework and externalize the authenticator system
This commit is contained in:
parent
f7199f7a64
commit
af0957c028
8 changed files with 83 additions and 608 deletions
|
@ -465,38 +465,32 @@ class ActionsMap(object):
|
|||
self.extraparser = ExtraArgumentParser(top_parser.interface)
|
||||
self.parser = self._construct_parser(actionsmaps, top_parser)
|
||||
|
||||
def get_authenticator_for_profile(self, auth_profile):
|
||||
def get_authenticator(self, auth_method):
|
||||
|
||||
# Fetch the configuration for the authenticator module as defined in the actionmap
|
||||
try:
|
||||
auth_conf = self.parser.global_conf["authenticator"][auth_profile]
|
||||
except KeyError:
|
||||
raise ValueError("Unknown authenticator profile '%s'" % auth_profile)
|
||||
if auth_method == "default":
|
||||
auth_method = self.default_authentication
|
||||
|
||||
# Load and initialize the authenticator module
|
||||
auth_module = "%s.authenticators.%s" % (self.main_namespace, auth_method)
|
||||
logger.debug(f"Loading auth module {auth_module}")
|
||||
try:
|
||||
mod = import_module("moulinette.authenticators.%s" % auth_conf["vendor"])
|
||||
except ImportError:
|
||||
error_message = (
|
||||
"unable to load authenticator vendor module 'moulinette.authenticators.%s'"
|
||||
% auth_conf["vendor"]
|
||||
)
|
||||
logger.exception(error_message)
|
||||
raise MoulinetteError(error_message, raw_msg=True)
|
||||
mod = import_module(auth_module)
|
||||
except ImportError as e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
raise MoulinetteError(f"unable to load authenticator {auth_module} : {e}", raw_msg=True)
|
||||
else:
|
||||
return mod.Authenticator(**auth_conf)
|
||||
return mod.Authenticator()
|
||||
|
||||
def check_authentication_if_required(self, args, **kwargs):
|
||||
|
||||
auth_profile = self.parser.auth_required(args, **kwargs)
|
||||
auth_method = self.parser.auth_method(args, **kwargs)
|
||||
|
||||
if not auth_profile:
|
||||
if auth_method is None:
|
||||
return
|
||||
|
||||
authenticator = self.get_authenticator_for_profile(auth_profile)
|
||||
auth = msignals.authenticate(authenticator)
|
||||
|
||||
if not auth.is_authenticated:
|
||||
authenticator = self.get_authenticator(auth_method)
|
||||
if not msignals.authenticate(authenticator):
|
||||
raise MoulinetteError("authentication_required_long")
|
||||
|
||||
def process(self, args, timeout=None, **kwargs):
|
||||
|
@ -681,6 +675,8 @@ class ActionsMap(object):
|
|||
logger.debug("building parser...")
|
||||
start = time()
|
||||
|
||||
interface_type = top_parser.interface
|
||||
|
||||
# If loading from cache, extra were already checked when cache was
|
||||
# loaded ? Not sure about this ... old code is a bit mysterious...
|
||||
validate_extra = not self.from_cache
|
||||
|
@ -694,25 +690,26 @@ class ActionsMap(object):
|
|||
# Retrieve global parameters
|
||||
_global = actionsmap.pop("_global", {})
|
||||
|
||||
# Set the global configuration to use for the parser.
|
||||
top_parser.set_global_conf(_global["configuration"])
|
||||
if _global:
|
||||
if getattr(self, "main_namespace", None) is not None:
|
||||
raise MoulinetteError("It's not possible to have several namespaces with a _global section")
|
||||
else:
|
||||
self.main_namespace = namespace
|
||||
self.default_authentication = _global["authentication"][interface_type]
|
||||
|
||||
if top_parser.has_global_parser():
|
||||
top_parser.add_global_arguments(_global["arguments"])
|
||||
|
||||
if not hasattr(self, "main_namespace"):
|
||||
raise MoulinetteError("Did not found the main namespace")
|
||||
|
||||
for namespace, actionsmap in actionsmaps.items():
|
||||
# category_name is stuff like "user", "domain", "hooks"...
|
||||
# category_values is the values of this category (like actions)
|
||||
for category_name, category_values in actionsmap.items():
|
||||
|
||||
if "actions" in category_values:
|
||||
actions = category_values.pop("actions")
|
||||
else:
|
||||
actions = {}
|
||||
|
||||
if "subcategories" in category_values:
|
||||
subcategories = category_values.pop("subcategories")
|
||||
else:
|
||||
subcategories = {}
|
||||
actions = category_values.pop("actions", {})
|
||||
subcategories = category_values.pop("subcategories", {})
|
||||
|
||||
# Get category parser
|
||||
category_parser = top_parser.add_category_parser(
|
||||
|
@ -723,6 +720,7 @@ class ActionsMap(object):
|
|||
# action_options are the values
|
||||
for action_name, action_options in actions.items():
|
||||
arguments = action_options.pop("arguments", {})
|
||||
authentication = action_options.pop("authentication", {})
|
||||
tid = (namespace, category_name, action_name)
|
||||
|
||||
# Get action parser
|
||||
|
@ -742,8 +740,9 @@ class ActionsMap(object):
|
|||
validate_extra=validate_extra,
|
||||
)
|
||||
|
||||
if "configuration" in action_options:
|
||||
category_parser.set_conf(tid, action_options["configuration"])
|
||||
action_parser.authentication = self.default_authentication
|
||||
if interface_type in authentication:
|
||||
action_parser.authentication = authentication[interface_type]
|
||||
|
||||
# subcategory_name is like "cert" in "domain cert status"
|
||||
# subcategory_values is the values of this subcategory (like actions)
|
||||
|
@ -760,6 +759,7 @@ class ActionsMap(object):
|
|||
# action_options are the values
|
||||
for action_name, action_options in actions.items():
|
||||
arguments = action_options.pop("arguments", {})
|
||||
authentication = action_options.pop("authentication", {})
|
||||
tid = (namespace, category_name, subcategory_name, action_name)
|
||||
|
||||
try:
|
||||
|
@ -780,10 +780,9 @@ class ActionsMap(object):
|
|||
validate_extra=validate_extra,
|
||||
)
|
||||
|
||||
if "configuration" in action_options:
|
||||
category_parser.set_conf(
|
||||
tid, action_options["configuration"]
|
||||
)
|
||||
action_parser.authentication = self.default_authentication
|
||||
if interface_type in authentication:
|
||||
action_parser.authentication = authentication[interface_type]
|
||||
|
||||
logger.debug("building parser took %.3fs", time() - start)
|
||||
return top_parser
|
||||
|
|
|
@ -27,28 +27,8 @@ class BaseAuthenticator(object):
|
|||
must be given on instantiation - with the corresponding vendor
|
||||
configuration of the authenticator.
|
||||
|
||||
Keyword arguments:
|
||||
- name -- The authenticator profile name
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, name, vendor, parameters, extra):
|
||||
self._name = name
|
||||
self.vendor = vendor
|
||||
self.is_authenticated = False
|
||||
self.extra = extra
|
||||
|
||||
@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
|
||||
|
||||
# Virtual methods
|
||||
# Each authenticator classes must implement these methods.
|
||||
|
||||
|
@ -82,12 +62,12 @@ class BaseAuthenticator(object):
|
|||
- 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
|
||||
|
||||
if hasattr(self, "is_authenticated"):
|
||||
return self.is_authenticated
|
||||
|
||||
is_authenticated = False
|
||||
|
||||
#
|
||||
# Authenticate using the password
|
||||
|
@ -99,15 +79,10 @@ class BaseAuthenticator(object):
|
|||
except MoulinetteError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
"authentication (name: '%s', vendor: '%s') fails because '%s'",
|
||||
self.name,
|
||||
self.vendor,
|
||||
e,
|
||||
)
|
||||
logger.exception("authentication {self.name} failed because '{e}'")
|
||||
raise MoulinetteError("unable_authenticate")
|
||||
|
||||
self.is_authenticated = True
|
||||
else:
|
||||
is_authenticated = True
|
||||
|
||||
# Store session for later using the provided (new) token if any
|
||||
if token:
|
||||
|
@ -133,15 +108,10 @@ class BaseAuthenticator(object):
|
|||
except MoulinetteError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
"authentication (name: '%s', vendor: '%s') fails because '%s'",
|
||||
self.name,
|
||||
self.vendor,
|
||||
e,
|
||||
)
|
||||
logger.exception("authentication {self.name} failed because '{e}'")
|
||||
raise MoulinetteError("unable_authenticate")
|
||||
else:
|
||||
self.is_authenticated = True
|
||||
is_authenticated = True
|
||||
|
||||
#
|
||||
# No credentials given, can't authenticate
|
||||
|
@ -149,7 +119,8 @@ class BaseAuthenticator(object):
|
|||
else:
|
||||
raise MoulinetteError("unable_authenticate")
|
||||
|
||||
return self
|
||||
self.is_authenticated = is_authenticated
|
||||
return is_authenticated
|
||||
|
||||
# Private methods
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
from moulinette.core import MoulinetteError
|
||||
from moulinette.authenticators import BaseAuthenticator
|
||||
|
||||
logger = logging.getLogger("moulinette.authenticator.dummy")
|
||||
|
||||
# Dummy authenticator implementation
|
||||
|
||||
|
||||
class Authenticator(BaseAuthenticator):
|
||||
|
||||
"""Dummy authenticator used for tests"""
|
||||
|
||||
vendor = "dummy"
|
||||
|
||||
def __init__(self, name, vendor, parameters, extra):
|
||||
logger.debug("initialize authenticator dummy")
|
||||
|
||||
super(Authenticator, self).__init__(name, vendor, parameters, extra)
|
||||
|
||||
def authenticate(self, password=None):
|
||||
|
||||
if not password == self.name:
|
||||
raise MoulinetteError("invalid_password")
|
||||
|
||||
return self
|
|
@ -1,311 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# TODO: Use Python3 to remove this fix!
|
||||
from __future__ import absolute_import
|
||||
import os
|
||||
import logging
|
||||
import ldap
|
||||
import ldap.sasl
|
||||
import time
|
||||
import ldap.modlist as modlist
|
||||
|
||||
from moulinette import m18n
|
||||
from moulinette.core import MoulinetteError, MoulinetteLdapIsDownError
|
||||
from moulinette.authenticators import BaseAuthenticator
|
||||
|
||||
logger = logging.getLogger("moulinette.authenticator.ldap")
|
||||
|
||||
# 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, vendor, parameters, extra):
|
||||
self.uri = parameters["uri"]
|
||||
self.basedn = parameters["base_dn"]
|
||||
self.userdn = parameters["user_rdn"]
|
||||
self.extra = extra
|
||||
self.sasldn = "cn=external,cn=auth"
|
||||
self.adminuser = "admin"
|
||||
self.admindn = "cn=%s,dc=yunohost,dc=org" % self.adminuser
|
||||
self.admindn = "cn=%s,dc=yunohost,dc=org" % self.adminuser
|
||||
logger.debug(
|
||||
"initialize authenticator '%s' with: uri='%s', "
|
||||
"base_dn='%s', user_rdn='%s'",
|
||||
name,
|
||||
self._get_uri(),
|
||||
self.basedn,
|
||||
self.userdn,
|
||||
)
|
||||
super(Authenticator, self).__init__(name, vendor, parameters, extra)
|
||||
|
||||
if self.userdn and self.sasldn in self.userdn:
|
||||
self.authenticate(None)
|
||||
else:
|
||||
self.con = None
|
||||
|
||||
def __del__(self):
|
||||
"""Disconnect and free ressources"""
|
||||
if hasattr(self, "con") and self.con:
|
||||
self.con.unbind_s()
|
||||
|
||||
# Implement virtual properties
|
||||
|
||||
vendor = "ldap"
|
||||
|
||||
# Implement virtual methods
|
||||
|
||||
def authenticate(self, password=None):
|
||||
def _reconnect():
|
||||
con = ldap.ldapobject.ReconnectLDAPObject(
|
||||
self._get_uri(), retry_max=10, retry_delay=0.5
|
||||
)
|
||||
if self.userdn:
|
||||
if self.sasldn in self.userdn:
|
||||
con.sasl_non_interactive_bind_s("EXTERNAL")
|
||||
else:
|
||||
con.simple_bind_s(self.userdn, password)
|
||||
else:
|
||||
con.simple_bind_s()
|
||||
|
||||
return con
|
||||
|
||||
try:
|
||||
con = _reconnect()
|
||||
except ldap.INVALID_CREDENTIALS:
|
||||
raise MoulinetteError("invalid_password")
|
||||
except ldap.SERVER_DOWN:
|
||||
# ldap is down, attempt to restart it before really failing
|
||||
logger.warning(m18n.g("ldap_server_is_down_restart_it"))
|
||||
os.system("systemctl restart slapd")
|
||||
time.sleep(10) # waits 10 secondes so we are sure that slapd has restarted
|
||||
|
||||
try:
|
||||
con = _reconnect()
|
||||
except ldap.SERVER_DOWN:
|
||||
raise MoulinetteLdapIsDownError("ldap_server_down")
|
||||
|
||||
# Check that we are indeed logged in with the right identity
|
||||
try:
|
||||
# whoami_s return dn:..., then delete these 3 characters
|
||||
who = con.whoami_s()[3:]
|
||||
except Exception as e:
|
||||
logger.warning("Error during ldap authentication process: %s", e)
|
||||
raise
|
||||
else:
|
||||
# FIXME: During SASL bind whoami from the test server return the admindn while userdn is returned normally :
|
||||
if not (who == self.admindn or who == self.userdn):
|
||||
raise MoulinetteError("Not logged in with the expected userdn ?!")
|
||||
else:
|
||||
self.con = con
|
||||
|
||||
# Additional LDAP methods
|
||||
# TODO: Review these methods
|
||||
|
||||
def search(self, base=None, filter="(objectClass=*)", attrs=["dn"]):
|
||||
"""Search in LDAP base
|
||||
|
||||
Perform an LDAP search operation with given arguments and return
|
||||
results as a list.
|
||||
|
||||
Keyword arguments:
|
||||
- base -- The dn to search into
|
||||
- filter -- A string representation of the filter to apply
|
||||
- attrs -- A list of attributes to fetch
|
||||
|
||||
Returns:
|
||||
A list of all results
|
||||
|
||||
"""
|
||||
if not base:
|
||||
base = self.basedn
|
||||
|
||||
try:
|
||||
result = self.con.search_s(base, ldap.SCOPE_SUBTREE, filter, attrs)
|
||||
except Exception as e:
|
||||
raise MoulinetteError(
|
||||
"error during LDAP search operation with: base='%s', "
|
||||
"filter='%s', attrs=%s and exception %s" % (base, filter, attrs, e),
|
||||
raw_msg=True,
|
||||
)
|
||||
|
||||
result_list = []
|
||||
if not attrs or "dn" not in attrs:
|
||||
result_list = [entry for dn, entry in result]
|
||||
else:
|
||||
for dn, entry in result:
|
||||
entry["dn"] = [dn]
|
||||
result_list.append(entry)
|
||||
|
||||
def decode(value):
|
||||
if isinstance(value, bytes):
|
||||
value = value.decode("utf-8")
|
||||
return value
|
||||
|
||||
# result_list is for example :
|
||||
# [{'virtualdomain': [b'test.com']}, {'virtualdomain': [b'yolo.test']},
|
||||
for stuff in result_list:
|
||||
if isinstance(stuff, dict):
|
||||
for key, values in stuff.items():
|
||||
stuff[key] = [decode(v) for v in values]
|
||||
|
||||
return result_list
|
||||
|
||||
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)
|
||||
for i, (k, v) in enumerate(ldif):
|
||||
if isinstance(v, list):
|
||||
v = [a.encode("utf-8") for a in v]
|
||||
elif isinstance(v, str):
|
||||
v = [v.encode("utf-8")]
|
||||
ldif[i] = (k, v)
|
||||
|
||||
try:
|
||||
self.con.add_s(dn, ldif)
|
||||
except Exception as e:
|
||||
raise MoulinetteError(
|
||||
"error during LDAP add operation with: rdn='%s', "
|
||||
"attr_dict=%s and exception %s" % (rdn, attr_dict, e),
|
||||
raw_msg=True,
|
||||
)
|
||||
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 Exception as e:
|
||||
raise MoulinetteError(
|
||||
"error during LDAP delete operation with: rdn='%s' and exception %s"
|
||||
% (rdn, e),
|
||||
raw_msg=True,
|
||||
)
|
||||
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)
|
||||
|
||||
if ldif == []:
|
||||
logger.debug("Nothing to update in LDAP")
|
||||
return True
|
||||
|
||||
try:
|
||||
if new_rdn:
|
||||
self.con.rename_s(dn, new_rdn)
|
||||
new_base = dn.split(",", 1)[1]
|
||||
dn = new_rdn + "," + new_base
|
||||
|
||||
for i, (a, k, vs) in enumerate(ldif):
|
||||
if isinstance(vs, list):
|
||||
vs = [v.encode("utf-8") for v in vs]
|
||||
elif isinstance(vs, str):
|
||||
vs = [vs.encode("utf-8")]
|
||||
ldif[i] = (a, k, vs)
|
||||
|
||||
self.con.modify_ext_s(dn, ldif)
|
||||
except Exception as e:
|
||||
raise MoulinetteError(
|
||||
"error during LDAP update operation with: rdn='%s', "
|
||||
"attr_dict=%s, new_rdn=%s and exception: %s"
|
||||
% (rdn, attr_dict, new_rdn, e),
|
||||
raw_msg=True,
|
||||
)
|
||||
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
|
||||
|
||||
"""
|
||||
attr_found = self.get_conflict(value_dict)
|
||||
if attr_found:
|
||||
logger.info(
|
||||
"attribute '%s' with value '%s' is not unique",
|
||||
attr_found[0],
|
||||
attr_found[1],
|
||||
)
|
||||
raise MoulinetteError(
|
||||
"ldap_attribute_already_exists",
|
||||
attribute=attr_found[0],
|
||||
value=attr_found[1],
|
||||
)
|
||||
return True
|
||||
|
||||
def get_conflict(self, value_dict, base_dn=None):
|
||||
"""
|
||||
Check uniqueness of values
|
||||
|
||||
Keyword arguments:
|
||||
value_dict -- Dictionnary of attributes/values to check
|
||||
|
||||
Returns:
|
||||
None | tuple with Fist conflict attribute name and value
|
||||
|
||||
"""
|
||||
for attr, value in value_dict.items():
|
||||
if not self.search(base=base_dn, filter=attr + "=" + value):
|
||||
continue
|
||||
else:
|
||||
return (attr, value)
|
||||
return None
|
||||
|
||||
def _get_uri(self):
|
||||
return self.uri
|
|
@ -314,22 +314,10 @@ class MoulinetteSignals(object):
|
|||
signals = {"authenticate", "prompt", "display"}
|
||||
|
||||
def authenticate(self, authenticator):
|
||||
"""Process the authentication
|
||||
|
||||
Attempt to authenticate to the given authenticator and return
|
||||
it.
|
||||
It is called when authentication is needed (e.g. to process an
|
||||
action).
|
||||
|
||||
Keyword arguments:
|
||||
- authenticator -- The authenticator object to use
|
||||
|
||||
Returns:
|
||||
The authenticator object
|
||||
|
||||
"""
|
||||
if authenticator.is_authenticated:
|
||||
return authenticator
|
||||
if hasattr(authenticator, "is_authenticated"):
|
||||
return authenticator.is_authenticated
|
||||
# self._authenticate corresponds to the stuff defined with
|
||||
# msignals.set_handler("authenticate", ...) per interface...
|
||||
return self._authenticate(authenticator)
|
||||
|
||||
def prompt(self, message, is_password=False, confirm=False, color="blue"):
|
||||
|
@ -396,10 +384,6 @@ class MoulinetteError(Exception):
|
|||
return self.strerror
|
||||
|
||||
|
||||
class MoulinetteLdapIsDownError(MoulinetteError):
|
||||
"""Used when ldap is down"""
|
||||
|
||||
|
||||
class MoulinetteLock(object):
|
||||
|
||||
"""Locker for a moulinette instance
|
||||
|
|
|
@ -35,16 +35,10 @@ class BaseActionsMapParser(object):
|
|||
"""
|
||||
|
||||
def __init__(self, parent=None, **kwargs):
|
||||
if parent:
|
||||
self._o = parent
|
||||
else:
|
||||
if not parent:
|
||||
logger.debug("initializing base actions map parser for %s", self.interface)
|
||||
msettings["interface"] = self.interface
|
||||
|
||||
self._o = self
|
||||
self._global_conf = {}
|
||||
self._conf = {}
|
||||
|
||||
# Virtual properties
|
||||
# Each parser classes must implement these properties.
|
||||
|
||||
|
@ -121,7 +115,7 @@ class BaseActionsMapParser(object):
|
|||
"derived class '%s' must override this method" % self.__class__.__name__
|
||||
)
|
||||
|
||||
def auth_required(self, args, **kwargs):
|
||||
def auth_method(self, args, **kwargs):
|
||||
"""Check if authentication is required to run the requested action
|
||||
|
||||
Keyword arguments:
|
||||
|
@ -172,130 +166,6 @@ class BaseActionsMapParser(object):
|
|||
|
||||
return namespace
|
||||
|
||||
# Configuration access
|
||||
|
||||
@property
|
||||
def global_conf(self):
|
||||
"""Return the global configuration of the parser"""
|
||||
return self._o._global_conf
|
||||
|
||||
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:
|
||||
return self._o._conf[action][name]
|
||||
except KeyError:
|
||||
return self.global_conf[name]
|
||||
|
||||
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 is False:
|
||||
conf["authenticate"] = False
|
||||
elif isinstance(ifaces, list):
|
||||
if "all" in ifaces:
|
||||
conf["authenticate"] = "all"
|
||||
else:
|
||||
# Store only if authentication is needed
|
||||
conf["authenticate"] = True if self.interface in ifaces else False
|
||||
else:
|
||||
error_message = (
|
||||
"expecting 'all', 'False' or a list for "
|
||||
"configuration 'authenticate', got %r" % ifaces,
|
||||
)
|
||||
logger.error(error_message)
|
||||
raise MoulinetteError(error_message, raw_msg=True)
|
||||
|
||||
# -- 'authenticator'
|
||||
auth = configuration.get("authenticator", "default")
|
||||
if not is_global and isinstance(auth, str):
|
||||
# Store needed authenticator profile
|
||||
if auth not in self.global_conf["authenticator"]:
|
||||
error_message = (
|
||||
"requesting profile '%s' which is undefined in "
|
||||
"global configuration of 'authenticator'" % auth,
|
||||
)
|
||||
logger.error(error_message)
|
||||
raise MoulinetteError(error_message, raw_msg=True)
|
||||
else:
|
||||
conf["authenticator"] = auth
|
||||
elif is_global and isinstance(auth, dict):
|
||||
if len(auth) == 0:
|
||||
logger.warning(
|
||||
"no profile defined in global configuration " "for 'authenticator'"
|
||||
)
|
||||
else:
|
||||
auths = {}
|
||||
for auth_name, auth_conf in auth.items():
|
||||
auths[auth_name] = {
|
||||
"name": auth_name,
|
||||
"vendor": auth_conf.get("vendor"),
|
||||
"parameters": auth_conf.get("parameters", {}),
|
||||
"extra": {"help": auth_conf.get("help", None)},
|
||||
}
|
||||
conf["authenticator"] = auths
|
||||
else:
|
||||
error_message = (
|
||||
"expecting a dict of profile(s) or a profile name "
|
||||
"for configuration 'authenticator', got %r",
|
||||
auth,
|
||||
)
|
||||
logger.error(error_message)
|
||||
raise MoulinetteError(error_message, raw_msg=True)
|
||||
|
||||
return conf
|
||||
|
||||
|
||||
class BaseInterface(object):
|
||||
|
||||
|
|
|
@ -254,7 +254,7 @@ class _ActionsMapPlugin(object):
|
|||
except KeyError:
|
||||
raise HTTPBadRequestResponse("Missing password parameter")
|
||||
|
||||
kwargs["profile"] = request.POST.get("profile", "default")
|
||||
kwargs["profile"] = request.POST.get("profile", self.actionsmap.default_authentication)
|
||||
return callback(**kwargs)
|
||||
|
||||
return wrapper
|
||||
|
@ -263,7 +263,7 @@ class _ActionsMapPlugin(object):
|
|||
def _logout(callback):
|
||||
def wrapper():
|
||||
kwargs = {}
|
||||
kwargs["profile"] = request.POST.get("profile", "default")
|
||||
kwargs["profile"] = request.POST.get("profile", self.actionsmap.default_authentication)
|
||||
return callback(**kwargs)
|
||||
|
||||
return wrapper
|
||||
|
@ -379,7 +379,7 @@ class _ActionsMapPlugin(object):
|
|||
|
||||
try:
|
||||
# Attempt to authenticate
|
||||
authenticator = self.actionsmap.get_authenticator_for_profile(profile)
|
||||
authenticator = self.actionsmap.get_authenticator(profile)
|
||||
authenticator(password, token=(s_id, s_new_token))
|
||||
except MoulinetteError as e:
|
||||
if len(s_tokens) > 0:
|
||||
|
@ -423,7 +423,7 @@ class _ActionsMapPlugin(object):
|
|||
raise HTTPUnauthorizedResponse(m18n.g("not_logged_in"))
|
||||
else:
|
||||
del self.secrets[s_id]
|
||||
authenticator = self.actionsmap.get_authenticator_for_profile(profile)
|
||||
authenticator = self.actionsmap.get_authenticator(profile)
|
||||
authenticator._clean_session(s_id)
|
||||
# TODO: Clean the session for profile only
|
||||
# Delete cookie and clean the session
|
||||
|
@ -481,6 +481,7 @@ class _ActionsMapPlugin(object):
|
|||
- arguments -- A dict of arguments for the route
|
||||
|
||||
"""
|
||||
|
||||
try:
|
||||
ret = self.actionsmap.process(arguments, timeout=30, route=_route)
|
||||
except MoulinetteError as e:
|
||||
|
@ -683,31 +684,17 @@ class ActionsMapParser(BaseActionsMapParser):
|
|||
# Return the created parser
|
||||
return parser
|
||||
|
||||
def auth_required(self, args, **kwargs):
|
||||
def auth_method(self, args, route, **kwargs):
|
||||
|
||||
try:
|
||||
# Retrieve the tid for the route
|
||||
tid, _ = self._parsers[kwargs.get("route")]
|
||||
_, parser = self._parsers[route]
|
||||
except KeyError as e:
|
||||
error_message = "no argument parser found for route '%s': %s" % (
|
||||
kwargs.get("route"),
|
||||
e,
|
||||
)
|
||||
error_message = "no argument parser found for route '%s': %s" % (route, e)
|
||||
logger.error(error_message)
|
||||
raise MoulinetteError(error_message, raw_msg=True)
|
||||
|
||||
if self.get_conf(tid, "authenticate"):
|
||||
authenticator = self.get_conf(tid, "authenticator")
|
||||
|
||||
# If several authenticator, use the default one
|
||||
if isinstance(authenticator, dict):
|
||||
if "default" in authenticator:
|
||||
authenticator = "default"
|
||||
else:
|
||||
# TODO which one should we use?
|
||||
pass
|
||||
return authenticator
|
||||
else:
|
||||
return False
|
||||
return parser.authentication
|
||||
|
||||
def parse_args(self, args, route, **kwargs):
|
||||
"""Parse arguments
|
||||
|
|
|
@ -398,7 +398,7 @@ class ActionsMapParser(BaseActionsMapParser):
|
|||
|
||||
self.global_parser.add_argument(*names, **argument_options)
|
||||
|
||||
def auth_required(self, args, **kwargs):
|
||||
def auth_method(self, args, **kwargs):
|
||||
# FIXME? idk .. this try/except is duplicated from parse_args below
|
||||
# Just to be able to obtain the tid
|
||||
try:
|
||||
|
@ -414,19 +414,23 @@ class ActionsMapParser(BaseActionsMapParser):
|
|||
raise MoulinetteError(error_message, raw_msg=True)
|
||||
|
||||
tid = getattr(ret, "_tid", None)
|
||||
if self.get_conf(tid, "authenticate"):
|
||||
authenticator = self.get_conf(tid, "authenticator")
|
||||
|
||||
# If several authenticator, use the default one
|
||||
if isinstance(authenticator, dict):
|
||||
if "default" in authenticator:
|
||||
authenticator = "default"
|
||||
else:
|
||||
# TODO which one should we use?
|
||||
pass
|
||||
return authenticator
|
||||
else:
|
||||
return False
|
||||
# Ugh that's for yunohost --version ...
|
||||
if tid is None:
|
||||
return None
|
||||
|
||||
# We go down in the subparser tree until we find the leaf
|
||||
# corresponding to the tid with a defined authentication
|
||||
# (yeah it's a mess because the datastructure is a mess..)
|
||||
_p = self._subparsers
|
||||
for word in tid[1:]:
|
||||
_p = _p.choices[word]
|
||||
if hasattr(_p, "authentication"):
|
||||
return _p.authentication
|
||||
else:
|
||||
_p = _p._actions[1]
|
||||
|
||||
raise MoulinetteError(f"Authentication undefined for {tid} ?", raw_msg=True)
|
||||
|
||||
def parse_args(self, args, **kwargs):
|
||||
try:
|
||||
|
@ -533,8 +537,7 @@ class Interface(BaseInterface):
|
|||
# I guess we could imagine some yunohost-independant use-case where
|
||||
# moulinette is used to create a CLI for non-root user that needs to
|
||||
# auth somehow but hmpf -.-
|
||||
help = authenticator.extra.get("help")
|
||||
msg = m18n.n(help) if help else m18n.g("password")
|
||||
msg = m18n.g("password")
|
||||
return authenticator(password=self._do_prompt(msg, True, False, color="yellow"))
|
||||
|
||||
def _do_prompt(self, message, is_password, confirm, color="blue"):
|
||||
|
|
Loading…
Reference in a new issue