diff --git a/etc/glance-api.conf b/etc/glance-api.conf index 4de3c72d5d..c633cb3692 100644 --- a/etc/glance-api.conf +++ b/etc/glance-api.conf @@ -90,6 +90,13 @@ workers = 1 # The default value is false. #show_image_direct_url = False +# Send headers containing user and tenant information when making requests to +# the v1 glance registry. This allows the registry to function as if a user is +# authenticated without the need to authenticate a user itself using the +# auth_token middleware. +# The default value is false. +#send_identity_headers = False + # ================= Syslog Options ============================ # Send logs to syslog (/dev/log) instead of to file specified diff --git a/etc/glance-registry-paste.ini b/etc/glance-registry-paste.ini index 5519c5cc56..4089f190cd 100644 --- a/etc/glance-registry-paste.ini +++ b/etc/glance-registry-paste.ini @@ -6,6 +6,12 @@ pipeline = unauthenticated-context registryapp [pipeline:glance-registry-keystone] pipeline = authtoken context registryapp +# Use this pipeline for authZ only. This means that the registry will treat a +# user as authenticated without making requests to keystone to reauthenticate +# the user. +[pipeline:glance-registry-trusted-auth] +pipeline = context registryapp + [app:registryapp] paste.app_factory = glance.registry.api.v1:API.factory diff --git a/glance/registry/client/v1/api.py b/glance/registry/client/v1/api.py index a1847ee4ff..8e9fb9cc0c 100644 --- a/glance/registry/client/v1/api.py +++ b/glance/registry/client/v1/api.py @@ -19,6 +19,7 @@ Registry's Client API """ +import json import os from oslo.config import cfg @@ -55,6 +56,16 @@ registry_client_ctx_opts = [ cfg.BoolOpt('use_user_token', default=True, help=_('Whether to pass through the user token when ' 'making requests to the registry.')), + cfg.BoolOpt('send_identity_headers', default=False, + help=_('Whether to pass through headers containing user ' + 'and tenant information when making requests to ' + 'the registry. This allows the registry to use the ' + 'context middleware without the keystoneclients\' ' + 'auth_token middleware, removing calls to the keystone ' + 'auth service. It is recommended that when using this ' + 'option, secure communication between glance api and ' + 'glance registry is ensured by means other than ' + 'auth_token middleware.')), cfg.StrOpt('admin_user', secret=True, help=_('The administrators user name.')), cfg.StrOpt('admin_password', secret=True, @@ -141,6 +152,16 @@ def get_registry_client(cxt): kwargs['auth_tok'] = cxt.auth_tok if _CLIENT_CREDS: kwargs['creds'] = _CLIENT_CREDS + + if CONF.send_identity_headers: + identity_headers = { + 'X-User-Id': cxt.user, + 'X-Tenant-Id': cxt.tenant, + 'X-Roles': ','.join(cxt.roles), + 'X-Identity-Status': 'Confirmed', + 'X-Service-Catalog': json.dumps(cxt.service_catalog), + } + kwargs['identity_headers'] = identity_headers return client.RegistryClient(_CLIENT_HOST, _CLIENT_PORT, _METADATA_ENCRYPTION_KEY, **kwargs) diff --git a/glance/registry/client/v1/client.py b/glance/registry/client/v1/client.py index 1759df6d89..f74c082f38 100644 --- a/glance/registry/client/v1/client.py +++ b/glance/registry/client/v1/client.py @@ -37,7 +37,7 @@ class RegistryClient(BaseClient): DEFAULT_PORT = 9191 def __init__(self, host=None, port=None, metadata_encryption_key=None, - **kwargs): + identity_headers=None, **kwargs): """ :param metadata_encryption_key: Key used to encrypt 'location' metadata """ @@ -45,6 +45,7 @@ class RegistryClient(BaseClient): # NOTE (dprince): by default base client overwrites host and port # settings when using keystone. configure_via_auth=False disables # this behaviour to ensure we still send requests to the Registry API + self.identity_headers = identity_headers BaseClient.__init__(self, host, port, configure_via_auth=False, **kwargs) @@ -85,6 +86,8 @@ class RegistryClient(BaseClient): def do_request(self, method, action, **kwargs): try: + kwargs['headers'] = kwargs.get('headers', {}) + kwargs['headers'].update(self.identity_headers or {}) res = super(RegistryClient, self).do_request(method, action, **kwargs) diff --git a/glance/tests/unit/v1/test_registry_client.py b/glance/tests/unit/v1/test_registry_client.py index ddb74dc10e..0b8d98d129 100644 --- a/glance/tests/unit/v1/test_registry_client.py +++ b/glance/tests/unit/v1/test_registry_client.py @@ -1215,6 +1215,7 @@ class TestRegistryV1ClientApi(base.IsolatedUnitTest): """Establish a clean test environment""" super(TestRegistryV1ClientApi, self).setUp() self.mox = mox.Mox() + self.context = context.RequestContext() reload(rapi) def tearDown(self): @@ -1222,6 +1223,23 @@ class TestRegistryV1ClientApi(base.IsolatedUnitTest): super(TestRegistryV1ClientApi, self).tearDown() self.mox.UnsetStubs() + def test_get_registry_client(self): + actual_client = rapi.get_registry_client(self.context) + self.assertEqual(actual_client.identity_headers, None) + + def test_get_registry_client_with_identity_headers(self): + self.config(send_identity_headers=True) + expected_identity_headers = { + 'X-User-Id': self.context.user, + 'X-Tenant-Id': self.context.tenant, + 'X-Roles': ','.join(self.context.roles), + 'X-Identity-Status': 'Confirmed', + 'X-Service-Catalog': 'null', + } + actual_client = rapi.get_registry_client(self.context) + self.assertEqual(actual_client.identity_headers, + expected_identity_headers) + def test_configure_registry_client_not_using_use_user_token(self): self.config(use_user_token=False) self.mox.StubOutWithMock(rapi, 'configure_registry_admin_creds') @@ -1273,3 +1291,48 @@ class TestRegistryV1ClientApi(base.IsolatedUnitTest): self.assertEquals(rapi._CLIENT_CREDS, None) rapi.configure_registry_admin_creds() self.assertEquals(rapi._CLIENT_CREDS, expected) + + +class FakeResponse(): + status = 202 + + def getheader(*args, **kwargs): + return None + + +class TestRegistryV1ClientRequests(base.IsolatedUnitTest): + + def setUp(self): + super(TestRegistryV1ClientRequests, self).setUp() + self.mox = mox.Mox() + + def tearDown(self): + super(TestRegistryV1ClientRequests, self).tearDown() + self.mox.UnsetStubs() + + def test_do_request_with_identity_headers(self): + identity_headers = {'foo': 'bar'} + self.client = rclient.RegistryClient("0.0.0.0", + identity_headers=identity_headers) + + self.mox.StubOutWithMock(test_client.BaseClient, 'do_request') + test_client.BaseClient.do_request("GET", "/images", + headers=identity_headers).AndReturn( + FakeResponse()) + self.mox.ReplayAll() + + self.client.do_request("GET", "/images") + + self.mox.VerifyAll() + + def test_do_request(self): + self.client = rclient.RegistryClient("0.0.0.0") + + self.mox.StubOutWithMock(test_client.BaseClient, 'do_request') + test_client.BaseClient.do_request("GET", "/images", + headers={}).AndReturn(FakeResponse()) + self.mox.ReplayAll() + + self.client.do_request("GET", "/images") + + self.mox.VerifyAll()