Skip to content

Commit

Permalink
Allow patron location-based restriction.
Browse files Browse the repository at this point in the history
  • Loading branch information
tdilauro committed Sep 10, 2024
1 parent 6b2380b commit d93e441
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 33 deletions.
1 change: 1 addition & 0 deletions src/palace/manager/api/authentication/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ def __bool__(self):

def __init__(
self,
*,
permanent_id=None,
authorization_identifier=None,
username=None,
Expand Down
89 changes: 56 additions & 33 deletions src/palace/manager/api/authentication/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ class LibraryIdentifierRestriction(Enum):
LIST = "list"


class LibraryIdenfitierRestrictionFields(Enum):
BARCODE = "barcode"
PATRON_LIBRARY = "patron location"


class Keyboards(Enum):
"""Used by the mobile app to determine which keyboard to display"""

Expand Down Expand Up @@ -223,7 +228,8 @@ class BasicAuthProviderLibrarySettings(AuthProviderLibrarySettings):
"values here. This value is not used if <em>Library Identifier Restriction Type</em> "
"is set to 'No restriction'.",
options={
"barcode": "Barcode",
LibraryIdenfitierRestrictionFields.BARCODE: "Barcode",
LibraryIdenfitierRestrictionFields.PATRON_LIBRARY: "Patron Location",
},
),
)
Expand Down Expand Up @@ -487,8 +493,6 @@ def authenticate(

# Check that the patron belongs to this library.
patrondata = self.enforce_library_identifier_restriction(patrondata)
if patrondata is None:
return PATRON_OF_ANOTHER_LIBRARY

# At this point we know there is _some_ authenticated patron,
# but it might not correspond to a Patron in our database, and
Expand Down Expand Up @@ -721,39 +725,51 @@ def identifies_individuals(self):
@classmethod
def _restriction_matches(
cls,
field: str | None,
value: str | None,
restriction: str | list[str] | re.Pattern | None,
match_type: LibraryIdentifierRestriction,
) -> bool:
) -> tuple[bool, str]:
"""Does the given patron match the given restriction?"""
if not field:
# No field -- nothing matches.
return False
if not value:
return False, "No value for field."

if not restriction:
# No restriction -- anything matches.
return True

if match_type == LibraryIdentifierRestriction.REGEX:
if restriction.search(field): # type: ignore[union-attr]
return True
elif match_type == LibraryIdentifierRestriction.PREFIX:
if field.startswith(restriction): # type: ignore[arg-type]
return True
elif match_type == LibraryIdentifierRestriction.STRING:
if field == restriction:
return True
elif match_type == LibraryIdentifierRestriction.LIST:
if field in restriction: # type: ignore[operator]
return True

return False
return True, "No restriction specified."

if match_type == LibraryIdentifierRestriction.REGEX and not restriction.search(
value
):
reason = (
f"{value!r} does not match regular expression {restriction.pattern!r}"
)
return False, reason
elif match_type == LibraryIdentifierRestriction.PREFIX and not value.startswith(
restriction
):
reason = f"{value!r} does not start with {restriction!r}"
return False, reason
elif match_type == LibraryIdentifierRestriction.STRING and value != restriction:
reason = f"{value!r} does not exactly match {restriction!r}"
return False, reason
elif (
match_type == LibraryIdentifierRestriction.LIST and value not in restriction
):
reason = f"{value!r} not in list {restriction!r}"
return False, reason

return True, ""

def get_library_identifier_field_data(
self, patrondata: PatronData
) -> tuple[PatronData, str | None]:
if self.library_identifier_field.lower() == "barcode":
return patrondata, patrondata.authorization_identifier
supported_fields = {
LibraryIdenfitierRestrictionFields.BARCODE.value: patrondata.authorization_identifier,
LibraryIdenfitierRestrictionFields.PATRON_LIBRARY.value: patrondata.library_identifier,
}
library_verification_field = self.library_identifier_field.lower()
if library_verification_field in supported_fields:
return patrondata, supported_fields[library_verification_field]

if not patrondata.complete:
remote_patrondata = self.remote_patron_lookup(patrondata)
Expand All @@ -766,8 +782,11 @@ def get_library_identifier_field_data(

def enforce_library_identifier_restriction(
self, patrondata: PatronData
) -> PatronData | None:
"""Does the given patron match the configured library identifier restriction?"""
) -> PatronData | ProblemDetail:
"""Does the given patron match the configured library identifier restriction?
If not, raise a ProblemDetail exception.
"""
if (
self.library_identifier_restriction_type
== LibraryIdentifierRestriction.NONE
Expand All @@ -783,14 +802,18 @@ def enforce_library_identifier_restriction(
return patrondata

patrondata, field = self.get_library_identifier_field_data(patrondata)
if self._restriction_matches(
isValid, reason = self._restriction_matches(
field,
self.library_identifier_restriction_criteria,
self.library_identifier_restriction_type,
):
return patrondata
else:
return None
)
if not isValid:
raise ProblemDetailException(
PATRON_OF_ANOTHER_LIBRARY.with_debug(
f"{self.library_identifier_field!r} does not match library restriction: {reason}."
)
)
return patrondata

def authenticated_patron(
self, _db: Session, authorization: dict | str
Expand Down
2 changes: 2 additions & 0 deletions src/palace/manager/api/sip/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,8 @@ def info_to_patrondata(
patrondata.email_address = info["email_address"]
if "personal_name" in info:
patrondata.personal_name = info["personal_name"]
if "permanent_location" in info:
patrondata.library_identifier = info["permanent_location"]
if "fee_amount" in info:
fines = info["fee_amount"]
else:
Expand Down

0 comments on commit d93e441

Please sign in to comment.