Browse Source

Web: plug the authorization engine

Add an "authorize_user" RPC call allowing to test a set of claims
against the rules of a given tenant. Make zuul-web use this call
to authorize access to tenant-scoped privileged actions.

Change-Id: I50575f25b6db06f56b231bb47f8ad675febb9d82
tags/3.10.0
mhuin 9 months ago
parent
commit
19474fb62f
18 changed files with 446 additions and 104 deletions
  1. +5
    -2
      doc/source/admin/components.rst
  2. +29
    -2
      doc/source/admin/tenant-scoped-rest-api.rst
  3. +124
    -2
      doc/source/admin/tenants.rst
  4. +0
    -1
      etc/zuul.conf-sample
  5. +3
    -2
      releasenotes/notes/admin_web_api-1331c81070a3e67f.yaml
  6. +1
    -3
      tests/base.py
  7. +0
    -17
      tests/fixtures/config/authorization/rules/rules.yaml
  8. +23
    -1
      tests/fixtures/config/authorization/single-tenant/main.yaml
  9. +31
    -0
      tests/fixtures/zuul-admin-web-no-override.conf
  10. +7
    -0
      tests/unit/test_configloader.py
  11. +31
    -0
      tests/unit/test_scheduler.py
  12. +126
    -0
      tests/unit/test_web.py
  13. +0
    -5
      zuul/cmd/web.py
  14. +4
    -0
      zuul/configloader.py
  15. +8
    -2
      zuul/driver/auth/jwt.py
  16. +0
    -34
      zuul/lib/auth.py
  17. +24
    -0
      zuul/rpclistener.py
  18. +30
    -33
      zuul/web/__init__.py

+ 5
- 2
doc/source/admin/components.rst View File

@@ -830,6 +830,8 @@ 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.

.. _web-server-tenant-scoped-api:

Enabling tenant-scoped access to privileged actions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

@@ -853,8 +855,9 @@ protected endpoints and configure JWT validation:
.. attr:: allow_authz_override
:default: false

Allow a JWT to override predefined access rules. Since predefined access
rules are not supported yet, this should be set to ``true``.
Allow a JWT to override predefined access rules. See the section on
:ref:`JWT contents <jwt-format>` for more details on how to grant access
to tenants with a JWT.

.. attr:: realm


+ 29
- 2
doc/source/admin/tenant-scoped-rest-api.rst View File

@@ -1,5 +1,7 @@
:title: Tenant Scoped REST API

.. _tenant-scoped-rest-api:

Tenant Scoped REST API
======================

@@ -36,8 +38,33 @@ and Tokens should be handed over with discernment.
Configuration
-------------

See the Zuul Web Server component's section about enabling tenant-scoped access to
privileged actions.
To enable tenant-scoped access to privileged actions, see the Zuul Web Server
component's section.

To set access rules for a tenant, see :ref:`the documentation about tenant
definition <admin_rule_definition>`.

Most of the time, only one authenticator will be needed in Zuul's configuration;
namely the configuration matching a third party identity provider service like
dex, auth0, keycloak or others. It can be useful however to add another
authenticator similar to this one:

.. code-block:: ini

[auth zuul_operator]
driver=HS256
allow_authz_override=true
realm=zuul.example.com
client_id=zuul.example.com
issuer_id=zuul_operator
secret=NoDanaOnlyZuul

With such an authenticator, a Zuul operator can use Zuul's CLI to
issue Tokens overriding a tenant's access rules if need
be. A user can then use these Tokens with Zuul's CLI to perform protected actions
on a tenant temporarily, without having to modify a tenant's access rules.

.. _jwt-format:

JWT Format
----------

+ 124
- 2
doc/source/admin/tenants.rst View File

@@ -15,11 +15,15 @@ rest (no pipelines, jobs, etc are shared between them).
A project may appear in more than one tenant; this may be useful if
you wish to use common job definitions across multiple tenants.

Actions normally available to the Zuul operator only can be performed by specific
users on Zuul's REST API, if admin rules are listed for the tenant. Admin rules
are also defined in the tenant configuration file.

The tenant configuration file is specified by the
:attr:`scheduler.tenant_config` setting in ``zuul.conf``. It is a
YAML file which, like other Zuul configuration files, is a list of
configuration objects, though only one type of object is supported:
``tenant``.
configuration objects, though only two types of objects are supported:
``tenant`` and ``admin-rule``.

Alternatively the :attr:`scheduler.tenant_config_script`
can be the path to an executable that will be executed and its stdout
@@ -55,6 +59,9 @@ configuration. Some examples of tenant definitions are:

- tenant:
name: my-tenant
admin-rules:
- acl1
- acl2
source:
gerrit:
config-projects:
@@ -84,6 +91,17 @@ configuration. Some examples of tenant definitions are:
characters (ASCII letters, numbers, hyphen and underscore) and
you should avoid changing it unless necessary.

.. attr:: admin-rules

A list of access rules for the tenant. These rules are checked to grant
privileged actions to users at the tenant level, through Zuul's REST API.

At least one rule in the list must match for the user to be allowed the
privileged action.

More information on tenant-scoped actions can be found in
:ref:`this section <tenant-scoped-rest-api>`.

.. attr:: source
:required:

@@ -288,3 +306,107 @@ configuration. Some examples of tenant definitions are:
The list of labels regexp a tenant can use in job's nodeset. When set,
this setting can be used to restrict what labels a tenant can use.
Without this setting, the tenant can use any labels.

.. _admin_rule_definition:

Access Rule
-----------

An access rule is a set of conditions the claims of a user's JWT must match
in order to be allowed to perform protected actions at a tenant's level.

The protected actions available at tenant level are **autohold**, **enqueue**
or **dequeue**.

.. note::

Rules can be overridden by the ``zuul.admin`` claim in a Token if if matches
an authenticator configuration where `allow_authz_override` is set to true.
See :ref:`Zuul web server's configuration <web-server-tenant-scoped-api>` for
more details.

Below are some examples of how access rules can be defined:

.. code-block:: yaml

- admin-rule:
name: ghostbuster_or_gozerian
conditions:
- resources_access.account.roles: "ghostbuster"
iss: columbia_university
- resources_access.account.roles: "gozerian"
- admin-rule:
name: venkman_or_stantz
conditions:
- zuul_uid: venkman
- zuul_uid: stantz


.. attr:: admin-rule

The following attributes are supported:

.. attr:: name
:required:

The name of the rule, so that it can be referenced in the ``admin-rules``
attribute of a tenant's definition. It must be unique.

.. attr:: conditions
:required:

This is the list of conditions that define a rule. A JWT must match **at
least one** of the conditions for the rule to apply. A condition is a
dictionary where keys are claims. **All** the associated values must
match the claims in the user's Token.

Zuul's authorization engine will adapt matching tests depending on the
nature of the claim in the Token, eg:

* if the claim is a JSON list, check that the condition value is in the
claim
* if the claim is a string, check that the condition value is equal to
the claim's value

In order to allow the parsing of claims with complex structures like
dictionaries, claim names can be written in the XPath format.

The special ``zuul_uid`` claim refers to the ``uid_claim`` setting in an
authenticator's configuration. By default it refers to the ``sub`` claim
of a Token. For more details see the :ref:`configuration section
<web-server-tenant-scoped-api>` for Zuul web server.

Under the above example, the following Token would match rules
``ghostbuster_or_gozerian`` and ``venkman_or_stantz``:

.. code-block:: javascript

{
'iss': 'columbia_university',
'aud': 'my_zuul_deployment',
'exp': 1234567890,
'iat': 1234556780,
'sub': 'venkman',
'resources_access': {
'account': {
'roles': ['ghostbuster', 'played_by_bill_murray']
}
},
}

And this Token would only match rule ``ghostbuster_or_gozerian``:

.. code-block:: javascript

{
'iss': 'some_hellish_dimension',
'aud': 'my_zuul_deployment',
'exp': 1234567890,
'sub': 'vinz_clortho',
'iat': 1234556780,
'resources_access': {
'account': {
'roles': ['gozerian', 'keymaster']
}
},
}

+ 0
- 1
etc/zuul.conf-sample View File

@@ -39,7 +39,6 @@ listen_address=127.0.0.1
port=9000
static_cache_expiry=0
status_url=https://zuul.example.com/status
authorizations_config=/etc/zuul/authorizations.yaml

[webclient]
url=https://zuul.example.com

+ 3
- 2
releasenotes/notes/admin_web_api-1331c81070a3e67f.yaml View File

@@ -3,8 +3,9 @@ features:
- |
Allow users to perform tenant-scoped, privileged actions either through
zuul-web's REST API or zuul's client, based on the JWT standard. The users
need a valid bearer token to perform such actions; the scope is set via a
token claim.
need a valid bearer token to perform such actions; the scope is set by matching
conditions on tokens' claims; these conditions can be defined in zuul's tenant
configuration file.
Zuul supports token signing and validation using the HS256 or RS256 algorithms.
External JWKS are also supported for token validation only.
Current tenant-scoped actions are "autohold", "enqueue" and "dequeue".

+ 1
- 3
tests/base.py View File

@@ -2531,7 +2531,6 @@ class ZuulWebFixture(fixtures.Fixture):
zuul.driver.pagure.PagureDriver])
self.authenticators = zuul.lib.auth.AuthenticatorRegistry()
self.authenticators.configure(config)
self.authorizations = zuul.lib.auth.AuthorizationRegistry()
if info is None:
self.info = zuul.model.WebInfo()
else:
@@ -2548,8 +2547,7 @@ class ZuulWebFixture(fixtures.Fixture):
connections=self.connections,
zk_hosts=self.zk_hosts,
command_socket=os.path.join(self.test_root, 'web.socket'),
authenticators=self.authenticators,
authorizations=self.authorizations)
authenticators=self.authenticators)
self.web.start()
self.addCleanup(self.stop)


+ 0
- 17
tests/fixtures/config/authorization/rules/rules.yaml View File

@@ -1,17 +0,0 @@
- rule:
name: venkman_rule
conditions:
- zuul_uid: venkman
- rule:
name: columbia_rule
conditions:
- sub: stantz
iss: columbia.edu
- sub: zeddemore
iss: columbia.edu
- rule:
name: gb_rule
conditions:
- groups: ghostbusters
claim_types:
- groups: list

+ 23
- 1
tests/fixtures/config/authorization/single-tenant/main.yaml View File

@@ -1,7 +1,29 @@
- admin-rule:
name: venkman_rule
conditions:
- zuul_uid: venkman
- admin-rule:
name: columbia_rule
conditions:
- sub: stantz
iss: columbia.edu
- sub: zeddemore
iss: columbia.edu
- admin-rule:
name: gb_rule
conditions:
- groups: ghostbusters
- admin-rule:
name: car_rule
conditions:
- car: ecto-1
- tenant:
name: tenant-one
admin_rules:
admin-rules:
- venkman_rule
- car_rule
- gb_rule
- columbia_rule
source:
gerrit:
config-projects:

+ 31
- 0
tests/fixtures/zuul-admin-web-no-override.conf View File

@@ -0,0 +1,31 @@
[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

[auth zuul_operator]
driver=HS256
allow_authz_override=false
realm=zuul.example.com
client_id=zuul.example.com
issuer_id=zuul_operator
secret=NoDanaOnlyZuul

+ 7
- 0
tests/unit/test_configloader.py View File

@@ -427,6 +427,13 @@ class TestAuthorizationRuleParser(ZuulTestCase):
rules = self.sched.abide.admin_rules
self.assertTrue('auth-rule-one' in rules, self.sched.abide)
self.assertTrue('auth-rule-two' in rules, self.sched.abide)
claims_1 = {'sub': 'venkman'}
claims_2 = {'sub': 'gozer',
'iss': 'another_dimension'}
self.assertTrue(rules['auth-rule-one'](claims_1))
self.assertTrue(not rules['auth-rule-one'](claims_2))
self.assertTrue(not rules['auth-rule-two'](claims_1))
self.assertTrue(rules['auth-rule-two'](claims_2))

def test_parse_simplest_rule_from_yaml(self):
rule_d = {'name': 'my-rule',

+ 31
- 0
tests/unit/test_scheduler.py View File

@@ -107,6 +107,37 @@ class TestSchedulerZone(ZuulTestCase):
'label1')


class TestAuthorizeViaRPC(ZuulTestCase):
tenant_config_file = 'config/authorization/single-tenant/main.yaml'

def test_authorize_via_rpc(self):
client = zuul.rpcclient.RPCClient('127.0.0.1',
self.gearman_server.port)
self.addCleanup(client.shutdown)
claims = {'__zuul_uid_claim': 'venkman'}
authorized = client.submitJob('zuul:authorize_user',
{'tenant': 'tenant-one',
'claims': claims}).data[0]
self.assertTrue(json.loads(authorized))
claims = {'sub': 'gozer'}
authorized = client.submitJob('zuul:authorize_user',
{'tenant': 'tenant-one',
'claims': claims}).data[0]
self.assertTrue(not json.loads(authorized))
claims = {'sub': 'stantz',
'iss': 'columbia.edu'}
authorized = client.submitJob('zuul:authorize_user',
{'tenant': 'tenant-one',
'claims': claims}).data[0]
self.assertTrue(json.loads(authorized))
claims = {'sub': 'slimer',
'groups': ['ghostbusters', 'ectoplasms']}
authorized = client.submitJob('zuul:authorize_user',
{'tenant': 'tenant-one',
'claims': claims}).data[0]
self.assertTrue(json.loads(authorized))


class TestScheduler(ZuulTestCase):
tenant_config_file = 'config/single-tenant/main.yaml'


+ 126
- 0
tests/unit/test_web.py View File

@@ -1413,3 +1413,129 @@ class TestTenantScopedWebApi(BaseTestWeb):
self.executor_server.release()
self.waitUntilSettled()
self.assertEqual(self.countJobResults(self.history, 'ABORTED'), 1)


class TestTenantScopedWebApiWithAuthRules(BaseTestWeb):
config_file = 'zuul-admin-web-no-override.conf'
tenant_config_file = 'config/authorization/single-tenant/main.yaml'

def test_override_not_allowed(self):
"""Test that authz cannot be overriden if config does not allow it"""
args = {"reason": "some reason",
"count": 1,
'job': 'project-test2',
'change': None,
'ref': None,
'node_hold_expiration': None}
authz = {'iss': 'zuul_operator',
'aud': 'zuul.example.com',
'sub': 'testuser',
'zuul': {
'admin': ['tenant-one', ],
},
'exp': time.time() + 3600}
token = jwt.encode(authz, key='NoDanaOnlyZuul',
algorithm='HS256').decode('utf-8')
req = self.post_url(
'api/tenant/tenant-one/project/org/project/autohold',
headers={'Authorization': 'Bearer %s' % token},
json=args)
self.assertEqual(401, req.status_code, req.text)

def test_tenant_level_rule(self):
"""Test that authz rules defined at tenant level are checked"""
path = "api/tenant/%(tenant)s/project/%(project)s/enqueue"

def _test_project_enqueue_with_authz(i, project, authz, expected):
f_ch = self.fake_gerrit.addFakeChange(project, 'master',
'%s %i' % (project, i))
f_ch.addApproval('Code-Review', 2)
f_ch.addApproval('Approved', 1)
change = {'trigger': 'gerrit',
'change': '%i,1' % i,
'pipeline': 'gate', }
enqueue_args = {'tenant': 'tenant-one',
'project': project, }

token = jwt.encode(authz, key='NoDanaOnlyZuul',
algorithm='HS256').decode('utf-8')
req = self.post_url(path % enqueue_args,
headers={'Authorization': 'Bearer %s' % token},
json=change)
self.assertEqual(expected, req.status_code, req.text)
self.waitUntilSettled()

i = 0
for p in ['org/project', 'org/project1', 'org/project2']:
i += 1
# Authorized sub
authz = {'iss': 'zuul_operator',
'aud': 'zuul.example.com',
'sub': 'venkman',
'exp': time.time() + 3600}
_test_project_enqueue_with_authz(i, p, authz, 200)
i += 1
# Unauthorized sub
authz = {'iss': 'zuul_operator',
'aud': 'zuul.example.com',
'sub': 'vigo',
'exp': time.time() + 3600}
_test_project_enqueue_with_authz(i, p, authz, 403)
i += 1
# unauthorized issuer
authz = {'iss': 'columbia.edu',
'aud': 'zuul.example.com',
'sub': 'stantz',
'exp': time.time() + 3600}
_test_project_enqueue_with_authz(i, p, authz, 401)
self.waitUntilSettled()

def test_group_rule(self):
"""Test a group rule"""
A = self.fake_gerrit.addFakeChange('org/project2', 'master', 'A')
A.addApproval('Code-Review', 2)
A.addApproval('Approved', 1)

authz = {'iss': 'zuul_operator',
'aud': 'zuul.example.com',
'sub': 'melnitz',
'groups': ['ghostbusters', 'secretary'],
'exp': time.time() + 3600}
token = jwt.encode(authz, key='NoDanaOnlyZuul',
algorithm='HS256').decode('utf-8')
path = "api/tenant/%(tenant)s/project/%(project)s/enqueue"
enqueue_args = {'tenant': 'tenant-one',
'project': 'org/project2', }
change = {'trigger': 'gerrit',
'change': '1,1',
'pipeline': 'gate', }
req = self.post_url(path % enqueue_args,
headers={'Authorization': 'Bearer %s' % token},
json=change)
self.assertEqual(200, req.status_code, req.text)
self.waitUntilSettled()

def test_arbitrary_claim_rule(self):
"""Test a rule based on a specific claim"""
A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
A.addApproval('Code-Review', 2)
A.addApproval('Approved', 1)

authz = {'iss': 'zuul_operator',
'aud': 'zuul.example.com',
'sub': 'zeddemore',
'car': 'ecto-1',
'exp': time.time() + 3600}
token = jwt.encode(authz, key='NoDanaOnlyZuul',
algorithm='HS256').decode('utf-8')
path = "api/tenant/%(tenant)s/project/%(project)s/enqueue"
enqueue_args = {'tenant': 'tenant-one',
'project': 'org/project', }
change = {'trigger': 'gerrit',
'change': '1,1',
'pipeline': 'gate', }
req = self.post_url(path % enqueue_args,
headers={'Authorization': 'Bearer %s' % token},
json=change)
self.assertEqual(200, req.status_code, req.text)
self.waitUntilSettled()

+ 0
- 5
zuul/cmd/web.py View File

@@ -74,7 +74,6 @@ class WebServer(zuul.cmd.ZuulDaemonApp):

params['connections'] = self.connections
params['authenticators'] = self.authenticators
params['authorizations'] = self.authorizations
# Validate config here before we spin up the ZuulWeb object
for conn_name, connection in self.connections.connections.items():
try:
@@ -113,9 +112,6 @@ class WebServer(zuul.cmd.ZuulDaemonApp):
self.authenticators = zuul.lib.auth.AuthenticatorRegistry()
self.authenticators.configure(self.config)

def configure_authorizations(self):
self.authorizations = zuul.lib.auth.AuthorizationRegistry()

def run(self):
if self.args.command in zuul.web.COMMANDS:
self.send_command(self.args.command)
@@ -130,7 +126,6 @@ class WebServer(zuul.cmd.ZuulDaemonApp):
zuul.driver.github.GithubDriver,
zuul.driver.pagure.PagureDriver])
self.configure_authenticators()
self.configure_authorizations()
self._run()
except Exception:
self.log.exception("Exception from WebServer:")

+ 4
- 0
zuul/configloader.py View File

@@ -1340,6 +1340,8 @@ class AuthorizationRuleParser(object):
elif isinstance(node, dict):
subrules = []
for claim, value in node.items():
if claim == 'zuul_uid':
claim = '__zuul_uid_claim'
subrules.append(model.ClaimRule(claim, value))
return model.AndRule(subrules)
else:
@@ -1465,6 +1467,8 @@ class TenantParser(object):
if conf.get('exclude-unprotected-branches') is not None:
tenant.exclude_unprotected_branches = \
conf['exclude-unprotected-branches']
if conf.get('admin-rules') is not None:
tenant.authorization_rules = conf['admin-rules']
tenant.allowed_triggers = conf.get('allowed-triggers')
tenant.allowed_reporters = conf.get('allowed-reporters')
tenant.allowed_labels = conf.get('allowed-labels')

+ 8
- 2
zuul/driver/auth/jwt.py View File

@@ -36,6 +36,11 @@ class JWTAuthenticator(AuthenticatorInterface):
self.audience = conf.get('client_id')
self.realm = conf.get('realm')
self.allow_authz_override = conf.get('allow_authz_override', False)
if isinstance(self.allow_authz_override, str):
if self.allow_authz_override.lower() == 'true':
self.allow_authz_override = True
else:
self.allow_authz_override = False

def _decode(self, rawToken):
raise NotImplementedError
@@ -90,8 +95,9 @@ class JWTAuthenticator(AuthenticatorInterface):

def authenticate(self, rawToken):
decoded = self.decodeToken(rawToken)
return (decoded[self.uid_claim],
decoded.get('zuul', {}).get('admin', []))
# inject the special authenticator-specific uid
decoded['__zuul_uid_claim'] = decoded[self.uid_claim]
return decoded


class HS256Authenticator(JWTAuthenticator):

+ 0
- 34
zuul/lib/auth.py View File

@@ -19,45 +19,11 @@ import jwt

from zuul import exceptions
import zuul.driver.auth.jwt as auth_jwt
from zuul.configloader import AuthorizationRuleParser


"""AuthN/AuthZ related library, used by zuul-web."""


class AuthorizationRegistry(object):
"""Registry of authorization rules.

reconfigure(rules) takes a JSON list to create the ruleset; typically
provided by the scheduler."""

log = logging.getLogger("Zuul.AuthorizationRegistry")

def __init__(self):
self.ruleset = {}

def reconfigure(self, rules):
if not isinstance(rules, list):
raise Exception('Authorizations file must be a list of rules')
new_ruleset = {}
ruleparser = AuthorizationRuleParser()
for rule in rules:
if not isinstance(rule, dict):
raise Exception('Invalid rule format for rule "%r"' % rule)
if len(rule.keys()) > 1:
raise Exception('Rules must consist of "rule" element only')
if 'rule' in rule:
rule_tree = ruleparser.fromYaml(rule['rule'])
if rule_tree.name in new_ruleset:
raise Exception(
'Rule "%s" is defined at least twice' % rule_tree.name)
else:
new_ruleset[rule_tree.name] = rule_tree
else:
raise Exception('Unknown element "%s"' % rule.keys()[0])
self.ruleset = new_ruleset


class AuthenticatorRegistry(object):
"""Registry of authenticators as they are declared in the configuration"""


+ 24
- 0
zuul/rpclistener.py View File

@@ -54,6 +54,7 @@ class RPCListener(object):
'key_get',
'config_errors_list',
'connection_list',
'authorize_user',
]
for func in functions:
f = getattr(self, 'handle_%s' % func)
@@ -281,6 +282,29 @@ 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)
authorized = False
if tenant:
rules = tenant.authorization_rules
for rule in rules:
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'
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))
break
job.sendWorkComplete(json.dumps(authorized))

def handle_tenant_list(self, job):
output = []
for tenant_name, tenant in self.sched.abide.tenants.items():

+ 30
- 33
zuul/web/__init__.py View File

@@ -44,16 +44,6 @@ cherrypy.tools.websocket = WebSocketTool()
COMMANDS = ['stop', 'repl', 'norepl']


def is_authorized(uid, tenant, authN=None):
"""Simple authorization checker. For now, relies on the passed authN
dictionary to figure out whether 'uid' is allowed 'action' on
'tenant/project'.
This is just a stub that will be expanded in subsequent patches."""
if authN is None:
authN = []
return (tenant in authN)


class SaveParamsTool(cherrypy.Tool):
"""
Save the URL parameters to allow them to take precedence over query
@@ -259,19 +249,17 @@ class ZuulWebAPI(object):
# AuthN/AuthZ
rawToken = cherrypy.request.headers['Authorization'][len('Bearer '):]
try:
uid, authz = self.zuulweb.authenticators.authenticate(rawToken)
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 '<h1>%s</h1>' % e.error_description
# TODO plug an actual authorization mechanism, for now rely on token
# content
if not is_authorized(uid, tenant, authz):
raise cherrypy.HTTPError(403)
self.is_authorized(claims, tenant)
msg = 'User "%s" requesting "%s" on %s/%s'
self.log.info(
'User "%s" requesting "%s" on %s/%s' % (uid, 'dequeue',
tenant, project))
msg % (claims['__zuul_uid_claim'], 'dequeue',
tenant, project))

body = cherrypy.request.json
if 'pipeline' in body and (
@@ -303,19 +291,17 @@ class ZuulWebAPI(object):
# AuthN/AuthZ
rawToken = cherrypy.request.headers['Authorization'][len('Bearer '):]
try:
uid, authz = self.zuulweb.authenticators.authenticate(rawToken)
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 '<h1>%s</h1>' % e.error_description
# TODO plug an actual authorization mechanism, for now rely on token
# content
if not is_authorized(uid, tenant, authz):
raise cherrypy.HTTPError(403)
self.is_authorized(claims, tenant)
msg = 'User "%s" requesting "%s" on %s/%s'
self.log.info(
'User "%s" requesting "%s" on %s/%s' % (uid, 'enqueue',
tenant, project))
msg % (claims['__zuul_uid_claim'], 'enqueue',
tenant, project))

body = cherrypy.request.json
if all(p in body for p in ['trigger', 'change', 'pipeline']):
@@ -380,19 +366,17 @@ class ZuulWebAPI(object):
rawToken = \
cherrypy.request.headers['Authorization'][len('Bearer '):]
try:
uid, authz = self.zuulweb.authenticators.authenticate(rawToken)
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 '<h1>%s</h1>' % e.error_description
# TODO plug an actual authorization mechanism, for now rely on
# token content
if not is_authorized(uid, tenant, authz):
raise cherrypy.HTTPError(403)
self.is_authorized(claims, tenant)
msg = 'User "%s" requesting "%s" on %s/%s'
self.log.info(
'User "%s" requesting "%s" on %s/%s' % (uid, 'autohold',
tenant, project))
msg % (claims['__zuul_uid_claim'], 'autohold',
tenant, project))

length = int(cherrypy.request.headers['Content-Length'])
body = cherrypy.request.body.read(length)
@@ -502,6 +486,21 @@ class ZuulWebAPI(object):
resp.last_modified = self.zuulweb.start_time
return ret

def is_authorized(self, claims, tenant):
# First, check for zuul.admin override
override = claims.get('zuul', {}).get('admin', [])
if (override == '*' or
(isinstance(override, list) and tenant in override)):
return True
# Next, get the rules for tenant
data = {'tenant': tenant, 'claims': claims}
# TODO: it is probably worth caching
job = self.rpc.submitJob('zuul:authorize_user', data)
user_authz = json.loads(job.data[0])
if not user_authz:
raise cherrypy.HTTPError(403)
return user_authz

@cherrypy.expose
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
def tenants(self):
@@ -968,7 +967,6 @@ class ZuulWeb(object):
static_path=None,
zk_hosts=None,
authenticators=None,
authorizations=None,
command_socket=None,
):
self.start_time = time.time()
@@ -988,7 +986,6 @@ class ZuulWeb(object):
self.zk.connect(hosts=zk_hosts, read_only=True)
self.connections = connections
self.authenticators = authenticators
self.authorizations = authorizations
self.stream_manager = StreamManager()

self.command_socket = commandsocket.CommandSocket(command_socket)

Loading…
Cancel
Save