Merge "Zuul Web: add /api/user/authorizations endpoint"

This commit is contained in:
Zuul 2019-11-21 19:50:03 +00:00 committed by Gerrit Code Review
commit 03290d8b2a
4 changed files with 136 additions and 17 deletions

View File

@ -16,7 +16,7 @@
- admin-rule:
name: car_rule
conditions:
- car: ecto-1
- vehicle.car: ecto-1
- tenant:
name: tenant-one
admin-rules:

View File

@ -1629,8 +1629,8 @@ class TestTenantScopedWebApiWithAuthRules(BaseTestWeb):
self.assertEqual(200, req.status_code, req.text)
self.waitUntilSettled()
def test_arbitrary_claim_rule(self):
"""Test a rule based on a specific claim"""
def test_depth_claim_rule(self):
"""Test a rule based on a complex claim"""
A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
A.addApproval('Code-Review', 2)
A.addApproval('Approved', 1)
@ -1638,7 +1638,8 @@ class TestTenantScopedWebApiWithAuthRules(BaseTestWeb):
authz = {'iss': 'zuul_operator',
'aud': 'zuul.example.com',
'sub': 'zeddemore',
'car': 'ecto-1',
'vehicle': {
'car': 'ecto-1'},
'exp': time.time() + 3600}
token = jwt.encode(authz, key='NoDanaOnlyZuul',
algorithm='HS256').decode('utf-8')
@ -1654,6 +1655,66 @@ class TestTenantScopedWebApiWithAuthRules(BaseTestWeb):
self.assertEqual(200, req.status_code, req.text)
self.waitUntilSettled()
def test_user_actions_action_override(self):
"""Test that user with 'zuul.admin' claim does NOT get it back"""
admin_tenants = ['tenant-zero', ]
authz = {'iss': 'zuul_operator',
'aud': 'zuul.example.com',
'sub': 'testuser',
'zuul': {'admin': admin_tenants},
'exp': time.time() + 3600}
token = jwt.encode(authz, key='NoDanaOnlyZuul',
algorithm='HS256').decode('utf-8')
req = self.get_url('/api/user/authorizations',
headers={'Authorization': 'Bearer %s' % token})
self.assertEqual(401, req.status_code, req.text)
def test_user_actions(self):
"""Test that users get the right 'zuul.actions' trees"""
users = [
{'authz': {'iss': 'zuul_operator',
'aud': 'zuul.example.com',
'sub': 'vigo'},
'zuul.admin': []},
{'authz': {'iss': 'zuul_operator',
'aud': 'zuul.example.com',
'sub': 'venkman'},
'zuul.admin': ['tenant-one', ]},
{'authz': {'iss': 'zuul_operator',
'aud': 'zuul.example.com',
'sub': 'stantz'},
'zuul.admin': []},
{'authz': {'iss': 'zuul_operator',
'aud': 'zuul.example.com',
'sub': 'zeddemore',
'vehicle': {
'car': 'ecto-1'
}},
'zuul.admin': ['tenant-one', ]},
{'authz': {'iss': 'zuul_operator',
'aud': 'zuul.example.com',
'sub': 'melnitz',
'groups': ['secretary', 'ghostbusters']},
'zuul.admin': ['tenant-one', ]},
]
for test_user in users:
authz = test_user['authz']
authz['exp'] = time.time() + 3600
token = jwt.encode(authz, key='NoDanaOnlyZuul',
algorithm='HS256').decode('utf-8')
req = self.get_url('/api/user/authorizations',
headers={'Authorization': 'Bearer %s' % token})
self.assertEqual(200, req.status_code, req.text)
data = req.json()
self.assertTrue('zuul' in data,
"%s got %s" % (authz['sub'], data))
self.assertTrue('admin' in data['zuul'],
"%s got %s" % (authz['sub'], data))
self.assertEqual(test_user['zuul.admin'],
data['zuul']['admin'],
"%s got %s" % (authz['sub'], data))
class TestWebMulti(BaseTestWeb):
config_file = 'zuul-gerrit-github.conf'

View File

@ -42,6 +42,7 @@ class RPCListener(object):
'enqueue',
'enqueue_ref',
'promote',
'get_admin_tenants',
'get_running_jobs',
'get_job_log_stream_address',
'tenant_list',
@ -297,11 +298,7 @@ class RPCListener(object):
job_log_stream_address['port'] = build.worker.log_port
job.sendWorkComplete(json.dumps(job_log_stream_address))
def handle_authorize_user(self, job):
args = json.loads(job.arguments)
tenant_name = args['tenant']
claims = args['claims']
tenant = self.sched.abide.tenants.get(tenant_name)
def _is_authorized(self, tenant, claims):
authorized = False
if tenant:
rules = tenant.authorization_rules
@ -309,17 +306,39 @@ class RPCListener(object):
if rule not in self.sched.abide.admin_rules.keys():
self.log.error('Undefined rule "%s"' % rule)
continue
debug_msg = 'Applying rule "%s" from tenant "%s" to claims %s'
debug_msg = ('Applying rule "%s" from tenant '
'"%s" to claims %s')
self.log.debug(
debug_msg % (rule, tenant, json.dumps(claims)))
authorized = self.sched.abide.admin_rules[rule](claims)
if authorized:
debug_msg = '%s authorized on tenant "%s" by rule "%s"'
self.log.debug(
debug_msg % (json.dumps(claims), tenant, rule))
if '__zuul_uid_claim' in claims:
uid = claims['__zuul_uid_claim']
else:
uid = json.dumps(claims)
msg = '%s authorized on tenant "%s" by rule "%s"'
self.log.info(
msg % (uid, tenant, rule))
break
return authorized
def handle_authorize_user(self, job):
args = json.loads(job.arguments)
tenant_name = args['tenant']
claims = args['claims']
tenant = self.sched.abide.tenants.get(tenant_name)
authorized = self._is_authorized(tenant, claims)
job.sendWorkComplete(json.dumps(authorized))
def handle_get_admin_tenants(self, job):
args = json.loads(job.arguments)
claims = args['claims']
admin_tenants = []
for tenant_name, tenant in self.sched.abide.tenants.items():
if self._is_authorized(tenant, claims):
admin_tenants.append(tenant_name)
job.sendWorkComplete(json.dumps(admin_tenants))
def handle_tenant_list(self, job):
output = []
for tenant_name, tenant in self.sched.abide.tenants.items():

View File

@ -235,7 +235,9 @@ class ZuulWebAPI(object):
e_desc)
cherrypy.response.status = status
cherrypy.response.headers["WWW-Authenticate"] = error_header
return '<h1>%s</h1>' % e_desc
return {'description': e_desc,
'error': e,
'realm': self.zuulweb.authenticators.default_realm}
@cherrypy.expose
@cherrypy.tools.json_in()
@ -254,7 +256,9 @@ class ZuulWebAPI(object):
for header, contents in e.getAdditionalHeaders().items():
cherrypy.response.headers[header] = contents
cherrypy.response.status = e.HTTPError
return '<h1>%s</h1>' % e.error_description
return {'description': e.error_description,
'error': e.error,
'realm': e.realm}
self.is_authorized(claims, tenant)
msg = 'User "%s" requesting "%s" on %s/%s'
self.log.info(
@ -296,7 +300,9 @@ class ZuulWebAPI(object):
for header, contents in e.getAdditionalHeaders().items():
cherrypy.response.headers[header] = contents
cherrypy.response.status = e.HTTPError
return '<h1>%s</h1>' % e.error_description
return {'description': e.error_description,
'error': e.error,
'realm': e.realm}
self.is_authorized(claims, tenant)
msg = 'User "%s" requesting "%s" on %s/%s'
self.log.info(
@ -371,7 +377,9 @@ class ZuulWebAPI(object):
for header, contents in e.getAdditionalHeaders().items():
cherrypy.response.headers[header] = contents
cherrypy.response.status = e.HTTPError
return '<h1>%s</h1>' % e.error_description
return {'description': e.error_description,
'error': e.error,
'realm': e.realm}
self.is_authorized(claims, tenant)
msg = 'User "%s" requesting "%s" on %s/%s'
self.log.info(
@ -524,6 +532,14 @@ class ZuulWebAPI(object):
'buildsets': '/api/tenant/{tenant}/buildsets',
'buildset': '/api/tenant/{tenant}/buildset/{uuid}',
'config_errors': '/api/tenant/{tenant}/config-errors',
'authorizations': '/api/user/authorizations',
'autohold': '/api/tenant/{tenant}/project/{project:.*}/autohold',
'autohold_list': '/api/tenant/{tenant}/autohold',
'autohold_by_request_id': '/api/tenant/{tenant}/'
'autohold/{request_id}',
'autohold_delete': '/api/tenant/{tenant}/autohold/{request_id}',
'enqueue': '/api/tenant/{tenant}/project/{project:.*}/enqueue',
'dequeue': '/api/tenant/{tenant}/project/{project:.*}/dequeue',
}
@cherrypy.expose
@ -564,6 +580,27 @@ class ZuulWebAPI(object):
raise cherrypy.HTTPError(403)
return user_authz
# TODO good candidate for caching
@cherrypy.expose
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
def user_authorizations(self):
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
return {'description': e.error_description,
'error': e.error,
'realm': e.realm}
if 'zuul' in claims and 'admin' in claims.get('zuul', {}):
return {'zuul': {'admin': claims['zuul']['admin']}, }
job = self.rpc.submitJob('zuul:get_admin_tenants',
{'claims': claims})
admin_tenants = json.loads(job.data[0])
return {'zuul': {'admin': admin_tenants}, }
@cherrypy.expose
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
def tenants(self):
@ -1088,6 +1125,8 @@ class ZuulWeb(object):
if self.authenticators.authenticators:
# route order is important, put project actions before the more
# generic tenant/{tenant}/project/{project} route
route_map.connect('api', '/api/user/authorizations',
controller=api, action='user_authorizations')
route_map.connect(
'api',
'/api/tenant/{tenant}/project/{project:.*}/autohold',