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/process.py
Joseph Schorr 4b926ae189 Add new metrics as requested by some customers
Note that the `status` field on the pull and push metrics will eventually be set to False for failed pulls and pushes in a followup PR
2016-11-03 15:28:40 -04:00

292 lines
9.5 KiB
Python

import logging
from functools import wraps
from uuid import UUID
from datetime import datetime
from base64 import b64decode
from flask import request, session
from flask.sessions import SecureCookieSessionInterface, BadSignature
from flask_login import current_user
from flask_principal import identity_changed, Identity
import scopes
from app import app, authentication, metric_queue
from auth_context import (set_authenticated_user, set_validated_token, set_grant_context,
set_validated_oauth_token)
from data import model
from endpoints.exception import InvalidToken, ExpiredToken
from permissions import QuayDeferredPermissionUser
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())
db_user = current_user.db_user()
if db_user is not None:
# Don't allow disabled users to login.
if not db_user.enabled:
return None
set_authenticated_user(db_user)
loaded = QuayDeferredPermissionUser.for_user(db_user)
identity_changed.send(app, identity=loaded)
return db_user
return None
def _validate_and_apply_oauth_token(token):
validated = model.oauth.validate_access_token(token)
if not validated:
logger.warning('OAuth access token could not be validated: %s', token)
metric_queue.authentication_count.Inc(labelvalues=['oauth', False])
raise InvalidToken('OAuth access token could not be validated: {token}'.format(token=token))
elif validated.expires_at <= datetime.utcnow():
logger.info('OAuth access with an expired token: %s', token)
metric_queue.authentication_count.Inc(labelvalues=['oauth', False])
raise ExpiredToken('OAuth access token has expired: {token}'.format(token=token))
# Don't allow disabled users to login.
if not validated.authorized_user.enabled:
metric_queue.authentication_count.Inc(labelvalues=['oauth', False])
return False
# 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.for_user(validated.authorized_user, scope_set)
identity_changed.send(app, identity=new_identity)
metric_queue.authentication_count.Inc(labelvalues=['oauth', True])
return True
def _parse_basic_auth_header(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 None
logger.debug('Found basic auth header: %s', auth)
try:
credentials = [part.decode('utf-8') for part in b64decode(normalized[1]).split(':', 1)]
except TypeError:
logger.exception('Exception when parsing basic auth header')
return None
if len(credentials) != 2:
logger.debug('Invalid basic auth credential format.')
return None
return credentials
def _process_basic_auth(auth):
credentials = _parse_basic_auth_header(auth)
if credentials is None:
return
if credentials[0] == '$token':
# Use as token auth
try:
token = model.token.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'))
metric_queue.authentication_count.Inc(labelvalues=['token', True])
return True
except model.DataModelException:
logger.debug('Invalid token: %s', credentials[1])
metric_queue.authentication_count.Inc(labelvalues=['token', False])
elif credentials[0] == '$oauthtoken':
oauth_token = credentials[1]
return _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.user.verify_robot(credentials[0], credentials[1])
logger.debug('Successfully validated robot: %s', credentials[0])
set_authenticated_user(robot)
deferred_robot = QuayDeferredPermissionUser.for_user(robot)
identity_changed.send(app, identity=deferred_robot)
metric_queue.authentication_count.Inc(labelvalues=['robot', True])
return True
except model.InvalidRobotException:
logger.debug('Invalid robot or password for robot: %s', credentials[0])
metric_queue.authentication_count.Inc(labelvalues=['robot', False])
else:
(authenticated, _) = authentication.verify_and_link_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.for_user(authenticated)
identity_changed.send(app, identity=new_identity)
metric_queue.authentication_count.Inc(labelvalues=['user', True])
return True
else:
metric_queue.authentication_count.Inc(labelvalues=['user', False])
# We weren't able to authenticate via basic auth.
logger.debug('Basic auth present but could not be validated.')
return False
def has_basic_auth(username):
auth = request.headers.get('authorization', '')
if not auth:
return False
credentials = _parse_basic_auth_header(auth)
if not credentials:
return False
(authenticated, _) = authentication.verify_and_link_user(credentials[0], credentials[1],
basic_auth=True)
if not authenticated:
return False
return authenticated.username == username
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 False
if not normalized[1].startswith(SIGNATURE_PREFIX):
logger.debug('Not a signed grant token: %s', auth)
return False
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)
metric_queue.authentication_count.Inc(labelvalues=['signed', False])
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')
if token_data['user_context']:
set_grant_context({
'user': token_data['user_context'],
'kind': 'user',
})
loaded_identity.provides.update(token_data['grants'])
identity_changed.send(app, identity=loaded_identity)
metric_queue.authentication_count.Inc(labelvalues=['signed', True])
return True
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 func(*args, **kwargs)
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 process_auth_or_cookie(func):
@wraps(func)
def wrapper(*args, **kwargs):
auth = request.headers.get('authorization', '')
if auth:
logger.debug('Validating auth header: %s', auth)
_process_basic_auth(auth)
else:
logger.debug('No auth header.')
_load_user_from_cookie()
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