bc29561f8f
Make it explicit that the registry-v2 stuff is not ready for prime time.
110 lines
3.7 KiB
Python
110 lines
3.7 KiB
Python
import logging
|
|
import jwt
|
|
import re
|
|
|
|
from datetime import datetime, timedelta
|
|
from functools import wraps
|
|
from flask import request
|
|
from flask.ext.principal import identity_changed, Identity
|
|
from cryptography.x509 import load_pem_x509_certificate
|
|
from cryptography.hazmat.backends import default_backend
|
|
from cachetools import lru_cache
|
|
|
|
from app import app
|
|
from auth_context import set_grant_user_context
|
|
from permissions import repository_read_grant, repository_write_grant
|
|
from util.names import parse_namespace_repository
|
|
from util.http import abort
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
TOKEN_REGEX = re.compile(r'Bearer (([a-zA-Z0-9+/]+\.)+[a-zA-Z0-9+-_/]+)')
|
|
|
|
|
|
class InvalidJWTException(Exception):
|
|
pass
|
|
|
|
|
|
def identity_from_bearer_token(bearer_token, max_signed_s, public_key):
|
|
""" Process a bearer token and return the loaded identity, or raise InvalidJWTException if an
|
|
identity could not be loaded. Expects tokens and grants in the format of the Docker registry
|
|
v2 auth spec: https://docs.docker.com/registry/spec/auth/token/
|
|
"""
|
|
logger.debug('Validating auth header: %s', bearer_token)
|
|
|
|
# Extract the jwt token from the header
|
|
match = TOKEN_REGEX.match(bearer_token)
|
|
if match is None or match.end() != len(bearer_token):
|
|
raise InvalidJWTException('Invalid bearer token format')
|
|
|
|
encoded = match.group(1)
|
|
logger.debug('encoded JWT: %s', encoded)
|
|
|
|
# Load the JWT returned.
|
|
try:
|
|
payload = jwt.decode(encoded, public_key, algorithms=['RS256'], audience='quay',
|
|
issuer='token-issuer')
|
|
except jwt.InvalidTokenError:
|
|
raise InvalidJWTException('Invalid token')
|
|
|
|
if not 'sub' in payload:
|
|
raise InvalidJWTException('Missing sub field in JWT')
|
|
|
|
if not 'exp' in payload:
|
|
raise InvalidJWTException('Missing exp 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
|
|
|
|
namespace, repo_name = parse_namespace_repository(grant['name'])
|
|
|
|
if 'push' in grant['actions']:
|
|
loaded_identity.provides.add(repository_write_grant(namespace, repo_name))
|
|
elif 'pull' in grant['actions']:
|
|
loaded_identity.provides.add(repository_read_grant(namespace, repo_name))
|
|
|
|
return loaded_identity
|
|
|
|
|
|
@lru_cache(maxsize=1)
|
|
def load_public_key(certificate_file_path):
|
|
with open(certificate_file_path) as cert_file:
|
|
cert_obj = load_pem_x509_certificate(cert_file.read(), default_backend())
|
|
return cert_obj.public_key()
|
|
|
|
|
|
def process_jwt_auth(func):
|
|
@wraps(func)
|
|
def wrapper(*args, **kwargs):
|
|
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)
|
|
certificate_file_path = app.config['JWT_AUTH_CERTIFICATE_PATH']
|
|
public_key = load_public_key(certificate_file_path)
|
|
|
|
try:
|
|
extracted_identity = identity_from_bearer_token(auth, max_signature_seconds, public_key)
|
|
identity_changed.send(app, identity=extracted_identity)
|
|
set_grant_user_context(extracted_identity.id)
|
|
logger.debug('Identity changed to %s', extracted_identity.id)
|
|
except InvalidJWTException as ije:
|
|
abort(401, message=ije.message)
|
|
|
|
else:
|
|
logger.debug('No auth header.')
|
|
|
|
return func(*args, **kwargs)
|
|
return wrapper
|