Work on Authenticators (support for session token and LDAP)

This commit is contained in:
Jerome Lebleu 2014-03-20 21:19:30 +01:00
parent 33752ce01b
commit 5da9f6add8
5 changed files with 169 additions and 53 deletions

View file

@ -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
#############################

View file

@ -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

View file

@ -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

View file

@ -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 ----------------------------------------------

View file

@ -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