From af0957c028429ad1eda5679cd589d9fc4389ea8a Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 9 Mar 2021 05:43:09 +0100 Subject: [PATCH] Rework and externalize the authenticator system --- moulinette/actionsmap.py | 75 +++-- .../__init__.py => authentication.py} | 53 +-- moulinette/authenticators/dummy.py | 28 -- moulinette/authenticators/ldap.py | 311 ------------------ moulinette/core.py | 24 +- moulinette/interfaces/__init__.py | 134 +------- moulinette/interfaces/api.py | 33 +- moulinette/interfaces/cli.py | 33 +- 8 files changed, 83 insertions(+), 608 deletions(-) rename moulinette/{authenticators/__init__.py => authentication.py} (85%) delete mode 100644 moulinette/authenticators/dummy.py delete mode 100644 moulinette/authenticators/ldap.py diff --git a/moulinette/actionsmap.py b/moulinette/actionsmap.py index d4a5f079..edd6a1f1 100644 --- a/moulinette/actionsmap.py +++ b/moulinette/actionsmap.py @@ -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 diff --git a/moulinette/authenticators/__init__.py b/moulinette/authentication.py similarity index 85% rename from moulinette/authenticators/__init__.py rename to moulinette/authentication.py index 0170d345..aecc65a6 100644 --- a/moulinette/authenticators/__init__.py +++ b/moulinette/authentication.py @@ -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 diff --git a/moulinette/authenticators/dummy.py b/moulinette/authenticators/dummy.py deleted file mode 100644 index e2978d12..00000000 --- a/moulinette/authenticators/dummy.py +++ /dev/null @@ -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 diff --git a/moulinette/authenticators/ldap.py b/moulinette/authenticators/ldap.py deleted file mode 100644 index 13c635a3..00000000 --- a/moulinette/authenticators/ldap.py +++ /dev/null @@ -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 diff --git a/moulinette/core.py b/moulinette/core.py index c41b7d21..3aa7b67c 100644 --- a/moulinette/core.py +++ b/moulinette/core.py @@ -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 diff --git a/moulinette/interfaces/__init__.py b/moulinette/interfaces/__init__.py index a73bc5f6..e53c5f43 100644 --- a/moulinette/interfaces/__init__.py +++ b/moulinette/interfaces/__init__.py @@ -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): diff --git a/moulinette/interfaces/api.py b/moulinette/interfaces/api.py index a3427a2b..4d27d743 100644 --- a/moulinette/interfaces/api.py +++ b/moulinette/interfaces/api.py @@ -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 diff --git a/moulinette/interfaces/cli.py b/moulinette/interfaces/cli.py index 19505365..7245458b 100644 --- a/moulinette/interfaces/cli.py +++ b/moulinette/interfaces/cli.py @@ -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"):