Merge "Zuul Web: add /api/user/authorizations endpoint"
This commit is contained in:
commit
03290d8b2a
|
@ -16,7 +16,7 @@
|
|||
- admin-rule:
|
||||
name: car_rule
|
||||
conditions:
|
||||
- car: ecto-1
|
||||
- vehicle.car: ecto-1
|
||||
- tenant:
|
||||
name: tenant-one
|
||||
admin-rules:
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Reference in New Issue