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

# This file handles all flask-restful resources for /v3/users

import base64
import secrets
import uuid

import flask
import http.client
from oslo_serialization import jsonutils
from werkzeug import exceptions

from keystone.api._shared import json_home_relations
from keystone.application_credential import schema as app_cred_schema
from keystone.common import json_home
from keystone.common import provider_api
from keystone.common import rbac_enforcer
from keystone.common import utils
from keystone.common import validation
import keystone.conf
from keystone import exception as ks_exception
from keystone.i18n import _
from keystone.identity import schema
from keystone import notifications
from keystone.server import flask as ks_flask


CRED_TYPE_EC2 = 'ec2'
CONF = keystone.conf.CONF
ENFORCER = rbac_enforcer.RBACEnforcer
PROVIDERS = provider_api.ProviderAPIs

ACCESS_TOKEN_ID_PARAMETER_RELATION = (
    json_home_relations.os_oauth1_parameter_rel_func(
        parameter_name='access_token_id')
)


def _convert_v3_to_ec2_credential(credential):
    # Prior to bug #1259584 fix, blob was stored unserialized
    # but it should be stored as a json string for compatibility
    # with the v3 credentials API.  Fall back to the old behavior
    # for backwards compatibility with existing DB contents
    try:
        blob = jsonutils.loads(credential['blob'])
    except TypeError:
        blob = credential['blob']
    return {'user_id': credential.get('user_id'),
            'tenant_id': credential.get('project_id'),
            'access': blob.get('access'),
            'secret': blob.get('secret'),
            'trust_id': blob.get('trust_id')}


def _format_token_entity(entity):

    formatted_entity = entity.copy()
    access_token_id = formatted_entity['id']
    user_id = formatted_entity.get('authorizing_user_id', '')
    if 'role_ids' in entity:
        formatted_entity.pop('role_ids')
    if 'access_secret' in entity:
        formatted_entity.pop('access_secret')

    url = ('/users/%(user_id)s/OS-OAUTH1/access_tokens/%(access_token_id)s'
           '/roles' % {'user_id': user_id,
                       'access_token_id': access_token_id})

    formatted_entity.setdefault('links', {})
    formatted_entity['links']['roles'] = (ks_flask.base_url(url))

    return formatted_entity


def _check_unrestricted_application_credential(token):
    if 'application_credential' in token.methods:
        if not token.application_credential['unrestricted']:
            action = _("Using method 'application_credential' is not "
                       "allowed for managing additional application "
                       "credentials.")
            raise ks_exception.ForbiddenAction(action=action)


def _build_user_target_enforcement():
    target = {}
    try:
        target['user'] = PROVIDERS.identity_api.get_user(
            flask.request.view_args.get('user_id')
        )
        if flask.request.view_args.get('group_id'):
            target['group'] = PROVIDERS.identity_api.get_group(
                flask.request.view_args.get('group_id')
            )
    except ks_exception.NotFound:  # nosec
        # Defer existence in the event the user doesn't exist, we'll
        # check this later anyway.
        pass

    return target


def _build_enforcer_target_data_owner_and_user_id_match():
    ref = {}
    if flask.request.view_args:
        credential_id = flask.request.view_args.get('credential_id')
        if credential_id is not None:
            hashed_id = utils.hash_access_key(credential_id)
            ref['credential'] = PROVIDERS.credential_api.get_credential(
                hashed_id)
    return ref


def _update_request_user_id_attribute():
    # This method handles a special case in policy enforcement. The application
    # credential API is underneath the user path (e.g.,
    # /v3/users/{user_id}/application_credentials/{application_credential_id}).
    # The RBAC enforcer thinks the user to evaluate for application credential
    # ownership comes from the path, but it should come from the actual
    # application credential reference. By ensuring we pull the user ID from
    # the application credential, we close a loop hole where users could
    # effectively bypass authorization to view or delete any application
    # credential in the system, assuming the attacker knows the application
    # credential ID of another user. So long as the attacker matches the user
    # ID in the request path to the user in the token of the request, they can
    # pass the `rule:owner` policy check. This method protects against that by
    # ensuring we use the application credential user ID and not something
    # determined from the client.
    try:
        app_cred = (
            PROVIDERS.application_credential_api.get_application_credential(
                flask.request.view_args.get('application_credential_id')
            )
        )
        flask.request.view_args['user_id'] = app_cred['user_id']

        # This target isn't really used in the default policy for application
        # credentials, but we return it since we're using this method as a hook
        # to update the flask request variables, which are used later in the
        # keystone RBAC enforcer to populate the policy_dict, which ultimately
        # turns into target attributes.
        return {'user_id': app_cred['user_id']}
    except ks_exception.NotFound:  # nosec
        # Defer existance in the event the application credential doesn't
        # exist, we'll check this later anyway.
        pass


def _format_role_entity(role_id):
    role = PROVIDERS.role_api.get_role(role_id)
    formatted_entity = role.copy()
    if 'description' in role:
        formatted_entity.pop('description')
    if 'enabled' in role:
        formatted_entity.pop('enabled')
    return formatted_entity


class UserResource(ks_flask.ResourceBase):
    collection_key = 'users'
    member_key = 'user'
    get_member_from_driver = PROVIDERS.deferred_provider_lookup(
        api='identity_api', method='get_user')

    def get(self, user_id=None):
        """Get a user resource or list users.

        GET/HEAD /v3/users
        GET/HEAD /v3/users/{user_id}
        """
        if user_id is not None:
            return self._get_user(user_id)
        return self._list_users()

    def _get_user(self, user_id):
        """Get a user resource.

        GET/HEAD /v3/users/{user_id}
        """
        ENFORCER.enforce_call(
            action='identity:get_user',
            build_target=_build_user_target_enforcement
        )
        ref = PROVIDERS.identity_api.get_user(user_id)
        return self.wrap_member(ref)

    def _list_users(self):
        """List users.

        GET/HEAD /v3/users
        """
        filters = ('domain_id', 'enabled', 'idp_id', 'name', 'protocol_id',
                   'unique_id', 'password_expires_at')
        target = None
        if self.oslo_context.domain_id:
            target = {'domain_id': self.oslo_context.domain_id}
        hints = self.build_driver_hints(filters)
        ENFORCER.enforce_call(
            action='identity:list_users', filters=filters, target_attr=target
        )
        domain = self._get_domain_id_for_list_request()
        if domain is None and self.oslo_context.domain_id:
            domain = self.oslo_context.domain_id
        refs = PROVIDERS.identity_api.list_users(
            domain_scope=domain, hints=hints)

        # If the user making the request used a domain-scoped token, let's make
        # sure we filter out users that are not in that domain. Otherwise, we'd
        # be exposing users in other domains. This if statement is needed in
        # case _get_domain_id_for_list_request() short-circuits due to
        # configuration and protects against information from other domains
        # leaking to people who shouldn't see it.
        if self.oslo_context.domain_id:
            domain_id = self.oslo_context.domain_id
            users = [user for user in refs if user['domain_id'] == domain_id]
        else: