diff --git a/etc/glare-paste.ini b/etc/glare-paste.ini index c85fdd2..4f6eadb 100644 --- a/etc/glare-paste.ini +++ b/etc/glare-paste.ini @@ -1,15 +1,20 @@ -# Use this pipeline for no auth - DEFAULT +# Use this pipeline for trusted auth - DEFAULT +# Auth token has format user:tenant:roles [pipeline:glare-api] -pipeline = cors faultwrapper healthcheck http_proxy_to_wsgi versionnegotiation osprofiler unauthenticated-context glarev1api +pipeline = cors faultwrapper healthcheck http_proxy_to_wsgi versionnegotiation osprofiler trustedauth glarev1api # Use this pipeline for keystone auth [pipeline:glare-api-keystone] pipeline = cors faultwrapper healthcheck http_proxy_to_wsgi versionnegotiation osprofiler authtoken context glarev1api # Use this pipeline for Keycloak auth -[pipeline:glare-api-keystone] +[pipeline:glare-api-keycloak] pipeline = cors faultwrapper healthcheck http_proxy_to_wsgi versionnegotiation osprofiler keycloak context glarev1api +# Use this pipeline when you want to specify context params manually +[pipeline:glare-api-noauth] +pipeline = cors faultwrapper healthcheck http_proxy_to_wsgi versionnegotiation osprofiler context glarev1api + [app:glarev1api] paste.app_factory = glare.api.v1.router:API.factory @@ -27,8 +32,8 @@ paste.filter_factory = glare.api.middleware.fault:GlareFaultWrapperFilter.factor [filter:context] paste.filter_factory = glare.api.middleware.context:ContextMiddleware.factory -[filter:unauthenticated-context] -paste.filter_factory = glare.api.middleware.context:UnauthenticatedContextMiddleware.factory +[filter:trustedauth] +paste.filter_factory = glare.api.middleware.context:TrustedAuthMiddleware.factory [filter:authtoken] paste.filter_factory = keystonemiddleware.auth_token:filter_factory diff --git a/glare/api/middleware/context.py b/glare/api/middleware/context.py index 35fb34e..13a10a3 100644 --- a/glare/api/middleware/context.py +++ b/glare/api/middleware/context.py @@ -83,15 +83,10 @@ class ContextMiddleware(base_middleware.ConfigurableMiddleware): if req.headers.get('X-Identity-Status') == 'Confirmed': req.context = ContextMiddleware._get_authenticated_context(req) elif CONF.allow_anonymous_access: - req.context = ContextMiddleware._get_anonymous_context() + req.context = RequestContext(read_only=True, is_admin=False) else: raise exception.Unauthorized() - @staticmethod - def _get_anonymous_context(): - """Anonymous user has only Read-Only grants.""" - return RequestContext(read_only=True, is_admin=False) - @staticmethod def _get_authenticated_context(req): headers = req.headers @@ -110,14 +105,31 @@ class ContextMiddleware(base_middleware.ConfigurableMiddleware): return RequestContext.from_environ(req.environ, **kwargs) -class UnauthenticatedContextMiddleware(base_middleware.ConfigurableMiddleware): - """Process requests and responses when auth is turned off at all.""" +class TrustedAuthMiddleware(base_middleware.ConfigurableMiddleware): @staticmethod def process_request(req): - """Create a context without an authorized user. + auth_token = req.headers.get('X-Auth-Token') + if not auth_token: + msg = _("Auth token must be provided") + raise exception.Unauthorized(msg) + try: + user, tenant, roles = auth_token.split(':') + except ValueError: + msg = _("Wrong auth token format. It must be 'user:tenant:roles'") + raise exception.Unauthorized(msg) + if tenant.lower() == 'none': + tenant = None + req.headers['X-User-Id'] = user + req.headers['X-Tenant-Id'] = tenant + req.headers['X-Roles'] = roles.split(',') + req.headers['X-Identity-Status'] = 'Confirmed' + kwargs = { + 'user': user, + 'tenant': tenant, + 'roles': roles, + 'is_admin': 'admin' in req.headers['X-Roles'], + 'auth_token': auth_token, + } - When glare deployed as public repo everybody is admin - without any credentials. - """ - req.context = RequestContext(is_admin=True) + req.context = context.RequestContext(**kwargs) diff --git a/glare/api/middleware/keycloak_auth.py b/glare/api/middleware/keycloak_auth.py index 3eead9b..d2d8823 100644 --- a/glare/api/middleware/keycloak_auth.py +++ b/glare/api/middleware/keycloak_auth.py @@ -16,6 +16,7 @@ import memcache from oslo_config import cfg from oslo_log import log as logging +from oslo_middleware import base as base_middleware import pprint import requests import webob.dec @@ -53,10 +54,10 @@ CONF = cfg.CONF CONF.register_opts(keycloak_oidc_opts, group="keycloak_oidc") -class KeycloakAuthMiddleware(object): +class KeycloakAuthMiddleware(base_middleware.Middleware): def __init__(self, app): - self.app = app - mcserv_url = CONF.memcached_server + super(KeycloakAuthMiddleware, self).__init__(application=app) + mcserv_url = CONF.keycloak_oidc.memcached_server self.mcclient = memcache.Client(mcserv_url) if mcserv_url else None def authenticate(self, request): @@ -82,7 +83,7 @@ class KeycloakAuthMiddleware(object): resp.raise_for_status() if self.mcclient: self.mcclient.set(access_token, resp.json(), - time=CONF.token_cache_time) + time=CONF.keycloak_oidc.token_cache_time) info = resp.json() LOG.debug( @@ -114,7 +115,7 @@ class KeycloakAuthMiddleware(object): roles = [role['name'] for role in resp.json()] if self.mcclient: self.mcclient.set(realm_name, roles, - time=CONF.token_cache_time) + time=CONF.keycloak_oidc.token_cache_time) LOG.debug( "Roles for realm %s: %s" % @@ -131,4 +132,4 @@ class KeycloakAuthMiddleware(object): roles = ','.join(self.get_roles(request)) request.headers["X-Identity-Status"] = "Confirmed" request.headers["X-Roles"] = roles - return request.get_response(self.app) + return request.get_response(self.application) diff --git a/glare/tests/functional/__init__.py b/glare/tests/functional/__init__.py index cf34da3..5edb8cd 100644 --- a/glare/tests/functional/__init__.py +++ b/glare/tests/functional/__init__.py @@ -315,10 +315,7 @@ default_store = %(default_store)s connection = %(sql_connection)s """ self.paste_conf_base = """[pipeline:glare-api] -pipeline = faultwrapper versionnegotiation unauthenticated-context glarev1api - -[pipeline:glare-api-fakeauth] -pipeline = faultwrapper versionnegotiation fakeauth context glarev1api +pipeline = faultwrapper versionnegotiation trustedauth glarev1api [pipeline:glare-api-noauth] pipeline = faultwrapper versionnegotiation context glarev1api @@ -339,12 +336,9 @@ paste.filter_factory = [filter:context] paste.filter_factory = glare.api.middleware.context:ContextMiddleware.factory -[filter:unauthenticated-context] +[filter:trustedauth] paste.filter_factory = -glare.api.middleware.context:UnauthenticatedContextMiddleware.factory - -[filter:fakeauth] -paste.filter_factory = glare.tests.utils:FakeAuthMiddleware.factory + glare.api.middleware.context:TrustedAuthMiddleware.factory """ diff --git a/glare/tests/utils.py b/glare/tests/utils.py index 00e8a9c..685c0f4 100644 --- a/glare/tests/utils.py +++ b/glare/tests/utils.py @@ -26,12 +26,8 @@ import fixtures from oslo_config import cfg from oslo_config import fixture as cfg_fixture from oslo_log import log -from oslo_middleware import base as base_middleware -import six import testtools -import webob -from glare.api.middleware import context from glare.common import config from glare.common import utils @@ -371,103 +367,3 @@ def xattr_writes_supported(path): os.unlink(fake_filepath) return result - - -class FakeAuthMiddleware(base_middleware.ConfigurableMiddleware): - - @staticmethod - def process_request(req): - auth_token = req.headers.get('X-Auth-Token') - user = None - tenant = None - roles = [] - if auth_token: - user, tenant, role = auth_token.split(':') - if tenant.lower() == 'none': - tenant = None - roles = [role] - req.headers['X-User-Id'] = user - req.headers['X-Tenant-Id'] = tenant - req.headers['X-Roles'] = role - req.headers['X-Identity-Status'] = 'Confirmed' - kwargs = { - 'user': user, - 'tenant': tenant, - 'roles': roles, - 'is_admin': False, - 'auth_token': auth_token, - } - - req.context = context.RequestContext(**kwargs) - - -class FakeHTTPResponse(object): - def __init__(self, status=200, headers=None, data=None, *args, **kwargs): - data = data or b'I am a teapot, short and stout\n' - self.data = six.BytesIO(data) - self.read = self.data.read - self.status = status - self.headers = headers or {'content-length': len(data)} - - def getheader(self, name, default=None): - return self.headers.get(name.lower(), default) - - def getheaders(self): - return self.headers or {} - - def read(self, amt): - self.data.read(amt) - - -class Httplib2WsgiAdapter(object): - def __init__(self, app): - self.app = app - - def request(self, uri, method="GET", body=None, headers=None): - req = webob.Request.blank(uri, method=method, headers=headers) - req.body = body - resp = req.get_response(self.app) - return Httplib2WebobResponse(resp), resp.body - - -class Httplib2WebobResponse(object): - def __init__(self, webob_resp): - self.webob_resp = webob_resp - - @property - def status(self): - return self.webob_resp.status_code - - def __getitem__(self, key): - return self.webob_resp.headers[key] - - def get(self, key): - return self.webob_resp.headers[key] - - @property - def allow(self): - return self.webob_resp.allow - - @allow.setter - def allow(self, allowed): - if type(allowed) is not str: - raise TypeError('Allow header should be a str') - - self.webob_resp.allow = allowed - - -class HttplibWsgiAdapter(object): - def __init__(self, app): - self.app = app - self.req = None - - def request(self, method, url, body=None, headers=None): - if headers is None: - headers = {} - self.req = webob.Request.blank(url, method=method, headers=headers) - self.req.body = body - - def getresponse(self): - response = self.req.get_response(self.app) - return FakeHTTPResponse(response.status_code, response.headers, - response.body)