diff options
author | Zuul <zuul@review.opendev.org> | 2021-10-30 09:25:07 +0000 |
---|---|---|
committer | Gerrit Code Review <review@openstack.org> | 2021-10-30 09:25:07 +0000 |
commit | 46519e2089b9bc0ec88a6c4b19517126a0a36980 (patch) | |
tree | 0f34b7a31f04eeb5a9ed9131a148ecb0d47492ab | |
parent | db5744a1bbdbaa494d4bad8d05f8c72e99821865 (diff) | |
parent | 0cfd75d7ef0048e497ea44b694841c616e70cd6a (diff) | |
download | zuul-46519e2089b9bc0ec88a6c4b19517126a0a36980.tar.gz |
Merge "Zuul-web: Add authentication-realm attribute to tenants"
-rw-r--r-- | doc/source/reference/tenants.rst | 16 | ||||
-rw-r--r-- | tests/fixtures/config/authorization/rules-templating/main.yaml | 2 | ||||
-rw-r--r-- | tests/unit/test_web.py | 29 | ||||
-rw-r--r-- | zuul/configloader.py | 3 | ||||
-rw-r--r-- | zuul/model.py | 7 | ||||
-rwxr-xr-x | zuul/web/__init__.py | 42 |
6 files changed, 96 insertions, 3 deletions
diff --git a/doc/source/reference/tenants.rst b/doc/source/reference/tenants.rst index 8e8d5f070..a5c828dda 100644 --- a/doc/source/reference/tenants.rst +++ b/doc/source/reference/tenants.rst @@ -340,6 +340,22 @@ configuration. Some examples of tenant definitions are: :ref:`tenant-scoped-rest-api`. + .. attr:: authentication-realm + + Each authenticator defined in Zuul's configuration is associated to a realm. + When authenticating through Zuul's Web User Interface under this tenant, the + Web UI will redirect the user to this realm's authentication service. The + authenticator must be of the type ``OpenIDConnect``. + + .. note:: + + Defining a default realm for a tenant will not invalidate access tokens + issued from other configured realms, especially if they match the tenant's + admin rules. This is intended, so that an operator can for example issue + an overriding access token manually. If this is an issue, it is advised + to add finer filtering to admin rules, for example filtering by the ``iss`` + claim (generally equal to the issuer ID). + .. _admin_rule_definition: Access Rule diff --git a/tests/fixtures/config/authorization/rules-templating/main.yaml b/tests/fixtures/config/authorization/rules-templating/main.yaml index 3e6b00765..c621115a3 100644 --- a/tests/fixtures/config/authorization/rules-templating/main.yaml +++ b/tests/fixtures/config/authorization/rules-templating/main.yaml @@ -14,6 +14,7 @@ - org/project - tenant: name: tenant-one + authentication-realm: myOIDC1 admin-rules: - tenant-admin source: @@ -24,6 +25,7 @@ - org/project1 - tenant: name: tenant-two + authentication-realm: myOIDC2 admin-rules: - tenant-admin source: diff --git a/tests/unit/test_web.py b/tests/unit/test_web.py index 1fe8989f4..7048f8797 100644 --- a/tests/unit/test_web.py +++ b/tests/unit/test_web.py @@ -1325,6 +1325,35 @@ class TestWebCapabilitiesInfo(TestInfo): return info +class TestTenantAuthRealmInfo(TestWebCapabilitiesInfo): + + tenant_config_file = 'config/authorization/rules-templating/main.yaml' + + def test_tenant_info(self): + expected_info = self._expected_info() + info = self.get_url("api/tenant/tenant-zero/info").json() + expected_info['info']['tenant'] = 'tenant-zero' + expected_info['info']['capabilities']['auth']['default_realm'] =\ + 'myOIDC1' + self.assertEqual(expected_info, + info, + info) + info = self.get_url("api/tenant/tenant-one/info").json() + expected_info['info']['tenant'] = 'tenant-one' + expected_info['info']['capabilities']['auth']['default_realm'] =\ + 'myOIDC1' + self.assertEqual(expected_info, + info, + info) + info = self.get_url("api/tenant/tenant-two/info").json() + expected_info['info']['tenant'] = 'tenant-two' + expected_info['info']['capabilities']['auth']['default_realm'] =\ + 'myOIDC2' + self.assertEqual(expected_info, + info, + info) + + class TestTenantInfoConfigBroken(BaseTestWeb): tenant_config_file = 'config/broken/main.yaml' diff --git a/zuul/configloader.py b/zuul/configloader.py index d0a6965da..6493857cf 100644 --- a/zuul/configloader.py +++ b/zuul/configloader.py @@ -1510,6 +1510,7 @@ class TenantParser(object): 'default-parent': str, 'default-ansible-version': vs.Any(str, float), 'admin-rules': to_list(str), + 'authentication-realm': str, # TODO: Ignored, allowed for backwards compat, remove for v5. 'report-build-page': bool, 'web-root': str, @@ -1531,6 +1532,8 @@ class TenantParser(object): conf['exclude-unprotected-branches'] if conf.get('admin-rules') is not None: tenant.authorization_rules = conf['admin-rules'] + if conf.get('authentication-realm') is not None: + tenant.default_auth_realm = conf['authentication-realm'] tenant.web_root = conf.get('web-root', self.scheduler.globals.web_root) if tenant.web_root and not tenant.web_root.endswith('/'): tenant.web_root += '/' diff --git a/zuul/model.py b/zuul/model.py index e50a5163d..d35c9ce31 100644 --- a/zuul/model.py +++ b/zuul/model.py @@ -7006,6 +7006,7 @@ class Tenant(object): self.default_ansible_version = None self.authorization_rules = [] + self.default_auth_realm = None @property def all_projects(self): @@ -7296,20 +7297,20 @@ class Capabilities(object): or not, keep track of distinct capability flags. """ def __init__(self, **kwargs): - self._capabilities = kwargs + self.capabilities = kwargs def __repr__(self): return '<Capabilities 0x%x %s>' % (id(self), self._renderFlags()) def _renderFlags(self): return " ".join(['{k}={v}'.format(k=k, v=repr(v)) - for (k, v) in self._capabilities.items()]) + for (k, v) in self.capabilities.items()]) def copy(self): return Capabilities(**self.toDict()) def toDict(self): - return self._capabilities + return self.capabilities class WebInfo(object): diff --git a/zuul/web/__init__.py b/zuul/web/__init__.py index 77ba9967d..1e3cf8783 100755 --- a/zuul/web/__init__.py +++ b/zuul/web/__init__.py @@ -42,6 +42,7 @@ from zuul.zk.components import ComponentRegistry, WebComponent from zuul.zk.executor import ExecutorApi from zuul.zk.nodepool import ZooKeeperNodepool from zuul.zk.system import ZuulSystem +from zuul.zk.config_cache import SystemConfigCache from zuul.lib.auth import AuthenticatorRegistry from zuul.lib.config import get_default @@ -646,6 +647,15 @@ class ZuulWebAPI(object): def tenant_info(self, tenant): info = self.zuulweb.info.copy() info.tenant = tenant + tenant_config = self.zuulweb.unparsed_abide.tenants.get(tenant) + if tenant_config is not None: + # TODO: should we return 404 if tenant not found? + tenant_auth_realm = tenant_config.get('authentication-realm') + if tenant_auth_realm is not None: + if (info.capabilities is not None and + info.capabilities.toDict().get('auth') is not None): + info.capabilities.capabilities['auth']['default_realm'] =\ + tenant_auth_realm return self._handleInfo(info) def _handleInfo(self, info): @@ -694,6 +704,8 @@ class ZuulWebAPI(object): return {'description': e.error_description, 'error': e.error, 'realm': e.realm} + resp = cherrypy.response + resp.headers['Access-Control-Allow-Origin'] = '*' return {'zuul': {'admin': admin_tenants}, } @cherrypy.expose @@ -716,6 +728,8 @@ class ZuulWebAPI(object): return {'description': e.error_description, 'error': e.error, 'realm': e.realm} + resp = cherrypy.response + resp.headers['Access-Control-Allow-Origin'] = '*' return {'zuul': {'admin': tenant in admin_tenants, 'scope': [tenant, ]}, } @@ -1327,6 +1341,14 @@ class ZuulWeb(object): self.component_registry = ComponentRegistry(self.zk_client) + self.system_config_cache_wake_event = threading.Event() + self.system_config_cache = SystemConfigCache( + self.zk_client, + self.system_config_cache_wake_event.set) + # Fetch an initial value so we we have something to serve + # requests before the initial callback fires. + self.unparsed_abide, _ = self.system_config_cache.get() + self.connections = connections self.authenticators = authenticators self.stream_manager = StreamManager() @@ -1480,6 +1502,16 @@ class ZuulWeb(object): def port(self): return cherrypy.server.bound_addr[1] + def updateSystemConfigCache(self): + while self._system_config_running: + try: + self.system_config_cache_wake_event.wait() + if not self._system_config_running: + return + self.unparsed_abide, _ = self.system_config_cache.get() + except Exception: + self.log.exception("Exception while processing command") + def start(self): self.log.debug("ZuulWeb starting") self.stream_manager.start() @@ -1496,6 +1528,13 @@ class ZuulWeb(object): self.command_thread.start() self.component_info.state = self.component_info.RUNNING + self.system_config_thread = threading.Thread( + target=self.updateSystemConfigCache, + name='system_config') + self._system_config_running = True + self.system_config_thread.daemon = True + self.system_config_thread.start() + def stop(self): self.log.debug("ZuulWeb stopping") self.component_info.state = self.component_info.STOPPED @@ -1507,6 +1546,9 @@ class ZuulWeb(object): cherrypy.server.httpserver = None self.wsplugin.unsubscribe() self.stream_manager.stop() + self._system_config_running = False + self.system_config_cache_wake_event.set() + self.system_config_thread.join() self.zk_client.disconnect() self.stop_repl() self._command_running = False |