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

"""
Auth driver for ldap.  Includes FakeLdapDriver.

It should be easy to create a replacement for this driver supporting
other backends by creating another class that exposes the same
public methods.
"""

import functools
import sys

from nova import exception
from nova import flags
from nova import log as logging
from nova.openstack.common import cfg


ldap_opts = [
    cfg.IntOpt('ldap_schema_version',
               default=2,
               help='Current version of the LDAP schema'),
    cfg.StrOpt('ldap_url',
               default='ldap://localhost',
               help='Point this at your ldap server'),
    cfg.StrOpt('ldap_password',
               default='changeme',
               help='LDAP password'),
    cfg.StrOpt('ldap_user_dn',
               default='cn=Manager,dc=example,dc=com',
               help='DN of admin user'),
    cfg.StrOpt('ldap_user_id_attribute',
               default='uid',
               help='Attribute to use as id'),
    cfg.StrOpt('ldap_user_name_attribute',
               default='cn',
               help='Attribute to use as name'),
    cfg.StrOpt('ldap_user_unit',
               default='Users',
               help='OID for Users'),
    cfg.StrOpt('ldap_user_subtree',
               default='ou=Users,dc=example,dc=com',
               help='OU for Users'),
    cfg.BoolOpt('ldap_user_modify_only',
                default=False,
                help='Modify user attributes instead of creating/deleting'),
    cfg.StrOpt('ldap_project_subtree',
               default='ou=Groups,dc=example,dc=com',
               help='OU for Projects'),
    cfg.StrOpt('role_project_subtree',
               default='ou=Groups,dc=example,dc=com',
               help='OU for Roles'),

    # NOTE(vish): mapping with these flags is necessary because we're going
    #             to tie in to an existing ldap schema
    cfg.StrOpt('ldap_cloudadmin',
               default='cn=cloudadmins,ou=Groups,dc=example,dc=com',
               help='cn for Cloud Admins'),
    cfg.StrOpt('ldap_itsec',
               default='cn=itsec,ou=Groups,dc=example,dc=com',
               help='cn for ItSec'),
    cfg.StrOpt('ldap_sysadmin',
               default='cn=sysadmins,ou=Groups,dc=example,dc=com',
               help='cn for Sysadmins'),
    cfg.StrOpt('ldap_netadmin',
               default='cn=netadmins,ou=Groups,dc=example,dc=com',
               help='cn for NetAdmins'),
    cfg.StrOpt('ldap_developer',
               default='cn=developers,ou=Groups,dc=example,dc=com',
               help='cn for Developers'),
    ]

FLAGS = flags.FLAGS
FLAGS.register_opts(ldap_opts)

LOG = logging.getLogger(__name__)


if FLAGS.memcached_servers:
    import memcache
else:
    from nova.common import memorycache as memcache


# TODO(vish): make an abstract base class with the same public methods
#             to define a set interface for AuthDrivers. I'm delaying
#             creating this now because I'm expecting an auth refactor
#             in which we may want to change the interface a bit more.


def _clean(attr):
    """Clean attr for insertion into ldap"""
    if attr is None:
        return None
    if isinstance(attr, unicode):
        return str(attr)
    return attr


def sanitize(fn):
    """Decorator to sanitize all args"""
    @functools.wraps(fn)
    def _wrapped(self, *args, **kwargs):
        args = [_clean(x) for x in args]
        kwargs = dict((k, _clean(v)) for (k, v) in kwargs)
        return fn(self, *args, **kwargs)
    _wrapped.func_name = fn.func_name
    return _wrapped


class LDAPWrapper(object):
    def __init__(self, ldap, url, user, password):
        self.ldap = ldap
        self.url = url
        self.user = user
        self.password = password
        self.conn = None

    def __wrap_reconnect(f):
        def inner(self, *args, **kwargs):
            if self.conn is None:
                self.connect()
                return f(self.conn)(*args, **kwargs)
            else:
                try:
                    return f(self.conn)(*args, **kwargs)
                except self.ldap.SERVER_DOWN:
                    self.connect()
                    return f(self.conn)(*args, **kwargs)
        return inner

    def connect(self):
        try:
            self.conn = self.ldap.initialize(self.url)
            self.conn.simple_bind_s(self.user, self.password)
        except self.ldap.SERVER_DOWN:
            self.conn = None
            raise

    search_s = __wrap_reconnect(lambda conn: conn.search_s)
    add_s = __wrap_reconnect(lambda conn: conn.add_s)
    delete_s = __wrap_reconnect(lambda conn: conn.delete_s)
    modify_s = __wrap_reconnect(lambda conn: conn.modify_s)


class LdapDriver(object):
    """Ldap Auth driver

    Defines enter and exit and therefore supports the with/as syntax.
    """

    project_pattern = '(owner=*)'
    isadmin_attribute = 'isNovaAdmin'
    project_attribute = 'owner'
    project_objectclass = 'groupOfNames'
    conn = None
    mc = None

    def __init__(self):
        """Imports the LDAP module"""
        self.ldap = __import__('ldap')
        if FLAGS.ldap_schema_version == 1:
            LdapDriver.project_pattern = '(objectclass=novaProject)'
            LdapDriver.isadmin_attribute = 'isAdmin'
            LdapDriver.project_attribute = 'projectManager'
            LdapDriver.project_objectclass = 'novaProject'
        self.__cache = None
        if LdapDriver.conn is None:
            LdapDriver.conn = LDAPWrapper(self.ldap, FLAGS.ldap_url,
                                          FLAGS.ldap_user_dn,
                                          FLAGS.ldap_password)
        if LdapDriver.mc is None:
            LdapDriver.mc = memcache.Client(FLAGS.memcached_servers, debug=0)

    def __enter__(self):
        # TODO(yorik-sar): Should be per-request cache, not per-driver-request
        self.__cache = {}
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self.__cache = None
        return False

    def __local_cache(key_fmt):  # pylint: disable=E0213
        """Wrap function to cache it's result in self.__cache.
        Works only with functions with one fixed argument.
        """
        def do_wrap(fn):
            @functools.wraps(fn)
            def inner(self, arg, **kwargs):
                cache_key = key_fmt % (arg,)
                try:
                    res = self.__cache[cache_key]
                    LOG.debug('Local cache hit for %s by key %s' %
                              (fn.__name__, cache_key))
                    return res
                except KeyError:
                    res = fn(self, arg, **kwargs)
                    self.__cache[cache_key] = res
                    return res
            return inner
        return do_wrap

    @sanitize
    @__local_cache('uid_user-%s')
    def get_user(self, uid):
        """Retrieve user by id"""
        attr = self.__get_ldap_user(uid)
        if attr is None:
            raise exception.LDAPUserNotFound(user_id=uid)
        return self.__to_user(attr)

    @sanitize
    def get_user_from_access_key(self, access):
        """Retrieve user by access key"""
        cache_key = 'uak_dn_%s' % (access,)
        user_dn = self.mc.get(cache_key)
        if user_dn:
            user = self.__to_user(
                self.__find_object(user_dn, scope=self.ldap.SCOPE_BASE))
            if user:
                if user['access'] == access:
                    return user
                else:
                    self.mc.set(cache_key, None)
        query = '(accessKey=%s)' % access
        dn = FLAGS.ldap_user_subtree
        user_obj = self.__find_object(dn, query)
        user = self.__to_user(user_obj)
        if user:
            self.mc.set(cache_key, user_obj['dn'][0])
        return user

    @sanitize
    @__local_cache('pid_project-%s')
    def get_project(self, pid):
        """Retrieve project by id"""
        dn = self.__project_to_dn(pid, search=False)
        attr = self.__find_object(dn, LdapDriver.project_pattern,
                                  scope=self.ldap.SCOPE_BASE)
        return self.__to_project(attr)

    @sanitize
    def get_users(self):
        """Retrieve list of users"""
        attrs = self.__find_objects(FLAGS.ldap_user_subtree,
                                    '(objectclass=novaUser)')
        users = []
        for attr in attrs:
            user = self.__to_user(attr)
            if user is not None:
                users.append(user)
        return users

    @sanitize
    def get_projects(self, uid=None):
        """Retrieve list of projects"""
        pattern = LdapDriver.project_pattern
        if uid:
            pattern = "(&%s(member=%s))" % (pattern, self.__uid_to_dn(uid))
        attrs = self.__find_objects(FLAGS.ldap_project_subtree,
                                    pattern)
        return [self.__to_project(attr) for attr in attrs]

    @sanitize
    def create_user(self, name, access_key, secret_key, is_admin):
        """Create a user"""
        if self.__user_exists(name):
            raise exception.LDAPUserExists(user=name)
        if FLAGS.ldap_user_modify_only:
            if self.__ldap_user_exists(name):
                # Retrieve user by name
                user = self.__get_ldap_user(name)
                # Entry could be malformed, test for missing attrs.
                # Malformed entries are useless, replace attributes found.
                attr = []
                if 'secretKey' in user.keys():
                    attr.append((self.ldap.MOD_REPLACE, 'secretKey',
                                 [secret_key]))
                else:
                    attr.append((self.ldap.MOD_ADD, 'secretKey',
                                 [secret_key]))
                if 'accessKey' in user.keys():