@rowstile/prisma 0.1.0-alpha.4 → 0.1.0-alpha.6
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 +59 -69
- package/dist/index.js +11 -5
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,83 +1,73 @@
|
|
|
1
|
-
# rowstile
|
|
1
|
+
# @rowstile/prisma
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
row-level security. It holds no rowstile logic: it calls the `authz.*` functions the policy made, and
|
|
7
|
-
translates their answers.
|
|
3
|
+
[rowstile](https://rowstile.dev) is a policy language for PostgreSQL. You write who may see and change each
|
|
4
|
+
row in one file, and it becomes row-level security: the database filters every read and refuses every write
|
|
5
|
+
the rules don't allow, whatever the code asks.
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
This package is rowstile for Prisma. `signedIn(adapter)` signs every transaction Prisma begins in as the
|
|
8
|
+
request's user. `authz()` is a client extension: an update or delete of a row the user can't see is
|
|
9
|
+
`NotFound`, of one they may not change `Refused` with the database's reason, and `db.$authz` asks the
|
|
10
|
+
questions (`can`, `ids`, `permsOf`, `share`). Prisma 7, with a driver adapter.
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
# @next on each while only an alpha is published: a plain install gets an older one
|
|
14
|
+
npm install @rowstile/client@next @rowstile/prisma@next
|
|
15
|
+
npm install --save-dev rowstile@next # the command: rowstile dev, rowstile migrate
|
|
16
|
+
```
|
|
10
17
|
|
|
11
18
|
```ts
|
|
12
19
|
// src/db.ts
|
|
13
20
|
import { PrismaPg } from "@prisma/adapter-pg";
|
|
21
|
+
import pg from "pg";
|
|
14
22
|
import { authz, signedIn } from "@rowstile/prisma";
|
|
15
|
-
import "
|
|
16
|
-
|
|
23
|
+
import "./authz.gen.ts"; // the policy's names, for the SDK's types
|
|
24
|
+
|
|
25
|
+
const url = process.env.ROWSTILE_APP_URL;
|
|
26
|
+
export const pool = new pg.Pool({ connectionString: url, max: Number(process.env.PG_POOL_MAX ?? 5) });
|
|
27
|
+
export const db = new PrismaClient({ adapter: signedIn(new PrismaPg(pool), { user: requestUser }) }).$extends(authz());
|
|
28
|
+
```
|
|
17
29
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
30
|
+
`ROWSTILE_APP_URL` is the app's own connection, as the role the policy names. It is never the owner's URL,
|
|
31
|
+
which Prisma migrates with: row-level security doesn't apply to a table's owner.
|
|
32
|
+
|
|
33
|
+
Then the code has no checks. The database filters what a query reads:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
const notes = await db.note.findMany({ select: { id: true, body: true }, orderBy: { id: "asc" } });
|
|
21
37
|
```
|
|
22
38
|
|
|
39
|
+
and a query by permission is one call:
|
|
40
|
+
|
|
23
41
|
```ts
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
await db.folder.update({ where: { id: Number((await params).id) }, data: await req.json() });
|
|
27
|
-
return Response.json({ ok: true });
|
|
28
|
-
});
|
|
42
|
+
const ids = await db.$authz.ids("project", "edit", Number);
|
|
43
|
+
const projects = await db.project.findMany({ where: { id: { in: ids } }, select: { id: true }, orderBy: { id: "asc" } });
|
|
29
44
|
```
|
|
30
45
|
|
|
31
|
-
|
|
46
|
+
## Good to know
|
|
47
|
+
|
|
48
|
+
- Use the client `$extends(authz())` returns, and only that one.
|
|
49
|
+
- `$transaction([...])` is refused: Prisma starts an array's transaction from whichever request came first.
|
|
50
|
+
Use `$transaction(async (tx) => ...)`.
|
|
51
|
+
- A `create` reads its row back, so it needs the select rule too. A refusal there is `Refused` naming it.
|
|
52
|
+
- Policy changes ship as Prisma migrations: `tool = "prisma"` in `rowstile.toml`, then `npx rowstile migrate`.
|
|
53
|
+
|
|
54
|
+
## More
|
|
55
|
+
|
|
56
|
+
- [Next.js with Prisma](https://rowstile.dev/stacks/nextjs): the whole setup, step by step, from an app the tests run.
|
|
57
|
+
- [The TypeScript SDK](https://rowstile.dev/sdk/typescript): every package, and the traps they handle.
|
|
58
|
+
- [Getting started](https://rowstile.dev/getting-started): a first policy, step by step.
|
|
59
|
+
- [Source and issues](https://github.com/rowstile/rowstile).
|
|
60
|
+
|
|
61
|
+
The other packages:
|
|
62
|
+
|
|
63
|
+
| package | for |
|
|
32
64
|
|---|---|
|
|
33
|
-
| `@rowstile/client` |
|
|
34
|
-
| `@rowstile/
|
|
35
|
-
| `@rowstile/
|
|
36
|
-
| `@rowstile/
|
|
37
|
-
| `@rowstile/
|
|
38
|
-
| `@rowstile/
|
|
39
|
-
| `@rowstile/
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
Who a transaction acts for: `42` or `"42"` (a user), `["service", 3]` (another principal type of the policy),
|
|
43
|
-
or `null` (nobody: only what `anyone` may see). A plain id is always a user's, whatever it holds: `"service:3"`
|
|
44
|
-
is the user with that id, so an id from outside (a username, an identity provider's subject) can't name a
|
|
45
|
-
service. `parsePrincipal("service:3")` reads back what `describe()` wrote, for text your own code wrote. Whoever the code acts for (`actingAs`, a `job`)
|
|
46
|
-
comes first, else the `user` function, else nobody; a `who` given to one call (`transaction(fn, who)`) comes
|
|
47
|
-
before all, and `null` there is nobody. Every transaction signs in, even as nobody, and nothing uses a
|
|
48
|
-
session-level `SET`, so pools and poolers in transaction mode are safe; the change feed needs its own connection
|
|
49
|
-
to Postgres ([Behind a pooler](../../docs/operations.md#behind-a-pooler)).
|
|
50
|
-
|
|
51
|
-
Traps it handles:
|
|
52
|
-
|
|
53
|
-
- **Next.js caching.** Before a transaction signs in as someone, `@rowstile/next` calls Next's `connection()`:
|
|
54
|
-
the render is dynamic, and a signed-in read inside `"use cache"` or `unstable_cache` fails instead of being
|
|
55
|
-
cached for the next user. Reads as nobody may be cached.
|
|
56
|
-
- **Prisma answers some calls together.** It dispatches the `findUnique` calls of one tick, and array
|
|
57
|
-
transactions, from the first caller's context. `authz()` runs each `findUnique` in a transaction of its own,
|
|
58
|
-
and refuses `$transaction([...])` (use `$transaction(async (tx) => ...)`), so no query runs as someone else.
|
|
59
|
-
Use the client `$extends(authz())` returns, and only that one: the client it was made from is refused
|
|
60
|
-
whenever it is used, inside your own extensions too. An extension you add to the client `authz()` returns is
|
|
61
|
-
placed before it, so `authz()` stays the last one: each of your query hooks runs once for a call, and sees
|
|
62
|
-
the errors `authz()` makes (`Refused`, `NotFound`).
|
|
63
|
-
- **Inserts that read the row back** (`RETURNING`, Prisma's `create`) also need the select rule: Postgres's own
|
|
64
|
-
message comes back as `Refused` naming the select rule.
|
|
65
|
-
- **Prisma's promises are lazy**: `actingAs` and `job` await what they return inside their scope.
|
|
66
|
-
- **A connection that drops.** node-postgres stops the process when a client that is checked out loses its
|
|
67
|
-
connection and nobody listens: `@rowstile/pg` listens while it holds one, so the transaction fails instead.
|
|
68
|
-
`changes(pool)` listens again after its connection drops, and tells its subscribers once.
|
|
69
|
-
- **An update that changed nothing** is `NotFound` when the user can't see the row, `Refused` when the database
|
|
70
|
-
says no, and `NotFound` again when the rule allows it and the statement matched nothing for another reason
|
|
71
|
-
(a `where` with more than the key).
|
|
72
|
-
- **The answers in the browser are one user's.** Give `<AuthzProvider user={id}>` if the user can change
|
|
73
|
-
without a page load: the hooks forget what they had and ask again.
|
|
74
|
-
|
|
75
|
-
The types and permissions of the policy are generated names: with a `@rowstile/*` dependency in
|
|
76
|
-
`package.json`, `rowstile client` writes only the names (`[clients] ts = "src/authz.gen.ts"`), registered with
|
|
77
|
-
the SDK, so `db.$authz.can("note", 1, "edt")` doesn't type-check. Policy changes ship as migrations for your
|
|
78
|
-
tool (`rowstile migrate`, with `tool = "prisma"` or `"drizzle"` in `rowstile.toml`).
|
|
79
|
-
|
|
80
|
-
The packages are an npm workspace at the repository root (`npm ci`, then `npx tsc -b sdk/typescript`).
|
|
81
|
-
`integrations/nextjs/` is the conformance suite: a small Next.js app on Prisma 7 and Prisma Migrate, and the
|
|
82
|
-
checks every supported stack passes, with pg, postgres.js, Drizzle and the React kit
|
|
83
|
-
(`integrations/nextjs/test.sh`).
|
|
65
|
+
| [`@rowstile/client`](https://www.npmjs.com/package/@rowstile/client) | who a transaction acts for, the errors, the runtime's calls |
|
|
66
|
+
| [`@rowstile/pg`](https://www.npmjs.com/package/@rowstile/pg) | node-postgres |
|
|
67
|
+
| [`@rowstile/postgres`](https://www.npmjs.com/package/@rowstile/postgres) | postgres.js |
|
|
68
|
+
| [`@rowstile/drizzle`](https://www.npmjs.com/package/@rowstile/drizzle) | Drizzle ORM |
|
|
69
|
+
| [`@rowstile/next`](https://www.npmjs.com/package/@rowstile/next) | Next.js: route handlers, server actions, caches |
|
|
70
|
+
| [`@rowstile/react`](https://www.npmjs.com/package/@rowstile/react) | React: hooks and a share dialog |
|
|
71
|
+
| [`@rowstile/vitest`](https://www.npmjs.com/package/@rowstile/vitest) | Vitest: matchers, a database per worker |
|
|
72
|
+
|
|
73
|
+
rowstile is a 0.x preview: names may still change before 1.0. Apache-2.0.
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* in with authz.act_as() as whoever the code acts for (actingAs, a job), else user(), else nobody; a query
|
|
9
9
|
* outside a transaction runs in one of its own. `authz()` turns the answers into errors: an update or delete
|
|
10
10
|
* of a row the user can't see is NotFound (Prisma's P2025), of one they may not change Refused with the
|
|
11
|
-
* reason, a refused create Refused naming the rule
|
|
11
|
+
* reason, a refused create Refused naming the rule, a findUniqueOrThrow or findFirstOrThrow that finds no row
|
|
12
|
+
* NotFound; and adds db.$authz (can, ids, permsOf, ...). It refuses
|
|
12
13
|
* array transactions, $transaction([...]): Prisma starts them from another caller's context.
|
|
13
14
|
*
|
|
14
15
|
* Use the client `$extends(authz())` returns, and only that one: the client it was made from is refused
|
|
@@ -19,7 +20,7 @@
|
|
|
19
20
|
*/
|
|
20
21
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
21
22
|
import { Prisma } from "@prisma/client/extension";
|
|
22
|
-
import { actAs, calls, idText, signingIn, translate,
|
|
23
|
+
import { actAs, calls, idShown, idText, NotFound, signingIn, translate, } from "@rowstile/client";
|
|
23
24
|
const ARG_TEXT = { scalarType: "string", dbType: undefined, arity: "scalar" };
|
|
24
25
|
// Unless the adapter says otherwise (usePhantomQuery), Prisma sends COMMIT and ROLLBACK itself, and the
|
|
25
26
|
// adapter's commit() and rollback() only give the connection back: so these send them too
|
|
@@ -203,14 +204,19 @@ export function authz(options = {}) {
|
|
|
203
204
|
const table = tableOf(model);
|
|
204
205
|
const key = keyOf(model, args.where);
|
|
205
206
|
if (key !== undefined) {
|
|
206
|
-
const
|
|
207
|
-
if (
|
|
208
|
-
const v = verdict(table, operation, key, why);
|
|
207
|
+
const v = await c.verdict(table, operation, key).catch(() => undefined);
|
|
208
|
+
if (v !== undefined) {
|
|
209
209
|
v.cause = e;
|
|
210
210
|
throw v;
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
213
|
}
|
|
214
|
+
// a read that must find a row and found none: the row isn't there, or this user can't see it (the
|
|
215
|
+
// two answer alike). Left as Prisma's P2025 it is a 500, where an update or a delete is a 404
|
|
216
|
+
if (model && isP2025(e) && (operation === "findUniqueOrThrow" || operation === "findFirstOrThrow")) {
|
|
217
|
+
const key = keyOf(model, args.where);
|
|
218
|
+
throw new NotFound(tableOf(model), key === undefined ? undefined : idShown(key), { cause: e });
|
|
219
|
+
}
|
|
214
220
|
throw translate(e, schemaOf) ?? e;
|
|
215
221
|
}
|
|
216
222
|
},
|
package/package.json
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rowstile/prisma",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.6",
|
|
4
4
|
"description": "rowstile with Prisma: a client extension that signs every query in, and turns refusals into errors",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
|
+
"homepage": "https://rowstile.dev",
|
|
7
|
+
"keywords": ["rowstile", "postgres", "postgresql", "authorization", "row-level-security", "rls", "permissions", "access-control", "rbac", "rebac", "multi-tenant", "prisma"],
|
|
6
8
|
"repository": { "type": "git", "url": "git+https://github.com/rowstile/rowstile.git", "directory": "sdk/typescript/prisma" },
|
|
7
9
|
"type": "module",
|
|
8
10
|
"main": "./dist/index.js",
|
|
@@ -10,6 +12,6 @@
|
|
|
10
12
|
"exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } },
|
|
11
13
|
"files": ["dist"],
|
|
12
14
|
"sideEffects": false,
|
|
13
|
-
"dependencies": { "@rowstile/client": "0.1.0-alpha.
|
|
15
|
+
"dependencies": { "@rowstile/client": "0.1.0-alpha.6" },
|
|
14
16
|
"peerDependencies": { "@prisma/client": ">=7" }
|
|
15
17
|
}
|