<commit_before># Copyright (c) 2011 OpenStack, LLC.
#
# 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.

import json

from urllib import quote
from urlparse import urlparse

from webob.exc import HTTPForbidden, HTTPNotFound, \
    HTTPUnauthorized, HTTPBadRequest
from webob import Request

from swift.common.utils import cache_from_env, get_logger, split_path, \
    get_remote_client
from swift.common.middleware.acl import clean_acl, parse_acl, referrer_allowed
from swift.common.bufferedhttp import http_connect_raw as http_connect
from time import time, mktime
from datetime import datetime


class AuthProtocol(object):
    """
    Keystone to Swift authentication and authorization system.

    Add to your pipeline in proxy-server.conf, such as::

        [pipeline:main]
        pipeline = catch_errors cache keystone proxy-server

    Set account auto creation to true::

        [app:proxy-server]
        account_autocreate = true

    And add a keystone filter section, such as::

        [filter:keystone]
        use = egg:keystone#swiftauth
        keystone_url = http://keystone_url:5000/v2.0
        keystone_admin_token = <PASSWORD>
        keystone_admin_group = Admin

    This middleware handle ACL as well and allow Keystone groups to
    map to swift ACL.

    :param app: The next WSGI app in the pipeline
    :param conf: The dict of configuration values
    """
    def __init__(self, app, conf):
        self.app = app
        self.conf = conf
        self.logger = get_logger(conf, log_route='keystone')
        self.reseller_prefix = conf.get('reseller_prefix', 'AUTH').strip()
        #TODO: Error out if no url
        self.keystone_url = urlparse(conf.get('keystone_url'))
        self.keystone_admin_group = conf.get('keystone_admin_group', 'Admin')
        self.admin_token = conf.get('keystone_admin_token')
        self.auth_timeout = conf.get('keystone_auth_timeout', 30)
        self.allowed_sync_hosts = [h.strip()
            for h in conf.get('allowed_sync_hosts', '127.0.0.1').split(',')
            if h.strip()]

    def __call__(self, environ, start_response):
        self.logger.debug('Initialising keystone middleware')

        req = Request(environ)
        token = environ.get('HTTP_X_AUTH_TOKEN',
                            environ.get('HTTP_X_STORAGE_TOKEN'))
        if not token:
            self.logger.debug('No token: exiting')
            environ['swift.authorize'] = self.denied_response
            return self.app(environ, start_response)

        self.logger.debug('Got token: %s' % (token))

        identity = None
        memcache_client = cache_from_env(environ)
        memcache_key = 'tokens/%s' % (token)
        candidate_cache = memcache_client.get(memcache_key)
        if candidate_cache:
            expires, _identity = candidate_cache
            if expires > time():
                self.logger.debug('getting identity info from memcache')
                identity = _identity

        if not identity:
            identity = self._keystone_validate_token(token)
            if identity and memcache_client:
                expires = identity['expires']
                memcache_client.set(memcache_key,
                                    (expires, identity),
                                    timeout=expires - time())
                ts = str(datetime.fromtimestamp(expires))
                self.logger.debug('setting memcache expiration to %s' % ts)
            else:  # if we didn't get identity it means there was an error.
                return HTTPBadRequest(request=req)

        if not identity:
            #TODO: non authenticated access allow via refer
            environ['swift.authorize'] = self.denied_response
            return self.app(environ, start_response)

        self.logger.debug("Using identity: %r" % (identity))
        environ['keystone.identity'] = identity
        environ['REMOTE_USER'] = identity.get('tenant')
        environ['swift.authorize'] = self.authorize
        environ['swift.clean_acl'] = clean_acl
        return self.app(environ, start_response)

    def convert_date(self, date):
        """ Convert datetime to unix timestamp """
        return mktime(datetime.strptime(
                date[:date.rfind(':')].replace('-', ''), "%Y%m%dT%H:%M",
                ).timetuple())

    def _keystone_validate_token(self, claim):
        """
        Will take a claimed token and validate it in keystone.
        """
        headers = {"X-Auth-Token": self.admin_token}
        conn = http_connect(self.keystone_url.hostname,
                            self.keystone_url.port, 'GET',
                            '%s/tokens/%s' % \
                                (self.keystone_url.path,
                                 quote(claim)),
                            headers=headers,
                            ssl=(self.keystone_url.scheme == 'https'),
                            timeout=self.auth_timeout)
        resp = conn.getresponse()
        data = resp.read()
        conn.close()

        if not str(resp.status).startswith('20'):
            #TODO: Make the self.keystone_url more meaningfull
            raise Exception('Error: Keystone : %s Returned: %d' % \
                                (self.keystone_url, resp.status))
            return False
        identity_info = json.loads(data)

        try:
            tenant = identity_info['access']['token']['tenant']['id']
            expires = self.convert_date(
                identity_info['access']['token']['expires'])
            user = identity_info['access']['user']['name']
            roles = [x['name'] for x in \
                         identity_info['access']['user']['roles']]
        except(KeyError, IndexError):
            raise Exception("Could not get information from keystone")

        identity = {'user': user,
                    'tenant': tenant,
                    'roles': roles,
                    'expires': expires,
                    }

        return identity

    def authorize(self, req):
        env = req.environ
        env_identity = env.get('keystone.identity', {})
        tenant = env_identity.get('tenant')

        try:
            version, account, container, obj = split_path(req.path, 1, 4, True)
        except ValueError:
            return HTTPNotFound(request=req)

        if account != '%s_%s' % (self.reseller_prefix, tenant):
            self.log.debug('tenant mismatch')
            return self.denied_response(req)

        user_groups = env_identity.get('roles', [])
        if self.keystone_admin_group in user_groups:
            req.environ['swift_owner'] = True
            return None

        # Allow container sync<commit_msg>Make owner the user named same as tenant/account<commit_after># Copyright (c) 2011 OpenStack, LLC.
#
# 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.

import json

from urllib import quote
from urlparse import urlparse

from webob.exc import HTTPForbidden, HTTPNotFound, \
    HTTPUnauthorized, HTTPBadRequest
from webob import Request

from swift.common.utils import cache_from_env, get_logger, split_path, \
    get_remote_client
from swift.common.middleware.acl import clean_acl, parse_acl, referrer_allowed
from swift.common.bufferedhttp import http_connect_raw as http_connect
from time import time, mktime
from datetime import datetime


class AuthProtocol(object):
    """
    Keystone to Swift authentication and authorization system.

    Add to your pipeline in proxy-server.conf, such as::

        [pipeline:main]
        pipeline = catch_errors cache keystone proxy-server

    Set account auto creation to true::

        [app:proxy-server]
        account_autocreate = true

    And add a keystone filter section, such as::

        [filter:keystone]
        use = egg:keystone#swiftauth
        keystone_url = http://keystone_url:5000/v2.0
        keystone_admin_token = <PASSWORD>token
        keystone_admin_group = Admin

    This maps tenants to account in Swift. The user who's able to give
    ACL / Create Containers permision will be the same user as the account.

    Example: If we have the account called hellocorp with a user
    hellocorp that user will be admin on that account and can give ACL
    to all other users for hellocorp.

    If there is a user who has not the same name the account and if it
    is inside the group (or roles in keystone lingua)
    keystone_admin_group as specifed in the configuration variable
    (Admin by default) it will be allowed to be an admin the account.

    :param app: The next WSGI app in the pipeline
    :param conf: The dict of configuration values
    """
    def __init__(self, app, conf):
        self.app = app
        self.conf = conf
        self.logger = get_logger(conf, log_route='keystone')
        self.reseller_prefix = conf.get('reseller_prefix', 'AUTH').strip()
        #TODO: Error out if no url
        self.keystone_url = urlparse(conf.get('keystone_url'))
        self.keystone_admin_group = conf.get('keystone_admin_group', 'Admin')
        self.admin_token = conf.get('keystone_admin_token')
        self.auth_timeout = conf.get('keystone_auth_timeout', 30)
        self.allowed_sync_hosts = [h.strip()
            for h in conf.get('allowed_sync_hosts', '127.0.0.1').split(',')
            if h.strip()]

    def __call__(self, environ, start_response):
        self.logger.debug('Initialise keystone middleware')

        req = Request(environ)
        token = environ.get('HTTP_X_AUTH_TOKEN',
                            environ.get('HTTP_X_STORAGE_TOKEN'))
        if not token:
            self.logger.debug('No token: exiting')
            environ['swift.authorize'] = self.denied_response
            return self.app(environ, start_response)

        self.logger.debug('Got token: %s' % (token))

        identity = None
        memcache_client = cache_from_env(environ)
        memcache_key = 'tokens/%s' % (token)
        candidate_cache = memcache_client.get(memcache_key)
        if candidate_cache:
            expires, _identity = candidate_cache
            if expires > time():
                self.logger.debug('getting identity info from memcache')
                identity = _identity

        if not identity:
            self.logger.debug("No memcache, requesting it from keystone")
            identity = self._keystone_validate_token(token)
            if identity and memcache_client:
                expires = identity['expires']
                memcache_client.set(memcache_key,
                                    (expires, identity),
                                    timeout=expires - time())
                ts = str(datetime.fromtimestamp(expires))
                self.logger.debug('setting memcache expiration to %s' % ts)
            else:  # if we didn't get identity it means there was an error.
                return HTTPBadRequest(request=req)

        self.logger.debug("Using identity: %r" % (identity))

        if not identity:
            #TODO: non authenticated access allow via refer
            environ['swift.authorize'] = self.denied_response
            return self.app(environ, start_response)

        self.logger.debug("Using identity: %r" % (identity))
        environ['keystone.identity'] = identity
        environ['REMOTE_USER'] = identity.get('tenant')
        environ['swift.authorize'] = self.authorize
        environ['swift.clean_acl'] = clean_acl
        return self.app(environ, start_response)

    def convert_date(self, date):
        """ Convert datetime to unix timestamp """
        return mktime(datetime.strptime(
                date[:date.rfind(':')].replace('-', ''), "%Y%m%dT%H:%M",
                ).timetuple())

    def _keystone_validate_token(self, claim):
        """
        Will take a claimed token and validate it in keystone.
        """
        headers = {"X-Auth-Token": self.admin_token}
        conn = http_connect(self.keystone_url.hostname,
                            self.keystone_url.port, 'GET',
                            '%s/tokens/%s' % \
                                (self.keystone_url.path,
                                 quote(claim)),
                            headers=headers,
                            ssl=(self.keystone_url.scheme == 'https'),
                            timeout=self.auth_timeout)
        resp = conn.getresponse()
        data = resp.read()
        conn.close()
        self.logger.debug("Keystone came back with: status:%d, data:%s" % \
                            (resp.status, data))

        if not str(resp.status).startswith('20'):
            #TODO: Make the self.keystone_url more meaningfull
            raise Exception('Error: Keystone : %s Returned: %d' % \
                                (self.keystone_url, resp.status))
        identity_info = json.loads(data)

        try:
            tenant = (identity_info['access']['token']['tenant']['id'],
                         identity_info['access']['token']['tenant']['name'])
            expires = self.convert_date(
                identity_info['access']['token']['expires'])
            user = 'username' in identity_info['access']['user'] and \
                identity_info['access']['user']['username'] or \
                identity_info['access']['user']['name']
            roles = [x['name'] for x in \
                         identity_info['access']['user']['roles']]
        except (KeyError, IndexError):
            raise

        identity = {'user': user,
                    'tenant': tenant,
                    'roles': roles,
                    'expires': expires,
                    }

        return identity

    def authorize(self, req):
        env = req.environ
        env_identity = env.get('keystone.identity', {})
        tenant = env_identity.get('tenant')

        try:
            version, account, container, obj = split_path(req.path, 1, 4, True)
        except ValueError:
            return HTTPNotFound(request=req)

        if account != '%s_%s' % (self.reseller_prefix, tenant[0]):
            self.log.debug('tenant mismatch')
            return self.denied_response(req)

        # If user is in admin group then make the owner of it.
        user_groups = env_identity.get('roles', [])
        if self.keystone_admin_group in user_groups:
            req.environ['swift_owner'] = True
            return None

        # If user is of the same name of the tenant then make owner of it.
        user = env_identity.get('user', '')
        if user == tenant[1]:
            req.environ['swift_owner'] = True
            return None

        # Allow container sync
{'fraction_non_alphanumeric': 0.09933902329461593, 'fraction_numerical': 0.003465314766091253, 'mean_word_length': 2.184958103413039, 'pattern_counts': {'":': 2, '<': 5, '<?xml version=': 0, '>': 7, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 2, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3608097', 'n_tokens_mistral': 4330, 'n_tokens_neox': 4149, 'n_words': 1175}