diff --git a/etc/zuul.conf-sample b/etc/zuul.conf-sample index c0916c6c43..c054169629 100644 --- a/etc/zuul.conf-sample +++ b/etc/zuul.conf-sample @@ -39,6 +39,7 @@ 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 diff --git a/requirements.txt b/requirements.txt index 450b45b572..c741f078b2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,3 +28,4 @@ paho-mqtt cherrypy ws4py routes +jsonpath-rw diff --git a/tests/base.py b/tests/base.py index 3b940b4529..a30af1b101 100644 --- a/tests/base.py +++ b/tests/base.py @@ -2529,8 +2529,9 @@ class ZuulWebFixture(fixtures.Fixture): include_drivers=[zuul.driver.sql.SQLDriver, zuul.driver.github.GithubDriver, zuul.driver.pagure.PagureDriver]) - self.auths = zuul.lib.auth.AuthenticatorRegistry() - self.auths.configure(config) + 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: @@ -2547,7 +2548,8 @@ class ZuulWebFixture(fixtures.Fixture): connections=self.connections, zk_hosts=self.zk_hosts, command_socket=os.path.join(self.test_root, 'web.socket'), - auths=self.auths) + authenticators=self.authenticators, + authorizations=self.authorizations) self.web.start() self.addCleanup(self.stop) @@ -3194,6 +3196,8 @@ class ZuulTestCase(BaseTestCase): with open(os.path.join(FIXTURE_DIR, path)) as f: tenant_config = yaml.safe_load(f.read()) for tenant in tenant_config: + if 'tenant' not in tenant.keys(): + continue sources = tenant['tenant']['source'] for source, conf in sources.items(): for project in conf.get('config-projects', []): diff --git a/tests/fixtures/config/authorization/rules/rules.yaml b/tests/fixtures/config/authorization/rules/rules.yaml new file mode 100644 index 0000000000..dea5c15816 --- /dev/null +++ b/tests/fixtures/config/authorization/rules/rules.yaml @@ -0,0 +1,17 @@ +- 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 diff --git a/tests/fixtures/config/authorization/single-tenant/git/common-config/playbooks/nonvoting-project-merge.yaml b/tests/fixtures/config/authorization/single-tenant/git/common-config/playbooks/nonvoting-project-merge.yaml new file mode 100644 index 0000000000..f679dceaef --- /dev/null +++ b/tests/fixtures/config/authorization/single-tenant/git/common-config/playbooks/nonvoting-project-merge.yaml @@ -0,0 +1,2 @@ +- hosts: all + tasks: [] diff --git a/tests/fixtures/config/authorization/single-tenant/git/common-config/playbooks/nonvoting-project-test1.yaml b/tests/fixtures/config/authorization/single-tenant/git/common-config/playbooks/nonvoting-project-test1.yaml new file mode 100644 index 0000000000..f679dceaef --- /dev/null +++ b/tests/fixtures/config/authorization/single-tenant/git/common-config/playbooks/nonvoting-project-test1.yaml @@ -0,0 +1,2 @@ +- hosts: all + tasks: [] diff --git a/tests/fixtures/config/authorization/single-tenant/git/common-config/playbooks/nonvoting-project-test2.yaml b/tests/fixtures/config/authorization/single-tenant/git/common-config/playbooks/nonvoting-project-test2.yaml new file mode 100644 index 0000000000..f679dceaef --- /dev/null +++ b/tests/fixtures/config/authorization/single-tenant/git/common-config/playbooks/nonvoting-project-test2.yaml @@ -0,0 +1,2 @@ +- hosts: all + tasks: [] diff --git a/tests/fixtures/config/authorization/single-tenant/git/common-config/playbooks/project-merge.yaml b/tests/fixtures/config/authorization/single-tenant/git/common-config/playbooks/project-merge.yaml new file mode 100644 index 0000000000..f679dceaef --- /dev/null +++ b/tests/fixtures/config/authorization/single-tenant/git/common-config/playbooks/project-merge.yaml @@ -0,0 +1,2 @@ +- hosts: all + tasks: [] diff --git a/tests/fixtures/config/authorization/single-tenant/git/common-config/playbooks/project-post.yaml b/tests/fixtures/config/authorization/single-tenant/git/common-config/playbooks/project-post.yaml new file mode 100644 index 0000000000..f679dceaef --- /dev/null +++ b/tests/fixtures/config/authorization/single-tenant/git/common-config/playbooks/project-post.yaml @@ -0,0 +1,2 @@ +- hosts: all + tasks: [] diff --git a/tests/fixtures/config/authorization/single-tenant/git/common-config/playbooks/project-test1.yaml b/tests/fixtures/config/authorization/single-tenant/git/common-config/playbooks/project-test1.yaml new file mode 100644 index 0000000000..f679dceaef --- /dev/null +++ b/tests/fixtures/config/authorization/single-tenant/git/common-config/playbooks/project-test1.yaml @@ -0,0 +1,2 @@ +- hosts: all + tasks: [] diff --git a/tests/fixtures/config/authorization/single-tenant/git/common-config/playbooks/project-test2.yaml b/tests/fixtures/config/authorization/single-tenant/git/common-config/playbooks/project-test2.yaml new file mode 100644 index 0000000000..f679dceaef --- /dev/null +++ b/tests/fixtures/config/authorization/single-tenant/git/common-config/playbooks/project-test2.yaml @@ -0,0 +1,2 @@ +- hosts: all + tasks: [] diff --git a/tests/fixtures/config/authorization/single-tenant/git/common-config/playbooks/project-testfile.yaml b/tests/fixtures/config/authorization/single-tenant/git/common-config/playbooks/project-testfile.yaml new file mode 100644 index 0000000000..f679dceaef --- /dev/null +++ b/tests/fixtures/config/authorization/single-tenant/git/common-config/playbooks/project-testfile.yaml @@ -0,0 +1,2 @@ +- hosts: all + tasks: [] diff --git a/tests/fixtures/config/authorization/single-tenant/git/common-config/playbooks/project1-project2-integration.yaml b/tests/fixtures/config/authorization/single-tenant/git/common-config/playbooks/project1-project2-integration.yaml new file mode 100644 index 0000000000..f679dceaef --- /dev/null +++ b/tests/fixtures/config/authorization/single-tenant/git/common-config/playbooks/project1-project2-integration.yaml @@ -0,0 +1,2 @@ +- hosts: all + tasks: [] diff --git a/tests/fixtures/config/authorization/single-tenant/git/common-config/zuul.yaml b/tests/fixtures/config/authorization/single-tenant/git/common-config/zuul.yaml new file mode 100644 index 0000000000..750d578ec0 --- /dev/null +++ b/tests/fixtures/config/authorization/single-tenant/git/common-config/zuul.yaml @@ -0,0 +1,197 @@ +- pipeline: + name: check + manager: independent + trigger: + gerrit: + - event: patchset-created + 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 + +- job: + name: project-merge + hold-following-changes: true + nodeset: + nodes: + - name: controller + label: label1 + run: playbooks/project-merge.yaml + +- job: + name: project-test1 + attempts: 4 + nodeset: + nodes: + - name: controller + label: label1 + run: playbooks/project-test1.yaml + +- job: + name: project-test1 + branches: stable + nodeset: + nodes: + - name: controller + label: label2 + run: playbooks/project-test1.yaml + +- job: + name: project-post + nodeset: + nodes: + - name: static + label: ubuntu-xenial + run: playbooks/project-post.yaml + +- job: + name: project-test2 + nodeset: + nodes: + - name: controller + label: label1 + run: playbooks/project-test2.yaml + +- job: + name: project1-project2-integration + nodeset: + nodes: + - name: controller + label: label1 + run: playbooks/project1-project2-integration.yaml + +- job: + name: project-testfile + files: + - .*-requires + run: playbooks/project-testfile.yaml + +- project: + name: org/project + check: + jobs: + - project-merge + - project-test1: + dependencies: project-merge + - project-test2: + dependencies: project-merge + gate: + jobs: + - project-merge + - project-test1: + dependencies: project-merge + - project-test2: + dependencies: project-merge + - project-testfile: + dependencies: project-merge + post: + jobs: + - project-post + +- project: + name: org/project1 + check: + jobs: + - project-merge + - project-test1: + dependencies: project-merge + - project-test2: + dependencies: project-merge + - project1-project2-integration: + dependencies: project-merge + gate: + queue: integrated + jobs: + - project-merge + - project-test1: + dependencies: project-merge + - project-test2: + dependencies: project-merge + - project1-project2-integration: + dependencies: project-merge + +- project: + name: org/project2 + check: + jobs: + - project-merge + - project-test1: + dependencies: project-merge + - project-test2: + dependencies: project-merge + - project1-project2-integration: + dependencies: project-merge + gate: + queue: integrated + jobs: + - project-merge + - project-test1: + dependencies: project-merge + - project-test2: + dependencies: project-merge + - project1-project2-integration: + dependencies: project-merge + +- project: + name: common-config + check: + jobs: + - project-merge + - project-test1: + dependencies: project-merge + - project-test2: + dependencies: project-merge + - project1-project2-integration: + dependencies: project-merge + gate: + queue: integrated + jobs: + - project-merge + - project-test1: + dependencies: project-merge + - project-test2: + dependencies: project-merge + - project1-project2-integration: + dependencies: project-merge + +- job: + name: test-job + run: playbooks/project-merge.yaml + required-projects: + - org/project diff --git a/tests/fixtures/config/authorization/single-tenant/git/org_project/README b/tests/fixtures/config/authorization/single-tenant/git/org_project/README new file mode 100644 index 0000000000..9daeafb986 --- /dev/null +++ b/tests/fixtures/config/authorization/single-tenant/git/org_project/README @@ -0,0 +1 @@ +test diff --git a/tests/fixtures/config/authorization/single-tenant/git/org_project1/README b/tests/fixtures/config/authorization/single-tenant/git/org_project1/README new file mode 100644 index 0000000000..9daeafb986 --- /dev/null +++ b/tests/fixtures/config/authorization/single-tenant/git/org_project1/README @@ -0,0 +1 @@ +test diff --git a/tests/fixtures/config/authorization/single-tenant/git/org_project2/README b/tests/fixtures/config/authorization/single-tenant/git/org_project2/README new file mode 100644 index 0000000000..9daeafb986 --- /dev/null +++ b/tests/fixtures/config/authorization/single-tenant/git/org_project2/README @@ -0,0 +1 @@ +test diff --git a/tests/fixtures/config/authorization/single-tenant/main.yaml b/tests/fixtures/config/authorization/single-tenant/main.yaml new file mode 100644 index 0000000000..8baca519ce --- /dev/null +++ b/tests/fixtures/config/authorization/single-tenant/main.yaml @@ -0,0 +1,12 @@ +- tenant: + name: tenant-one + admin_rules: + - venkman_rule + source: + gerrit: + config-projects: + - common-config + untrusted-projects: + - org/project + - org/project1 + - org/project2 diff --git a/tests/fixtures/config/tenant-parser/authorizations.yaml b/tests/fixtures/config/tenant-parser/authorizations.yaml new file mode 100644 index 0000000000..d84e7d8a6d --- /dev/null +++ b/tests/fixtures/config/tenant-parser/authorizations.yaml @@ -0,0 +1,22 @@ +- admin-rule: + name: auth-rule-one + conditions: + - sub: venkman + - sub: zeddemore +- admin-rule: + name: auth-rule-two + conditions: + - sub: gozer + iss: another_dimension +- tenant: + name: tenant-one + admin-rules: + - auth-rule-one + - auth-rule-two + source: + gerrit: + config-projects: + - common-config + untrusted-projects: + - org/project1 + - org/project2 diff --git a/tests/unit/test_configloader.py b/tests/unit/test_configloader.py index d09d521e96..4729d86557 100644 --- a/tests/unit/test_configloader.py +++ b/tests/unit/test_configloader.py @@ -16,6 +16,8 @@ import fixtures import logging import textwrap +from zuul.configloader import AuthorizationRuleParser + from tests.base import ZuulTestCase @@ -418,6 +420,108 @@ class TestConfigConflict(ZuulTestCase): jobs) +class TestAuthorizationRuleParser(ZuulTestCase): + tenant_config_file = 'config/tenant-parser/authorizations.yaml' + + def test_rules_are_loaded(self): + 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) + + def test_parse_simplest_rule_from_yaml(self): + rule_d = {'name': 'my-rule', + 'conditions': {'sub': 'user1'} + } + rule = AuthorizationRuleParser().fromYaml(rule_d) + self.assertEqual('my-rule', rule.name) + claims = {'iss': 'my-idp', + 'sub': 'user1', + 'groups': ['admin', 'ghostbusters']} + self.assertTrue(rule(claims)) + claims = {'iss': 'my-2nd-idp', + 'sub': 'user2', + 'groups': ['admin', 'ghostbusters']} + self.assertFalse(rule(claims)) + + def test_parse_AND_rule_from_yaml(self): + rule_d = {'name': 'my-rule', + 'conditions': {'sub': 'user1', + 'iss': 'my-idp'} + } + rule = AuthorizationRuleParser().fromYaml(rule_d) + self.assertEqual('my-rule', rule.name) + claims = {'iss': 'my-idp', + 'sub': 'user1', + 'groups': ['admin', 'ghostbusters']} + self.assertTrue(rule(claims)) + claims = {'iss': 'my-2nd-idp', + 'sub': 'user1', + 'groups': ['admin', 'ghostbusters']} + self.assertFalse(rule(claims)) + + def test_parse_OR_rule_from_yaml(self): + rule_d = {'name': 'my-rule', + 'conditions': [{'sub': 'user1', + 'iss': 'my-idp'}, + {'sub': 'user2', + 'iss': 'my-2nd-idp'} + ] + } + rule = AuthorizationRuleParser().fromYaml(rule_d) + self.assertEqual('my-rule', rule.name) + claims = {'iss': 'my-idp', + 'sub': 'user1', + 'groups': ['admin', 'ghostbusters']} + self.assertTrue(rule(claims)) + claims = {'iss': 'my-2nd-idp', + 'sub': 'user1', + 'groups': ['admin', 'ghostbusters']} + self.assertFalse(rule(claims)) + claims = {'iss': 'my-2nd-idp', + 'sub': 'user2', + 'groups': ['admin', 'ghostbusters']} + self.assertTrue(rule(claims)) + + def test_parse_rule_with_list_claim_from_yaml(self): + rule_d = {'name': 'my-rule', + 'conditions': [{'groups': 'ghostbusters', + 'iss': 'my-idp'}, + {'sub': 'user2', + 'iss': 'my-2nd-idp'} + ], + } + rule = AuthorizationRuleParser().fromYaml(rule_d) + self.assertEqual('my-rule', rule.name) + claims = {'iss': 'my-idp', + 'sub': 'user1', + 'groups': ['admin', 'ghostbusters']} + self.assertTrue(rule(claims)) + claims = {'iss': 'my-idp', + 'sub': 'user1', + 'groups': ['admin', 'ghostbeaters']} + self.assertFalse(rule(claims)) + claims = {'iss': 'my-2nd-idp', + 'sub': 'user2', + 'groups': ['admin', 'ghostbusters']} + self.assertTrue(rule(claims)) + + def test_check_complex_rule_from_yaml(self): + rule_d = {'name': 'my-rule', + 'conditions': [{'hello.this.is': 'a complex value'}, + ], + } + rule = AuthorizationRuleParser().fromYaml(rule_d) + self.assertEqual('my-rule', rule.name) + claims = {'iss': 'my-idp', + 'hello': { + 'this': { + 'is': 'a complex value' + } + } + } + self.assertTrue(rule(claims)) + + class TestTenantExtra(TenantParserTestCase): tenant_config_file = 'config/tenant-parser/extra.yaml' diff --git a/zuul/cmd/web.py b/zuul/cmd/web.py index 8a7d38e247..1c88f56d49 100755 --- a/zuul/cmd/web.py +++ b/zuul/cmd/web.py @@ -73,7 +73,8 @@ class WebServer(zuul.cmd.ZuulDaemonApp): '/var/lib/zuul/web.socket') params['connections'] = self.connections - params['auths'] = self.auths + 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: @@ -108,9 +109,12 @@ class WebServer(zuul.cmd.ZuulDaemonApp): self.web.stop() self.log.info("Zuul Web Server stopped") - def configure_auth(self): - self.auths = zuul.lib.auth.AuthenticatorRegistry() - self.auths.configure(self.config) + def configure_authenticators(self): + 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: @@ -125,7 +129,8 @@ class WebServer(zuul.cmd.ZuulDaemonApp): include_drivers=[zuul.driver.sql.SQLDriver, zuul.driver.github.GithubDriver, zuul.driver.pagure.PagureDriver]) - self.configure_auth() + self.configure_authenticators() + self.configure_authorizations() self._run() except Exception: self.log.exception("Exception from WebServer:") diff --git a/zuul/configloader.py b/zuul/configloader.py index dfa51ac80a..52c539053e 100644 --- a/zuul/configloader.py +++ b/zuul/configloader.py @@ -1317,6 +1317,38 @@ class SemaphoreParser(object): return semaphore +class AuthorizationRuleParser(object): + def __init__(self): + self.log = logging.getLogger("zuul.AuthorizationRuleParser") + self.schema = self.getSchema() + + def getSchema(self): + + authRule = {vs.Required('name'): str, + vs.Required('conditions'): to_list(dict) + } + + return vs.Schema(authRule) + + def fromYaml(self, conf): + self.schema(conf) + a = model.AuthZRuleTree(conf['name']) + + def parse_tree(node): + if isinstance(node, list): + return model.OrRule(parse_tree(x) for x in node) + elif isinstance(node, dict): + subrules = [] + for claim, value in node.items(): + subrules.append(model.ClaimRule(claim, value)) + return model.AndRule(subrules) + else: + raise Exception('Invalid claim declaration %r' % node) + + a.ruletree = parse_tree(conf['conditions']) + return a + + class ParseContext(object): """Hold information about a particular run of the parser""" @@ -1419,6 +1451,7 @@ class TenantParser(object): 'allowed-labels': to_list(str), 'default-parent': str, 'default-ansible-version': vs.Any(str, float), + 'admin-rules': to_list(str), } return vs.Schema(tenant) @@ -2064,6 +2097,7 @@ class ConfigLoader(object): self.keystorage = None self.tenant_parser = TenantParser(connections, scheduler, merger, self.keystorage) + self.admin_rule_parser = AuthorizationRuleParser() def expandConfigPath(self, config_path): if config_path: @@ -2105,6 +2139,9 @@ class ConfigLoader(object): def loadConfig(self, unparsed_abide, ansible_manager): abide = model.Abide() + for conf_admin_rule in unparsed_abide.admin_rules: + admin_rule = self.admin_rule_parser.fromYaml(conf_admin_rule) + abide.admin_rules[admin_rule.name] = admin_rule for conf_tenant in unparsed_abide.tenants: # When performing a full reload, do not use cached data. tenant = self.tenant_parser.fromYaml( @@ -2123,6 +2160,7 @@ class ConfigLoader(object): def reloadTenant(self, abide, tenant, ansible_manager): new_abide = model.Abide() new_abide.tenants = abide.tenants.copy() + new_abide.admin_rules = abide.admin_rules.copy() new_abide.unparsed_project_branch_cache = \ abide.unparsed_project_branch_cache diff --git a/zuul/lib/auth.py b/zuul/lib/auth.py index 9be7fb9447..14f5e0c0ac 100644 --- a/zuul/lib/auth.py +++ b/zuul/lib/auth.py @@ -19,6 +19,43 @@ 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): diff --git a/zuul/model.py b/zuul/model.py index 8be9cc4873..8356ac9331 100644 --- a/zuul/model.py +++ b/zuul/model.py @@ -27,6 +27,8 @@ import textwrap import types import itertools +import jsonpath_rw + from zuul import change_matcher from zuul.lib.config import get_default from zuul.lib.artifacts import get_artifacts_from_result_data @@ -3443,16 +3445,18 @@ class UnparsedAbideConfig(object): """A collection of yaml lists that has not yet been parsed into objects. - An Abide is a collection of tenants. + An Abide is a collection of tenants and access rules to those tenants. """ def __init__(self): self.tenants = [] + self.admin_rules = [] self.known_tenants = set() def extend(self, conf): if isinstance(conf, UnparsedAbideConfig): self.tenants.extend(conf.tenants) + self.admin_rules.extend(conf.admin_rules) return if not isinstance(conf, list): @@ -3468,6 +3472,8 @@ class UnparsedAbideConfig(object): self.tenants.append(value) if 'name' in value: self.known_tenants.add(value['name']) + elif key == 'admin-rule': + self.admin_rules.append(value) else: raise ConfigItemUnknownError() @@ -4237,6 +4243,8 @@ class Tenant(object): # The per tenant default ansible version self.default_ansible_version = None + self.authorization_rules = [] + def _addProject(self, tpc): """Add a project to the project index @@ -4429,6 +4437,7 @@ class UnparsedBranchCache(object): class Abide(object): def __init__(self): + self.admin_rules = OrderedDict() self.tenants = OrderedDict() # project -> branch -> UnparsedBranchCache self.unparsed_project_branch_cache = {} @@ -4619,3 +4628,110 @@ class WebInfo(object): if self.tenant: d['tenant'] = self.tenant return d + + +# AuthZ models + +class AuthZRule(object): + """The base class for authorization rules""" + + def __ne__(self, other): + return not self.__eq__(other) + + +class ClaimRule(AuthZRule): + """This rule checks the value of a claim. + The check tries to be smart by assessing the type of the tested value.""" + def __init__(self, claim=None, value=None): + super(ClaimRule, self).__init__() + self.claim = claim or 'sub' + self.value = value + + def __call__(self, claims): + matches = [match.value + for match in jsonpath_rw.parse(self.claim).find(claims)] + if len(matches) == 1: + match = matches[0] + if isinstance(match, list): + return self.value in match + elif isinstance(match, str): + return self.value == match + else: + # unsupported type - don't raise, but this should be notified + return False + else: + # TODO we should differentiate no match and 2+ matches + return False + + def __eq__(self, other): + if not isinstance(other, ClaimRule): + return False + return (self.claim == other.claim and self.value == other.value) + + def __repr__(self): + return '' % (self.claim, self.value) + + def __hash__(self): + return hash(repr(self)) + + +class OrRule(AuthZRule): + + def __init__(self, subrules): + super(OrRule, self).__init__() + self.rules = set(subrules) + + def __call__(self, claims): + return any(rule(claims) for rule in self.rules) + + def __eq__(self, other): + if not isinstance(other, OrRule): + return False + return self.rules == other.rules + + def __repr__(self): + return '' % (' || '.join(repr(r) for r in self.rules)) + + def __hash__(self): + return hash(repr(self)) + + +class AndRule(AuthZRule): + + def __init__(self, subrules): + super(AndRule, self).__init__() + self.rules = set(subrules) + + def __call__(self, claims): + return all(rule(claims) for rule in self.rules) + + def __eq__(self, other): + if not isinstance(other, AndRule): + return False + return self.rules == other.rules + + def __repr__(self): + return '' % (' && '.join(repr(r) for r in self.rules)) + + def __hash__(self): + return hash(repr(self)) + + +class AuthZRuleTree(object): + + def __init__(self, name): + self.name = name + # initialize actions as unauthorized + self.ruletree = None + + def __call__(self, claims): + return self.ruletree(claims) + + def __eq__(self, other): + if not isinstance(other, AuthZRuleTree): + return False + return (self.name == other.name and + self.ruletree == other.ruletree) + + def __repr__(self): + return '' % self.ruletree diff --git a/zuul/web/__init__.py b/zuul/web/__init__.py index 45e6345564..14810414ce 100755 --- a/zuul/web/__init__.py +++ b/zuul/web/__init__.py @@ -240,7 +240,7 @@ class ZuulWebAPI(object): return None error_header = '''Bearer realm="%s" error="%s" - error_description="%s"''' % (self.zuulweb.auths.default_realm, + error_description="%s"''' % (self.zuulweb.authenticators.default_realm, e, e_desc) cherrypy.response.status = status @@ -259,7 +259,7 @@ class ZuulWebAPI(object): # AuthN/AuthZ rawToken = cherrypy.request.headers['Authorization'][len('Bearer '):] try: - uid, authz = self.zuulweb.auths.authenticate(rawToken) + uid, authz = self.zuulweb.authenticators.authenticate(rawToken) except exceptions.AuthTokenException as e: for header, contents in e.getAdditionalHeaders().items(): cherrypy.response.headers[header] = contents @@ -303,7 +303,7 @@ class ZuulWebAPI(object): # AuthN/AuthZ rawToken = cherrypy.request.headers['Authorization'][len('Bearer '):] try: - uid, authz = self.zuulweb.auths.authenticate(rawToken) + uid, authz = self.zuulweb.authenticators.authenticate(rawToken) except exceptions.AuthTokenException as e: for header, contents in e.getAdditionalHeaders().items(): cherrypy.response.headers[header] = contents @@ -380,7 +380,7 @@ class ZuulWebAPI(object): rawToken = \ cherrypy.request.headers['Authorization'][len('Bearer '):] try: - uid, authz = self.zuulweb.auths.authenticate(rawToken) + uid, authz = self.zuulweb.authenticators.authenticate(rawToken) except exceptions.AuthTokenException as e: for header, contents in e.getAdditionalHeaders().items(): cherrypy.response.headers[header] = contents @@ -967,8 +967,10 @@ class ZuulWeb(object): info=None, static_path=None, zk_hosts=None, + authenticators=None, + authorizations=None, command_socket=None, - auths=None): + ): self.start_time = time.time() self.listen_address = listen_address self.listen_port = listen_port @@ -985,7 +987,8 @@ class ZuulWeb(object): if zk_hosts: self.zk.connect(hosts=zk_hosts, read_only=True) self.connections = connections - self.auths = auths + self.authenticators = authenticators + self.authorizations = authorizations self.stream_manager = StreamManager() self.command_socket = commandsocket.CommandSocket(command_socket) @@ -1019,7 +1022,7 @@ class ZuulWeb(object): route_map.connect('api', '/api/tenant/{tenant}/job/{job_name}', controller=api, action='job') # if no auth configured, deactivate admin routes - if self.auths.authenticators: + if self.authenticators.authenticators: # route order is important, put project actions before the more # generic tenant/{tenant}/project/{project} route route_map.connect( @@ -1158,11 +1161,11 @@ class ZuulWeb(object): if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG) import zuul.lib.connections - import zuul.lib.authenticators + import zuul.lib.auth.authenticators connections = zuul.lib.connections.ConnectionRegistry() - auths = zuul.lib.authenticators.AuthenticatorRegistry() + auths = zuul.lib.auth.authenticators.AuthenticatorRegistry() z = ZuulWeb(listen_address="127.0.0.1", listen_port=9000, gear_server="127.0.0.1", gear_port=4730, - connections=connections, auths=auths) + connections=connections, authenticators=auths) z.start() cherrypy.engine.block()