@jantstack/adonis-authz 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ For the OpenFGA driver, also install its SDK (optional peer): `npm i @openfga/sd
|
|
|
36
36
|
2. **Explicit deny wins.** A deny anywhere in the scope chain blocks the permission even if a role grants it. Removing the deny restores it.
|
|
37
37
|
3. **Expiry is observable.** An assignment past its `expiresAt` grants nothing — enforced in SQL by the `database` driver and by an FGA *condition* in `openfga`, so no scheduler is needed.
|
|
38
38
|
4. **Polymorphic holders.** Users, admins, API integrations — any model with a morph name. Two holders with the same uuid and different type never cross.
|
|
39
|
-
5. **Deny by default.** Unknown permission, role without it, or no valid assignment → `false`. `authorize()`
|
|
39
|
+
5. **Deny by default.** Unknown permission, role without it, or no valid assignment → `false`. `authorize()` doesn't throw on any of those: an *unanswerable* question is answered "no". A backend that is *unreachable* is a different matter — see below.
|
|
40
40
|
6. **Idempotent writes.** Re-granting doesn't duplicate (it refreshes the expiry); re-revoking is a safe no-op.
|
|
41
41
|
|
|
42
42
|
These aren't prose promises: they're `tests/…` cases in the contract suite below.
|
|
@@ -127,12 +127,20 @@ The import **copies**, it doesn't move: your `authz_*` tables stay intact, so ro
|
|
|
127
127
|
|
|
128
128
|
### Operational notes for this driver
|
|
129
129
|
|
|
130
|
-
|
|
130
|
+
Choosing this driver adds a **second runtime dependency to every authorization check**: the catalog is read from your database and the facts from FGA. If FGA is unreachable, `authorize()` **throws** — it does not quietly return `false`.
|
|
131
|
+
|
|
132
|
+
That is deliberate. Denying silently during an outage strips every user of their permissions with nothing to indicate why, and sends you looking for a misconfigured role that doesn't exist. Failing loudly says "the backend is down". Access is denied either way; only the diagnosis differs. Wrap the call if your endpoint needs a specific response, and alert on it.
|
|
133
|
+
|
|
134
|
+
The `database` driver has no equivalent failure: authorization is available whenever your database is, which you need anyway.
|
|
135
|
+
|
|
136
|
+
Three more properties worth knowing before putting it in front of production traffic — none of them can grant access that wasn't granted, all fail towards *denied*:
|
|
131
137
|
|
|
132
138
|
- **Changing an expiry is not atomic.** FGA rejects deleting and writing the same tuple key in one transaction, so *replacing* an assignment's expiry is a delete followed by a write. Between the two, `authorize()` answers `false`, and a crash in that window loses the assignment; re-running the grant restores it (writes are idempotent). The driver reads the current tuple first, so this only happens when the expiry actually changes — a first grant is a plain write, and re-granting something identical (a seeder run again) touches nothing at all. That read is a shortcut, not a precondition: if it fails, or if a concurrent writer wins the race, the grant is still written.
|
|
133
139
|
- **Expiry follows the app server's clock.** The `not_expired` condition is evaluated against a `current_time` your process sends with each check, so a skewed clock makes assignments expire early or late. Keep NTP running — the same requirement your JWTs already have.
|
|
134
140
|
|
|
135
|
-
|
|
141
|
+
- **There is no distributed transaction with your database.** A `grant` validates the role against the local catalog and then writes the tuple to FGA. Delete that role from the catalog afterwards and the tuple is orphaned — but `authorize()` finds no permission→role mapping for it and denies, so the inconsistency fails closed. `openfga:import` is likewise not atomic; it is idempotent, so a run that dies half-way is fixed by running it again.
|
|
142
|
+
|
|
143
|
+
All of these are consequences of the facts living in another system, and none of them apply to the `database` driver.
|
|
136
144
|
|
|
137
145
|
## Compatibility
|
|
138
146
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jantstack/adonis-authz",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Driver-based authorization engine for AdonisJS + Lucid: hierarchical scopes with downward inheritance, explicit denies, expiring assignments and polymorphic holders. Ships a self-contained database driver and an OpenFGA driver, both judged by the same executable contract suite.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|