Zuul-web: Add authentication-realm attribute to tenants
With this attribute, an operator can map a tenant to an authenticator. This is the authenticator that will be used by the Web UI on a whitelabeled setup, or when scoping the UI to the tenant. Change-Id: Ifd3d629ff3ed0c3c902433bdf808d457e755c528
This commit is contained in:
parent
1d05b0a95f
commit
0cfd75d7ef
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -1270,6 +1270,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'
|
||||
|
|
|
@ -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,
|
||||
|
@ -1530,6 +1531,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 += '/'
|
||||
|
|
|
@ -6058,6 +6058,7 @@ class Tenant(object):
|
|||
self.default_ansible_version = None
|
||||
|
||||
self.authorization_rules = []
|
||||
self.default_auth_realm = None
|
||||
|
||||
@property
|
||||
def all_projects(self):
|
||||
|
@ -6348,20 +6349,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):
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue