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

"""Keystone Client functionality for use by resources."""

import collections
import uuid

from keystoneclient.auth.identity import v3 as kc_auth_v3
import keystoneclient.exceptions as kc_exception
from keystoneclient import session
from keystoneclient.v3 import client as kc_v3
from oslo_config import cfg
from oslo_log import log as logging
from oslo_serialization import jsonutils
from oslo_utils import importutils

from heat.common import context
from heat.common import exception
from heat.common.i18n import _
from heat.common.i18n import _LE
from heat.common.i18n import _LW

LOG = logging.getLogger('heat.common.keystoneclient')

AccessKey = collections.namedtuple('AccessKey', ['id', 'access', 'secret'])

_default_keystone_backend = "heat.common.heat_keystoneclient.KeystoneClientV3"

keystone_opts = [
    cfg.StrOpt('keystone_backend',
               default=_default_keystone_backend,
               help="Fully qualified class name to use as a keystone backend.")
]
cfg.CONF.register_opts(keystone_opts)


class KeystoneClientV3(object):

    """Wrap keystone client so we can encapsulate logic used in resources.

    Note this is intended to be initialized from a resource on a per-session
    basis, so the session context is passed in on initialization
    Also note that an instance of this is created in each request context as
    part of a lazy-loaded cloud backend and it can be easily referenced in
    each resource as ``self.keystone()``, so there should not be any need to
    directly instantiate instances of this class inside resources themselves.
    """

    def __init__(self, context):
        # If a trust_id is specified in the context, we immediately
        # authenticate so we can populate the context with a trust token
        # otherwise, we delay client authentication until needed to avoid
        # unnecessary calls to keystone.
        #
        # Note that when you obtain a token using a trust, it cannot be
        # used to reauthenticate and get another token, so we have to
        # get a new trust-token even if context.auth_token is set.
        #
        # - context.auth_url is expected to contain a versioned keystone
        #   path, we will work with either a v2.0 or v3 path
        self.context = context
        self._client = None
        self._admin_auth = None
        self._domain_admin_auth = None
        self._domain_admin_client = None

        self.session = session.Session.construct(self._ssl_options())
        self.v3_endpoint = self.context.keystone_v3_endpoint

        if self.context.trust_id:
            # Create a client with the specified trust_id, this
            # populates self.context.auth_token with a trust-scoped token
            self._client = self._v3_client_init()

        # The stack domain user ID should be set in heat.conf
        # It can be created via python-openstackclient
        # openstack --os-identity-api-version=3 domain create heat
        # If the domain is specified, then you must specify a domain
        # admin user.  If no domain is specified, we fall back to
        # legacy behavior with warnings.
        self._stack_domain_id = cfg.CONF.stack_user_domain_id
        self.stack_domain_name = cfg.CONF.stack_user_domain_name
        self.domain_admin_user = cfg.CONF.stack_domain_admin
        self.domain_admin_password = cfg.CONF.stack_domain_admin_password

        LOG.debug('Using stack domain %s' % self.stack_domain)

    @property
    def stack_domain(self):
        """Domain scope data.

        This is only used for checking for scoping data, not using the value
        """
        return self._stack_domain_id or self.stack_domain_name

    @property
    def client(self):
        if not self._client:
            # Create connection to v3 API
            self._client = self._v3_client_init()
        return self._client

    @property
    def admin_auth(self):
        if not self._admin_auth:
            importutils.import_module('keystonemiddleware.auth_token')

            self._admin_auth = kc_auth_v3.Password(
                username=cfg.CONF.keystone_authtoken.admin_user,
                password=cfg.CONF.keystone_authtoken.admin_password,
                user_domain_id='default',
                auth_url=self.v3_endpoint)

        return self._admin_auth

    @property
    def domain_admin_auth(self):
        if not self._domain_admin_auth:
            # Note we must specify the domain when getting the token
            # as only a domain scoped token can create projects in the domain
            auth = kc_auth_v3.Password(username=self.domain_admin_user,
                                       password=self.domain_admin_password,
                                       auth_url=self.v3_endpoint,
                                       domain_id=self._stack_domain_id,
                                   
    domain_name=self.stack_domain_name,
                                       user_domain_id=self._stack_
domain_id,
                                       user_domain_name=self.stack_domain_name)

            # NOTE(jamielennox): just do something to ensure a valid token
            try:
                auth.get_token(self.session)
            except kc_exception.Unauthorized:
                LOG.error(_LE("Domain admin client authentication failed"))
                raise exception.AuthorizationFailure()

            self._domain_admin_auth = auth

        return self._domain_admin_auth

    @property
    def domain_admin_client(self):
        if not self._domain_admin_client:
            self._domain_admin_client = kc_v3.Client(
                session=self.session,
                auth=self.domain_admin_auth)

        return self._domain_admin_client

    def _v3_client_init(self):
        client = kc_v3.Client(session=self.session,
                              auth=self.context.auth_plugin)

        if hasattr(self.context.auth_plugin, 'get_access'):
            # NOTE(jamielennox): get_access returns the current token without
            # reauthenticating if it's present and valid.
            try:
                auth_ref = self.context.auth_plugin.get_access(self.session)
            except kc_exception.Unauthorized:
                LOG.error(_LE("Keystone client authentication failed"))
                raise exception.AuthorizationFailure()

            if self.context.trust_id:
                # Sanity check
                if not auth_ref.trust_scoped:
                    LOG.error(_LE("trust token re-scoping failed!"))
                    raise exception.AuthorizationFailure()
                # Sanity check that impersonation is effective
                if self.context.trustor_user_id != auth_ref.user_id:
                    LOG.error(_LE("Trust impersonation failed"))
                    raise exception.AuthorizationFailure()

        return client

    def _ssl_options(self):
        opts = {'cacert': self._get_client_option('ca_file'),
                'insecure': self._get_client_option('insecure'),
                'cert': self._get_client_option('cert_file'),
                'key': self._get_client_option('key_file')}
        return opts

    def _get_client_option(self, option):
        # look for the option in the [clients_keystone] section
        # unknown options raise cfg.NoSuchOptError
        cfg.CONF.import_opt(option, 'heat.common.config',
                            group='clients_keystone')
        v = getattr(cfg.CONF.clients_keystone, option)
        if v is not None:
            return v
        # look for the option in the generic [clients] section
  