diff options
Diffstat (limited to 'zuul')
-rw-r--r-- | zuul/configloader.py | 7 | ||||
-rw-r--r-- | zuul/model.py | 5 | ||||
-rwxr-xr-x | zuul/web/__init__.py | 419 |
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', |