@reventlessdev/reventless-spec 3.0.0-alpha.156 → 3.0.0-alpha.158
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 +2 -2
- package/src/generator/Codegen.res +10 -5
- package/src/generator/Codegen.res.mjs +5 -5
- package/src/types/Role.res +49 -14
- package/src/types/Role.res.mjs +45 -3
- package/src/types/RoleCoverage.res +39 -8
- package/src/types/RoleCoverage.res.mjs +24 -3
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.158 (2026-10-06)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @reventlessdev/reventless-spec
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# 3.0.0-alpha.157 (2026-10-01)
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* check a plugin's roles against the user pool's groups ([ddb3c42](https://github.com/ReventlessDev/reventless-core/commit/ddb3c422b7226a19800e781abd5f03af18867007))
|
|
19
|
+
|
|
20
|
+
|
|
6
21
|
# 3.0.0-alpha.156 (2026-10-01)
|
|
7
22
|
|
|
8
23
|
* feat!: roles a plugin declares, groups a platform provides ([43daace](https://github.com/ReventlessDev/reventless-core/commit/43daace6ca588afe22562d079af1e6f1da81942e))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-spec",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.158",
|
|
4
4
|
"description": "Specifications for Reventless",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"bin": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"sury": "11.0.0",
|
|
27
27
|
"sury-ppx": "11.0.0",
|
|
28
28
|
"yaml": "^2.8.3",
|
|
29
|
-
"@reventlessdev/rescript-node": "2.0.0-alpha.
|
|
29
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.21"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"rescript": "12.3.1",
|
|
@@ -567,12 +567,17 @@ let renderMain = (~config: Config.config): string => {
|
|
|
567
567
|
"module Platform = ReventlessAws.Platform.Make()",
|
|
568
568
|
"module " ++ name ++ " = Plugin.Make(Platform)",
|
|
569
569
|
"",
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
570
|
+
// The plugin is deployed once the user pool's groups are in, for the check
|
|
571
|
+
// that every role it needs has one, which runs while `deployPlugin` builds
|
|
572
|
+
// it. A promise rather than a top-level await: Pulumi `require`s the
|
|
573
|
+
// program, which Node refuses for a module that awaits at the top level.
|
|
574
|
+
// Pulumi resolves the promise in the stack outputs.
|
|
575
|
+
"let default = ReventlessAws.Platform.loadProvidedGroups()->Promise.thenResolve(() => {",
|
|
576
|
+
" let deployed = Platform.deployPlugin(~plugin=module(" ++ name ++ "))",
|
|
574
577
|
// PostDeploy runs after the graph is registered (exports, cross-stack output).
|
|
575
|
-
"ReventlessInfra.DeployBootstrap.run(PostDeploy)",
|
|
578
|
+
" ReventlessInfra.DeployBootstrap.run(PostDeploy)",
|
|
579
|
+
" deployed",
|
|
580
|
+
"})",
|
|
576
581
|
"",
|
|
577
582
|
]->Array.join("\n")
|
|
578
583
|
}
|
|
@@ -365,11 +365,11 @@ function renderMain(config) {
|
|
|
365
365
|
"module Platform = ReventlessAws.Platform.Make()",
|
|
366
366
|
"module " + name + " = Plugin.Make(Platform)",
|
|
367
367
|
"",
|
|
368
|
-
"let default = Platform.
|
|
369
|
-
" ~plugin=module(" + name + ")
|
|
370
|
-
")",
|
|
371
|
-
"",
|
|
372
|
-
"
|
|
368
|
+
"let default = ReventlessAws.Platform.loadProvidedGroups()->Promise.thenResolve(() => {",
|
|
369
|
+
" let deployed = Platform.deployPlugin(~plugin=module(" + name + "))",
|
|
370
|
+
" ReventlessInfra.DeployBootstrap.run(PostDeploy)",
|
|
371
|
+
" deployed",
|
|
372
|
+
"})",
|
|
373
373
|
""
|
|
374
374
|
].join("\n");
|
|
375
375
|
}
|
package/src/types/Role.res
CHANGED
|
@@ -107,9 +107,9 @@ let groupOf = (role: name): string =>
|
|
|
107
107
|
let adminGroup = (): string => groupOf(admin)
|
|
108
108
|
|
|
109
109
|
/**
|
|
110
|
-
Where the groups this deployment provides come from: the
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
Where the groups this deployment provides come from: what the identity provider
|
|
111
|
+
reports where the platform can ask it, else the administrator group and accounts
|
|
112
|
+
manifest a platform finds for itself and the groups a platform root declares.
|
|
113
113
|
|
|
114
114
|
Sources rather than values, read when the check runs: a platform states them
|
|
115
115
|
while its functor is applied, before the root has said how its roles map. Empty
|
|
@@ -118,26 +118,61 @@ has an answer to compare against.
|
|
|
118
118
|
*/
|
|
119
119
|
let groupSources: ref<array<unit => array<string>>> = ref([])
|
|
120
120
|
|
|
121
|
+
/** The groups a root declared with `provideGroups`, kept apart so a listing can
|
|
122
|
+
contradict them. */
|
|
123
|
+
let declaredGroups: ref<array<string>> = ref([])
|
|
124
|
+
|
|
125
|
+
/** The groups the identity provider itself reported, and which provider that was. */
|
|
126
|
+
type listing = {source: string, groups: array<string>}
|
|
127
|
+
|
|
128
|
+
let listing: ref<option<listing>> = ref(None)
|
|
129
|
+
|
|
121
130
|
let provideGroupsFrom = (source: unit => array<string>) =>
|
|
122
131
|
groupSources := groupSources.contents->Array.concat([source])
|
|
123
132
|
|
|
124
|
-
let provideGroups = (groups: array<string>) =>
|
|
133
|
+
let provideGroups = (groups: array<string>) => {
|
|
134
|
+
declaredGroups := declaredGroups.contents->Array.concat(groups)
|
|
135
|
+
provideGroupsFrom(() => groups)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
Record what the identity provider says it has. A platform that can ask (the AWS
|
|
140
|
+
user pool) does, before its plugins are built. The listing is then the whole
|
|
141
|
+
answer: a group a manifest names, a root declares or the mapping points to is
|
|
142
|
+
provided only if the provider has it, since a group it lacks refuses everybody.
|
|
143
|
+
*/
|
|
144
|
+
let provideListedGroups = (~source: string, groups: array<string>) =>
|
|
145
|
+
listing := Some({source, groups})
|
|
146
|
+
|
|
147
|
+
let clearProvidedGroups = () => {
|
|
148
|
+
groupSources := []
|
|
149
|
+
declaredGroups := []
|
|
150
|
+
listing := None
|
|
151
|
+
}
|
|
125
152
|
|
|
126
|
-
let
|
|
153
|
+
let _distinct = (groups: array<string>) => groups->Set.fromArray->Set.values->Array.fromIterator
|
|
127
154
|
|
|
128
|
-
/** The groups a deployment provides:
|
|
129
|
-
names. `None` when nothing was
|
|
130
|
-
unit test, a function runtime)
|
|
155
|
+
/** The groups a deployment provides: the provider's listing where there is one,
|
|
156
|
+
else every source plus every group the mapping names. `None` when nothing was
|
|
157
|
+
provided — a process that is not a platform (a unit test, a function runtime)
|
|
158
|
+
has no answer to give. */
|
|
131
159
|
let providedGroups = (): option<array<string>> =>
|
|
132
|
-
switch groupSources.contents {
|
|
133
|
-
|
|
|
134
|
-
|
|
|
160
|
+
switch (listing.contents, groupSources.contents) {
|
|
161
|
+
| (Some({groups}), _) => Some(_distinct(groups))
|
|
162
|
+
| (None, []) => None
|
|
163
|
+
| (None, sources) =>
|
|
135
164
|
Some(
|
|
136
165
|
sources
|
|
137
166
|
->Array.flatMap(source => source())
|
|
138
167
|
->Array.concat(renamed()->Array.map(((_, group)) => group))
|
|
139
|
-
->
|
|
140
|
-
->Set.values
|
|
141
|
-
->Array.fromIterator,
|
|
168
|
+
->_distinct,
|
|
142
169
|
)
|
|
143
170
|
}
|
|
171
|
+
|
|
172
|
+
/** The declared groups the provider's listing does not have. Empty without a
|
|
173
|
+
listing, when there is nothing to contradict them. */
|
|
174
|
+
let declaredButMissing = (): array<string> =>
|
|
175
|
+
switch listing.contents {
|
|
176
|
+
| None => []
|
|
177
|
+
| Some({groups}) => declaredGroups.contents->Array.filter(g => !(groups->Array.includes(g)))
|
|
178
|
+
}
|
package/src/types/Role.res.mjs
CHANGED
|
@@ -85,23 +85,60 @@ let groupSources = {
|
|
|
85
85
|
contents: []
|
|
86
86
|
};
|
|
87
87
|
|
|
88
|
+
let declaredGroups = {
|
|
89
|
+
contents: []
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
let listing = {
|
|
93
|
+
contents: undefined
|
|
94
|
+
};
|
|
95
|
+
|
|
88
96
|
function provideGroupsFrom(source) {
|
|
89
97
|
groupSources.contents = groupSources.contents.concat([source]);
|
|
90
98
|
}
|
|
91
99
|
|
|
92
100
|
function provideGroups(groups) {
|
|
101
|
+
declaredGroups.contents = declaredGroups.contents.concat(groups);
|
|
93
102
|
provideGroupsFrom(() => groups);
|
|
94
103
|
}
|
|
95
104
|
|
|
105
|
+
function provideListedGroups(source, groups) {
|
|
106
|
+
listing.contents = {
|
|
107
|
+
source: source,
|
|
108
|
+
groups: groups
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
96
112
|
function clearProvidedGroups() {
|
|
97
113
|
groupSources.contents = [];
|
|
114
|
+
declaredGroups.contents = [];
|
|
115
|
+
listing.contents = undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function _distinct(groups) {
|
|
119
|
+
return Array.from(new Set(groups).values());
|
|
98
120
|
}
|
|
99
121
|
|
|
100
122
|
function providedGroups() {
|
|
101
|
-
let
|
|
102
|
-
|
|
103
|
-
|
|
123
|
+
let match = listing.contents;
|
|
124
|
+
let match$1 = groupSources.contents;
|
|
125
|
+
if (match !== undefined) {
|
|
126
|
+
return Array.from(new Set(match.groups).values());
|
|
127
|
+
}
|
|
128
|
+
if (match$1.length === 0) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
let groups = match$1.flatMap(source => source()).concat(renamed().map(param => param[1]));
|
|
132
|
+
return Array.from(new Set(groups).values());
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function declaredButMissing() {
|
|
136
|
+
let match = listing.contents;
|
|
137
|
+
if (match === undefined) {
|
|
138
|
+
return [];
|
|
104
139
|
}
|
|
140
|
+
let groups = match.groups;
|
|
141
|
+
return declaredGroups.contents.filter(g => !groups.includes(g));
|
|
105
142
|
}
|
|
106
143
|
|
|
107
144
|
let envKey = "REVENTLESS_ROLE_GROUPS";
|
|
@@ -121,9 +158,14 @@ export {
|
|
|
121
158
|
groupOf,
|
|
122
159
|
adminGroup,
|
|
123
160
|
groupSources,
|
|
161
|
+
declaredGroups,
|
|
162
|
+
listing,
|
|
124
163
|
provideGroupsFrom,
|
|
125
164
|
provideGroups,
|
|
165
|
+
provideListedGroups,
|
|
126
166
|
clearProvidedGroups,
|
|
167
|
+
_distinct,
|
|
127
168
|
providedGroups,
|
|
169
|
+
declaredButMissing,
|
|
128
170
|
}
|
|
129
171
|
/* nameSchema Not a pure module */
|
|
@@ -4,9 +4,9 @@ Whether a platform can provide every role a plugin needs.
|
|
|
4
4
|
A rule naming a role no group stands for refuses everybody it was written for, and
|
|
5
5
|
nothing says so until someone is refused on a deployed stack. So every role a
|
|
6
6
|
component's rule names, and every elevated role, must map to a group the platform
|
|
7
|
-
provides: the
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
provides: the groups its identity provider reports where the platform can ask it,
|
|
8
|
+
else the administrator group, the accounts manifest, the role mapping and any
|
|
9
|
+
groups a root declares (see `Role.providedGroups`).
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
type need = {
|
|
@@ -55,12 +55,37 @@ let describe = (n: need): string => {
|
|
|
55
55
|
: `${n.plugin}: ${n.item} needs the role ${role} (mapped to the group ${n.group})`
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
/** What to do about it depends on where the provided groups came from: a
|
|
59
|
+
provider that was asked can only be given the group, while a manifest or a
|
|
60
|
+
declaration can be corrected. */
|
|
61
|
+
let remedy = (~provided: array<string>, ~listing: option<Role.listing>): string =>
|
|
62
|
+
switch listing {
|
|
63
|
+
| Some({source}) =>
|
|
64
|
+
let source = source->String.charAt(0)->String.toUpperCase ++ source->String.slice(~start=1)
|
|
65
|
+
` ${source} has the groups: ${provided->Array.join(", ")}.\n` ++
|
|
66
|
+
` Create the group there (provision-accounts creates the groups of the accounts ` ++ `manifest), or map the role to a group it has (Platform.roleGroups).`
|
|
67
|
+
| None =>
|
|
68
|
+
` The platform provides the groups: ${provided->Array.join(", ")}.\n` ++
|
|
69
|
+
` Add an account in that group to the accounts manifest (.reventless/users.yaml), ` ++
|
|
70
|
+
`map the role to a group that exists (Platform.roleGroups), or declare the groups ` ++ `of a user pool this deployment did not create (Platform.providedGroups).`
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let unmetMessage = (
|
|
74
|
+
missing: array<need>,
|
|
75
|
+
~provided: array<string>,
|
|
76
|
+
~listing: option<Role.listing>=?,
|
|
77
|
+
): string =>
|
|
59
78
|
`Roles this platform cannot provide — every caller would be refused:\n` ++
|
|
60
79
|
missing->Array.map(n => ` ${describe(n)}`)->Array.join("\n") ++
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
80
|
+
"\n" ++
|
|
81
|
+
remedy(~provided, ~listing)
|
|
82
|
+
|
|
83
|
+
/** A declared group the provider says it does not have: the declaration is wrong,
|
|
84
|
+
whether or not a plugin needs the group today. */
|
|
85
|
+
let contradictedMessage = (groups: array<string>, ~listing: Role.listing): string =>
|
|
86
|
+
`Groups declared with Platform.providedGroups that ${listing.source} does not have: ` ++
|
|
87
|
+
`${groups->Array.join(", ")}.\n` ++
|
|
88
|
+
` It has: ${listing.groups->Array.join(", ")}. Create them there or remove the declaration.`
|
|
64
89
|
|
|
65
90
|
/**
|
|
66
91
|
Refuse a plugin needing a role nobody provides, and a deployment electing a role
|
|
@@ -76,9 +101,15 @@ let check = (structure: Plugin.pluginStructure, ~plugin: string): unit =>
|
|
|
76
101
|
switch Role.providedGroups() {
|
|
77
102
|
| None => ()
|
|
78
103
|
| Some(provided) =>
|
|
104
|
+
let listing = Role.listing.contents
|
|
105
|
+
switch (listing, Role.declaredButMissing()) {
|
|
106
|
+
| (Some(listing), groups) if groups != [] =>
|
|
107
|
+
JsError.throwWithMessage(contradictedMessage(groups, ~listing))
|
|
108
|
+
| _ => ()
|
|
109
|
+
}
|
|
79
110
|
let elevated = OwnerScope.explicitElevatedRoles.contents->Option.getOr([])
|
|
80
111
|
switch unmet(needsOf(structure, ~plugin)->Array.concat(elevatedNeeds(elevated)), ~provided) {
|
|
81
112
|
| [] => ()
|
|
82
|
-
| missing => JsError.throwWithMessage(unmetMessage(missing, ~provided))
|
|
113
|
+
| missing => JsError.throwWithMessage(unmetMessage(missing, ~provided, ~listing?))
|
|
83
114
|
}
|
|
84
115
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
4
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
5
5
|
import * as Role$Reventless from "./Role.res.mjs";
|
|
6
|
+
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
6
7
|
import * as OwnerScope$Reventless from "./OwnerScope.res.mjs";
|
|
7
8
|
|
|
8
9
|
function needsOf(structure, plugin) {
|
|
@@ -45,8 +46,21 @@ function describe(n) {
|
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
function
|
|
49
|
-
|
|
49
|
+
function remedy(provided, listing) {
|
|
50
|
+
if (listing === undefined) {
|
|
51
|
+
return ` The platform provides the groups: ` + provided.join(", ") + `.\n Add an account in that group to the accounts manifest (.reventless/users.yaml), map the role to a group that exists (Platform.roleGroups), or declare the groups of a user pool this deployment did not create (Platform.providedGroups).`;
|
|
52
|
+
}
|
|
53
|
+
let source = listing.source;
|
|
54
|
+
let source$1 = source.charAt(0).toUpperCase() + source.slice(1);
|
|
55
|
+
return ` ` + source$1 + ` has the groups: ` + provided.join(", ") + `.\n Create the group there (provision-accounts creates the groups of the accounts manifest), or map the role to a group it has (Platform.roleGroups).`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function unmetMessage(missing, provided, listing) {
|
|
59
|
+
return `Roles this platform cannot provide — every caller would be refused:\n` + missing.map(n => ` ` + describe(n)).join("\n") + "\n" + remedy(provided, listing);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function contradictedMessage(groups, listing) {
|
|
63
|
+
return `Groups declared with Platform.providedGroups that ` + listing.source + ` does not have: ` + (groups.join(", ") + `.\n`) + (` It has: ` + listing.groups.join(", ") + `. Create them there or remove the declaration.`);
|
|
50
64
|
}
|
|
51
65
|
|
|
52
66
|
function check(structure, plugin) {
|
|
@@ -54,10 +68,15 @@ function check(structure, plugin) {
|
|
|
54
68
|
if (provided === undefined) {
|
|
55
69
|
return;
|
|
56
70
|
}
|
|
71
|
+
let listing = Role$Reventless.listing.contents;
|
|
72
|
+
let match = Role$Reventless.declaredButMissing();
|
|
73
|
+
if (listing !== undefined && Primitive_object.notequal(match, [])) {
|
|
74
|
+
Stdlib_JsError.throwWithMessage(contradictedMessage(match, listing));
|
|
75
|
+
}
|
|
57
76
|
let elevated = Stdlib_Option.getOr(OwnerScope$Reventless.explicitElevatedRoles.contents, []);
|
|
58
77
|
let missing = unmet(needsOf(structure, plugin).concat(elevatedNeeds(elevated)), provided);
|
|
59
78
|
if (missing.length !== 0) {
|
|
60
|
-
return Stdlib_JsError.throwWithMessage(unmetMessage(missing, provided));
|
|
79
|
+
return Stdlib_JsError.throwWithMessage(unmetMessage(missing, provided, listing));
|
|
61
80
|
}
|
|
62
81
|
}
|
|
63
82
|
|
|
@@ -66,7 +85,9 @@ export {
|
|
|
66
85
|
elevatedNeeds,
|
|
67
86
|
unmet,
|
|
68
87
|
describe,
|
|
88
|
+
remedy,
|
|
69
89
|
unmetMessage,
|
|
90
|
+
contradictedMessage,
|
|
70
91
|
check,
|
|
71
92
|
}
|
|
72
93
|
/* Role-Reventless Not a pure module */
|