Add Authorization Rules configuration
Allow an operator to define authorization rules. Allow an operator to add authorization rules to a tenant. Add a rule parser and a rule registry. The authZ engine is not plugged in yet. Change-Id: I3a86c6c7d62ad2bce68a98dbd2fff18549b94fb9
This commit is contained in:
parent
9d86c00111
commit
7a622a5823
|
@ -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
|
||||
|
|
|
@ -28,3 +28,4 @@ paho-mqtt
|
|||
cherrypy
|
||||
ws4py
|
||||
routes
|
||||
jsonpath-rw
|
||||
|
|
|
@ -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', []):
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1,2 @@
|
|||
- hosts: all
|
||||
tasks: []
|
|
@ -0,0 +1,2 @@
|
|||
- hosts: all
|
||||
tasks: []
|
|
@ -0,0 +1,2 @@
|
|||
- hosts: all
|
||||
tasks: []
|
|
@ -0,0 +1,2 @@
|
|||
- hosts: all
|
||||
tasks: []
|
|
@ -0,0 +1,2 @@
|
|||
- hosts: all
|
||||
tasks: []
|
|
@ -0,0 +1,2 @@
|
|||
- hosts: all
|
||||
tasks: []
|
|
@ -0,0 +1,2 @@
|
|||
- hosts: all
|
||||
tasks: []
|
|
@ -0,0 +1,2 @@
|
|||
- hosts: all
|
||||
tasks: []
|
|
@ -0,0 +1,2 @@
|
|||
- hosts: all
|
||||
tasks: []
|
197
tests/fixtures/config/authorization/single-tenant/git/common-config/zuul.yaml
vendored
Normal file
197
tests/fixtures/config/authorization/single-tenant/git/common-config/zuul.yaml
vendored
Normal file
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
test
|
|
@ -0,0 +1 @@
|
|||
test
|
|
@ -0,0 +1 @@
|
|||
test
|
|
@ -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
|
|
@ -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
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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:")
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
118
zuul/model.py
118
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 '<ClaimRule "%s":"%s">' % (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 '<OrRule %s>' % (' || '.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 '<AndRule %s>' % (' && '.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 '<AuthZRuleTree [ %s ]>' % self.ruletree
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue