Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
de7daab
Add per-library patron blocking rules to SIP2 LibrarySettings (PP-3772)
dbernstein Feb 27, 2026
1544416
Refactor to push patron blocks up the inheritance stack.
dbernstein Feb 27, 2026
f01559d
Add rules engine
dbernstein Feb 28, 2026
4ca159a
Tie patron blocking rules data into the rules engine.
dbernstein Feb 28, 2026
bff70e0
* rule_engine.py — Richer error messages:
dbernstein Mar 2, 2026
81f9b16
* Deprecated code removed
dbernstein Mar 2, 2026
5bd417f
Add int() as an allowable function to the simpleeval rules engine.
dbernstein Mar 2, 2026
1758f4a
Add documentation for functions available for building patron blockin…
dbernstein Mar 3, 2026
b020ca7
Fix syntactic error introduced accidentally during the manual merge p…
dbernstein Mar 4, 2026
5c17c72
Fix linting errors.
dbernstein Mar 4, 2026
ece3259
Update lock file.
dbernstein Mar 6, 2026
07d7a87
Adds backend support for front end rule validations before saving:
dbernstein Mar 6, 2026
b4d641c
Fix mypy
dbernstein Mar 9, 2026
0f87fc9
Add test coverage to fetch_live_rule_validation_values
dbernstein Mar 9, 2026
560dcb7
Remove inline imports.
dbernstein Mar 9, 2026
73f6944
Fix lock file.
dbernstein Mar 10, 2026
f4170ba
Remove in-method imports.
dbernstein Mar 11, 2026
f0abc14
Add simpleeval to toml.
dbernstein Mar 12, 2026
bdfb62f
Remove divider comment blocks from rules_engine.py.
dbernstein Mar 12, 2026
797aa78
Convert docstrings to reST from google.
dbernstein Mar 12, 2026
5b9fc20
Replace _sip2_thread_local with explicit RemoteAuthResult API
dbernstein Mar 16, 2026
69a86d2
Remove stale comment.
dbernstein Mar 16, 2026
b3639d3
Wire validate_message into patron blocking rules validation
dbernstein Mar 16, 2026
598840f
Add validation when supports_patron_blocking_rules is False
dbernstein Mar 16, 2026
30ee26b
Replace SIP2-specific checks with supports_patron_blocking_rules (OCP)
dbernstein Mar 16, 2026
c91a030
Unify evaluator lifecycle between admin validation and runtime (#9)
dbernstein Mar 16, 2026
a8ceca5
Rename tests to accord with CLAUDE.md recommentations.
dbernstein Mar 16, 2026
26b5503
README.md link for available functions for patron blocking rules.
dbernstein Mar 16, 2026
49d27dc
Rename BLOCKED_CREDENTIALS to SUSPENDED_CREDENTIALS and add BLOCKED_B…
dbernstein Mar 16, 2026
478b37e
Fix poetry lock and mypy.
dbernstein Mar 17, 2026
53981e1
Patron blocking: fail-open, CloudWatch logging, remove live validation
dbernstein Mar 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@

Palace Manager is a backend service for digital library systems, maintained by [The Palace Project](https://thepalaceproject.org).

## Documentation

- [Patron Blocking Rules — Allowed Functions](docs/FUNCTIONS.md) —
Reference for the functions available in patron blocking rule expressions.

## Installation

Docker images created from this code are available at:
Expand Down
4 changes: 2 additions & 2 deletions bin/informational/patron_information
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ class PatronInformationScript(LibraryInputScript):
auth = LibraryAuthenticator.from_config(
_db, args.libraries[0]
).basic_auth_provider
patron_data = auth.remote_authenticate(args.barcode, args.pin)
self.explain(patron_data)
result = auth.remote_authenticate(args.barcode, args.pin)
self.explain(result.patron_data)

def explain(self, patron_data):
if patron_data is None or patron_data is False:
Expand Down
135 changes: 135 additions & 0 deletions docs/FUNCTIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Patron Blocking Rules — Allowed Functions
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doc is created, but its not referenced anywhere. At the very least this should be referenced in the README so people can find this documentation.


Patron blocking rule expressions are evaluated by a locked-down
[simpleeval](https://github.com/danthedeckie/simpleeval) sandbox.
Only the functions listed below may be called inside a rule expression.
Any reference to an unlisted function causes the rule to **fail closed**
(the patron is blocked at runtime; the rule is rejected at admin-save time).

---

## `age_in_years`

Calculates the age of a person in **whole years** from a date string.
Use this to write rules that gate access by age (e.g. block minors or
enforce senior-only services).

### Signature

```text
age_in_years(date_str, fmt=None) -> int
```

### Parameters

| Parameter | Type | Required |
|------------|-----------------|----------|
| `date_str` | `str` | Yes |
| `fmt` | `str` or `None` | No |

- **`date_str`** — A date string representing the person's date of birth.
ISO 8601 format (`YYYY-MM-DD`) is tried first; if that fails,
`dateutil.parser` is used as a fallback, accepting most common
human-readable formats (e.g. `"Jan 1, 1990"`, `"01/01/1990"`).
- **`fmt`** — An explicit
[`strptime`](https://docs.python.org/3/library/datetime.html#datetime.datetime.strptime)
format string (e.g. `"%d/%m/%Y"`). When supplied, no automatic parsing
is attempted.

### Returns

`int` — The person's age in complete years (fractional years are truncated,
not rounded).

### Raises

`ValueError` — If `date_str` cannot be parsed (either by ISO 8601, the
supplied `fmt`, or `dateutil`). At runtime this causes the rule to
**fail closed**.

### Examples

```python
# Block patrons under 18 (field returned verbatim from the SIP2 server)
age_in_years({polaris_patron_birthdate}) < 18

# Block patrons under 18 using an explicit strptime format
age_in_years({dob_field}, "%d/%m/%Y") < 18

# Block patrons aged 65 or over (e.g. senior-only restriction)
age_in_years({polaris_patron_birthdate}) >= 65
```

---

## `int`

Converts a value to a Python `int`. Useful when the SIP2 server returns
a numeric field as a string (a common occurrence) and you need to compare
it numerically rather than lexicographically.

### Signature

```text
int(value) -> int
```

### Parameters

| Parameter | Type | Required |
|-----------|-------|----------|
| `value` | `Any` | Yes |

- **`value`** — The value to convert. Typically a string such as `"3"` or
a float such as `2.9`. Any value accepted by Python's built-in `int()` is
valid. Passing a non-numeric string (e.g. `"adult"`) raises a `ValueError`
and causes the rule to **fail closed**.

### Returns

`int` — The integer representation of `value`. Floating-point values are
**truncated** toward zero (e.g. `int("2.9")` raises `ValueError`; pass a
float literal or cast via `{field} * 1` first if you need truncation of
floats).

### Raises

`ValueError` — If `value` cannot be converted to an integer. At runtime
this causes the rule to **fail closed**.

### Examples

```python
# Block patron class codes above 2 (SIP2 returns the code as a string)
int({sipserver_patron_class}) > 2

# Block if a numeric expiry-year field indicates an expired account
int({expire_year}) < 2025
```

---

## Notes

- **String methods are available** — methods on Python `str` values can be
called directly on string-valued placeholders. For example, to check
whether a patron identifier starts with a certain prefix:

```python
{patron_identifier}.startswith("1234")
```

- **Fail-closed behaviour** — any function call that raises an exception
(e.g. an unparseable date or a non-numeric string passed to `int()`)
causes the patron to be **blocked** at runtime and the rule to be
**rejected** at admin-save time. Write test rules carefully using
representative patron data before enabling them in production.
- **No other builtins** — Python builtins such as `len`, `str`, `float`,
`abs`, and `round` are **not** available. If you need additional
functions, request them via the standard feature-request process so they
can be reviewed and added to `DEFAULT_ALLOWED_FUNCTIONS` in
`rule_engine.py`.
- **Placeholder syntax** — field values from the SIP2 response are
referenced as `{field_name}`. All fields returned by the SIP2
`patron_information` command are available, plus the normalised `{fines}`
key (a `float` derived from `fee_amount`).
Loading
Loading