Skip to content

Commit

Permalink
Merge pull request #280 from NYPL/SFR-1827_New_fulfill_endpoint
Browse files Browse the repository at this point in the history
Add fulfill route and tests
  • Loading branch information
Apophenia authored Jan 2, 2024
2 parents 422c61b + dfa365a commit 43ee8af
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 1 deletion.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# CHANGELOG
## unreleased version -- v0.12.4
## Added
New /fulfill endpoint with ability to check for NYPL login in Bearer authorization header

## unreleased version -- v0.12.4
## Added
Expand Down
3 changes: 2 additions & 1 deletion api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from logger import createLog
from .blueprints import (
search, work, info, edition, utils, link, opds, collection, citation
search, work, info, edition, utils, link, opds, collection, citation, fulfill
)
from .utils import APIUtils

Expand All @@ -36,6 +36,7 @@ def __init__(self, dbEngine, redisClient):
self.app.register_blueprint(opds)
self.app.register_blueprint(collection)
self.app.register_blueprint(citation)
self.app.register_blueprint(fulfill)

def run(self):
if 'local-compose' in os.environ['ENVIRONMENT'] or 'sample-compose' in os.environ['ENVIRONMENT']:
Expand Down
1 change: 1 addition & 0 deletions api/blueprints/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
from .drbSearch import search
from .drbUtils import utils
from .drbWork import work
from .drbFulfill import fulfill
62 changes: 62 additions & 0 deletions api/blueprints/drbFulfill.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import json
import os

import jwt

from flask import Blueprint, request
from ..utils import APIUtils
from logger import createLog

JWT_ALGORITHM = ''
logger = createLog(__name__)

fulfill = Blueprint('fulfill', __name__, url_prefix='/fulfill')

@fulfill.route('/<uuid>', methods=['GET'])
def workFulfill(uuid):
logger.info('Checking if authorization is needed for work {}'.format(uuid))

requires_authorization = True

if requires_authorization:
try:
bearer = request.headers.get('Authorization')
token = bearer.split()[1]

jwt_secret = os.environ['NYPL_API_CLIENT_PUBLIC_KEY']
decoded_token =(jwt.decode(token, jwt_secret, 'RS256',
audience="app_myaccount"))
if json.loads(json.dumps(decoded_token))['iss'] == "https://www.nypl.org":
statusCode = 200
responseBody = uuid
else:
statusCode = 401
responseBody = 'Invalid access token'

except jwt.exceptions.ExpiredSignatureError:
statusCode = 401
responseBody = 'Expired access token'
except (jwt.exceptions.DecodeError, UnicodeDecodeError, IndexError, AttributeError):
statusCode = 401
responseBody = 'Invalid access token'
except ValueError:
logger.warning("Could not deserialize NYPL-issued public key")
statusCode = 500
responseBody = 'Server error'

else:
# TODO: In the future, this could record an analytics timestamp
# and redirect to URL of a work if authentication is not required.
# For now, only use /fulfill endpoint in response if authentication is required.
statusCode = 400
responseBody = "Bad Request"

response = APIUtils.formatResponseObject(
statusCode, 'fulfill', responseBody
)

if statusCode == 401:
response[0].headers['WWW-Authenticate'] = 'Bearer'

return response

6 changes: 6 additions & 0 deletions config/sample-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ HATHI_API_ROOT: https://babel.hathitrust.org/cgi/htd
OCLC_QUERY_LIMIT: '390000'
#OCLC_API_KEY:

# NYPL AUTH CONFIGURATION
NYPL_API_CLIENT_PUBLIC_KEY: >
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA44ilHg/PxcJYsISHMRyoxsmez178qZpkJVXg7rOMVTLZuf05an7Pl+lX4nw/rqcvGQDXyrimciLgLkWu00xhm6h6klTeJSNq2DgseF8OMw2olfuBKq1NBQ/vC8U0l5NJu34oSN4/iipgpovqAHHBGV4zDt0EWSXE5xpnBWi+w1NMAX/muB2QRfRxkkhueDkAmwKvz5MXJPay7FB/WRjf+7r2EN78x5iQKyCw0tpEZ5hpBX831SEnVULCnpFOcJWMPLdg0Ff6tBmgDxKQBVFIQ9RrzMLTqxKnVVn2+hVpk4F/8tMsGCdd4s/AJqEQBy5lsq7ji1B63XYqi5fc1SnJEQIDAQAB
-----END PUBLIC KEY-----
# Bardo CCE API URL
BARDO_CCE_API: http://sfr-bardo-copyright-development.us-east-1.elasticbeanstalk.com/search

Expand Down
55 changes: 55 additions & 0 deletions tests/unit/test_api_fulfill_blueprint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from flask import Flask
import pytest

import jwt

from api.blueprints.drbFulfill import workFulfill
from api.utils import APIUtils


class TestSearchBlueprint:
@pytest.fixture
def mockUtils(self, mocker):
return mocker.patch.multiple(
APIUtils,
formatResponseObject=mocker.DEFAULT
)

@pytest.fixture
def testApp(self):
flaskApp = Flask('test')
flaskApp.config['DB_CLIENT'] = 'testDBClient'
flaskApp.config['READER_VERSION'] = 'test'

return flaskApp

def test_workFulfill_invalid_token(self, testApp, mockUtils, monkeypatch):
with testApp.test_request_context('/fulfill/12345',
headers={'Authorization': 'Bearer Whatever'}):
monkeypatch.setenv('NYPL_API_CLIENT_PUBLIC_KEY', "SomeKeyValue")
workFulfill('12345')
mockUtils['formatResponseObject'].assert_called_once_with(
401, 'fulfill', 'Invalid access token')

def test_workFulfill_no_bearer_auth(self, testApp, mockUtils):
with testApp.test_request_context('/fulfill/12345',
headers={'Authorization': 'Whatever'}):
workFulfill('12345')
mockUtils['formatResponseObject'].assert_called_once_with(
401, 'fulfill', 'Invalid access token')

def test_workFulfill_empty_token(self, testApp, mockUtils):
with testApp.test_request_context('/fulfill/12345',
headers={'Authorization': ''}):
workFulfill('12345')
mockUtils['formatResponseObject'].assert_called_once_with(
401, 'fulfill', 'Invalid access token')

def test_workFulfill_no_header(self, testApp, mockUtils):
with testApp.test_request_context('/fulfill/12345'):
workFulfill('12345')
mockUtils['formatResponseObject'].assert_called_once_with(
401, 'fulfill', 'Invalid access token')



0 comments on commit 43ee8af

Please sign in to comment.