@_linked/server 2.22.0 → 2.23.0
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/CHANGELOG.md +13 -0
- package/lib/esm/backend.js +6 -5
- package/lib/esm/backend.js.map +1 -1
- package/lib/esm/utils/queryPlane.d.ts +2 -21
- package/lib/esm/utils/queryPlane.js +10 -89
- package/lib/esm/utils/queryPlane.js.map +1 -1
- package/lib/esm/utils/rawQueries.d.ts +3 -3
- package/lib/esm/utils/rawQueries.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @\_linked/server
|
|
2
2
|
|
|
3
|
+
## 2.23.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#138](https://github.com/linked-fw/server/pull/138) [`e1cc552`](https://github.com/linked-fw/server/commit/e1cc5523c5147830c7b363a72b7d225396418835) Thanks [@flyon](https://github.com/flyon)! - The generic query endpoints (`/call/@_linked/server/*Query` and `LincdAPI` `/api/*`) now apply the per-store access rules of `@_linked/server-utils` 1.11 (`withAccess`), declared on the stores in the app's `linked.backend.storage.js`. A store without a rule requires a session.
|
|
8
|
+
|
|
9
|
+
**This access model is interim. It needs to be revised and rethought from the ground up.**
|
|
10
|
+
|
|
11
|
+
- Removed from `utils/queryPlane`: `setProtectedNodeProbe`, `askProtectedNodes` and `protectedNodeAsk`. The server no longer runs an `ASK` against the store before a mutation.
|
|
12
|
+
- Kept: `@ctx` references are refused (400), the query that was checked is the builder the store runs, an operation must match its query's kind, a mutation may not choose the ids of new nodes, and `server.rawQueries` decides raw SPARQL.
|
|
13
|
+
- Queries providers run themselves are not checked.
|
|
14
|
+
- Requires `@_linked/server-utils` ^1.11.0.
|
|
15
|
+
|
|
3
16
|
## 2.22.0
|
|
4
17
|
|
|
5
18
|
### Minor Changes
|
package/lib/esm/backend.js
CHANGED
|
@@ -54,11 +54,12 @@ export default class LincdServerBackendProvider extends BackendProvider {
|
|
|
54
54
|
// `LinkedStorage` then routes it by the QUERY's shape — which is, and always was, what
|
|
55
55
|
// decides the target dataset. The store instance never participated.
|
|
56
56
|
//
|
|
57
|
-
// Every query is checked against the
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
57
|
+
// Every query is checked first against the access rules of the stores it
|
|
58
|
+
// maps to, declared in the app's storage config (`withAccess`; a store with
|
|
59
|
+
// no rule requires a session): see utils/queryPlane. INTERIM model, to be
|
|
60
|
+
// rethought. The store runs the builder that was checked. They are declared
|
|
61
|
+
// 'public' because that check, not the RPC layer, decides whether a session
|
|
62
|
+
// is required.
|
|
62
63
|
async selectQuery(json) {
|
|
63
64
|
const query = toQueryBuilder(json);
|
|
64
65
|
await authorizeGenericQuery('select', json, query, 'selectQuery');
|
package/lib/esm/backend.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backend.js","sourceRoot":"","sources":["../../src/backend.ts"],"names":[],"mappings":";;;;;;;;;AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,6CAA6C,CAAC;AAC9E,OAAO,EAAE,QAAQ,EAAE,MAAM,sCAAsC,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,MAAM,uCAAuC,CAAC;AAC1E,OAAO,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAClE,OAAO,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAE,qBAAqB,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC9E,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,MAAM,aAAa,CAAC;AAE7B,MAAM,CAAC,OAAO,OAAO,0BAA2B,SAAQ,eAAe;IACrE,KAAK,CAAC,sBAAsB;QAC1B,IAAI,CAAC,iBAAiB,CAAC,eAAe,EAAE,EAAE,CAAC;YACzC,iBAAiB,CAAC,eAAe,CAC/B,IAAI,cAAc,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,GAAG,YAAY,CAAC,CACxD,CAAC;QACJ,CAAC;QACD,IAAI,oBAAoB,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;QAC3D,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;QACxC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,oBAAoB,EAAE,YAAY,CAAC,EAAE;gBAC5D,SAAS,EAAE,IAAI;aAChB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,yEAAyE;IACzE,8EAA8E;IAC9E,iDAAiD;IACjD,SAAS;QACP,OAAO,aAAa,EAAE,CAAC;IACzB,CAAC;IAED,4EAA4E;IAC5E,EAAE;IACF,2DAA2D;IAC3D,gFAAgF;IAChF,6EAA6E;IAC7E,EAAE;IACF,mFAAmF;IACnF,qFAAqF;IACrF,uFAAuF;IACvF,uFAAuF;IACvF,4DAA4D;IAC5D,EAAE;IACF,sFAAsF;IACtF,4EAA4E;IAC5E,uFAAuF;IACvF,qEAAqE;IAErE,EAAE;IACF,yEAAyE;IACzE,
|
|
1
|
+
{"version":3,"file":"backend.js","sourceRoot":"","sources":["../../src/backend.ts"],"names":[],"mappings":"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","sourcesContent":["import { BackendProvider } from '@_linked/server-utils/utils/BackendProvider';\nimport { callable } from '@_linked/server-utils/utils/callable';\nimport { LinkedFileStorage } from '@_linked/core/utils/LinkedFileStorage';\nimport { LinkedStorage } from '@_linked/core/utils/LinkedStorage';\nimport { LocalFileStore } from './shapes/filestores/LocalFileStore.js';\nimport { getShapeIndex } from './utils/Shapes.js';\nimport { authorizeGenericQuery, toQueryBuilder } from './utils/queryPlane.js';\nimport path from 'path';\nimport fs from 'fs/promises';\n\nexport default class LincdServerBackendProvider extends BackendProvider {\n async setupBeforeControllers() {\n if (!LinkedFileStorage.getDefaultStore()) {\n LinkedFileStorage.setDefaultStore(\n new LocalFileStore(process.env.NODE_ENV + '-filestore')\n );\n }\n let fileSystemUploadPath = path.join('data', 'filestores');\n try {\n await fs.access(fileSystemUploadPath);\n } catch (err) {\n await fs.mkdir(path.join(fileSystemUploadPath, 'filestores'), {\n recursive: true,\n });\n }\n }\n\n // Not callable over RPC: no client calls it. Clients read the same index\n // through LincdAPI's `/api/all-shapes` and `/api/shape-details`; backend code\n // can still reach it with a local `Server.call`.\n getShapes() {\n return getShapeIndex();\n }\n\n // ── BackendAPIStore query execution ──────────────────────────────────────\n //\n // These answer the frontend `BackendAPIStore`, which calls\n // `Server.call(packageName, {method}, json)` — the PACKAGE form, so the request\n // arrives here on the generic provider rather than through Shape resolution.\n //\n // They used to live on `BackendAPIStoreProvider`, a `ShapeProvider`, whose methods\n // took the store as a first argument and never read it. Reaching that provider meant\n // the transport had to carry a `shapeURI` and an `instanceNode`, and the server had to\n // rebuild the store with `new (providerShapeClass)({id})` — instantiating a Shape only\n // to discard it. Addressing by package removes all of that.\n //\n // A query cannot cross the wire live (core 2.10.0 contract flip), so the client ships\n // `toJSON()` and `fromJSON()` rehydrates it here into a live, closed query.\n // `LinkedStorage` then routes it by the QUERY's shape — which is, and always was, what\n // decides the target dataset. The store instance never participated.\n\n //\n // Every query is checked first against the access rules of the stores it\n // maps to, declared in the app's storage config (`withAccess`; a store with\n // no rule requires a session): see utils/queryPlane. INTERIM model, to be\n // rethought. The store runs the builder that was checked. They are declared\n // 'public' because that check, not the RPC layer, decides whether a session\n // is required.\n\n @callable('public')\n async selectQuery(json: any) {\n const query = toQueryBuilder(json);\n await authorizeGenericQuery('select', json, query, 'selectQuery');\n return LinkedStorage.selectQuery(query);\n }\n\n @callable('public')\n async askQuery(json: any) {\n const query = toQueryBuilder(json);\n await authorizeGenericQuery('ask', json, query, 'askQuery');\n return LinkedStorage.askQuery(query);\n }\n\n @callable('public')\n async updateQuery(json: any) {\n const query = toQueryBuilder(json);\n await authorizeGenericQuery('update', json, query, 'updateQuery');\n return LinkedStorage.updateQuery(query);\n }\n\n @callable('public')\n async createQuery(json: any) {\n const query = toQueryBuilder(json);\n await authorizeGenericQuery('create', json, query, 'createQuery');\n return LinkedStorage.createQuery(query);\n }\n\n @callable('public')\n async deleteQuery(json: any) {\n const query = toQueryBuilder(json);\n await authorizeGenericQuery('delete', json, query, 'deleteQuery');\n return LinkedStorage.deleteQuery(query);\n }\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type QueryOperation } from '@_linked/server-utils/utils/QueryAccess';
|
|
2
2
|
/**
|
|
3
3
|
* Whether DSL-JSON carries a query-context reference (`{"@ctx": name}`) in any
|
|
4
4
|
* position: a select or count subject, an update target, a delete id, a
|
|
@@ -26,28 +26,9 @@ export declare function containsContextRef(json: unknown): boolean;
|
|
|
26
26
|
export declare function toQueryBuilder(query: any): any;
|
|
27
27
|
/** The lowered IR of a query, or `undefined` when it cannot be lowered (refused). */
|
|
28
28
|
export declare function lowerForAccessCheck(builder: any): unknown;
|
|
29
|
-
/**
|
|
30
|
-
* The `ASK` that answers a protected-node check: is any node the mutation would
|
|
31
|
-
* write or delete typed with a protected class, or a subclass of one?
|
|
32
|
-
* `undefined` when there is nothing to ask.
|
|
33
|
-
*/
|
|
34
|
-
export declare function protectedNodeAsk(check: ProtectedNodeCheck): string | undefined;
|
|
35
|
-
/**
|
|
36
|
-
* The default protected-node lookup: one `ASK` on the dataset the mutation's
|
|
37
|
-
* shape routes to (run in the current call's context, so a routing dataset
|
|
38
|
-
* resolves it exactly as it will resolve the mutation). A dataset that cannot
|
|
39
|
-
* run SPARQL (`rawQuery`) cannot be checked, and the mutation is refused.
|
|
40
|
-
*/
|
|
41
|
-
export declare const askProtectedNodes: ProtectedNodeProbe;
|
|
42
|
-
/**
|
|
43
|
-
* Replace the protected-node lookup (see `checkQueryAccess`): an app whose
|
|
44
|
-
* protected nodes live in a known store can answer it there. Pass `undefined`
|
|
45
|
-
* to restore the default `ASK`.
|
|
46
|
-
*/
|
|
47
|
-
export declare function setProtectedNodeProbe(fn: ProtectedNodeProbe | undefined): void;
|
|
48
29
|
/**
|
|
49
30
|
* Check `builder` (rehydrated from `query` with `toQueryBuilder`) against the
|
|
50
|
-
*
|
|
31
|
+
* store access rules. Throws a `ServerCallError` (400/401/403) when the current
|
|
51
32
|
* call may not run it; the caller then hands the same builder to the store.
|
|
52
33
|
*
|
|
53
34
|
* Deliberately returns nothing: a query builder is a thenable, so resolving an
|
|
@@ -3,16 +3,18 @@
|
|
|
3
3
|
* client built (`LincdServerBackendProvider.*Query` behind
|
|
4
4
|
* `/call/@_linked/server/*Query`, and `LincdAPI` `post_*` behind `/api/*`).
|
|
5
5
|
*
|
|
6
|
-
* The rules
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* The rules are declared per store in the app's storage config
|
|
7
|
+
* (`linked.backend.storage.js`) with `withAccess` from
|
|
8
|
+
* `@_linked/server-utils/utils/QueryAccess`; see `checkQueryAccess` there for
|
|
9
|
+
* how a query is mapped to stores and the order the checks apply in. This
|
|
10
|
+
* module turns the incoming query into the builder the store runs and the IR
|
|
11
|
+
* the rules read.
|
|
12
|
+
*
|
|
13
|
+
* INTERIM: that per-store access model needs to be revised and rethought from
|
|
14
|
+
* the ground up (see QueryAccess).
|
|
11
15
|
*/
|
|
12
16
|
import { fromJSON } from '@_linked/core';
|
|
13
17
|
import { lower } from '@_linked/core/queries/lower';
|
|
14
|
-
import { LinkedStorage } from '@_linked/core/utils/LinkedStorage';
|
|
15
|
-
import { getShapeClass } from '@_linked/core/utils/ShapeClass';
|
|
16
18
|
import { checkQueryAccess, } from '@_linked/server-utils/utils/QueryAccess';
|
|
17
19
|
import { ServerCallError } from '@_linked/server-utils/utils/ServerCallError';
|
|
18
20
|
import { getRawQueriesMode } from './rawQueries.js';
|
|
@@ -92,89 +94,9 @@ export function lowerForAccessCheck(builder) {
|
|
|
92
94
|
return undefined;
|
|
93
95
|
}
|
|
94
96
|
}
|
|
95
|
-
const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
|
|
96
|
-
const RDFS_SUBCLASS_OF = 'http://www.w3.org/2000/01/rdf-schema#subClassOf';
|
|
97
|
-
/** `<iri>`, refusing anything that is not a plain absolute IRI. */
|
|
98
|
-
function iriTerm(value) {
|
|
99
|
-
if (typeof value !== 'string' ||
|
|
100
|
-
!/^[A-Za-z][A-Za-z0-9+.-]*:[^\s<>"{}|^`\\\u0000-\u001f\u007f]*$/.test(value)) {
|
|
101
|
-
throw new ServerCallError(400, 'Invalid node id');
|
|
102
|
-
}
|
|
103
|
-
return `<${value}>`;
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* The `ASK` that answers a protected-node check: is any node the mutation would
|
|
107
|
-
* write or delete typed with a protected class, or a subclass of one?
|
|
108
|
-
* `undefined` when there is nothing to ask.
|
|
109
|
-
*/
|
|
110
|
-
export function protectedNodeAsk(check) {
|
|
111
|
-
if (!check.classes.length)
|
|
112
|
-
return undefined;
|
|
113
|
-
const branches = [];
|
|
114
|
-
const owned = check.containsPredicates.length
|
|
115
|
-
? '(' + check.containsPredicates.map(iriTerm).join('|') + ')+'
|
|
116
|
-
: undefined;
|
|
117
|
-
if (check.ids.length) {
|
|
118
|
-
branches.push(`{ VALUES ?n { ${check.ids.map(iriTerm).join(' ')} } }`);
|
|
119
|
-
}
|
|
120
|
-
if (owned && check.ownerIds.length) {
|
|
121
|
-
branches.push(`{ VALUES ?owner { ${check.ownerIds.map(iriTerm).join(' ')} } ?owner ${owned} ?n . }`);
|
|
122
|
-
}
|
|
123
|
-
for (const cls of check.scanClasses) {
|
|
124
|
-
branches.push(`{ ?n <${RDF_TYPE}> ${iriTerm(cls)} . }`);
|
|
125
|
-
if (owned)
|
|
126
|
-
branches.push(`{ ?s <${RDF_TYPE}> ${iriTerm(cls)} . ?s ${owned} ?n . }`);
|
|
127
|
-
}
|
|
128
|
-
if (!branches.length)
|
|
129
|
-
return undefined;
|
|
130
|
-
return (`ASK WHERE {\n ${branches.join('\n UNION ')}\n` +
|
|
131
|
-
` ?n <${RDF_TYPE}> ?t .\n` +
|
|
132
|
-
` VALUES ?c { ${check.classes.map(iriTerm).join(' ')} }\n` +
|
|
133
|
-
` ?t <${RDFS_SUBCLASS_OF}>* ?c .\n}`);
|
|
134
|
-
}
|
|
135
|
-
/**
|
|
136
|
-
* The default protected-node lookup: one `ASK` on the dataset the mutation's
|
|
137
|
-
* shape routes to (run in the current call's context, so a routing dataset
|
|
138
|
-
* resolves it exactly as it will resolve the mutation). A dataset that cannot
|
|
139
|
-
* run SPARQL (`rawQuery`) cannot be checked, and the mutation is refused.
|
|
140
|
-
*/
|
|
141
|
-
export const askProtectedNodes = async (check) => {
|
|
142
|
-
var _a;
|
|
143
|
-
const sparql = protectedNodeAsk(check);
|
|
144
|
-
if (!sparql)
|
|
145
|
-
return false;
|
|
146
|
-
let shapeClass;
|
|
147
|
-
try {
|
|
148
|
-
shapeClass = getShapeClass(check.shape);
|
|
149
|
-
}
|
|
150
|
-
catch (_b) {
|
|
151
|
-
shapeClass = undefined;
|
|
152
|
-
}
|
|
153
|
-
const dataset = (_a = LinkedStorage.getDatasetForShapeClass(shapeClass)) !== null && _a !== void 0 ? _a : LinkedStorage.getDefaultDataset();
|
|
154
|
-
if (!dataset || typeof dataset.rawQuery !== 'function') {
|
|
155
|
-
throw new Error(`the dataset for ${check.shape} cannot run SPARQL, so the nodes this mutation writes cannot be checked`);
|
|
156
|
-
}
|
|
157
|
-
const json = await dataset.rawQuery(sparql);
|
|
158
|
-
if (typeof (json === null || json === void 0 ? void 0 : json.boolean) !== 'boolean') {
|
|
159
|
-
throw new Error('the dataset did not answer the ASK with a boolean');
|
|
160
|
-
}
|
|
161
|
-
return json.boolean;
|
|
162
|
-
};
|
|
163
|
-
let probe = askProtectedNodes;
|
|
164
|
-
/**
|
|
165
|
-
* Replace the protected-node lookup (see `checkQueryAccess`): an app whose
|
|
166
|
-
* protected nodes live in a known store can answer it there. Pass `undefined`
|
|
167
|
-
* to restore the default `ASK`.
|
|
168
|
-
*/
|
|
169
|
-
export function setProtectedNodeProbe(fn) {
|
|
170
|
-
if (fn !== undefined && typeof fn !== 'function') {
|
|
171
|
-
throw new TypeError('setProtectedNodeProbe: expected a function');
|
|
172
|
-
}
|
|
173
|
-
probe = fn !== null && fn !== void 0 ? fn : askProtectedNodes;
|
|
174
|
-
}
|
|
175
97
|
/**
|
|
176
98
|
* Check `builder` (rehydrated from `query` with `toQueryBuilder`) against the
|
|
177
|
-
*
|
|
99
|
+
* store access rules. Throws a `ServerCallError` (400/401/403) when the current
|
|
178
100
|
* call may not run it; the caller then hands the same builder to the store.
|
|
179
101
|
*
|
|
180
102
|
* Deliberately returns nothing: a query builder is a thenable, so resolving an
|
|
@@ -187,7 +109,6 @@ export async function authorizeGenericQuery(operation, query, builder, endpoint)
|
|
|
187
109
|
ir: lowerForAccessCheck(builder),
|
|
188
110
|
mode: getRpcExposureMode(),
|
|
189
111
|
endpoint,
|
|
190
|
-
probeProtectedNodes: probe,
|
|
191
112
|
});
|
|
192
113
|
}
|
|
193
114
|
/** Check a raw SPARQL query against the `rawQueries` setting (see utils/rawQueries). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"queryPlane.js","sourceRoot":"","sources":["../../../src/utils/queryPlane.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Authorization for the generic query plane: the endpoints that run a query the\n * client built (`LincdServerBackendProvider.*Query` behind\n * `/call/@_linked/server/*Query`, and `LincdAPI` `post_*` behind `/api/*`).\n *\n * The rules themselves (protected shapes, authorizers) are registered through\n * `@_linked/server-utils/utils/QueryAccess`; see `checkQueryAccess` there for the\n * order they apply in. This module turns the incoming query into the builder the\n * store runs and the IR the rules read, and answers the rules' protected-node\n * lookup with a SPARQL `ASK` against the store the mutation routes to.\n */\nimport { fromJSON } from '@_linked/core';\nimport { lower } from '@_linked/core/queries/lower';\nimport { LinkedStorage } from '@_linked/core/utils/LinkedStorage';\nimport { getShapeClass } from '@_linked/core/utils/ShapeClass';\nimport {\n checkQueryAccess,\n type ProtectedNodeCheck,\n type ProtectedNodeProbe,\n type QueryOperation,\n} from '@_linked/server-utils/utils/QueryAccess';\nimport { ServerCallError } from '@_linked/server-utils/utils/ServerCallError';\nimport { getRawQueriesMode } from './rawQueries.js';\nimport { getRpcExposureMode } from './rpcExposure.js';\n\n/** The DSL-JSON tag of a query-context reference (`CONTEXT_REF_KEY` in core). */\nconst CONTEXT_REF_KEY = '@ctx';\n\n/**\n * Whether DSL-JSON carries a query-context reference (`{\"@ctx\": name}`) in any\n * position: a select or count subject, an update target, a delete id, a\n * where-clause operand or a mutation field value. Every DSL-JSON decoder in\n * core reads a context reference from this one key.\n */\nexport function containsContextRef(json: unknown): boolean {\n const seen = new Set<object>();\n const stack: unknown[] = [json];\n while (stack.length) {\n const value = stack.pop();\n if (!value || typeof value !== 'object' || seen.has(value)) continue;\n seen.add(value);\n if (Array.isArray(value)) {\n for (const item of value) stack.push(item);\n continue;\n }\n if (Object.prototype.hasOwnProperty.call(value, CONTEXT_REF_KEY)) return true;\n for (const key of Object.keys(value)) stack.push((value as any)[key]);\n }\n return false;\n}\n\n/**\n * A rehydrated query builder from the DSL-JSON a client sent. The result is\n * what the store runs, so the access check and the store see the same query.\n * A body that is not a query answers 400.\n *\n * A query-context reference (`{\"@ctx\": \"user\"}`) also answers 400. Core\n * resolves one at lowering time against a process-wide context map, which on a\n * server holds whatever some code last set there, not the caller's identity,\n * and lowering runs twice (once for the access check, once in the store), so\n * the two could even resolve it differently. A query built from client JSON\n * names its nodes by id.\n *\n * Without context references, lowering a rehydrated builder is a pure function\n * of the builder (closed and never mutated after `fromJSON`) and the shape\n * registry: the IR `authorizeGenericQuery` checks is the IR the store lowers\n * from the same builder.\n */\nexport function toQueryBuilder(query: any): any {\n // A live builder passed by backend code (never a plain object from JSON).\n if (\n query &&\n typeof query === 'object' &&\n Object.getPrototypeOf(query) !== Object.prototype &&\n typeof query.__queryKind === 'string'\n ) {\n return query;\n }\n if (containsContextRef(query)) {\n throw new ServerCallError(\n 400,\n 'Query context references (@ctx) are not accepted by the generic query endpoints'\n );\n }\n try {\n const builder = fromJSON(query);\n if (!builder || typeof builder !== 'object') throw new Error('not a query');\n return builder;\n } catch {\n throw new ServerCallError(400, 'Invalid query');\n }\n}\n\n/** The lowered IR of a query, or `undefined` when it cannot be lowered (refused). */\nexport function lowerForAccessCheck(builder: any): unknown {\n try {\n return lower(builder);\n } catch {\n return undefined;\n }\n}\n\nconst RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';\nconst RDFS_SUBCLASS_OF = 'http://www.w3.org/2000/01/rdf-schema#subClassOf';\n\n/** `<iri>`, refusing anything that is not a plain absolute IRI. */\nfunction iriTerm(value: unknown): string {\n if (\n typeof value !== 'string' ||\n !/^[A-Za-z][A-Za-z0-9+.-]*:[^\\s<>\"{}|^`\\\\\\u0000-\\u001f\\u007f]*$/.test(value)\n ) {\n throw new ServerCallError(400, 'Invalid node id');\n }\n return `<${value}>`;\n}\n\n/**\n * The `ASK` that answers a protected-node check: is any node the mutation would\n * write or delete typed with a protected class, or a subclass of one?\n * `undefined` when there is nothing to ask.\n */\nexport function protectedNodeAsk(check: ProtectedNodeCheck): string | undefined {\n if (!check.classes.length) return undefined;\n const branches: string[] = [];\n const owned = check.containsPredicates.length\n ? '(' + check.containsPredicates.map(iriTerm).join('|') + ')+'\n : undefined;\n if (check.ids.length) {\n branches.push(`{ VALUES ?n { ${check.ids.map(iriTerm).join(' ')} } }`);\n }\n if (owned && check.ownerIds.length) {\n branches.push(\n `{ VALUES ?owner { ${check.ownerIds.map(iriTerm).join(' ')} } ?owner ${owned} ?n . }`\n );\n }\n for (const cls of check.scanClasses) {\n branches.push(`{ ?n <${RDF_TYPE}> ${iriTerm(cls)} . }`);\n if (owned) branches.push(`{ ?s <${RDF_TYPE}> ${iriTerm(cls)} . ?s ${owned} ?n . }`);\n }\n if (!branches.length) return undefined;\n return (\n `ASK WHERE {\\n ${branches.join('\\n UNION ')}\\n` +\n ` ?n <${RDF_TYPE}> ?t .\\n` +\n ` VALUES ?c { ${check.classes.map(iriTerm).join(' ')} }\\n` +\n ` ?t <${RDFS_SUBCLASS_OF}>* ?c .\\n}`\n );\n}\n\n/**\n * The default protected-node lookup: one `ASK` on the dataset the mutation's\n * shape routes to (run in the current call's context, so a routing dataset\n * resolves it exactly as it will resolve the mutation). A dataset that cannot\n * run SPARQL (`rawQuery`) cannot be checked, and the mutation is refused.\n */\nexport const askProtectedNodes: ProtectedNodeProbe = async (check) => {\n const sparql = protectedNodeAsk(check);\n if (!sparql) return false;\n let shapeClass: any;\n try {\n shapeClass = getShapeClass(check.shape as any);\n } catch {\n shapeClass = undefined;\n }\n const dataset: any =\n LinkedStorage.getDatasetForShapeClass(shapeClass) ?? LinkedStorage.getDefaultDataset();\n if (!dataset || typeof dataset.rawQuery !== 'function') {\n throw new Error(\n `the dataset for ${check.shape} cannot run SPARQL, so the nodes this mutation writes cannot be checked`\n );\n }\n const json = await dataset.rawQuery(sparql);\n if (typeof json?.boolean !== 'boolean') {\n throw new Error('the dataset did not answer the ASK with a boolean');\n }\n return json.boolean;\n};\n\nlet probe: ProtectedNodeProbe = askProtectedNodes;\n\n/**\n * Replace the protected-node lookup (see `checkQueryAccess`): an app whose\n * protected nodes live in a known store can answer it there. Pass `undefined`\n * to restore the default `ASK`.\n */\nexport function setProtectedNodeProbe(fn: ProtectedNodeProbe | undefined): void {\n if (fn !== undefined && typeof fn !== 'function') {\n throw new TypeError('setProtectedNodeProbe: expected a function');\n }\n probe = fn ?? askProtectedNodes;\n}\n\n/**\n * Check `builder` (rehydrated from `query` with `toQueryBuilder`) against the\n * generic-plane rules. Throws a `ServerCallError` (400/401/403) when the current\n * call may not run it; the caller then hands the same builder to the store.\n *\n * Deliberately returns nothing: a query builder is a thenable, so resolving an\n * async function with one would run it.\n */\nexport async function authorizeGenericQuery(\n operation: QueryOperation,\n query: unknown,\n builder: any,\n endpoint: string\n): Promise<void> {\n await checkQueryAccess({\n operation,\n query,\n ir: lowerForAccessCheck(builder),\n mode: getRpcExposureMode(),\n endpoint,\n probeProtectedNodes: probe,\n });\n}\n\n/** Check a raw SPARQL query against the `rawQueries` setting (see utils/rawQueries). */\nexport async function authorizeRawQuery(query: unknown, endpoint: string): Promise<void> {\n if (typeof query !== 'string') {\n throw new ServerCallError(400, 'Invalid query');\n }\n await checkQueryAccess({\n operation: 'select',\n query,\n ir: undefined,\n raw: true,\n rawQueries: getRawQueriesMode(),\n mode: getRpcExposureMode(),\n endpoint,\n });\n}\n"]}
|
|
1
|
+
{"version":3,"file":"queryPlane.js","sourceRoot":"","sources":["../../../src/utils/queryPlane.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Authorization for the generic query plane: the endpoints that run a query the\n * client built (`LincdServerBackendProvider.*Query` behind\n * `/call/@_linked/server/*Query`, and `LincdAPI` `post_*` behind `/api/*`).\n *\n * The rules are declared per store in the app's storage config\n * (`linked.backend.storage.js`) with `withAccess` from\n * `@_linked/server-utils/utils/QueryAccess`; see `checkQueryAccess` there for\n * how a query is mapped to stores and the order the checks apply in. This\n * module turns the incoming query into the builder the store runs and the IR\n * the rules read.\n *\n * INTERIM: that per-store access model needs to be revised and rethought from\n * the ground up (see QueryAccess).\n */\nimport { fromJSON } from '@_linked/core';\nimport { lower } from '@_linked/core/queries/lower';\nimport {\n checkQueryAccess,\n type QueryOperation,\n} from '@_linked/server-utils/utils/QueryAccess';\nimport { ServerCallError } from '@_linked/server-utils/utils/ServerCallError';\nimport { getRawQueriesMode } from './rawQueries.js';\nimport { getRpcExposureMode } from './rpcExposure.js';\n\n/** The DSL-JSON tag of a query-context reference (`CONTEXT_REF_KEY` in core). */\nconst CONTEXT_REF_KEY = '@ctx';\n\n/**\n * Whether DSL-JSON carries a query-context reference (`{\"@ctx\": name}`) in any\n * position: a select or count subject, an update target, a delete id, a\n * where-clause operand or a mutation field value. Every DSL-JSON decoder in\n * core reads a context reference from this one key.\n */\nexport function containsContextRef(json: unknown): boolean {\n const seen = new Set<object>();\n const stack: unknown[] = [json];\n while (stack.length) {\n const value = stack.pop();\n if (!value || typeof value !== 'object' || seen.has(value)) continue;\n seen.add(value);\n if (Array.isArray(value)) {\n for (const item of value) stack.push(item);\n continue;\n }\n if (Object.prototype.hasOwnProperty.call(value, CONTEXT_REF_KEY)) return true;\n for (const key of Object.keys(value)) stack.push((value as any)[key]);\n }\n return false;\n}\n\n/**\n * A rehydrated query builder from the DSL-JSON a client sent. The result is\n * what the store runs, so the access check and the store see the same query.\n * A body that is not a query answers 400.\n *\n * A query-context reference (`{\"@ctx\": \"user\"}`) also answers 400. Core\n * resolves one at lowering time against a process-wide context map, which on a\n * server holds whatever some code last set there, not the caller's identity,\n * and lowering runs twice (once for the access check, once in the store), so\n * the two could even resolve it differently. A query built from client JSON\n * names its nodes by id.\n *\n * Without context references, lowering a rehydrated builder is a pure function\n * of the builder (closed and never mutated after `fromJSON`) and the shape\n * registry: the IR `authorizeGenericQuery` checks is the IR the store lowers\n * from the same builder.\n */\nexport function toQueryBuilder(query: any): any {\n // A live builder passed by backend code (never a plain object from JSON).\n if (\n query &&\n typeof query === 'object' &&\n Object.getPrototypeOf(query) !== Object.prototype &&\n typeof query.__queryKind === 'string'\n ) {\n return query;\n }\n if (containsContextRef(query)) {\n throw new ServerCallError(\n 400,\n 'Query context references (@ctx) are not accepted by the generic query endpoints'\n );\n }\n try {\n const builder = fromJSON(query);\n if (!builder || typeof builder !== 'object') throw new Error('not a query');\n return builder;\n } catch {\n throw new ServerCallError(400, 'Invalid query');\n }\n}\n\n/** The lowered IR of a query, or `undefined` when it cannot be lowered (refused). */\nexport function lowerForAccessCheck(builder: any): unknown {\n try {\n return lower(builder);\n } catch {\n return undefined;\n }\n}\n\n/**\n * Check `builder` (rehydrated from `query` with `toQueryBuilder`) against the\n * store access rules. Throws a `ServerCallError` (400/401/403) when the current\n * call may not run it; the caller then hands the same builder to the store.\n *\n * Deliberately returns nothing: a query builder is a thenable, so resolving an\n * async function with one would run it.\n */\nexport async function authorizeGenericQuery(\n operation: QueryOperation,\n query: unknown,\n builder: any,\n endpoint: string\n): Promise<void> {\n await checkQueryAccess({\n operation,\n query,\n ir: lowerForAccessCheck(builder),\n mode: getRpcExposureMode(),\n endpoint,\n });\n}\n\n/** Check a raw SPARQL query against the `rawQueries` setting (see utils/rawQueries). */\nexport async function authorizeRawQuery(query: unknown, endpoint: string): Promise<void> {\n if (typeof query !== 'string') {\n throw new ServerCallError(400, 'Invalid query');\n }\n await checkQueryAccess({\n operation: 'select',\n query,\n ir: undefined,\n raw: true,\n rawQueries: getRawQueriesMode(),\n mode: getRpcExposureMode(),\n endpoint,\n });\n}\n"]}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Who may run raw SPARQL through the generic query plane (`/api/select-raw`).
|
|
3
3
|
*
|
|
4
|
-
* Raw SPARQL cannot be analysed for the shapes it touches, so the
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Raw SPARQL cannot be analysed for the shapes it touches, so the per-store
|
|
5
|
+
* access rules of `@_linked/server-utils/utils/QueryAccess` do not apply to
|
|
6
|
+
* it. This one setting decides instead:
|
|
7
7
|
*
|
|
8
8
|
* - `'session'` (the default): any signed-in session may run it; 401 without one.
|
|
9
9
|
* - `'off'`: every raw query is refused (403).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rawQueries.js","sourceRoot":"","sources":["../../../src/utils/rawQueries.ts"],"names":[],"mappings":"AAgBA,IAAI,cAA0C,CAAC;AAC/C,IAAI,gBAAgB,GAAG,KAAK,CAAC;AAE7B,SAAS,SAAS,CAAC,KAAc;IAC/B,OAAO,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AACpE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAa;IAC7C,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;QACxC,cAAc,GAAG,SAAS,CAAC;QAC3B,OAAO;IACT,CAAC;IACD,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAC/B,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,CAAC,IAAI,CACV,8DAA8D,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,uBAAuB,CAC1G,CAAC;QACF,cAAc,GAAG,KAAK,CAAC;QACvB,OAAO;IACT,CAAC;IACD,cAAc,GAAG,MAAM,CAAC;AAC1B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB;IAC/B,IAAI,cAAc;QAAE,OAAO,cAAc,CAAC;IAC1C,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC;IAC3C,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,EAAE;QAAE,OAAO,SAAS,CAAC;IACtD,MAAM,MAAM,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;IAC9B,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC;IAC1B,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACtB,gBAAgB,GAAG,IAAI,CAAC;QACxB,OAAO,CAAC,IAAI,CACV,+DAA+D,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,uBAAuB,CAC1G,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC","sourcesContent":["/**\n * Who may run raw SPARQL through the generic query plane (`/api/select-raw`).\n *\n * Raw SPARQL cannot be analysed for the shapes it touches, so the
|
|
1
|
+
{"version":3,"file":"rawQueries.js","sourceRoot":"","sources":["../../../src/utils/rawQueries.ts"],"names":[],"mappings":"AAgBA,IAAI,cAA0C,CAAC;AAC/C,IAAI,gBAAgB,GAAG,KAAK,CAAC;AAE7B,SAAS,SAAS,CAAC,KAAc;IAC/B,OAAO,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AACpE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAa;IAC7C,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;QACxC,cAAc,GAAG,SAAS,CAAC;QAC3B,OAAO;IACT,CAAC;IACD,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAC/B,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,CAAC,IAAI,CACV,8DAA8D,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,uBAAuB,CAC1G,CAAC;QACF,cAAc,GAAG,KAAK,CAAC;QACvB,OAAO;IACT,CAAC;IACD,cAAc,GAAG,MAAM,CAAC;AAC1B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB;IAC/B,IAAI,cAAc;QAAE,OAAO,cAAc,CAAC;IAC1C,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC;IAC3C,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,EAAE;QAAE,OAAO,SAAS,CAAC;IACtD,MAAM,MAAM,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;IAC9B,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC;IAC1B,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACtB,gBAAgB,GAAG,IAAI,CAAC;QACxB,OAAO,CAAC,IAAI,CACV,+DAA+D,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,uBAAuB,CAC1G,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC","sourcesContent":["/**\n * Who may run raw SPARQL through the generic query plane (`/api/select-raw`).\n *\n * Raw SPARQL cannot be analysed for the shapes it touches, so the per-store\n * access rules of `@_linked/server-utils/utils/QueryAccess` do not apply to\n * it. This one setting decides instead:\n *\n * - `'session'` (the default): any signed-in session may run it; 401 without one.\n * - `'off'`: every raw query is refused (403).\n *\n * Set it with `server.rawQueries` in the app's config, or `LINKED_RAW_QUERIES`.\n */\nimport type { RawQueriesMode } from '@_linked/server-utils/utils/QueryAccess';\n\nexport type { RawQueriesMode };\n\nlet configuredMode: RawQueriesMode | undefined;\nlet invalidEnvWarned = false;\n\nfunction parseMode(value: unknown): RawQueriesMode | undefined {\n return value === 'off' || value === 'session' ? value : undefined;\n}\n\n/**\n * Set from the server's config (`server.rawQueries`). An invalid value turns\n * raw queries off, with a warning: a typo must not open them up.\n */\nexport function setRawQueriesMode(mode: unknown): void {\n if (mode === undefined || mode === null) {\n configuredMode = undefined;\n return;\n }\n const parsed = parseMode(mode);\n if (!parsed) {\n console.warn(\n `[linked] server.rawQueries must be 'off' or 'session', got ${JSON.stringify(mode)}; raw queries are off`\n );\n configuredMode = 'off';\n return;\n }\n configuredMode = parsed;\n}\n\n/**\n * `server.rawQueries`, else `LINKED_RAW_QUERIES`, else `'session'`. An invalid\n * `LINKED_RAW_QUERIES` turns raw queries off.\n */\nexport function getRawQueriesMode(): RawQueriesMode {\n if (configuredMode) return configuredMode;\n const env = process.env.LINKED_RAW_QUERIES;\n if (env === undefined || env === '') return 'session';\n const parsed = parseMode(env);\n if (parsed) return parsed;\n if (!invalidEnvWarned) {\n invalidEnvWarned = true;\n console.warn(\n `[linked] LINKED_RAW_QUERIES must be 'off' or 'session', got ${JSON.stringify(env)}; raw queries are off`\n );\n }\n return 'off';\n}\n"]}
|
package/package.json
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"type": "git",
|
|
11
11
|
"url": "git+https://github.com/linked-fw/server.git"
|
|
12
12
|
},
|
|
13
|
-
"version": "2.
|
|
13
|
+
"version": "2.23.0",
|
|
14
14
|
"packageManager": "npm@11.20.0",
|
|
15
15
|
"linkedPackage": true,
|
|
16
16
|
"type": "module",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@_linked/cli": "^1.2",
|
|
39
39
|
"@_linked/core": "^2.22.8",
|
|
40
|
-
"@_linked/server-utils": "^1.
|
|
40
|
+
"@_linked/server-utils": "^1.11.0",
|
|
41
41
|
"autoprefixer": "^10.4.18",
|
|
42
42
|
"compression": "^1.7.4",
|
|
43
43
|
"cors": "^2.8.5",
|