summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZuul <zuul@review.opendev.org>2021-10-30 09:25:07 +0000
committerGerrit Code Review <review@openstack.org>2021-10-30 09:25:07 +0000
commit46519e2089b9bc0ec88a6c4b19517126a0a36980 (patch)
tree0f34b7a31f04eeb5a9ed9131a148ecb0d47492ab
parentdb5744a1bbdbaa494d4bad8d05f8c72e99821865 (diff)
parent0cfd75d7ef0048e497ea44b694841c616e70cd6a (diff)
downloadzuul-46519e2089b9bc0ec88a6c4b19517126a0a36980.tar.gz
Merge "Zuul-web: Add authentication-realm attribute to tenants"
-rw-r--r--doc/source/reference/tenants.rst16
-rw-r--r--tests/fixtures/config/authorization/rules-templating/main.yaml2
-rw-r--r--tests/unit/test_web.py29
-rw-r--r--zuul/configloader.py3
-rw-r--r--zuul/model.py7
-rwxr-xr-xzuul/web/__init__.py42
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