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
|
||||
authenticator:
|
||||
default:
|
||||
type: ldap
|
||||
vendor: ldap
|
||||
help: Admin Password
|
||||
parameters:
|
||||
uri: ldap://localhost:389
|
||||
base: dc=yunohost,dc=org
|
||||
anonymous: false
|
||||
base_dn: dc=yunohost,dc=org
|
||||
user_rdn: cn=admin
|
||||
ldap-anonymous:
|
||||
type: ldap
|
||||
vendor: ldap
|
||||
parameters:
|
||||
uri: ldap://localhost:389
|
||||
base: dc=yunohost,dc=org
|
||||
anonymous: true
|
||||
base_dn: dc=yunohost,dc=org
|
||||
argument_auth: true
|
||||
|
||||
#############################
|
||||
|
|
|
@ -38,18 +38,17 @@ _global:
|
|||
- api
|
||||
authenticator:
|
||||
default:
|
||||
type: ldap
|
||||
vendor: ldap
|
||||
help: Admin Password
|
||||
parameters:
|
||||
uri: ldap://localhost:389
|
||||
base: dc=yunohost,dc=org
|
||||
anonymous: false
|
||||
base_dn: dc=yunohost,dc=org
|
||||
user_rdn: cn=admin
|
||||
ldap-anonymous:
|
||||
type: ldap
|
||||
vendor: ldap
|
||||
parameters:
|
||||
uri: ldap://localhost:389
|
||||
base: dc=yunohost,dc=org
|
||||
anonymous: true
|
||||
base_dn: dc=yunohost,dc=org
|
||||
arguments:
|
||||
-v:
|
||||
full: --version
|
||||
|
|
|
@ -330,7 +330,7 @@ class _AMapParser(object):
|
|||
for auth_name, auth_conf in auth.items():
|
||||
# Add authenticator name
|
||||
auths[auth_name] = ({ 'name': auth_name,
|
||||
'type': auth_conf.get('type'),
|
||||
'vendor': auth_conf.get('vendor'),
|
||||
'help': auth_conf.get('help', None)
|
||||
},
|
||||
auth_conf.get('parameters', {}))
|
||||
|
@ -366,12 +366,13 @@ class _AMapParser(object):
|
|||
"""
|
||||
if name == 'authenticator' and 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
|
||||
# it as a 2-tuple
|
||||
return (auth_conf,
|
||||
lambda: init_authenticator(auth_type, **auth_params))
|
||||
lambda: init_authenticator(auth_conf['name'],
|
||||
auth_vendor, **auth_params))
|
||||
|
||||
return value
|
||||
|
||||
|
@ -994,7 +995,7 @@ class ActionsMap(object):
|
|||
actionsmaps[n] = yaml.load(f)
|
||||
|
||||
# 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)
|
||||
|
||||
return actionsmaps
|
||||
|
|
|
@ -4,6 +4,7 @@ import os
|
|||
import sys
|
||||
import time
|
||||
import gettext
|
||||
import logging
|
||||
|
||||
from .helpers import colorize
|
||||
|
||||
|
@ -109,7 +110,7 @@ class Package(object):
|
|||
os.makedirs(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
|
||||
|
||||
Attempt to open in 'mode' the cache file 'filename' from the
|
||||
|
@ -119,85 +120,201 @@ class Package(object):
|
|||
|
||||
Keyword arguments:
|
||||
- filename -- The cache filename
|
||||
- subdir -- A subdirectory which contains the file
|
||||
- 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 -------------------------------------------------------
|
||||
|
||||
import ldap
|
||||
import gnupg
|
||||
|
||||
class AuthenticationError(Exception):
|
||||
pass
|
||||
|
||||
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
|
||||
# Each authenticator classes must implement these properties.
|
||||
|
||||
"""The name of the authenticator"""
|
||||
name = None
|
||||
"""The vendor of the authenticator"""
|
||||
vendor = None
|
||||
|
||||
@property
|
||||
def is_authenticated(self):
|
||||
"""Either the instance is authenticated or not"""
|
||||
raise NotImplementedError("derived class '%s' must override this property" % \
|
||||
self.__class__.__name__)
|
||||
self.__class__.__name__)
|
||||
|
||||
|
||||
## Virtual methods
|
||||
# Each authenticator classes must implement these methods.
|
||||
|
||||
def authenticate(password=None, token=None):
|
||||
def authenticate(password=None):
|
||||
"""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:
|
||||
- password -- A clear text password
|
||||
- token -- A session token
|
||||
|
||||
Returns:
|
||||
An optional session token
|
||||
|
||||
"""
|
||||
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):
|
||||
# TODO: Initialize LDAP connection
|
||||
def __call__(self, password=None, token=None):
|
||||
"""Attempt to authenticate
|
||||
|
||||
if anonymous:
|
||||
self._authenticated = True
|
||||
else:
|
||||
self._authenticated = False
|
||||
Attempt to authenticate either with password or with session
|
||||
token if 'password' is None. If the authentication succeed, the
|
||||
instance is returned and the session is registered for the token
|
||||
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
|
||||
def is_authenticated(self):
|
||||
return self._authenticated
|
||||
if token:
|
||||
try:
|
||||
# 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
|
||||
|
||||
def authenticate(self, password=None, token=None):
|
||||
# TODO: Perform LDAP authentication
|
||||
if password == 'test':
|
||||
self._authenticated = True
|
||||
else:
|
||||
raise MoulinetteError(13, _("Invalid password"))
|
||||
# Store session
|
||||
if store_session:
|
||||
self._store_session(s_id, s_hash, password)
|
||||
|
||||
return self
|
||||
|
||||
|
||||
def init_authenticator(_name, **kwargs):
|
||||
if _name == 'ldap':
|
||||
return LDAPAuthenticator(**kwargs)
|
||||
## Private methods
|
||||
|
||||
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 ----------------------------------------------
|
||||
|
|
|
@ -108,7 +108,7 @@ class MoulinetteCLI(object):
|
|||
"""
|
||||
# TODO: Allow token authentication?
|
||||
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):
|
||||
"""Prompt for a value
|
||||
|
|
Loading…
Reference in a new issue