import logging from functools import wraps 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 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_authenticated_user_deferred, set_validated_oauth_token) from util.http import abort logger = logging.getLogger(__name__) def _load_user_from_cookie(): if not current_user.is_anonymous(): 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(), 'username', {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.username, 'username', 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.username, 'username', {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 = authentication.verify_user(credentials[0], credentials[1]) if authenticated: logger.debug('Successfully validated user: %s' % authenticated.username) set_authenticated_user(authenticated) new_identity = QuayDeferredPermissionUser(authenticated.username, 'username', {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 process_token(auth): normalized = [part.strip() for part in auth.split(' ') if part] if normalized[0].lower() != 'token' or len(normalized) != 2: logger.debug('Not an auth token: %s' % auth) return token_details = normalized[1].split(',') if len(token_details) != 1: logger.warning('Invalid token format: %s' % auth) abort(401, message='Invalid token format: %(auth)s', issue='invalid-auth-token', auth=auth) token_vals = {val[0]: val[1] for val in (detail.split('=') for detail in token_details)} if 'signature' not in token_vals: logger.warning('Token does not contain signature: %s' % auth) abort(401, message='Token does not contain a valid signature: %(auth)s', issue='invalid-auth-token', auth=auth) try: token_data = model.load_token_data(token_vals['signature']) except model.InvalidTokenException: logger.warning('Token could not be validated: %s', token_vals['signature']) abort(401, message='Token could not be validated: %(auth)s', issue='invalid-auth-token', auth=auth) logger.debug('Successfully validated token: %s', token_data.code) set_validated_token(token_data) identity_changed.send(app, identity=Identity(token_data.code, 'token')) 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_token(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