Remove, cache or hardcode anything that could be cached when sending an email
Probably shouldn’t deploy this to production 😅
This shows exactly what I removed, bodged and hardcoded to test what the
performance implications of caching a bunch of stuff might look like.
Test command:
```bash
python -m timeit -n 1 -s "import notifications_python_client; c = notifications_python_client.notifications.NotificationsAPIClient('🤫', base_url='http://localhost:6011')" "c.send_email_notification(email_address='sender@something.com', template_id='be433bfc-fe31-464b-9f2c-5be11abf2176')"
```
Before:
```
raw times: 12.1 11.9 11.8
100 loops, best of 3: 118 msec per loop
```
After
```
raw times: 11.2 10.7 10.1
100 loops, best of 3: 101 msec per loop
```
Not a big improvement… So I was curious what it was doing in those
~100ms
Let’s go back to master and comment out persisting the notification to
the database:
```
raw times: 12.3 10.5 10.5
100 loops, best of 3: 105 msec per loop
```
(saves about 13ms)
If we comment out sending to the queue:
```
raw times: 3.43 3.24 4.88
100 loops, best of 3: 32.4 msec per loop
```
(saves about 85ms)
This means most of our request time is spent waiting for SQS.
If we test our fake caching while sending to the queue is commented
out we get a clearer picture of the potential improvements:
```
raw times: 2.13 1.84 2.18
100 loops, best of 3: 18.4 msec per loop
```
This is a saving of 14ms, from a baseline of 32.4ms, so 56%.
A typical call to fetch a service from Redis from the admin app takes
about 0.6ms, for context.
It’s also worth thinking about whether we’re holding a database
connection longer than we need to if we still have it while talking to
SQS.
2020-04-16 11:28:43 +01:00
from functools import lru_cache
2017-09-13 17:23:23 +01:00
from flask import request , _request_ctx_stack , current_app , g
2016-02-09 18:48:02 +00:00
from notifications_python_client . authentication import decode_jwt_token , get_token_issuer
2019-12-11 16:04:38 +00:00
from notifications_python_client . errors import (
2020-01-24 17:29:43 +00:00
TokenDecodeError , TokenExpiredError , TokenIssuerError , TokenAlgorithmError , TokenError
2019-12-11 16:04:38 +00:00
)
2018-03-27 17:37:09 +01:00
from notifications_utils import request_helper
2017-09-13 17:23:23 +01:00
from sqlalchemy . exc import DataError
from sqlalchemy . orm . exc import NoResultFound
2016-06-29 14:15:32 +01:00
2017-05-05 15:19:57 +01:00
from app . dao . services_dao import dao_fetch_service_by_id_with_api_keys
2016-01-15 16:22:03 +00:00
2015-12-15 10:47:20 +00:00
2020-01-24 17:32:41 +00:00
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
2016-06-29 17:37:20 +01:00
class AuthError ( Exception ) :
2017-12-12 18:13:31 +00:00
def __init__ ( self , message , code , service_id = None , api_key_id = None ) :
2016-06-29 17:37:20 +01:00
self . message = { " token " : [ message ] }
2016-11-01 10:33:34 +00:00
self . short_message = message
2016-06-29 17:37:20 +01:00
self . code = code
2017-12-12 18:13:31 +00:00
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__ )
2015-12-15 10:47:20 +00:00
2016-11-01 10:33:34 +00:00
def to_dict_v2 ( self ) :
2016-11-02 14:58:39 +00:00
return {
' status_code ' : self . code ,
2016-11-09 14:56:54 +00:00
" errors " : [
{
" error " : " AuthError " ,
" message " : self . short_message
}
]
2016-11-02 14:58:39 +00:00
}
2016-11-01 10:33:34 +00:00
2015-12-15 10:47:20 +00:00
2016-06-29 17:37:20 +01:00
def get_auth_token ( req ) :
auth_header = req . headers . get ( ' Authorization ' , None )
2015-12-15 10:47:20 +00:00
if not auth_header :
2020-01-24 17:32:41 +00:00
raise AuthError ( ' Unauthorized: authentication token must be provided ' , 401 )
2015-12-15 10:47:20 +00:00
2016-11-07 10:45:18 +00:00
auth_scheme = auth_header [ : 7 ] . title ( )
2015-12-15 10:47:20 +00:00
if auth_scheme != ' Bearer ' :
2020-01-24 17:32:41 +00:00
raise AuthError ( ' Unauthorized: authentication bearer scheme must be used ' , 401 )
2016-06-29 17:37:20 +01:00
return auth_header [ 7 : ]
2015-12-15 10:47:20 +00:00
2017-03-16 18:15:49 +00:00
def requires_no_auth ( ) :
pass
2017-03-15 16:52:44 +00:00
def requires_admin_auth ( ) :
2018-03-27 17:37:09 +01:00
request_helper . check_proxy_header_before_request ( )
2017-03-15 16:52:44 +00:00
auth_token = get_auth_token ( request )
2017-03-16 18:15:49 +00:00
client = __get_token_issuer ( auth_token )
2017-03-15 16:52:44 +00:00
if client == current_app . config . get ( ' ADMIN_CLIENT_USER_NAME ' ) :
g . service_id = current_app . config . get ( ' ADMIN_CLIENT_USER_NAME ' )
2020-02-19 16:42:40 +00:00
2020-02-20 15:16:37 +00:00
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
2020-02-19 17:50:00 +00:00
# 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 )
2017-03-15 16:52:44 +00:00
else :
2020-01-24 17:32:41 +00:00
raise AuthError ( ' Unauthorized: admin authentication token required ' , 401 )
2017-03-15 16:52:44 +00:00
Remove, cache or hardcode anything that could be cached when sending an email
Probably shouldn’t deploy this to production 😅
This shows exactly what I removed, bodged and hardcoded to test what the
performance implications of caching a bunch of stuff might look like.
Test command:
```bash
python -m timeit -n 1 -s "import notifications_python_client; c = notifications_python_client.notifications.NotificationsAPIClient('🤫', base_url='http://localhost:6011')" "c.send_email_notification(email_address='sender@something.com', template_id='be433bfc-fe31-464b-9f2c-5be11abf2176')"
```
Before:
```
raw times: 12.1 11.9 11.8
100 loops, best of 3: 118 msec per loop
```
After
```
raw times: 11.2 10.7 10.1
100 loops, best of 3: 101 msec per loop
```
Not a big improvement… So I was curious what it was doing in those
~100ms
Let’s go back to master and comment out persisting the notification to
the database:
```
raw times: 12.3 10.5 10.5
100 loops, best of 3: 105 msec per loop
```
(saves about 13ms)
If we comment out sending to the queue:
```
raw times: 3.43 3.24 4.88
100 loops, best of 3: 32.4 msec per loop
```
(saves about 85ms)
This means most of our request time is spent waiting for SQS.
If we test our fake caching while sending to the queue is commented
out we get a clearer picture of the potential improvements:
```
raw times: 2.13 1.84 2.18
100 loops, best of 3: 18.4 msec per loop
```
This is a saving of 14ms, from a baseline of 32.4ms, so 56%.
A typical call to fetch a service from Redis from the admin app takes
about 0.6ms, for context.
It’s also worth thinking about whether we’re holding a database
connection longer than we need to if we still have it while talking to
SQS.
2020-04-16 11:28:43 +01:00
@lru_cache ( maxsize = None )
def get_service ( issuer ) :
return dao_fetch_service_by_id_with_api_keys ( issuer )
2016-06-29 17:37:20 +01:00
def requires_auth ( ) :
2018-03-27 17:37:09 +01:00
request_helper . check_proxy_header_before_request ( )
2016-06-29 17:37:20 +01:00
auth_token = get_auth_token ( request )
2020-01-24 17:32:41 +00:00
issuer = __get_token_issuer ( auth_token ) # ie the `iss` claim which should be a service ID
2016-06-29 17:37:20 +01:00
Remove, cache or hardcode anything that could be cached when sending an email
Probably shouldn’t deploy this to production 😅
This shows exactly what I removed, bodged and hardcoded to test what the
performance implications of caching a bunch of stuff might look like.
Test command:
```bash
python -m timeit -n 1 -s "import notifications_python_client; c = notifications_python_client.notifications.NotificationsAPIClient('🤫', base_url='http://localhost:6011')" "c.send_email_notification(email_address='sender@something.com', template_id='be433bfc-fe31-464b-9f2c-5be11abf2176')"
```
Before:
```
raw times: 12.1 11.9 11.8
100 loops, best of 3: 118 msec per loop
```
After
```
raw times: 11.2 10.7 10.1
100 loops, best of 3: 101 msec per loop
```
Not a big improvement… So I was curious what it was doing in those
~100ms
Let’s go back to master and comment out persisting the notification to
the database:
```
raw times: 12.3 10.5 10.5
100 loops, best of 3: 105 msec per loop
```
(saves about 13ms)
If we comment out sending to the queue:
```
raw times: 3.43 3.24 4.88
100 loops, best of 3: 32.4 msec per loop
```
(saves about 85ms)
This means most of our request time is spent waiting for SQS.
If we test our fake caching while sending to the queue is commented
out we get a clearer picture of the potential improvements:
```
raw times: 2.13 1.84 2.18
100 loops, best of 3: 18.4 msec per loop
```
This is a saving of 14ms, from a baseline of 32.4ms, so 56%.
A typical call to fetch a service from Redis from the admin app takes
about 0.6ms, for context.
It’s also worth thinking about whether we’re holding a database
connection longer than we need to if we still have it while talking to
SQS.
2020-04-16 11:28:43 +01:00
service = get_service ( issuer )
g . service_id = issuer
_request_ctx_stack . top . authenticated_service = service
_request_ctx_stack . top . api_user = None
return
2016-06-29 17:37:20 +01:00
2017-03-16 18:15:49 +00:00
def __get_token_issuer ( auth_token ) :
try :
2020-01-24 17:32:41 +00:00
issuer = get_token_issuer ( auth_token )
2017-03-16 18:15:49 +00:00
except TokenIssuerError :
raise AuthError ( " Invalid token: iss field not provided " , 403 )
2018-11-07 13:39:08 +00:00
except TokenDecodeError :
2020-01-24 17:32:41 +00:00
raise AuthError ( GENERAL_TOKEN_ERROR_MESSAGE , 403 )
return issuer