parent
5029ab62f6
commit
9c3ddf846f
7 changed files with 290 additions and 35 deletions
|
@ -1,7 +1,7 @@
|
|||
import logging
|
||||
import re
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from jsonschema import validate, ValidationError
|
||||
from functools import wraps
|
||||
from flask import request
|
||||
from flask.ext.principal import identity_changed, Identity
|
||||
|
@ -20,7 +20,45 @@ from util.security import strictjwt
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
TOKEN_REGEX = re.compile(r'Bearer (([a-zA-Z0-9+/]+\.)+[a-zA-Z0-9+-_/]+)')
|
||||
TOKEN_REGEX = re.compile(r'^Bearer (([a-zA-Z0-9+/]+\.)+[a-zA-Z0-9+-_/]+)$')
|
||||
|
||||
|
||||
ACCESS_SCHEMA = {
|
||||
'type': 'array',
|
||||
'description': 'List of access granted to the subject',
|
||||
'items': {
|
||||
'type': 'object',
|
||||
'required': [
|
||||
'type',
|
||||
'name',
|
||||
'actions',
|
||||
],
|
||||
'properties': {
|
||||
'type': {
|
||||
'type': 'string',
|
||||
'description': 'We only allow repository permissions',
|
||||
'enum': [
|
||||
'repository',
|
||||
],
|
||||
},
|
||||
'name': {
|
||||
'type': 'string',
|
||||
'description': 'The name of the repository for which we are receiving access'
|
||||
},
|
||||
'actions': {
|
||||
'type': 'array',
|
||||
'description': 'List of specific verbs which can be performed against repository',
|
||||
'items': {
|
||||
'type': 'string',
|
||||
'enum': [
|
||||
'push',
|
||||
'pull',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class InvalidJWTException(Exception):
|
||||
|
@ -36,7 +74,7 @@ def identity_from_bearer_token(bearer_token, max_signed_s, public_key):
|
|||
|
||||
# Extract the jwt token from the header
|
||||
match = TOKEN_REGEX.match(bearer_token)
|
||||
if match is None or match.end() != len(bearer_token):
|
||||
if match is None:
|
||||
raise InvalidJWTException('Invalid bearer token format')
|
||||
|
||||
encoded = match.group(1)
|
||||
|
@ -44,27 +82,31 @@ def identity_from_bearer_token(bearer_token, max_signed_s, public_key):
|
|||
|
||||
# Load the JWT returned.
|
||||
try:
|
||||
payload = strictjwt.decode(encoded, public_key, algorithms=['RS256'], audience='quay',
|
||||
issuer='token-issuer')
|
||||
expected_issuer = app.config['JWT_AUTH_TOKEN_ISSUER']
|
||||
audience = app.config['SERVER_HOSTNAME']
|
||||
max_exp = strictjwt.exp_max_s_option(max_signed_s)
|
||||
payload = strictjwt.decode(encoded, public_key, algorithms=['RS256'], audience=audience,
|
||||
issuer=expected_issuer, options=max_exp)
|
||||
except strictjwt.InvalidTokenError:
|
||||
logger.exception('Invalid token reason')
|
||||
raise InvalidJWTException('Invalid token')
|
||||
|
||||
if not 'sub' in payload:
|
||||
raise InvalidJWTException('Missing sub field in JWT')
|
||||
|
||||
# Verify that the expiration is no more than 300 seconds in the future.
|
||||
if datetime.fromtimestamp(payload['exp']) > datetime.utcnow() + timedelta(seconds=max_signed_s):
|
||||
raise InvalidJWTException('Token was signed for more than %s seconds' % max_signed_s)
|
||||
|
||||
username = payload['sub']
|
||||
loaded_identity = Identity(username, 'signed_jwt')
|
||||
|
||||
# Process the grants from the payload
|
||||
if 'access' in payload:
|
||||
for grant in payload['access']:
|
||||
if grant['type'] != 'repository':
|
||||
continue
|
||||
|
||||
if 'access' in payload:
|
||||
try:
|
||||
validate(payload['access'], ACCESS_SCHEMA)
|
||||
except ValidationError:
|
||||
logger.exception('We should not be minting invalid credentials')
|
||||
raise InvalidJWTException('Token contained invalid or malformed access grants')
|
||||
|
||||
for grant in payload['access']:
|
||||
namespace, repo_name = parse_namespace_repository(grant['name'])
|
||||
|
||||
if 'push' in grant['actions']:
|
||||
|
@ -88,7 +130,7 @@ def process_jwt_auth(func):
|
|||
logger.debug('Called with params: %s, %s', args, kwargs)
|
||||
auth = request.headers.get('authorization', '').strip()
|
||||
if auth:
|
||||
max_signature_seconds = app.config.get('JWT_AUTH_MAX_FRESH_S', 300)
|
||||
max_signature_seconds = app.config.get('JWT_AUTH_MAX_FRESH_S', 3660)
|
||||
certificate_file_path = app.config['JWT_AUTH_CERTIFICATE_PATH']
|
||||
public_key = load_public_key(certificate_file_path)
|
||||
|
||||
|
|
Reference in a new issue