mirror of
https://github.com/YunoHost/moulinette.git
synced 2024-09-03 20:06:31 +02:00
Work on Authenticators (support for session token and LDAP)
This commit is contained in:
parent
33752ce01b
commit
5da9f6add8
5 changed files with 169 additions and 53 deletions
|
@ -8,18 +8,17 @@ _global:
|
||||||
- api
|
- api
|
||||||
authenticator:
|
authenticator:
|
||||||
default:
|
default:
|
||||||
type: ldap
|
vendor: ldap
|
||||||
help: Admin Password
|
help: Admin Password
|
||||||
parameters:
|
parameters:
|
||||||
uri: ldap://localhost:389
|
uri: ldap://localhost:389
|
||||||
base: dc=yunohost,dc=org
|
base_dn: dc=yunohost,dc=org
|
||||||
anonymous: false
|
user_rdn: cn=admin
|
||||||
ldap-anonymous:
|
ldap-anonymous:
|
||||||
type: ldap
|
vendor: ldap
|
||||||
parameters:
|
parameters:
|
||||||
uri: ldap://localhost:389
|
uri: ldap://localhost:389
|
||||||
base: dc=yunohost,dc=org
|
base_dn: dc=yunohost,dc=org
|
||||||
anonymous: true
|
|
||||||
argument_auth: true
|
argument_auth: true
|
||||||
|
|
||||||
#############################
|
#############################
|
||||||
|
|
|
@ -38,18 +38,17 @@ _global:
|
||||||
- api
|
- api
|
||||||
authenticator:
|
authenticator:
|
||||||
default:
|
default:
|
||||||
type: ldap
|
vendor: ldap
|
||||||
help: Admin Password
|
help: Admin Password
|
||||||
parameters:
|
parameters:
|
||||||
uri: ldap://localhost:389
|
uri: ldap://localhost:389
|
||||||
base: dc=yunohost,dc=org
|
base_dn: dc=yunohost,dc=org
|
||||||
anonymous: false
|
user_rdn: cn=admin
|
||||||
ldap-anonymous:
|
ldap-anonymous:
|
||||||
type: ldap
|
vendor: ldap
|
||||||
parameters:
|
parameters:
|
||||||
uri: ldap://localhost:389
|
uri: ldap://localhost:389
|
||||||
base: dc=yunohost,dc=org
|
base_dn: dc=yunohost,dc=org
|
||||||
anonymous: true
|
|
||||||
arguments:
|
arguments:
|
||||||
-v:
|
-v:
|
||||||
full: --version
|
full: --version
|
||||||
|
|
|
@ -330,7 +330,7 @@ class _AMapParser(object):
|
||||||
for auth_name, auth_conf in auth.items():
|
for auth_name, auth_conf in auth.items():
|
||||||
# Add authenticator name
|
# Add authenticator name
|
||||||
auths[auth_name] = ({ 'name': auth_name,
|
auths[auth_name] = ({ 'name': auth_name,
|
||||||
'type': auth_conf.get('type'),
|
'vendor': auth_conf.get('vendor'),
|
||||||
'help': auth_conf.get('help', None)
|
'help': auth_conf.get('help', None)
|
||||||
},
|
},
|
||||||
auth_conf.get('parameters', {}))
|
auth_conf.get('parameters', {}))
|
||||||
|
@ -366,12 +366,13 @@ class _AMapParser(object):
|
||||||
"""
|
"""
|
||||||
if name == 'authenticator' and value:
|
if name == 'authenticator' and value:
|
||||||
auth_conf, auth_params = value
|
auth_conf, auth_params = value
|
||||||
auth_type = auth_conf.pop('type')
|
auth_vendor = auth_conf.pop('vendor')
|
||||||
|
|
||||||
# Return authenticator configuration and an instanciator for
|
# Return authenticator configuration and an instanciator for
|
||||||
# it as a 2-tuple
|
# it as a 2-tuple
|
||||||
return (auth_conf,
|
return (auth_conf,
|
||||||
lambda: init_authenticator(auth_type, **auth_params))
|
lambda: init_authenticator(auth_conf['name'],
|
||||||
|
auth_vendor, **auth_params))
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
@ -994,7 +995,7 @@ class ActionsMap(object):
|
||||||
actionsmaps[n] = yaml.load(f)
|
actionsmaps[n] = yaml.load(f)
|
||||||
|
|
||||||
# Cache actions map into pickle file
|
# Cache actions map into pickle file
|
||||||
with pkg.open_cache('%s.pkl' % n, subdir='actionsmap') as f:
|
with pkg.open_cachefile('%s.pkl' % n, 'w', subdir='actionsmap') as f:
|
||||||
pickle.dump(actionsmaps[n], f)
|
pickle.dump(actionsmaps[n], f)
|
||||||
|
|
||||||
return actionsmaps
|
return actionsmaps
|
||||||
|
|
|
@ -4,6 +4,7 @@ import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import gettext
|
import gettext
|
||||||
|
import logging
|
||||||
|
|
||||||
from .helpers import colorize
|
from .helpers import colorize
|
||||||
|
|
||||||
|
@ -109,7 +110,7 @@ class Package(object):
|
||||||
os.makedirs(path)
|
os.makedirs(path)
|
||||||
return path
|
return path
|
||||||
|
|
||||||
def open_cache(self, filename, subdir='', mode='w'):
|
def open_cachefile(self, filename, mode='r', **kwargs):
|
||||||
"""Open a cache file and return a stream
|
"""Open a cache file and return a stream
|
||||||
|
|
||||||
Attempt to open in 'mode' the cache file 'filename' from the
|
Attempt to open in 'mode' the cache file 'filename' from the
|
||||||
|
@ -119,85 +120,201 @@ class Package(object):
|
||||||
|
|
||||||
Keyword arguments:
|
Keyword arguments:
|
||||||
- filename -- The cache filename
|
- filename -- The cache filename
|
||||||
- subdir -- A subdirectory which contains the file
|
|
||||||
- mode -- The mode in which the file is opened
|
- mode -- The mode in which the file is opened
|
||||||
|
- **kwargs -- Optional arguments for get_cachedir
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return open('%s/%s' % (self.get_cachedir(subdir), filename), mode)
|
# Set make_dir if not given
|
||||||
|
kwargs['make_dir'] = kwargs.get('make_dir',
|
||||||
|
True if mode[0] == 'w' else False)
|
||||||
|
return open('%s/%s' % (self.get_cachedir(**kwargs), filename), mode)
|
||||||
|
|
||||||
|
|
||||||
# Authenticators -------------------------------------------------------
|
# Authenticators -------------------------------------------------------
|
||||||
|
|
||||||
|
import ldap
|
||||||
|
import gnupg
|
||||||
|
|
||||||
|
class AuthenticationError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
class _BaseAuthenticator(object):
|
class _BaseAuthenticator(object):
|
||||||
|
|
||||||
|
def __init__(self, name):
|
||||||
|
self._name = name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the authenticator instance"""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
|
||||||
## Virtual properties
|
## Virtual properties
|
||||||
# Each authenticator classes must implement these properties.
|
# Each authenticator classes must implement these properties.
|
||||||
|
|
||||||
"""The name of the authenticator"""
|
"""The vendor of the authenticator"""
|
||||||
name = None
|
vendor = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_authenticated(self):
|
def is_authenticated(self):
|
||||||
"""Either the instance is authenticated or not"""
|
"""Either the instance is authenticated or not"""
|
||||||
raise NotImplementedError("derived class '%s' must override this property" % \
|
raise NotImplementedError("derived class '%s' must override this property" % \
|
||||||
self.__class__.__name__)
|
self.__class__.__name__)
|
||||||
|
|
||||||
|
|
||||||
## Virtual methods
|
## Virtual methods
|
||||||
# Each authenticator classes must implement these methods.
|
# Each authenticator classes must implement these methods.
|
||||||
|
|
||||||
def authenticate(password=None, token=None):
|
def authenticate(password=None):
|
||||||
"""Attempt to authenticate
|
"""Attempt to authenticate
|
||||||
|
|
||||||
Attempt to authenticate with given password or session token.
|
Attempt to authenticate with given password. It should raise an
|
||||||
|
AuthenticationError exception if authentication fails.
|
||||||
|
|
||||||
Keyword arguments:
|
Keyword arguments:
|
||||||
- password -- A clear text password
|
- password -- A clear text password
|
||||||
- token -- A session token
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
An optional session token
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError("derived class '%s' must override this method" % \
|
raise NotImplementedError("derived class '%s' must override this method" % \
|
||||||
self.__class__.__name__)
|
self.__class__.__name__)
|
||||||
|
|
||||||
|
|
||||||
class LDAPAuthenticator(object):
|
## Authentication methods
|
||||||
|
|
||||||
def __init__(self, uri, base, anonymous=False):
|
def __call__(self, password=None, token=None):
|
||||||
# TODO: Initialize LDAP connection
|
"""Attempt to authenticate
|
||||||
|
|
||||||
if anonymous:
|
Attempt to authenticate either with password or with session
|
||||||
self._authenticated = True
|
token if 'password' is None. If the authentication succeed, the
|
||||||
else:
|
instance is returned and the session is registered for the token
|
||||||
self._authenticated = False
|
if 'token' and 'password' are given.
|
||||||
|
The token is composed by the session identifier and a session
|
||||||
|
hash - to use for encryption - as a 2-tuple.
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
- password -- A clear text password
|
||||||
|
- token -- The session token in the form of (id, hash)
|
||||||
|
|
||||||
## Implement virtual properties
|
Returns:
|
||||||
|
The authenticated instance
|
||||||
|
|
||||||
name = 'ldap'
|
"""
|
||||||
|
if self.is_authenticated:
|
||||||
|
return self
|
||||||
|
store_session = True if password and token else False
|
||||||
|
|
||||||
@property
|
if token:
|
||||||
def is_authenticated(self):
|
try:
|
||||||
return self._authenticated
|
# Extract id and hash from token
|
||||||
|
s_id, s_hash = token
|
||||||
|
except TypeError:
|
||||||
|
if not password:
|
||||||
|
raise MoulinetteError(22, _("Invalid format for token"))
|
||||||
|
else:
|
||||||
|
# TODO: Log error
|
||||||
|
store_session = False
|
||||||
|
else:
|
||||||
|
if password is None:
|
||||||
|
# Retrieve session
|
||||||
|
password = self._retrieve_session(s_id, s_hash)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Attempt to authenticate
|
||||||
|
self.authenticate(password)
|
||||||
|
except AuthenticationError as e:
|
||||||
|
raise MoulinetteError(13, str(e))
|
||||||
|
except Exception as e:
|
||||||
|
logging.error("authentication (name: '%s', type: '%s') fails: %s" % \
|
||||||
|
(self.name, self.vendor, e))
|
||||||
|
raise MoulinetteError(13, _("Unable to authenticate"))
|
||||||
|
|
||||||
## Implement virtual methods
|
# Store session
|
||||||
|
if store_session:
|
||||||
def authenticate(self, password=None, token=None):
|
self._store_session(s_id, s_hash, password)
|
||||||
# TODO: Perform LDAP authentication
|
|
||||||
if password == 'test':
|
|
||||||
self._authenticated = True
|
|
||||||
else:
|
|
||||||
raise MoulinetteError(13, _("Invalid password"))
|
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
def init_authenticator(_name, **kwargs):
|
## Private methods
|
||||||
if _name == 'ldap':
|
|
||||||
return LDAPAuthenticator(**kwargs)
|
def _open_sessionfile(self, session_id, mode='r'):
|
||||||
|
"""Open a session file for this instance in given mode"""
|
||||||
|
return pkg.open_cachefile('%s.asc' % session_id, mode,
|
||||||
|
subdir='session/%s' % self.name)
|
||||||
|
|
||||||
|
def _store_session(self, session_id, session_hash, password):
|
||||||
|
"""Store a session and its associated password"""
|
||||||
|
gpg = gnupg.GPG()
|
||||||
|
gpg.encoding = 'utf-8'
|
||||||
|
with self._open_sessionfile(session_id, 'w') as f:
|
||||||
|
f.write(str(gpg.encrypt(password, None, symmetric=True,
|
||||||
|
passphrase=session_hash)))
|
||||||
|
|
||||||
|
def _retrieve_session(self, session_id, session_hash):
|
||||||
|
"""Retrieve a session and return its associated password"""
|
||||||
|
try:
|
||||||
|
with self._open_sessionfile(session_id, 'r') as f:
|
||||||
|
enc_pwd = f.read()
|
||||||
|
except IOError:
|
||||||
|
# TODO: Set proper error code
|
||||||
|
raise MoulinetteError(167, _("Unable to retrieve session"))
|
||||||
|
else:
|
||||||
|
gpg = gnupg.GPG()
|
||||||
|
gpg.encoding = 'utf-8'
|
||||||
|
return str(gpg.decrypt(enc_pwd, passphrase=session_hash))
|
||||||
|
|
||||||
|
|
||||||
|
class LDAPAuthenticator(_BaseAuthenticator):
|
||||||
|
|
||||||
|
def __init__(self, uri, base_dn, user_rdn=None, **kwargs):
|
||||||
|
super(LDAPAuthenticator, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
self.uri = uri
|
||||||
|
self.basedn = base_dn
|
||||||
|
if user_rdn:
|
||||||
|
self.userdn = '%s,%s' % (user_rdn, base_dn)
|
||||||
|
self.con = None
|
||||||
|
else:
|
||||||
|
# Initialize anonymous usage
|
||||||
|
self.userdn = ''
|
||||||
|
self.authenticate(None)
|
||||||
|
|
||||||
|
|
||||||
|
## Implement virtual properties
|
||||||
|
|
||||||
|
vendor = 'ldap'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_authenticated(self):
|
||||||
|
try:
|
||||||
|
# Retrieve identity
|
||||||
|
who = self.con.whoami_s()
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
if who[3:] == self.userdn:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
## Implement virtual methods
|
||||||
|
|
||||||
|
def authenticate(self, password):
|
||||||
|
try:
|
||||||
|
con = ldap.initialize(self.uri)
|
||||||
|
if self.userdn:
|
||||||
|
con.simple_bind_s(self.userdn, password)
|
||||||
|
else:
|
||||||
|
con.simple_bind_s()
|
||||||
|
except ldap.INVALID_CREDENTIALS:
|
||||||
|
raise AuthenticationError(_("Invalid password"))
|
||||||
|
else:
|
||||||
|
self.con = con
|
||||||
|
|
||||||
|
|
||||||
|
def init_authenticator(_name, _vendor, **kwargs):
|
||||||
|
if _vendor == 'ldap':
|
||||||
|
return LDAPAuthenticator(name=_name, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
# Moulinette core classes ----------------------------------------------
|
# Moulinette core classes ----------------------------------------------
|
||||||
|
|
|
@ -108,7 +108,7 @@ class MoulinetteCLI(object):
|
||||||
"""
|
"""
|
||||||
# TODO: Allow token authentication?
|
# TODO: Allow token authentication?
|
||||||
msg = help or _("Password")
|
msg = help or _("Password")
|
||||||
return authenticator.authenticate(password=self._do_prompt(msg, True, False))
|
return authenticator(password=self._do_prompt(msg, True, False))
|
||||||
|
|
||||||
def _do_prompt(self, message, is_password, confirm):
|
def _do_prompt(self, message, is_password, confirm):
|
||||||
"""Prompt for a value
|
"""Prompt for a value
|
||||||
|
|
Loading…
Add table
Reference in a new issue