@reventlessdev/reventless-core 3.0.0-alpha.284 → 3.0.0-alpha.286
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 +23 -0
- package/package.json +7 -7
- package/src/Message.res.mjs +3 -0
- package/src/Roles.res +3 -0
- package/src/Roles.res.mjs +2 -0
- package/src/admin/Platform_AdminGroup.res +12 -0
- package/src/admin/Platform_AdminGroup.res.mjs +23 -0
- package/src/admin/Platform_Admin_Structure.res +14 -10
- package/src/admin/Platform_Admin_Structure.res.mjs +22 -33
- package/src/components/Aggregate/Aggregate_Callback.res +116 -39
- package/src/components/Aggregate/Aggregate_Callback.res.mjs +96 -37
- package/src/components/AutomationSlice/AutomationSlice_Builder.res +2 -1
- package/src/components/CommandGenerator/CommandGenerator_Callback.res +9 -8
- package/src/components/CommandGenerator/CommandGenerator_Callback.res.mjs +10 -8
- package/src/components/CommandTopic/CommandTopic.res.mjs +3 -0
- package/src/components/CommandTopic/CommandTopic_Helpers.res +37 -1
- package/src/components/CommandTopic/CommandTopic_Helpers.res.mjs +29 -0
- package/src/components/Counter/Counter_Builder.res +4 -2
- package/src/components/Dcb/Dcb_Builder.res +9 -6
- package/src/components/Dcb/Dcb_Builder.res.mjs +6 -6
- package/src/components/InboundTranslationSlice/InboundTranslationSlice_Builder.res +2 -1
- package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Builder.res +2 -1
- package/src/components/QueryDb/QueryDb_Builder.res +1 -1
- package/src/components/QueryDb/QueryDb_Builder.res.mjs +3 -2
- package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +191 -141
- package/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs +66 -12
- package/src/components/StateViewSlice/StateViewSlice_Builder.res +1 -0
- package/src/plugin/api/PluginBaseFragment.res +1 -1
- package/src/plugin/api/PluginBaseFragment.res.mjs +2 -1
- package/src/plugin/component/Plugin_Builder.res +7 -3
- package/src/plugin/component/Plugin_Builder.res.mjs +6 -3
- package/src/plugin/component/Plugin_Helpers.res +35 -6
- package/src/plugin/component/Plugin_Helpers.res.mjs +47 -5
- package/src/plugin/component/Plugin_Structure.res +52 -19
- package/src/plugin/component/Plugin_Structure.res.mjs +40 -15
- package/src/plugin/lifecycle/PluginsReadModelSpec.res +1 -1
- package/src/plugin/lifecycle/PluginsReadModelSpec.res.mjs +1 -1
- package/tests/admin/AdminApiSchemaDriftTest.res +9 -3
- package/tests/admin/AdminApiSchemaDriftTest.res.mjs +3 -3
- package/tests/admin/Platform_Admin_StructureTest.res +17 -14
- package/tests/admin/Platform_Admin_StructureTest.res.mjs +31 -19
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +8 -72
- package/tests/admin/Platform_PluginStructuresApiTest.res.mjs +9 -39
- package/tests/api/GraphQL_FragmentGeneratorTest.res +1 -1
- package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +1 -1
- package/tests/commandgenerator/OwnerActingTest.res +601 -0
- package/tests/commandgenerator/OwnerActingTest.res.mjs +1018 -0
- package/tests/commandgenerator/OwnerStampingTest.res +90 -0
- package/tests/commandgenerator/OwnerStampingTest.res.mjs +87 -0
- package/tests/commandtopic/CommandOutcomeHookTest.res +1 -0
- package/tests/commandtopic/CommandOutcomeHookTest.res.mjs +11 -1
- package/tests/plugin/PluginStructureAccessTest.res +30 -1
- package/tests/plugin/PluginStructureAccessTest.res.mjs +28 -1
- package/tests/plugin/StateChange/PsGatedCommands.res +9 -1
- package/tests/plugin/StateChange/PsGatedCommands.res.mjs +4 -1
- package/tests/plugin/StateView/PsGatedView.res +1 -1
- package/tests/plugin/StateView/PsGatedView.res.mjs +1 -1
- package/tests/plugin/pluginDefinitionRequiredScalars.txt +5 -0
|
@@ -8,9 +8,11 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
|
8
8
|
import * as Effect from "effect/Effect";
|
|
9
9
|
import * as Stream from "effect/Stream";
|
|
10
10
|
import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js";
|
|
11
|
+
import * as Owner$Reventless from "@reventlessdev/reventless-spec/src/components/Owner.res.mjs";
|
|
11
12
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
12
13
|
import * as Lru$ReventlessCore from "../../util/Lru.res.mjs";
|
|
13
14
|
import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
|
|
15
|
+
import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
|
|
14
16
|
import * as Message$ReventlessCore from "../../Message.res.mjs";
|
|
15
17
|
import * as LogFormat$ReventlessCore from "../../util/LogFormat.res.mjs";
|
|
16
18
|
import * as EffectLogger$ReventlessCore from "../../util/EffectLogger.res.mjs";
|
|
@@ -29,21 +31,55 @@ function Make(Spec) {
|
|
|
29
31
|
Spec.Id.makeFromString(param[0]),
|
|
30
32
|
param[1]
|
|
31
33
|
]));
|
|
34
|
+
let ownerFieldsByEventType = Owner$Reventless.fieldNamesByVariant(Spec.eventSchema);
|
|
35
|
+
let actsOnOwned = Object.keys(ownerFieldsByEventType).length !== 0;
|
|
36
|
+
let recordEventOwners = (owners, event) => {
|
|
37
|
+
if (!actsOnOwned) {
|
|
38
|
+
return owners;
|
|
39
|
+
}
|
|
40
|
+
let match = Message$ReventlessCore.splitMessage(Message$ReventlessCore.encode(event, Spec.eventSchema));
|
|
41
|
+
let fields = ownerFieldsByEventType[match[0]];
|
|
42
|
+
if (fields !== undefined) {
|
|
43
|
+
return OwnerScope$Reventless.recordOwners(owners, fields, match[1]);
|
|
44
|
+
} else {
|
|
45
|
+
return owners;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
32
48
|
let processCommand = seq => ((param, topicItem) => {
|
|
33
49
|
let reference = topicItem.reference;
|
|
34
50
|
let command$p = topicItem.command;
|
|
35
|
-
let outcomes = param[
|
|
51
|
+
let outcomes = param[2];
|
|
52
|
+
let owners = param[1];
|
|
36
53
|
let state = param[0];
|
|
37
54
|
let meta = Message$ReventlessCore.deriveMeta(command$p.meta, undefined);
|
|
38
55
|
let id = Spec.Id.toString(command$p.id);
|
|
39
|
-
let
|
|
56
|
+
let cmdVariant = Message$ReventlessCore.variantNameOfJson(Message$ReventlessCore.encode(command$p.command, Spec.commandSchema));
|
|
57
|
+
let cmdName = LogFormat$ReventlessCore.bold(cmdVariant);
|
|
40
58
|
Effect.runSync(EffectLogger$ReventlessCore.logDebug(comp, undefined, `deciding: id=` + id + ` seq=` + seq.toString() + ` cmd=` + cmdName));
|
|
59
|
+
let refusal = actsOnOwned ? CommandTopic_Helpers$ReventlessCore.ownershipRefusal(OwnerScope$Reventless.decideActing(command$p.meta.callerClaim, owners), Spec.name, cmdVariant) : undefined;
|
|
60
|
+
if (refusal !== undefined) {
|
|
61
|
+
let line = `refused: ` + refusal.rejected.errorDetail + ` id=` + id;
|
|
62
|
+
Effect.runSync(refusal.isDefect ? EffectLogger$ReventlessCore.logError(comp, undefined, line) : EffectLogger$ReventlessCore.logWarn(comp, undefined, line));
|
|
63
|
+
return [
|
|
64
|
+
state,
|
|
65
|
+
owners,
|
|
66
|
+
outcomes.concat([[
|
|
67
|
+
reference,
|
|
68
|
+
{
|
|
69
|
+
TAG: "CmdRefused",
|
|
70
|
+
_0: refusal
|
|
71
|
+
},
|
|
72
|
+
meta
|
|
73
|
+
]])
|
|
74
|
+
];
|
|
75
|
+
}
|
|
41
76
|
let generatedEvents = Behavior.decide(state, command$p.command);
|
|
42
77
|
if (generatedEvents.TAG === "Ok") {
|
|
43
78
|
let generatedEvents$1 = generatedEvents._0;
|
|
44
79
|
let newState = Stdlib_Array.reduce(generatedEvents$1, state, Behavior.evolve);
|
|
45
80
|
return [
|
|
46
81
|
newState,
|
|
82
|
+
Stdlib_Array.reduce(generatedEvents$1, owners, recordEventOwners),
|
|
47
83
|
outcomes.concat([[
|
|
48
84
|
reference,
|
|
49
85
|
{
|
|
@@ -62,6 +98,7 @@ function Make(Spec) {
|
|
|
62
98
|
Effect.runSync(EffectLogger$ReventlessCore.logError(comp, undefined, `decide rejected: ` + errorCode + ` ` + errorDetail + ` id=` + id));
|
|
63
99
|
return [
|
|
64
100
|
state,
|
|
101
|
+
owners,
|
|
65
102
|
outcomes.concat([[
|
|
66
103
|
reference,
|
|
67
104
|
{
|
|
@@ -75,7 +112,10 @@ function Make(Spec) {
|
|
|
75
112
|
});
|
|
76
113
|
let replayCache = Lru$ReventlessCore.make(100);
|
|
77
114
|
let resetCache = () => Lru$ReventlessCore.clear(replayCache);
|
|
78
|
-
let snapshotConfig = Behavior.snapshot;
|
|
115
|
+
let snapshotConfig = actsOnOwned ? undefined : Behavior.snapshot;
|
|
116
|
+
if (actsOnOwned && Stdlib_Option.isSome(Behavior.snapshot)) {
|
|
117
|
+
Effect.runSync(EffectLogger$ReventlessCore.logWarn(comp, undefined, "snapshots disabled: this aggregate records an @owner, which a snapshot does not carry"));
|
|
118
|
+
}
|
|
79
119
|
let stateSchemaHash = snapshotConfig !== undefined ? HashObject(Object.fromEntries([[
|
|
80
120
|
"state",
|
|
81
121
|
SchemaWalker$ReventlessCore.describeSchema(snapshotConfig.stateSchema)
|
|
@@ -151,10 +191,12 @@ function Make(Spec) {
|
|
|
151
191
|
let seedSeq = param[1];
|
|
152
192
|
return Effect.tap(Stream.runFold(Ops.eventLog.replayStream(id, seedSeq), [
|
|
153
193
|
param[0],
|
|
154
|
-
seedSeq
|
|
194
|
+
seedSeq,
|
|
195
|
+
[]
|
|
155
196
|
], (param, ev) => [
|
|
156
197
|
Behavior.evolve(param[0], ev),
|
|
157
|
-
param[1] + 1 | 0
|
|
198
|
+
param[1] + 1 | 0,
|
|
199
|
+
recordEventOwners(param[2], ev)
|
|
158
200
|
]), param => {
|
|
159
201
|
let n = param[1];
|
|
160
202
|
let detail = seedSeq > 0 ? `(snapshot@` + seedSeq.toString() + `, ` + (n - seedSeq | 0).toString() + ` delta event(s))` : n.toString() + ` event(s)`;
|
|
@@ -199,35 +241,43 @@ function Make(Spec) {
|
|
|
199
241
|
return outcomes.map(param => {
|
|
200
242
|
let outcome = param[1];
|
|
201
243
|
let reference = param[0];
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
244
|
+
switch (outcome.TAG) {
|
|
245
|
+
case "CmdOk" :
|
|
246
|
+
if (appendSucceeded) {
|
|
247
|
+
CommandTopic_Helpers$ReventlessCore.reportAccepted(Spec.name, reference, {
|
|
248
|
+
entityId: entityId,
|
|
249
|
+
eventCount: appendedEventCount
|
|
250
|
+
});
|
|
251
|
+
return {
|
|
252
|
+
TAG: "Ok",
|
|
253
|
+
_0: reference
|
|
254
|
+
};
|
|
255
|
+
} else {
|
|
256
|
+
CommandTopic_Helpers$ReventlessCore.reportRejected(Spec.name, "InfrastructureFailure", reference, {
|
|
257
|
+
errorCode: "AppendFailed",
|
|
258
|
+
errorDetail: appendErrorDetail
|
|
259
|
+
});
|
|
260
|
+
return {
|
|
261
|
+
TAG: "Error",
|
|
262
|
+
_0: reference
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
case "CmdRejected" :
|
|
266
|
+
CommandTopic_Helpers$ReventlessCore.reportRejected(Spec.name, "DomainRejection", reference, {
|
|
267
|
+
errorCode: outcome.errorCode,
|
|
268
|
+
errorDetail: outcome.errorDetail
|
|
207
269
|
});
|
|
208
270
|
return {
|
|
209
271
|
TAG: "Ok",
|
|
210
272
|
_0: reference
|
|
211
273
|
};
|
|
212
|
-
|
|
213
|
-
CommandTopic_Helpers$ReventlessCore.reportRejected(Spec.name, "
|
|
214
|
-
errorCode: "AppendFailed",
|
|
215
|
-
errorDetail: appendErrorDetail
|
|
216
|
-
});
|
|
274
|
+
case "CmdRefused" :
|
|
275
|
+
CommandTopic_Helpers$ReventlessCore.reportRejected(Spec.name, "AccessRefusal", reference, outcome._0.rejected);
|
|
217
276
|
return {
|
|
218
|
-
TAG: "
|
|
277
|
+
TAG: "Ok",
|
|
219
278
|
_0: reference
|
|
220
279
|
};
|
|
221
|
-
}
|
|
222
280
|
}
|
|
223
|
-
CommandTopic_Helpers$ReventlessCore.reportRejected(Spec.name, "DomainRejection", reference, {
|
|
224
|
-
errorCode: outcome.errorCode,
|
|
225
|
-
errorDetail: outcome.errorDetail
|
|
226
|
-
});
|
|
227
|
-
return {
|
|
228
|
-
TAG: "Ok",
|
|
229
|
-
_0: reference
|
|
230
|
-
};
|
|
231
281
|
});
|
|
232
282
|
};
|
|
233
283
|
let replayProcessAppend = (id, topicItemsForId) => {
|
|
@@ -235,35 +285,42 @@ function Make(Spec) {
|
|
|
235
285
|
let match = Lru$ReventlessCore.get(replayCache, idStr);
|
|
236
286
|
let readState;
|
|
237
287
|
if (match !== undefined) {
|
|
288
|
+
let owners = match[2];
|
|
238
289
|
let seqNr = match[1];
|
|
239
290
|
let state = match[0];
|
|
240
291
|
readState = Effect.map(EffectLogger$ReventlessCore.logInfo(comp, undefined, `replay skipped (cached): id=` + idStr + `, seq=` + seqNr.toString()), () => [
|
|
241
292
|
state,
|
|
242
|
-
seqNr
|
|
293
|
+
seqNr,
|
|
294
|
+
owners
|
|
243
295
|
]);
|
|
244
296
|
} else {
|
|
245
297
|
readState = coldReadState(id);
|
|
246
298
|
}
|
|
247
299
|
return Effect.flatMap(readState, param => {
|
|
300
|
+
let initialOwners = param[2];
|
|
248
301
|
let sequenceNr = param[1];
|
|
249
302
|
let initialState = param[0];
|
|
250
303
|
let match = Stdlib_Array.reduce(topicItemsForId, [
|
|
251
304
|
initialState,
|
|
305
|
+
initialOwners,
|
|
252
306
|
[]
|
|
253
307
|
], processCommand(sequenceNr));
|
|
254
|
-
let outcomes = match[
|
|
308
|
+
let outcomes = match[2];
|
|
309
|
+
let finalOwners = match[1];
|
|
255
310
|
let finalState = match[0];
|
|
256
311
|
let eventsToAppend = outcomes.map(param => {
|
|
257
312
|
let meta = param[2];
|
|
258
313
|
let outcome = param[1];
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
314
|
+
switch (outcome.TAG) {
|
|
315
|
+
case "CmdOk" :
|
|
316
|
+
return outcome._0.map(event => ({
|
|
317
|
+
id: id,
|
|
318
|
+
meta: meta,
|
|
319
|
+
event: event
|
|
320
|
+
}));
|
|
321
|
+
case "CmdRejected" :
|
|
322
|
+
case "CmdRefused" :
|
|
323
|
+
return [];
|
|
267
324
|
}
|
|
268
325
|
}).flat();
|
|
269
326
|
if (eventsToAppend.length !== 0) {
|
|
@@ -280,7 +337,8 @@ function Make(Spec) {
|
|
|
280
337
|
let newSeq = sequenceNr + eventsToAppend.length | 0;
|
|
281
338
|
Lru$ReventlessCore.put(replayCache, idStr, [
|
|
282
339
|
finalState,
|
|
283
|
-
newSeq
|
|
340
|
+
newSeq,
|
|
341
|
+
finalOwners
|
|
284
342
|
]);
|
|
285
343
|
maybeWriteSnapshot(id, idStr, sequenceNr, newSeq, finalState);
|
|
286
344
|
let perRef = reportFinalOutcomes(outcomes, idStr, true, eventsToAppend.length, undefined);
|
|
@@ -308,7 +366,8 @@ function Make(Spec) {
|
|
|
308
366
|
}
|
|
309
367
|
Lru$ReventlessCore.put(replayCache, idStr, [
|
|
310
368
|
initialState,
|
|
311
|
-
sequenceNr
|
|
369
|
+
sequenceNr,
|
|
370
|
+
initialOwners
|
|
312
371
|
]);
|
|
313
372
|
let perRef = reportFinalOutcomes(outcomes, idStr, true, 0, undefined);
|
|
314
373
|
return Effect.map(EffectLogger$ReventlessCore.logInfo(comp, undefined, `no events produced: id=` + idStr), () => ({
|
|
@@ -54,7 +54,8 @@ module Make = (
|
|
|
54
54
|
let stateSchema = AutomationSlice_Callback.todoRowSchema
|
|
55
55
|
let config = Reventless.ReadModel.config()
|
|
56
56
|
let subIdConfig: option<Reventless.ReadModel.subIdConfig<state>> = None
|
|
57
|
-
|
|
57
|
+
type role = Reventless.Role.name
|
|
58
|
+
let authorization: Reventless.Authorization.rule<role> = AllowAuthenticated
|
|
58
59
|
let visibility: Reventless.Visibility.t = Public
|
|
59
60
|
}
|
|
60
61
|
|
|
@@ -55,13 +55,13 @@ let stampOwnerFields = (
|
|
|
55
55
|
obj: dict<JSON.t>,
|
|
56
56
|
~commandSchema: S.t<unknown>,
|
|
57
57
|
~command: string,
|
|
58
|
-
~
|
|
58
|
+
~scope: Reventless.OwnerScope.t,
|
|
59
59
|
~serviceName: string,
|
|
60
60
|
) =>
|
|
61
61
|
switch Reventless.Owner.variantFieldNames(commandSchema, ~variant=command) {
|
|
62
62
|
| [] => ()
|
|
63
63
|
| ownerFields =>
|
|
64
|
-
switch
|
|
64
|
+
switch scope {
|
|
65
65
|
| System | Elevated(_) => ()
|
|
66
66
|
| Owned({userId}) =>
|
|
67
67
|
ownerFields->Array.forEach(field => obj->Dict.set(field, JSON.Encode.string(userId)))
|
|
@@ -118,6 +118,11 @@ let makeGenerateCommand = (
|
|
|
118
118
|
(payload: CommandGenerator.payload) =>
|
|
119
119
|
Effect.sync(() => {
|
|
120
120
|
let msgId = Message.uuid()
|
|
121
|
+
// One classification for the stamp below and for the claim the handler
|
|
122
|
+
// checks ownership against, so the two cannot disagree about the caller.
|
|
123
|
+
// The claim is built here and nowhere from the payload, which is all a
|
|
124
|
+
// client controls.
|
|
125
|
+
let scope = payload.identity->Reventless.OwnerScope.resolve
|
|
121
126
|
let meta = {
|
|
122
127
|
{
|
|
123
128
|
Message.service: serviceName,
|
|
@@ -126,6 +131,7 @@ let makeGenerateCommand = (
|
|
|
126
131
|
time: Message.nowAsISOString(),
|
|
127
132
|
msgId,
|
|
128
133
|
correlationId: msgId,
|
|
134
|
+
callerClaim: scope->Reventless.OwnerScope.toClaim,
|
|
129
135
|
}
|
|
130
136
|
}
|
|
131
137
|
let params = switch payload.arguments
|
|
@@ -136,12 +142,7 @@ let makeGenerateCommand = (
|
|
|
136
142
|
obj->Dict.delete("id")
|
|
137
143
|
}
|
|
138
144
|
obj->dropNullArguments
|
|
139
|
-
obj->stampOwnerFields(
|
|
140
|
-
~commandSchema,
|
|
141
|
-
~command=payload.command,
|
|
142
|
-
~identity=payload.identity,
|
|
143
|
-
~serviceName,
|
|
144
|
-
)
|
|
145
|
+
obj->stampOwnerFields(~commandSchema, ~command=payload.command, ~scope, ~serviceName)
|
|
145
146
|
obj->Dict.toArray
|
|
146
147
|
}
|
|
147
148
|
| None =>
|
|
@@ -27,26 +27,25 @@ function clearCommandInterceptor() {
|
|
|
27
27
|
commandInterceptorHook.contents = undefined;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
function stampOwnerFields(obj, commandSchema, command,
|
|
30
|
+
function stampOwnerFields(obj, commandSchema, command, scope, serviceName) {
|
|
31
31
|
let ownerFields = Owner$Reventless.variantFieldNames(commandSchema, command);
|
|
32
32
|
if (ownerFields.length === 0) {
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
35
|
-
|
|
36
|
-
if (typeof why !== "object") {
|
|
35
|
+
if (typeof scope !== "object") {
|
|
37
36
|
return;
|
|
38
37
|
}
|
|
39
|
-
switch (
|
|
38
|
+
switch (scope.TAG) {
|
|
40
39
|
case "Elevated" :
|
|
41
40
|
return;
|
|
42
41
|
case "Owned" :
|
|
43
|
-
let userId =
|
|
42
|
+
let userId = scope.userId;
|
|
44
43
|
ownerFields.forEach(field => {
|
|
45
44
|
obj[field] = userId;
|
|
46
45
|
});
|
|
47
46
|
return;
|
|
48
47
|
case "Unidentified" :
|
|
49
|
-
return Plugin_ResolverError$ReventlessCore.throwCallerFault(`Forbidden: ` + serviceName + `.` + command + ` records an owner, but the caller could not be identified (` +
|
|
48
|
+
return Plugin_ResolverError$ReventlessCore.throwCallerFault(`Forbidden: ` + serviceName + `.` + command + ` records an owner, but the caller could not be identified (` + scope._0 + `)`);
|
|
50
49
|
}
|
|
51
50
|
}
|
|
52
51
|
|
|
@@ -63,19 +62,22 @@ function makeGenerateCommand(publishJsons, publishJsonsAndWait, serviceName, com
|
|
|
63
62
|
let stripIdFromParams = stripIdFromParamsOpt !== undefined ? stripIdFromParamsOpt : true;
|
|
64
63
|
return payload => Effect.flatMap(Effect.tap(Effect.sync(() => {
|
|
65
64
|
let msgId = Message$ReventlessCore.uuid();
|
|
65
|
+
let scope = OwnerScope$Reventless.resolve(payload.identity, undefined);
|
|
66
66
|
let meta_time = Message$ReventlessCore.nowAsISOString();
|
|
67
67
|
let meta_ip = Stdlib_Option.getOr(payload.meta.ip.shift(), "");
|
|
68
68
|
let meta_user = payload.meta.user;
|
|
69
|
+
let meta_callerClaim = OwnerScope$Reventless.toClaim(scope);
|
|
69
70
|
let meta = {
|
|
70
71
|
service: serviceName,
|
|
71
72
|
time: meta_time,
|
|
72
73
|
ip: meta_ip,
|
|
73
74
|
user: meta_user,
|
|
74
75
|
msgId: msgId,
|
|
75
|
-
correlationId: msgId
|
|
76
|
+
correlationId: msgId,
|
|
77
|
+
callerClaim: meta_callerClaim
|
|
76
78
|
};
|
|
77
79
|
let obj = Stdlib_Option.flatMap(JSON.stringify(payload.arguments), jsonString => Stdlib_JSON.Decode.object(JSON.parse(jsonString)));
|
|
78
|
-
let params = obj !== undefined ? (stripIdFromParams ? Stdlib_Dict.$$delete(obj, "id") : undefined, dropNullArguments(obj), stampOwnerFields(obj, commandSchema, payload.command,
|
|
80
|
+
let params = obj !== undefined ? (stripIdFromParams ? Stdlib_Dict.$$delete(obj, "id") : undefined, dropNullArguments(obj), stampOwnerFields(obj, commandSchema, payload.command, scope, serviceName), Object.entries(obj)) : Stdlib_JsError.throwWithMessage("Couldn't decode:" + Stdlib_Option.getOr(JSON.stringify(payload.arguments), "<payload.arguments>"));
|
|
79
81
|
let commandStr = payload.command;
|
|
80
82
|
let match = params.length;
|
|
81
83
|
let commandJson = match !== 0 ? Object.fromEntries([[
|
|
@@ -35,6 +35,8 @@ let acceptedResultChannel = CommandTopic_Helpers$ReventlessCore.acceptedResultCh
|
|
|
35
35
|
|
|
36
36
|
let rejectedResultChannel = CommandTopic_Helpers$ReventlessCore.rejectedResultChannel;
|
|
37
37
|
|
|
38
|
+
let ownershipRefusal = CommandTopic_Helpers$ReventlessCore.ownershipRefusal;
|
|
39
|
+
|
|
38
40
|
let reportAccepted = CommandTopic_Helpers$ReventlessCore.reportAccepted;
|
|
39
41
|
|
|
40
42
|
let reportRejected = CommandTopic_Helpers$ReventlessCore.reportRejected;
|
|
@@ -64,6 +66,7 @@ export {
|
|
|
64
66
|
fireCommandOutcome,
|
|
65
67
|
acceptedResultChannel,
|
|
66
68
|
rejectedResultChannel,
|
|
69
|
+
ownershipRefusal,
|
|
67
70
|
reportAccepted,
|
|
68
71
|
reportRejected,
|
|
69
72
|
commandOutcomeToJson,
|
|
@@ -25,8 +25,11 @@ Why a command was refused.
|
|
|
25
25
|
which is the model working. `InfrastructureFailure` is the decision never landing: an event-log
|
|
26
26
|
append that failed outright or exhausted its conflict retries. Both arrive on `reportRejected`,
|
|
27
27
|
so without this a consumer would read a broken event log as a business rejection.
|
|
28
|
+
|
|
29
|
+
`AccessRefusal` is the handler refusing before `decide` runs, because the caller does not
|
|
30
|
+
own what the command acts on (`OwnerScope.decideActing`).
|
|
28
31
|
*/
|
|
29
|
-
type refusalCause = DomainRejection | InfrastructureFailure
|
|
32
|
+
type refusalCause = DomainRejection | InfrastructureFailure | AccessRefusal
|
|
30
33
|
|
|
31
34
|
/**
|
|
32
35
|
What became of one command. Constructors are prefixed to stay clear of `commandOutcome`'s
|
|
@@ -93,6 +96,39 @@ let acceptedResultChannel: ref<option<(string, acceptedResult) => unit>> = ref(N
|
|
|
93
96
|
// `Rejected` outcomes that carry the real error code and detail.
|
|
94
97
|
let rejectedResultChannel: ref<option<(string, rejectedResult) => unit>> = ref(None)
|
|
95
98
|
|
|
99
|
+
/**
|
|
100
|
+
Turn an `acting` verdict into the refusal a command handler reports, or `None` to let
|
|
101
|
+
`decide` run.
|
|
102
|
+
|
|
103
|
+
Shared by both handlers so they refuse in the same words. `errorCode` is `"Forbidden"`,
|
|
104
|
+
the code an `@authorize` refusal carries, so a client handles both alike. `isDefect` marks
|
|
105
|
+
a refusal that is the data's or the slice's fault rather than the caller's.
|
|
106
|
+
*/
|
|
107
|
+
type ownershipRefusal = {rejected: rejectedResult, isDefect: bool}
|
|
108
|
+
|
|
109
|
+
let ownershipRefusal = (
|
|
110
|
+
verdict: Reventless.OwnerScope.acting,
|
|
111
|
+
~component: string,
|
|
112
|
+
~command: string,
|
|
113
|
+
): option<ownershipRefusal> => {
|
|
114
|
+
let refuse = (~isDefect=false, why) => Some({
|
|
115
|
+
rejected: {errorCode: "Forbidden", errorDetail: `${component}.${command}: ${why}`},
|
|
116
|
+
isDefect,
|
|
117
|
+
})
|
|
118
|
+
switch verdict {
|
|
119
|
+
| MayAct => None
|
|
120
|
+
| NotTheOwner(_) => refuse("the caller does not own what this command acts on")
|
|
121
|
+
| CallerUnidentified => refuse("the caller could not be identified")
|
|
122
|
+
| ConflictingOwners(owners) =>
|
|
123
|
+
refuse(
|
|
124
|
+
~isDefect=true,
|
|
125
|
+
`the history records ${owners->Array.length->Int.toString} different owners`,
|
|
126
|
+
)
|
|
127
|
+
| OwnerUnreadable =>
|
|
128
|
+
refuse(~isDefect=true, "the partition this command acts on could not be named")
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
96
132
|
// `~component` is the owning component's `Spec.name`: an outcome isn't interpretable without
|
|
97
133
|
// knowing which component produced it, and the side-channel (keyed per inline dispatch) never
|
|
98
134
|
// had to say. `~cause` is required rather than defaulted so a new rejection site has to state
|
|
@@ -46,6 +46,34 @@ let rejectedResultChannel = {
|
|
|
46
46
|
contents: undefined
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
+
function ownershipRefusal(verdict, component, command) {
|
|
50
|
+
let refuse = (isDefectOpt, why) => {
|
|
51
|
+
let isDefect = isDefectOpt !== undefined ? isDefectOpt : false;
|
|
52
|
+
return {
|
|
53
|
+
rejected: {
|
|
54
|
+
errorCode: "Forbidden",
|
|
55
|
+
errorDetail: component + `.` + command + `: ` + why
|
|
56
|
+
},
|
|
57
|
+
isDefect: isDefect
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
if (typeof verdict === "object") {
|
|
61
|
+
if (verdict.TAG === "NotTheOwner") {
|
|
62
|
+
return refuse(undefined, "the caller does not own what this command acts on");
|
|
63
|
+
} else {
|
|
64
|
+
return refuse(true, `the history records ` + verdict._0.length.toString() + ` different owners`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
switch (verdict) {
|
|
68
|
+
case "MayAct" :
|
|
69
|
+
return;
|
|
70
|
+
case "CallerUnidentified" :
|
|
71
|
+
return refuse(undefined, "the caller could not be identified");
|
|
72
|
+
case "OwnerUnreadable" :
|
|
73
|
+
return refuse(true, "the partition this command acts on could not be named");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
49
77
|
function reportAccepted(component, reference, result) {
|
|
50
78
|
Stdlib_Option.forEach(acceptedResultChannel.contents, cb => cb(reference, result));
|
|
51
79
|
fireCommandOutcome(component, reference, {
|
|
@@ -227,6 +255,7 @@ export {
|
|
|
227
255
|
fireCommandOutcome,
|
|
228
256
|
acceptedResultChannel,
|
|
229
257
|
rejectedResultChannel,
|
|
258
|
+
ownershipRefusal,
|
|
230
259
|
reportAccepted,
|
|
231
260
|
reportRejected,
|
|
232
261
|
commandOutcomeToJson,
|
|
@@ -29,7 +29,8 @@ module Make = (
|
|
|
29
29
|
|
|
30
30
|
let subIdConfig = None
|
|
31
31
|
let config = Reventless.ReadModel.config()
|
|
32
|
-
|
|
32
|
+
type role = Reventless.Role.name
|
|
33
|
+
let authorization: Reventless.Authorization.rule<role> = AllowAuthenticated
|
|
33
34
|
let visibility: Reventless.Visibility.t = Public
|
|
34
35
|
}
|
|
35
36
|
|
|
@@ -48,7 +49,8 @@ module Make = (
|
|
|
48
49
|
|
|
49
50
|
let subIdConfig = None
|
|
50
51
|
let config = Reventless.ReadModel.config()
|
|
51
|
-
|
|
52
|
+
type role = Reventless.Role.name
|
|
53
|
+
let authorization: Reventless.Authorization.rule<role> = AllowAuthenticated
|
|
52
54
|
let visibility: Reventless.Visibility.t = Public
|
|
53
55
|
}
|
|
54
56
|
module CountsDb = QueryDb_Builder.Make(
|
|
@@ -534,7 +534,8 @@ module Make = (
|
|
|
534
534
|
~commandSchema,
|
|
535
535
|
)->Array.map(((f, _)) => f),
|
|
536
536
|
~commandSchema,
|
|
537
|
-
~commandAuthorization=
|
|
537
|
+
~commandAuthorization=command =>
|
|
538
|
+
S.Spec.commandAuthorization(command->Obj.magic)->Reventless.Authorization.named,
|
|
538
539
|
)
|
|
539
540
|
}
|
|
540
541
|
})
|
|
@@ -549,7 +550,8 @@ module Make = (
|
|
|
549
550
|
~commandSchema,
|
|
550
551
|
)->Array.map(((f, _)) => f),
|
|
551
552
|
~commandSchema,
|
|
552
|
-
~commandAuthorization=
|
|
553
|
+
~commandAuthorization=command =>
|
|
554
|
+
S.Spec.commandAuthorization(command->Obj.magic)->Reventless.Authorization.named,
|
|
553
555
|
)
|
|
554
556
|
}
|
|
555
557
|
})
|
|
@@ -1076,8 +1078,8 @@ module Make = (
|
|
|
1076
1078
|
)
|
|
1077
1079
|
// One SDL field per constructor, each with its own constructor's args and
|
|
1078
1080
|
// its own per-constructor authorization rule.
|
|
1079
|
-
let commandAuthorization: unknown => Reventless.Authorization.permission =
|
|
1080
|
-
S.Spec.commandAuthorization->Obj.magic
|
|
1081
|
+
let commandAuthorization: unknown => Reventless.Authorization.permission = command =>
|
|
1082
|
+
S.Spec.commandAuthorization(command->Obj.magic)->Reventless.Authorization.named
|
|
1081
1083
|
let fieldPermissions = Dict.make()
|
|
1082
1084
|
fieldSpecs->Array.forEach(((fieldName, ctor)) => {
|
|
1083
1085
|
let hasPayload = Reventless.DcbTag.isVariantPayloadBearing(
|
|
@@ -1108,7 +1110,8 @@ module Make = (
|
|
|
1108
1110
|
let fieldPermissions = Dict.make()
|
|
1109
1111
|
switch permissionForFirstConstructor(
|
|
1110
1112
|
~commandSchema=ITS.Spec.commandSchema->Reventless.DcbTag.toUnknownSchema,
|
|
1111
|
-
~commandAuthorization=
|
|
1113
|
+
~commandAuthorization=command =>
|
|
1114
|
+
ITS.Spec.commandAuthorization(command->Obj.magic)->Reventless.Authorization.named,
|
|
1112
1115
|
) {
|
|
1113
1116
|
| Some(rule) => fieldPermissions->Dict.set(fieldName, rule)
|
|
1114
1117
|
| None => ()
|
|
@@ -1142,7 +1145,7 @@ module Make = (
|
|
|
1142
1145
|
returnTypeName: qn.returnTypeName,
|
|
1143
1146
|
stateSchema: V.Spec.stateSchema->Reventless.DcbTag.toUnknownSchema,
|
|
1144
1147
|
authorization: None,
|
|
1145
|
-
permission: V.Spec.authorization,
|
|
1148
|
+
permission: V.Spec.authorization->Reventless.Authorization.named,
|
|
1146
1149
|
systemCallable: systemCallableComponents->Array.includes(V.Spec.name),
|
|
1147
1150
|
includeIdParam: qn.includeIdParam,
|
|
1148
1151
|
connectionSpec: true,
|
|
@@ -13,6 +13,7 @@ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js
|
|
|
13
13
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
14
14
|
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
15
15
|
import * as Logger$ReventlessCore from "../../util/Logger.res.mjs";
|
|
16
|
+
import * as Authorization$Reventless from "@reventlessdev/reventless-spec/src/types/Authorization.res.mjs";
|
|
16
17
|
import * as Component$ReventlessCore from "../Component.res.mjs";
|
|
17
18
|
import * as DcbValidation$Reventless from "@reventlessdev/reventless-spec/src/components/DcbValidation.res.mjs";
|
|
18
19
|
import * as Api_Naming$ReventlessCore from "../Api/Api_Naming.res.mjs";
|
|
@@ -291,13 +292,13 @@ function Make(DcbEventLogStorage) {
|
|
|
291
292
|
syncSlices.forEach(S => {
|
|
292
293
|
let commandSchema = S.Spec.commandSchema;
|
|
293
294
|
if (!ApiNoApiHelpers$ReventlessCore.isNoApi(commandSchema)) {
|
|
294
|
-
return registerResolver("Dcb", Api_Naming$ReventlessCore.sliceMutationFields(apiNamePrefix, S.Spec.name, commandSchema).map(param => param[0]), commandSchema, S.Spec.commandAuthorization);
|
|
295
|
+
return registerResolver("Dcb", Api_Naming$ReventlessCore.sliceMutationFields(apiNamePrefix, S.Spec.name, commandSchema).map(param => param[0]), commandSchema, command => Authorization$Reventless.named(S.Spec.commandAuthorization(command)));
|
|
295
296
|
}
|
|
296
297
|
});
|
|
297
298
|
asyncSlices.forEach(S => {
|
|
298
299
|
let commandSchema = S.Spec.commandSchema;
|
|
299
300
|
if (!ApiNoApiHelpers$ReventlessCore.isNoApi(commandSchema)) {
|
|
300
|
-
return registerResolver("Dcb", Api_Naming$ReventlessCore.sliceMutationFields(apiNamePrefix, S.Spec.name, commandSchema).map(param => param[0]), commandSchema, S.Spec.commandAuthorization);
|
|
301
|
+
return registerResolver("Dcb", Api_Naming$ReventlessCore.sliceMutationFields(apiNamePrefix, S.Spec.name, commandSchema).map(param => param[0]), commandSchema, command => Authorization$Reventless.named(S.Spec.commandAuthorization(command)));
|
|
301
302
|
}
|
|
302
303
|
});
|
|
303
304
|
}
|
|
@@ -562,7 +563,6 @@ function Make(DcbEventLogStorage) {
|
|
|
562
563
|
return;
|
|
563
564
|
}
|
|
564
565
|
let sliceDef = Stdlib_Option.flatMap(pluginStructure, s => s.stateChangeSlices.find(d => d.name === S.Spec.name));
|
|
565
|
-
let commandAuthorization = S.Spec.commandAuthorization;
|
|
566
566
|
let fieldPermissions = {};
|
|
567
567
|
fieldSpecs.forEach(param => {
|
|
568
568
|
let ctor = param[1];
|
|
@@ -570,7 +570,7 @@ function Make(DcbEventLogStorage) {
|
|
|
570
570
|
let syntheticCmd = hasPayload ? ({
|
|
571
571
|
TAG: ctor
|
|
572
572
|
}) : ctor;
|
|
573
|
-
fieldPermissions[param[0]] = commandAuthorization(syntheticCmd);
|
|
573
|
+
fieldPermissions[param[0]] = Authorization$Reventless.named(S.Spec.commandAuthorization(syntheticCmd));
|
|
574
574
|
});
|
|
575
575
|
return {
|
|
576
576
|
fieldNames: fieldSpecs.map(param => param[0]),
|
|
@@ -585,7 +585,7 @@ function Make(DcbEventLogStorage) {
|
|
|
585
585
|
let mutationEntriesFromInboundSlices = inboundTranslationSlices.map(ITS => {
|
|
586
586
|
let fieldName = Api_Naming$ReventlessCore.sliceMutationField(name, ITS.Spec.name);
|
|
587
587
|
let fieldPermissions = {};
|
|
588
|
-
let rule = permissionForFirstConstructor(ITS.Spec.commandSchema, ITS.Spec.commandAuthorization);
|
|
588
|
+
let rule = permissionForFirstConstructor(ITS.Spec.commandSchema, command => Authorization$Reventless.named(ITS.Spec.commandAuthorization(command)));
|
|
589
589
|
if (rule !== undefined) {
|
|
590
590
|
fieldPermissions[fieldName] = rule;
|
|
591
591
|
}
|
|
@@ -609,7 +609,7 @@ function Make(DcbEventLogStorage) {
|
|
|
609
609
|
returnTypeName: qn.returnTypeName,
|
|
610
610
|
stateSchema: V.Spec.stateSchema,
|
|
611
611
|
authorization: undefined,
|
|
612
|
-
permission: V.Spec.authorization,
|
|
612
|
+
permission: Authorization$Reventless.named(V.Spec.authorization),
|
|
613
613
|
systemCallable: systemCallableComponents.includes(V.Spec.name),
|
|
614
614
|
includeIdParam: qn.includeIdParam,
|
|
615
615
|
connectionSpec: true,
|
|
@@ -35,7 +35,8 @@ module Make = (
|
|
|
35
35
|
let stateSchema = InboundTranslationSlice_Callback.auditRowSchema
|
|
36
36
|
let config = Reventless.ReadModel.config()
|
|
37
37
|
let subIdConfig: option<Reventless.ReadModel.subIdConfig<state>> = None
|
|
38
|
-
|
|
38
|
+
type role = Reventless.Role.name
|
|
39
|
+
let authorization: Reventless.Authorization.rule<role> = AllowAuthenticated
|
|
39
40
|
let visibility: Reventless.Visibility.t = Public
|
|
40
41
|
}
|
|
41
42
|
|
|
@@ -51,7 +51,8 @@ module Make = (
|
|
|
51
51
|
let stateSchema = OutboundTranslationSlice_Callback.todoRowSchema
|
|
52
52
|
let config = Reventless.ReadModel.config()
|
|
53
53
|
let subIdConfig: option<Reventless.ReadModel.subIdConfig<state>> = None
|
|
54
|
-
|
|
54
|
+
type role = Reventless.Role.name
|
|
55
|
+
let authorization: Reventless.Authorization.rule<role> = AllowAuthenticated
|
|
55
56
|
let visibility: Reventless.Visibility.t = Public
|
|
56
57
|
}
|
|
57
58
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
4
|
import * as QueryDb$ReventlessCore from "./QueryDb.res.mjs";
|
|
5
|
+
import * as Authorization$Reventless from "@reventlessdev/reventless-spec/src/types/Authorization.res.mjs";
|
|
5
6
|
import * as Component$ReventlessCore from "../Component.res.mjs";
|
|
6
7
|
import * as ComponentType$ReventlessCore from "../../ComponentType.res.mjs";
|
|
7
8
|
import * as QueryDb_Operations$ReventlessCore from "./QueryDb_Operations.res.mjs";
|
|
@@ -29,7 +30,7 @@ function Make(Spec) {
|
|
|
29
30
|
deleteBatch: Operations.deleteBatch
|
|
30
31
|
};
|
|
31
32
|
}));
|
|
32
|
-
let resolvers = Resolvers.make(none$1, api, apiRole, storage.dataSourceName, Spec.config.indexes, subIdField, Spec.config.idResolvers, Spec.config.idsResolvers, Spec.authorization, opts);
|
|
33
|
+
let resolvers = Resolvers.make(none$1, api, apiRole, storage.dataSourceName, Spec.config.indexes, subIdField, Spec.config.idResolvers, Spec.config.idsResolvers, Authorization$Reventless.named(Spec.authorization), opts);
|
|
33
34
|
let outputs_resources = storage.resources.concat(resolvers.resources);
|
|
34
35
|
let outputs_resolversMaker = resolvers.resourcesMaker;
|
|
35
36
|
let outputs_dataSourceName = storage.dataSourceName;
|
|
@@ -50,4 +51,4 @@ function Make(Spec) {
|
|
|
50
51
|
export {
|
|
51
52
|
Make,
|
|
52
53
|
}
|
|
53
|
-
/*
|
|
54
|
+
/* Authorization-Reventless Not a pure module */
|