Merge "authentication config: add optional max_validity_time, skew"

This commit is contained in:
Zuul 2019-12-20 19:41:10 +00:00 committed by Gerrit Code Review
commit 66a3e86f6a
7 changed files with 236 additions and 3 deletions

View File

@ -1005,6 +1005,13 @@ protected endpoints and configure JWT validation:
of time in seconds. This is useful if the Zuul operator has no control
over the service issueing JWTs, and the tokens are too long-lived.
.. attr:: skew
:default: 0
Optional integer value to compensate for skew between Zuul's and the
JWT emitter's respective clocks. Use a negative value if Zuul's clock
is running behind.
This section can be repeated as needed with different authenticators, allowing
access to privileged API actions from several JWT issuers.

View File

@ -501,6 +501,8 @@ Here is an example defining the three supported types of authenticators:
realm=openstack
# (optional) Ensure a Token cannot be valid for longer than this amount of time, in seconds
max_validity_time = 1800000
# (optional) Account for skew between clocks, in seconds
skew = 3
# asymmetrical encryption
[auth "my_oidc_idp"]
@ -517,6 +519,8 @@ Here is an example defining the three supported types of authenticators:
realm=openstack
# (optional) Ensure a Token cannot be valid for longer than this amount of time, in seconds
max_validity_time = 1800000
# (optional) Account for skew between clocks, in seconds
skew = 3
# asymmetrical encryption using JWKS for validation
# The signing secret being known to the Identity Provider only, this
@ -532,7 +536,8 @@ Here is an example defining the three supported types of authenticators:
uid_claim=name
# Auth realm, used in 401 error messages
realm=openstack
# (optional) Account for skew between clocks, in seconds
skew = 3
Implementation
==============

View File

@ -52,6 +52,8 @@ default=true
client_id=zuul.example.com
issuer_id=zuul_operator
secret=NoDanaOnlyZuul
max_validity_time=36000
skew=0
[connection gerrit]
driver=gerrit

View File

@ -0,0 +1,32 @@
[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=true
realm=zuul.example.com
client_id=zuul.example.com
issuer_id=zuul_operator
secret=NoDanaOnlyZuul
max_validity_time=5

View File

@ -1716,6 +1716,162 @@ class TestTenantScopedWebApiWithAuthRules(BaseTestWeb):
"%s got %s" % (authz['sub'], data))
class TestTenantScopedWebApiTokenWithExpiry(BaseTestWeb):
config_file = 'zuul-admin-web-token-expiry.conf'
def test_iat_claim_mandatory(self):
"""Test that the 'iat' claim is mandatory when
max_validity_time is set"""
authz = {'iss': 'zuul_operator',
'sub': 'testuser',
'aud': 'zuul.example.com',
'zuul': {
'admin': ['tenant-one', ]
},
'exp': time.time() + 3600}
token = jwt.encode(authz, key='NoDanaOnlyZuul',
algorithm='HS256').decode('utf-8')
resp = self.post_url(
"api/tenant/tenant-one/project/org/project/autohold",
headers={'Authorization': 'Bearer %s' % token},
json={'job': 'project-test1',
'count': 1,
'reason': 'because',
'node_hold_expiration': 36000})
self.assertEqual(401, resp.status_code)
resp = self.post_url(
"api/tenant/tenant-one/project/org/project/enqueue",
headers={'Authorization': 'Bearer %s' % token},
json={'trigger': 'gerrit',
'change': '2,1',
'pipeline': 'check'})
self.assertEqual(401, resp.status_code)
resp = self.post_url(
"api/tenant/tenant-one/project/org/project/enqueue",
headers={'Authorization': 'Bearer %s' % token},
json={'trigger': 'gerrit',
'ref': 'abcd',
'newrev': 'aaaa',
'oldrev': 'bbbb',
'pipeline': 'check'})
self.assertEqual(401, resp.status_code)
def test_token_from_the_future(self):
authz = {'iss': 'zuul_operator',
'sub': 'testuser',
'aud': 'zuul.example.com',
'zuul': {
'admin': ['tenant-one', ],
},
'exp': time.time() + 7200,
'iat': time.time() + 3600}
token = jwt.encode(authz, key='NoDanaOnlyZuul',
algorithm='HS256').decode('utf-8')
resp = self.post_url(
"api/tenant/tenant-one/project/org/project/autohold",
headers={'Authorization': 'Bearer %s' % token},
json={'job': 'project-test1',
'count': 1,
'reason': 'because',
'node_hold_expiration': 36000})
self.assertEqual(401, resp.status_code)
resp = self.post_url(
"api/tenant/tenant-one/project/org/project/enqueue",
headers={'Authorization': 'Bearer %s' % token},
json={'trigger': 'gerrit',
'change': '2,1',
'pipeline': 'check'})
self.assertEqual(401, resp.status_code)
resp = self.post_url(
"api/tenant/tenant-one/project/org/project/enqueue",
headers={'Authorization': 'Bearer %s' % token},
json={'trigger': 'gerrit',
'ref': 'abcd',
'newrev': 'aaaa',
'oldrev': 'bbbb',
'pipeline': 'check'})
self.assertEqual(401, resp.status_code)
def test_token_expired(self):
authz = {'iss': 'zuul_operator',
'sub': 'testuser',
'aud': 'zuul.example.com',
'zuul': {
'admin': ['tenant-one', ],
},
'exp': time.time() + 3600,
'iat': time.time()}
token = jwt.encode(authz, key='NoDanaOnlyZuul',
algorithm='HS256').decode('utf-8')
time.sleep(10)
resp = self.post_url(
"api/tenant/tenant-one/project/org/project/autohold",
headers={'Authorization': 'Bearer %s' % token},
json={'job': 'project-test1',
'count': 1,
'reason': 'because',
'node_hold_expiration': 36000})
self.assertEqual(401, resp.status_code)
resp = self.post_url(
"api/tenant/tenant-one/project/org/project/enqueue",
headers={'Authorization': 'Bearer %s' % token},
json={'trigger': 'gerrit',
'change': '2,1',
'pipeline': 'check'})
self.assertEqual(401, resp.status_code)
resp = self.post_url(
"api/tenant/tenant-one/project/org/project/enqueue",
headers={'Authorization': 'Bearer %s' % token},
json={'trigger': 'gerrit',
'ref': 'abcd',
'newrev': 'aaaa',
'oldrev': 'bbbb',
'pipeline': 'check'})
self.assertEqual(401, resp.status_code)
def test_autohold(self):
"""Test that autohold can be set through the admin web interface"""
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,
'iat': time.time()}
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(200, req.status_code, req.text)
data = req.json()
self.assertEqual(True, data)
# Check result in rpc client
client = zuul.rpcclient.RPCClient('127.0.0.1',
self.gearman_server.port)
self.addCleanup(client.shutdown)
autohold_requests = client.autohold_list()
self.assertNotEqual([], autohold_requests)
self.assertEqual(1, len(autohold_requests))
ah_request = autohold_requests[0]
self.assertEqual('tenant-one', ah_request['tenant'])
self.assertIn('org/project', ah_request['project'])
self.assertEqual('project-test2', ah_request['job'])
self.assertEqual(".*", ah_request['ref_filter'])
self.assertEqual("some reason", ah_request['reason'])
class TestWebMulti(BaseTestWeb):
config_file = 'zuul-gerrit-github.conf'

View File

@ -540,7 +540,9 @@ class Client(zuul.cmd.ZuulApp):
print('"%s" authenticator configuration not found.'
% self.args.auth_config)
sys.exit(1)
token = {'exp': time.time() + self.args.expires_in,
now = time.time()
token = {'iat': now,
'exp': now + self.args.expires_in,
'iss': get_default(self.config, auth_section, 'issuer_id'),
'aud': get_default(self.config, auth_section, 'client_id'),
'sub': self.args.user,

View File

@ -14,6 +14,7 @@
# under the License.
import logging
import math
import time
import jwt
import requests
@ -36,11 +37,21 @@ class JWTAuthenticator(AuthenticatorInterface):
self.audience = conf.get('client_id')
self.realm = conf.get('realm')
self.allow_authz_override = conf.get('allow_authz_override', False)
try:
self.skew = int(conf.get('skew', 0))
except Exception:
raise ValueError(
'skew must be an integer, got %s' % conf.get('skew'))
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
try:
self.max_validity_time = float(conf.get('max_validity_time',
math.inf))
except ValueError:
raise ValueError('"max_validity_time" must be a numerical value')
def _decode(self, rawToken):
raise NotImplementedError
@ -68,13 +79,31 @@ class JWTAuthenticator(AuthenticatorInterface):
raise exceptions.AuthTokenUnauthorizedException(
realm=self.realm,
msg=e)
# Missing claim tests
if not all(x in decoded for x in ['aud', 'iss', 'exp', 'sub']):
raise exceptions.MissingClaimError(realm=self.realm)
if self.max_validity_time < math.inf and 'iat' not in decoded:
raise exceptions.MissingClaimError(
msg='Missing "iat" claim',
realm=self.realm)
if self.uid_claim not in decoded:
raise exceptions.MissingUIDClaimError(realm=self.realm)
# Time related tests
expires = decoded.get('exp', 0)
if expires < time.time():
issued_at = decoded.get('iat', 0)
now = time.time()
if issued_at + self.skew > now:
raise exceptions.AuthTokenUnauthorizedException(
msg='"iat" claim set in the future',
realm=self.realm
)
if now - issued_at > self.max_validity_time:
raise exceptions.TokenExpiredError(
msg='Token was issued too long ago',
realm=self.realm)
if expires + self.skew < now:
raise exceptions.TokenExpiredError(realm=self.realm)
# Zuul-specific claims tests
zuul_claims = decoded.get('zuul', {})
admin_tenants = zuul_claims.get('admin', [])
if not isinstance(admin_tenants, list):