Merge pull request #1361 from ecordell/application-problem-json

Return application/problem+json format errors
This commit is contained in:
Evan Cordell 2016-04-12 17:25:14 -04:00
commit 29eb0304e5
30 changed files with 309 additions and 141 deletions

View file

@ -13,6 +13,7 @@ import scopes
from data import model from data import model
from app import app, authentication from app import app, authentication
from endpoints.exception import InvalidToken, ExpiredToken
from permissions import QuayDeferredPermissionUser from permissions import QuayDeferredPermissionUser
from auth_context import (set_authenticated_user, set_validated_token, set_grant_context, from auth_context import (set_authenticated_user, set_validated_token, set_grant_context,
set_validated_oauth_token) set_validated_oauth_token)
@ -50,20 +51,10 @@ def _validate_and_apply_oauth_token(token):
validated = model.oauth.validate_access_token(token) validated = model.oauth.validate_access_token(token)
if not validated: if not validated:
logger.warning('OAuth access token could not be validated: %s', token) logger.warning('OAuth access token could not be validated: %s', token)
authenticate_header = { raise InvalidToken('OAuth access token could not be validated: {token}'.format(token=token))
'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(): elif validated.expires_at <= datetime.utcnow():
logger.info('OAuth access with an expired token: %s', token) logger.info('OAuth access with an expired token: %s', token)
authenticate_header = { raise ExpiredToken('OAuth access token has expired: {token}'.format(token=token))
'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)
# Don't allow disabled users to login. # Don't allow disabled users to login.
if not validated.authorized_user.enabled: if not validated.authorized_user.enabled:

View file

@ -16,7 +16,7 @@ from buildtrigger.basehandler import BuildTriggerHandler
from util.security.ssh import generate_ssh_keypair from util.security.ssh import generate_ssh_keypair
from util.dict_wrappers import JSONPathDict, SafeDictSetter from util.dict_wrappers import JSONPathDict, SafeDictSetter
from endpoints.api import ExternalServiceTimeout from endpoints.exception import ExternalServiceTimeout
import gitlab import gitlab
import requests import requests

View file

@ -1,9 +1,10 @@
import logging import logging
import datetime import datetime
import json import json
from enum import Enum
from app import app, metric_queue from app import app, metric_queue
from flask import Blueprint, request, make_response, jsonify, session from flask import Blueprint, Response, request, make_response, jsonify, session, url_for
from flask.ext.restful import Resource, abort, Api, reqparse from flask.ext.restful import Resource, abort, Api, reqparse
from flask.ext.restful.utils.cors import crossdomain from flask.ext.restful.utils.cors import crossdomain
from calendar import timegm from calendar import timegm
@ -20,6 +21,7 @@ from auth import scopes
from auth.auth_context import get_authenticated_user, get_validated_oauth_token from auth.auth_context import get_authenticated_user, get_validated_oauth_token
from auth.auth import process_oauth from auth.auth import process_oauth
from endpoints.csrf import csrf_protect from endpoints.csrf import csrf_protect
from endpoints.exception import ApiException, Unauthorized, InvalidRequest, InvalidResponse
from endpoints.decorators import check_anon_protection from endpoints.decorators import check_anon_protection
from util.saas.metricqueue import time_decorator from util.saas.metricqueue import time_decorator
from util.pagination import encrypt_page_token, decrypt_page_token from util.pagination import encrypt_page_token, decrypt_page_token
@ -33,84 +35,15 @@ api.decorators = [csrf_protect,
process_oauth, time_decorator(api_bp.name, metric_queue)] process_oauth, time_decorator(api_bp.name, metric_queue)]
class ApiException(Exception):
def __init__(self, error_type, status_code, error_description, payload=None):
Exception.__init__(self)
self.error_description = error_description
self.status_code = status_code
self.payload = payload
self.error_type = error_type
def to_dict(self):
rv = dict(self.payload or ())
if self.error_description is not None:
rv['error_description'] = self.error_description
rv['error_type'] = self.error_type
return rv
class ExternalServiceTimeout(ApiException):
def __init__(self, error_description, payload=None):
ApiException.__init__(self, 'external_service_timeout', 520, error_description, payload)
class InvalidRequest(ApiException):
def __init__(self, error_description, payload=None):
ApiException.__init__(self, 'invalid_request', 400, error_description, payload)
class InvalidResponse(ApiException):
def __init__(self, error_description, payload=None):
ApiException.__init__(self, 'invalid_response', 400, error_description, payload)
class InvalidToken(ApiException):
def __init__(self, error_description, payload=None):
ApiException.__init__(self, 'invalid_token', 401, error_description, payload)
class Unauthorized(ApiException):
def __init__(self, payload=None):
user = get_authenticated_user()
if user is None or user.organization:
ApiException.__init__(self, 'invalid_token', 401, "Requires authentication", payload)
else:
ApiException.__init__(self, 'insufficient_scope', 403, 'Unauthorized', payload)
class FreshLoginRequired(ApiException):
def __init__(self, payload=None):
ApiException.__init__(self, 'fresh_login_required', 401, "Requires fresh login", payload)
class ExceedsLicenseException(ApiException):
def __init__(self, payload=None):
ApiException.__init__(self, None, 402, 'Payment Required', payload)
class NotFound(ApiException):
def __init__(self, payload=None):
ApiException.__init__(self, None, 404, 'Not Found', payload)
class DownstreamIssue(ApiException):
def __init__(self, payload=None):
ApiException.__init__(self, None, 520, 'Downstream Issue', payload)
@api_bp.app_errorhandler(ApiException) @api_bp.app_errorhandler(ApiException)
@crossdomain(origin='*', headers=['Authorization', 'Content-Type']) @crossdomain(origin='*', headers=['Authorization', 'Content-Type'])
def handle_api_error(error): def handle_api_error(error):
response = jsonify(error.to_dict()) response = Response(json.dumps(error.to_dict()), error.status_code, mimetype='application/problem+json')
response.status_code = error.status_code if error.status_code is 401:
if error.error_type is not None:
response.headers['WWW-Authenticate'] = ('Bearer error="%s" error_description="%s"' % response.headers['WWW-Authenticate'] = ('Bearer error="%s" error_description="%s"' %
(error.error_type, error.error_description)) (error.error_type, error.error_description))
return response return response
def resource(*urls, **kwargs): def resource(*urls, **kwargs):
def wrapper(api_resource): def wrapper(api_resource):
if not api_resource: if not api_resource:
@ -383,6 +316,7 @@ def request_error(exception=None, **kwargs):
message = 'Request error.' message = 'Request error.'
if exception: if exception:
message = exception.message message = exception.message
message = data.pop('message', message)
raise InvalidRequest(message, data) raise InvalidRequest(message, data)
@ -427,6 +361,7 @@ def define_json_response(schema_name):
import endpoints.api.billing import endpoints.api.billing
import endpoints.api.build import endpoints.api.build
import endpoints.api.discovery import endpoints.api.discovery
import endpoints.api.error
import endpoints.api.image import endpoints.api.image
import endpoints.api.logs import endpoints.api.logs
import endpoints.api.organization import endpoints.api.organization

View file

@ -5,8 +5,9 @@ import stripe
from flask import request from flask import request
from app import billing from app import billing
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, log_action, from endpoints.api import (resource, nickname, ApiResource, validate_json_request, log_action,
related_user_resource, internal_only, Unauthorized, NotFound, related_user_resource, internal_only, require_user_admin, show_if,
require_user_admin, show_if, path_param, require_scope, abort) path_param, require_scope, abort)
from endpoints.exception import Unauthorized, NotFound
from endpoints.api.subscribe import subscribe, subscription_view from endpoints.api.subscribe import subscribe, subscription_view
from auth.permissions import AdministerOrganizationPermission from auth.permissions import AdministerOrganizationPermission
from auth.auth_context import get_authenticated_user from auth.auth_context import get_authenticated_user

View file

@ -12,8 +12,9 @@ from app import app, userfiles as user_files, build_logs, log_archive, dockerfil
from buildtrigger.basehandler import BuildTriggerHandler from buildtrigger.basehandler import BuildTriggerHandler
from endpoints.api import (RepositoryParamResource, parse_args, query_param, nickname, resource, from endpoints.api import (RepositoryParamResource, parse_args, query_param, nickname, resource,
require_repo_read, require_repo_write, validate_json_request, require_repo_read, require_repo_write, validate_json_request,
ApiResource, internal_only, format_date, api, Unauthorized, NotFound, ApiResource, internal_only, format_date, api, path_param,
path_param, InvalidRequest, require_repo_admin) require_repo_admin)
from endpoints.exception import Unauthorized, NotFound, InvalidRequest
from endpoints.building import start_build, PreparedBuild from endpoints.building import start_build, PreparedBuild
from data import database from data import database
from data import model from data import model

View file

@ -3,7 +3,6 @@
import re import re
import logging import logging
import sys import sys
import copy
from flask.ext.restful import reqparse from flask.ext.restful import reqparse
@ -180,24 +179,54 @@ def swagger_route_data(include_internal=False, compact=False):
if response_schema_name: if response_schema_name:
models[response_schema_name] = view_class.schemas[response_schema_name] models[response_schema_name] = view_class.schemas[response_schema_name]
models['ApiError'] = {
'type': 'object',
'properties': {
'status': {
'type': 'integer',
'description': 'Status code of the response.'
},
'type': {
'type': 'string',
'description': 'Reference to the type of the error.'
},
'detail': {
'type': 'string',
'description': 'Details about the specific instance of the error.'
},
'title': {
'type': 'string',
'description': 'Unique error code to identify the type of error.'
}
},
'required': [
'status',
'type',
'title',
]
}
responses = { responses = {
'400': { '400': {
'description': 'Bad Request' 'description': 'Bad Request',
}, },
'401': { '401': {
'description': 'Session required' 'description': 'Session required',
}, },
'403': { '403': {
'description': 'Unauthorized access' 'description': 'Unauthorized access',
}, },
'404': { '404': {
'description': 'Not found' 'description': 'Not found',
}, },
} }
for status, body in responses.items():
body['schema'] = {'$ref': '#/definitions/ApiError'}
if method_name == 'DELETE': if method_name == 'DELETE':
responses['204'] = { responses['204'] = {
'description': 'Deleted' 'description': 'Deleted'

61
endpoints/api/error.py Normal file
View file

@ -0,0 +1,61 @@
""" Error details API """
from flask import url_for
from endpoints.api import (resource, nickname, ApiResource, path_param,
define_json_response)
from endpoints.exception import NotFound, ApiErrorType, ERROR_DESCRIPTION
def error_view(error_type):
return {
'type': url_for('error', error_type=error_type, _external=True),
'title': error_type,
'description': ERROR_DESCRIPTION[error_type]
}
@resource('/v1/error/<error_type>')
@path_param('error_type', 'The error code identifying the type of error.')
class Error(ApiResource):
""" Resource for Error Descriptions"""
schemas = {
'ApiErrorDescription': {
'type': 'object',
'description': 'Description of an error',
'required': [
'type',
'description',
'title',
],
'properties': {
'type': {
'type': 'string',
'description': 'A reference to the error type resource'
},
'title': {
'type': 'string',
'description': (
'The title of the error. Can be used to uniquely identify the kind'
' of error.'
),
'enum': list(ApiErrorType.__members__)
},
'description': {
'type': 'string',
'description': (
'A more detailed description of the error that may include help for'
' fixing the issue.'
)
}
},
},
}
@define_json_response('ApiErrorDescription')
@nickname('getErrorDescription')
def get(self, error_type):
""" Get a detailed description of the error """
if error_type in ERROR_DESCRIPTION.keys():
return error_view(error_type)
raise NotFound()

View file

@ -4,7 +4,8 @@ import json
from collections import defaultdict from collections import defaultdict
from endpoints.api import (resource, nickname, require_repo_read, RepositoryParamResource, from endpoints.api import (resource, nickname, require_repo_read, RepositoryParamResource,
format_date, NotFound, path_param) format_date, path_param)
from endpoints.exception import NotFound
from data import model from data import model
from util.cache import cache_control_flask_restful from util.cache import cache_control_flask_restful

View file

@ -7,8 +7,8 @@ from dateutil.relativedelta import relativedelta
from endpoints.api import (resource, nickname, ApiResource, query_param, parse_args, from endpoints.api import (resource, nickname, ApiResource, query_param, parse_args,
RepositoryParamResource, require_repo_admin, related_user_resource, RepositoryParamResource, require_repo_admin, related_user_resource,
format_date, Unauthorized, NotFound, require_user_admin, format_date, require_user_admin, path_param, require_scope, page_support)
path_param, require_scope, page_support) from endpoints.exception import Unauthorized, NotFound
from auth.permissions import AdministerOrganizationPermission, AdministerOrganizationPermission from auth.permissions import AdministerOrganizationPermission, AdministerOrganizationPermission
from auth.auth_context import get_authenticated_user from auth.auth_context import get_authenticated_user
from data import model, database from data import model, database

View file

@ -8,9 +8,9 @@ import features
from app import billing as stripe, avatar from app import billing as stripe, avatar
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error, from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
related_user_resource, internal_only, Unauthorized, NotFound, related_user_resource, internal_only, require_user_admin, log_action,
require_user_admin, log_action, show_if, path_param, show_if, path_param, require_scope)
require_scope) from endpoints.exception import Unauthorized, NotFound
from endpoints.api.team import team_view from endpoints.api.team import team_view
from endpoints.api.user import User, PrivateRepositories from endpoints.api.user import User, PrivateRepositories
from auth.permissions import (AdministerOrganizationPermission, OrganizationMemberPermission, from auth.permissions import (AdministerOrganizationPermission, OrganizationMemberPermission,

View file

@ -6,8 +6,8 @@ from flask import request
from app import avatar from app import avatar
from endpoints.api import (resource, nickname, require_repo_admin, RepositoryParamResource, from endpoints.api import (resource, nickname, require_repo_admin, RepositoryParamResource,
log_action, request_error, validate_json_request, path_param, log_action, request_error, validate_json_request, path_param)
NotFound) from endpoints.exception import NotFound
from data import model from data import model

View file

@ -3,7 +3,8 @@
from flask import request from flask import request
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error, from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
log_action, Unauthorized, NotFound, path_param, require_scope) log_action, path_param, require_scope)
from endpoints.exception import Unauthorized, NotFound
from auth.permissions import AdministerOrganizationPermission from auth.permissions import AdministerOrganizationPermission
from auth.auth_context import get_authenticated_user from auth.auth_context import get_authenticated_user
from auth import scopes from auth import scopes

View file

@ -5,9 +5,9 @@ import logging
from flask import request, abort from flask import request, abort
from endpoints.api import (resource, nickname, require_repo_admin, RepositoryParamResource, from endpoints.api import (resource, nickname, require_repo_admin, RepositoryParamResource,
log_action, validate_json_request, NotFound, internal_only, log_action, validate_json_request, internal_only,
path_param, show_if) path_param, show_if)
from endpoints.exception import NotFound
from app import tf from app import tf
from data import model from data import model
from data.database import db from data.database import db

View file

@ -13,8 +13,8 @@ from data.database import Repository as RepositoryTable
from endpoints.api import (truthy_bool, format_date, nickname, log_action, validate_json_request, from endpoints.api import (truthy_bool, format_date, nickname, log_action, validate_json_request,
require_repo_read, require_repo_write, require_repo_admin, require_repo_read, require_repo_write, require_repo_admin,
RepositoryParamResource, resource, query_param, parse_args, ApiResource, RepositoryParamResource, resource, query_param, parse_args, ApiResource,
request_error, require_scope, Unauthorized, NotFound, InvalidRequest, request_error, require_scope, path_param, page_support)
path_param, ExceedsLicenseException, page_support) from endpoints.exception import Unauthorized, NotFound, InvalidRequest, ExceedsLicenseException
from endpoints.api.billing import lookup_allowed_private_repos, get_namespace_plan from endpoints.api.billing import lookup_allowed_private_repos, get_namespace_plan
from endpoints.api.subscribe import check_repository_usage from endpoints.api.subscribe import check_repository_usage

View file

@ -6,8 +6,9 @@ from flask import request
from app import notification_queue from app import notification_queue
from endpoints.api import (RepositoryParamResource, nickname, resource, require_repo_admin, from endpoints.api import (RepositoryParamResource, nickname, resource, require_repo_admin,
log_action, validate_json_request, NotFound, request_error, log_action, validate_json_request, request_error,
path_param) path_param)
from endpoints.exception import NotFound
from endpoints.notificationevent import NotificationEvent from endpoints.notificationevent import NotificationEvent
from endpoints.notificationmethod import (NotificationMethod, from endpoints.notificationmethod import (NotificationMethod,
CannotValidateNotificationMethodException) CannotValidateNotificationMethodException)

View file

@ -5,7 +5,8 @@ import logging
from flask import request from flask import request
from endpoints.api import (resource, nickname, require_repo_admin, RepositoryParamResource, from endpoints.api import (resource, nickname, require_repo_admin, RepositoryParamResource,
log_action, validate_json_request, NotFound, path_param) log_action, validate_json_request, path_param)
from endpoints.exception import NotFound
from data import model from data import model

View file

@ -1,8 +1,9 @@
""" Manage user and organization robot accounts. """ """ Manage user and organization robot accounts. """
from endpoints.api import (resource, nickname, ApiResource, log_action, related_user_resource, from endpoints.api import (resource, nickname, ApiResource, log_action, related_user_resource,
Unauthorized, require_user_admin, require_scope, path_param, parse_args, require_user_admin, require_scope, path_param, parse_args,
truthy_bool, query_param) truthy_bool, query_param)
from endpoints.exception import Unauthorized
from auth.permissions import AdministerOrganizationPermission, OrganizationMemberPermission from auth.permissions import AdministerOrganizationPermission, OrganizationMemberPermission
from auth.auth_context import get_authenticated_user from auth.auth_context import get_authenticated_user
from auth import scopes from auth import scopes

View file

@ -5,9 +5,10 @@ import features
from app import secscan_api from app import secscan_api
from data import model from data import model
from endpoints.api import (require_repo_read, NotFound, DownstreamIssue, path_param, from endpoints.api import (require_repo_read, path_param,
RepositoryParamResource, resource, nickname, show_if, parse_args, RepositoryParamResource, resource, nickname, show_if, parse_args,
query_param, truthy_bool) query_param, truthy_bool)
from endpoints.exception import NotFound, DownstreamIssue
from util.secscan.api import APIRequestFailure from util.secscan.api import APIRequestFailure

View file

@ -4,7 +4,8 @@ import logging
import stripe import stripe
from app import billing from app import billing
from endpoints.api import request_error, log_action, NotFound from endpoints.api import request_error, log_action
from endpoints.exception import NotFound
from data import model from data import model
from data.billing import PLANS from data.billing import PLANS

View file

@ -3,8 +3,9 @@
from flask import request, abort from flask import request, abort
from endpoints.api import (resource, nickname, require_repo_read, require_repo_write, from endpoints.api import (resource, nickname, require_repo_read, require_repo_write,
RepositoryParamResource, log_action, NotFound, validate_json_request, RepositoryParamResource, log_action, validate_json_request,
path_param, parse_args, query_param, truthy_bool) path_param, parse_args, query_param, truthy_bool)
from endpoints.exception import NotFound
from endpoints.api.image import image_view from endpoints.api.image import image_view
from data import model from data import model
from auth.auth_context import get_authenticated_user from auth.auth_context import get_authenticated_user

View file

@ -5,9 +5,9 @@ from flask import request
import features import features
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error, from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
log_action, Unauthorized, NotFound, internal_only, require_scope, log_action, internal_only, require_scope, path_param, query_param,
path_param, query_param, truthy_bool, parse_args, require_user_admin, truthy_bool, parse_args, require_user_admin, show_if)
show_if) from endpoints.exception import Unauthorized, NotFound
from auth.permissions import AdministerOrganizationPermission, ViewTeamPermission from auth.permissions import AdministerOrganizationPermission, ViewTeamPermission
from auth.auth_context import get_authenticated_user from auth.auth_context import get_authenticated_user
from auth import scopes from auth import scopes

View file

@ -15,8 +15,8 @@ from buildtrigger.triggerutil import (TriggerDeactivationException,
RepositoryReadException, TriggerStartException) RepositoryReadException, TriggerStartException)
from endpoints.api import (RepositoryParamResource, nickname, resource, require_repo_admin, from endpoints.api import (RepositoryParamResource, nickname, resource, require_repo_admin,
log_action, request_error, query_param, parse_args, internal_only, log_action, request_error, query_param, parse_args, internal_only,
validate_json_request, api, Unauthorized, NotFound, InvalidRequest, validate_json_request, api, path_param)
path_param) from endpoints.exception import NotFound, Unauthorized, InvalidRequest
from endpoints.api.build import build_status_view, trigger_view, RepositoryBuildStatus from endpoints.api.build import build_status_view, trigger_view, RepositoryBuildStatus
from endpoints.building import start_build from endpoints.building import start_build
from data import model from data import model

View file

@ -13,10 +13,11 @@ import features
from app import app, billing as stripe, authentication, avatar from app import app, billing as stripe, authentication, avatar
from data.database import Repository as RepositoryTable from data.database import Repository as RepositoryTable
from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error, from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error,
log_action, internal_only, NotFound, require_user_admin, parse_args, log_action, internal_only, require_user_admin, parse_args,
query_param, InvalidToken, require_scope, format_date, show_if, query_param, require_scope, format_date, show_if,
license_error, require_fresh_login, path_param, define_json_response, license_error, require_fresh_login, path_param, define_json_response,
RepositoryParamResource, page_support) RepositoryParamResource, page_support)
from endpoints.exception import NotFound, InvalidToken
from endpoints.api.subscribe import subscribe from endpoints.api.subscribe import subscribe
from endpoints.common import common_login from endpoints.common import common_login
from endpoints.decorators import anon_allowed from endpoints.decorators import anon_allowed

127
endpoints/exception.py Normal file
View file

@ -0,0 +1,127 @@
from enum import Enum
from flask import url_for
from auth.auth_context import get_authenticated_user
class ApiErrorType(Enum):
external_service_timeout = 'external_service_timeout'
invalid_request = 'invalid_request'
invalid_response = 'invalid_response'
invalid_token = 'invalid_token'
expired_token = 'expired_token'
insufficient_scope = 'insufficient_scope'
fresh_login_required = 'fresh_login_required'
exceeds_license = 'exceeds_license'
not_found = 'not_found'
downstream_issue = 'downstream_issue'
ERROR_DESCRIPTION = {
ApiErrorType.external_service_timeout.value: "An external service timed out. Retrying the request may resolve the issue.",
ApiErrorType.invalid_request.value: "The request was invalid. It may have contained invalid values or was improperly formatted.",
ApiErrorType.invalid_response.value: "The response was invalid.",
ApiErrorType.invalid_token.value: "The access token provided was invalid.",
ApiErrorType.expired_token.value: "The access token provided has expired.",
ApiErrorType.insufficient_scope.value: "The access token did not have sufficient scope to access the requested resource.",
ApiErrorType.fresh_login_required.value: "The action requires a fresh login to succeed.",
ApiErrorType.exceeds_license.value: "The action was refused because the current license does not allow it.",
ApiErrorType.not_found.value: "The resource was not found.",
ApiErrorType.downstream_issue.value: "An error occurred in a downstream service.",
}
class ApiException(Exception):
"""
Represents an error in the application/problem+json format.
See: https://tools.ietf.org/html/rfc7807
- "type" (string) - A URI reference that identifies the
problem type.
- "title" (string) - A short, human-readable summary of the problem
type. It SHOULD NOT change from occurrence to occurrence of the
problem, except for purposes of localization
- "status" (number) - The HTTP status code
- "detail" (string) - A human-readable explanation specific to this
occurrence of the problem.
- "instance" (string) - A URI reference that identifies the specific
occurrence of the problem. It may or may not yield further
information if dereferenced.
"""
def __init__(self, error_type, status_code, error_description, payload=None):
Exception.__init__(self)
self.error_description = error_description
self.status_code = status_code
self.payload = payload
self.error_type = error_type
def to_dict(self):
rv = dict(self.payload or ())
if self.error_description is not None:
rv['detail'] = self.error_description
rv['title'] = self.error_type.value
rv['type'] = url_for('error', error_type=self.error_type.value, _external=True)
rv['status'] = self.status_code
return rv
class ExternalServiceTimeout(ApiException):
def __init__(self, error_description, payload=None):
ApiException.__init__(self, ApiErrorType.external_service_timeout, 520, error_description, payload)
class InvalidRequest(ApiException):
def __init__(self, error_description, payload=None):
ApiException.__init__(self, ApiErrorType.invalid_request, 400, error_description, payload)
class InvalidResponse(ApiException):
def __init__(self, error_description, payload=None):
ApiException.__init__(self, ApiErrorType.invalid_response, 400, error_description, payload)
class InvalidToken(ApiException):
def __init__(self, error_description, payload=None):
ApiException.__init__(self, ApiErrorType.invalid_token, 401, error_description, payload)
class ExpiredToken(ApiException):
def __init__(self, error_description, payload=None):
ApiException.__init__(self, ApiErrorType.expired_token, 401, error_description, payload)
class Unauthorized(ApiException):
def __init__(self, payload=None):
user = get_authenticated_user()
if user is None or user.organization:
ApiException.__init__(self, ApiErrorType.invalid_token, 401, "Requires authentication", payload)
else:
ApiException.__init__(self, ApiErrorType.insufficient_scope, 403, 'Unauthorized', payload)
class FreshLoginRequired(ApiException):
def __init__(self, payload=None):
ApiException.__init__(self, ApiErrorType.fresh_login_required, 401, "Requires fresh login", payload)
class ExceedsLicenseException(ApiException):
def __init__(self, payload=None):
ApiException.__init__(self, ApiErrorType.exceeds_license, 402, 'Payment Required', payload)
class NotFound(ApiException):
def __init__(self, payload=None):
ApiException.__init__(self, ApiErrorType.not_found, 404, 'Not Found', payload)
class DownstreamIssue(ApiException):
def __init__(self, payload=None):
ApiException.__init__(self, ApiErrorType.downstream_issue, 520, 'Downstream Issue', payload)

View file

@ -211,8 +211,9 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi
// Handle session expiration. // Handle session expiration.
Restangular.setErrorInterceptor(function(response) { Restangular.setErrorInterceptor(function(response) {
if (response.status == 401 && response.data['error_type'] == 'invalid_token' && //TODO: remove check for error_type (old style errors)
response.data['session_required'] !== false) { var invalid_token = response.data['title'] == 'invalid_token' || response.data['error_type'] == 'invalid_token';
if (response.status == 401 && invalid_token && response.data['session_required'] !== false) {
$('#sessionexpiredModal').modal({}); $('#sessionexpiredModal').modal({});
return false; return false;
} }

View file

@ -129,7 +129,7 @@
$scope.showInterface = true; $scope.showInterface = true;
}, function(resp) { }, function(resp) {
$scope.users = []; $scope.users = [];
$scope.usersError = resp['data']['message'] || resp['data']['error_description']; $scope.usersError = ApiService.getErrorMessage(resp);
}); });
}; };

View file

@ -124,7 +124,9 @@ angular.module('quay').factory('ApiService', ['Restangular', '$q', 'UtilService'
var deferred = $q.defer(); var deferred = $q.defer();
// If the error is a fresh login required, show the dialog. // If the error is a fresh login required, show the dialog.
if (resp.status == 401 && resp.data['error_type'] == 'fresh_login_required') { // TODO: remove error_type (old style error)
var fresh_login_required = resp.data['title'] == 'fresh_login_required' || resp.data['error_type'] == 'fresh_login_required';
if (resp.status == 401 && fresh_login_required) {
var retryOperation = function() { var retryOperation = function() {
apiService[opName].apply(apiService, opArgs).then(function(resp) { apiService[opName].apply(apiService, opArgs).then(function(resp) {
deferred.resolve(resp); deferred.resolve(resp);
@ -293,7 +295,8 @@ angular.module('quay').factory('ApiService', ['Restangular', '$q', 'UtilService'
apiService.getErrorMessage = function(resp, defaultMessage) { apiService.getErrorMessage = function(resp, defaultMessage) {
var message = defaultMessage; var message = defaultMessage;
if (resp['data']) { if (resp['data']) {
message = resp['data']['error_message'] || resp['data']['message'] || resp['data']['error_description'] || message; //TODO: remove error_message and error_description (old style error)
message = resp['data']['detail'] || resp['data']['error_message'] || resp['data']['message'] || resp['data']['error_description'] || message;
} }
return message; return message;

View file

@ -1,7 +1,7 @@
/** /**
* Service which provides helper methods for performing some simple UI operations. * Service which provides helper methods for performing some simple UI operations.
*/ */
angular.module('quay').factory('UIService', ['$timeout', '$rootScope', '$location', function($timeout, $rootScope, $location) { angular.module('quay').factory('UIService', ['$timeout', '$rootScope', '$location', 'ApiService', function($timeout, $rootScope, $location, ApiService) {
var CheckStateController = function(items, itemKey) { var CheckStateController = function(items, itemKey) {
this.items = items; this.items = items;
this.checked = []; this.checked = [];
@ -12,6 +12,7 @@ angular.module('quay').factory('UIService', ['$timeout', '$rootScope', '$locatio
this.itemKey_ = itemKey; this.itemKey_ = itemKey;
this.listeners_ = []; this.listeners_ = [];
this.page_ = null; this.page_ = null;
this.ApiService = ApiService
}; };
CheckStateController.prototype.listen = function(callback) { CheckStateController.prototype.listen = function(callback) {
@ -138,7 +139,7 @@ angular.module('quay').factory('UIService', ['$timeout', '$rootScope', '$locatio
}; };
uiService.showFormError = function(elem, result, opt_placement) { uiService.showFormError = function(elem, result, opt_placement) {
var message = result.data['message'] || result.data['error_description'] || ''; var message = ApiService.getErrorMessage(result, 'error');
if (message) { if (message) {
uiService.showPopover(elem, message, opt_placement || 'bottom'); uiService.showPopover(elem, message, opt_placement || 'bottom');
} else { } else {

View file

@ -44,6 +44,7 @@ from endpoints.api.logs import UserLogs, OrgLogs, OrgAggregateLogs, UserAggregat
from endpoints.api.billing import (UserCard, UserPlan, ListPlans, OrganizationCard, from endpoints.api.billing import (UserCard, UserPlan, ListPlans, OrganizationCard,
OrganizationPlan) OrganizationPlan)
from endpoints.api.discovery import DiscoveryResource from endpoints.api.discovery import DiscoveryResource
from endpoints.api.error import Error
from endpoints.api.organization import (OrganizationList, OrganizationMember, from endpoints.api.organization import (OrganizationList, OrganizationMember,
OrgPrivateRepositories, OrganizationMemberList, OrgPrivateRepositories, OrganizationMemberList,
Organization, ApplicationInformation, Organization, ApplicationInformation,
@ -236,6 +237,14 @@ class TestDiscovery(ApiTestCase):
assert 'paths' in json assert 'paths' in json
class TestErrorDescription(ApiTestCase):
def test_get_error(self):
json = self.getJsonResponse(Error, params=dict(error_type='not_found'))
assert json['title'] == 'not_found'
assert 'type' in json
assert 'description' in json
class TestPlans(ApiTestCase): class TestPlans(ApiTestCase):
def test_plans(self): def test_plans(self):
json = self.getJsonResponse(ListPlans) json = self.getJsonResponse(ListPlans)
@ -372,7 +381,7 @@ class TestConvertToOrganization(ApiTestCase):
'plan': 'free'}, 'plan': 'free'},
expected_code=400) expected_code=400)
self.assertEqual('The admin user is not valid', json['message']) self.assertEqual('The admin user is not valid', json['detail'])
def test_sameadminuser_by_email(self): def test_sameadminuser_by_email(self):
self.login(READ_ACCESS_USER) self.login(READ_ACCESS_USER)
@ -382,7 +391,7 @@ class TestConvertToOrganization(ApiTestCase):
'plan': 'free'}, 'plan': 'free'},
expected_code=400) expected_code=400)
self.assertEqual('The admin user is not valid', json['message']) self.assertEqual('The admin user is not valid', json['detail'])
def test_invalidadminuser(self): def test_invalidadminuser(self):
self.login(READ_ACCESS_USER) self.login(READ_ACCESS_USER)
@ -393,7 +402,7 @@ class TestConvertToOrganization(ApiTestCase):
expected_code=400) expected_code=400)
self.assertEqual('The admin user credentials are not valid', self.assertEqual('The admin user credentials are not valid',
json['message']) json['detail'])
def test_invalidadminpassword(self): def test_invalidadminpassword(self):
self.login(READ_ACCESS_USER) self.login(READ_ACCESS_USER)
@ -404,7 +413,7 @@ class TestConvertToOrganization(ApiTestCase):
expected_code=400) expected_code=400)
self.assertEqual('The admin user credentials are not valid', self.assertEqual('The admin user credentials are not valid',
json['message']) json['detail'])
def test_convert(self): def test_convert(self):
self.login(READ_ACCESS_USER) self.login(READ_ACCESS_USER)
@ -489,7 +498,7 @@ class TestCreateNewUser(ApiTestCase):
email='test@example.com'), email='test@example.com'),
expected_code=400) expected_code=400)
self.assertEquals('The username already exists', json['message']) self.assertEquals('The username already exists', json['detail'])
def test_trycreatetooshort(self): def test_trycreatetooshort(self):
json = self.postJsonResponse(User, json = self.postJsonResponse(User,
@ -499,7 +508,7 @@ class TestCreateNewUser(ApiTestCase):
expected_code=400) expected_code=400)
self.assertEquals('Invalid username a: Username must be between 4 and 30 characters in length', self.assertEquals('Invalid username a: Username must be between 4 and 30 characters in length',
json['error_description']) json['detail'])
def test_trycreateregexmismatch(self): def test_trycreateregexmismatch(self):
json = self.postJsonResponse(User, json = self.postJsonResponse(User,
@ -509,7 +518,7 @@ class TestCreateNewUser(ApiTestCase):
expected_code=400) expected_code=400)
self.assertEquals('Invalid username auserName: Username must match expression [a-z0-9_]+', self.assertEquals('Invalid username auserName: Username must match expression [a-z0-9_]+',
json['error_description']) json['detail'])
def test_createuser(self): def test_createuser(self):
data = self.postJsonResponse(User, data=NEW_USER_DETAILS, expected_code=200) data = self.postJsonResponse(User, data=NEW_USER_DETAILS, expected_code=200)
@ -740,7 +749,7 @@ class TestCreateOrganization(ApiTestCase):
expected_code=400) expected_code=400)
self.assertEquals('A user or organization with this name already exists', self.assertEquals('A user or organization with this name already exists',
json['message']) json['detail'])
def test_existingorg(self): def test_existingorg(self):
self.login(ADMIN_ACCESS_USER) self.login(ADMIN_ACCESS_USER)
@ -750,7 +759,7 @@ class TestCreateOrganization(ApiTestCase):
expected_code=400) expected_code=400)
self.assertEquals('A user or organization with this name already exists', self.assertEquals('A user or organization with this name already exists',
json['message']) json['detail'])
def test_createorg(self): def test_createorg(self):
self.login(ADMIN_ACCESS_USER) self.login(ADMIN_ACCESS_USER)
@ -845,7 +854,7 @@ class TestCreateOrganizationPrototypes(ApiTestCase):
delegate={'kind': 'team', 'name': 'owners'}), delegate={'kind': 'team', 'name': 'owners'}),
expected_code=400) expected_code=400)
self.assertEquals('Unknown activating user', json['message']) self.assertEquals('Unknown activating user', json['detail'])
def test_missingdelegate(self): def test_missingdelegate(self):
@ -1330,7 +1339,7 @@ class TestCreateRepo(ApiTestCase):
description=''), description=''),
expected_code=400) expected_code=400)
self.assertEquals('Invalid repository name', json['error_description']) self.assertEquals('Invalid repository name', json['detail'])
def test_duplicaterepo(self): def test_duplicaterepo(self):
self.login(ADMIN_ACCESS_USER) self.login(ADMIN_ACCESS_USER)
@ -1341,7 +1350,7 @@ class TestCreateRepo(ApiTestCase):
description=''), description=''),
expected_code=400) expected_code=400)
self.assertEquals('Repository already exists', json['message']) self.assertEquals('Repository already exists', json['detail'])
def test_createrepo(self): def test_createrepo(self):