@reventlessdev/reventless-local 3.0.0-alpha.275 → 3.0.0-alpha.277
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 +22 -0
- package/package.json +12 -12
- package/src/Platform.res +28 -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/Auth/UserStore.res +24 -1
- package/src/adapter/Auth/UserStore.res.mjs +15 -0
- 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/LocalAuthUserStoreTest.res +73 -0
- package/tests/adapter/LocalAuthUserStoreTest.res.mjs +92 -0
- 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,28 @@
|
|
|
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.277 (2026-10-01)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* check a plugin's roles against the user pool's groups ([ddb3c42](https://github.com/ReventlessDev/reventless-core/commit/ddb3c422b7226a19800e781abd5f03af18867007))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 3.0.0-alpha.276 (2026-10-01)
|
|
14
|
+
|
|
15
|
+
* feat!: roles a plugin declares, groups a platform provides ([43daace](https://github.com/ReventlessDev/reventless-core/commit/43daace6ca588afe22562d079af1e6f1da81942e))
|
|
16
|
+
|
|
17
|
+
### BREAKING CHANGES
|
|
18
|
+
|
|
19
|
+
* AllowGroups and AdminGroup are removed — write AllowRoles over
|
|
20
|
+
the plugin's Roles.t, and use Role.admin / Platform.adminGroup(). Spec module
|
|
21
|
+
types declare `type role`, and their rules are Authorization.rule<role>; a spec
|
|
22
|
+
that writes its own rule declares `type role` (Reventless.Role.name for a rule
|
|
23
|
+
over Authorization.permission). Role.name is a private string: make one from a
|
|
24
|
+
string with Role.make.
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
6
28
|
# 3.0.0-alpha.275 (2026-10-01)
|
|
7
29
|
|
|
8
30
|
### 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.277",
|
|
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": {
|
|
@@ -34,24 +34,24 @@
|
|
|
34
34
|
"graphql-yoga": "^5.21.0",
|
|
35
35
|
"sury": "11.0.0",
|
|
36
36
|
"ws": "^8.18.0",
|
|
37
|
-
"@reventlessdev/rescript-effect": "0.1.0-alpha.34",
|
|
38
37
|
"@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.29",
|
|
39
|
-
"@reventlessdev/rescript-
|
|
38
|
+
"@reventlessdev/rescript-effect": "0.1.0-alpha.34",
|
|
40
39
|
"@reventlessdev/rescript-node": "2.0.0-alpha.20",
|
|
41
40
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.11",
|
|
42
|
-
"@reventlessdev/rescript-
|
|
43
|
-
"@reventlessdev/
|
|
44
|
-
"@reventlessdev/reventless-
|
|
45
|
-
"@reventlessdev/reventless-
|
|
46
|
-
"@reventlessdev/reventless-
|
|
47
|
-
"@reventlessdev/reventless-
|
|
48
|
-
"@reventlessdev/reventless-
|
|
49
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.
|
|
41
|
+
"@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.23",
|
|
42
|
+
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.23",
|
|
43
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.287",
|
|
44
|
+
"@reventlessdev/reventless-gwt": "1.0.0-alpha.234",
|
|
45
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.151",
|
|
46
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.185",
|
|
47
|
+
"@reventlessdev/reventless-graphql-server": "1.0.0-alpha.135",
|
|
48
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.157",
|
|
49
|
+
"@reventlessdev/reventless-seed": "1.0.0-alpha.31"
|
|
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,11 @@ 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 loaded alike, or the file the store will load
|
|
111
|
+
// when the servers start — after the plugins are built and checked.
|
|
112
|
+
Reventless.Role.provideGroupsFrom(UserStore.providedGroups)
|
|
90
113
|
// Activate Pulumi mock mode — must happen before any component creation.
|
|
91
114
|
// Idempotent, so safe to call even if TestRunner.setup() was already called.
|
|
92
115
|
let _ = TestRunner.setup()
|
|
@@ -1789,7 +1812,7 @@ module MakeWithConfig = (
|
|
|
1789
1812
|
// structure manually.
|
|
1790
1813
|
pluginStructuresStore.contents->Dict.set(
|
|
1791
1814
|
ReventlessCore.Platform_Admin_Structure.pluginId,
|
|
1792
|
-
ReventlessCore.Platform_Admin_Structure.structure,
|
|
1815
|
+
ReventlessCore.Platform_Admin_Structure.structure(),
|
|
1793
1816
|
)
|
|
1794
1817
|
|
|
1795
1818
|
let pluginQueryDbName = ReventlessCore.PluginsReadModelSpec.name
|
|
@@ -2422,6 +2445,10 @@ module MakeWithConfig = (
|
|
|
2422
2445
|
}}`,
|
|
2423
2446
|
)
|
|
2424
2447
|
|
|
2448
|
+
// The role check below reads the accounts as they stand; a user store
|
|
2449
|
+
// loaded from elsewhere after this would leave it compared with the wrong ones.
|
|
2450
|
+
UserStore.pluginChecked := true
|
|
2451
|
+
|
|
2425
2452
|
// Set the active deploy target so resolveTargetGraphQL/MCP() and QueryDb serverRef/relayRef
|
|
2426
2453
|
// route registrations to the correct server. Mirrors AWS resolveTargetApi() pattern.
|
|
2427
2454
|
currentDeployTarget.contents = apiTarget
|
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";
|
|
@@ -140,6 +141,7 @@ function decodeUiFragmentRegistryEventEnvelope(eventJson) {
|
|
|
140
141
|
}
|
|
141
142
|
|
|
142
143
|
function MakeWithConfig(Config) {
|
|
144
|
+
Role$Reventless.provideGroupsFrom(UserStore$ReventlessLocal.providedGroups);
|
|
143
145
|
TestRunner$ReventlessLocal.setup();
|
|
144
146
|
let match = Config.backend;
|
|
145
147
|
if (typeof match !== "object") {
|
|
@@ -1360,7 +1362,7 @@ function MakeWithConfig(Config) {
|
|
|
1360
1362
|
connectPlugin(plugins$1);
|
|
1361
1363
|
seedPluginStructuresStore(plugins$1);
|
|
1362
1364
|
}
|
|
1363
|
-
pluginStructuresStore.contents[Platform_Admin_Structure$ReventlessCore.pluginId] = Platform_Admin_Structure$ReventlessCore.structure;
|
|
1365
|
+
pluginStructuresStore.contents[Platform_Admin_Structure$ReventlessCore.pluginId] = Platform_Admin_Structure$ReventlessCore.structure();
|
|
1364
1366
|
CommandGeneratorResolvers_GraphQL$ReventlessLocal.setPluginStatusGate(field => {
|
|
1365
1367
|
let parts = field.split("_");
|
|
1366
1368
|
let pluginPrefix = Stdlib_Option.getOr(parts[0], "");
|
|
@@ -1753,6 +1755,7 @@ function MakeWithConfig(Config) {
|
|
|
1753
1755
|
let tmp;
|
|
1754
1756
|
tmp = apiTarget === "Domain" ? "Domain" : "Platform";
|
|
1755
1757
|
log.info("Platform", undefined, `deployPlugin target=` + tmp);
|
|
1758
|
+
UserStore$ReventlessLocal.pluginChecked.contents = true;
|
|
1756
1759
|
currentDeployTarget.contents = apiTarget;
|
|
1757
1760
|
StateViewSliceMaker.QueryDbResolvers.serverRef.contents = resolveTargetGraphQL();
|
|
1758
1761
|
let tmp$1;
|
|
@@ -1905,6 +1908,7 @@ function MakeWithConfig(Config) {
|
|
|
1905
1908
|
|
|
1906
1909
|
function Make($star) {
|
|
1907
1910
|
let backend = Backend$ReventlessLocal.fromEnv();
|
|
1911
|
+
Role$Reventless.provideGroupsFrom(UserStore$ReventlessLocal.providedGroups);
|
|
1908
1912
|
TestRunner$ReventlessLocal.setup();
|
|
1909
1913
|
if (typeof backend !== "object") {
|
|
1910
1914
|
BackendState$ReventlessLocal.setMemory();
|
|
@@ -3113,7 +3117,7 @@ function Make($star) {
|
|
|
3113
3117
|
connectPlugin(plugins$1);
|
|
3114
3118
|
seedPluginStructuresStore(plugins$1);
|
|
3115
3119
|
}
|
|
3116
|
-
pluginStructuresStore.contents[Platform_Admin_Structure$ReventlessCore.pluginId] = Platform_Admin_Structure$ReventlessCore.structure;
|
|
3120
|
+
pluginStructuresStore.contents[Platform_Admin_Structure$ReventlessCore.pluginId] = Platform_Admin_Structure$ReventlessCore.structure();
|
|
3117
3121
|
CommandGeneratorResolvers_GraphQL$ReventlessLocal.setPluginStatusGate(field => {
|
|
3118
3122
|
let parts = field.split("_");
|
|
3119
3123
|
let pluginPrefix = Stdlib_Option.getOr(parts[0], "");
|
|
@@ -3497,6 +3501,7 @@ function Make($star) {
|
|
|
3497
3501
|
let tmp;
|
|
3498
3502
|
tmp = apiTarget === "Domain" ? "Domain" : "Platform";
|
|
3499
3503
|
log.info("Platform", undefined, `deployPlugin target=` + tmp);
|
|
3504
|
+
UserStore$ReventlessLocal.pluginChecked.contents = true;
|
|
3500
3505
|
currentDeployTarget.contents = apiTarget;
|
|
3501
3506
|
StateViewSliceMaker.QueryDbResolvers.serverRef.contents = resolveTargetGraphQL();
|
|
3502
3507
|
let tmp$1;
|
|
@@ -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,
|
|
@@ -56,12 +56,35 @@ type resolution =
|
|
|
56
56
|
// auto-discovery before `Platform.startServers`.
|
|
57
57
|
let resolved: ref<bool> = ref(false)
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
/** Set once a plugin has been built, and with it the check that every role it
|
|
60
|
+
needs has a group among these accounts. */
|
|
61
|
+
let pluginChecked: ref<bool> = ref(false)
|
|
62
|
+
|
|
63
|
+
let resetResolution = (): unit => {
|
|
64
|
+
resolved := false
|
|
65
|
+
pluginChecked := false
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
The groups the store's accounts will belong to, for the role check, which runs
|
|
70
|
+
before the servers start and load it: the loaded accounts once `load` has run,
|
|
71
|
+
otherwise the default file `autoLoadOnce` will read (or the template `setup`
|
|
72
|
+
makes it from). Never the working directory once another source was loaded.
|
|
73
|
+
*/
|
|
74
|
+
let providedGroups = (): array<string> =>
|
|
75
|
+
LocalAuth.knownGroups()->Array.concat(resolved.contents ? [] : AccountsManifest.declaredGroups())
|
|
60
76
|
|
|
61
77
|
let load = (~users: option<array<entry>>=?, ~usersFile: option<string>=?, ()): result<
|
|
62
78
|
resolution,
|
|
63
79
|
string,
|
|
64
80
|
> => {
|
|
81
|
+
// The default file is what the check already read; any other source arrives
|
|
82
|
+
// after it, and the check would have compared the plugins with other accounts.
|
|
83
|
+
if pluginChecked.contents && (users->Option.isSome || usersFile->Option.isSome) {
|
|
84
|
+
JsError.throwWithMessage(
|
|
85
|
+
"UserStore.load(~users / ~usersFile) after a plugin was built — the check that every role it needs has a group read other accounts. Call UserStore.load before deploying plugins.",
|
|
86
|
+
)
|
|
87
|
+
}
|
|
65
88
|
resolved := true
|
|
66
89
|
switch users {
|
|
67
90
|
| Some(entries) =>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as Nodefs from "node:fs";
|
|
4
4
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
5
|
import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
|
|
6
|
+
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
6
7
|
import * as LocalAuth$ReventlessLocal from "./LocalAuth.res.mjs";
|
|
7
8
|
import * as AccountsManifest$Reventless from "@reventlessdev/reventless-spec/src/types/AccountsManifest.res.mjs";
|
|
8
9
|
|
|
@@ -25,11 +26,23 @@ let resolved = {
|
|
|
25
26
|
contents: false
|
|
26
27
|
};
|
|
27
28
|
|
|
29
|
+
let pluginChecked = {
|
|
30
|
+
contents: false
|
|
31
|
+
};
|
|
32
|
+
|
|
28
33
|
function resetResolution() {
|
|
29
34
|
resolved.contents = false;
|
|
35
|
+
pluginChecked.contents = false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function providedGroups() {
|
|
39
|
+
return LocalAuth$ReventlessLocal.knownGroups().concat(resolved.contents ? [] : AccountsManifest$Reventless.declaredGroups());
|
|
30
40
|
}
|
|
31
41
|
|
|
32
42
|
function load(users, usersFile, param) {
|
|
43
|
+
if (pluginChecked.contents && (Stdlib_Option.isSome(users) || Stdlib_Option.isSome(usersFile))) {
|
|
44
|
+
Stdlib_JsError.throwWithMessage("UserStore.load(~users / ~usersFile) after a plugin was built — the check that every role it needs has a group read other accounts. Call UserStore.load before deploying plugins.");
|
|
45
|
+
}
|
|
33
46
|
resolved.contents = true;
|
|
34
47
|
if (users !== undefined) {
|
|
35
48
|
_registerEntries(users);
|
|
@@ -84,7 +97,9 @@ export {
|
|
|
84
97
|
_registerEntries,
|
|
85
98
|
_defaultPath,
|
|
86
99
|
resolved,
|
|
100
|
+
pluginChecked,
|
|
87
101
|
resetResolution,
|
|
102
|
+
providedGroups,
|
|
88
103
|
load,
|
|
89
104
|
autoLoadOnce,
|
|
90
105
|
}
|
|
@@ -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();
|
|
@@ -160,3 +160,76 @@ testPromise("autoLoadOnce is a no-op after an explicit load", async () => {
|
|
|
160
160
|
| None => JsError.throwWithMessage("autoLoadOnce wiped explicit registration")
|
|
161
161
|
}
|
|
162
162
|
})
|
|
163
|
+
|
|
164
|
+
// What the role check counts as provided, before the servers load the store.
|
|
165
|
+
describe("UserStore.providedGroups", () => {
|
|
166
|
+
// Runs `f` in a directory holding `files` (path relative to it → contents).
|
|
167
|
+
let inDir = (files: array<(string, string)>, f) => {
|
|
168
|
+
let dir = NodeFs.mkdtempSync(NodePath.join([NodeOs.tmpdir(), "reventless-provided-"]))
|
|
169
|
+
files->Array.forEach(((file, contents)) => {
|
|
170
|
+
let path = NodePath.join([dir, file])
|
|
171
|
+
NodeFs.mkdirSync(NodePath.dirname(path), {recursive: true})
|
|
172
|
+
NodeFs.writeFileSync(path, contents)
|
|
173
|
+
})
|
|
174
|
+
let cwd = NodeProcess.cwd()
|
|
175
|
+
NodeProcess.chdir(dir)
|
|
176
|
+
let result = try f() catch {
|
|
177
|
+
| exn =>
|
|
178
|
+
NodeProcess.chdir(cwd)
|
|
179
|
+
throw(exn)
|
|
180
|
+
}
|
|
181
|
+
NodeProcess.chdir(cwd)
|
|
182
|
+
NodeFs.rmSync(dir, {recursive: true, force: true})
|
|
183
|
+
result
|
|
184
|
+
}
|
|
185
|
+
let manifest = group => `- username: m\n password: m\n groups: [${group}]\n`
|
|
186
|
+
let has = (group, groups) => groups->Array.includes(group)
|
|
187
|
+
|
|
188
|
+
testSync("counts the default file the servers will load", () => {
|
|
189
|
+
resetAll()
|
|
190
|
+
let groups = inDir(
|
|
191
|
+
[(".reventless/users.yaml", manifest("Merchandiser"))],
|
|
192
|
+
() => UserStore.providedGroups(),
|
|
193
|
+
)
|
|
194
|
+
expect(has("Merchandiser", groups))->toBe(true)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
testSync("counts the template where setup has not made the file yet", () => {
|
|
198
|
+
resetAll()
|
|
199
|
+
let groups = inDir(
|
|
200
|
+
[("users.example.yaml", manifest("Fulfilment"))],
|
|
201
|
+
() => UserStore.providedGroups(),
|
|
202
|
+
)
|
|
203
|
+
expect(has("Fulfilment", groups))->toBe(true)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
testSync("once loaded from elsewhere, counts those accounts and not the directory's", () => {
|
|
207
|
+
resetAll()
|
|
208
|
+
let _ = UserStore.load(~users=[{username: "i", password: "i", groups: ["Inline"]}], ())
|
|
209
|
+
let groups = inDir(
|
|
210
|
+
[(".reventless/users.yaml", manifest("Merchandiser"))],
|
|
211
|
+
() => UserStore.providedGroups(),
|
|
212
|
+
)
|
|
213
|
+
expect((has("Inline", groups), has("Merchandiser", groups)))->toEqual((true, false))
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
testSync("loading other accounts after a plugin was checked is refused", () => {
|
|
217
|
+
resetAll()
|
|
218
|
+
UserStore.pluginChecked := true
|
|
219
|
+
let refused = switch UserStore.load(
|
|
220
|
+
~users=[{username: "late", password: "l", groups: ["Late"]}],
|
|
221
|
+
(),
|
|
222
|
+
) {
|
|
223
|
+
| _ => false
|
|
224
|
+
| exception JsExn(_) => true
|
|
225
|
+
}
|
|
226
|
+
expect(refused)->toBe(true)
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
testSync("the default load after a plugin was checked is the one the check read", () => {
|
|
230
|
+
resetAll()
|
|
231
|
+
UserStore.pluginChecked := true
|
|
232
|
+
let loaded = inDir([], () => UserStore.load()->Result.isOk)
|
|
233
|
+
expect(loaded)->toBe(true)
|
|
234
|
+
})
|
|
235
|
+
})
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
import * as Nodefs from "node:fs";
|
|
4
4
|
import * as Nodeos from "node:os";
|
|
5
5
|
import * as Nodepath from "node:path";
|
|
6
|
+
import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
|
|
6
7
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
8
|
+
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
7
9
|
import * as LocalAuth$ReventlessLocal from "../../src/adapter/Auth/LocalAuth.res.mjs";
|
|
8
10
|
import * as UserStore$ReventlessLocal from "../../src/adapter/Auth/UserStore.res.mjs";
|
|
9
11
|
|
|
@@ -201,6 +203,96 @@ globalThis.test("autoLoadOnce is a no-op after an explicit load", async () => {
|
|
|
201
203
|
}
|
|
202
204
|
});
|
|
203
205
|
|
|
206
|
+
globalThis.describe("UserStore.providedGroups", () => {
|
|
207
|
+
let inDir = (files, f) => {
|
|
208
|
+
let dir = Nodefs.mkdtempSync(Nodepath.join(Nodeos.tmpdir(), "reventless-provided-"));
|
|
209
|
+
files.forEach(param => {
|
|
210
|
+
let path = Nodepath.join(dir, param[0]);
|
|
211
|
+
Nodefs.mkdirSync(Nodepath.dirname(path), {
|
|
212
|
+
recursive: true
|
|
213
|
+
});
|
|
214
|
+
Nodefs.writeFileSync(path, param[1], "utf8");
|
|
215
|
+
});
|
|
216
|
+
let cwd = process.cwd();
|
|
217
|
+
process.chdir(dir);
|
|
218
|
+
let result;
|
|
219
|
+
try {
|
|
220
|
+
result = f();
|
|
221
|
+
} catch (exn) {
|
|
222
|
+
process.chdir(cwd);
|
|
223
|
+
throw exn;
|
|
224
|
+
}
|
|
225
|
+
process.chdir(cwd);
|
|
226
|
+
Nodefs.rmSync(dir, {
|
|
227
|
+
recursive: true,
|
|
228
|
+
force: true
|
|
229
|
+
});
|
|
230
|
+
return result;
|
|
231
|
+
};
|
|
232
|
+
globalThis.test("counts the default file the servers will load", () => {
|
|
233
|
+
resetAll();
|
|
234
|
+
let groups = inDir([[
|
|
235
|
+
".reventless/users.yaml",
|
|
236
|
+
`- username: m\n password: m\n groups: [` + "Merchandiser" + `]\n`
|
|
237
|
+
]], () => UserStore$ReventlessLocal.providedGroups());
|
|
238
|
+
globalThis.expect(groups.includes("Merchandiser")).toBe(true);
|
|
239
|
+
});
|
|
240
|
+
globalThis.test("counts the template where setup has not made the file yet", () => {
|
|
241
|
+
resetAll();
|
|
242
|
+
let groups = inDir([[
|
|
243
|
+
"users.example.yaml",
|
|
244
|
+
`- username: m\n password: m\n groups: [` + "Fulfilment" + `]\n`
|
|
245
|
+
]], () => UserStore$ReventlessLocal.providedGroups());
|
|
246
|
+
globalThis.expect(groups.includes("Fulfilment")).toBe(true);
|
|
247
|
+
});
|
|
248
|
+
globalThis.test("once loaded from elsewhere, counts those accounts and not the directory's", () => {
|
|
249
|
+
resetAll();
|
|
250
|
+
UserStore$ReventlessLocal.load([{
|
|
251
|
+
username: "i",
|
|
252
|
+
password: "i",
|
|
253
|
+
groups: ["Inline"]
|
|
254
|
+
}], undefined, undefined);
|
|
255
|
+
let groups = inDir([[
|
|
256
|
+
".reventless/users.yaml",
|
|
257
|
+
`- username: m\n password: m\n groups: [` + "Merchandiser" + `]\n`
|
|
258
|
+
]], () => UserStore$ReventlessLocal.providedGroups());
|
|
259
|
+
globalThis.expect([
|
|
260
|
+
groups.includes("Inline"),
|
|
261
|
+
groups.includes("Merchandiser")
|
|
262
|
+
]).toEqual([
|
|
263
|
+
true,
|
|
264
|
+
false
|
|
265
|
+
]);
|
|
266
|
+
});
|
|
267
|
+
globalThis.test("loading other accounts after a plugin was checked is refused", () => {
|
|
268
|
+
resetAll();
|
|
269
|
+
UserStore$ReventlessLocal.pluginChecked.contents = true;
|
|
270
|
+
let refused;
|
|
271
|
+
try {
|
|
272
|
+
UserStore$ReventlessLocal.load([{
|
|
273
|
+
username: "late",
|
|
274
|
+
password: "l",
|
|
275
|
+
groups: ["Late"]
|
|
276
|
+
}], undefined, undefined);
|
|
277
|
+
refused = false;
|
|
278
|
+
} catch (raw_exn) {
|
|
279
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
280
|
+
if (exn.RE_EXN_ID === "JsExn") {
|
|
281
|
+
refused = true;
|
|
282
|
+
} else {
|
|
283
|
+
throw exn;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
globalThis.expect(refused).toBe(true);
|
|
287
|
+
});
|
|
288
|
+
globalThis.test("the default load after a plugin was checked is the one the check read", () => {
|
|
289
|
+
resetAll();
|
|
290
|
+
UserStore$ReventlessLocal.pluginChecked.contents = true;
|
|
291
|
+
let loaded = inDir([], () => Stdlib_Result.isOk(UserStore$ReventlessLocal.load(undefined, undefined, undefined)));
|
|
292
|
+
globalThis.expect(loaded).toBe(true);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
204
296
|
export {
|
|
205
297
|
resetAll,
|
|
206
298
|
_writeYamlFile,
|
|
@@ -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,
|