@reventlessdev/reventless-spec 3.0.0-alpha.154 → 3.0.0-alpha.156
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/package.json +1 -1
- package/src/components/Aggregate.res +4 -1
- package/src/components/InboundTranslationSlice.res +4 -1
- package/src/components/Owner.res +30 -0
- package/src/components/Owner.res.mjs +36 -0
- package/src/components/Plugin.res +8 -0
- package/src/components/Plugin.res.mjs +5 -2
- package/src/components/ReadModel.res +4 -1
- package/src/components/StateChangeSlice.res +4 -1
- package/src/components/StateViewSlice.res +4 -1
- package/src/lifecycle/CheckLifecycleModel.res +3 -0
- package/src/lifecycle/CheckLifecycleModel.res.mjs +3 -0
- package/src/types/AccountsManifest.res +23 -1
- package/src/types/AccountsManifest.res.mjs +18 -0
- package/src/types/Authorization.res +37 -10
- package/src/types/Authorization.res.mjs +29 -15
- package/src/types/DeploymentProvenance.res +84 -0
- package/src/types/DeploymentProvenance.res.mjs +86 -0
- package/src/types/Environments.res +230 -0
- package/src/types/Environments.res.mjs +314 -0
- package/src/types/Message.res +21 -0
- package/src/types/Message.res.mjs +16 -1
- package/src/types/OwnerScope.res +113 -6
- package/src/types/OwnerScope.res.mjs +110 -3
- package/src/types/Role.res +143 -0
- package/src/types/Role.res.mjs +129 -0
- package/src/types/RoleCoverage.res +84 -0
- package/src/types/RoleCoverage.res.mjs +72 -0
- package/src/types/authorizationNamed.mjs +17 -0
- package/src/types/AdminGroup.res +0 -27
- package/src/types/AdminGroup.res.mjs +0 -9
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,29 @@
|
|
|
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.156 (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
|
+
|
|
21
|
+
# 3.0.0-alpha.155 (2026-10-01)
|
|
22
|
+
|
|
23
|
+
### Features
|
|
24
|
+
|
|
25
|
+
* **spec, aws, core, local:** an app declares its environments, and every deployment records its commit ([c7cc925](https://github.com/ReventlessDev/reventless-core/commit/c7cc92573e97742b681a9000cea0bdfd6e9a33fc))
|
|
26
|
+
* **spec, core, gwt, ppx:** a command acts only on what its caller owns ([2636f0e](https://github.com/ReventlessDev/reventless-core/commit/2636f0ee44797ee2c0b0c40a7107cf306113173f))
|
|
27
|
+
|
|
28
|
+
|
|
6
29
|
# 3.0.0-alpha.154 (2026-09-29)
|
|
7
30
|
|
|
8
31
|
**Note:** Version bump only for package @reventlessdev/reventless-spec
|
package/package.json
CHANGED
|
@@ -55,12 +55,15 @@ module type Spec = {
|
|
|
55
55
|
the npm specifier for runtime dynamic imports. */
|
|
56
56
|
let moduleUrl: string
|
|
57
57
|
|
|
58
|
+
/** The roles this spec's rules name: its plugin's `Roles.t`, which the PPX
|
|
59
|
+
supplies; `Role.name` where the plugin declares none. */
|
|
60
|
+
type role
|
|
58
61
|
/** Authorization rule evaluated at the GraphQL resolver entry before any
|
|
59
62
|
command is dispatched. Auto-injected by `@@reventless.spec` and on
|
|
60
63
|
structurally-detected inline spec modules — defaults to
|
|
61
64
|
`AllowAuthenticated`; override at the file/module level with
|
|
62
65
|
`@@reventless.authorize(<rule>)`. */
|
|
63
|
-
let commandAuthorization: command => Authorization.
|
|
66
|
+
let commandAuthorization: command => Authorization.rule<role>
|
|
64
67
|
|
|
65
68
|
/** The lifecycle enum this component's commands move a row through — the
|
|
66
69
|
linked view's own, e.g. `type lifecycleState = Customers.accountStatus`.
|
|
@@ -56,11 +56,14 @@ module type Spec = {
|
|
|
56
56
|
Auto-injected by `@@reventless.spec` defaulting to `None` — set it to name the box. */
|
|
57
57
|
let externalSystem: option<string>
|
|
58
58
|
|
|
59
|
+
/** The roles this spec's rules name: its plugin's `Roles.t`, which the PPX
|
|
60
|
+
supplies; `Role.name` where the plugin declares none. */
|
|
61
|
+
type role
|
|
59
62
|
/** Authorization rule evaluated at the GraphQL resolver entry before any
|
|
60
63
|
external input is translated. Auto-injected by `@@reventless.spec` and
|
|
61
64
|
on structurally-detected inline spec modules — defaults to
|
|
62
65
|
`AllowAuthenticated`. */
|
|
63
|
-
let commandAuthorization: command => Authorization.
|
|
66
|
+
let commandAuthorization: command => Authorization.rule<role>
|
|
64
67
|
|
|
65
68
|
/** The lifecycle enum this component's commands move a row through — the
|
|
66
69
|
linked view's own, e.g. `type lifecycleState = Customers.accountStatus`.
|
package/src/components/Owner.res
CHANGED
|
@@ -138,3 +138,33 @@ let variantFieldNames = (schema: S.t<unknown>, ~variant: string): array<string>
|
|
|
138
138
|
| _ => []
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
The owner fields of every constructor that declares one, keyed by its tag.
|
|
144
|
+
|
|
145
|
+
Read off a slice's consumed events, this says which recorded facts name the
|
|
146
|
+
owner of the partition a command acts on. An empty dict means the union records
|
|
147
|
+
no owner, and the handler then has nothing to enforce — so, as with every reader
|
|
148
|
+
here, it must be handed the real schema: a permissive stand-in answers empty.
|
|
149
|
+
*/
|
|
150
|
+
let fieldNamesByVariant = (schema: S.t<unknown>): dict<array<string>> => {
|
|
151
|
+
let byVariant = Dict.make()
|
|
152
|
+
let visit = (properties: dict<S.t<unknown>>) =>
|
|
153
|
+
switch (properties->Dict.get("TAG"), fieldNamesOfProperties(properties)) {
|
|
154
|
+
| (_, []) => ()
|
|
155
|
+
| (Some(String({const: ?Some(name)})), fields) => byVariant->Dict.set(name, fields)
|
|
156
|
+
| _ => ()
|
|
157
|
+
}
|
|
158
|
+
switch schema {
|
|
159
|
+
| AnyOf({anyOf}) =>
|
|
160
|
+
anyOf->Array.forEach(v =>
|
|
161
|
+
switch v {
|
|
162
|
+
| Object({properties}) => visit(properties)
|
|
163
|
+
| _ => ()
|
|
164
|
+
}
|
|
165
|
+
)
|
|
166
|
+
| Object({properties}) => visit(properties)
|
|
167
|
+
| _ => ()
|
|
168
|
+
}
|
|
169
|
+
byVariant
|
|
170
|
+
}
|
|
@@ -90,6 +90,41 @@ function variantFieldNames(schema, variant) {
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
function fieldNamesByVariant(schema) {
|
|
94
|
+
let byVariant = {};
|
|
95
|
+
let visit = properties => {
|
|
96
|
+
let match = properties["TAG"];
|
|
97
|
+
let match$1 = fieldNamesOfProperties(properties);
|
|
98
|
+
if (match$1.length === 0) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (match === undefined) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (match.type !== "string") {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
let name = match.const;
|
|
108
|
+
if (name !== undefined) {
|
|
109
|
+
byVariant[name] = match$1;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
switch (schema.type) {
|
|
114
|
+
case "object" :
|
|
115
|
+
visit(schema.properties);
|
|
116
|
+
break;
|
|
117
|
+
case "anyOf" :
|
|
118
|
+
schema.anyOf.forEach(v => {
|
|
119
|
+
if (v.type === "object") {
|
|
120
|
+
return visit(v.properties);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
return byVariant;
|
|
126
|
+
}
|
|
127
|
+
|
|
93
128
|
export {
|
|
94
129
|
ownerId,
|
|
95
130
|
mark,
|
|
@@ -100,5 +135,6 @@ export {
|
|
|
100
135
|
fieldNamesOfProperties,
|
|
101
136
|
fieldNames,
|
|
102
137
|
variantFieldNames,
|
|
138
|
+
fieldNamesByVariant,
|
|
103
139
|
}
|
|
104
140
|
/* ownerId Not a pure module */
|
|
@@ -194,6 +194,10 @@ type commandDef = {
|
|
|
194
194
|
/** Access keys — any one of them — a caller needs to be *offered* this command.
|
|
195
195
|
A hint derived from the server's rule, never the refusal itself. */
|
|
196
196
|
requiredAccess: option<array<string>>,
|
|
197
|
+
/** The roles `requiredAccess` was mapped from, as the rule names them. Absent
|
|
198
|
+
where the rule names none; read by the deploy check that every role maps to
|
|
199
|
+
a group the platform provides. */
|
|
200
|
+
requiredRoles?: array<string>,
|
|
197
201
|
/** The `@owner` command field the server stamps with the caller's identity; a
|
|
198
202
|
client omits it from a form, since whatever it collects is discarded. */
|
|
199
203
|
ownerField: option<string>,
|
|
@@ -249,6 +253,8 @@ type queryableDef = {
|
|
|
249
253
|
/** Access keys — any one of them — a caller needs to be *offered* this view. A
|
|
250
254
|
denied read comes back empty rather than erroring, hence the hint. */
|
|
251
255
|
requiredAccess: option<array<string>>,
|
|
256
|
+
/** The roles `requiredAccess` was mapped from; see `commandDef`. */
|
|
257
|
+
requiredRoles?: array<string>,
|
|
252
258
|
}
|
|
253
259
|
|
|
254
260
|
/** One emitted event of a write side. `name` is the variant name, `schema` its
|
|
@@ -326,6 +332,8 @@ type inboundTranslationSliceDef = {
|
|
|
326
332
|
externalSystem: option<string>,
|
|
327
333
|
/** Chapter grouping band — see `queryableDef.chapter`. */
|
|
328
334
|
chapter: option<string>,
|
|
335
|
+
/** The roles any of its commands' rules name; see `commandDef`. */
|
|
336
|
+
requiredRoles?: array<string>,
|
|
329
337
|
}
|
|
330
338
|
|
|
331
339
|
/** A published event of an extension point and the internal events producing it.
|
|
@@ -102,6 +102,7 @@ let commandDefSchema = Sury.$schema(s => ({
|
|
|
102
102
|
allowedStatesSource: s.m(Sury.$option(Sury.string)),
|
|
103
103
|
apiExposed: s.m(Sury.$option(Sury.boolean)),
|
|
104
104
|
requiredAccess: s.m(Sury.$option(Sury.array(Sury.string))),
|
|
105
|
+
requiredRoles: s.m(Sury.$option(Sury.array(Sury.string))),
|
|
105
106
|
ownerField: s.m(Sury.$option(Sury.string))
|
|
106
107
|
}));
|
|
107
108
|
|
|
@@ -124,7 +125,8 @@ let queryableDefSchema = Sury.$schema(s => ({
|
|
|
124
125
|
singleQueryField: s.m(Sury.$option(Sury.string)),
|
|
125
126
|
idField: s.m(Sury.$option(Sury.string)),
|
|
126
127
|
idFieldSource: s.m(Sury.$option(Sury.string)),
|
|
127
|
-
requiredAccess: s.m(Sury.$option(Sury.array(Sury.string)))
|
|
128
|
+
requiredAccess: s.m(Sury.$option(Sury.array(Sury.string))),
|
|
129
|
+
requiredRoles: s.m(Sury.$option(Sury.array(Sury.string)))
|
|
128
130
|
}));
|
|
129
131
|
|
|
130
132
|
let eventDefSchema = Sury.$schema(s => ({
|
|
@@ -174,7 +176,8 @@ let inboundTranslationSliceDefSchema = Sury.$schema(s => ({
|
|
|
174
176
|
commandTypes: s.m(Sury.array(Sury.string)),
|
|
175
177
|
targetName: s.m(Sury.string),
|
|
176
178
|
externalSystem: s.m(Sury.$option(Sury.string)),
|
|
177
|
-
chapter: s.m(Sury.$option(Sury.string))
|
|
179
|
+
chapter: s.m(Sury.$option(Sury.string)),
|
|
180
|
+
requiredRoles: s.m(Sury.$option(Sury.array(Sury.string)))
|
|
178
181
|
}));
|
|
179
182
|
|
|
180
183
|
let publishedEventDefSchema = Sury.$schema(s => ({
|
|
@@ -190,12 +190,15 @@ module type Spec = {
|
|
|
190
190
|
/** Optional composite-key configuration. `None` for single-key tables. */
|
|
191
191
|
let subIdConfig: option<subIdConfig<state>>
|
|
192
192
|
|
|
193
|
+
/** The roles this spec's rules name: its plugin's `Roles.t`, which the PPX
|
|
194
|
+
supplies; `Role.name` where the plugin declares none. */
|
|
195
|
+
type role
|
|
193
196
|
/** Authorization rule evaluated at the GraphQL resolver entry before any
|
|
194
197
|
query is resolved. Auto-injected by `@@reventless.spec` and on
|
|
195
198
|
structurally-detected inline spec modules — defaults to
|
|
196
199
|
`AllowAuthenticated`; override at the file/module level with
|
|
197
200
|
`@@reventless.authorize(<rule>)`. */
|
|
198
|
-
let authorization: Authorization.
|
|
201
|
+
let authorization: Authorization.rule<role>
|
|
199
202
|
|
|
200
203
|
/** AutoUI visibility hint. Auto-injected by `@@reventless.spec` and on
|
|
201
204
|
structurally-detected inline spec modules — defaults to
|
|
@@ -75,12 +75,15 @@ module type Spec = {
|
|
|
75
75
|
/** Schema for the command type — used to extract DCB tags for the conditional read. */
|
|
76
76
|
let commandSchema: S.t<command>
|
|
77
77
|
|
|
78
|
+
/** The roles this spec's rules name: its plugin's `Roles.t`, which the PPX
|
|
79
|
+
supplies; `Role.name` where the plugin declares none. */
|
|
80
|
+
type role
|
|
78
81
|
/** Authorization rule evaluated at the GraphQL resolver entry before any
|
|
79
82
|
command is dispatched. Auto-injected by `@@reventless.spec` and on
|
|
80
83
|
structurally-detected inline spec modules — defaults to
|
|
81
84
|
`AllowAuthenticated`; override at the file/module level with
|
|
82
85
|
`@@reventless.authorize(<rule>)`. */
|
|
83
|
-
let commandAuthorization: command => Authorization.
|
|
86
|
+
let commandAuthorization: command => Authorization.rule<role>
|
|
84
87
|
|
|
85
88
|
/** The lifecycle enum this component's commands move a row through — the
|
|
86
89
|
linked view's own, e.g. `type lifecycleState = Customers.accountStatus`.
|
|
@@ -73,11 +73,14 @@ module type Spec = {
|
|
|
73
73
|
/** Optional composite-key configuration. `None` for single-key tables. */
|
|
74
74
|
let subIdConfig: option<ReadModel.subIdConfig<state>>
|
|
75
75
|
|
|
76
|
+
/** The roles this spec's rules name: its plugin's `Roles.t`, which the PPX
|
|
77
|
+
supplies; `Role.name` where the plugin declares none. */
|
|
78
|
+
type role
|
|
76
79
|
/** Authorization rule evaluated at the GraphQL resolver entry before any
|
|
77
80
|
query is resolved. Auto-injected by `@@reventless.spec` and on
|
|
78
81
|
structurally-detected inline spec modules — defaults to
|
|
79
82
|
`AllowAuthenticated`. */
|
|
80
|
-
let authorization: Authorization.
|
|
83
|
+
let authorization: Authorization.rule<role>
|
|
81
84
|
|
|
82
85
|
/** AutoUI visibility hint. Auto-injected by `@@reventless.spec` and on
|
|
83
86
|
structurally-detected inline spec modules — defaults to
|
|
@@ -687,6 +687,9 @@ let observe = (
|
|
|
687
687
|
|
|
688
688
|
scenarios->Array.filterMap(s =>
|
|
689
689
|
switch s.whenElements->Array.get(0) {
|
|
690
|
+
// `thenRefused`: the caller did not own the row, which says nothing about
|
|
691
|
+
// the states the command is legal in.
|
|
692
|
+
| Some(_) if s.thenKind == "forbidden" => None
|
|
690
693
|
| Some(command) if s.whenKind == "command" =>
|
|
691
694
|
let idField = idFieldFor(command.name)
|
|
692
695
|
let idValue = idField->Option.flatMap(field => command.values->valueOf(~field))
|
|
@@ -8,7 +8,7 @@ reads it and writes back into it.
|
|
|
8
8
|
|
|
9
9
|
Here rather than in either platform package because a manifest that works on one
|
|
10
10
|
platform and silently fails on the other is the whole cost being avoided. It sits
|
|
11
|
-
beside [Identity] and [
|
|
11
|
+
beside [Identity] and [Role] for the same reason those do: the vocabulary
|
|
12
12
|
the two platforms share about who is signed in.
|
|
13
13
|
|
|
14
14
|
🚨 **The password field is a bootstrap credential, not a stored secret.** The file
|
|
@@ -237,6 +237,28 @@ let locate = (~given: option<string>=?, ()): result<located, string> =>
|
|
|
237
237
|
}
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
+
/**
|
|
241
|
+
The groups the accounts here belong to: the manifest's, else the template's, else
|
|
242
|
+
none. What a platform provides beyond its administrator group, for the check that
|
|
243
|
+
every role a plugin needs has a group.
|
|
244
|
+
|
|
245
|
+
Read, never seeded — this runs in a deploy or a start, which must not write the
|
|
246
|
+
file `locate` would. The template counts because it is the cast a fresh clone
|
|
247
|
+
provisions; a deploy from CI has no manifest and the same accounts to come.
|
|
248
|
+
*/
|
|
249
|
+
let declaredGroups = (): array<string> => {
|
|
250
|
+
let template = NodePath.join([NodeProcess.cwd(), templateName])
|
|
251
|
+
let file = NodeFs.existsSync(defaultPath())
|
|
252
|
+
? Some(defaultPath())
|
|
253
|
+
: NodeFs.existsSync(template)
|
|
254
|
+
? Some(template)
|
|
255
|
+
: None
|
|
256
|
+
switch file->Option.map(parseFile) {
|
|
257
|
+
| Some(Ok(entries)) => entries->Array.flatMap(e => e.groups)
|
|
258
|
+
| Some(Error(_)) | None => []
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
240
262
|
// ── Preparing ────────────────────────────────────────────────────────────────
|
|
241
263
|
|
|
242
264
|
/** One account after preparation: what the file now declares, and whether this
|
|
@@ -246,6 +246,23 @@ function locate(given, param) {
|
|
|
246
246
|
}
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
+
function declaredGroups() {
|
|
250
|
+
let template = Nodepath.join(process.cwd(), templateName);
|
|
251
|
+
let file = Nodefs.existsSync(defaultPath()) ? defaultPath() : (
|
|
252
|
+
Nodefs.existsSync(template) ? template : undefined
|
|
253
|
+
);
|
|
254
|
+
let match = Stdlib_Option.map(file, parseFile);
|
|
255
|
+
if (match !== undefined) {
|
|
256
|
+
if (match.TAG === "Ok") {
|
|
257
|
+
return match._0.flatMap(e => e.groups);
|
|
258
|
+
} else {
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
261
|
+
} else {
|
|
262
|
+
return [];
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
249
266
|
function prepare(file) {
|
|
250
267
|
let e = parseFile(file);
|
|
251
268
|
if (e.TAG !== "Ok") {
|
|
@@ -306,6 +323,7 @@ export {
|
|
|
306
323
|
templateName,
|
|
307
324
|
pathOf,
|
|
308
325
|
locate,
|
|
326
|
+
declaredGroups,
|
|
309
327
|
prepare,
|
|
310
328
|
}
|
|
311
329
|
/* entrySchema Not a pure module */
|
|
@@ -1,22 +1,49 @@
|
|
|
1
1
|
// Provider-agnostic authorization rules. Evaluated against the
|
|
2
2
|
// `Identity.t` resolved by an `Auth_Adapter.Provider.authenticate` call.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
4
|
+
// A spec writes its rules over its plugin's own roles, `rule<Roles.t>`, so a
|
|
5
|
+
// misspelled role does not compile. The framework reads them as `permission`,
|
|
6
|
+
// the same rule over role names, which `Role.groupOf` maps to the group that
|
|
7
|
+
// stands for each role in this deployment.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
| AllowGroups(array<string>)
|
|
9
|
+
type rule<'role> =
|
|
10
|
+
| AllowRoles(array<'role>)
|
|
12
11
|
| AllowAuthenticated
|
|
13
12
|
| AllowAnonymous
|
|
14
13
|
| DenyAll
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
/** A rule over role names: what the framework compares, maps and publishes. */
|
|
16
|
+
type permission = rule<Role.name>
|
|
17
|
+
|
|
18
|
+
/** A spec's rule as the framework reads it. Each role must be a case without a
|
|
19
|
+
payload, which is its name at run time; anything else is refused. */
|
|
20
|
+
@module("./authorizationNamed.mjs")
|
|
21
|
+
external named: rule<'role> => permission = "named"
|
|
22
|
+
|
|
23
|
+
/** The roles a rule names, before mapping. */
|
|
24
|
+
let rolesOf = (rule: permission): array<Role.name> =>
|
|
25
|
+
switch rule {
|
|
26
|
+
| AllowRoles(roles) => roles
|
|
27
|
+
| AllowAuthenticated | AllowAnonymous | DenyAll => []
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The groups a rule admits, mapped. */
|
|
31
|
+
let groupsOf = (rule: permission): array<string> => rule->rolesOf->Array.map(Role.groupOf)
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
Whether a rule admits a caller, given whether they are signed in and which roles
|
|
35
|
+
they hold. The one decision both `isAllowed` and a GWT scenario make, so the two
|
|
36
|
+
cannot read a rule differently.
|
|
37
|
+
*/
|
|
38
|
+
let admits = (rule: permission, ~signedIn: bool, ~holds: Role.name => bool): bool =>
|
|
17
39
|
switch rule {
|
|
18
40
|
| DenyAll => false
|
|
19
41
|
| AllowAnonymous => true
|
|
20
|
-
| AllowAuthenticated =>
|
|
21
|
-
|
|
|
42
|
+
| AllowAuthenticated => signedIn
|
|
43
|
+
| AllowRoles(roles) => roles->Array.some(holds)
|
|
22
44
|
}
|
|
45
|
+
|
|
46
|
+
let isAllowed = (rule: permission, identity: Identity.t): bool =>
|
|
47
|
+
admits(rule, ~signedIn=identity.userId !== "anonymous", ~holds=role =>
|
|
48
|
+
identity.groups->Array.includes(Role.groupOf(role))
|
|
49
|
+
)
|
|
@@ -1,24 +1,31 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
-
import * as
|
|
3
|
+
import * as Role$Reventless from "./Role.res.mjs";
|
|
4
|
+
import * as AuthorizationNamedMjs from "./authorizationNamed.mjs";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
_0: s.m(Sury.array(Sury.string))
|
|
9
|
-
})),
|
|
10
|
-
Sury.literal("AllowAuthenticated"),
|
|
11
|
-
Sury.literal("AllowAnonymous"),
|
|
12
|
-
Sury.literal("DenyAll")
|
|
13
|
-
]);
|
|
6
|
+
function named(prim) {
|
|
7
|
+
return AuthorizationNamedMjs.named(prim);
|
|
8
|
+
}
|
|
14
9
|
|
|
15
|
-
function
|
|
10
|
+
function rolesOf(rule) {
|
|
11
|
+
if (typeof rule !== "object") {
|
|
12
|
+
return [];
|
|
13
|
+
} else {
|
|
14
|
+
return rule._0;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function groupsOf(rule) {
|
|
19
|
+
return rolesOf(rule).map(Role$Reventless.groupOf);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function admits(rule, signedIn, holds) {
|
|
16
23
|
if (typeof rule === "object") {
|
|
17
|
-
return rule._0.some(
|
|
24
|
+
return rule._0.some(holds);
|
|
18
25
|
}
|
|
19
26
|
switch (rule) {
|
|
20
27
|
case "AllowAuthenticated" :
|
|
21
|
-
return
|
|
28
|
+
return signedIn;
|
|
22
29
|
case "AllowAnonymous" :
|
|
23
30
|
return true;
|
|
24
31
|
case "DenyAll" :
|
|
@@ -26,8 +33,15 @@ function isAllowed(rule, identity) {
|
|
|
26
33
|
}
|
|
27
34
|
}
|
|
28
35
|
|
|
36
|
+
function isAllowed(rule, identity) {
|
|
37
|
+
return admits(rule, identity.userId !== "anonymous", role => identity.groups.includes(Role$Reventless.groupOf(role)));
|
|
38
|
+
}
|
|
39
|
+
|
|
29
40
|
export {
|
|
30
|
-
|
|
41
|
+
named,
|
|
42
|
+
rolesOf,
|
|
43
|
+
groupsOf,
|
|
44
|
+
admits,
|
|
31
45
|
isAllowed,
|
|
32
46
|
}
|
|
33
|
-
/*
|
|
47
|
+
/* Role-Reventless Not a pure module */
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/***
|
|
2
|
+
Where a deployment came from: the commit it was built from, whether the tree had
|
|
3
|
+
uncommitted changes, and the environment it was made for.
|
|
4
|
+
|
|
5
|
+
Read by the deploy while it runs, and recorded with the deployment, so a reader
|
|
6
|
+
can say which commit runs in which environment without asking whoever deployed.
|
|
7
|
+
A deployment from CI knows its commit from the CI's variables; one from a laptop
|
|
8
|
+
asks git. Neither gives up a timestamp in place of a commit: a timestamp is not
|
|
9
|
+
an answer to *which commit*, and a reader cannot tell it from one.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
type t = {
|
|
13
|
+
commit: option<string>,
|
|
14
|
+
/** True only when the commit came from git and the working tree had changes:
|
|
15
|
+
what was deployed is not what the commit says. */
|
|
16
|
+
dirty: bool,
|
|
17
|
+
environment: option<string>,
|
|
18
|
+
purpose: option<string>,
|
|
19
|
+
tag: option<string>,
|
|
20
|
+
pullRequest: option<int>,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** What git can say about the checkout the deploy runs in. */
|
|
24
|
+
type git = {
|
|
25
|
+
head: unit => option<string>,
|
|
26
|
+
dirty: unit => bool,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let nonEmpty = (value: option<string>): option<string> =>
|
|
30
|
+
value->Option.flatMap(v => v->String.trim == "" ? None : Some(v->String.trim))
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
The provenance of a deployment from the process's variables, asking git only when
|
|
34
|
+
no CI variable names the commit.
|
|
35
|
+
|
|
36
|
+
`REVENTLESS_COMMIT` comes first because on a pull request `GITHUB_SHA` is the merge
|
|
37
|
+
commit GitHub made, not the head the review environment runs.
|
|
38
|
+
*/
|
|
39
|
+
let fromEnv = (env: dict<string>, ~git: git): t => {
|
|
40
|
+
let get = name => env->Dict.get(name)->nonEmpty
|
|
41
|
+
let fromCi =
|
|
42
|
+
get("REVENTLESS_COMMIT")
|
|
43
|
+
->Option.orElse(get("GITHUB_SHA"))
|
|
44
|
+
->Option.orElse(get("CI_COMMIT_SHA"))
|
|
45
|
+
let (commit, dirty) = switch fromCi {
|
|
46
|
+
| Some(_) => (fromCi, false)
|
|
47
|
+
| None =>
|
|
48
|
+
switch git.head() {
|
|
49
|
+
| Some(_) as head => (head, git.dirty())
|
|
50
|
+
| None => (None, false)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
{
|
|
54
|
+
commit,
|
|
55
|
+
dirty,
|
|
56
|
+
environment: get("REVENTLESS_ENVIRONMENT"),
|
|
57
|
+
purpose: get("REVENTLESS_ENVIRONMENT_PURPOSE"),
|
|
58
|
+
tag: get("GITHUB_REF_TYPE") == Some("tag") ? get("GITHUB_REF_NAME") : None,
|
|
59
|
+
pullRequest: get("REVENTLESS_PULL_REQUEST")->Option.flatMap(n => Int.fromString(n)),
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let run = (~cwd: string, args: array<string>): option<string> =>
|
|
64
|
+
try {
|
|
65
|
+
Some(
|
|
66
|
+
NodeChildProcess.execFileSync(
|
|
67
|
+
"git",
|
|
68
|
+
args,
|
|
69
|
+
{cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"]},
|
|
70
|
+
)->String.trim,
|
|
71
|
+
)
|
|
72
|
+
} catch {
|
|
73
|
+
| _ => None
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Git in `cwd`. A directory that is not a checkout, or a machine with no git,
|
|
77
|
+
answers *no commit* and *not dirty*. */
|
|
78
|
+
let gitAt = (~cwd: string): git => {
|
|
79
|
+
head: () => run(~cwd, ["rev-parse", "HEAD"])->nonEmpty,
|
|
80
|
+
dirty: () => run(~cwd, ["status", "--porcelain"])->nonEmpty->Option.isSome,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The provenance of the deployment this process is making, in its working directory. */
|
|
84
|
+
let current = (): t => fromEnv(NodeProcess.env, ~git=gitAt(~cwd=NodeProcess.cwd()))
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
6
|
+
import * as Nodechild_process from "node:child_process";
|
|
7
|
+
|
|
8
|
+
function nonEmpty(value) {
|
|
9
|
+
return Stdlib_Option.flatMap(value, v => {
|
|
10
|
+
if (v.trim() === "") {
|
|
11
|
+
return;
|
|
12
|
+
} else {
|
|
13
|
+
return v.trim();
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function fromEnv(env, git) {
|
|
19
|
+
let fromCi = Stdlib_Option.orElse(Stdlib_Option.orElse(nonEmpty(env["REVENTLESS_COMMIT"]), nonEmpty(env["GITHUB_SHA"])), nonEmpty(env["CI_COMMIT_SHA"]));
|
|
20
|
+
let match;
|
|
21
|
+
if (fromCi !== undefined) {
|
|
22
|
+
match = [
|
|
23
|
+
fromCi,
|
|
24
|
+
false
|
|
25
|
+
];
|
|
26
|
+
} else {
|
|
27
|
+
let head = git.head();
|
|
28
|
+
match = head !== undefined ? [
|
|
29
|
+
head,
|
|
30
|
+
git.dirty()
|
|
31
|
+
] : [
|
|
32
|
+
undefined,
|
|
33
|
+
false
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
commit: match[0],
|
|
38
|
+
dirty: match[1],
|
|
39
|
+
environment: nonEmpty(env["REVENTLESS_ENVIRONMENT"]),
|
|
40
|
+
purpose: nonEmpty(env["REVENTLESS_ENVIRONMENT_PURPOSE"]),
|
|
41
|
+
tag: Primitive_object.equal(nonEmpty(env["GITHUB_REF_TYPE"]), "tag") ? nonEmpty(env["GITHUB_REF_NAME"]) : undefined,
|
|
42
|
+
pullRequest: Stdlib_Option.flatMap(nonEmpty(env["REVENTLESS_PULL_REQUEST"]), n => Stdlib_Int.fromString(n, undefined))
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function run(cwd, args) {
|
|
47
|
+
try {
|
|
48
|
+
return Nodechild_process.execFileSync("git", args, {
|
|
49
|
+
cwd: cwd,
|
|
50
|
+
encoding: "utf8",
|
|
51
|
+
stdio: [
|
|
52
|
+
"ignore",
|
|
53
|
+
"pipe",
|
|
54
|
+
"ignore"
|
|
55
|
+
]
|
|
56
|
+
}).trim();
|
|
57
|
+
} catch (exn) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function gitAt(cwd) {
|
|
63
|
+
return {
|
|
64
|
+
head: () => nonEmpty(run(cwd, [
|
|
65
|
+
"rev-parse",
|
|
66
|
+
"HEAD"
|
|
67
|
+
])),
|
|
68
|
+
dirty: () => Stdlib_Option.isSome(nonEmpty(run(cwd, [
|
|
69
|
+
"status",
|
|
70
|
+
"--porcelain"
|
|
71
|
+
])))
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function current() {
|
|
76
|
+
return fromEnv(process.env, gitAt(process.cwd()));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export {
|
|
80
|
+
nonEmpty,
|
|
81
|
+
fromEnv,
|
|
82
|
+
run,
|
|
83
|
+
gitAt,
|
|
84
|
+
current,
|
|
85
|
+
}
|
|
86
|
+
/* node:child_process Not a pure module */
|