mirror of
https://github.com/GSA/notifications-api.git
synced 2026-04-26 04:10:13 -04:00
We think that holding open database transactions while we go and do something else is causing us to have poor performance. Because we’re not serialising everything as soon as we pull it out of the database we can guarantee that we don’t need to go back to the database again. So let’s see if explicitly closing the transaction helps with performance.
186 lines
6.9 KiB
Python
186 lines
6.9 KiB
Python
from flask import request, _request_ctx_stack, current_app, g
|
|
from notifications_python_client.authentication import decode_jwt_token, get_token_issuer
|
|
from notifications_python_client.errors import (
|
|
TokenDecodeError, TokenExpiredError, TokenIssuerError, TokenAlgorithmError, TokenError
|
|
)
|
|
from notifications_utils import request_helper
|
|
from sqlalchemy.exc import DataError
|
|
from sqlalchemy.orm.exc import NoResultFound
|
|
from gds_metrics import Histogram
|
|
|
|
from app import db
|
|
from app.dao.services_dao import dao_fetch_service_by_id
|
|
from app.dao.api_key_dao import get_model_api_keys
|
|
from app.serialised_models import (
|
|
SerialisedAPIKey,
|
|
SerialisedAPIKeyCollection,
|
|
SerialisedService,
|
|
)
|
|
|
|
|
|
GENERAL_TOKEN_ERROR_MESSAGE = 'Invalid token: make sure your API token matches the example at https://docs.notifications.service.gov.uk/rest-api.html#authorisation-header' # noqa
|
|
|
|
AUTH_DB_CONNECTION_DURATION_SECONDS = Histogram(
|
|
'auth_db_connection_duration_seconds',
|
|
'Time taken to get DB connection and fetch service from database',
|
|
)
|
|
|
|
|
|
class AuthError(Exception):
|
|
def __init__(self, message, code, service_id=None, api_key_id=None):
|
|
self.message = {"token": [message]}
|
|
self.short_message = message
|
|
self.code = code
|
|
self.service_id = service_id
|
|
self.api_key_id = api_key_id
|
|
|
|
def __str__(self):
|
|
return 'AuthError({message}, {code}, service_id={service_id}, api_key_id={api_key_id})'.format(**self.__dict__)
|
|
|
|
def to_dict_v2(self):
|
|
return {
|
|
'status_code': self.code,
|
|
"errors": [
|
|
{
|
|
"error": "AuthError",
|
|
"message": self.short_message
|
|
}
|
|
]
|
|
}
|
|
|
|
|
|
def get_auth_token(req):
|
|
auth_header = req.headers.get('Authorization', None)
|
|
if not auth_header:
|
|
raise AuthError('Unauthorized: authentication token must be provided', 401)
|
|
|
|
auth_scheme = auth_header[:7].title()
|
|
|
|
if auth_scheme != 'Bearer ':
|
|
raise AuthError('Unauthorized: authentication bearer scheme must be used', 401)
|
|
|
|
return auth_header[7:]
|
|
|
|
|
|
def requires_no_auth():
|
|
pass
|
|
|
|
|
|
def requires_admin_auth():
|
|
request_helper.check_proxy_header_before_request()
|
|
|
|
auth_token = get_auth_token(request)
|
|
client = __get_token_issuer(auth_token)
|
|
|
|
if client == current_app.config.get('ADMIN_CLIENT_USER_NAME'):
|
|
g.service_id = current_app.config.get('ADMIN_CLIENT_USER_NAME')
|
|
|
|
for secret in current_app.config.get('API_INTERNAL_SECRETS'):
|
|
try:
|
|
decode_jwt_token(auth_token, secret)
|
|
return
|
|
except TokenExpiredError:
|
|
raise AuthError("Invalid token: expired, check that your system clock is accurate", 403)
|
|
except TokenDecodeError:
|
|
# TODO: Change this so it doesn't also catch `TokenIssuerError` or `TokenIssuedAtError` exceptions
|
|
# (which are children of `TokenDecodeError`) as these should cause an auth error immediately rather
|
|
# than continue on to check the next admin client secret
|
|
continue
|
|
|
|
# Either there are no admin client secrets or their token didn't match one of them so error
|
|
raise AuthError("Unauthorized: admin authentication token not found", 401)
|
|
else:
|
|
raise AuthError('Unauthorized: admin authentication token required', 401)
|
|
|
|
|
|
def get_service_dict(issuer):
|
|
from app.schemas import service_schema
|
|
with AUTH_DB_CONNECTION_DURATION_SECONDS.time():
|
|
fetched = dao_fetch_service_by_id(issuer)
|
|
return service_schema.dump(fetched).data
|
|
|
|
|
|
def get_service_model(issuer):
|
|
return SerialisedService(get_service_dict(issuer))
|
|
|
|
|
|
def get_api_keys_dict(issuer):
|
|
return [
|
|
{k: getattr(key, k) for k in SerialisedAPIKey.ALLOWED_PROPERTIES}
|
|
for key in get_model_api_keys(issuer)
|
|
]
|
|
|
|
|
|
def get_api_keys_models(issuer):
|
|
return SerialisedAPIKeyCollection(get_api_keys_dict(issuer))
|
|
|
|
|
|
def requires_auth():
|
|
request_helper.check_proxy_header_before_request()
|
|
|
|
auth_token = get_auth_token(request)
|
|
issuer = __get_token_issuer(auth_token) # ie the `iss` claim which should be a service ID
|
|
|
|
try:
|
|
service = get_service_model(issuer)
|
|
service.api_keys = get_api_keys_models(issuer)
|
|
db.session.commit()
|
|
except DataError:
|
|
raise AuthError("Invalid token: service id is not the right data type", 403)
|
|
except NoResultFound:
|
|
raise AuthError("Invalid token: service not found", 403)
|
|
|
|
if not service.api_keys:
|
|
raise AuthError("Invalid token: service has no API keys", 403, service_id=service.id)
|
|
|
|
if not service.active:
|
|
raise AuthError("Invalid token: service is archived", 403, service_id=service.id)
|
|
|
|
for api_key in service.api_keys:
|
|
try:
|
|
decode_jwt_token(auth_token, api_key.secret)
|
|
except TokenExpiredError:
|
|
err_msg = "Error: Your system clock must be accurate to within 30 seconds"
|
|
raise AuthError(err_msg, 403, service_id=service.id, api_key_id=api_key.id)
|
|
except TokenAlgorithmError:
|
|
err_msg = "Invalid token: algorithm used is not HS256"
|
|
raise AuthError(err_msg, 403, service_id=service.id, api_key_id=api_key.id)
|
|
except TokenDecodeError:
|
|
# we attempted to validate the token but it failed meaning it was not signed using this api key.
|
|
# Let's try the next one
|
|
# TODO: Change this so it doesn't also catch `TokenIssuerError` or `TokenIssuedAtError` exceptions (which
|
|
# are children of `TokenDecodeError`) as these should cause an auth error immediately rather than
|
|
# continue on to check the next API key
|
|
continue
|
|
except TokenError:
|
|
# General error when trying to decode and validate the token
|
|
raise AuthError(GENERAL_TOKEN_ERROR_MESSAGE, 403, service_id=service.id, api_key_id=api_key.id)
|
|
|
|
if api_key.expiry_date:
|
|
raise AuthError("Invalid token: API key revoked", 403, service_id=service.id, api_key_id=api_key.id)
|
|
|
|
g.service_id = service.id
|
|
_request_ctx_stack.top.authenticated_service = service
|
|
_request_ctx_stack.top.api_user = api_key
|
|
|
|
current_app.logger.info('API authorised for service {} with api key {}, using issuer {} for URL: {}'.format(
|
|
service.id,
|
|
api_key.id,
|
|
request.headers.get('User-Agent'),
|
|
request.base_url
|
|
))
|
|
return
|
|
else:
|
|
# service has API keys, but none matching the one the user provided
|
|
raise AuthError("Invalid token: API key not found", 403, service_id=service.id)
|
|
|
|
|
|
def __get_token_issuer(auth_token):
|
|
try:
|
|
issuer = get_token_issuer(auth_token)
|
|
except TokenIssuerError:
|
|
raise AuthError("Invalid token: iss field not provided", 403)
|
|
except TokenDecodeError:
|
|
raise AuthError(GENERAL_TOKEN_ERROR_MESSAGE, 403)
|
|
return issuer
|