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
This commit is contained in:
parent
c2f2891bd3
commit
c22f2c98e0
|
@ -2,26 +2,32 @@
|
|||
|
||||
.. _authentication:
|
||||
|
||||
Authenticated Actions
|
||||
=====================
|
||||
Authenticated Access
|
||||
====================
|
||||
|
||||
Users can perform some privileged actions at the tenant level through protected
|
||||
endpoints of the REST API, if these endpoints are activated.
|
||||
Access to Zuul's REST API and web interface can optionally be
|
||||
restricted. By default, anonymous read access to any tenant is
|
||||
permitted. Optionally, some administrative actions may also be
|
||||
enabled and restricted to authorized users. Additionally, individual
|
||||
tenants or the entire system may have read-level access restricted
|
||||
to authorized users.
|
||||
|
||||
The supported actions are **autohold**, **enqueue/enqueue-ref**,
|
||||
**dequeue/dequeue-ref** and **promote**. These are similar to the ones available
|
||||
through Zuul's CLI.
|
||||
The supported administrative actions are **autohold**,
|
||||
**enqueue/enqueue-ref**, **dequeue/dequeue-ref** and
|
||||
**promote**. These are similar to the ones available through
|
||||
Zuul's CLI.
|
||||
|
||||
The protected endpoints require a bearer token, passed to Zuul Web Server as the
|
||||
**Authorization** header of the request. The token and this workflow follow the
|
||||
JWT standard as established in this `RFC <https://tools.ietf.org/html/rfc7519>`_.
|
||||
The protected endpoints require a bearer token, passed to Zuul Web
|
||||
Server as the **Authorization** header of the request. The token and
|
||||
this workflow follow the JWT standard as established in this `RFC
|
||||
<https://tools.ietf.org/html/rfc7519>`_.
|
||||
|
||||
Important Security Considerations
|
||||
---------------------------------
|
||||
|
||||
Anybody with a valid token can perform privileged actions exposed
|
||||
through the REST API. Furthermore revoking tokens, especially when manually
|
||||
issued, is not trivial.
|
||||
Anybody with a valid administrative token can perform privileged
|
||||
actions exposed through the REST API. Furthermore revoking tokens,
|
||||
especially when manually issued, is not trivial.
|
||||
|
||||
As a mitigation, tokens should be generated with a short time to
|
||||
live, like 10 minutes or less. If the token contains authorization Information
|
||||
|
@ -38,10 +44,12 @@ and tokens should be handed over with discernment.
|
|||
Configuration
|
||||
-------------
|
||||
|
||||
.. important:: In order to use admin commands in the zuul command line interface, at least one HS256 authenticator should be configured.
|
||||
.. important:: In order to use restricted commands in the zuul command
|
||||
line interface, at least one HS256 authenticator should
|
||||
be configured.
|
||||
|
||||
To enable tenant-scoped access to privileged actions, see the Zuul Web Server
|
||||
component's section.
|
||||
To enable tenant-scoped access to privileged actions or restrict
|
||||
read-level access, see the Zuul Web Server component's section.
|
||||
|
||||
To set access rules for a tenant, see :ref:`the documentation about tenant
|
||||
definition <authz_rule_definition>`.
|
||||
|
|
|
@ -403,7 +403,23 @@ configuration. Some examples of tenant definitions are:
|
|||
web interface.
|
||||
|
||||
At least one rule in the list must match for the user to be allowed to
|
||||
execute privileged actions.
|
||||
execute privileged actions. A matching rule will also allow the user
|
||||
access to the tenant in general (i.e., the rule does not need to be
|
||||
duplicated in `access-rules`).
|
||||
|
||||
More information on tenant-scoped actions can be found in
|
||||
:ref:`authentication`.
|
||||
|
||||
.. attr:: access-rules
|
||||
|
||||
A list of authorization rules to be checked in order to grant
|
||||
read access to the tenant through Zuul's REST API and web
|
||||
interface.
|
||||
|
||||
If no rules are listed, then anonymous access to the tenant is
|
||||
permitted. If any rules are present then at least one rule in
|
||||
the list must match for the user to be allowed to access the
|
||||
tenant.
|
||||
|
||||
More information on tenant-scoped actions can be found in
|
||||
:ref:`authentication`.
|
||||
|
@ -693,3 +709,17 @@ API root access is not a pre-requisite to access tenant-specific URLs.
|
|||
token manually. If this is an issue, it is advised to add
|
||||
finer filtering to admin rules, for example, filtering by the
|
||||
``iss`` claim (generally equal to the issuer ID).
|
||||
|
||||
.. attr:: access-rules
|
||||
|
||||
A list of authorization rules to be checked in order to grant
|
||||
read access to the top-level (i.e., non-tenant-specific) portion
|
||||
of Zuul's REST API and web interface.
|
||||
|
||||
If no rules are listed, then anonymous access to top-level
|
||||
methods is permitted. If any rules are present then at at least
|
||||
one rule in the list must match for the user to be allowed
|
||||
access.
|
||||
|
||||
More information on tenant-scoped actions can be found in
|
||||
:ref:`authentication`.
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
features:
|
||||
- |
|
||||
Read-level access to tenants or the tenant list may now be
|
||||
restricted to authorized users using the
|
||||
:attr:`tenant.access-rules` and :attr:`api-root.access-rules`
|
||||
attributes.
|
|
@ -0,0 +1 @@
|
|||
---
|
|
@ -0,0 +1,61 @@
|
|||
- pipeline:
|
||||
name: check
|
||||
manager: independent
|
||||
trigger:
|
||||
gerrit:
|
||||
- event: patchset-created
|
||||
- event: comment-added
|
||||
comment: '^(Patch Set [0-9]+:\n\n)?(?i:recheck)$'
|
||||
success:
|
||||
gerrit:
|
||||
Verified: 1
|
||||
failure:
|
||||
gerrit:
|
||||
Verified: -1
|
||||
|
||||
- pipeline:
|
||||
name: gate
|
||||
manager: dependent
|
||||
success-message: Build succeeded (gate).
|
||||
trigger:
|
||||
gerrit:
|
||||
- event: comment-added
|
||||
approval:
|
||||
- Approved: 1
|
||||
success:
|
||||
gerrit:
|
||||
Verified: 2
|
||||
submit: true
|
||||
failure:
|
||||
gerrit:
|
||||
Verified: -2
|
||||
start:
|
||||
gerrit:
|
||||
Verified: 0
|
||||
precedence: high
|
||||
|
||||
- pipeline:
|
||||
name: post
|
||||
manager: independent
|
||||
trigger:
|
||||
gerrit:
|
||||
- event: ref-updated
|
||||
ref: ^(?!refs/).*$
|
||||
precedence: low
|
||||
|
||||
- job:
|
||||
name: base
|
||||
parent: null
|
||||
run: playbooks/run.yaml
|
||||
|
||||
- job:
|
||||
name: testjob
|
||||
|
||||
- project:
|
||||
name: org/project
|
||||
check:
|
||||
jobs:
|
||||
- testjob
|
||||
gate:
|
||||
jobs:
|
||||
- testjob
|
|
@ -0,0 +1 @@
|
|||
test
|
|
@ -0,0 +1,17 @@
|
|||
- authorization-rule:
|
||||
name: user-rule
|
||||
conditions:
|
||||
- groups: users
|
||||
|
||||
- api-root:
|
||||
access-rules: user-rule
|
||||
|
||||
- tenant:
|
||||
name: tenant-one
|
||||
access-rules: user-rule
|
||||
source:
|
||||
gerrit:
|
||||
config-projects:
|
||||
- common-config
|
||||
untrusted-projects:
|
||||
- org/project
|
|
@ -1581,7 +1581,8 @@ class TestInfo(BaseTestWeb):
|
|||
"job_history": True,
|
||||
"auth": {
|
||||
"realms": {},
|
||||
"default_realm": None
|
||||
"default_realm": None,
|
||||
"read_protected": False,
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
|
@ -1637,7 +1638,8 @@ class TestWebCapabilitiesInfo(TestInfo):
|
|||
'driver': 'HS256',
|
||||
}
|
||||
},
|
||||
'default_realm': 'myOIDC1'
|
||||
'default_realm': 'myOIDC1',
|
||||
'read_protected': False,
|
||||
}
|
||||
return info
|
||||
|
||||
|
@ -3538,3 +3540,84 @@ class TestWebUnprotectedBranches(BaseWithWeb):
|
|||
config_errors = self.get_url(
|
||||
"api/tenant/tenant-one/config-errors").json()
|
||||
self.assertEqual(len(config_errors), 0)
|
||||
|
||||
|
||||
class TestWebApiAccessRules(BaseTestWeb):
|
||||
# Test read-level access restrictions
|
||||
config_file = 'zuul-admin-web.conf'
|
||||
tenant_config_file = 'config/access-rules/main.yaml'
|
||||
|
||||
routes = [
|
||||
'/api/connections',
|
||||
'/api/components',
|
||||
'/api/tenants',
|
||||
'/api/tenant/{tenant}/status',
|
||||
'/api/tenant/{tenant}/status/change/{change}',
|
||||
'/api/tenant/{tenant}/jobs',
|
||||
'/api/tenant/{tenant}/job/{job_name}',
|
||||
'/api/tenant/{tenant}/projects',
|
||||
'/api/tenant/{tenant}/project/{project}',
|
||||
('/api/tenant/{tenant}/pipeline/{pipeline}/'
|
||||
'project/{project}/branch/{branch}/freeze-jobs'),
|
||||
'/api/tenant/{tenant}/pipelines',
|
||||
'/api/tenant/{tenant}/semaphores',
|
||||
'/api/tenant/{tenant}/labels',
|
||||
'/api/tenant/{tenant}/nodes',
|
||||
'/api/tenant/{tenant}/key/{project}.pub',
|
||||
'/api/tenant/{tenant}/project-ssh-key/{project}.pub',
|
||||
'/api/tenant/{tenant}/console-stream',
|
||||
'/api/tenant/{tenant}/badge',
|
||||
'/api/tenant/{tenant}/builds',
|
||||
'/api/tenant/{tenant}/build/{uuid}',
|
||||
'/api/tenant/{tenant}/buildsets',
|
||||
'/api/tenant/{tenant}/buildset/{uuid}',
|
||||
'/api/tenant/{tenant}/config-errors',
|
||||
'/api/tenant/{tenant}/authorizations',
|
||||
'/api/tenant/{tenant}/project/{project}/autohold',
|
||||
'/api/tenant/{tenant}/autohold',
|
||||
'/api/tenant/{tenant}/autohold/{request_id}',
|
||||
'/api/tenant/{tenant}/autohold/{request_id}',
|
||||
'/api/tenant/{tenant}/project/{project}/enqueue',
|
||||
'/api/tenant/{tenant}/project/{project}/dequeue',
|
||||
'/api/tenant/{tenant}/promote',
|
||||
]
|
||||
|
||||
info_routes = [
|
||||
'/api/info',
|
||||
'/api/tenant/{tenant}/info',
|
||||
]
|
||||
|
||||
def test_read_routes_no_token(self):
|
||||
for route in self.routes:
|
||||
url = route.format(tenant='tenant-one',
|
||||
project='org/project',
|
||||
change='1,1',
|
||||
job_name='testjob',
|
||||
pipeline='check',
|
||||
branch='master',
|
||||
uuid='1',
|
||||
request_id='1')
|
||||
resp = self.get_url(url)
|
||||
self.assertEqual(
|
||||
401,
|
||||
resp.status_code,
|
||||
"get %s failed: %s" % (url, resp.text))
|
||||
|
||||
def test_read_info_routes_no_token(self):
|
||||
for route in self.info_routes:
|
||||
url = route.format(tenant='tenant-one',
|
||||
project='org/project',
|
||||
change='1,1',
|
||||
job_name='testjob',
|
||||
pipeline='check',
|
||||
branch='master',
|
||||
uuid='1',
|
||||
request_id='1')
|
||||
resp = self.get_url(url)
|
||||
self.assertEqual(
|
||||
200,
|
||||
resp.status_code,
|
||||
"get %s failed: %s" % (url, resp.text))
|
||||
info = resp.json()
|
||||
self.assertTrue(
|
||||
info['info']['capabilities']['auth']['read_protected'])
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
||||
request.params['auth'] = AuthInfo(claims['__zuul_uid_claim'],
|
||||
admin)
|
||||
return _check_auth(**kw, tenant=tenant)
|
||||
|
||||
|
||||
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}
|
||||
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):
|
||||
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)
|
||||
|
@ -1503,19 +1635,30 @@ class ZuulWebAPI(object):
|
|||
resp.headers['Access-Control-Allow-Origin'] = '*'
|
||||
return result
|
||||
|
||||
@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',
|
||||
|
|
Loading…
Reference in New Issue