summaryrefslogtreecommitdiff
path: root/zuul
diff options
context:
space:
mode:
authorJames E. Blair <jim@acmegating.com>2022-09-29 13:47:17 -0700
committerJames E. Blair <jim@acmegating.com>2022-10-25 20:22:33 -0700
commitc22f2c98e0af910b5d7a58966a16741fdb53cb0b (patch)
treeda6cbd877f26d34d1ee1fa67d13b11f36fc8fde3 /zuul
parentc2f2891bd33411db1989a6a1a1c575ce41046a33 (diff)
downloadzuul-c22f2c98e0af910b5d7a58966a16741fdb53cb0b.tar.gz
Add access-rules configuration and documentation
This allows configuration of read-only access rules, and corresponding documentation. It wraps every API method in an auth check (other than info endpoints). It exposes information in the info endpoints that the web UI can use to decide whether it should send authentication information for all requests. A later change will update the web UI to use that. Change-Id: I3985c3d0b9f831fd004b2bb010ab621c00486e05
Diffstat (limited to 'zuul')
-rw-r--r--zuul/configloader.py7
-rw-r--r--zuul/model.py5
-rwxr-xr-xzuul/web/__init__.py419
3 files changed, 294 insertions, 137 deletions
diff --git a/zuul/configloader.py b/zuul/configloader.py
index a839ed137..49a0b95b9 100644
--- a/zuul/configloader.py
+++ b/zuul/configloader.py
@@ -1519,13 +1519,15 @@ class ApiRootParser(object):
def getSchema(self):
api_root = {
- 'authentication-realm': str
+ 'authentication-realm': str,
+ 'access-rules': to_list(str),
}
return vs.Schema(api_root)
def fromYaml(self, conf):
self.schema(conf)
api_root = model.ApiRoot(conf.get('authentication-realm'))
+ api_root.access_rules = conf.get('access-rules', [])
api_root.freeze()
return api_root
@@ -1645,6 +1647,7 @@ class TenantParser(object):
'allow-circular-dependencies': bool,
'default-parent': str,
'default-ansible-version': vs.Any(str, float),
+ 'access-rules': to_list(str),
'admin-rules': to_list(str),
'semaphores': to_list(str),
'authentication-realm': str,
@@ -1677,6 +1680,8 @@ class TenantParser(object):
conf['exclude-unprotected-branches']
if conf.get('admin-rules') is not None:
tenant.admin_rules = conf['admin-rules']
+ if conf.get('access-rules') is not None:
+ tenant.access_rules = conf['access-rules']
if conf.get('authentication-realm') is not None:
tenant.default_auth_realm = conf['authentication-realm']
if conf.get('semaphores') is not None:
diff --git a/zuul/model.py b/zuul/model.py
index c90589363..7269dee24 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -1252,6 +1252,7 @@ class ApiRoot(ConfigObject):
def __init__(self, default_auth_realm=None):
super().__init__()
self.default_auth_realm = default_auth_realm
+ self.access_rules = []
def __ne__(self, other):
return not self.__eq__(other)
@@ -1259,7 +1260,8 @@ class ApiRoot(ConfigObject):
def __eq__(self, other):
if not isinstance(other, ApiRoot):
return False
- return (self.default_auth_realm == other.default_auth_realm)
+ return (self.default_auth_realm == other.default_auth_realm,
+ self.access_rules == other.access_rules)
def __repr__(self):
return f'<ApiRoot realm={self.default_auth_realm}>'
@@ -7991,6 +7993,7 @@ class Tenant(object):
# The per tenant default ansible version
self.default_ansible_version = None
+ self.access_rules = []
self.admin_rules = []
self.default_auth_realm = None
self.global_semaphores = set()
diff --git a/zuul/web/__init__.py b/zuul/web/__init__.py
index cd8e2956b..eb30e1d28 100755
--- a/zuul/web/__init__.py
+++ b/zuul/web/__init__.py
@@ -102,6 +102,24 @@ def get_request_logger(logger=None):
return get_annotated_logger(logger, None, request=request.zuul_request_id)
+class APIError(cherrypy.HTTPError):
+ def __init__(self, code, json_doc=None):
+ self._json_doc = json_doc
+ super().__init__(code)
+
+ def set_response(self):
+ super().set_response()
+ resp = cherrypy.response
+ if self._json_doc:
+ ret = json.dumps(self._json_doc).encode('utf8')
+ resp.body = ret
+ resp.headers['Content-Type'] = 'application/json'
+ resp.headers["Content-Length"] = len(ret)
+ else:
+ resp.body = b''
+ resp.headers["Content-Length"] = '0'
+
+
class SaveParamsTool(cherrypy.Tool):
"""
Save the URL parameters to allow them to take precedence over query
@@ -109,7 +127,7 @@ class SaveParamsTool(cherrypy.Tool):
"""
def __init__(self):
cherrypy.Tool.__init__(self, 'on_start_resource',
- self.saveParams)
+ self.saveParams, priority=10)
def _setup(self):
cherrypy.Tool._setup(self)
@@ -149,7 +167,8 @@ def handle_options(allowed_methods=None):
cherrypy.tools.handle_options = cherrypy.Tool('on_start_resource',
- handle_options)
+ handle_options,
+ priority=50)
class AuthInfo:
@@ -158,39 +177,67 @@ class AuthInfo:
self.admin = admin
-def check_auth(require_admin=False, require_auth=False):
+def _check_auth(require_admin=False, require_auth=False, tenant=None):
if require_admin:
require_auth = True
request = cherrypy.serving.request
zuulweb = request.app.root
+
+ if tenant:
+ if not require_auth and tenant.access_rules:
+ # This tenant requires auth for read-only access
+ require_auth = True
+ else:
+ if not require_auth and zuulweb.zuulweb.abide.api_root.access_rules:
+ # The API root requires auth for read-only access
+ require_auth = True
+ # Always set the auth variable
+ request.params['auth'] = None
+
+ basic_error = zuulweb._basic_auth_header_check(required=require_auth)
+ if basic_error is not None:
+ return
+ claims, token_error = zuulweb._auth_token_check(required=require_auth)
+ if token_error is not None:
+ return
+ access, admin = zuulweb._isAuthorized(tenant, claims)
+ if (require_auth and not access) or (require_admin and not admin):
+ raise APIError(403)
+
+ request.params['auth'] = AuthInfo(claims['__zuul_uid_claim'],
+ admin)
+
+
+def check_root_auth(**kw):
+ """Use this for root-level (non-tenant) methods"""
+ request = cherrypy.serving.request
+ if request.handler is None:
+ # handle_options has already aborted the request.
+ return
+ return _check_auth(**kw)
+
+
+def check_tenant_auth(**kw):
+ """Use this for tenant-scoped methods"""
+ request = cherrypy.serving.request
+ zuulweb = request.app.root
if request.handler is None:
# handle_options has already aborted the request.
return
- # Always set the tenant and uid variables
tenant_name = request.params.get('tenant_name')
+ # Always set the tenant variable
tenant = zuulweb._getTenantOrRaise(tenant_name)
request.params['tenant'] = tenant
- request.params['auth'] = None
-
- basic_error = zuulweb._basic_auth_header_check()
- if basic_error is not None and require_auth:
- request.handler = None
- return basic_error
- claims, token_error = zuulweb._auth_token_check()
- if token_error is not None and require_auth:
- request.handler = None
- return token_error
- admin = zuulweb._is_authorized(tenant, claims)
- if not admin and require_admin:
- raise cherrypy.HTTPError(403)
+ return _check_auth(**kw, tenant=tenant)
- request.params['auth'] = AuthInfo(claims['__zuul_uid_claim'],
- admin)
-
-cherrypy.tools.check_auth = cherrypy.Tool('before_request_body',
- check_auth)
+cherrypy.tools.check_root_auth = cherrypy.Tool('on_start_resource',
+ check_root_auth,
+ priority=90)
+cherrypy.tools.check_tenant_auth = cherrypy.Tool('on_start_resource',
+ check_tenant_auth,
+ priority=90)
class StatsTool(cherrypy.Tool):
@@ -418,17 +465,15 @@ class ZuulWebAPI(object):
def log(self):
return get_request_logger()
- def _basic_auth_header_check(self):
+ def _basic_auth_header_check(self, required=True):
"""make sure protected endpoints have a Authorization header with the
bearer token."""
token = cherrypy.request.headers.get('Authorization', None)
# Add basic checks here
if token is None:
- status = 401
e = 'Missing "Authorization" header'
e_desc = e
elif not token.lower().startswith('bearer '):
- status = 401
e = 'Invalid Authorization header format'
e_desc = '"Authorization" header must start with "Bearer"'
else:
@@ -438,21 +483,24 @@ class ZuulWebAPI(object):
error_description="%s"''' % (self.zuulweb.authenticators.default_realm,
e,
e_desc)
- cherrypy.response.status = status
- cherrypy.response.headers["WWW-Authenticate"] = error_header
- return {'description': e_desc,
- 'error': e,
- 'realm': self.zuulweb.authenticators.default_realm}
-
- def _auth_token_check(self):
+ error_data = {'description': e_desc,
+ 'error': e,
+ 'realm': self.zuulweb.authenticators.default_realm}
+ if required:
+ cherrypy.response.headers["WWW-Authenticate"] = error_header
+ raise APIError(401, error_data)
+ return error_data
+
+ def _auth_token_check(self, required=True):
rawToken = \
cherrypy.request.headers['Authorization'][len('Bearer '):]
try:
claims = self.zuulweb.authenticators.authenticate(rawToken)
except exceptions.AuthTokenException as e:
- for header, contents in e.getAdditionalHeaders().items():
- cherrypy.response.headers[header] = contents
- cherrypy.response.status = e.HTTPError
+ if required:
+ for header, contents in e.getAdditionalHeaders().items():
+ cherrypy.response.headers[header] = contents
+ raise APIError(e.HTTPError)
return ({},
{'description': e.error_description,
'error': e.error,
@@ -463,8 +511,8 @@ class ZuulWebAPI(object):
@cherrypy.tools.json_in()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
@cherrypy.tools.handle_options(allowed_methods=['POST', ])
- @cherrypy.tools.check_auth(require_admin=True)
- def dequeue(self, tenant_name, project_name, tenant, auth):
+ @cherrypy.tools.check_tenant_auth(require_admin=True)
+ def dequeue(self, tenant_name, tenant, auth, project_name):
if cherrypy.request.method != 'POST':
raise cherrypy.HTTPError(405)
self.log.info(f'User {auth.uid} requesting dequeue on '
@@ -499,8 +547,8 @@ class ZuulWebAPI(object):
@cherrypy.tools.json_in()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
@cherrypy.tools.handle_options(allowed_methods=['POST', ])
- @cherrypy.tools.check_auth(require_admin=True)
- def enqueue(self, tenant_name, project_name, tenant, auth):
+ @cherrypy.tools.check_tenant_auth(require_admin=True)
+ def enqueue(self, tenant_name, tenant, auth, project_name):
if cherrypy.request.method != 'POST':
raise cherrypy.HTTPError(405)
self.log.info(f'User {auth.uid} requesting enqueue on '
@@ -555,7 +603,7 @@ class ZuulWebAPI(object):
@cherrypy.tools.json_in()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
@cherrypy.tools.handle_options(allowed_methods=['POST', ])
- @cherrypy.tools.check_auth(require_admin=True)
+ @cherrypy.tools.check_tenant_auth(require_admin=True)
def promote(self, tenant_name, tenant, auth):
if cherrypy.request.method != 'POST':
raise cherrypy.HTTPError(405)
@@ -584,8 +632,9 @@ class ZuulWebAPI(object):
@cherrypy.expose
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
- def autohold_list(self, tenant_name, *args, **kwargs):
- _ = self._getTenantOrRaise(tenant_name)
+ @cherrypy.tools.handle_options()
+ @cherrypy.tools.check_tenant_auth()
+ def autohold_list(self, tenant_name, tenant, auth, *args, **kwargs):
# filter by project if passed as a query string
project_name = cherrypy.request.params.get('project', None)
return self._autohold_list(tenant_name, project_name)
@@ -593,7 +642,8 @@ class ZuulWebAPI(object):
@cherrypy.expose
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
@cherrypy.tools.handle_options(allowed_methods=['GET', 'POST'])
- def autohold_project_get(self, tenant_name, project_name):
+ @cherrypy.tools.check_tenant_auth()
+ def autohold_project_get(self, tenant_name, tenant, auth, project_name):
# Note: GET handling is redundant with autohold_list
# and could be removed.
return self._autohold_list(tenant_name, project_name)
@@ -601,8 +651,9 @@ class ZuulWebAPI(object):
@cherrypy.expose
@cherrypy.tools.json_in()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
- @cherrypy.tools.check_auth(require_admin=True)
- def autohold_project_post(self, tenant_name, project_name, tenant, auth):
+ # Options handled by _get method
+ @cherrypy.tools.check_tenant_auth(require_admin=True)
+ def autohold_project_post(self, tenant_name, tenant, auth, project_name):
project = self._getProjectOrRaise(tenant, project_name)
self.log.info(f'User {auth.uid} requesting autohold on '
f'{tenant_name}/{project_name}')
@@ -700,7 +751,8 @@ class ZuulWebAPI(object):
@cherrypy.expose
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
@cherrypy.tools.handle_options(allowed_methods=['GET', 'DELETE', ])
- def autohold_get(self, tenant_name, request_id):
+ @cherrypy.tools.check_tenant_auth()
+ def autohold_get(self, tenant_name, tenant, auth, request_id):
request = self._getAutoholdRequest(tenant_name, request_id)
resp = cherrypy.response
resp.headers['Access-Control-Allow-Origin'] = '*'
@@ -720,8 +772,9 @@ class ZuulWebAPI(object):
@cherrypy.expose
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
- @cherrypy.tools.check_auth(require_admin=True)
- def autohold_delete(self, tenant_name, request_id, tenant, auth):
+ # Options handled by get method
+ @cherrypy.tools.check_tenant_auth(require_admin=True)
+ def autohold_delete(self, tenant_name, tenant, auth, request_id):
request = self._getAutoholdRequest(tenant_name, request_id)
self.log.info(f'User {auth.uid} requesting autohold-delete on '
f'{request.tenant}/{request.project}')
@@ -756,7 +809,9 @@ class ZuulWebAPI(object):
@cherrypy.expose
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
- def index(self):
+ @cherrypy.tools.handle_options()
+ @cherrypy.tools.check_root_auth()
+ def index(self, auth):
return {
'info': '/api/info',
'connections': '/api/connections',
@@ -800,33 +855,38 @@ class ZuulWebAPI(object):
}
@cherrypy.expose
+ @cherrypy.tools.handle_options()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
+ # Info endpoints never require authentication because they supply
+ # authentication information.
def info(self):
info = self.zuulweb.info.copy()
+ auth_info = info.capabilities.capabilities['auth']
root_realm = self.zuulweb.abide.api_root.default_auth_realm
if root_realm:
- if (info.capabilities is not None and
- info.capabilities.toDict().get('auth') is not None):
- info.capabilities.capabilities['auth']['default_realm'] =\
- root_realm
+ auth_info['default_realm'] = root_realm
+ read_protected = bool(self.zuulweb.abide.api_root.access_rules)
+ auth_info['read_protected'] = read_protected
return self._handleInfo(info)
@cherrypy.expose
@cherrypy.tools.save_params()
+ @cherrypy.tools.handle_options()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
+ # Info endpoints never require authentication because they supply
+ # authentication information.
def tenant_info(self, tenant_name):
info = self.zuulweb.info.copy()
+ auth_info = info.capabilities.capabilities['auth']
info.tenant = tenant_name
- tenant_config = self.zuulweb.unparsed_abide.tenants.get(tenant_name)
- if tenant_config is not None:
+ tenant = self.zuulweb.abide.tenants.get(tenant_name)
+ if tenant 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
+ if tenant.default_auth_realm is not None:
+ auth_info['default_realm'] = tenant.default_auth_realm
+ read_protected = bool(tenant.access_rules)
+ auth_info['read_protected'] = read_protected
return self._handleInfo(info)
def _handleInfo(self, info):
@@ -839,35 +899,74 @@ class ZuulWebAPI(object):
resp.last_modified = self.zuulweb.start_time
return ret
- def _is_authorized(self, tenant, claims):
+ def _isAuthorized(self, tenant, claims):
# First, check for zuul.admin override
+ if tenant:
+ tenant_name = tenant.name
+ admin_rules = tenant.admin_rules
+ access_rules = tenant.access_rules
+ else:
+ tenant_name = '*'
+ admin_rules = []
+ access_rules = self.zuulweb.api_root.access_rules
override = claims.get('zuul', {}).get('admin', [])
if (override == '*' or
- (isinstance(override, list) and tenant.name in override)):
- return True
+ (isinstance(override, list) and tenant_name in override)):
+ return (True, True)
- for rule_name in tenant.admin_rules:
+ if not tenant:
+ tenant_name = '<root>'
+
+ if access_rules:
+ access = False
+ else:
+ access = True
+ for rule_name in access_rules:
rule = self.zuulweb.abide.authz_rules.get(rule_name)
if not rule:
self.log.error('Undefined rule "%s"', rule_name)
continue
- self.log.debug('Applying rule "%s" from tenant "%s" to claims %s',
- rule_name, tenant.name, json.dumps(claims))
+ self.log.debug('Applying access rule "%s" from '
+ 'tenant "%s" to claims %s',
+ rule_name, tenant_name, json.dumps(claims))
authorized = rule(claims, tenant)
if authorized:
if '__zuul_uid_claim' in claims:
uid = claims['__zuul_uid_claim']
else:
uid = json.dumps(claims)
- self.log.info('%s authorized on tenant "%s" by rule "%s"',
- uid, tenant.name, rule_name)
- return True
- return False
+ self.log.info('%s authorized access on '
+ 'tenant "%s" by rule "%s"',
+ uid, tenant_name, rule_name)
+ access = True
+ break
+
+ admin = False
+ for rule_name in admin_rules:
+ rule = self.zuulweb.abide.authz_rules.get(rule_name)
+ if not rule:
+ self.log.error('Undefined rule "%s"', rule_name)
+ continue
+ self.log.debug('Applying admin rule "%s" from '
+ 'tenant "%s" to claims %s',
+ rule_name, tenant_name, json.dumps(claims))
+ authorized = rule(claims, tenant)
+ if authorized:
+ if '__zuul_uid_claim' in claims:
+ uid = claims['__zuul_uid_claim']
+ else:
+ uid = json.dumps(claims)
+ self.log.info('%s authorized admin on '
+ 'tenant "%s" by rule "%s"',
+ uid, tenant_name, rule_name)
+ access = admin = True
+ break
+ return (access, admin)
@cherrypy.expose
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
- @cherrypy.tools.handle_options(allowed_methods=['GET', ])
- @cherrypy.tools.check_auth(require_auth=True)
+ @cherrypy.tools.handle_options()
+ @cherrypy.tools.check_tenant_auth(require_auth=True)
def tenant_authorizations(self, tenant_name, tenant, auth):
resp = cherrypy.response
resp.headers['Access-Control-Allow-Origin'] = '*'
@@ -895,7 +994,9 @@ class ZuulWebAPI(object):
@cherrypy.expose
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
- def tenants(self):
+ @cherrypy.tools.handle_options()
+ @cherrypy.tools.check_root_auth()
+ def tenants(self, auth):
cache_time = self.tenants_cache_time
if time.time() - cache_time > self.cache_expiry:
with self.tenants_cache_lock:
@@ -914,7 +1015,9 @@ class ZuulWebAPI(object):
@cherrypy.expose
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
- def connections(self):
+ @cherrypy.tools.handle_options()
+ @cherrypy.tools.check_root_auth()
+ def connections(self, auth):
ret = [s.connection.toDict()
for s in self.zuulweb.connections.getSources()]
resp = cherrypy.response
@@ -923,7 +1026,9 @@ class ZuulWebAPI(object):
@cherrypy.expose
@cherrypy.tools.json_out(content_type="application/json; charset=utf-8")
- def components(self):
+ @cherrypy.tools.handle_options()
+ @cherrypy.tools.check_root_auth()
+ def components(self, auth):
ret = {}
for kind, components in self.zuulweb.component_registry.all():
for comp in components:
@@ -937,33 +1042,32 @@ class ZuulWebAPI(object):
resp.headers["Access-Control-Allow-Origin"] = "*"
return ret
- def _getStatus(self, tenant_name):
- tenant = self._getTenantOrRaise(tenant_name)
- cache_time = self.status_cache_times.get(tenant_name, 0)
- if tenant_name not in self.status_cache_locks or \
+ def _getStatus(self, tenant):
+ cache_time = self.status_cache_times.get(tenant.name, 0)
+ if tenant.name not in self.status_cache_locks or \
(time.time() - cache_time) > self.cache_expiry:
- if self.status_cache_locks[tenant_name].acquire(
+ if self.status_cache_locks[tenant.name].acquire(
blocking=False
):
try:
- self.status_caches[tenant_name] =\
+ self.status_caches[tenant.name] =\
self.formatStatus(tenant)
- self.status_cache_times[tenant_name] =\
+ self.status_cache_times[tenant.name] =\
time.time()
finally:
- self.status_cache_locks[tenant_name].release()
- if not self.status_caches.get(tenant_name):
+ self.status_cache_locks[tenant.name].release()
+ if not self.status_caches.get(tenant.name):
# If the cache is empty at this point it means that we didn't
# get the lock but another thread is initializing the cache
# for the first time. In this case we just wait for the lock
# to wait for it to finish.
- with self.status_cache_locks[tenant_name]:
+ with self.status_cache_locks[tenant.name]:
pass
- payload = self.status_caches[tenant_name]
+ payload = self.status_caches[tenant.name]
resp = cherrypy.response
resp.headers["Cache-Control"] = f"public, max-age={self.cache_expiry}"
last_modified = datetime.utcfromtimestamp(
- self.status_cache_times[tenant_name]
+ self.status_cache_times[tenant.name]
)
last_modified_header = last_modified.strftime('%a, %d %b %Y %X GMT')
resp.headers["Last-modified"] = last_modified_header
@@ -1027,14 +1131,18 @@ class ZuulWebAPI(object):
@cherrypy.expose
@cherrypy.tools.save_params()
- def status(self, tenant_name):
- return self._getStatus(tenant_name)[1]
+ @cherrypy.tools.handle_options()
+ @cherrypy.tools.check_tenant_auth()
+ def status(self, tenant_name, tenant, auth):
+ return self._getStatus(tenant)[1]
@cherrypy.expose
@cherrypy.tools.save_params()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
- def status_change(self, tenant_name, change):
- payload = self._getStatus(tenant_name)[0]
+ @cherrypy.tools.handle_options()
+ @cherrypy.tools.check_tenant_auth()
+ def status_change(self, tenant_name, tenant, auth, change):
+ payload = self._getStatus(tenant)[0]
result_filter = ChangeFilter(change)
return result_filter.filterPayload(payload)
@@ -1043,8 +1151,9 @@ class ZuulWebAPI(object):
@cherrypy.tools.json_out(
content_type='application/json; charset=utf-8', handler=json_handler,
)
- def jobs(self, tenant_name):
- tenant = self._getTenantOrRaise(tenant_name)
+ @cherrypy.tools.handle_options()
+ @cherrypy.tools.check_tenant_auth()
+ def jobs(self, tenant_name, tenant, auth):
result = []
for job_name in sorted(tenant.layout.jobs):
desc = None
@@ -1083,8 +1192,9 @@ class ZuulWebAPI(object):
@cherrypy.expose
@cherrypy.tools.save_params()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
- def config_errors(self, tenant_name):
- tenant = self._getTenantOrRaise(tenant_name)
+ @cherrypy.tools.handle_options()
+ @cherrypy.tools.check_tenant_auth()
+ def config_errors(self, tenant_name, tenant, auth):
ret = [
{'source_context': e.key.context.toDict(),
'error': e.error}
@@ -1098,9 +1208,10 @@ class ZuulWebAPI(object):
@cherrypy.tools.save_params()
@cherrypy.tools.json_out(
content_type='application/json; charset=utf-8', handler=json_handler)
- def job(self, tenant_name, job_name):
+ @cherrypy.tools.handle_options()
+ @cherrypy.tools.check_tenant_auth()
+ def job(self, tenant_name, tenant, auth, job_name):
job_name = urllib.parse.unquote_plus(job_name)
- tenant = self._getTenantOrRaise(tenant_name)
job_variants = tenant.layout.jobs.get(job_name)
result = []
for job in job_variants:
@@ -1113,8 +1224,9 @@ class ZuulWebAPI(object):
@cherrypy.expose
@cherrypy.tools.save_params()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
- def projects(self, tenant_name):
- tenant = self._getTenantOrRaise(tenant_name)
+ @cherrypy.tools.handle_options()
+ @cherrypy.tools.check_tenant_auth()
+ def projects(self, tenant_name, tenant, auth):
result = []
for project in tenant.config_projects:
pobj = project.toDict()
@@ -1133,8 +1245,9 @@ class ZuulWebAPI(object):
@cherrypy.tools.save_params()
@cherrypy.tools.json_out(
content_type='application/json; charset=utf-8', handler=json_handler)
- def project(self, tenant_name, project_name):
- tenant = self._getTenantOrRaise(tenant_name)
+ @cherrypy.tools.handle_options()
+ @cherrypy.tools.check_tenant_auth()
+ def project(self, tenant_name, tenant, auth, project_name):
project = self._getProjectOrRaise(tenant, project_name)
result = project.toDict()
@@ -1163,8 +1276,9 @@ class ZuulWebAPI(object):
@cherrypy.expose
@cherrypy.tools.save_params()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
- def pipelines(self, tenant_name):
- tenant = self._getTenantOrRaise(tenant_name)
+ @cherrypy.tools.handle_options()
+ @cherrypy.tools.check_tenant_auth()
+ def pipelines(self, tenant_name, tenant, auth):
ret = []
for pipeline, pipeline_config in tenant.layout.pipelines.items():
triggers = []
@@ -1187,8 +1301,9 @@ class ZuulWebAPI(object):
@cherrypy.expose
@cherrypy.tools.save_params()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
- def labels(self, tenant_name):
- tenant = self._getTenantOrRaise(tenant_name)
+ @cherrypy.tools.handle_options()
+ @cherrypy.tools.check_tenant_auth()
+ def labels(self, tenant_name, tenant, auth):
allowed_labels = tenant.allowed_labels or []
disallowed_labels = tenant.disallowed_labels or []
labels = set()
@@ -1204,7 +1319,9 @@ class ZuulWebAPI(object):
@cherrypy.expose
@cherrypy.tools.save_params()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
- def nodes(self, tenant_name):
+ @cherrypy.tools.handle_options()
+ @cherrypy.tools.check_tenant_auth()
+ def nodes(self, tenant_name, tenant, auth):
ret = []
for node_id in self.zk_nodepool.getNodes(cached=True):
node = self.zk_nodepool.getNode(node_id)
@@ -1228,8 +1345,9 @@ class ZuulWebAPI(object):
@cherrypy.expose
@cherrypy.tools.save_params()
- def key(self, tenant_name, project_name):
- tenant = self._getTenantOrRaise(tenant_name)
+ @cherrypy.tools.handle_options()
+ @cherrypy.tools.check_tenant_auth()
+ def key(self, tenant_name, tenant, auth, project_name):
project = self._getProjectOrRaise(tenant, project_name)
key = encryption.serialize_rsa_public_key(project.public_secrets_key)
@@ -1240,8 +1358,9 @@ class ZuulWebAPI(object):
@cherrypy.expose
@cherrypy.tools.save_params()
- def project_ssh_key(self, tenant_name, project_name):
- tenant = self._getTenantOrRaise(tenant_name)
+ @cherrypy.tools.handle_options()
+ @cherrypy.tools.check_tenant_auth()
+ def project_ssh_key(self, tenant_name, tenant, auth, project_name):
project = self._getProjectOrRaise(tenant, project_name)
key = f"{project.public_ssh_key}\n"
@@ -1320,12 +1439,14 @@ class ZuulWebAPI(object):
@cherrypy.expose
@cherrypy.tools.save_params()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
- def builds(self, tenant_name, project=None, pipeline=None, change=None,
- branch=None, patchset=None, ref=None, newrev=None,
- uuid=None, job_name=None, voting=None, nodeset=None,
- result=None, final=None, held=None, complete=None,
- limit=50, skip=0, idx_min=None, idx_max=None,
- exclude_result=None):
+ @cherrypy.tools.handle_options()
+ @cherrypy.tools.check_tenant_auth()
+ def builds(self, tenant_name, tenant, auth, project=None,
+ pipeline=None, change=None, branch=None, patchset=None,
+ ref=None, newrev=None, uuid=None, job_name=None,
+ voting=None, nodeset=None, result=None, final=None,
+ held=None, complete=None, limit=50, skip=0,
+ idx_min=None, idx_max=None, exclude_result=None):
connection = self._get_connection()
if tenant_name not in self.zuulweb.abide.tenants.keys():
@@ -1361,7 +1482,9 @@ class ZuulWebAPI(object):
@cherrypy.expose
@cherrypy.tools.save_params()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
- def build(self, tenant_name, uuid):
+ @cherrypy.tools.handle_options()
+ @cherrypy.tools.check_tenant_auth()
+ def build(self, tenant_name, tenant, auth, uuid):
connection = self._get_connection()
data = connection.getBuilds(tenant=tenant_name, uuid=uuid, limit=1)
@@ -1402,7 +1525,10 @@ class ZuulWebAPI(object):
@cherrypy.expose
@cherrypy.tools.save_params()
- def badge(self, tenant_name, project=None, pipeline=None, branch=None):
+ @cherrypy.tools.handle_options()
+ @cherrypy.tools.check_tenant_auth()
+ def badge(self, tenant_name, tenant, auth, project=None,
+ pipeline=None, branch=None):
connection = self._get_connection()
buildsets = connection.getBuildsets(
@@ -1426,9 +1552,12 @@ class ZuulWebAPI(object):
@cherrypy.expose
@cherrypy.tools.save_params()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
- def buildsets(self, tenant_name, project=None, pipeline=None, change=None,
- branch=None, patchset=None, ref=None, newrev=None,
- uuid=None, result=None, complete=None, limit=50, skip=0,
+ @cherrypy.tools.handle_options()
+ @cherrypy.tools.check_tenant_auth()
+ def buildsets(self, tenant_name, tenant, auth, project=None,
+ pipeline=None, change=None, branch=None,
+ patchset=None, ref=None, newrev=None, uuid=None,
+ result=None, complete=None, limit=50, skip=0,
idx_min=None, idx_max=None):
connection = self._get_connection()
@@ -1454,7 +1583,9 @@ class ZuulWebAPI(object):
@cherrypy.expose
@cherrypy.tools.save_params()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
- def buildset(self, tenant_name, uuid):
+ @cherrypy.tools.handle_options()
+ @cherrypy.tools.check_tenant_auth()
+ def buildset(self, tenant_name, tenant, auth, uuid):
connection = self._get_connection()
data = connection.getBuildset(tenant_name, uuid)
@@ -1470,8 +1601,9 @@ class ZuulWebAPI(object):
@cherrypy.tools.json_out(
content_type='application/json; charset=utf-8', handler=json_handler,
)
- def semaphores(self, tenant_name):
- tenant = self._getTenantOrRaise(tenant_name)
+ @cherrypy.tools.handle_options()
+ @cherrypy.tools.check_tenant_auth()
+ def semaphores(self, tenant_name, tenant, auth):
result = []
names = set(tenant.layout.semaphores.keys())
names = names.union(tenant.global_semaphores)
@@ -1505,17 +1637,28 @@ class ZuulWebAPI(object):
@cherrypy.expose
@cherrypy.tools.save_params()
+ @cherrypy.tools.handle_options()
+ # We don't check auth here since we would never fall through to it
+ def console_stream_options(self, tenant_name):
+ cherrypy.request.ws_handler.zuulweb = self.zuulweb
+
+ @cherrypy.expose
+ @cherrypy.tools.save_params()
@cherrypy.tools.websocket(handler_cls=LogStreamHandler)
- def console_stream(self, tenant_name):
+ # Options handling in _options method
+ @cherrypy.tools.check_tenant_auth()
+ def console_stream_get(self, tenant_name, tenant, auth):
cherrypy.request.ws_handler.zuulweb = self.zuulweb
@cherrypy.expose
@cherrypy.tools.save_params()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
- def project_freeze_jobs(self, tenant_name, pipeline_name, project_name,
- branch_name):
+ @cherrypy.tools.handle_options()
+ @cherrypy.tools.check_tenant_auth()
+ def project_freeze_jobs(self, tenant_name, tenant, auth,
+ pipeline_name, project_name, branch_name):
item = self._freeze_jobs(
- tenant_name, pipeline_name, project_name, branch_name)
+ tenant, pipeline_name, project_name, branch_name)
output = []
for job in item.current_build_set.job_graph.getJobs():
@@ -1533,12 +1676,15 @@ class ZuulWebAPI(object):
@cherrypy.expose
@cherrypy.tools.save_params()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
- def project_freeze_job(self, tenant_name, pipeline_name, project_name,
- branch_name, job_name):
+ @cherrypy.tools.handle_options()
+ @cherrypy.tools.check_tenant_auth()
+ def project_freeze_job(self, tenant_name, tenant, auth,
+ pipeline_name, project_name, branch_name,
+ job_name):
# TODO(jhesketh): Allow a canonical change/item to be passed in which
# would return the job with any in-change modifications.
item = self._freeze_jobs(
- tenant_name, pipeline_name, project_name, branch_name)
+ tenant, pipeline_name, project_name, branch_name)
job = item.current_build_set.jobs.get(job_name)
if not job:
raise cherrypy.HTTPError(404)
@@ -1573,10 +1719,9 @@ class ZuulWebAPI(object):
resp.headers['Access-Control-Allow-Origin'] = '*'
return ret
- def _freeze_jobs(self, tenant_name, pipeline_name, project_name,
+ def _freeze_jobs(self, tenant, pipeline_name, project_name,
branch_name):
- tenant = self._getTenantOrRaise(tenant_name)
project = self._getProjectOrRaise(tenant, project_name)
pipeline = tenant.layout.pipelines.get(pipeline_name)
if not pipeline:
@@ -1921,7 +2066,11 @@ class ZuulWeb(object):
'project-ssh-key/{project_name:.*}.pub',
controller=api, action='project_ssh_key')
route_map.connect('api', '/api/tenant/{tenant_name}/console-stream',
- controller=api, action='console_stream')
+ controller=api, action='console_stream_get',
+ conditions=dict(method=['GET']))
+ route_map.connect('api', '/api/tenant/{tenant_name}/console-stream',
+ controller=api, action='console_stream_options',
+ conditions=dict(method=['OPTIONS']))
route_map.connect('api', '/api/tenant/{tenant_name}/builds',
controller=api, action='builds')
route_map.connect('api', '/api/tenant/{tenant_name}/badge',