This repository has been archived on 2020-03-24. You can view files and clone it, but cannot push or open issues or pull requests.
quay/auth/auth.py
Jimmy Zelinskie dee4c389a8 Base sessions on UUIDs.
Now that a backfill has been applied, sessions can now be based on UUIDs
because all users will have one.
2014-11-20 18:44:36 -05:00

215 lines
7.4 KiB
Python

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(), '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 = 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.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 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)
def safe_get(lst, index, default_value):
try:
return lst[index]
except IndexError:
return default_value
token_vals = {val[0]: safe_get(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