[api][cors] Add CORS configuration

CORS can be enabled in zuul.conf (disabled by default). If enabled,
zuul-web will check the Origin header if presented by the requester.

A white list of allowed request origins can be defined in zuul.conf.

Co-Authored-By: Matthieu Huin <mhuin@redhat.com>

Change-Id: If9cf545dab3c72a2a4e46058eee076af3409ee1b
This commit is contained in:
Clément Mondion 2020-12-18 11:17:51 +01:00 committed by Matthieu Huin
parent a2b8b975d0
commit 578fd9223b
7 changed files with 365 additions and 88 deletions

View File

@ -1041,6 +1041,18 @@ sections of ``zuul.conf`` are used by the web server:
The Cache-Control max-age response header value for static files served
by the zuul-web. Set to 0 during development to disable Cache-Control.
.. attr:: enable_cors
:default: false
Whether or not CORS shall be enabled. If true, CORS checks will be strictly enforced,
ie clients must set the "Origin" header in their queries.
.. attr:: allowed_origins
:default: localhost
A comma-separated list of origins to be allowed when enabling CORS. Use '*' to enable
CORS but allow any origin.
.. _web-server-tenant-scoped-api:
Enabling tenant-scoped access to privileged actions

View File

@ -0,0 +1,37 @@
[gearman]
server=127.0.0.1
[scheduler]
tenant_config=main.yaml
relative_priority=true
[merger]
git_dir=/tmp/zuul-test/merger-git
git_user_email=zuul@example.com
git_user_name=zuul
[executor]
git_dir=/tmp/zuul-test/executor-git
[connection gerrit]
driver=gerrit
server=review.example.com
user=jenkins
sshkey=fake_id_rsa_path
[web]
static_cache_expiry=1200
enable_cors=true
allowed_origins=foo.zuul,bar.zuul
[auth zuul_operator]
driver=HS256
allow_authz_override=true
realm=zuul.example.com
client_id=zuul.example.com
issuer_id=zuul_operator
secret=NoDanaOnlyZuul
[database]
dburi=$MYSQL_FIXTURE_DBURI$

View File

@ -0,0 +1,37 @@
[gearman]
server=127.0.0.1
[scheduler]
tenant_config=main.yaml
relative_priority=true
[merger]
git_dir=/tmp/zuul-test/merger-git
git_user_email=zuul@example.com
git_user_name=zuul
[executor]
git_dir=/tmp/zuul-test/executor-git
[connection gerrit]
driver=gerrit
server=review.example.com
user=jenkins
sshkey=fake_id_rsa_path
[web]
static_cache_expiry=1200
enable_cors=true
allowed_origins=*
[auth zuul_operator]
driver=HS256
allow_authz_override=true
realm=zuul.example.com
client_id=zuul.example.com
issuer_id=zuul_operator
secret=NoDanaOnlyZuul
[database]
dburi=$MYSQL_FIXTURE_DBURI$

View File

@ -0,0 +1,30 @@
[gearman]
server=127.0.0.1
[scheduler]
tenant_config=main.yaml
[merger]
git_dir=/tmp/zuul-test/merger-git
git_user_email=zuul@example.com
git_user_name=zuul
[executor]
git_dir=/tmp/zuul-test/executor-git
[connection gerrit]
driver=gerrit
server=review.example.com
user=jenkins
sshkey=fake_id_rsa1
[database]
dburi=$MYSQL_FIXTURE_DBURI$
[connection resultsdb_failures]
driver=sql
dburi=$MYSQL_FIXTURE_DBURI$
[web]
enable_cors=true
allowed_origins=foo.zuul,bar.zuul

View File

@ -137,7 +137,6 @@ class TestWeb(BaseTestWeb):
self.assertIn('Content-Type', resp.headers)
self.assertEqual(
'application/json; charset=utf-8', resp.headers['Content-Type'])
self.assertIn('Access-Control-Allow-Origin', resp.headers)
self.assertIn('Cache-Control', resp.headers)
self.assertIn('Last-Modified', resp.headers)
self.assertTrue(resp.headers['Last-Modified'].endswith(' GMT'))
@ -1431,6 +1430,7 @@ class TestArtifacts(BaseTestWeb, AnsibleZuulTestCase):
class TestTenantScopedWebApi(BaseTestWeb):
config_file = 'zuul-admin-web.conf'
extra_headers = {}
def test_admin_routes_no_token(self):
resp = self.post_url(
@ -1438,13 +1438,15 @@ class TestTenantScopedWebApi(BaseTestWeb):
json={'job': 'project-test1',
'count': 1,
'reason': 'because',
'node_hold_expiration': 36000})
'node_hold_expiration': 36000},
headers=self.extra_headers or None)
self.assertEqual(401, resp.status_code)
resp = self.post_url(
"api/tenant/tenant-one/project/org/project/enqueue",
json={'trigger': 'gerrit',
'change': '2,1',
'pipeline': 'check'})
'pipeline': 'check'},
headers=self.extra_headers or None)
self.assertEqual(401, resp.status_code)
resp = self.post_url(
"api/tenant/tenant-one/project/org/project/enqueue",
@ -1452,7 +1454,8 @@ class TestTenantScopedWebApi(BaseTestWeb):
'ref': 'abcd',
'newrev': 'aaaa',
'oldrev': 'bbbb',
'pipeline': 'check'})
'pipeline': 'check'},
headers=self.extra_headers or None)
self.assertEqual(401, resp.status_code)
def test_bad_key_JWT_token(self):
@ -1465,9 +1468,11 @@ class TestTenantScopedWebApi(BaseTestWeb):
'exp': time.time() + 3600}
token = jwt.encode(authz, key='OnlyZuulNoDana',
algorithm='HS256')
headers = {'Authorization': 'Bearer %s' % token}
headers.update(self.extra_headers)
resp = self.post_url(
"api/tenant/tenant-one/project/org/project/autohold",
headers={'Authorization': 'Bearer %s' % token},
headers=headers,
json={'job': 'project-test1',
'count': 1,
'reason': 'because',
@ -1475,14 +1480,14 @@ class TestTenantScopedWebApi(BaseTestWeb):
self.assertEqual(401, resp.status_code)
resp = self.post_url(
"api/tenant/tenant-one/project/org/project/enqueue",
headers={'Authorization': 'Bearer %s' % token},
headers=headers,
json={'trigger': 'gerrit',
'change': '2,1',
'pipeline': 'check'})
self.assertEqual(401, resp.status_code)
resp = self.post_url(
"api/tenant/tenant-one/project/org/project/enqueue",
headers={'Authorization': 'Bearer %s' % token},
headers=headers,
json={'trigger': 'gerrit',
'ref': 'abcd',
'newrev': 'aaaa',
@ -1500,9 +1505,11 @@ class TestTenantScopedWebApi(BaseTestWeb):
'exp': time.time() - 3600}
token = jwt.encode(authz, key='NoDanaOnlyZuul',
algorithm='HS256')
headers = {'Authorization': 'Bearer %s' % token}
headers.update(self.extra_headers)
resp = self.post_url(
"api/tenant/tenant-one/project/org/project/autohold",
headers={'Authorization': 'Bearer %s' % token},
headers=headers,
json={'job': 'project-test1',
'count': 1,
'reason': 'because',
@ -1510,14 +1517,14 @@ class TestTenantScopedWebApi(BaseTestWeb):
self.assertEqual(401, resp.status_code)
resp = self.post_url(
"api/tenant/tenant-one/project/org/project/enqueue",
headers={'Authorization': 'Bearer %s' % token},
headers=headers,
json={'trigger': 'gerrit',
'change': '2,1',
'pipeline': 'check'})
self.assertEqual(401, resp.status_code)
resp = self.post_url(
"api/tenant/tenant-one/project/org/project/enqueue",
headers={'Authorization': 'Bearer %s' % token},
headers=headers,
json={'trigger': 'gerrit',
'ref': 'abcd',
'newrev': 'aaaa',
@ -1535,9 +1542,11 @@ class TestTenantScopedWebApi(BaseTestWeb):
'exp': time.time() + 3600}
token = jwt.encode(authz, key='NoDanaOnlyZuul',
algorithm='HS256')
headers = {'Authorization': 'Bearer %s' % token}
headers.update(self.extra_headers)
resp = self.post_url(
"api/tenant/tenant-one/project/org/project/autohold",
headers={'Authorization': 'Bearer %s' % token},
headers=headers,
json={'job': 'project-test1',
'count': 1,
'reason': 'because',
@ -1545,14 +1554,14 @@ class TestTenantScopedWebApi(BaseTestWeb):
self.assertEqual(403, resp.status_code)
resp = self.post_url(
"api/tenant/tenant-one/project/org/project/enqueue",
headers={'Authorization': 'Bearer %s' % token},
headers=headers,
json={'trigger': 'gerrit',
'change': '2,1',
'pipeline': 'check'})
self.assertEqual(403, resp.status_code)
resp = self.post_url(
"api/tenant/tenant-one/project/org/project/enqueue",
headers={'Authorization': 'Bearer %s' % token},
headers=headers,
json={'trigger': 'gerrit',
'ref': 'abcd',
'newrev': 'aaaa',
@ -1575,9 +1584,11 @@ class TestTenantScopedWebApi(BaseTestWeb):
'node_hold_expiration': None}
good_token = jwt.encode(good_authz, key='NoDanaOnlyZuul',
algorithm='HS256')
headers = {'Authorization': 'Bearer %s' % good_token}
headers.update(self.extra_headers)
req = self.post_url(
'api/tenant/tenant-one/project/org/project/autohold',
headers={'Authorization': 'Bearer %s' % good_token},
headers=headers,
json=args)
self.assertEqual(200, req.status_code, req.text)
client = zuul.rpcclient.RPCClient('127.0.0.1',
@ -1587,9 +1598,11 @@ class TestTenantScopedWebApi(BaseTestWeb):
self.assertNotEqual([], autohold_requests)
self.assertEqual(1, len(autohold_requests))
request = autohold_requests[0]
headers = {'Authorization': 'Bearer %s' % token}
headers.update(self.extra_headers)
resp = self.delete_url(
"api/tenant/tenant-one/autohold/%s" % request['id'],
headers={'Authorization': 'Bearer %s' % token})
headers=headers)
self.assertEqual(403, resp.status_code)
def test_autohold(self):
@ -1609,10 +1622,16 @@ class TestTenantScopedWebApi(BaseTestWeb):
'exp': time.time() + 3600}
token = jwt.encode(authz, key='NoDanaOnlyZuul',
algorithm='HS256')
headers = {'Authorization': 'Bearer %s' % token}
headers.update(self.extra_headers)
req = self.post_url(
'api/tenant/tenant-one/project/org/project/autohold',
headers={'Authorization': 'Bearer %s' % token},
headers=headers,
json=args)
# Expected failure if testing CORS with a bad origin
if self.extra_headers.get('Origin') == 'bad.zuul':
self.assertEqual(400, req.status_code, req.text)
return
self.assertEqual(200, req.status_code, req.text)
data = req.json()
self.assertEqual(True, data)
@ -1643,7 +1662,6 @@ class TestTenantScopedWebApi(BaseTestWeb):
"", "", "reason text", 1)
self.assertTrue(r)
# Use autohold-list API to retrieve request ID
resp = self.get_url(
"api/tenant/tenant-one/autohold")
self.assertEqual(200, resp.status_code, resp.text)
@ -1673,9 +1691,15 @@ class TestTenantScopedWebApi(BaseTestWeb):
'exp': time.time() + 3600}
bad_token = jwt.encode(bad_authz, key='NoDanaOnlyZuul',
algorithm='HS256')
headers = {'Authorization': 'Bearer %s' % bad_token}
headers.update(self.extra_headers)
resp = self.delete_url(
"api/tenant/tenant-one/autohold/%s" % request_id,
headers={'Authorization': 'Bearer %s' % bad_token})
headers=headers)
# Expected failure if testing CORS with a bad origin
if self.extra_headers.get('Origin') == 'bad.zuul':
self.assertEqual(400, resp.status_code, resp.text)
return
# Throw a "Forbidden" error, because user is authenticated but not
# authorized for tenant-one
self.assertEqual(403, resp.status_code, resp.text)
@ -1692,9 +1716,15 @@ class TestTenantScopedWebApi(BaseTestWeb):
},
'exp': time.time() + 3600}
client, request_id, token = self._init_autohold_delete(authz)
headers = {'Authorization': 'Bearer %s' % token}
headers.update(self.extra_headers)
resp = self.delete_url(
"api/tenant/tenant-one/autohold/%s" % request_id,
headers={'Authorization': 'Bearer %s' % token})
headers=headers)
# Expected failure if testing CORS with a bad origin
if self.extra_headers.get('Origin') == 'bad.zuul':
self.assertEqual(400, resp.status_code, resp.text)
return
self.assertEqual(204, resp.status_code, resp.text)
# autohold-list should be empty now
resp = self.get_url(
@ -1724,9 +1754,15 @@ class TestTenantScopedWebApi(BaseTestWeb):
'pipeline': 'gate', }
if use_trigger:
change['trigger'] = 'gerrit'
headers = {'Authorization': 'Bearer %s' % token}
headers.update(self.extra_headers)
req = self.post_url(path % enqueue_args,
headers={'Authorization': 'Bearer %s' % token},
headers=headers,
json=change)
# Expected failure if testing CORS with a bad origin
if self.extra_headers.get('Origin') == 'bad.zuul':
self.assertEqual(400, req.status_code, req.text)
return
# The JSON returned is the same as the client's output
self.assertEqual(200, req.status_code, req.text)
data = req.json()
@ -1769,9 +1805,14 @@ class TestTenantScopedWebApi(BaseTestWeb):
'exp': time.time() + 3600}
token = jwt.encode(authz, key='NoDanaOnlyZuul',
algorithm='HS256')
headers = {'Authorization': 'Bearer %s' % token}
headers.update(self.extra_headers)
req = self.post_url(path % enqueue_args,
headers={'Authorization': 'Bearer %s' % token},
headers=headers,
json=ref)
if self.extra_headers.get('Origin') == 'bad.zuul':
self.assertEqual(400, req.status_code, req.text)
return
self.assertEqual(200, req.status_code, req.text)
# The JSON returned is the same as the client's output
data = req.json()
@ -1813,14 +1854,19 @@ class TestTenantScopedWebApi(BaseTestWeb):
'exp': time.time() + 3600}
token = jwt.encode(authz, key='NoDanaOnlyZuul',
algorithm='HS256')
headers = {'Authorization': 'Bearer %s' % token}
headers.update(self.extra_headers)
path = "api/tenant/%(tenant)s/project/%(project)s/dequeue"
dequeue_args = {'tenant': 'tenant-one',
'project': 'org/project', }
change = {'ref': 'refs/heads/stable',
'pipeline': 'periodic', }
req = self.post_url(path % dequeue_args,
headers={'Authorization': 'Bearer %s' % token},
headers=headers,
json=change)
if self.extra_headers.get('Origin') == 'bad.zuul':
self.assertEqual(400, req.status_code, req.text)
return
# The JSON returned is the same as the client's output
self.assertEqual(200, req.status_code, req.text)
data = req.json()
@ -1841,6 +1887,8 @@ class TestTenantScopedWebApi(BaseTestWeb):
properly"""
# Note that %tenant, %project are not relevant here. The client is
# just checking what the endpoint allows.
web_config = self.config.get('web', {})
cors_enabled = web_config.get('enable_cors', False)
endpoints = [
{'action': 'promote',
'path': 'api/tenant/my-tenant/promote',
@ -1868,19 +1916,31 @@ class TestTenantScopedWebApi(BaseTestWeb):
'path': 'api/user/authorizations',
'allowed_methods': ['GET', ]},
]
headers = {'Access-Control-Request-Method': 'GET',
'Access-Control-Request-Headers': 'Authorization',
'Origin': 'test.zuul'}
headers.update(self.extra_headers)
for endpoint in endpoints:
preflight = self.options_url(
endpoint['path'],
headers={'Access-Control-Request-Method': 'GET',
'Access-Control-Request-Headers': 'Authorization'})
headers=headers)
if self.extra_headers.get('Origin') == 'bad.zuul':
self.assertEqual(400, preflight.status_code, preflight.text)
continue
self.assertEqual(
204,
preflight.status_code,
"%s failed: %s" % (endpoint['action'], preflight.text))
self.assertEqual(
'*',
preflight.headers.get('Access-Control-Allow-Origin'),
"%s failed: %s" % (endpoint['action'], preflight.headers))
if cors_enabled:
self.assertEqual(
self.extra_headers.get('Origin'),
preflight.headers.get('Access-Control-Allow-Origin'),
"%s failed: %s" % (endpoint['action'], preflight.headers))
else:
self.assertEqual(
'*',
preflight.headers.get('Access-Control-Allow-Origin'),
"%s failed: %s" % (endpoint['action'], preflight.headers))
self.assertEqual(
'Authorization, Content-Type',
preflight.headers.get('Access-Control-Allow-Headers'),
@ -1935,10 +1995,15 @@ class TestTenantScopedWebApi(BaseTestWeb):
'iat': time.time()}
token = jwt.encode(authz, key='NoDanaOnlyZuul',
algorithm='HS256')
headers = {'Authorization': 'Bearer %s' % token}
headers.update(self.extra_headers)
req = self.post_url(
'api/tenant/tenant-one/promote',
headers={'Authorization': 'Bearer %s' % token},
headers=headers,
json=args)
if self.extra_headers.get('Origin') == 'bad.zuul':
self.assertEqual(400, req.status_code, req.text)
return
self.assertEqual(200, req.status_code, req.text)
data = req.json()
self.assertEqual(True, data)
@ -1988,6 +2053,44 @@ class TestTenantScopedWebApi(BaseTestWeb):
self.assertEqual(C.reported, 2)
class TestTenantScopedWebAPICORSEnabledMultiOrigins(TestTenantScopedWebApi):
config_file = 'zuul-admin-web-CORS-multiple-origins.conf'
extra_headers = {'Origin': 'foo.zuul'}
class TestTenantScopedWebAPICORSEnabledDirectCalls(TestTenantScopedWebApi):
"""Test that non-CORS calls work with CORS enabled."""
config_file = 'zuul-admin-web-CORS-multiple-origins.conf'
def test_OPTIONS(self):
self.skipTest('this tests a CORS-only call')
class TestTenantScopedWebAPICORSEnabledBadOrigin(TestTenantScopedWebApi):
config_file = 'zuul-admin-web-CORS-multiple-origins.conf'
extra_headers = {'Origin': 'bad.zuul'}
def test_admin_routes_no_token(self):
self.skipTest('N/A')
def test_bad_key_JWT_token(self):
self.skipTest('N/A')
def test_expired_JWT_token(self):
self.skipTest('N/A')
def test_valid_JWT_bad_tenants(self):
self.skipTest('N/A')
def test_autohold_delete_wrong_tenant(self):
self.skipTest('N/A')
class TestTenantScopedWebAPICORSEnabledWildcard(TestTenantScopedWebApi):
config_file = 'zuul-admin-web-CORS-wildcard.conf'
extra_headers = {'Origin': 'nodanaonly.zuul'}
class TestTenantScopedWebApiWithAuthRules(BaseTestWeb):
config_file = 'zuul-admin-web-no-override.conf'
tenant_config_file = 'config/authorization/single-tenant/main.yaml'

View File

@ -419,6 +419,19 @@ class TestZuulClientAdmin(BaseTestWeb):
self.assertEqual(C.reported, 2)
class TestZuulClientAdminCORSEnabled(TestZuulClientAdmin):
"""Test the admin commands of zuul-client, CORS enabled"""
config_file = 'zuul-admin-web-CORS-multiple-origins.conf'
class TestZuulClientAdminCORSEnabledWildCard(TestZuulClientAdmin):
"""
Test the admin commands of zuul-client, CORS enabled with origin
wildcard
"""
config_file = 'zuul-admin-web-CORS-wildcard.conf'
class TestZuulClientQueryData(BaseTestWeb):
"""Test that zuul-client can fetch builds"""
config_file = 'zuul-sql-driver-mysql.conf'
@ -578,6 +591,11 @@ class TestZuulClientBuilds(TestZuulClientQueryData,
results)
class TestZuulClientBuildsCORSEnabled(TestZuulClientBuilds):
"""Test that zuul-client can fetch builds, CORS enabled"""
config_file = 'zuul-sql-driver-mysql-CORS-enabled.conf'
class TestZuulClientBuildInfo(TestZuulClientQueryData,
AnsibleZuulTestCase):
"""Test that zuul-client can fetch a build's details"""
@ -631,3 +649,8 @@ class TestZuulClientBuildInfo(TestZuulClientQueryData,
x['url'] == 'http://example.com/docs'
for x in artifacts),
output)
class TestZuulClientBuildInfoCORSEnabled(TestZuulClientBuildInfo):
"""Test that zuul-client can fetch builds, CORS enabled"""
config_file = 'zuul-sql-driver-mysql-CORS-enabled.conf'

View File

@ -72,8 +72,58 @@ class SaveParamsTool(cherrypy.Tool):
cherrypy.tools.save_params = SaveParamsTool()
def handle_options(allowed_methods=None):
if cherrypy.request.method == 'OPTIONS':
class CORSTool(cherrypy.Tool):
"""
Handle CORS headers and preflight exchanges.
"""
def __init__(self, CORS_enabled=None, allowed_origins=None):
cherrypy.Tool.__init__(self, 'on_start_resource',
self.handle_CORS)
self.CORS_enabled = CORS_enabled
self.allowed_origins = allowed_origins
def handle_CORS(self, allowed_methods=None):
if cherrypy.request.method == 'OPTIONS':
self.handle_OPTIONS(allowed_methods)
resp = cherrypy.response
origin = cherrypy.request.headers.get('Origin', None)
# CORS queries occur within specific conditions:
# https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS and
# https://fetch.spec.whatwg.org/#cors-safelisted-request-header
# For simplicity's sake we will just enforce CORS if the
# "Origin" header is present in the request. This way calls
# to the API issued without a browser (zuul-client for example)
# won't run through this check.
if self.CORS_enabled and origin is not None:
if origin in self.allowed_origins:
resp.headers['Access-Control-Allow-Origin'] = origin
if len(self.allowed_origins) > 1:
resp.headers['Vary'] = 'Origin'
elif '*' in self.allowed_origins:
resp.headers['Access-Control-Allow-Origin'] = '*'
else:
# Pick arbitrarily the first allowed origin
resp.headers['Access-Control-Allow-Origin'] =\
self.allowed_origins[0]
if len(self.allowed_origins) > 1:
resp.headers['Vary'] = 'Origin'
error_message = ('Cross-Origin Request blocked: '
'access not allowed '
'from origin "%s"' % origin)
raise cherrypy.HTTPError(
400,
error_message)
else:
# Be polite to the client if it sent a CORS header.
if origin is not None:
resp.headers['Access-Control-Allow-Origin'] = '*'
def handle_OPTIONS(self, allowed_methods):
"""Specific logic for handling CORS preflight"""
methods = allowed_methods or ['GET', 'OPTIONS']
if allowed_methods and 'OPTIONS' not in allowed_methods:
methods = methods + ['OPTIONS']
@ -82,7 +132,6 @@ def handle_options(allowed_methods=None):
request.handler = None
# Set CORS response headers
resp = cherrypy.response
resp.headers['Access-Control-Allow-Origin'] = '*'
resp.headers['Access-Control-Allow-Headers'] =\
', '.join(['Authorization', 'Content-Type'])
resp.headers['Access-Control-Allow-Methods'] =\
@ -92,8 +141,8 @@ def handle_options(allowed_methods=None):
resp.status = 204
cherrypy.tools.handle_options = cherrypy.Tool('on_start_resource',
handle_options)
# this will be overridden at ZuulWeb instantiation
cherrypy.tools.handle_CORS = CORSTool()
class ChangeFilter(object):
@ -284,7 +333,7 @@ class ZuulWebAPI(object):
@cherrypy.expose
@cherrypy.tools.json_in()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
@cherrypy.tools.handle_options(allowed_methods=['POST', ])
@cherrypy.tools.handle_CORS(allowed_methods=['POST', ])
def dequeue(self, tenant, project):
basic_error = self._basic_auth_header_check()
if basic_error is not None:
@ -312,8 +361,6 @@ class ZuulWebAPI(object):
'change': body.get('change', None),
'ref': body.get('ref', None)})
result = not job.failure
resp = cherrypy.response
resp.headers['Access-Control-Allow-Origin'] = '*'
return result
else:
raise cherrypy.HTTPError(400,
@ -322,7 +369,7 @@ class ZuulWebAPI(object):
@cherrypy.expose
@cherrypy.tools.json_in()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
@cherrypy.tools.handle_options(allowed_methods=['POST', ])
@cherrypy.tools.handle_CORS(allowed_methods=['POST', ])
def enqueue(self, tenant, project):
basic_error = self._basic_auth_header_check()
if basic_error is not None:
@ -356,8 +403,6 @@ class ZuulWebAPI(object):
'project': project,
'change': change, })
result = not job.failure
resp = cherrypy.response
resp.headers['Access-Control-Allow-Origin'] = '*'
return result
def _enqueue_ref(self, tenant, project, ref,
@ -370,14 +415,12 @@ class ZuulWebAPI(object):
'oldrev': oldrev,
'newrev': newrev, })
result = not job.failure
resp = cherrypy.response
resp.headers['Access-Control-Allow-Origin'] = '*'
return result
@cherrypy.expose
@cherrypy.tools.json_in()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
@cherrypy.tools.handle_options(allowed_methods=['POST', ])
@cherrypy.tools.handle_CORS(allowed_methods=['POST', ])
def promote(self, tenant):
basic_error = self._basic_auth_header_check()
if basic_error is not None:
@ -406,12 +449,11 @@ class ZuulWebAPI(object):
'change_ids': changes,
})
result = not job.failure
resp = cherrypy.response
resp.headers['Access-Control-Allow-Origin'] = '*'
return result
@cherrypy.expose
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
@cherrypy.tools.handle_CORS(allowed_methods=['GET', ])
def autohold_list(self, tenant, *args, **kwargs):
# we don't use json_in because a payload is not mandatory with GET
if cherrypy.request.method != 'GET':
@ -422,7 +464,7 @@ class ZuulWebAPI(object):
@cherrypy.expose
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
@cherrypy.tools.handle_options(allowed_methods=['GET', 'POST', ])
@cherrypy.tools.handle_CORS(allowed_methods=['GET', 'POST', ])
def autohold(self, tenant, project=None):
# we don't use json_in because a payload is not mandatory with GET
# Note: GET handling is redundant with autohold_list
@ -499,13 +541,11 @@ class ZuulWebAPI(object):
'expired': request['expired'],
'nodes': request['nodes']
})
resp = cherrypy.response
resp.headers['Access-Control-Allow-Origin'] = '*'
return result
@cherrypy.expose
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
@cherrypy.tools.handle_options(allowed_methods=['GET', 'DELETE', ])
@cherrypy.tools.handle_CORS(allowed_methods=['GET', 'DELETE', ])
def autohold_by_request_id(self, tenant, request_id):
if cherrypy.request.method == 'GET':
return self._autohold_info(tenant, request_id)
@ -528,8 +568,6 @@ class ZuulWebAPI(object):
# return 404 rather than 403 to avoid leaking tenant info
raise cherrypy.HTTPError(
404, 'Hold request %s not found.' % request_id)
resp = cherrypy.response
resp.headers['Access-Control-Allow-Origin'] = '*'
return {
'id': request['id'],
'tenant': request['tenant'],
@ -626,9 +664,10 @@ class ZuulWebAPI(object):
return self._handleInfo(info)
def _handleInfo(self, info):
ret = {'info': info.toDict()}
ret = {
'info': info.toDict(),
}
resp = cherrypy.response
resp.headers['Access-Control-Allow-Origin'] = '*'
if self.static_cache_expiry:
resp.headers['Cache-Control'] = "public, max-age=%d" % \
self.static_cache_expiry
@ -653,7 +692,7 @@ class ZuulWebAPI(object):
# TODO(mhu) deprecated, remove next version
@cherrypy.expose
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
@cherrypy.tools.handle_options(allowed_methods=['GET', ])
@cherrypy.tools.handle_CORS(allowed_methods=['GET', ])
def authorizations(self):
basic_error = self._basic_auth_header_check()
if basic_error is not None:
@ -675,7 +714,7 @@ class ZuulWebAPI(object):
@cherrypy.expose
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
@cherrypy.tools.handle_options(allowed_methods=['GET', ])
@cherrypy.tools.handle_CORS(allowed_methods=['GET', ])
def tenant_authorizations(self, tenant):
basic_error = self._basic_auth_header_check()
if basic_error is not None:
@ -714,19 +753,17 @@ class ZuulWebAPI(object):
@cherrypy.expose
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
@cherrypy.tools.handle_CORS(allowed_methods=['GET', ])
def tenants(self):
ret = self._tenants()
resp = cherrypy.response
resp.headers['Access-Control-Allow-Origin'] = '*'
return ret
@cherrypy.expose
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
@cherrypy.tools.handle_CORS(allowed_methods=['GET', ])
def connections(self):
job = self.rpc.submitJob('zuul:connection_list', {})
ret = json.loads(job.data[0])
resp = cherrypy.response
resp.headers['Access-Control-Allow-Origin'] = '*'
return ret
def _getStatus(self, tenant):
@ -746,18 +783,19 @@ class ZuulWebAPI(object):
last_modified = datetime.utcfromtimestamp(self.cache_time[tenant])
last_modified_header = last_modified.strftime('%a, %d %b %Y %X GMT')
resp.headers["Last-modified"] = last_modified_header
resp.headers['Access-Control-Allow-Origin'] = '*'
return payload
@cherrypy.expose
@cherrypy.tools.save_params()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
@cherrypy.tools.handle_CORS(allowed_methods=['GET', ])
def status(self, tenant):
return self._getStatus(tenant)
@cherrypy.expose
@cherrypy.tools.save_params()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
@cherrypy.tools.handle_CORS(allowed_methods=['GET', ])
def status_change(self, tenant, change):
payload = self._getStatus(tenant)
result_filter = ChangeFilter(change)
@ -766,56 +804,53 @@ class ZuulWebAPI(object):
@cherrypy.expose
@cherrypy.tools.save_params()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
@cherrypy.tools.handle_CORS(allowed_methods=['GET', ])
def jobs(self, tenant):
job = self.rpc.submitJob('zuul:job_list', {'tenant': tenant})
ret = json.loads(job.data[0])
if ret is None:
raise cherrypy.HTTPError(404, 'Tenant %s does not exist.' % tenant)
resp = cherrypy.response
resp.headers['Access-Control-Allow-Origin'] = '*'
return ret
@cherrypy.expose
@cherrypy.tools.save_params()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
@cherrypy.tools.handle_CORS(allowed_methods=['GET', ])
def config_errors(self, tenant):
config_errors = self.rpc.submitJob(
'zuul:config_errors_list', {'tenant': tenant})
ret = json.loads(config_errors.data[0])
if ret is None:
raise cherrypy.HTTPError(404, 'Tenant %s does not exist.' % tenant)
resp = cherrypy.response
resp.headers['Access-Control-Allow-Origin'] = '*'
return ret
@cherrypy.expose
@cherrypy.tools.save_params()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
@cherrypy.tools.handle_CORS(allowed_methods=['GET', ])
def job(self, tenant, job_name):
job = self.rpc.submitJob(
'zuul:job_get', {'tenant': tenant, 'job': job_name})
ret = json.loads(job.data[0])
if not ret:
raise cherrypy.HTTPError(404, 'Job %s does not exist.' % job_name)
resp = cherrypy.response
resp.headers['Access-Control-Allow-Origin'] = '*'
return ret
@cherrypy.expose
@cherrypy.tools.save_params()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
@cherrypy.tools.handle_CORS(allowed_methods=['GET', ])
def projects(self, tenant):
job = self.rpc.submitJob('zuul:project_list', {'tenant': tenant})
ret = json.loads(job.data[0])
if ret is None:
raise cherrypy.HTTPError(404, 'Tenant %s does not exist.' % tenant)
resp = cherrypy.response
resp.headers['Access-Control-Allow-Origin'] = '*'
return ret
@cherrypy.expose
@cherrypy.tools.save_params()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
@cherrypy.tools.handle_CORS(allowed_methods=['GET', ])
def project(self, tenant, project):
job = self.rpc.submitJob(
'zuul:project_get', {'tenant': tenant, 'project': project})
@ -825,25 +860,23 @@ class ZuulWebAPI(object):
if not ret:
raise cherrypy.HTTPError(
404, 'Project %s does not exist.' % project)
resp = cherrypy.response
resp.headers['Access-Control-Allow-Origin'] = '*'
return ret
@cherrypy.expose
@cherrypy.tools.save_params()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
@cherrypy.tools.handle_CORS(allowed_methods=['GET', ])
def pipelines(self, tenant):
job = self.rpc.submitJob('zuul:pipeline_list', {'tenant': tenant})
ret = json.loads(job.data[0])
if ret is None:
raise cherrypy.HTTPError(404, 'Tenant %s does not exist.' % tenant)
resp = cherrypy.response
resp.headers['Access-Control-Allow-Origin'] = '*'
return ret
@cherrypy.expose
@cherrypy.tools.save_params()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
@cherrypy.tools.handle_CORS(allowed_methods=['GET', ])
def labels(self, tenant):
job = self.rpc.submitJob('zuul:allowed_labels_get', {'tenant': tenant})
data = json.loads(job.data[0])
@ -863,13 +896,12 @@ class ZuulWebAPI(object):
launcher.supported_labels,
allowed_labels, disallowed_labels))
ret = [{'name': label} for label in sorted(labels)]
resp = cherrypy.response
resp.headers['Access-Control-Allow-Origin'] = '*'
return ret
@cherrypy.expose
@cherrypy.tools.save_params()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
@cherrypy.tools.handle_CORS(allowed_methods=['GET', ])
def nodes(self, tenant):
ret = []
for node in self.zk_nodepool.nodeIterator():
@ -878,12 +910,11 @@ class ZuulWebAPI(object):
"provider", "state", "state_time", "comment"):
node_data[key] = node.get(key)
ret.append(node_data)
resp = cherrypy.response
resp.headers['Access-Control-Allow-Origin'] = '*'
return ret
@cherrypy.expose
@cherrypy.tools.save_params()
@cherrypy.tools.handle_CORS(allowed_methods=['GET', ])
def key(self, tenant, project):
job = self.rpc.submitJob('zuul:key_get', {'tenant': tenant,
'project': project,
@ -892,12 +923,12 @@ class ZuulWebAPI(object):
raise cherrypy.HTTPError(
404, 'Project %s does not exist.' % project)
resp = cherrypy.response
resp.headers['Access-Control-Allow-Origin'] = '*'
resp.headers['Content-Type'] = 'text/plain'
return job.data[0]
@cherrypy.expose
@cherrypy.tools.save_params()
@cherrypy.tools.handle_CORS(allowed_methods=['GET', ])
def project_ssh_key(self, tenant, project):
job = self.rpc.submitJob('zuul:key_get', {'tenant': tenant,
'project': project,
@ -906,7 +937,6 @@ class ZuulWebAPI(object):
raise cherrypy.HTTPError(
404, 'Project %s does not exist.' % project)
resp = cherrypy.response
resp.headers['Access-Control-Allow-Origin'] = '*'
resp.headers['Content-Type'] = 'text/plain'
return job.data[0] + '\n'
@ -978,6 +1008,7 @@ class ZuulWebAPI(object):
@cherrypy.expose
@cherrypy.tools.save_params()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
@cherrypy.tools.handle_CORS(allowed_methods=['GET', ])
def builds(self, tenant, project=None, pipeline=None, change=None,
branch=None, patchset=None, ref=None, newrev=None,
uuid=None, job_name=None, voting=None, nodeset=None,
@ -1002,13 +1033,12 @@ class ZuulWebAPI(object):
result=result, final=final, held=held, complete=complete,
limit=limit, offset=skip)
resp = cherrypy.response
resp.headers['Access-Control-Allow-Origin'] = '*'
return [self.buildToDict(b, b.buildset) for b in builds]
@cherrypy.expose
@cherrypy.tools.save_params()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
@cherrypy.tools.handle_CORS(allowed_methods=['GET', ])
def build(self, tenant, uuid):
connection = self._get_connection()
@ -1016,8 +1046,6 @@ class ZuulWebAPI(object):
if not data:
raise cherrypy.HTTPError(404, "Build not found")
data = self.buildToDict(data[0], data[0].buildset)
resp = cherrypy.response
resp.headers['Access-Control-Allow-Origin'] = '*'
return data
def buildsetToDict(self, buildset, builds=[]):
@ -1067,6 +1095,7 @@ class ZuulWebAPI(object):
@cherrypy.expose
@cherrypy.tools.save_params()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
@cherrypy.tools.handle_CORS(allowed_methods=['GET', ])
def buildsets(self, tenant, project=None, pipeline=None, change=None,
branch=None, patchset=None, ref=None, newrev=None,
uuid=None, result=None, complete=None, limit=50, skip=0):
@ -1081,13 +1110,12 @@ class ZuulWebAPI(object):
uuid=uuid, result=result, complete=complete,
limit=limit, offset=skip)
resp = cherrypy.response
resp.headers['Access-Control-Allow-Origin'] = '*'
return [self.buildsetToDict(b) for b in buildsets]
@cherrypy.expose
@cherrypy.tools.save_params()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
@cherrypy.tools.handle_CORS(allowed_methods=['GET', ])
def buildset(self, tenant, uuid):
connection = self._get_connection()
@ -1095,8 +1123,6 @@ class ZuulWebAPI(object):
if not data:
raise cherrypy.HTTPError(404, "Buildset not found")
data = self.buildsetToDict(data, data.builds)
resp = cherrypy.response
resp.headers['Access-Control-Allow-Origin'] = '*'
return data
@cherrypy.expose
@ -1108,6 +1134,7 @@ class ZuulWebAPI(object):
@cherrypy.expose
@cherrypy.tools.save_params()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
@cherrypy.tools.handle_CORS(allowed_methods=['GET', ])
def project_freeze_jobs(self, tenant, pipeline, project, branch):
job = self.rpc.submitJob(
'zuul:project_freeze_jobs',
@ -1121,13 +1148,12 @@ class ZuulWebAPI(object):
ret = json.loads(job.data[0])
if not ret:
raise cherrypy.HTTPError(404)
resp = cherrypy.response
resp.headers['Access-Control-Allow-Origin'] = '*'
return ret
@cherrypy.expose
@cherrypy.tools.save_params()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
@cherrypy.tools.handle_CORS(allowed_methods=['GET', ])
def project_freeze_job(self, tenant, pipeline, project, branch, job):
# TODO(jhesketh): Allow a canonical change/item to be passed in which
# would return the job with any in-change modifications.
@ -1262,6 +1288,14 @@ class ZuulWeb(object):
ssl_key = get_default(self.config, 'gearman', 'ssl_key')
ssl_cert = get_default(self.config, 'gearman', 'ssl_cert')
ssl_ca = get_default(self.config, 'gearman', 'ssl_ca')
enable_cors = get_default(self.config, 'web', 'enable_cors',
False)
allowed_origins = get_default(
self.config, 'web',
'allowed_origins', "localhost").split(',')
self.CORS_config = {'CORS_enabled': enable_cors,
'allowed_origins': allowed_origins}
cherrypy.tools.handle_CORS = CORSTool(**self.CORS_config)
# instanciate handlers
self.rpc = zuul.rpcclient.RPCClient(gear_server, gear_port,
@ -1399,6 +1433,7 @@ class ZuulWeb(object):
'request.dispatch': route_map
}
}
cherrypy.config.update({
'global': {
'environment': 'production',