Skip to content

Commit

Permalink
SIP2 patron location restriction. (PP-1375) (ThePalaceProject#1926)
Browse files Browse the repository at this point in the history
  • Loading branch information
tdilauro authored Jul 11, 2024
1 parent aa4abb6 commit e384f76
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 4 deletions.
39 changes: 37 additions & 2 deletions src/palace/manager/api/sip/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
BasicAuthProviderLibrarySettings,
BasicAuthProviderSettings,
)
from palace.manager.api.problem_details import INVALID_CREDENTIALS
from palace.manager.api.problem_details import (
INVALID_CREDENTIALS,
PATRON_OF_ANOTHER_LIBRARY,
)
from palace.manager.api.sip.client import Sip2Encoding, SIPClient
from palace.manager.api.sip.dialect import Dialect as Sip2Dialect
from palace.manager.integration.settings import (
Expand All @@ -24,7 +27,7 @@
from palace.manager.service.analytics.analytics import Analytics
from palace.manager.sqlalchemy.model.patron import Patron
from palace.manager.util import MoneyUtility
from palace.manager.util.problem_detail import ProblemDetail
from palace.manager.util.problem_detail import ProblemDetail, ProblemDetailException


class SIP2Settings(BasicAuthProviderSettings):
Expand Down Expand Up @@ -192,6 +195,19 @@ class SIP2LibrarySettings(BasicAuthProviderLibrarySettings):
description="A specific identifier for the library or branch, if used in patron authentication",
),
)
# Used with SIP2, when it is available in the patron information response.
patron_location_restriction: str | None = FormField(
None,
form=ConfigurationFormItem(
label="Patron Location Restriction",
description=(
"A code for the library or branch, which, when specified, "
"must exactly match the permanent location for the patron."
"<br>If an ILS does not return a location for its patrons, specifying "
"a value here will always result in authentication failure."
),
),
)


class SIP2AuthenticationProvider(
Expand Down Expand Up @@ -241,6 +257,7 @@ def __init__(
self.ssl_verification = settings.ssl_verification
self.dialect = settings.ils
self.institution_id = library_settings.institution_id
self.patron_location_restriction = library_settings.patron_location_restriction
self._client = client

# Check if patrons should be blocked based on SIP status
Expand Down Expand Up @@ -332,8 +349,26 @@ def remote_authenticate(
# passing it on.
password = None
info = self.patron_information(username, password)
self._enforce_patron_location_restriction(info)
return self.info_to_patrondata(info)

def _enforce_patron_location_restriction(
self, info: dict[str, Any] | ProblemDetail
) -> None:
"""Raise an exception if patron location does not match the restriction.
If a location restriction is specified for the library against which the
patron is attempting to authenticate, then the authentication will fail
if either (1) the patron does not have an associated location or (2) the
patron's location does not exactly match the one configured.
"""
if (
not isinstance(info, ProblemDetail)
and self.patron_location_restriction is not None
and info.get("permanent_location") != self.patron_location_restriction
):
raise ProblemDetailException(PATRON_OF_ANOTHER_LIBRARY)

def _run_self_tests(self, _db):
def makeConnection(sip):
sip.connect()
Expand Down
5 changes: 5 additions & 0 deletions src/palace/manager/api/sip/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,10 @@ def _add(cls, internal_name, *args, **kwargs):
named._add("screen_message", "AF", allow_multiple=True)
named._add("print_line", "AG")

# This is a standard field for items, but Evergreen allows it to
# be returned in a Patron Information response (64) message.
named._add("permanent_location", "AQ")

# SIP extensions defined by Georgia Public Library Service's SIP
# server, used by Evergreen and Koha.
named._add("sipserver_patron_expiration", "PA")
Expand Down Expand Up @@ -712,6 +716,7 @@ def patron_information_parser(self, data):
named.screen_message,
named.print_line,
# Add common extension fields.
named.permanent_location,
named.sipserver_patron_expiration,
named.polaris_patron_expiration,
named.sipserver_patron_class,
Expand Down
43 changes: 41 additions & 2 deletions tests/manager/api/sip/test_authentication_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
BasicAuthProviderLibrarySettings,
Keyboards,
)
from palace.manager.api.problem_details import INVALID_CREDENTIALS
from palace.manager.api.problem_details import (
INVALID_CREDENTIALS,
PATRON_OF_ANOTHER_LIBRARY,
)
from palace.manager.api.sip import (
SIP2AuthenticationProvider,
SIP2LibrarySettings,
Expand All @@ -21,7 +24,7 @@
from palace.manager.api.sip.client import Sip2Encoding
from palace.manager.api.sip.dialect import Dialect
from palace.manager.core.config import CannotLoadConfiguration
from palace.manager.util.problem_detail import ProblemDetail
from palace.manager.util.problem_detail import ProblemDetail, ProblemDetailException
from tests.fixtures.database import DatabaseTransactionFixture
from tests.mocks.sip import MockSIPClient

Expand Down Expand Up @@ -105,6 +108,9 @@ class TestSIP2AuthenticationProvider:
evergreen_hold_privileges_denied = b"64 Y 00020161021 143002000000000000000100000000AA12345|AEBooth Excessive Fines Test|BHUSD|BV100.00|BDChildrens Circ Desk 1 Newtown, CT USA 06470|AQNEWTWN|BLY|PA20191004|PCAdult|PIAllowed|XI863718|AOBiblioTest|AY2AZ0000"
evergreen_card_reported_lost = b"64 Y 00020161021 143002000000000000000100000000AA12345|AEBooth Excessive Fines Test|BHUSD|BV100.00|BDChildrens Circ Desk 1 Newtown, CT USA 06470|AQNEWTWN|BLY|PA20191004|PCAdult|PIAllowed|XI863718|AOBiblioTest|AY2AZ0000"
evergreen_inactive_account = b"64YYYY 00020161021 143028000000000000000000000000AE|AA12345|BLN|AOBiblioTest|AY2AZ0000"
evergreen_patron_with_location = b"64 Y 00020161021 151441000000000000000000000000AOgapines|AAuser|AEPatron Name|BHUSD|BDCirc Desk, Anytown, Anystate USA 00000|AQTestLoc|BLY|PA20250520|PB19640101|PCDigital Only|PIFiltered|XI5784348|AY2AZ0000"
evergreen_patron_wo_location = b"64 Y 00020161021 151441000000000000000000000000AOgapines|AAuser|AEPatron Name|BHUSD|BDCirc Desk, Anytown, Anystate USA 00000|BLY|PA20250520|PB19640101|PCDigital Only|PIFiltered|XI5784348|AY2AZ0000"
evergreen_patron_with_wrong_loc = b"64 Y 00020161021 151441000000000000000000000000AOgapines|AAuser|AEPatron Name|BHUSD|BDCirc Desk, Anytown, Anystate USA 00000|AQOtherLoc|BLY|PA20250520|PB19640101|PCDigital Only|PIFiltered|XI5784348|AY2AZ0000"

polaris_valid_pin = b"64 00120161121 143327000000000000000000000000AO3|AA25891000331441|AEFalk, Jen|BZ0050|CA0075|CB0075|BLY|CQY|BHUSD|BV9.25|CC9.99|BD123 Charlotte Hall, MD 20622|[email protected]|BF501-555-1212|BC19710101 000000|PA1|PEHALL|PSSt. Mary's|U1|U2|U3|U4|U5|PZ20622|PX20180609 235959|PYN|FA0.00|AFPatron status is ok.|AGPatron status is ok.|AY2AZ94F3"

Expand Down Expand Up @@ -334,6 +340,39 @@ def test_remote_authenticate_no_password(
assert b"user2" in request
assert b"some password" not in request

def test_remote_authenticate_location_restriction(
self,
create_provider: Callable[..., SIP2AuthenticationProvider],
create_library_settings: Callable[..., SIP2Settings],
):
# This patron authentication library instance is configured with "TestLoc".
library_settings = create_library_settings(
patron_location_restriction="TestLoc"
)
provider = create_provider(library_settings=library_settings)
client = cast(MockSIPClient, provider.client)

# This patron has the CORRECT location.
client.queue_response(self.evergreen_patron_with_location)
client.queue_response(self.end_session_response)
patrondata = provider.remote_authenticate("user", "pass")
assert isinstance(patrondata, PatronData)
assert "Patron Name" == patrondata.personal_name

# This patron does NOT have an associated location.
client.queue_response(self.evergreen_patron_wo_location)
client.queue_response(self.end_session_response)
with pytest.raises(ProblemDetailException) as exc:
provider.remote_authenticate("user", "pass")
assert exc.value.problem_detail == PATRON_OF_ANOTHER_LIBRARY

# This patron has the WRONG location.
client.queue_response(self.evergreen_patron_with_wrong_loc)
client.queue_response(self.end_session_response)
with pytest.raises(ProblemDetailException) as exc:
provider.remote_authenticate("user", "pass")
assert exc.value.problem_detail == PATRON_OF_ANOTHER_LIBRARY

def test_encoding(
self,
create_provider: Callable[..., SIP2AuthenticationProvider],
Expand Down

0 comments on commit e384f76

Please sign in to comment.