diff --git a/etc/glance-api.conf b/etc/glance-api.conf index 5873827895..0b8635eb81 100644 --- a/etc/glance-api.conf +++ b/etc/glance-api.conf @@ -56,6 +56,10 @@ workers = 0 # Role used to identify an authenticated user as administrator #admin_role = admin +# Allow unauthenticated users to access the API with read-only +# privileges. This only applies when using ContextMiddleware. +#allow_anonymous_access = False + # ================= Syslog Options ============================ # Send logs to syslog (/dev/log) instead of to file specified diff --git a/glance/common/context.py b/glance/common/context.py index be37ec8418..56daca013f 100644 --- a/glance/common/context.py +++ b/glance/common/context.py @@ -132,6 +132,7 @@ class ContextMiddleware(wsgi.Middleware): opts = [ cfg.BoolOpt('owner_is_tenant', default=True), cfg.StrOpt('admin_role', default='admin'), + cfg.BoolOpt('allow_anonymous_access', default=False), ] def __init__(self, app, conf, **local_conf): @@ -140,7 +141,7 @@ class ContextMiddleware(wsgi.Middleware): super(ContextMiddleware, self).__init__(app) def process_request(self, req): - """Convert authentication informtion into a request context + """Convert authentication information into a request context Generate a RequestContext object from the available authentication headers and store on the 'context' attribute @@ -148,11 +149,27 @@ class ContextMiddleware(wsgi.Middleware): :param req: wsgi request object that will be given the context object :raises webob.exc.HTTPUnauthorized: when value of the X-Identity-Status - header is not 'Confirmed' + header is not 'Confirmed' and + anonymous access is disallowed """ - if req.headers.get('X-Identity-Status') != 'Confirmed': + if req.headers.get('X-Identity-Status') == 'Confirmed': + req.context = self._get_authenticated_context(req) + elif self.conf.allow_anonymous_access: + req.context = self._get_anonymous_context() + else: raise webob.exc.HTTPUnauthorized() + def _get_anonymous_context(self): + kwargs = { + 'user': None, + 'tenant': None, + 'roles': [], + 'is_admin': False, + 'read_only': True, + } + return RequestContext(**kwargs) + + def _get_authenticated_context(self, req): #NOTE(bcwaldon): X-Roles is a csv string, but we need to parse # it into a list to be useful roles_header = req.headers.get('X-Roles', '') @@ -170,7 +187,7 @@ class ContextMiddleware(wsgi.Middleware): 'owner_is_tenant': self.conf.owner_is_tenant, } - req.context = RequestContext(**kwargs) + return RequestContext(**kwargs) class UnauthenticatedContextMiddleware(wsgi.Middleware): diff --git a/glance/tests/unit/test_context_middleware.py b/glance/tests/unit/test_context_middleware.py new file mode 100644 index 0000000000..17b9d2d2c2 --- /dev/null +++ b/glance/tests/unit/test_context_middleware.py @@ -0,0 +1,76 @@ + +import webob + +from glance.common import context +from glance.tests.unit import base + + +class TestContextMiddleware(base.IsolatedUnitTest): + def _build_request(self, roles=None, identity_status='Confirmed'): + req = webob.Request.blank('/') + req.headers['x-auth-token'] = 'token1' + req.headers['x-identity-status'] = identity_status + req.headers['x-user-id'] = 'user1' + req.headers['x-tenant-id'] = 'tenant1' + _roles = roles or ['role1', 'role2'] + req.headers['x-roles'] = ','.join(_roles) + + return req + + def _build_middleware(self, **extra_config): + for k, v in extra_config.items(): + setattr(self.conf, k, v) + return context.ContextMiddleware(None, self.conf) + + def test_header_parsing(self): + req = self._build_request() + self._build_middleware().process_request(req) + self.assertEqual(req.context.auth_tok, 'token1') + self.assertEqual(req.context.user, 'user1') + self.assertEqual(req.context.tenant, 'tenant1') + self.assertEqual(req.context.roles, ['role1', 'role2']) + + def test_is_admin_flag(self): + # is_admin check should look for 'admin' role by default + req = self._build_request(roles=['admin', 'role2']) + self._build_middleware().process_request(req) + self.assertTrue(req.context.is_admin) + + # without the 'admin' role, is_admin shoud be False + req = self._build_request() + self._build_middleware().process_request(req) + self.assertFalse(req.context.is_admin) + + # if we change the admin_role attribute, we should be able to use it + req = self._build_request() + self._build_middleware(admin_role='role1').process_request(req) + self.assertTrue(req.context.is_admin) + + def test_anonymous_access_enabled(self): + req = self._build_request(identity_status='Nope') + middleware = self._build_middleware(allow_anonymous_access=True) + middleware.process_request(req) + self.assertEqual(req.context.auth_tok, None) + self.assertEqual(req.context.user, None) + self.assertEqual(req.context.tenant, None) + self.assertEqual(req.context.roles, []) + self.assertFalse(req.context.is_admin) + self.assertTrue(req.context.read_only) + + def test_anonymous_access_defaults_to_disabled(self): + req = self._build_request(identity_status='Nope') + middleware = self._build_middleware() + self.assertRaises(webob.exc.HTTPUnauthorized, + middleware.process_request, req) + + +class TestUnauthenticatedContextMiddleware(base.IsolatedUnitTest): + def test_request(self): + middleware = context.UnauthenticatedContextMiddleware(None, self.conf) + req = webob.Request.blank('/') + middleware.process_request(req) + self.assertEqual(req.context.auth_tok, None) + self.assertEqual(req.context.user, None) + self.assertEqual(req.context.tenant, None) + self.assertEqual(req.context.roles, []) + self.assertTrue(req.context.is_admin) diff --git a/glance/tests/unit/v1/test_api.py b/glance/tests/unit/v1/test_api.py index ef51fed659..33b4e8002a 100644 --- a/glance/tests/unit/v1/test_api.py +++ b/glance/tests/unit/v1/test_api.py @@ -3110,44 +3110,3 @@ class TestImageSerializer(base.IsolatedUnitTest): self.serializer.image_send_notification(17, 19, image_meta, req) self.assertTrue(called['notified']) - - -class TestContextMiddleware(base.IsolatedUnitTest): - def _build_request(self, roles=None): - req = webob.Request.blank('/') - req.headers['x-auth-token'] = 'token1' - req.headers['x-identity-status'] = 'Confirmed' - req.headers['x-user-id'] = 'user1' - req.headers['x-tenant-id'] = 'tenant1' - _roles = roles or ['role1', 'role2'] - req.headers['x-roles'] = ','.join(_roles) - return req - - def _build_middleware(self, **extra_config): - for k, v in extra_config.items(): - setattr(self.conf, k, v) - return context.ContextMiddleware(None, self.conf) - - def test_header_parsing(self): - req = self._build_request() - self._build_middleware().process_request(req) - self.assertEqual(req.context.auth_tok, 'token1') - self.assertEqual(req.context.user, 'user1') - self.assertEqual(req.context.tenant, 'tenant1') - self.assertEqual(req.context.roles, ['role1', 'role2']) - - def test_is_admin_flag(self): - # is_admin check should look for 'admin' role by default - req = self._build_request(roles=['admin', 'role2']) - self._build_middleware().process_request(req) - self.assertTrue(req.context.is_admin) - - # without the 'admin' role, is_admin shoud be False - req = self._build_request() - self._build_middleware().process_request(req) - self.assertFalse(req.context.is_admin) - - # if we change the admin_role attribute, we should be able to use it - req = self._build_request() - self._build_middleware(admin_role='role1').process_request(req) - self.assertTrue(req.context.is_admin)