import logging from functools import wraps from uuid import UUID from datetime import datetime from flask import request, session from flask.ext.principal import identity_changed, Identity from flask.ext.login import current_user from flask.sessions import SecureCookieSessionInterface, BadSignature from base64 import b64decode import scopes from data import model from data.model import oauth from app import app, authentication from permissions import QuayDeferredPermissionUser from auth_context import (set_authenticated_user, set_validated_token, set_grant_user_context, set_authenticated_user_deferred, set_validated_oauth_token) from util.http import abort logger = logging.getLogger(__name__) SIGNATURE_PREFIX = 'sigv2=' def _load_user_from_cookie(): if not current_user.is_anonymous(): try: # Attempt to parse the user uuid to make sure the cookie has the right value type UUID(current_user.get_id()) except ValueError: return None logger.debug('Loading user from cookie: %s', current_user.get_id()) set_authenticated_user_deferred(current_user.get_id()) loaded = QuayDeferredPermissionUser(current_user.get_id(), 'user_uuid', {scopes.DIRECT_LOGIN}) identity_changed.send(app, identity=loaded) return current_user.db_user() return None def _validate_and_apply_oauth_token(token): validated = oauth.validate_access_token(token) if not validated: logger.warning('OAuth access token could not be validated: %s', token) authenticate_header = { 'WWW-Authenticate': ('Bearer error="invalid_token", ' 'error_description="The access token is invalid"'), } abort(401, message='OAuth access token could not be validated: %(token)s', issue='invalid-oauth-token', token=token, headers=authenticate_header) elif validated.expires_at <= datetime.utcnow(): logger.info('OAuth access with an expired token: %s', token) authenticate_header = { 'WWW-Authenticate': ('Bearer error="invalid_token", ' 'error_description="The access token expired"'), } abort(401, message='OAuth access token has expired: %(token)s', issue='invalid-oauth-token', token=token, headers=authenticate_header) # We have a valid token scope_set = scopes.scopes_from_scope_string(validated.scope) logger.debug('Successfully validated oauth access token: %s with scope: %s', token, scope_set) set_authenticated_user(validated.authorized_user) set_validated_oauth_token(validated) new_identity = QuayDeferredPermissionUser(validated.authorized_user.uuid, 'user_uuid', scope_set) identity_changed.send(app, identity=new_identity) def _process_basic_auth(auth): normalized = [part.strip() for part in auth.split(' ') if part] if normalized[0].lower() != 'basic' or len(normalized) != 2: logger.debug('Invalid basic auth format.') return credentials = [part.decode('utf-8') for part in b64decode(normalized[1]).split(':', 1)] if len(credentials) != 2: logger.debug('Invalid basic auth credential format.') elif credentials[0] == '$token': # Use as token auth try: token = model.load_token_data(credentials[1]) logger.debug('Successfully validated token: %s' % credentials[1]) set_validated_token(token) identity_changed.send(app, identity=Identity(token.code, 'token')) return except model.DataModelException: logger.debug('Invalid token: %s' % credentials[1]) elif credentials[0] == '$oauthtoken': oauth_token = credentials[1] _validate_and_apply_oauth_token(oauth_token) elif '+' in credentials[0]: logger.debug('Trying robot auth with credentials %s' % str(credentials)) # Use as robot auth try: robot = model.verify_robot(credentials[0], credentials[1]) logger.debug('Successfully validated robot: %s' % credentials[0]) set_authenticated_user(robot) deferred_robot = QuayDeferredPermissionUser(robot.uuid, 'user_uuid', {scopes.DIRECT_LOGIN}) identity_changed.send(app, identity=deferred_robot) return except model.InvalidRobotException: logger.debug('Invalid robot or password for robot: %s' % credentials[0]) else: (authenticated, error_message) = authentication.verify_user(credentials[0], credentials[1], basic_auth=True) if authenticated: logger.debug('Successfully validated user: %s' % authenticated.username) set_authenticated_user(authenticated) new_identity = QuayDeferredPermissionUser(authenticated.uuid, 'user_uuid', {scopes.DIRECT_LOGIN}) identity_changed.send(app, identity=new_identity) return # We weren't able to authenticate via basic auth. logger.debug('Basic auth present but could not be validated.') def generate_signed_token(grants, user_context): ser = SecureCookieSessionInterface().get_signing_serializer(app) data_to_sign = { 'grants': grants, 'user_context': user_context, } encrypted = ser.dumps(data_to_sign) return '{0}{1}'.format(SIGNATURE_PREFIX, encrypted) def _process_signed_grant(auth): normalized = [part.strip() for part in auth.split(' ') if part] if normalized[0].lower() != 'token' or len(normalized) != 2: logger.debug('Not a token: %s', auth) return if not normalized[1].startswith(SIGNATURE_PREFIX): logger.debug('Not a signed grant token: %s', auth) return encrypted = normalized[1][len(SIGNATURE_PREFIX):] ser = SecureCookieSessionInterface().get_signing_serializer(app) try: token_data = ser.loads(encrypted, max_age=app.config['SIGNED_GRANT_EXPIRATION_SEC']) except BadSignature: logger.warning('Signed grant could not be validated: %s', encrypted) abort(401, message='Signed grant could not be validated: %(auth)s', issue='invalid-auth-token', auth=auth) logger.debug('Successfully validated signed grant with data: %s', token_data) loaded_identity = Identity(None, 'signed_grant') set_grant_user_context(token_data['user_context']) loaded_identity.provides.update(token_data['grants']) identity_changed.send(app, identity=loaded_identity) def process_oauth(func): @wraps(func) def wrapper(*args, **kwargs): auth = request.headers.get('authorization', '') if auth: normalized = [part.strip() for part in auth.split(' ') if part] if normalized[0].lower() != 'bearer' or len(normalized) != 2: logger.debug('Invalid oauth bearer token format.') return token = normalized[1] _validate_and_apply_oauth_token(token) elif _load_user_from_cookie() is None: logger.debug('No auth header or login cookie.') return func(*args, **kwargs) return wrapper def process_auth(func): @wraps(func) def wrapper(*args, **kwargs): auth = request.headers.get('authorization', '') if auth: logger.debug('Validating auth header: %s' % auth) _process_signed_grant(auth) _process_basic_auth(auth) else: logger.debug('No auth header.') return func(*args, **kwargs) return wrapper def require_session_login(func): @wraps(func) def wrapper(*args, **kwargs): loaded = _load_user_from_cookie() if loaded is None or loaded.organization: abort(401, message='Method requires login and no valid login could be loaded.') return func(*args, **kwargs) return wrapper def extract_namespace_repo_from_session(func): @wraps(func) def wrapper(*args, **kwargs): if 'namespace' not in session or 'repository' not in session: logger.error('Unable to load namespace or repository from session: %s' % session) abort(400, message='Missing namespace in request') return func(session['namespace'], session['repository'], *args, **kwargs) return wrapper