Skip to content

Commit

Permalink
Modularise eauth token storage and authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
Kunal Bajpai committed Aug 3, 2017
1 parent 1b9cb26 commit b8e34e1
Show file tree
Hide file tree
Showing 6 changed files with 251 additions and 32 deletions.
40 changes: 8 additions & 32 deletions salt/auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@

# Import python libs
from __future__ import print_function
import os
import collections
import hashlib
import time
import logging
import random
Expand Down Expand Up @@ -56,6 +54,7 @@ def __init__(self, opts, ckminions=None):
self.max_fail = 1.0
self.serial = salt.payload.Serial(opts)
self.auth = salt.loader.auth(opts)
self.tokens = salt.loader.eauth_tokens(opts)
self.ckminions = ckminions or salt.utils.minions.CkMinions(opts)

def load_name(self, load):
Expand Down Expand Up @@ -200,13 +199,6 @@ def mk_token(self, load):
'''
if not self.authenticate_eauth(load):
return {}
fstr = '{0}.auth'.format(load['eauth'])
hash_type = getattr(hashlib, self.opts.get('hash_type', 'md5'))
tok = str(hash_type(os.urandom(512)).hexdigest())
t_path = os.path.join(self.opts['token_dir'], tok)
while os.path.isfile(t_path):
tok = str(hash_type(os.urandom(512)).hexdigest())
t_path = os.path.join(self.opts['token_dir'], tok)

if self._allow_custom_expire(load):
token_expire = load.pop('token_expire', self.opts['token_expire'])
Expand All @@ -217,8 +209,7 @@ def mk_token(self, load):
tdata = {'start': time.time(),
'expire': time.time() + token_expire,
'name': self.load_name(load),
'eauth': load['eauth'],
'token': tok}
'eauth': load['eauth']}

if self.opts['keep_acl_in_token']:
acl_ret = self.__get_acl(load)
Expand All @@ -227,41 +218,26 @@ def mk_token(self, load):
if 'groups' in load:
tdata['groups'] = load['groups']

try:
with salt.utils.files.set_umask(0o177):
with salt.utils.files.fopen(t_path, 'w+b') as fp_:
fp_.write(self.serial.dumps(tdata))
except (IOError, OSError):
log.warning('Authentication failure: can not write token file "{0}".'.format(t_path))
return {}
return tdata
return self.tokens["{0}.mk_token".format(self.opts['eauth_tokens'])](self.opts, tdata)

def get_tok(self, tok):
'''
Return the name associated with the token, or False if the token is
not valid
'''
t_path = os.path.join(self.opts['token_dir'], tok)
if not os.path.isfile(t_path):
return {}
try:
with salt.utils.files.fopen(t_path, 'rb') as fp_:
tdata = self.serial.loads(fp_.read())
except (IOError, OSError):
log.warning('Authentication failure: can not read token file "{0}".'.format(t_path))
tdata = self.tokens["{0}.get_token".format(self.opts['eauth_tokens'])](self.opts, tok)
if not tdata:
return {}

rm_tok = False
if 'expire' not in tdata:
# invalid token, delete it!
rm_tok = True
if tdata.get('expire', '0') < time.time():
rm_tok = True
if rm_tok:
try:
os.remove(t_path)
return {}
except (IOError, OSError):
pass
self.tokens["{0}.rm_token".format(self.opts['eauth_tokens'])](self.opts, tok)

return tdata

def authenticate_token(self, load):
Expand Down
5 changes: 5 additions & 0 deletions salt/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,10 @@ def _gather_buffer_space():
# same module used for external authentication.
'eauth_acl_module': str,

# Subsystem to use to maintain eauth tokens. By default, tokens are stored on the local
# filesystem
'eauth_tokens': str,

# The number of open files a daemon is allowed to have open. Frequently needs to be increased
# higher than the system default in order to account for the way zeromq consumes file handles.
'max_open_files': int,
Expand Down Expand Up @@ -1483,6 +1487,7 @@ def _gather_buffer_space():
'token_expire_user_override': False,
'keep_acl_in_token': False,
'eauth_acl_module': '',
'eauth_tokens': 'localfs',
'extension_modules': os.path.join(salt.syspaths.CACHE_DIR, 'master', 'extmods'),
'file_recv': False,
'file_recv_max_size': 100,
Expand Down
13 changes: 13 additions & 0 deletions salt/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,19 @@ def serializers(opts):
)


def eauth_tokens(opts):
'''
Returns the tokens modules
:param dict opts: The Salt options dictionary
:returns: LazyLoader instance, with only serializers present in the keyspace
'''
return LazyLoader(
_module_dirs(opts, 'tokens'),
opts,
tag='tokens',
)


def auth(opts, whitelist=None):
'''
Returns the auth modules
Expand Down
15 changes: 15 additions & 0 deletions salt/tokens/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
'''
salt.tokens
~~~~~~~~~~~~
This module implements all the token stores used by salt during eauth authentication.
Each store must implement the following methods:
:mk_token: function to mint a new unique token and store it
:get_token: function to get data of a given token if it exists
:rm_token: remove the given token from storage
'''
84 changes: 84 additions & 0 deletions salt/tokens/localfs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-

'''
Stores eauth tokens in the filesystem of the master. Location is configured by the master config option 'token_dir'
'''

from __future__ import absolute_import

import hashlib
import os
import logging

import salt.utils
import salt.payload

log = logging.getLogger(__name__)

__virtualname__ = 'localfs'


def mk_token(opts, tdata):
'''
Mint a new token using the config option hash_type and store tdata with 'token' attribute set
to the token.
This module uses the hash of random 512 bytes as a token.
:param opts: Salt master config options
:param tdata: Token data to be stored with 'token' attirbute of this dict set to the token.
:returns: tdata with token if successful. Empty dict if failed.
'''
hash_type = getattr(hashlib, opts.get('hash_type', 'md5'))
tok = str(hash_type(os.urandom(512)).hexdigest())
t_path = os.path.join(opts['token_dir'], tok)
while os.path.isfile(t_path):
tok = str(hash_type(os.urandom(512)).hexdigest())
t_path = os.path.join(opts['token_dir'], tok)
tdata['token'] = tok
serial = salt.payload.Serial(opts)
try:
with salt.utils.files.set_umask(0o177):
with salt.utils.files.fopen(t_path, 'w+b') as fp_:
fp_.write(serial.dumps(tdata))
except (IOError, OSError):
log.warning('Authentication failure: can not write token file "{0}".'.format(t_path))
return {}
return tdata


def get_token(opts, tok):
'''
Fetch the token data from the store.
:param opts: Salt master config options
:param tok: Token value to get
:returns: Token data if successful. Empty dict if failed.
'''
t_path = os.path.join(opts['token_dir'], tok)
t_path = os.path.join(opts['token_dir'], tok)
if not os.path.isfile(t_path):
return {}
serial = salt.payload.Serial(opts)
try:
with salt.utils.files.fopen(t_path, 'rb') as fp_:
tdata = serial.loads(fp_.read())
return tdata
except (IOError, OSError):
log.warning('Authentication failure: can not read token file "{0}".'.format(t_path))
return {}


def rm_token(opts, tok):
'''
Remove token from the store.
:param opts: Salt master config options
:param tok: Token to remove
:returns: Empty dict if successful. None if failed.
'''
try:
os.remove(t_path)
return {}
except (IOError, OSError):
log.warning('Could not remove token {0}'.format(tok))
pass
126 changes: 126 additions & 0 deletions salt/tokens/rediscluster.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# -*- coding: utf-8 -*-

'''
Provide token storage in Redis cluster.
To get started simply start a redis cluster and assign all hashslots to the connected nodes.
Add the redis hostname and port to master configs as eauth_redis_host and eauth_redis_port.
Default values for these configs are as follow:
.. code-block:: yaml
eauth_redis_host: localhost
eauth_redis_port: 6379
:depends: - redis-py-cluster Python package
'''

from __future__ import absolute_import


try:
import rediscluster
HAS_REDIS = True
except ImportError:
HAS_REDIS = False


import os
import logging
import hashlib

import salt.payload

log = logging.getLogger(__name__)

__virtualname__ = 'rediscluster'


def __virtual__():
if not HAS_REDIS:
return False, 'Could not use redis for tokens; '\
'rediscluster python client is not installed.'
return __virtualname__


def _redis_client(opts):
'''
Connect to the redis host and return a StrictRedisCluster client object.
If connection fails then return None.
'''
redis_host = opts.get("eauth_redis_host", "localhost")
redis_port = opts.get("eauth_redis_port", 6379)
try:
return rediscluster.StrictRedisCluster(host=redis_host, port=redis_port)
except rediscluster.exceptions.RedisClusterException as err:
log.warning("Failed to connect to redis at {0}:{1} - {2}".format(redis_host, redis_port, err))
return None


def mk_token(opts, tdata):
'''
Mint a new token using the config option hash_type and store tdata with 'token' attribute set
to the token.
This module uses the hash of random 512 bytes as a token.
:param opts: Salt master config options
:param tdata: Token data to be stored with 'token' attirbute of this dict set to the token.
:returns: tdata with token if successful. Empty dict if failed.
'''
redis_client = _redis_client(opts)
if not redis_client:
return {}
hash_type = getattr(hashlib, opts.get('hash_type', 'md5'))
tok = str(hash_type(os.urandom(512)).hexdigest())
try:
while redis_client.get(tok) is not None:
tok = str(hash_type(os.urandom(512)).hexdigest())
except Exception as err:
log.warning("Authentication failure: cannot get token {0} from redis: {1}".format(tok, err))
return {}
tdata['token'] = tok
serial = salt.payload.Serial(opts)
try:
redis_client.set(tok, serial.dumps(tdata))
except Exception as err:
log.warning("Authentication failure: cannot save token {0} to redis: {1}".format(tok, err))
return {}
return tdata


def get_token(opts, tok):
'''
Fetch the token data from the store.
:param opts: Salt master config options
:param tok: Token value to get
:returns: Token data if successful. Empty dict if failed.
'''
redis_client = _redis_client(opts)
if not redis_client:
return {}
serial = salt.payload.Serial(opts)
try:
tdata = serial.loads(redis_client.get(tok))
return tdata
except Exception as err:
log.warning("Authentication failure: cannot get token {0} from redis: {1}".format(tok, err))
return {}


def rm_token(opts, tok):
'''
Remove token from the store.
:param opts: Salt master config options
:param tok: Token to remove
:returns: Empty dict if successful. None if failed.
'''
redis_client = _redis_client(opts)
if not redis_client:
return
try:
redis_client.delete(tok)
return {}
except Exception as err:
log.warning("Could not remove token {0}: {1}".format(tok, err))

0 comments on commit b8e34e1

Please sign in to comment.