@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.
Files changed (58) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/package.json +7 -7
  3. package/src/Message.res.mjs +3 -0
  4. package/src/Roles.res +3 -0
  5. package/src/Roles.res.mjs +2 -0
  6. package/src/admin/Platform_AdminGroup.res +12 -0
  7. package/src/admin/Platform_AdminGroup.res.mjs +23 -0
  8. package/src/admin/Platform_Admin_Structure.res +14 -10
  9. package/src/admin/Platform_Admin_Structure.res.mjs +22 -33
  10. package/src/components/Aggregate/Aggregate_Callback.res +116 -39
  11. package/src/components/Aggregate/Aggregate_Callback.res.mjs +96 -37
  12. package/src/components/AutomationSlice/AutomationSlice_Builder.res +2 -1
  13. package/src/components/CommandGenerator/CommandGenerator_Callback.res +9 -8
  14. package/src/components/CommandGenerator/CommandGenerator_Callback.res.mjs +10 -8
  15. package/src/components/CommandTopic/CommandTopic.res.mjs +3 -0
  16. package/src/components/CommandTopic/CommandTopic_Helpers.res +37 -1
  17. package/src/components/CommandTopic/CommandTopic_Helpers.res.mjs +29 -0
  18. package/src/components/Counter/Counter_Builder.res +4 -2
  19. package/src/components/Dcb/Dcb_Builder.res +9 -6
  20. package/src/components/Dcb/Dcb_Builder.res.mjs +6 -6
  21. package/src/components/InboundTranslationSlice/InboundTranslationSlice_Builder.res +2 -1
  22. package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Builder.res +2 -1
  23. package/src/components/QueryDb/QueryDb_Builder.res +1 -1
  24. package/src/components/QueryDb/QueryDb_Builder.res.mjs +3 -2
  25. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +191 -141
  26. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs +66 -12
  27. package/src/components/StateViewSlice/StateViewSlice_Builder.res +1 -0
  28. package/src/plugin/api/PluginBaseFragment.res +1 -1
  29. package/src/plugin/api/PluginBaseFragment.res.mjs +2 -1
  30. package/src/plugin/component/Plugin_Builder.res +7 -3
  31. package/src/plugin/component/Plugin_Builder.res.mjs +6 -3
  32. package/src/plugin/component/Plugin_Helpers.res +35 -6
  33. package/src/plugin/component/Plugin_Helpers.res.mjs +47 -5
  34. package/src/plugin/component/Plugin_Structure.res +52 -19
  35. package/src/plugin/component/Plugin_Structure.res.mjs +40 -15
  36. package/src/plugin/lifecycle/PluginsReadModelSpec.res +1 -1
  37. package/src/plugin/lifecycle/PluginsReadModelSpec.res.mjs +1 -1
  38. package/tests/admin/AdminApiSchemaDriftTest.res +9 -3
  39. package/tests/admin/AdminApiSchemaDriftTest.res.mjs +3 -3
  40. package/tests/admin/Platform_Admin_StructureTest.res +17 -14
  41. package/tests/admin/Platform_Admin_StructureTest.res.mjs +31 -19
  42. package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +8 -72
  43. package/tests/admin/Platform_PluginStructuresApiTest.res.mjs +9 -39
  44. package/tests/api/GraphQL_FragmentGeneratorTest.res +1 -1
  45. package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +1 -1
  46. package/tests/commandgenerator/OwnerActingTest.res +601 -0
  47. package/tests/commandgenerator/OwnerActingTest.res.mjs +1018 -0
  48. package/tests/commandgenerator/OwnerStampingTest.res +90 -0
  49. package/tests/commandgenerator/OwnerStampingTest.res.mjs +87 -0
  50. package/tests/commandtopic/CommandOutcomeHookTest.res +1 -0
  51. package/tests/commandtopic/CommandOutcomeHookTest.res.mjs +11 -1
  52. package/tests/plugin/PluginStructureAccessTest.res +30 -1
  53. package/tests/plugin/PluginStructureAccessTest.res.mjs +28 -1
  54. package/tests/plugin/StateChange/PsGatedCommands.res +9 -1
  55. package/tests/plugin/StateChange/PsGatedCommands.res.mjs +4 -1
  56. package/tests/plugin/StateView/PsGatedView.res +1 -1
  57. package/tests/plugin/StateView/PsGatedView.res.mjs +1 -1
  58. 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[1];
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 cmdName = LogFormat$ReventlessCore.bold(Message$ReventlessCore.variantNameOfJson(Message$ReventlessCore.encode(command$p.command, Spec.commandSchema)));
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
- if (outcome.TAG === "CmdOk") {
203
- if (appendSucceeded) {
204
- CommandTopic_Helpers$ReventlessCore.reportAccepted(Spec.name, reference, {
205
- entityId: entityId,
206
- eventCount: appendedEventCount
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
- } else {
213
- CommandTopic_Helpers$ReventlessCore.reportRejected(Spec.name, "InfrastructureFailure", reference, {
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: "Error",
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[1];
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
- if (outcome.TAG === "CmdOk") {
260
- return outcome._0.map(event => ({
261
- id: id,
262
- meta: meta,
263
- event: event
264
- }));
265
- } else {
266
- return [];
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
- let authorization: Reventless.Authorization.permission = AllowAuthenticated
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
- ~identity: Reventless.Identity.t,
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 identity->Reventless.OwnerScope.resolve {
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, identity, serviceName) {
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
- let why = OwnerScope$Reventless.resolve(identity, undefined);
36
- if (typeof why !== "object") {
35
+ if (typeof scope !== "object") {
37
36
  return;
38
37
  }
39
- switch (why.TAG) {
38
+ switch (scope.TAG) {
40
39
  case "Elevated" :
41
40
  return;
42
41
  case "Owned" :
43
- let userId = why.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 (` + why._0 + `)`);
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, payload.identity, serviceName), Object.entries(obj)) : Stdlib_JsError.throwWithMessage("Couldn't decode:" + Stdlib_Option.getOr(JSON.stringify(payload.arguments), "<payload.arguments>"));
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
- let authorization: Reventless.Authorization.permission = AllowAuthenticated
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
- let authorization: Reventless.Authorization.permission = AllowAuthenticated
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=S.Spec.commandAuthorization->Obj.magic,
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=S.Spec.commandAuthorization->Obj.magic,
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=ITS.Spec.commandAuthorization->Obj.magic,
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
- let authorization: Reventless.Authorization.permission = AllowAuthenticated
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
- let authorization: Reventless.Authorization.permission = AllowAuthenticated
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
 
@@ -55,7 +55,7 @@ module Make = (
55
55
  ~subIdField,
56
56
  ~idResolverConfigs=Spec.config.idResolvers,
57
57
  ~idsResolverConfigs=Spec.config.idsResolvers,
58
- ~authorization=Spec.authorization,
58
+ ~authorization=Spec.authorization->Reventless.Authorization.named,
59
59
  ~opts,
60
60
  )
61
61
 
@@ -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
- /* Component-ReventlessCore Not a pure module */
54
+ /* Authorization-Reventless Not a pure module */