Skip to content

Commit 21d718a

Browse files
committed
ni
1 parent 1517921 commit 21d718a

9 files changed

Lines changed: 671 additions & 139 deletions

File tree

assets/picoforge/config/picoforge.yaml

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -123,14 +123,19 @@ mesh:
123123
# constraints/indexes/joins/transactions, sharded by namespace_id % N.
124124
# The core product services use this; sharded_storage stays for KV-shaped
125125
# runtime state (sessions, tokens, caches). See docs/relational-storage.md.
126-
relational_storage:
126+
# Four relational clusters (docs/sharded-relational-storage.md): one
127+
# `relational_storage` instance per cluster. rstore_uid is the uid-sharded
128+
# DATA cluster (users + product tables); the other three are lookup clusters
129+
# sharded by hash(external id). Each is the same generic plugin, its own
130+
# shard files.
131+
rstore_uid:
127132
port: 8213
128133
workers: 4
129134
plugins: [relational_storage]
130135
config:
131136
relational_storage:
132137
shards: 8
133-
path: /tmp/picoforge/relational
138+
path: /tmp/picoforge/rel/uid
134139
# The picoforge PRODUCT data model lives HERE, not in the engine.
135140
# relational_storage is generic: it applies this DDL verbatim to
136141
# each shard on open. Sharded by namespace (the caller's shard key).
@@ -185,6 +190,42 @@ mesh:
185190
- service: trace_collector
186191
port: 8220
187192

193+
# Lookup clusters: map an external id → uid, sharded by hash(id). Tables are
194+
# plugin-owned (accounts/session/token_issuer create them via DDL broadcast).
195+
rstore_username:
196+
port: 8215
197+
workers: 4
198+
plugins: [relational_storage]
199+
config:
200+
relational_storage:
201+
shards: 8
202+
path: /tmp/picoforge/rel/username
203+
remotes:
204+
- service: trace_collector
205+
port: 8220
206+
rstore_session:
207+
port: 8216
208+
workers: 4
209+
plugins: [relational_storage]
210+
config:
211+
relational_storage:
212+
shards: 8
213+
path: /tmp/picoforge/rel/session
214+
remotes:
215+
- service: trace_collector
216+
port: 8220
217+
rstore_token:
218+
port: 8217
219+
workers: 4
220+
plugins: [relational_storage]
221+
config:
222+
relational_storage:
223+
shards: 8
224+
path: /tmp/picoforge/rel/token
225+
remotes:
226+
- service: trace_collector
227+
port: 8220
228+
188229
session:
189230
port: 8203
190231
# Hit on EVERY authenticated /_rpc (token→uid). State is in
@@ -197,11 +238,10 @@ mesh:
197238
remotes:
198239
- service: sharded_storage
199240
port: 8212
200-
# session rows live in relational_storage (issue #18 migration):
201-
# the sid↔access-JWT table is owned by the session plugin and
202-
# reached over yrpc, not collocated.
203-
- service: relational_storage
204-
port: 8213
241+
# session rows live in the rstore_session lookup cluster, sharded by
242+
# hash(session_id); the session plugin owns the table.
243+
- service: rstore_session
244+
port: 8216
205245
- service: trace_collector
206246
port: 8220
207247
- service: token_issuer
@@ -214,9 +254,12 @@ mesh:
214254
remotes:
215255
- service: sharded_storage
216256
port: 8212
217-
# accounts' users/roster rows live in relational_storage (issue #18).
218-
- service: relational_storage
257+
# accounts: users in rstore_uid (sharded by uid); the username→uid
258+
# uniqueness lookup in rstore_username (sharded by hash(username)).
259+
- service: rstore_uid
219260
port: 8213
261+
- service: rstore_username
262+
port: 8215
220263
- service: trace_collector
221264
port: 8220
222265

@@ -249,9 +292,9 @@ mesh:
249292
remotes:
250293
- service: sharded_storage
251294
port: 8212
252-
# refresh_tokens rows live in relational_storage (issue #18).
253-
- service: relational_storage
254-
port: 8213
295+
# refresh_tokens live in rstore_token, sharded by hash(token).
296+
- service: rstore_token
297+
port: 8217
255298
- service: trace_collector
256299
port: 8220
257300
- service: accounts
@@ -577,8 +620,14 @@ mesh:
577620
remotes:
578621
- service: sharded_storage
579622
port: 8212
580-
- service: relational_storage
623+
- service: rstore_uid
581624
port: 8213
625+
- service: rstore_username
626+
port: 8215
627+
- service: rstore_session
628+
port: 8216
629+
- service: rstore_token
630+
port: 8217
582631
- service: trace_collector
583632
port: 8220
584633
- service: session

docs/security.md

Lines changed: 97 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,32 @@ This document defines the Picomesh security design to port from yaapp. It is a
44
target design, not a statement that the current C implementation already
55
matches it.
66

7-
The important yaapp idea is a pluggable auth pipeline on every untrusted
8-
frontend:
7+
The core idea: authentication/authorization is a **core picomesh mechanism run
8+
at the service-invoke boundary**, selected per node by config — not logic baked
9+
into any frontend. Every protocol carries headers, so a frontend's only auth job
10+
is to fold its wire headers into `yheaders` and invoke the target method with
11+
them; the core then runs the configured authn chain + authorizer off those
12+
headers before the method body executes:
913

1014
```text
11-
request
12-
-> frontend authenticator chain
13-
-> JWT or anonymous
14-
-> frontend authorizer(endpoint, args/kwargs, JWT)
15-
-> service invocation with verified auth context
15+
request (any frontend: yhttp / yrpc / msgpack)
16+
-> frontend folds protocol headers into yheaders, invokes the method
17+
-> CORE auth-at-invoke (only when this node's config enables it):
18+
authenticator chain -> JWT or anonymous
19+
authorizer(endpoint, args/kwargs, JWT)
20+
-> verified auth context (JWT claims) replaces the credential in yheaders
21+
-> the resolved service method runs
1622
```
1723

24+
Because the mechanism is core and reads `yheaders` (which every protocol
25+
carries), the same auth works under any frontend with zero per-frontend auth
26+
code. Which authenticators/authorizer a node runs — and therefore whether it is
27+
an auth boundary at all — is that node's own config.
28+
1829
Services such as `session`, `token_issuer`, `accounts`, `password_authn`, and
1930
`personal_access_tokens` are mesh plugins/services. They can run in the same
20-
node as the gateway or in another node reached through the mesh. The frontend
21-
pipeline must call them through configured service paths; it must not hardcode
31+
node as the boundary or in another node reached through the mesh. The auth
32+
mechanism must call them through configured service paths; it must not hardcode
2233
their storage details.
2334

2435
## Yaapp Reference Model
@@ -45,18 +56,22 @@ Picomesh may keep that framework-authorizer shape or deliberately implement an
4556
In Picomesh, "plugin" means a mesh feature/service that can be activated in a
4657
specific node or run in a remote node. Security must preserve that boundary.
4758

48-
The frontend/gateway is responsible for orchestration:
59+
Orchestration is a CORE step at the invoke boundary, not a frontend
60+
responsibility. The frontend only carries headers and invokes; the core, when
61+
this node's config enables auth, does:
4962

50-
- extract a cookie token or bearer token;
63+
- read the credential from `yheaders` (the frontend already folded the wire
64+
headers in — cookie / bearer / forwarded header);
5165
- run the configured authenticator chain;
5266
- receive or verify an access JWT;
5367
- run the configured authorizer;
54-
- invoke the target service only after allow;
55-
- forward only verified auth context/JWT downstream.
68+
- invoke the target service method only after allow;
69+
- replace the credential in `yheaders` with the verified JWT/auth context and
70+
forward only that downstream.
5671

57-
The frontend/gateway must not know how PATs are stored, how session rows are
58-
laid out, or how account memberships are stored. Those are plugin/service
59-
contracts.
72+
Neither the frontend nor the core auth step may know how PATs are stored, how
73+
session rows are laid out, or how account memberships are stored. Those are
74+
plugin/service contracts the configured authenticators reach over RPC.
6075

6176
Low-level library code may exist for crypto/JWT primitives, but it must stay
6277
boring:
@@ -70,50 +85,67 @@ boring:
7085
It must not contain policy decisions such as "fall back to uid if JWT
7186
verification fails".
7287

73-
## Gateway Pattern
88+
## Boundary Pattern (config-selected, not a special frontend)
7489

7590
A deployment may have several frontends: `yhttp`, `yrpc`, future bridge
76-
frontends, and service-console frontends. Any frontend reachable by untrusted
77-
clients is an auth boundary and must run the same security pipeline.
91+
frontends, and service-console frontends. **Whether a node is an auth boundary
92+
is decided by that node's config, not by which frontend it runs.** The
93+
"gateway" is simply a node whose config declares opaque-resolving authenticators
94+
plus a policy authorizer; the same yhttp binary on another node with no
95+
`security` block is a dumb transport carrier. One core mechanism, three config
96+
shapes:
97+
98+
- **boundary** — opaque-resolving authenticators (`session_cookie`,
99+
`bearer_opaque_token`) + an authorizer: resolves the external credential to a
100+
JWT, scrubs the opaque credential, authorizes, forwards JWT-only;
101+
- **internal verifier** — a `bearer_jwt_token` authenticator only: verifies the
102+
JWT already in the prefix and rejects if absent/invalid;
103+
- **trusting internal** — no `security` block: runs no auth-at-invoke and trusts
104+
the upstream-stamped context (safe only behind a boundary).
78105

79106
Typical Picoforge shape:
80107

81108
```text
82-
browser or API client
83-
-> Picoforge webapp or direct gateway client
84-
-> Picomesh gateway frontend
85-
1. authenticate cookie/bearer credential to JWT or anonymous
86-
2. authorize endpoint and arguments
87-
3. invoke active local or remote service
88-
-> backend service plugins
109+
browser or API client (presents an OPAQUE cookie/bearer)
110+
-> Picoforge webapp or direct client
111+
-> a node configured as a BOUNDARY
112+
core auth-at-invoke:
113+
1. resolve cookie/bearer -> JWT (or anonymous), SCRUB the opaque
114+
2. authorize endpoint + arguments
115+
3. invoke active local or remote service with JWT-only context
116+
-> backend service nodes (see only the internal JWT)
89117
```
90118

91-
Backend services that do not run their own auth pipeline are safe only behind
92-
this gateway trust boundary. Their RPC surface must not be reachable by
93-
untrusted clients. If a backend frontend is exposed, it must run the same
94-
pipeline or reject external traffic.
119+
Internally only the JWT travels. The opaque cookie/bearer crosses only the
120+
external edge into the boundary node and is scrubbed there; every hop after that
121+
carries the verified JWT in `yheaders`, never the opaque secret. Backend nodes
122+
without their own auth config are safe only behind a boundary; their RPC surface
123+
must not be reachable by untrusted clients. The external edge stays opaque-only:
124+
a boundary must reject a client-presented raw JWT, so `bearer_jwt_token` is an
125+
internal-verification authenticator, never enabled on the external edge.
95126

96127
## Per-Request Pipeline
97128

98-
When a frontend has security configured, it gates service-call surfaces such as
99-
`/_rpc`, `/_describe`, `/_describe_tree`, and any yrpc/msgpack service-call
100-
entrypoint. HTML/static/debug routes may pass through only if they do not
101-
invoke backend service methods.
129+
When a node has security configured, the core gates every service invocation at
130+
the invoke boundary — independent of frontend. The frontend has already folded
131+
its wire headers into `yheaders` and called the method; the core auth step runs
132+
before the method body. Surfaces gated this way include `/_rpc`, `/_describe`,
133+
`/_describe_tree`, and any yrpc/msgpack service-call entrypoint. HTML/static/debug
134+
routes may pass through only if they do not invoke backend service methods.
102135

103-
For each service call:
136+
For each service invocation:
104137

105138
```text
106-
request
107-
-> parse endpoint path, args/kwargs, headers, cookies
108-
-> run authenticator chain
139+
invoke(method, yheaders, args) (yheaders already carries the credential)
140+
-> run authenticator chain, reading the credential from yheaders
109141
no credential shape matched: jwt = none
110142
credential shape matched and valid: jwt = verified JWT
111143
credential shape matched and invalid: fail 401
112144
-> run authorizer(endpoint, args/kwargs, jwt)
113145
allowed: continue
114146
denied: fail 403
115-
-> attach verified JWT/auth context to request headers/context
116-
-> invoke resolved active service
147+
-> replace the credential in yheaders with the verified JWT/auth context
148+
-> run the resolved active service method
117149
```
118150

119151
The failure rule is security-critical: if a credential of a known shape is
@@ -135,11 +167,14 @@ Authentication answers "who is this caller?" It does not decide whether the
135167
caller may perform the requested operation.
136168

137169
Authenticators are configured by type. Each authenticator owns only one
138-
credential shape.
170+
credential shape, and reads it from `yheaders` — the frontend has already folded
171+
its protocol's cookie / `Authorization` / forwarded headers into the bag, so an
172+
authenticator never parses a wire format and behaves identically under any
173+
frontend.
139174

140-
| Type | Credential read | How it yields a JWT |
175+
| Type | Credential read (from `yheaders`) | How it yields a JWT |
141176
|---|---|---|
142-
| `session_cookie` | configured cookie or same-named forwarded header | calls configured `lookup` RPC, usually `session.session.lookup`, and verifies the returned JWT |
177+
| `session_cookie` | configured cookie or same-named forwarded header | calls configured `lookup` RPC, usually `session.session.jwt`, and verifies the returned JWT |
143178
| `bearer_jwt_token` | `Authorization: Bearer <jwt>` | verifies the JWT signature and expiry directly |
144179
| `bearer_opaque_token` | `Authorization: Bearer <prefix>...` | calls configured `lookup` RPC, such as PAT or runner lookup, and verifies the returned JWT |
145180

@@ -151,7 +186,7 @@ security:
151186
- type: session_cookie
152187
cookie: picomesh-sid
153188
header: picomesh-sid
154-
lookup: session.session.lookup
189+
lookup: session.session.jwt
155190

156191
- type: bearer_jwt_token
157192
header: Authorization
@@ -266,8 +301,8 @@ request can create races with parallel browser requests.
266301

267302
Some methods consume credentials as their arguments:
268303

269-
- `session.session.lookup(session_id)` exchanges an opaque session id for the
270-
stored access JWT.
304+
- `session.session.jwt(session_id)` exchanges an opaque session id for the
305+
stored access JWT (the `session_cookie` authenticator's `lookup`).
271306
- `token_issuer.token_issuer.refresh(refresh_token)` exchanges a refresh token
272307
for a fresh access JWT and rotated refresh token.
273308
- `personal_access_tokens.personal_access_tokens.lookup(token)` exchanges an
@@ -291,9 +326,10 @@ holder of the opaque secret can retrieve a JWT. Prefer one of these designs:
291326
Authorization answers: "may this caller invoke this endpoint with these
292327
arguments?"
293328

294-
Picomesh should support a frontend-configured authorizer. The yaapp-compatible
295-
default is a policy authorizer keyed by endpoint name. Endpoints absent from
296-
policy are denied by default.
329+
Picomesh runs a config-selected authorizer at the core invoke boundary (per
330+
node, from that node's `security` config). The yaapp-compatible default is a
331+
policy authorizer keyed by endpoint name. Endpoints absent from policy are
332+
denied by default.
297333

298334
Policy example:
299335

@@ -396,11 +432,12 @@ The current implementation only partially matches this design.
396432

397433
Known mismatches:
398434

399-
- `src/picomesh/frontends/yhttp/authz.c` mixes the authenticator chain,
400-
hardcoded credential-exchange calls, runner/PAT special cases, and policy
401-
evaluation inside the yhttp frontend. The yaapp-compatible target is a
402-
reusable frontend pipeline with configured authenticators and a configured
403-
authorizer.
435+
- `src/picomesh/frontends/yhttp/frontend.c` builds and runs the authenticator
436+
chain + authorizer (`gateway_security`, the auth block in `route_json_rpc`)
437+
inside the yhttp frontend, and legacy mutation routes resolve identity
438+
themselves. The target moves that mechanism into the CORE invoke boundary,
439+
config-selected per node, so every frontend gets it by carrying headers and
440+
invoking — no per-frontend auth code.
404441
- The yhttp authenticator code knows PAT and runner internals instead of using
405442
generic `bearer_opaque_token` entries with configured `lookup` service paths.
406443
- `src/picomesh/ysecurity/secret.c` is low-level helper code but currently
@@ -424,8 +461,10 @@ Known mismatches:
424461

425462
1. Keep `ysecurity` as a low-level library only. Remove policy decisions and
426463
any invalid-JWT fallback behavior from it.
427-
2. Introduce a reusable frontend auth pipeline shared by yhttp, yrpc/msgpack,
428-
bridges, and future service-console frontends.
464+
2. Move the auth pipeline into the CORE invoke boundary (not a frontend), so
465+
yhttp, yrpc/msgpack, bridges, and future frontends share it by carrying
466+
headers and invoking — no per-frontend auth code. Boundary vs internal vs
467+
trusting is each node's own `security` config.
429468
3. Implement configured authenticator modules:
430469
`session_cookie`, `bearer_jwt_token`, and generic
431470
`bearer_opaque_token`.
@@ -455,8 +494,8 @@ The target design provides:
455494
- default deny for methods absent from policy;
456495
- no credential downgrade on malformed tokens;
457496
- clear separation of authn and authz;
458-
- clear separation between frontend pipeline and mesh service plugins;
459-
- one gateway/frontend policy decision before service invocation;
497+
- clear separation between the core auth mechanism and the mesh service plugins;
498+
- one core policy decision at the invoke boundary before service invocation;
460499
- short-lived JWT claims with refresh/login updates;
461500
- browser sessions that expose only opaque HttpOnly cookies;
462501
- support for API clients using bearer JWTs or bearer opaque tokens;

0 commit comments

Comments
 (0)