@reventlessdev/reventless-local 3.0.0-alpha.275 → 3.0.0-alpha.276
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 +15 -0
- package/package.json +9 -9
- package/src/Platform.res +27 -1
- package/src/Platform.res.mjs +19 -2
- package/src/ShellConfig.res +8 -1
- package/src/ShellConfig.res.mjs +6 -1
- package/src/adapter/Auth/LocalAuth.res +20 -5
- package/src/adapter/Auth/LocalAuth.res.mjs +26 -22
- package/src/adapter/PlatformGraphQL_Server.res +1 -1
- package/src/adapter/PlatformGraphQL_Server.res.mjs +2 -2
- package/tests/ShellConfigTest.res +31 -0
- package/tests/ShellConfigTest.res.mjs +26 -0
- package/tests/adapter/CommandAuthorizationTest.res +27 -3
- package/tests/adapter/CommandAuthorizationTest.res.mjs +23 -1
- package/tests/adapter/QueryDbListResolverTest.res +4 -2
- package/tests/adapter/QueryDbResolvedFieldsTest.res +4 -2
- package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res +6 -2
- package/tests/plugin/PluginBehavior_GWT.res.mjs +4 -2
- package/tests/plugin/UiFragmentRegistry_GWT.res.mjs +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,21 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 3.0.0-alpha.276 (2026-10-01)
|
|
7
|
+
|
|
8
|
+
* feat!: roles a plugin declares, groups a platform provides ([43daace](https://github.com/ReventlessDev/reventless-core/commit/43daace6ca588afe22562d079af1e6f1da81942e))
|
|
9
|
+
|
|
10
|
+
### BREAKING CHANGES
|
|
11
|
+
|
|
12
|
+
* AllowGroups and AdminGroup are removed — write AllowRoles over
|
|
13
|
+
the plugin's Roles.t, and use Role.admin / Platform.adminGroup(). Spec module
|
|
14
|
+
types declare `type role`, and their rules are Authorization.rule<role>; a spec
|
|
15
|
+
that writes its own rule declares `type role` (Reventless.Role.name for a rule
|
|
16
|
+
over Authorization.permission). Role.name is a private string: make one from a
|
|
17
|
+
string with Role.make.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
6
21
|
# 3.0.0-alpha.275 (2026-10-01)
|
|
7
22
|
|
|
8
23
|
### Features
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-local",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.276",
|
|
4
4
|
"description": "Local platform for Reventless (in-memory or SQLite backend, for development and testing without AWS)",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"bin": {
|
|
@@ -36,22 +36,22 @@
|
|
|
36
36
|
"ws": "^8.18.0",
|
|
37
37
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.34",
|
|
38
38
|
"@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.29",
|
|
39
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.11",
|
|
39
40
|
"@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.23",
|
|
40
41
|
"@reventlessdev/rescript-node": "2.0.0-alpha.20",
|
|
41
|
-
"@reventlessdev/rescript-jest": "1.0.0-alpha.11",
|
|
42
42
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.22",
|
|
43
|
-
"@reventlessdev/reventless-
|
|
44
|
-
"@reventlessdev/reventless-
|
|
45
|
-
"@reventlessdev/reventless-
|
|
46
|
-
"@reventlessdev/reventless-
|
|
47
|
-
"@reventlessdev/reventless-
|
|
43
|
+
"@reventlessdev/reventless-gwt": "1.0.0-alpha.233",
|
|
44
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.286",
|
|
45
|
+
"@reventlessdev/reventless-graphql-server": "1.0.0-alpha.134",
|
|
46
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.184",
|
|
47
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.156",
|
|
48
48
|
"@reventlessdev/reventless-seed": "1.0.0-alpha.31",
|
|
49
|
-
"@reventlessdev/reventless-
|
|
49
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.150"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"rescript": "12.3.1",
|
|
53
53
|
"sury-ppx": "11.0.0",
|
|
54
|
-
"@reventlessdev/reventless-ppx": "1.0.0-alpha.
|
|
54
|
+
"@reventlessdev/reventless-ppx": "1.0.0-alpha.96"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"rescript": "^12.3.1"
|
package/src/Platform.res
CHANGED
|
@@ -73,6 +73,24 @@ let decodeUiFragmentRegistryEventEnvelope = (eventJson: JSON.t): option<
|
|
|
73
73
|
// splitApi=true to serve core and plugin APIs on separate ports,
|
|
74
74
|
// backend=Backend.Sqlite({...}) to opt into file-backed SQLite persistence.
|
|
75
75
|
// Usage: module Platform = Platform.MakeWithConfig({let silent = true; let splitApi = false; let cloner = false; let backend = Backend.Memory})
|
|
76
|
+
/** Which group stands for which role, only where the two differ; every other
|
|
77
|
+
role maps to the group of its own name. Call before `Make()`: the platform
|
|
78
|
+
resolves roles while it is built, and a mapping stated later is refused. */
|
|
79
|
+
let roleGroups = Reventless.Role.setGroups
|
|
80
|
+
|
|
81
|
+
/** The groups of a user pool this deployment did not create, which no accounts
|
|
82
|
+
manifest names. They count as provided by the check that every role a plugin
|
|
83
|
+
needs has a group. */
|
|
84
|
+
let providedGroups = Reventless.Role.provideGroups
|
|
85
|
+
|
|
86
|
+
/** The group a role resolves to — the name this platform writes into its
|
|
87
|
+
directives, its shell configuration and the elevated groups. For code that
|
|
88
|
+
grants access by group, so it never spells one itself. */
|
|
89
|
+
let groupOf = Reventless.Role.groupOf
|
|
90
|
+
|
|
91
|
+
/** `groupOf(Role.admin)`. */
|
|
92
|
+
let adminGroup = Reventless.Role.adminGroup
|
|
93
|
+
|
|
76
94
|
module MakeWithConfig = (
|
|
77
95
|
Config: {
|
|
78
96
|
let silent: bool
|
|
@@ -87,6 +105,14 @@ module MakeWithConfig = (
|
|
|
87
105
|
// …) are no-ops in-memory. envVars are not propagated here because the
|
|
88
106
|
// local platform has no per-component process boundary.
|
|
89
107
|
let _ = Config.commandHandlerConfig
|
|
108
|
+
|
|
109
|
+
// What this platform provides for the roles its plugins need: every account it
|
|
110
|
+
// can sign in as, built-in and manifest alike. The manifest is read here rather
|
|
111
|
+
// than from the user store, which loads only when the servers start — after
|
|
112
|
+
// the plugins are built and checked.
|
|
113
|
+
Reventless.Role.provideGroupsFrom(() =>
|
|
114
|
+
LocalAuth.knownGroups()->Array.concat(Reventless.AccountsManifest.declaredGroups())
|
|
115
|
+
)
|
|
90
116
|
// Activate Pulumi mock mode — must happen before any component creation.
|
|
91
117
|
// Idempotent, so safe to call even if TestRunner.setup() was already called.
|
|
92
118
|
let _ = TestRunner.setup()
|
|
@@ -1789,7 +1815,7 @@ module MakeWithConfig = (
|
|
|
1789
1815
|
// structure manually.
|
|
1790
1816
|
pluginStructuresStore.contents->Dict.set(
|
|
1791
1817
|
ReventlessCore.Platform_Admin_Structure.pluginId,
|
|
1792
|
-
ReventlessCore.Platform_Admin_Structure.structure,
|
|
1818
|
+
ReventlessCore.Platform_Admin_Structure.structure(),
|
|
1793
1819
|
)
|
|
1794
1820
|
|
|
1795
1821
|
let pluginQueryDbName = ReventlessCore.PluginsReadModelSpec.name
|
package/src/Platform.res.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
|
12
12
|
import * as Effect from "effect/Effect";
|
|
13
13
|
import * as Pulumi from "@pulumi/pulumi";
|
|
14
14
|
import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js";
|
|
15
|
+
import * as Role$Reventless from "@reventlessdev/reventless-spec/src/types/Role.res.mjs";
|
|
15
16
|
import * as Plugin$Reventless from "@reventlessdev/reventless-spec/src/components/Plugin.res.mjs";
|
|
16
17
|
import * as AnsiStyle$Reventless from "@reventlessdev/reventless-spec/src/AnsiStyle.res.mjs";
|
|
17
18
|
import * as ReadModel$Reventless from "@reventlessdev/reventless-spec/src/components/ReadModel.res.mjs";
|
|
@@ -25,10 +26,12 @@ import * as UiSlots$ReventlessLocal from "./UiSlots.res.mjs";
|
|
|
25
26
|
import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
|
|
26
27
|
import * as LocalBus$ReventlessLocal from "./adapter/LocalBus.res.mjs";
|
|
27
28
|
import * as Api_Naming$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/Api_Naming.res.mjs";
|
|
29
|
+
import * as LocalAuth$ReventlessLocal from "./adapter/Auth/LocalAuth.res.mjs";
|
|
28
30
|
import * as PluginSpec$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/lifecycle/PluginSpec.res.mjs";
|
|
29
31
|
import * as UserStore$ReventlessLocal from "./adapter/Auth/UserStore.res.mjs";
|
|
30
32
|
import * as TestRunner$ReventlessLocal from "./test/TestRunner.res.mjs";
|
|
31
33
|
import * as UiFragments$ReventlessCore from "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateView/UiFragments.res.mjs";
|
|
34
|
+
import * as AccountsManifest$Reventless from "@reventlessdev/reventless-spec/src/types/AccountsManifest.res.mjs";
|
|
32
35
|
import * as CommandTopic$ReventlessCore from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic.res.mjs";
|
|
33
36
|
import * as EffectLogger$ReventlessCore from "@reventlessdev/reventless-core/src/util/EffectLogger.res.mjs";
|
|
34
37
|
import * as ShellConfig$ReventlessLocal from "./ShellConfig.res.mjs";
|
|
@@ -140,6 +143,7 @@ function decodeUiFragmentRegistryEventEnvelope(eventJson) {
|
|
|
140
143
|
}
|
|
141
144
|
|
|
142
145
|
function MakeWithConfig(Config) {
|
|
146
|
+
Role$Reventless.provideGroupsFrom(() => LocalAuth$ReventlessLocal.knownGroups().concat(AccountsManifest$Reventless.declaredGroups()));
|
|
143
147
|
TestRunner$ReventlessLocal.setup();
|
|
144
148
|
let match = Config.backend;
|
|
145
149
|
if (typeof match !== "object") {
|
|
@@ -1360,7 +1364,7 @@ function MakeWithConfig(Config) {
|
|
|
1360
1364
|
connectPlugin(plugins$1);
|
|
1361
1365
|
seedPluginStructuresStore(plugins$1);
|
|
1362
1366
|
}
|
|
1363
|
-
pluginStructuresStore.contents[Platform_Admin_Structure$ReventlessCore.pluginId] = Platform_Admin_Structure$ReventlessCore.structure;
|
|
1367
|
+
pluginStructuresStore.contents[Platform_Admin_Structure$ReventlessCore.pluginId] = Platform_Admin_Structure$ReventlessCore.structure();
|
|
1364
1368
|
CommandGeneratorResolvers_GraphQL$ReventlessLocal.setPluginStatusGate(field => {
|
|
1365
1369
|
let parts = field.split("_");
|
|
1366
1370
|
let pluginPrefix = Stdlib_Option.getOr(parts[0], "");
|
|
@@ -1905,6 +1909,7 @@ function MakeWithConfig(Config) {
|
|
|
1905
1909
|
|
|
1906
1910
|
function Make($star) {
|
|
1907
1911
|
let backend = Backend$ReventlessLocal.fromEnv();
|
|
1912
|
+
Role$Reventless.provideGroupsFrom(() => LocalAuth$ReventlessLocal.knownGroups().concat(AccountsManifest$Reventless.declaredGroups()));
|
|
1908
1913
|
TestRunner$ReventlessLocal.setup();
|
|
1909
1914
|
if (typeof backend !== "object") {
|
|
1910
1915
|
BackendState$ReventlessLocal.setMemory();
|
|
@@ -3113,7 +3118,7 @@ function Make($star) {
|
|
|
3113
3118
|
connectPlugin(plugins$1);
|
|
3114
3119
|
seedPluginStructuresStore(plugins$1);
|
|
3115
3120
|
}
|
|
3116
|
-
pluginStructuresStore.contents[Platform_Admin_Structure$ReventlessCore.pluginId] = Platform_Admin_Structure$ReventlessCore.structure;
|
|
3121
|
+
pluginStructuresStore.contents[Platform_Admin_Structure$ReventlessCore.pluginId] = Platform_Admin_Structure$ReventlessCore.structure();
|
|
3117
3122
|
CommandGeneratorResolvers_GraphQL$ReventlessLocal.setPluginStatusGate(field => {
|
|
3118
3123
|
let parts = field.split("_");
|
|
3119
3124
|
let pluginPrefix = Stdlib_Option.getOr(parts[0], "");
|
|
@@ -3642,6 +3647,14 @@ function Make($star) {
|
|
|
3642
3647
|
};
|
|
3643
3648
|
}
|
|
3644
3649
|
|
|
3650
|
+
let roleGroups = Role$Reventless.setGroups;
|
|
3651
|
+
|
|
3652
|
+
let providedGroups = Role$Reventless.provideGroups;
|
|
3653
|
+
|
|
3654
|
+
let groupOf = Role$Reventless.groupOf;
|
|
3655
|
+
|
|
3656
|
+
let adminGroup = Role$Reventless.adminGroup;
|
|
3657
|
+
|
|
3645
3658
|
export {
|
|
3646
3659
|
log,
|
|
3647
3660
|
platformGraphQLRef,
|
|
@@ -3649,6 +3662,10 @@ export {
|
|
|
3649
3662
|
platformMCPRef,
|
|
3650
3663
|
decodePluginEventEnvelope,
|
|
3651
3664
|
decodeUiFragmentRegistryEventEnvelope,
|
|
3665
|
+
roleGroups,
|
|
3666
|
+
providedGroups,
|
|
3667
|
+
groupOf,
|
|
3668
|
+
adminGroup,
|
|
3652
3669
|
MakeWithConfig,
|
|
3653
3670
|
Make,
|
|
3654
3671
|
}
|
package/src/ShellConfig.res
CHANGED
|
@@ -41,7 +41,12 @@ let manifestUrlOf = (config: ReventlessInfra.Platform.bakedManifest): string =>
|
|
|
41
41
|
// caller matching no declared group gets and what every existing deployment has.
|
|
42
42
|
let journeyManifestsKey = "journeyManifestUrls"
|
|
43
43
|
|
|
44
|
-
let computedKeys = [
|
|
44
|
+
let computedKeys = [
|
|
45
|
+
"manifestUrl",
|
|
46
|
+
journeyManifestsKey,
|
|
47
|
+
ReventlessCore.Platform_UiSlots.configKey,
|
|
48
|
+
ReventlessCore.Platform_AdminGroup.configKey,
|
|
49
|
+
]
|
|
45
50
|
|
|
46
51
|
/**
|
|
47
52
|
The overlay this platform puts on top of the shipped `config.json`.
|
|
@@ -69,6 +74,8 @@ let overlay = (
|
|
|
69
74
|
)
|
|
70
75
|
)
|
|
71
76
|
|
|
77
|
+
ReventlessCore.Platform_AdminGroup.configEntry()->Option.forEach(((k, v)) => out->Dict.set(k, v))
|
|
78
|
+
|
|
72
79
|
bakedManifest->Option.forEach(config => {
|
|
73
80
|
out->Dict.set("manifestUrl", JSON.Encode.string(manifestUrlOf(config)))
|
|
74
81
|
// Omitted entirely when nothing is declared, so a single-audience deployment
|
package/src/ShellConfig.res.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
|
9
9
|
import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
|
|
10
10
|
import * as HostShellDist$ReventlessLocal from "./HostShellDist.res.mjs";
|
|
11
11
|
import * as Platform_UiSlots$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_UiSlots.res.mjs";
|
|
12
|
+
import * as Platform_AdminGroup$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_AdminGroup.res.mjs";
|
|
12
13
|
import * as Platform_BakedManifest$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_BakedManifest.res.mjs";
|
|
13
14
|
|
|
14
15
|
let log = Logger$ReventlessCore.fromEnv();
|
|
@@ -26,7 +27,8 @@ let journeyManifestsKey = "journeyManifestUrls";
|
|
|
26
27
|
let computedKeys = [
|
|
27
28
|
"manifestUrl",
|
|
28
29
|
journeyManifestsKey,
|
|
29
|
-
Platform_UiSlots$ReventlessCore.configKey
|
|
30
|
+
Platform_UiSlots$ReventlessCore.configKey,
|
|
31
|
+
Platform_AdminGroup$ReventlessCore.configKey
|
|
30
32
|
];
|
|
31
33
|
|
|
32
34
|
function overlay(bakedManifest, uiSlotsFile, shellConfig) {
|
|
@@ -34,6 +36,9 @@ function overlay(bakedManifest, uiSlotsFile, shellConfig) {
|
|
|
34
36
|
Stdlib_Option.forEach(uiSlotsFile, param => {
|
|
35
37
|
out[Platform_UiSlots$ReventlessCore.configKey] = Platform_UiSlots$ReventlessCore.url;
|
|
36
38
|
});
|
|
39
|
+
Stdlib_Option.forEach(Platform_AdminGroup$ReventlessCore.configEntry(), param => {
|
|
40
|
+
out[param[0]] = param[1];
|
|
41
|
+
});
|
|
37
42
|
Stdlib_Option.forEach(bakedManifest, config => {
|
|
38
43
|
out["manifestUrl"] = Platform_BakedManifest$ReventlessCore.urlForKey(config.key);
|
|
39
44
|
let urls = Platform_BakedManifest$ReventlessCore.journeyUrls(config);
|
|
@@ -21,19 +21,22 @@ let defaultUser: Identity.t = {
|
|
|
21
21
|
provider: InMemory,
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
// A function, not a value: the administrator's group is the one the platform
|
|
25
|
+
// root maps `Role.admin` to, and this module is evaluated before the root runs.
|
|
26
|
+
let adminUser = (): Identity.t => {
|
|
25
27
|
userId: "local-admin",
|
|
26
28
|
username: "admin",
|
|
27
|
-
groups: [
|
|
29
|
+
groups: [Role.adminGroup(), "User"],
|
|
28
30
|
provider: InMemory,
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
// ── User registry ─────────────────────────────────────────────────────────
|
|
32
34
|
//
|
|
33
35
|
// A `dict<Identity.t>` keyed by `username`. A4 hydrates this from
|
|
34
|
-
// `.reventless/users.yaml`; tests inject via `registerUser`.
|
|
36
|
+
// `.reventless/users.yaml`; tests inject via `registerUser`. The built-in
|
|
37
|
+
// `admin` is answered on lookup, for the reason above, unless one is registered.
|
|
35
38
|
|
|
36
|
-
let initialUsers = () => Dict.fromArray([("user", defaultUser)
|
|
39
|
+
let initialUsers = () => Dict.fromArray([("user", defaultUser)])
|
|
37
40
|
|
|
38
41
|
let users: ref<dict<Identity.t>> = ref(initialUsers())
|
|
39
42
|
|
|
@@ -42,7 +45,19 @@ let registerUser = (~username: string, ~identity: Identity.t): unit =>
|
|
|
42
45
|
|
|
43
46
|
let resetUsers = (): unit => users := initialUsers()
|
|
44
47
|
|
|
45
|
-
let lookupUser = (username: string): option<Identity.t> =>
|
|
48
|
+
let lookupUser = (username: string): option<Identity.t> =>
|
|
49
|
+
switch users.contents->Dict.get(username) {
|
|
50
|
+
| None if username == "admin" => Some(adminUser())
|
|
51
|
+
| found => found
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The groups of every account this adapter can authenticate as, built-in
|
|
55
|
+
included — what the local platform provides. */
|
|
56
|
+
let knownGroups = (): array<string> =>
|
|
57
|
+
users.contents
|
|
58
|
+
->Dict.valuesToArray
|
|
59
|
+
->Array.concat([adminUser()])
|
|
60
|
+
->Array.flatMap(identity => identity.groups)
|
|
46
61
|
|
|
47
62
|
// ── Header parsing ────────────────────────────────────────────────────────
|
|
48
63
|
|
|
@@ -6,9 +6,9 @@ import * as Nodepath from "node:path";
|
|
|
6
6
|
import * as Nodecrypto from "node:crypto";
|
|
7
7
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
8
8
|
import * as Pulumi from "@pulumi/pulumi";
|
|
9
|
+
import * as Role$Reventless from "@reventlessdev/reventless-spec/src/types/Role.res.mjs";
|
|
9
10
|
import * as Identity$Reventless from "@reventlessdev/reventless-spec/src/types/Identity.res.mjs";
|
|
10
11
|
import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
|
|
11
|
-
import * as AdminGroup$Reventless from "@reventlessdev/reventless-spec/src/types/AdminGroup.res.mjs";
|
|
12
12
|
import * as Auth_ActiveRole$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/Auth/Auth_ActiveRole.res.mjs";
|
|
13
13
|
|
|
14
14
|
let defaultUser_groups = ["User"];
|
|
@@ -20,29 +20,23 @@ let defaultUser = {
|
|
|
20
20
|
provider: "InMemory"
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
23
|
+
function adminUser() {
|
|
24
|
+
return {
|
|
25
|
+
userId: "local-admin",
|
|
26
|
+
username: "admin",
|
|
27
|
+
groups: [
|
|
28
|
+
Role$Reventless.adminGroup(),
|
|
29
|
+
"User"
|
|
30
|
+
],
|
|
31
|
+
provider: "InMemory"
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
34
|
|
|
35
35
|
function initialUsers() {
|
|
36
|
-
return Object.fromEntries([
|
|
37
|
-
[
|
|
36
|
+
return Object.fromEntries([[
|
|
38
37
|
"user",
|
|
39
38
|
defaultUser
|
|
40
|
-
]
|
|
41
|
-
[
|
|
42
|
-
"admin",
|
|
43
|
-
adminUser
|
|
44
|
-
]
|
|
45
|
-
]);
|
|
39
|
+
]]);
|
|
46
40
|
}
|
|
47
41
|
|
|
48
42
|
let users = {
|
|
@@ -58,7 +52,16 @@ function resetUsers() {
|
|
|
58
52
|
}
|
|
59
53
|
|
|
60
54
|
function lookupUser(username) {
|
|
61
|
-
|
|
55
|
+
let found = users.contents[username];
|
|
56
|
+
if (found !== undefined || username !== "admin") {
|
|
57
|
+
return found;
|
|
58
|
+
} else {
|
|
59
|
+
return adminUser();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function knownGroups() {
|
|
64
|
+
return Object.values(users.contents).concat([adminUser()]).flatMap(identity => identity.groups);
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
function parseGroups(s) {
|
|
@@ -329,7 +332,7 @@ async function authenticate(ctx) {
|
|
|
329
332
|
let userHeader = ctx.headers["X-User".toLowerCase()];
|
|
330
333
|
let groupsHeader = ctx.headers["X-Groups".toLowerCase()];
|
|
331
334
|
if (userHeader !== undefined) {
|
|
332
|
-
let identity$1 =
|
|
335
|
+
let identity$1 = lookupUser(userHeader);
|
|
333
336
|
if (identity$1 === undefined) {
|
|
334
337
|
return "Anonymous";
|
|
335
338
|
}
|
|
@@ -371,6 +374,7 @@ export {
|
|
|
371
374
|
registerUser,
|
|
372
375
|
resetUsers,
|
|
373
376
|
lookupUser,
|
|
377
|
+
knownGroups,
|
|
374
378
|
parseGroups,
|
|
375
379
|
getHeader,
|
|
376
380
|
Login,
|
|
@@ -41,7 +41,7 @@ let wrapAdmin = (resolvers: dict<ReventlessGraphqlServer.GraphQL_ServerInstance.
|
|
|
41
41
|
resolvers
|
|
42
42
|
->Dict.toArray
|
|
43
43
|
->Array.forEach(((k, v)) =>
|
|
44
|
-
wrapped->Dict.set(k, Auth_GraphqlContext.requireGroup(~group=Reventless.
|
|
44
|
+
wrapped->Dict.set(k, Auth_GraphqlContext.requireGroup(~group=Reventless.Role.adminGroup(), v))
|
|
45
45
|
)
|
|
46
46
|
wrapped
|
|
47
47
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
-
import * as
|
|
3
|
+
import * as Role$Reventless from "@reventlessdev/reventless-spec/src/types/Role.res.mjs";
|
|
4
4
|
import * as Auth_GraphqlContext$ReventlessLocal from "./Auth/Auth_GraphqlContext.res.mjs";
|
|
5
5
|
import * as GraphQL_ServerInstance$ReventlessGraphqlServer from "@reventlessdev/reventless-graphql-server/src/GraphQL_ServerInstance.res.mjs";
|
|
6
6
|
|
|
@@ -9,7 +9,7 @@ let instance = GraphQL_ServerInstance$ReventlessGraphqlServer.make("GraphQL:Plat
|
|
|
9
9
|
function wrapAdmin(resolvers) {
|
|
10
10
|
let wrapped = {};
|
|
11
11
|
Object.entries(resolvers).forEach(param => {
|
|
12
|
-
wrapped[param[0]] = Auth_GraphqlContext$ReventlessLocal.requireGroup(
|
|
12
|
+
wrapped[param[0]] = Auth_GraphqlContext$ReventlessLocal.requireGroup(Role$Reventless.adminGroup(), param[1]);
|
|
13
13
|
});
|
|
14
14
|
return wrapped;
|
|
15
15
|
}
|
|
@@ -268,3 +268,34 @@ describe("ShellConfig.overlay — journeys", () => {
|
|
|
268
268
|
)->toBe(true)
|
|
269
269
|
)
|
|
270
270
|
})
|
|
271
|
+
|
|
272
|
+
describe("ShellConfig.overlay — adminGroup", () => {
|
|
273
|
+
testSync("is absent while the administrator role keeps its own name", () => {
|
|
274
|
+
Reventless.Role.clearGroups()
|
|
275
|
+
expect(
|
|
276
|
+
ShellConfig.overlay(~bakedManifest=None, ~shellConfig=None)->Dict.keysToArray,
|
|
277
|
+
)->toEqual([])
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
testSync("carries the group a renamed administrator role maps to", () => {
|
|
281
|
+
Reventless.Role.clearGroups()
|
|
282
|
+
Reventless.Role.setGroups([(Reventless.Role.admin, "shop-admins")])
|
|
283
|
+
let out = ShellConfig.overlay(~bakedManifest=None, ~shellConfig=None)
|
|
284
|
+
Reventless.Role.clearGroups()
|
|
285
|
+
expect(out->str("adminGroup"))->toEqual(Some("shop-admins"))
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
// Computed from the role mapping, so a passthrough cannot point the shell at
|
|
289
|
+
// another group than the one the server gates on.
|
|
290
|
+
testSync("a shellConfig naming it is refused", () =>
|
|
291
|
+
expect(
|
|
292
|
+
threw(
|
|
293
|
+
() =>
|
|
294
|
+
ShellConfig.overlay(
|
|
295
|
+
~bakedManifest=None,
|
|
296
|
+
~shellConfig=Some(Dict.fromArray([("adminGroup", JSON.Encode.string("x"))])),
|
|
297
|
+
)->ignore,
|
|
298
|
+
),
|
|
299
|
+
)->toBe(true)
|
|
300
|
+
)
|
|
301
|
+
})
|
|
@@ -5,6 +5,7 @@ import * as Nodeos from "node:os";
|
|
|
5
5
|
import * as Nodepath from "node:path";
|
|
6
6
|
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
7
7
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
8
|
+
import * as Role$Reventless from "@reventlessdev/reventless-spec/src/types/Role.res.mjs";
|
|
8
9
|
import * as TestRunner$ReventlessLocal from "../src/test/TestRunner.res.mjs";
|
|
9
10
|
import * as ShellConfig$ReventlessLocal from "../src/ShellConfig.res.mjs";
|
|
10
11
|
|
|
@@ -215,6 +216,31 @@ globalThis.describe("ShellConfig.overlay — journeys", () => {
|
|
|
215
216
|
});
|
|
216
217
|
});
|
|
217
218
|
|
|
219
|
+
globalThis.describe("ShellConfig.overlay — adminGroup", () => {
|
|
220
|
+
globalThis.test("is absent while the administrator role keeps its own name", () => {
|
|
221
|
+
Role$Reventless.clearGroups();
|
|
222
|
+
globalThis.expect(Object.keys(ShellConfig$ReventlessLocal.overlay(undefined, undefined, undefined))).toEqual([]);
|
|
223
|
+
});
|
|
224
|
+
globalThis.test("carries the group a renamed administrator role maps to", () => {
|
|
225
|
+
Role$Reventless.clearGroups();
|
|
226
|
+
Role$Reventless.setGroups([[
|
|
227
|
+
Role$Reventless.admin,
|
|
228
|
+
"shop-admins"
|
|
229
|
+
]]);
|
|
230
|
+
let out = ShellConfig$ReventlessLocal.overlay(undefined, undefined, undefined);
|
|
231
|
+
Role$Reventless.clearGroups();
|
|
232
|
+
globalThis.expect(str(out, "adminGroup")).toEqual("shop-admins");
|
|
233
|
+
});
|
|
234
|
+
globalThis.test("a shellConfig naming it is refused", () => {
|
|
235
|
+
globalThis.expect(threw(() => {
|
|
236
|
+
ShellConfig$ReventlessLocal.overlay(undefined, undefined, Object.fromEntries([[
|
|
237
|
+
"adminGroup",
|
|
238
|
+
"x"
|
|
239
|
+
]]));
|
|
240
|
+
})).toBe(true);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
218
244
|
export {
|
|
219
245
|
manifest,
|
|
220
246
|
str,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Resolver-boundary tests for per-constructor authorization (A3.3 + A3.3b).
|
|
2
2
|
// Mirrors the catalog Category aggregate's command shape: a record-payload
|
|
3
3
|
// constructor `Add({name})` defaulting to AllowAuthenticated, plus a
|
|
4
|
-
// payload-less `Archive` constrained to
|
|
4
|
+
// payload-less `Archive` constrained to AllowRoles([Admin]).
|
|
5
5
|
//
|
|
6
6
|
// The PPX emits a `commandAuthorization` switch at compile time (see
|
|
7
7
|
// examples/online-shop-hybrid/catalog/.../Category.res.mjs). This test hand-
|
|
@@ -23,11 +23,11 @@ type command =
|
|
|
23
23
|
| Rename({name: string})
|
|
24
24
|
| Archive
|
|
25
25
|
|
|
26
|
-
// Matches the shape the PPX emits for `@authorize(
|
|
26
|
+
// Matches the shape the PPX emits for `@authorize(AllowRoles([Admin]))` on
|
|
27
27
|
// the payload-less `Archive` constructor with a file-level default.
|
|
28
28
|
let commandAuthorization = (cmd: unknown): Reventless.Authorization.permission =>
|
|
29
29
|
if (cmd->Obj.magic: 'a) === "Archive" {
|
|
30
|
-
|
|
30
|
+
AllowRoles([Reventless.Role.make("Admin")])
|
|
31
31
|
} else {
|
|
32
32
|
AllowAuthenticated
|
|
33
33
|
}
|
|
@@ -189,6 +189,30 @@ describe("CommandGeneratorResolvers_GraphQL — per-constructor authorization",
|
|
|
189
189
|
expect(calls.contents)->toEqual(["Add"])
|
|
190
190
|
})
|
|
191
191
|
|
|
192
|
+
// The rule names the role; the resolver checks the group the platform maps it
|
|
193
|
+
// to, so a deployment that calls its administrators "ops" admits those.
|
|
194
|
+
testPromise("a renamed role admits its group and refuses the old name", async () => {
|
|
195
|
+
let (_, _, archiveResolver, calls) = buildFixture(~namespace="Cat8")
|
|
196
|
+
Reventless.Role.clearGroups()
|
|
197
|
+
Reventless.Role.setGroups([(Reventless.Role.make("Admin"), "ops")])
|
|
198
|
+
let asOps = await archiveResolver(
|
|
199
|
+
JSON.Encode.null,
|
|
200
|
+
JSON.Encode.object(Dict.make()),
|
|
201
|
+
ctxFor({...userIdentity, groups: ["ops"]}),
|
|
202
|
+
)
|
|
203
|
+
let asAdminGroup = await archiveResolver(
|
|
204
|
+
JSON.Encode.null,
|
|
205
|
+
JSON.Encode.object(Dict.make()),
|
|
206
|
+
ctxFor(adminIdentity),
|
|
207
|
+
)
|
|
208
|
+
Reventless.Role.clearGroups()
|
|
209
|
+
expect((getTypename(asOps), getErrorCode(asAdminGroup)))->toEqual((
|
|
210
|
+
"CommandAccepted",
|
|
211
|
+
"Forbidden",
|
|
212
|
+
))
|
|
213
|
+
expect(calls.contents)->toEqual(["Archive"])
|
|
214
|
+
})
|
|
215
|
+
|
|
192
216
|
testPromise(
|
|
193
217
|
"payload-less Archive registered as a GraphQL mutation field (no longer filtered)",
|
|
194
218
|
async () => {
|
|
@@ -6,6 +6,7 @@ import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
|
6
6
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
7
|
import * as Effect from "effect/Effect";
|
|
8
8
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
9
|
+
import * as Role$Reventless from "@reventlessdev/reventless-spec/src/types/Role.res.mjs";
|
|
9
10
|
import * as Identity$Reventless from "@reventlessdev/reventless-spec/src/types/Identity.res.mjs";
|
|
10
11
|
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
11
12
|
import * as TestRunner$ReventlessLocal from "../../src/test/TestRunner.res.mjs";
|
|
@@ -30,7 +31,7 @@ let commandSchema = Sury.union([
|
|
|
30
31
|
function commandAuthorization(cmd) {
|
|
31
32
|
if (cmd === "Archive") {
|
|
32
33
|
return {
|
|
33
|
-
TAG: "
|
|
34
|
+
TAG: "AllowRoles",
|
|
34
35
|
_0: ["Admin"]
|
|
35
36
|
};
|
|
36
37
|
} else {
|
|
@@ -170,6 +171,27 @@ globalThis.describe("CommandGeneratorResolvers_GraphQL — per-constructor autho
|
|
|
170
171
|
globalThis.expect(getTypename(response)).toEqual("CommandAccepted");
|
|
171
172
|
globalThis.expect(match[3].contents).toEqual(["Add"]);
|
|
172
173
|
});
|
|
174
|
+
globalThis.test("a renamed role admits its group and refuses the old name", async () => {
|
|
175
|
+
let match = buildFixture("Cat8");
|
|
176
|
+
let archiveResolver = match[2];
|
|
177
|
+
Role$Reventless.clearGroups();
|
|
178
|
+
Role$Reventless.setGroups([[
|
|
179
|
+
"Admin",
|
|
180
|
+
"ops"
|
|
181
|
+
]]);
|
|
182
|
+
let newrecord = {...userIdentity};
|
|
183
|
+
let asOps = await archiveResolver(null, {}, ctxFor((newrecord.groups = ["ops"], newrecord)));
|
|
184
|
+
let asAdminGroup = await archiveResolver(null, {}, ctxFor(adminIdentity));
|
|
185
|
+
Role$Reventless.clearGroups();
|
|
186
|
+
globalThis.expect([
|
|
187
|
+
getTypename(asOps),
|
|
188
|
+
getErrorCode(asAdminGroup)
|
|
189
|
+
]).toEqual([
|
|
190
|
+
"CommandAccepted",
|
|
191
|
+
"Forbidden"
|
|
192
|
+
]);
|
|
193
|
+
globalThis.expect(match[3].contents).toEqual(["Archive"]);
|
|
194
|
+
});
|
|
173
195
|
globalThis.test("payload-less Archive registered as a GraphQL mutation field (no longer filtered)", async () => {
|
|
174
196
|
buildFixture("Cat7");
|
|
175
197
|
let sdl = DomainGraphQL_Server$ReventlessLocal.asInterface.buildSdl();
|
|
@@ -145,7 +145,8 @@ let buildFixture = async (~name: string, ~indexes: array<Reventless.ReadModel.in
|
|
|
145
145
|
type state = rowState
|
|
146
146
|
let config = Reventless.ReadModel.config()
|
|
147
147
|
let subIdConfig = None
|
|
148
|
-
|
|
148
|
+
type role = Reventless.Role.name
|
|
149
|
+
let authorization: Reventless.Authorization.rule<role> = AllowAuthenticated
|
|
149
150
|
let visibility: Reventless.Visibility.t = Public
|
|
150
151
|
}
|
|
151
152
|
|
|
@@ -646,7 +647,8 @@ let buildSubIdFixture = async (~name: string) => {
|
|
|
646
647
|
Reventless.ReadModel.subIdField: "lineNo",
|
|
647
648
|
getSubId: (state: lineState) => state.lineNo,
|
|
648
649
|
})
|
|
649
|
-
|
|
650
|
+
type role = Reventless.Role.name
|
|
651
|
+
let authorization: Reventless.Authorization.rule<role> = AllowAuthenticated
|
|
650
652
|
let visibility: Reventless.Visibility.t = Public
|
|
651
653
|
}
|
|
652
654
|
|
|
@@ -96,7 +96,8 @@ let buildFixture = async (
|
|
|
96
96
|
type state = productRow
|
|
97
97
|
let config = Reventless.ReadModel.config()
|
|
98
98
|
let subIdConfig = None
|
|
99
|
-
|
|
99
|
+
type role = Reventless.Role.name
|
|
100
|
+
let authorization: Reventless.Authorization.rule<role> = AllowAuthenticated
|
|
100
101
|
let visibility: Reventless.Visibility.t = Public
|
|
101
102
|
}
|
|
102
103
|
|
|
@@ -125,7 +126,8 @@ let buildFixture = async (
|
|
|
125
126
|
],
|
|
126
127
|
)
|
|
127
128
|
let subIdConfig = None
|
|
128
|
-
|
|
129
|
+
type role = Reventless.Role.name
|
|
130
|
+
let authorization: Reventless.Authorization.rule<role> = AllowAuthenticated
|
|
129
131
|
let visibility: Reventless.Visibility.t = Public
|
|
130
132
|
}
|
|
131
133
|
|
|
@@ -222,7 +222,9 @@ describe("InboundTranslationSlice Callback", () => {
|
|
|
222
222
|
),
|
|
223
223
|
)
|
|
224
224
|
|
|
225
|
-
|
|
225
|
+
type role = Reventless.Role.name
|
|
226
|
+
|
|
227
|
+
let commandAuthorization = (_: command): Reventless.Authorization.rule<role> =>
|
|
226
228
|
AllowAuthenticated
|
|
227
229
|
type lifecycleState = unit
|
|
228
230
|
let commandTransition = (_: command): Reventless.Transition.t<lifecycleState> =>
|
|
@@ -289,7 +291,9 @@ describe("InboundTranslationSlice Callback", () => {
|
|
|
289
291
|
|
|
290
292
|
let translate = (_input: externalInput) => Ok([])
|
|
291
293
|
|
|
292
|
-
|
|
294
|
+
type role = Reventless.Role.name
|
|
295
|
+
|
|
296
|
+
let commandAuthorization = (_: command): Reventless.Authorization.rule<role> =>
|
|
293
297
|
AllowAuthenticated
|
|
294
298
|
type lifecycleState = unit
|
|
295
299
|
let commandTransition = (_: command): Reventless.Transition.t<lifecycleState> =>
|
|
@@ -9,13 +9,15 @@ let PluginTest = Behavior_GWT$ReventlessGwt.MakeFromAggregate({
|
|
|
9
9
|
name: PluginSpec$ReventlessCore.name,
|
|
10
10
|
commandSchema: PluginSpec$ReventlessCore.commandSchema,
|
|
11
11
|
eventSchema: PluginSpec$ReventlessCore.eventSchema,
|
|
12
|
-
errorSchema: PluginSpec$ReventlessCore.errorSchema
|
|
12
|
+
errorSchema: PluginSpec$ReventlessCore.errorSchema,
|
|
13
|
+
commandAuthorization: PluginSpec$ReventlessCore.commandAuthorization
|
|
13
14
|
})({
|
|
14
15
|
Spec: {
|
|
15
16
|
name: PluginSpec$ReventlessCore.name,
|
|
16
17
|
commandSchema: PluginSpec$ReventlessCore.commandSchema,
|
|
17
18
|
eventSchema: PluginSpec$ReventlessCore.eventSchema,
|
|
18
|
-
errorSchema: PluginSpec$ReventlessCore.errorSchema
|
|
19
|
+
errorSchema: PluginSpec$ReventlessCore.errorSchema,
|
|
20
|
+
commandAuthorization: PluginSpec$ReventlessCore.commandAuthorization
|
|
19
21
|
},
|
|
20
22
|
initialState: PluginBehavior$ReventlessCore.initialState,
|
|
21
23
|
evolve: PluginBehavior$ReventlessCore.evolve,
|
|
@@ -10,7 +10,8 @@ let Test = Behavior_GWT$ReventlessGwt.Make({
|
|
|
10
10
|
consumedEventSchema: UiFragmentRegistry$ReventlessCore.consumedEventSchema,
|
|
11
11
|
commandSchema: UiFragmentRegistry$ReventlessCore.commandSchema,
|
|
12
12
|
errorSchema: UiFragmentRegistry$ReventlessCore.errorSchema,
|
|
13
|
-
eventSchema: UiFragmentRegistry$ReventlessCore.eventSchema
|
|
13
|
+
eventSchema: UiFragmentRegistry$ReventlessCore.eventSchema,
|
|
14
|
+
commandAuthorization: UiFragmentRegistry$ReventlessCore.commandAuthorization
|
|
14
15
|
})({
|
|
15
16
|
initialState: undefined,
|
|
16
17
|
evolve: UiFragmentRegistry_Behavior$ReventlessCore.evolve,
|