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.extraparser = ExtraArgumentParser(top_parser.interface)
|
||||||
self.parser = self._construct_parser(actionsmaps, top_parser)
|
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
|
if auth_method == "default":
|
||||||
try:
|
auth_method = self.default_authentication
|
||||||
auth_conf = self.parser.global_conf["authenticator"][auth_profile]
|
|
||||||
except KeyError:
|
|
||||||
raise ValueError("Unknown authenticator profile '%s'" % auth_profile)
|
|
||||||
|
|
||||||
# Load and initialize the authenticator module
|
# 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:
|
try:
|
||||||
mod = import_module("moulinette.authenticators.%s" % auth_conf["vendor"])
|
mod = import_module(auth_module)
|
||||||
except ImportError:
|
except ImportError as e:
|
||||||
error_message = (
|
import traceback
|
||||||
"unable to load authenticator vendor module 'moulinette.authenticators.%s'"
|
traceback.print_exc()
|
||||||
% auth_conf["vendor"]
|
raise MoulinetteError(f"unable to load authenticator {auth_module} : {e}", raw_msg=True)
|
||||||
)
|
|
||||||
logger.exception(error_message)
|
|
||||||
raise MoulinetteError(error_message, raw_msg=True)
|
|
||||||
else:
|
else:
|
||||||
return mod.Authenticator(**auth_conf)
|
return mod.Authenticator()
|
||||||
|
|
||||||
def check_authentication_if_required(self, args, **kwargs):
|
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
|
return
|
||||||
|
|
||||||
authenticator = self.get_authenticator_for_profile(auth_profile)
|
authenticator = self.get_authenticator(auth_method)
|
||||||
auth = msignals.authenticate(authenticator)
|
if not msignals.authenticate(authenticator):
|
||||||
|
|
||||||
if not auth.is_authenticated:
|
|
||||||
raise MoulinetteError("authentication_required_long")
|
raise MoulinetteError("authentication_required_long")
|
||||||
|
|
||||||
def process(self, args, timeout=None, **kwargs):
|
def process(self, args, timeout=None, **kwargs):
|
||||||
|
@ -681,6 +675,8 @@ class ActionsMap(object):
|
||||||
logger.debug("building parser...")
|
logger.debug("building parser...")
|
||||||
start = time()
|
start = time()
|
||||||
|
|
||||||
|
interface_type = top_parser.interface
|
||||||
|
|
||||||
# If loading from cache, extra were already checked when cache was
|
# If loading from cache, extra were already checked when cache was
|
||||||
# loaded ? Not sure about this ... old code is a bit mysterious...
|
# loaded ? Not sure about this ... old code is a bit mysterious...
|
||||||
validate_extra = not self.from_cache
|
validate_extra = not self.from_cache
|
||||||
|
@ -694,25 +690,26 @@ class ActionsMap(object):
|
||||||
# Retrieve global parameters
|
# Retrieve global parameters
|
||||||
_global = actionsmap.pop("_global", {})
|
_global = actionsmap.pop("_global", {})
|
||||||
|
|
||||||
# Set the global configuration to use for the parser.
|
if _global:
|
||||||
top_parser.set_global_conf(_global["configuration"])
|
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():
|
if top_parser.has_global_parser():
|
||||||
top_parser.add_global_arguments(_global["arguments"])
|
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_name is stuff like "user", "domain", "hooks"...
|
||||||
# category_values is the values of this category (like actions)
|
# category_values is the values of this category (like actions)
|
||||||
for category_name, category_values in actionsmap.items():
|
for category_name, category_values in actionsmap.items():
|
||||||
|
|
||||||
if "actions" in category_values:
|
actions = category_values.pop("actions", {})
|
||||||
actions = category_values.pop("actions")
|
subcategories = category_values.pop("subcategories", {})
|
||||||
else:
|
|
||||||
actions = {}
|
|
||||||
|
|
||||||
if "subcategories" in category_values:
|
|
||||||
subcategories = category_values.pop("subcategories")
|
|
||||||
else:
|
|
||||||
subcategories = {}
|
|
||||||
|
|
||||||
# Get category parser
|
# Get category parser
|
||||||
category_parser = top_parser.add_category_parser(
|
category_parser = top_parser.add_category_parser(
|
||||||
|
@ -723,6 +720,7 @@ class ActionsMap(object):
|
||||||
# action_options are the values
|
# action_options are the values
|
||||||
for action_name, action_options in actions.items():
|
for action_name, action_options in actions.items():
|
||||||
arguments = action_options.pop("arguments", {})
|
arguments = action_options.pop("arguments", {})
|
||||||
|
authentication = action_options.pop("authentication", {})
|
||||||
tid = (namespace, category_name, action_name)
|
tid = (namespace, category_name, action_name)
|
||||||
|
|
||||||
# Get action parser
|
# Get action parser
|
||||||
|
@ -742,8 +740,9 @@ class ActionsMap(object):
|
||||||
validate_extra=validate_extra,
|
validate_extra=validate_extra,
|
||||||
)
|
)
|
||||||
|
|
||||||
if "configuration" in action_options:
|
action_parser.authentication = self.default_authentication
|
||||||
category_parser.set_conf(tid, action_options["configuration"])
|
if interface_type in authentication:
|
||||||
|
action_parser.authentication = authentication[interface_type]
|
||||||
|
|
||||||
# subcategory_name is like "cert" in "domain cert status"
|
# subcategory_name is like "cert" in "domain cert status"
|
||||||
# subcategory_values is the values of this subcategory (like actions)
|
# subcategory_values is the values of this subcategory (like actions)
|
||||||
|
@ -760,6 +759,7 @@ class ActionsMap(object):
|
||||||
# action_options are the values
|
# action_options are the values
|
||||||
for action_name, action_options in actions.items():
|
for action_name, action_options in actions.items():
|
||||||
arguments = action_options.pop("arguments", {})
|
arguments = action_options.pop("arguments", {})
|
||||||
|
authentication = action_options.pop("authentication", {})
|
||||||
tid = (namespace, category_name, subcategory_name, action_name)
|
tid = (namespace, category_name, subcategory_name, action_name)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -780,10 +780,9 @@ class ActionsMap(object):
|
||||||
validate_extra=validate_extra,
|
validate_extra=validate_extra,
|
||||||
)
|
)
|
||||||
|
|
||||||
if "configuration" in action_options:
|
action_parser.authentication = self.default_authentication
|
||||||
category_parser.set_conf(
|
if interface_type in authentication:
|
||||||
tid, action_options["configuration"]
|
action_parser.authentication = authentication[interface_type]
|
||||||
)
|
|
||||||
|
|
||||||
logger.debug("building parser took %.3fs", time() - start)
|
logger.debug("building parser took %.3fs", time() - start)
|
||||||
return top_parser
|
return top_parser
|
||||||
|
|
|
@ -27,28 +27,8 @@ class BaseAuthenticator(object):
|
||||||
must be given on instantiation - with the corresponding vendor
|
must be given on instantiation - with the corresponding vendor
|
||||||
configuration of the authenticator.
|
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
|
# Virtual methods
|
||||||
# Each authenticator classes must implement these methods.
|
# Each authenticator classes must implement these methods.
|
||||||
|
|
||||||
|
@ -82,12 +62,12 @@ class BaseAuthenticator(object):
|
||||||
- password -- A clear text password
|
- password -- A clear text password
|
||||||
- token -- The session token in the form of (id, hash)
|
- 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
|
# Authenticate using the password
|
||||||
|
@ -99,15 +79,10 @@ class BaseAuthenticator(object):
|
||||||
except MoulinetteError:
|
except MoulinetteError:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(
|
logger.exception("authentication {self.name} failed because '{e}'")
|
||||||
"authentication (name: '%s', vendor: '%s') fails because '%s'",
|
|
||||||
self.name,
|
|
||||||
self.vendor,
|
|
||||||
e,
|
|
||||||
)
|
|
||||||
raise MoulinetteError("unable_authenticate")
|
raise MoulinetteError("unable_authenticate")
|
||||||
|
else:
|
||||||
self.is_authenticated = True
|
is_authenticated = True
|
||||||
|
|
||||||
# Store session for later using the provided (new) token if any
|
# Store session for later using the provided (new) token if any
|
||||||
if token:
|
if token:
|
||||||
|
@ -133,15 +108,10 @@ class BaseAuthenticator(object):
|
||||||
except MoulinetteError:
|
except MoulinetteError:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(
|
logger.exception("authentication {self.name} failed because '{e}'")
|
||||||
"authentication (name: '%s', vendor: '%s') fails because '%s'",
|
|
||||||
self.name,
|
|
||||||
self.vendor,
|
|
||||||
e,
|
|
||||||
)
|
|
||||||
raise MoulinetteError("unable_authenticate")
|
raise MoulinetteError("unable_authenticate")
|
||||||
else:
|
else:
|
||||||
self.is_authenticated = True
|
is_authenticated = True
|
||||||
|
|
||||||
#
|
#
|
||||||
# No credentials given, can't authenticate
|
# No credentials given, can't authenticate
|
||||||
|
@ -149,7 +119,8 @@ class BaseAuthenticator(object):
|
||||||
else:
|
else:
|
||||||
raise MoulinetteError("unable_authenticate")
|
raise MoulinetteError("unable_authenticate")
|
||||||
|
|
||||||
return self
|
self.is_authenticated = is_authenticated
|
||||||
|
return is_authenticated
|
||||||
|
|
||||||
# Private methods
|
# 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"}
|
signals = {"authenticate", "prompt", "display"}
|
||||||
|
|
||||||
def authenticate(self, authenticator):
|
def authenticate(self, authenticator):
|
||||||
"""Process the authentication
|
if hasattr(authenticator, "is_authenticated"):
|
||||||
|
return authenticator.is_authenticated
|
||||||
Attempt to authenticate to the given authenticator and return
|
# self._authenticate corresponds to the stuff defined with
|
||||||
it.
|
# msignals.set_handler("authenticate", ...) per interface...
|
||||||
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
|
|
||||||
return self._authenticate(authenticator)
|
return self._authenticate(authenticator)
|
||||||
|
|
||||||
def prompt(self, message, is_password=False, confirm=False, color="blue"):
|
def prompt(self, message, is_password=False, confirm=False, color="blue"):
|
||||||
|
@ -396,10 +384,6 @@ class MoulinetteError(Exception):
|
||||||
return self.strerror
|
return self.strerror
|
||||||
|
|
||||||
|
|
||||||
class MoulinetteLdapIsDownError(MoulinetteError):
|
|
||||||
"""Used when ldap is down"""
|
|
||||||
|
|
||||||
|
|
||||||
class MoulinetteLock(object):
|
class MoulinetteLock(object):
|
||||||
|
|
||||||
"""Locker for a moulinette instance
|
"""Locker for a moulinette instance
|
||||||
|
|
|
@ -35,16 +35,10 @@ class BaseActionsMapParser(object):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, parent=None, **kwargs):
|
def __init__(self, parent=None, **kwargs):
|
||||||
if parent:
|
if not parent:
|
||||||
self._o = parent
|
|
||||||
else:
|
|
||||||
logger.debug("initializing base actions map parser for %s", self.interface)
|
logger.debug("initializing base actions map parser for %s", self.interface)
|
||||||
msettings["interface"] = self.interface
|
msettings["interface"] = self.interface
|
||||||
|
|
||||||
self._o = self
|
|
||||||
self._global_conf = {}
|
|
||||||
self._conf = {}
|
|
||||||
|
|
||||||
# Virtual properties
|
# Virtual properties
|
||||||
# Each parser classes must implement these 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__
|
"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
|
"""Check if authentication is required to run the requested action
|
||||||
|
|
||||||
Keyword arguments:
|
Keyword arguments:
|
||||||
|
@ -172,130 +166,6 @@ class BaseActionsMapParser(object):
|
||||||
|
|
||||||
return namespace
|
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):
|
class BaseInterface(object):
|
||||||
|
|
||||||
|
|
|
@ -254,7 +254,7 @@ class _ActionsMapPlugin(object):
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise HTTPBadRequestResponse("Missing password parameter")
|
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 callback(**kwargs)
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
@ -263,7 +263,7 @@ class _ActionsMapPlugin(object):
|
||||||
def _logout(callback):
|
def _logout(callback):
|
||||||
def wrapper():
|
def wrapper():
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
kwargs["profile"] = request.POST.get("profile", "default")
|
kwargs["profile"] = request.POST.get("profile", self.actionsmap.default_authentication)
|
||||||
return callback(**kwargs)
|
return callback(**kwargs)
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
@ -379,7 +379,7 @@ class _ActionsMapPlugin(object):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Attempt to authenticate
|
# 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))
|
authenticator(password, token=(s_id, s_new_token))
|
||||||
except MoulinetteError as e:
|
except MoulinetteError as e:
|
||||||
if len(s_tokens) > 0:
|
if len(s_tokens) > 0:
|
||||||
|
@ -423,7 +423,7 @@ class _ActionsMapPlugin(object):
|
||||||
raise HTTPUnauthorizedResponse(m18n.g("not_logged_in"))
|
raise HTTPUnauthorizedResponse(m18n.g("not_logged_in"))
|
||||||
else:
|
else:
|
||||||
del self.secrets[s_id]
|
del self.secrets[s_id]
|
||||||
authenticator = self.actionsmap.get_authenticator_for_profile(profile)
|
authenticator = self.actionsmap.get_authenticator(profile)
|
||||||
authenticator._clean_session(s_id)
|
authenticator._clean_session(s_id)
|
||||||
# TODO: Clean the session for profile only
|
# TODO: Clean the session for profile only
|
||||||
# Delete cookie and clean the session
|
# Delete cookie and clean the session
|
||||||
|
@ -481,6 +481,7 @@ class _ActionsMapPlugin(object):
|
||||||
- arguments -- A dict of arguments for the route
|
- arguments -- A dict of arguments for the route
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ret = self.actionsmap.process(arguments, timeout=30, route=_route)
|
ret = self.actionsmap.process(arguments, timeout=30, route=_route)
|
||||||
except MoulinetteError as e:
|
except MoulinetteError as e:
|
||||||
|
@ -683,31 +684,17 @@ class ActionsMapParser(BaseActionsMapParser):
|
||||||
# Return the created parser
|
# Return the created parser
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
def auth_required(self, args, **kwargs):
|
def auth_method(self, args, route, **kwargs):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Retrieve the tid for the route
|
# Retrieve the tid for the route
|
||||||
tid, _ = self._parsers[kwargs.get("route")]
|
_, parser = self._parsers[route]
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
error_message = "no argument parser found for route '%s': %s" % (
|
error_message = "no argument parser found for route '%s': %s" % (route, e)
|
||||||
kwargs.get("route"),
|
|
||||||
e,
|
|
||||||
)
|
|
||||||
logger.error(error_message)
|
logger.error(error_message)
|
||||||
raise MoulinetteError(error_message, raw_msg=True)
|
raise MoulinetteError(error_message, raw_msg=True)
|
||||||
|
|
||||||
if self.get_conf(tid, "authenticate"):
|
return parser.authentication
|
||||||
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
|
|
||||||
|
|
||||||
def parse_args(self, args, route, **kwargs):
|
def parse_args(self, args, route, **kwargs):
|
||||||
"""Parse arguments
|
"""Parse arguments
|
||||||
|
|
|
@ -398,7 +398,7 @@ class ActionsMapParser(BaseActionsMapParser):
|
||||||
|
|
||||||
self.global_parser.add_argument(*names, **argument_options)
|
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
|
# FIXME? idk .. this try/except is duplicated from parse_args below
|
||||||
# Just to be able to obtain the tid
|
# Just to be able to obtain the tid
|
||||||
try:
|
try:
|
||||||
|
@ -414,19 +414,23 @@ class ActionsMapParser(BaseActionsMapParser):
|
||||||
raise MoulinetteError(error_message, raw_msg=True)
|
raise MoulinetteError(error_message, raw_msg=True)
|
||||||
|
|
||||||
tid = getattr(ret, "_tid", None)
|
tid = getattr(ret, "_tid", None)
|
||||||
if self.get_conf(tid, "authenticate"):
|
|
||||||
authenticator = self.get_conf(tid, "authenticator")
|
|
||||||
|
|
||||||
# If several authenticator, use the default one
|
# Ugh that's for yunohost --version ...
|
||||||
if isinstance(authenticator, dict):
|
if tid is None:
|
||||||
if "default" in authenticator:
|
return None
|
||||||
authenticator = "default"
|
|
||||||
else:
|
# We go down in the subparser tree until we find the leaf
|
||||||
# TODO which one should we use?
|
# corresponding to the tid with a defined authentication
|
||||||
pass
|
# (yeah it's a mess because the datastructure is a mess..)
|
||||||
return authenticator
|
_p = self._subparsers
|
||||||
else:
|
for word in tid[1:]:
|
||||||
return False
|
_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):
|
def parse_args(self, args, **kwargs):
|
||||||
try:
|
try:
|
||||||
|
@ -533,8 +537,7 @@ class Interface(BaseInterface):
|
||||||
# I guess we could imagine some yunohost-independant use-case where
|
# 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
|
# moulinette is used to create a CLI for non-root user that needs to
|
||||||
# auth somehow but hmpf -.-
|
# auth somehow but hmpf -.-
|
||||||
help = authenticator.extra.get("help")
|
msg = m18n.g("password")
|
||||||
msg = m18n.n(help) if help else m18n.g("password")
|
|
||||||
return authenticator(password=self._do_prompt(msg, True, False, color="yellow"))
|
return authenticator(password=self._do_prompt(msg, True, False, color="yellow"))
|
||||||
|
|
||||||
def _do_prompt(self, message, is_password, confirm, color="blue"):
|
def _do_prompt(self, message, is_password, confirm, color="blue"):
|
||||||
|
|
Loading…
Reference in a new issue