
Adjust auth unittests to skip fakeldap tests if Redis isn't around. Adjust auth unittests to actually run the fakeldap tests if Redis /is/ around.
259 lines
7.7 KiB
Python
259 lines
7.7 KiB
Python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
|
|
# Copyright 2010 United States Government as represented by the
|
|
# Administrator of the National Aeronautics and Space Administration.
|
|
# All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
"""
|
|
Fake LDAP server for test harnesses.
|
|
|
|
This class does very little error checking, and knows nothing about ldap
|
|
class definitions. It implements the minimum emulation of the python ldap
|
|
library to work with nova.
|
|
"""
|
|
|
|
import json
|
|
import redis
|
|
|
|
from nova import flags
|
|
|
|
FLAGS = flags.FLAGS
|
|
flags.DEFINE_string('redis_host', '127.0.0.1',
|
|
'Host that redis is running on.')
|
|
flags.DEFINE_integer('redis_port', 6379,
|
|
'Port that redis is running on.')
|
|
flags.DEFINE_integer('redis_db', 0, 'Multiple DB keeps tests away')
|
|
|
|
class Redis(object):
|
|
def __init__(self):
|
|
if hasattr(self.__class__, '_instance'):
|
|
raise Exception('Attempted to instantiate singleton')
|
|
|
|
@classmethod
|
|
def instance(cls):
|
|
if not hasattr(cls, '_instance'):
|
|
inst = redis.Redis(host=FLAGS.redis_host,
|
|
port=FLAGS.redis_port,
|
|
db=FLAGS.redis_db)
|
|
cls._instance = inst
|
|
return cls._instance
|
|
|
|
|
|
SCOPE_BASE = 0
|
|
SCOPE_ONELEVEL = 1 # not implemented
|
|
SCOPE_SUBTREE = 2
|
|
MOD_ADD = 0
|
|
MOD_DELETE = 1
|
|
MOD_REPLACE = 2
|
|
|
|
|
|
class NO_SUCH_OBJECT(Exception): # pylint: disable-msg=C0103
|
|
"""Duplicate exception class from real LDAP module."""
|
|
pass
|
|
|
|
|
|
class OBJECT_CLASS_VIOLATION(Exception): # pylint: disable-msg=C0103
|
|
"""Duplicate exception class from real LDAP module."""
|
|
pass
|
|
|
|
|
|
def initialize(_uri):
|
|
"""Opens a fake connection with an LDAP server."""
|
|
return FakeLDAP()
|
|
|
|
|
|
def _match_query(query, attrs):
|
|
"""Match an ldap query to an attribute dictionary.
|
|
|
|
&, |, and ! are supported in the query. No syntax checking is performed,
|
|
so malformed querys will not work correctly.
|
|
|
|
"""
|
|
# cut off the parentheses
|
|
inner = query[1:-1]
|
|
if inner.startswith('&'):
|
|
# cut off the &
|
|
l, r = _paren_groups(inner[1:])
|
|
return _match_query(l, attrs) and _match_query(r, attrs)
|
|
if inner.startswith('|'):
|
|
# cut off the |
|
|
l, r = _paren_groups(inner[1:])
|
|
return _match_query(l, attrs) or _match_query(r, attrs)
|
|
if inner.startswith('!'):
|
|
# cut off the ! and the nested parentheses
|
|
return not _match_query(query[2:-1], attrs)
|
|
|
|
(k, _sep, v) = inner.partition('=')
|
|
return _match(k, v, attrs)
|
|
|
|
|
|
def _paren_groups(source):
|
|
"""Split a string into parenthesized groups."""
|
|
count = 0
|
|
start = 0
|
|
result = []
|
|
for pos in xrange(len(source)):
|
|
if source[pos] == '(':
|
|
if count == 0:
|
|
start = pos
|
|
count += 1
|
|
if source[pos] == ')':
|
|
count -= 1
|
|
if count == 0:
|
|
result.append(source[start:pos + 1])
|
|
return result
|
|
|
|
|
|
def _match(key, value, attrs):
|
|
"""Match a given key and value against an attribute list."""
|
|
if key not in attrs:
|
|
return False
|
|
if key != "objectclass":
|
|
return value in attrs[key]
|
|
# it is an objectclass check, so check subclasses
|
|
values = _subs(value)
|
|
for v in values:
|
|
if v in attrs[key]:
|
|
return True
|
|
return False
|
|
|
|
|
|
def _subs(value):
|
|
"""Returns a list of subclass strings.
|
|
|
|
The strings represent the ldap objectclass plus any subclasses that
|
|
inherit from it. Fakeldap doesn't know about the ldap object structure,
|
|
so subclasses need to be defined manually in the dictionary below.
|
|
|
|
"""
|
|
subs = {'groupOfNames': ['novaProject']}
|
|
if value in subs:
|
|
return [value] + subs[value]
|
|
return [value]
|
|
|
|
|
|
def _from_json(encoded):
|
|
"""Convert attribute values from json representation.
|
|
|
|
Args:
|
|
encoded -- a json encoded string
|
|
|
|
Returns a list of strings
|
|
|
|
"""
|
|
return [str(x) for x in json.loads(encoded)]
|
|
|
|
|
|
def _to_json(unencoded):
|
|
"""Convert attribute values into json representation.
|
|
|
|
Args:
|
|
unencoded -- an unencoded string or list of strings. If it
|
|
is a single string, it will be converted into a list.
|
|
|
|
Returns a json string
|
|
|
|
"""
|
|
return json.dumps(list(unencoded))
|
|
|
|
|
|
class FakeLDAP(object):
|
|
#TODO(vish): refactor this class to use a wrapper instead of accessing
|
|
# redis directly
|
|
"""Fake LDAP connection."""
|
|
|
|
def simple_bind_s(self, dn, password):
|
|
"""This method is ignored, but provided for compatibility."""
|
|
pass
|
|
|
|
def unbind_s(self):
|
|
"""This method is ignored, but provided for compatibility."""
|
|
pass
|
|
|
|
def add_s(self, dn, attr):
|
|
"""Add an object with the specified attributes at dn."""
|
|
key = "%s%s" % (self.__redis_prefix, dn)
|
|
|
|
value_dict = dict([(k, _to_json(v)) for k, v in attr])
|
|
Redis.instance().hmset(key, value_dict)
|
|
|
|
def delete_s(self, dn):
|
|
"""Remove the ldap object at specified dn."""
|
|
Redis.instance().delete("%s%s" % (self.__redis_prefix, dn))
|
|
|
|
def modify_s(self, dn, attrs):
|
|
"""Modify the object at dn using the attribute list.
|
|
|
|
Args:
|
|
dn -- a dn
|
|
attrs -- a list of tuples in the following form:
|
|
([MOD_ADD | MOD_DELETE | MOD_REPACE], attribute, value)
|
|
|
|
"""
|
|
redis = Redis.instance()
|
|
key = "%s%s" % (self.__redis_prefix, dn)
|
|
|
|
for cmd, k, v in attrs:
|
|
values = _from_json(redis.hget(key, k))
|
|
if cmd == MOD_ADD:
|
|
values.append(v)
|
|
elif cmd == MOD_REPLACE:
|
|
values = [v]
|
|
else:
|
|
values.remove(v)
|
|
values = redis.hset(key, k, _to_json(values))
|
|
|
|
def search_s(self, dn, scope, query=None, fields=None):
|
|
"""Search for all matching objects under dn using the query.
|
|
|
|
Args:
|
|
dn -- dn to search under
|
|
scope -- only SCOPE_BASE and SCOPE_SUBTREE are supported
|
|
query -- query to filter objects by
|
|
fields -- fields to return. Returns all fields if not specified
|
|
|
|
"""
|
|
if scope != SCOPE_BASE and scope != SCOPE_SUBTREE:
|
|
raise NotImplementedError(str(scope))
|
|
redis = Redis.instance()
|
|
if scope == SCOPE_BASE:
|
|
keys = ["%s%s" % (self.__redis_prefix, dn)]
|
|
else:
|
|
keys = redis.keys("%s*%s" % (self.__redis_prefix, dn))
|
|
objects = []
|
|
for key in keys:
|
|
# get the attributes from redis
|
|
attrs = redis.hgetall(key)
|
|
# turn the values from redis into lists
|
|
# pylint: disable-msg=E1103
|
|
attrs = dict([(k, _from_json(v))
|
|
for k, v in attrs.iteritems()])
|
|
# filter the objects by query
|
|
if not query or _match_query(query, attrs):
|
|
# filter the attributes by fields
|
|
attrs = dict([(k, v) for k, v in attrs.iteritems()
|
|
if not fields or k in fields])
|
|
objects.append((key[len(self.__redis_prefix):], attrs))
|
|
# pylint: enable-msg=E1103
|
|
if objects == []:
|
|
raise NO_SUCH_OBJECT()
|
|
return objects
|
|
|
|
@property
|
|
def __redis_prefix(self): # pylint: disable-msg=R0201
|
|
"""Get the prefix to use for all redis keys."""
|
|
return 'ldap:'
|
|
|
|
|