mxrb 0.1.1 → 0.1.2
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.
- checksums.yaml +4 -4
- data/README.de-DE.md +4 -0
- data/README.md +4 -0
- data/README.pt-BR.md +4 -0
- data/bin/mxrb +35 -2
- data/docs/de-DE/oql-sql.md +18 -0
- data/docs/de-DE/scaffolds.md +23 -0
- data/docs/en-US/oql-sql.md +18 -0
- data/docs/en-US/scaffolds.md +22 -0
- data/docs/pt-BR/oql-sql.md +18 -0
- data/docs/pt-BR/scaffolds.md +22 -0
- data/lib/mxrb/compare.rb +7 -0
- data/lib/mxrb/dsl/builder.rb +81 -0
- data/lib/mxrb/exporter.rb +6 -0
- data/lib/mxrb/initializer.rb +1 -0
- data/lib/mxrb/runtime/clipboard.rb +62 -0
- data/lib/mxrb/runtime/database_workspace.rb +28 -0
- data/lib/mxrb/scaffold/cli.rb +20 -1
- data/lib/mxrb/scaffold/generator.rb +125 -3
- data/lib/mxrb/scaffold/help.rb +16 -2
- data/lib/mxrb/scaffold/recipes.rb +27 -0
- data/lib/mxrb/scaffold/templates.rb +11 -0
- data/lib/mxrb/scaffold/transaction.rb +2 -2
- data/lib/mxrb/version.rb +1 -1
- data/lib/mxrb/writer.rb +15 -1
- data/lib/mxrb.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 571786ae22b838daaa4700d7a99816653046f645a5d4f49ee29d155f4a05fa99
|
|
4
|
+
data.tar.gz: 4366822b4731a3604eb2be09ee1f4408fe93108229486b6b45528772022c709f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 97f38f00ed166946e0ea54e30c940631c2dfd91d919a20c9b701e8df25acd0cb653a68ae6f0bee8eb0742333ec1c921fb523882200be48def5a3a827bfe82e99
|
|
7
|
+
data.tar.gz: 648bd2c542071702fb6e9ac44559bfac32dd6128c0bb666f24c0f058992db702a69be6815db9bbd9093d5e92928c836d61e4cb84e28235d2dbf4742a1acac3f3
|
data/README.de-DE.md
CHANGED
|
@@ -19,6 +19,7 @@ und Testen von Mendix-Projekten (`.mpr`) ohne MDL oder `mxcli`.
|
|
|
19
19
|
- Lint und ausführbare Modellbewertungen;
|
|
20
20
|
- Erkennung von nativem OQL und logische SQL-Ansicht nur bei vorhandenem OQL;
|
|
21
21
|
- isolierte, durch das Runtime synchronisierte PostgreSQL für direkte SQL-Abfragen;
|
|
22
|
+
- Demo-Benutzer mit lokalen `.env`-Geheimnissen und sichere Runtime-Zugangsdaten;
|
|
22
23
|
- Analyse von PostgreSQL-/SQL-Server-Plänen und kumulativem PostgreSQL-Workload;
|
|
23
24
|
- funktionale Microflow-Tests lokal oder in Docker.
|
|
24
25
|
- Suche und Installation wiederverwendbarer Ruby-Module mit SHA-256-Lock.
|
|
@@ -35,6 +36,7 @@ wird kompatibles Java benötigt; Docker stellt JDK und Runtime bereit.
|
|
|
35
36
|
|
|
36
37
|
```sh
|
|
37
38
|
bundle install
|
|
39
|
+
bundle exec mxrb --version
|
|
38
40
|
bundle exec mxrb validate App.mpr
|
|
39
41
|
bundle exec mxrb export App.mpr app-ruby
|
|
40
42
|
bundle exec mxrb compare original.mpr rebuilt.mpr
|
|
@@ -45,7 +47,9 @@ bundle exec mxrb cache warm App.mpr
|
|
|
45
47
|
bundle exec mxrb cache clear App.mpr
|
|
46
48
|
bundle exec mxrb oql App.mpr --dialect postgresql
|
|
47
49
|
bundle exec mxrb db up App.mpr
|
|
50
|
+
bundle exec mxrb db credentials App.mpr --copy
|
|
48
51
|
bundle exec mxrb db sql App.mpr 'SELECT * FROM "sales$order" LIMIT 20'
|
|
52
|
+
bundle exec mxrb demo-user new manager --entity System.User --role User
|
|
49
53
|
```
|
|
50
54
|
|
|
51
55
|
Die MPR speichert das Modell, nicht die Anwendungsdaten. `db up` baut das
|
data/README.md
CHANGED
|
@@ -23,6 +23,7 @@ remain optional external gates for compatibility and functional execution.
|
|
|
23
23
|
- inventory design tokens, check contrast and preview atomic literal migration;
|
|
24
24
|
- discover native OQL and render safe logical SQL views only when OQL exists;
|
|
25
25
|
- materialize an isolated, Runtime-synchronized PostgreSQL for direct SQL inspection;
|
|
26
|
+
- scaffold demo users with local `.env` secrets and safely inspect Runtime credentials;
|
|
26
27
|
- analyze PostgreSQL/SQL Server plans and rank cumulative PostgreSQL workload pressure;
|
|
27
28
|
- move standalone units between folders in the same module with a preview;
|
|
28
29
|
- remove standalone units only after reference and child-unit safety checks;
|
|
@@ -57,6 +58,7 @@ Install dependencies:
|
|
|
57
58
|
|
|
58
59
|
```sh
|
|
59
60
|
bundle install
|
|
61
|
+
bundle exec mxrb --version
|
|
60
62
|
```
|
|
61
63
|
|
|
62
64
|
## Ruby DSL
|
|
@@ -92,7 +94,9 @@ bundle exec mxrb cache warm Shop.mpr
|
|
|
92
94
|
bundle exec mxrb cache clear Shop.mpr
|
|
93
95
|
bundle exec mxrb oql Shop.mpr --dialect postgresql
|
|
94
96
|
bundle exec mxrb db up Shop.mpr
|
|
97
|
+
bundle exec mxrb db credentials Shop.mpr --copy
|
|
95
98
|
bundle exec mxrb db sql Shop.mpr 'SELECT * FROM "sales$order" LIMIT 20'
|
|
99
|
+
bundle exec mxrb demo-user new manager --entity System.User --role User
|
|
96
100
|
bundle exec mxrb pack Shop.mpr --output build/Shop.mda
|
|
97
101
|
bundle exec mxrb portable Shop.mpr --output build/runtime.zip
|
|
98
102
|
bundle exec mxrb team-server clone APP_ID ./shop --pat-file /secure/team-server.env
|
data/README.pt-BR.md
CHANGED
|
@@ -19,6 +19,7 @@ projetos Mendix (`.mpr`) sem MDL ou `mxcli`.
|
|
|
19
19
|
- lint e avaliações executáveis de modelo;
|
|
20
20
|
- descoberta de OQL nativo e visão SQL lógica somente quando o projeto contém OQL;
|
|
21
21
|
- PostgreSQL isolado e sincronizado pelo Runtime para consultas SQL diretas;
|
|
22
|
+
- scaffold de demo users com segredos locais no `.env` e consulta segura de credenciais;
|
|
22
23
|
- análise de planos PostgreSQL/SQL Server e workload cumulativo PostgreSQL;
|
|
23
24
|
- testes funcionais de microflows localmente ou em Docker.
|
|
24
25
|
- busca e instalação de módulos Ruby reutilizáveis com SHA-256 travado.
|
|
@@ -34,6 +35,7 @@ local precisa do Java compatível; o modo Docker fornece JDK e Runtime.
|
|
|
34
35
|
|
|
35
36
|
```sh
|
|
36
37
|
bundle install
|
|
38
|
+
bundle exec mxrb --version
|
|
37
39
|
bundle exec mxrb init vet_clinic
|
|
38
40
|
bundle exec mxrb validate App.mpr
|
|
39
41
|
bundle exec mxrb export App.mpr app-ruby
|
|
@@ -45,7 +47,9 @@ bundle exec mxrb cache warm App.mpr
|
|
|
45
47
|
bundle exec mxrb cache clear App.mpr
|
|
46
48
|
bundle exec mxrb oql App.mpr --dialect postgresql
|
|
47
49
|
bundle exec mxrb db up App.mpr
|
|
50
|
+
bundle exec mxrb db credentials App.mpr --copy
|
|
48
51
|
bundle exec mxrb db sql App.mpr 'SELECT * FROM "sales$order" LIMIT 20'
|
|
52
|
+
bundle exec mxrb demo-user new manager --entity System.User --role User
|
|
49
53
|
```
|
|
50
54
|
|
|
51
55
|
O MPR armazena o modelo, não os dados da aplicação. `db up` compila o Runtime
|
data/bin/mxrb
CHANGED
|
@@ -67,6 +67,9 @@ SCAFFOLD_COMMANDS = (Mxrb::Scaffold::Help::COMMANDS.keys - ["design"]).freeze
|
|
|
67
67
|
command = ARGV.shift
|
|
68
68
|
|
|
69
69
|
case command
|
|
70
|
+
when '--version', '-v'
|
|
71
|
+
puts "mxrb #{Mxrb::VERSION}"
|
|
72
|
+
|
|
70
73
|
when 'doctor'
|
|
71
74
|
json = ARGV.delete('--json')
|
|
72
75
|
root = ARGV.shift || Dir.pwd
|
|
@@ -742,7 +745,7 @@ when "design"
|
|
|
742
745
|
|
|
743
746
|
when "db"
|
|
744
747
|
action = ARGV.shift or
|
|
745
|
-
abort "Usage: mxrb db <up|sync|down|destroy|status|url|sql|explain|workload|indexes|shell> <file.mpr>"
|
|
748
|
+
abort "Usage: mxrb db <up|sync|down|destroy|status|credentials|url|sql|explain|workload|indexes|shell> <file.mpr>"
|
|
746
749
|
path = ARGV.shift or abort "Usage: mxrb db #{action} <file.mpr>"
|
|
747
750
|
option = lambda do |name, fallback = nil|
|
|
748
751
|
index = ARGV.index(name)
|
|
@@ -795,6 +798,30 @@ when "db"
|
|
|
795
798
|
puts "Database : #{info.database_container}"
|
|
796
799
|
puts "Runtime : #{info.runtime_container}"
|
|
797
800
|
puts "State : #{info.state_dir}"
|
|
801
|
+
when "credentials"
|
|
802
|
+
copy = !ARGV.delete("--copy").nil?
|
|
803
|
+
show_password = !ARGV.delete("--show-password").nil?
|
|
804
|
+
json = !ARGV.delete("--json").nil?
|
|
805
|
+
abort '--copy and --show-password are mutually exclusive' if copy && show_password
|
|
806
|
+
abort "Unknown arguments: #{ARGV.join(' ')}" unless ARGV.empty?
|
|
807
|
+
|
|
808
|
+
credentials = workspace.admin_credentials
|
|
809
|
+
visible_password = nil
|
|
810
|
+
visible_password = credentials.password if show_password
|
|
811
|
+
if copy
|
|
812
|
+
command = Mxrb::Runtime::Clipboard.new.copy(credentials.password)
|
|
813
|
+
puts "[mxrb] Password for #{credentials.username} copied with #{command}"
|
|
814
|
+
elsif json
|
|
815
|
+
puts JSON.pretty_generate(
|
|
816
|
+
username: credentials.username, password: visible_password,
|
|
817
|
+
state: credentials.state_dir, credentials_file: credentials.path
|
|
818
|
+
)
|
|
819
|
+
else
|
|
820
|
+
visible_password ||= '<hidden; use --copy or --show-password>'
|
|
821
|
+
puts "User name : #{credentials.username}"
|
|
822
|
+
puts "Password : #{visible_password}"
|
|
823
|
+
puts "State : #{credentials.state_dir}"
|
|
824
|
+
end
|
|
798
825
|
when "url"
|
|
799
826
|
puts workspace.connection_url
|
|
800
827
|
when "sql"
|
|
@@ -833,7 +860,7 @@ when "db"
|
|
|
833
860
|
exec(*workspace.shell_command(write:))
|
|
834
861
|
else
|
|
835
862
|
abort "Unknown db action #{action.inspect}; " \
|
|
836
|
-
"use up, sync, down, destroy, status, url, sql, explain, workload, indexes or shell"
|
|
863
|
+
"use up, sync, down, destroy, status, credentials, url, sql, explain, workload, indexes or shell"
|
|
837
864
|
end
|
|
838
865
|
|
|
839
866
|
when "serve"
|
|
@@ -1479,6 +1506,7 @@ else
|
|
|
1479
1506
|
mxrb — Pure-Ruby Mendix .mpr toolkit
|
|
1480
1507
|
|
|
1481
1508
|
Commands:
|
|
1509
|
+
--version, -v Show the installed MXRB version
|
|
1482
1510
|
doctor [DIR] [--json] Check project files and local toolchain
|
|
1483
1511
|
benchmark <file.mpr> Measure open, index and validation time
|
|
1484
1512
|
--iterations N --json Configure repetitions or emit JSON
|
|
@@ -1537,6 +1565,8 @@ else
|
|
|
1537
1565
|
down <file.mpr> Stop containers while preserving all data
|
|
1538
1566
|
destroy <file.mpr> --yes Remove isolated containers, data, and state
|
|
1539
1567
|
status <file.mpr> Show isolated database/runtime status
|
|
1568
|
+
credentials <file.mpr> [--copy] Show the admin user or copy its password
|
|
1569
|
+
--show-password Explicitly print the Runtime admin password
|
|
1540
1570
|
url <file.mpr> [--port PORT] Print the read-only PostgreSQL connection URL
|
|
1541
1571
|
sql <file.mpr> "SELECT …" Execute SQL as the read-only analyst role
|
|
1542
1572
|
--write Explicitly use the Runtime owner role
|
|
@@ -1595,6 +1625,9 @@ else
|
|
|
1595
1625
|
nanoflow new Module.Name Scaffold a client nanoflow
|
|
1596
1626
|
repository new Module.Name Scaffold a repository port and adapter
|
|
1597
1627
|
security init Module Scaffold module roles
|
|
1628
|
+
demo-user [new] NAME Scaffold a local Mendix demo user
|
|
1629
|
+
[--entity ENTITY] Default: System.User
|
|
1630
|
+
[--role ROLE] Repeatable; default: User
|
|
1598
1631
|
scheduled-event new Module.Name Scaffold an event and handler
|
|
1599
1632
|
constant new Module.Name Scaffold a module constant
|
|
1600
1633
|
integration new Module.Name Scaffold an integration adapter
|
data/docs/de-DE/oql-sql.md
CHANGED
|
@@ -142,6 +142,24 @@ bundle exec mxrb db sql Shop.mpr \
|
|
|
142
142
|
bundle exec mxrb db shell Shop.mpr
|
|
143
143
|
```
|
|
144
144
|
|
|
145
|
+
Die Zugangsdaten des vorhandenen Runtime-Administrators lassen sich prüfen,
|
|
146
|
+
ohne das Passwort standardmäßig auszugeben:
|
|
147
|
+
|
|
148
|
+
```sh
|
|
149
|
+
bundle exec mxrb db credentials Shop.mpr
|
|
150
|
+
bundle exec mxrb db credentials Shop.mpr --copy
|
|
151
|
+
bundle exec mxrb db credentials Shop.mpr --json
|
|
152
|
+
bundle exec mxrb db credentials Shop.mpr --show-password
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`--copy` übergibt das Passwort per stdin an die Zwischenablage und nie als
|
|
156
|
+
Prozessargument. Nur `--show-password` gibt es aus; im JSON bleibt `password`
|
|
157
|
+
ohne diese Option `null`. `--copy` und `--show-password` schließen einander
|
|
158
|
+
aus. Der Befehl liest nur einen vorhandenen Workspace: Er erzeugt oder rotiert
|
|
159
|
+
keine Zugangsdaten und verweist auf `db up`, falls noch kein Zustand existiert.
|
|
160
|
+
Dies sind die Zugangsdaten des Mendix-Anwendungsadministrators, getrennt von
|
|
161
|
+
den PostgreSQL-Rollen `mxrb_reader` und Runtime-Eigentümer.
|
|
162
|
+
|
|
145
163
|
Nach einer Änderung der MPR wird unter Beibehaltung der Daten neu gebaut und
|
|
146
164
|
synchronisiert:
|
|
147
165
|
|
data/docs/de-DE/scaffolds.md
CHANGED
|
@@ -30,6 +30,29 @@ Data Source. Ketten, die mit einem Microflow enden, erzeugen zusätzlich
|
|
|
30
30
|
Jede erzeugte Kette wird als gültiges MPR materialisiert und durch den
|
|
31
31
|
Compiler-Preflight geprüft.
|
|
32
32
|
|
|
33
|
+
## Demo-Benutzer
|
|
34
|
+
|
|
35
|
+
Nach der Initialisierung der Sicherheit wird jeder Demo-Benutzer als eigene
|
|
36
|
+
Ruby-Datei erzeugt:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
mxrb security init App
|
|
40
|
+
mxrb demo-user new manager --entity System.User --role User
|
|
41
|
+
mxrb demo-user operator --entity App.Account --role User --role Administrator
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`new` ist optional. `--entity` verwendet standardmäßig `System.User`, und
|
|
45
|
+
`--role` kann wiederholt werden; ohne Angabe wird die Rolle `User` gewählt. Das
|
|
46
|
+
Scaffold prüft sofort, ob Entität und Anwendungsrollen vorhanden sind,
|
|
47
|
+
verbindet `app/security/demo_users` mit dem `security`-Block und aktiviert
|
|
48
|
+
Demo-Benutzer in der MPR.
|
|
49
|
+
|
|
50
|
+
Das zufällige Passwort liegt nur in der ignorierten `.env`, die beim Erstellen
|
|
51
|
+
den Modus `0600` erhält. Der erzeugte Ruby-Code nutzt `ENV.fetch`, während
|
|
52
|
+
`.env.example` nur eine leere Variable enthält. Danach wird die MPR wie gewohnt
|
|
53
|
+
generiert und validiert. Demo-Benutzer sind für Entwicklung und Vorführungen
|
|
54
|
+
gedacht, nicht für Produktionsbenutzer.
|
|
55
|
+
|
|
33
56
|
## Seitenvorlagen
|
|
34
57
|
|
|
35
58
|
In Mendix sind Page Templates Ausgangspunkte, deren Struktur zu einer normalen,
|
data/docs/en-US/oql-sql.md
CHANGED
|
@@ -137,6 +137,24 @@ bundle exec mxrb db sql Shop.mpr \
|
|
|
137
137
|
bundle exec mxrb db shell Shop.mpr
|
|
138
138
|
```
|
|
139
139
|
|
|
140
|
+
You can inspect the existing Runtime administrator credentials without
|
|
141
|
+
printing the password by default:
|
|
142
|
+
|
|
143
|
+
```sh
|
|
144
|
+
bundle exec mxrb db credentials Shop.mpr
|
|
145
|
+
bundle exec mxrb db credentials Shop.mpr --copy
|
|
146
|
+
bundle exec mxrb db credentials Shop.mpr --json
|
|
147
|
+
bundle exec mxrb db credentials Shop.mpr --show-password
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`--copy` sends the password to the clipboard through stdin, never in process
|
|
151
|
+
arguments. `--show-password` is the only mode that prints it; JSON keeps
|
|
152
|
+
`password` as `null` without that option. `--copy` and `--show-password` are
|
|
153
|
+
mutually exclusive. The command only reads an existing workspace: it neither
|
|
154
|
+
creates nor rotates credentials and tells you to run `db up` when no state
|
|
155
|
+
exists. These are Mendix application-administrator credentials, separate from
|
|
156
|
+
the PostgreSQL `mxrb_reader` and Runtime-owner roles.
|
|
157
|
+
|
|
140
158
|
After changing the MPR, rebuild and synchronize while retaining the data:
|
|
141
159
|
|
|
142
160
|
```sh
|
data/docs/en-US/scaffolds.md
CHANGED
|
@@ -29,6 +29,28 @@ end in a microflow also create `ACT_RefreshOrder`. `page generate` and `page g`
|
|
|
29
29
|
are aliases of `page new`. Every generated chain is materialized as a valid MPR
|
|
30
30
|
and checked by compiler preflight.
|
|
31
31
|
|
|
32
|
+
## Demo users
|
|
33
|
+
|
|
34
|
+
After initializing security, scaffold each demo user as an independent Ruby
|
|
35
|
+
file:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
mxrb security init App
|
|
39
|
+
mxrb demo-user new manager --entity System.User --role User
|
|
40
|
+
mxrb demo-user operator --entity App.Account --role User --role Administrator
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`new` is optional. `--entity` defaults to `System.User`, and `--role` is
|
|
44
|
+
repeatable; without it, the `User` role is selected. The scaffold immediately
|
|
45
|
+
validates that the entity and application roles exist, connects
|
|
46
|
+
`app/security/demo_users` to the `security` block, and enables demo users in
|
|
47
|
+
the MPR.
|
|
48
|
+
|
|
49
|
+
The random password exists only in the ignored `.env`, created with mode
|
|
50
|
+
`0600`. Generated Ruby uses `ENV.fetch`, while `.env.example` receives only an
|
|
51
|
+
empty variable. Generate and validate the MPR normally after scaffolding. Demo
|
|
52
|
+
users are intended for development and demonstrations, not production users.
|
|
53
|
+
|
|
32
54
|
## Page templates
|
|
33
55
|
|
|
34
56
|
In Mendix, page templates are starting points whose structure becomes a normal,
|
data/docs/pt-BR/oql-sql.md
CHANGED
|
@@ -144,6 +144,24 @@ bundle exec mxrb db sql Shop.mpr \
|
|
|
144
144
|
bundle exec mxrb db shell Shop.mpr
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
+
As credenciais do administrador do Runtime já criado podem ser consultadas sem
|
|
148
|
+
imprimir a senha por padrão:
|
|
149
|
+
|
|
150
|
+
```sh
|
|
151
|
+
bundle exec mxrb db credentials Shop.mpr
|
|
152
|
+
bundle exec mxrb db credentials Shop.mpr --copy
|
|
153
|
+
bundle exec mxrb db credentials Shop.mpr --json
|
|
154
|
+
bundle exec mxrb db credentials Shop.mpr --show-password
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`--copy` envia a senha ao clipboard por stdin, sem colocá-la nos argumentos do
|
|
158
|
+
processo. `--show-password` é a única forma de imprimi-la; no JSON, `password`
|
|
159
|
+
permanece `null` sem essa opção. `--copy` e `--show-password` são mutuamente
|
|
160
|
+
exclusivos. O comando apenas lê o workspace existente: não cria nem gira
|
|
161
|
+
credenciais e orienta executar `db up` quando o estado ainda não existe. Essas
|
|
162
|
+
são as credenciais do administrador da aplicação Mendix, distintas dos papéis
|
|
163
|
+
PostgreSQL `mxrb_reader` e proprietário do Runtime.
|
|
164
|
+
|
|
147
165
|
Depois de alterar o MPR, recompile e sincronize preservando os dados:
|
|
148
166
|
|
|
149
167
|
```sh
|
data/docs/pt-BR/scaffolds.md
CHANGED
|
@@ -30,6 +30,7 @@ o hash criado pelo gerador, protegendo edições posteriores.
|
|
|
30
30
|
| `mxrb page new App.Order --chain page:nanoflow:microflow` | Fatia vertical executável |
|
|
31
31
|
| `mxrb nanoflow new App.NAN_OpenCustomer` | Nanoflow cliente |
|
|
32
32
|
| `mxrb security init App` | Papéis, `CheckEverything` no projeto e orientação de acesso |
|
|
33
|
+
| `mxrb demo-user new manager --role User` | Demo user com segredo local em `.env` |
|
|
33
34
|
| `mxrb integration new App.PetApi` | Adaptador de integração |
|
|
34
35
|
| `mxrb published-rest new App.CustomersApi` | Handler editável para REST publicado |
|
|
35
36
|
| `mxrb consumed-rest new App.ExternalPets` | Adaptador REST consumido |
|
|
@@ -71,6 +72,27 @@ são aliases de `page new`. Cada cadeia é materializada em um MPR válido, serv
|
|
|
71
72
|
como ponto de partida removível e exercita o preflight do compilador; arquivos
|
|
72
73
|
existentes nunca são sobrescritos.
|
|
73
74
|
|
|
75
|
+
## Demo users
|
|
76
|
+
|
|
77
|
+
Depois de inicializar a segurança, um demo user pode ser declarado como um
|
|
78
|
+
arquivo Ruby independente:
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
mxrb security init App
|
|
82
|
+
mxrb demo-user new manager --entity System.User --role User
|
|
83
|
+
mxrb demo-user operator --entity App.Account --role User --role Administrator
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`new` é opcional. `--entity` usa `System.User` por padrão e `--role` pode ser
|
|
87
|
+
repetido; sem ele, o papel `User` é usado. O scaffold valida imediatamente se
|
|
88
|
+
a entidade e os papéis existem, conecta `app/security/demo_users` ao bloco
|
|
89
|
+
`security` e habilita demo users no MPR.
|
|
90
|
+
|
|
91
|
+
A senha aleatória fica apenas no `.env` ignorado, com modo `0600` quando o
|
|
92
|
+
arquivo é criado. O Ruby gerado usa `ENV.fetch` e `.env.example` recebe somente
|
|
93
|
+
a variável vazia. Gere e valide o MPR normalmente depois do scaffold. Demo
|
|
94
|
+
users destinam-se a desenvolvimento e demonstração, não a usuários de produção.
|
|
95
|
+
|
|
74
96
|
## Modelos de página
|
|
75
97
|
|
|
76
98
|
No Mendix, page templates são pontos de partida: sua estrutura é copiada para
|
data/lib/mxrb/compare.rb
CHANGED
|
@@ -72,6 +72,13 @@ module Mxrb
|
|
|
72
72
|
admin_user_name: doc["AdminUserName"],
|
|
73
73
|
admin_user_role: doc["AdminUserRole"],
|
|
74
74
|
demo_users_enabled: doc["EnableDemoUsers"],
|
|
75
|
+
demo_users: IO::BsonCodec.parse_array(doc["DemoUsers"]).fetch(:items).map do |user|
|
|
76
|
+
{
|
|
77
|
+
name: user["UserName"], entity: user["Entity"],
|
|
78
|
+
roles: IO::BsonCodec.parse_array(user["UserRoles"]).fetch(:items).sort,
|
|
79
|
+
password_sha256: Digest::SHA256.hexdigest(user["Password"].to_s)
|
|
80
|
+
}
|
|
81
|
+
end.sort_by { _1[:name].to_s },
|
|
75
82
|
guest_access_enabled: doc["EnableGuestAccess"],
|
|
76
83
|
guest_user_role: doc["GuestUserRole"],
|
|
77
84
|
sign_in_microflow: doc["SignInMicroflow"],
|
data/lib/mxrb/dsl/builder.rb
CHANGED
|
@@ -6,6 +6,49 @@ require "base64"
|
|
|
6
6
|
|
|
7
7
|
module Mxrb
|
|
8
8
|
module Dsl
|
|
9
|
+
# Validates project-security references that cross DSL builders.
|
|
10
|
+
class SecurityValidator
|
|
11
|
+
def initialize(modules, security)
|
|
12
|
+
@modules = modules
|
|
13
|
+
@security = security
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def validate!
|
|
17
|
+
return unless @security
|
|
18
|
+
|
|
19
|
+
errors = demo_users.flat_map { reference_errors(_1) }
|
|
20
|
+
duplicates = demo_users.group_by { _1.fetch(:name) }.select { |_name, users| users.size > 1 }
|
|
21
|
+
errors << "duplicate demo user name(s): #{duplicates.keys.join(', ')}" unless duplicates.empty?
|
|
22
|
+
return if errors.empty?
|
|
23
|
+
|
|
24
|
+
raise ValidationError, "security validation failed:\n- #{errors.join("\n- ")}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def demo_users = Array(@security[:demo_users])
|
|
30
|
+
|
|
31
|
+
def reference_errors(user)
|
|
32
|
+
errors = []
|
|
33
|
+
missing_roles = user.fetch(:roles) - user_roles
|
|
34
|
+
errors << "demo user #{user[:name]} references missing user role(s): #{missing_roles.join(', ')}" unless
|
|
35
|
+
missing_roles.empty?
|
|
36
|
+
entity = user.fetch(:entity)
|
|
37
|
+
errors << "demo user #{user[:name]} references missing user entity #{entity}" unless
|
|
38
|
+
entity == "System.User" || entities.include?(entity)
|
|
39
|
+
errors
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def user_roles = @security.fetch(:user_roles, []).map { _1.fetch(:name) }
|
|
43
|
+
|
|
44
|
+
def entities
|
|
45
|
+
@modules.flat_map do |mod|
|
|
46
|
+
definition = mod.to_h
|
|
47
|
+
definition.fetch(:entities, []).map { "#{definition.fetch(:name)}.#{_1.fetch(:name)}" }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
9
52
|
# Shared widget-building methods for PageBuilder and ContainerBuilder.
|
|
10
53
|
# Includers must implement private `_widget_list` returning the target array.
|
|
11
54
|
module WidgetDsl
|
|
@@ -361,9 +404,14 @@ module Mxrb
|
|
|
361
404
|
end
|
|
362
405
|
|
|
363
406
|
def validate!
|
|
407
|
+
validate_security!
|
|
364
408
|
Architecture::Validator.new(graph).validate!
|
|
365
409
|
end
|
|
366
410
|
|
|
411
|
+
def validate_security!
|
|
412
|
+
SecurityValidator.new(@modules, @security).validate!
|
|
413
|
+
end
|
|
414
|
+
|
|
367
415
|
def definition
|
|
368
416
|
{
|
|
369
417
|
version: @mendix_version,
|
|
@@ -382,6 +430,8 @@ module Mxrb
|
|
|
382
430
|
class SecurityBuilder
|
|
383
431
|
def initialize
|
|
384
432
|
@user_roles = []
|
|
433
|
+
@demo_users = []
|
|
434
|
+
@demo_users_declared = false
|
|
385
435
|
@security_level = nil
|
|
386
436
|
@admin_user_role = nil
|
|
387
437
|
@demo_users_enabled = nil
|
|
@@ -414,6 +464,36 @@ module Mxrb
|
|
|
414
464
|
@demo_users_enabled = enabled == true
|
|
415
465
|
end
|
|
416
466
|
|
|
467
|
+
def demo_user(name, entity:, roles:, password:)
|
|
468
|
+
user_name = name.to_s
|
|
469
|
+
entity_name = entity.to_s
|
|
470
|
+
role_names = Array(roles).map(&:to_s).uniq
|
|
471
|
+
secret = password.to_s
|
|
472
|
+
raise ArgumentError, 'demo user name must not be empty' if user_name.empty?
|
|
473
|
+
raise ArgumentError, 'demo user name must not contain whitespace' unless user_name.match?(/\A[^\s]+\z/)
|
|
474
|
+
unless entity_name.match?(/\A[A-Za-z][A-Za-z0-9_]*\.[A-Za-z][A-Za-z0-9_]*\z/)
|
|
475
|
+
raise ArgumentError, 'demo user entity must be qualified as Module.Entity'
|
|
476
|
+
end
|
|
477
|
+
raise ArgumentError, 'demo user requires at least one role' if role_names.empty?
|
|
478
|
+
raise ArgumentError, 'demo user password must not be empty' if secret.empty?
|
|
479
|
+
|
|
480
|
+
@demo_users << {
|
|
481
|
+
name: user_name, entity: entity_name, roles: role_names, password: secret
|
|
482
|
+
}
|
|
483
|
+
@demo_users_declared = true
|
|
484
|
+
@demo_users_enabled = true
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def evaluate(path)
|
|
488
|
+
instance_eval(File.read(path), path, 1)
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def evaluate_dir(dir)
|
|
492
|
+
return unless File.directory?(dir)
|
|
493
|
+
|
|
494
|
+
Dir[File.join(dir, '*.rb')].sort.each { |path| evaluate(path) }
|
|
495
|
+
end
|
|
496
|
+
|
|
417
497
|
def guest_access(enabled = true, role: nil)
|
|
418
498
|
@guest_access_enabled = enabled == true
|
|
419
499
|
@guest_user_role = role&.to_s
|
|
@@ -430,6 +510,7 @@ module Mxrb
|
|
|
430
510
|
def to_h
|
|
431
511
|
{
|
|
432
512
|
user_roles: @user_roles,
|
|
513
|
+
demo_users: @demo_users_declared ? @demo_users : nil,
|
|
433
514
|
security_level: @security_level,
|
|
434
515
|
admin_user_role: @admin_user_role,
|
|
435
516
|
demo_users_enabled: @demo_users_enabled,
|
data/lib/mxrb/exporter.rb
CHANGED
|
@@ -415,6 +415,12 @@ module Mxrb
|
|
|
415
415
|
options = []
|
|
416
416
|
options << " admin_user_role #{ruby(doc["AdminUserRole"])}" unless doc["AdminUserRole"].to_s.empty?
|
|
417
417
|
options << " demo_users #{doc["EnableDemoUsers"] == true}"
|
|
418
|
+
demo_users = doc["DemoUsers"] || IO::BsonCodec.build_array([])
|
|
419
|
+
IO::BsonCodec.parse_array(demo_users).fetch(:items).each do |user|
|
|
420
|
+
assigned_roles = IO::BsonCodec.parse_array(user["UserRoles"]).fetch(:items)
|
|
421
|
+
options << " demo_user #{ruby(user["UserName"])}, entity: #{ruby(user["Entity"])}, " \
|
|
422
|
+
"roles: #{ruby(assigned_roles)}, password: #{ruby(user["Password"])}"
|
|
423
|
+
end
|
|
418
424
|
guest = " guest_access #{doc["EnableGuestAccess"] == true}"
|
|
419
425
|
guest += ", role: #{ruby(doc["GuestUserRole"])}" unless doc["GuestUserRole"].to_s.empty?
|
|
420
426
|
options << guest
|
data/lib/mxrb/initializer.rb
CHANGED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'tempfile'
|
|
4
|
+
|
|
5
|
+
module Mxrb
|
|
6
|
+
module Runtime
|
|
7
|
+
# Copies secrets through stdin so they never appear in the process list.
|
|
8
|
+
class Clipboard
|
|
9
|
+
COMMANDS = [
|
|
10
|
+
['wl-copy'], ['pbcopy'], ['xclip', '-selection', 'clipboard'],
|
|
11
|
+
['xsel', '--clipboard', '--input'], ['clip.exe']
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
def initialize(path: ENV.fetch('PATH', ''), runner: nil)
|
|
15
|
+
@path = path
|
|
16
|
+
@runner = runner || method(:capture)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def copy(text)
|
|
20
|
+
command = available_command
|
|
21
|
+
unless command
|
|
22
|
+
raise ToolchainError,
|
|
23
|
+
'no clipboard command found; install wl-copy, pbcopy, xclip, xsel, or clip.exe'
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
_stdout, stderr, status = @runner.call(command, text.to_s)
|
|
27
|
+
raise ToolchainError, "clipboard command failed: #{stderr}" unless status.success?
|
|
28
|
+
|
|
29
|
+
command.first
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def available_command
|
|
35
|
+
COMMANDS.find { executable?(_1.first) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def executable?(name)
|
|
39
|
+
@path.split(File::PATH_SEPARATOR).any? do |directory|
|
|
40
|
+
file = File.join(directory, name)
|
|
41
|
+
File.file?(file) && File.executable?(file)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def capture(command, input)
|
|
46
|
+
Tempfile.create('mxrb-clipboard') { capture_process(command, input, _1) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def capture_process(command, input, stderr)
|
|
50
|
+
::IO.pipe do |reader, writer|
|
|
51
|
+
pid = Process.spawn({ 'PATH' => @path }, *command, in: reader, out: File::NULL, err: stderr)
|
|
52
|
+
reader.close
|
|
53
|
+
writer.write(input)
|
|
54
|
+
writer.close
|
|
55
|
+
_child, status = Process.wait2(pid)
|
|
56
|
+
stderr.rewind
|
|
57
|
+
['', stderr.read, status]
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -14,6 +14,7 @@ module Mxrb
|
|
|
14
14
|
:host, :port, :database, :reader_user, :running, :model_fingerprint,
|
|
15
15
|
:runtime_url
|
|
16
16
|
)
|
|
17
|
+
AdminCredentials = Data.define(:username, :password, :state_dir, :path)
|
|
17
18
|
|
|
18
19
|
# Materializes a private PostgreSQL database for one MPR. The Mendix Runtime
|
|
19
20
|
# owns schema synchronization; analyst access uses a separate read-only role.
|
|
@@ -148,8 +149,35 @@ module Mxrb
|
|
|
148
149
|
"postgresql://#{READER}:#{secret}@127.0.0.1:#{@port}/#{DATABASE}"
|
|
149
150
|
end
|
|
150
151
|
|
|
152
|
+
def admin_credentials
|
|
153
|
+
unless File.file?(credentials_path)
|
|
154
|
+
raise ToolchainError,
|
|
155
|
+
"Runtime credentials not found; run `mxrb db up #{@project_path}` first"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
values = JSON.parse(File.read(credentials_path))
|
|
159
|
+
password = values.fetch('admin_password')
|
|
160
|
+
AdminCredentials.new(admin_user_name, password, @state_dir, credentials_path)
|
|
161
|
+
rescue JSON::ParserError, KeyError => e
|
|
162
|
+
raise ToolchainError, "invalid Runtime credentials file #{credentials_path}: #{e.message}"
|
|
163
|
+
end
|
|
164
|
+
|
|
151
165
|
private
|
|
152
166
|
|
|
167
|
+
def admin_user_name
|
|
168
|
+
mpr = IO::MprFile.open(@project_path, readonly: true)
|
|
169
|
+
begin
|
|
170
|
+
security = mpr.all_units.lazy.map { mpr.parse_contents(_1) }
|
|
171
|
+
.find { _1['$Type'] == 'Security$ProjectSecurity' }
|
|
172
|
+
name = security.to_h.fetch('AdminUserName', '').to_s
|
|
173
|
+
raise ToolchainError, 'MPR has no configured administrator user' if name.empty?
|
|
174
|
+
|
|
175
|
+
name
|
|
176
|
+
ensure
|
|
177
|
+
mpr.close
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
153
181
|
def bind_parameters(statement, params)
|
|
154
182
|
values = params.to_h.transform_keys(&:to_s)
|
|
155
183
|
return [statement, []] if values.empty?
|
data/lib/mxrb/scaffold/cli.rb
CHANGED
|
@@ -31,9 +31,12 @@ module Mxrb
|
|
|
31
31
|
def generator
|
|
32
32
|
chain = extract_option('--chain') if @command == 'page'
|
|
33
33
|
template = extract_option('--template') if @command == 'page'
|
|
34
|
+
entity = extract_option('--entity') if @command == 'demo-user'
|
|
35
|
+
roles = extract_options('--role') if @command == 'demo-user'
|
|
34
36
|
Generator.new(
|
|
35
37
|
kind, scaffold_name, target: target, dry_run: @dry_run,
|
|
36
|
-
page_chain: chain, page_template: template
|
|
38
|
+
page_chain: chain, page_template: template,
|
|
39
|
+
demo_entity: entity, demo_roles: roles
|
|
37
40
|
)
|
|
38
41
|
end
|
|
39
42
|
|
|
@@ -56,6 +59,12 @@ module Mxrb
|
|
|
56
59
|
end
|
|
57
60
|
|
|
58
61
|
def validate_action!
|
|
62
|
+
if @command == 'demo-user'
|
|
63
|
+
@argv.shift if @argv.first == 'new'
|
|
64
|
+
abort Help.text(@command) if @argv.empty?
|
|
65
|
+
return
|
|
66
|
+
end
|
|
67
|
+
|
|
59
68
|
action = @argv.shift
|
|
60
69
|
abort Help.text(@command) unless accepted_actions.include?(action)
|
|
61
70
|
end
|
|
@@ -90,6 +99,16 @@ module Mxrb
|
|
|
90
99
|
@argv.slice!(index, 2).last
|
|
91
100
|
end
|
|
92
101
|
|
|
102
|
+
def extract_options(name)
|
|
103
|
+
values = []
|
|
104
|
+
while (index = @argv.index(name))
|
|
105
|
+
abort "#{name} requires a value" unless @argv[index + 1]
|
|
106
|
+
|
|
107
|
+
values << @argv.slice!(index, 2).last
|
|
108
|
+
end
|
|
109
|
+
values
|
|
110
|
+
end
|
|
111
|
+
|
|
93
112
|
def kind = @command.tr('-', '_').to_sym
|
|
94
113
|
|
|
95
114
|
def render(result) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
3
5
|
module Mxrb
|
|
4
6
|
module Scaffold
|
|
5
7
|
# Creates conventional Ruby-first artifacts and connects missing aggregators.
|
|
@@ -24,8 +26,7 @@ module Mxrb
|
|
|
24
26
|
@root = File.expand_path(target)
|
|
25
27
|
@transaction = Transaction.new
|
|
26
28
|
@dry_run = dry_run
|
|
27
|
-
|
|
28
|
-
@page_template = page_options.delete(:page_template)
|
|
29
|
+
assign_options(page_options)
|
|
29
30
|
raise ArgumentError, "unknown generator options: #{page_options.keys.join(', ')}" unless
|
|
30
31
|
page_options.empty?
|
|
31
32
|
end
|
|
@@ -42,6 +43,13 @@ module Mxrb
|
|
|
42
43
|
|
|
43
44
|
private
|
|
44
45
|
|
|
46
|
+
def assign_options(options)
|
|
47
|
+
@page_chain = options.delete(:page_chain)
|
|
48
|
+
@page_template = options.delete(:page_template)
|
|
49
|
+
@demo_entity = options.delete(:demo_entity)
|
|
50
|
+
@demo_roles = options.delete(:demo_roles)
|
|
51
|
+
end
|
|
52
|
+
|
|
45
53
|
def page_chain
|
|
46
54
|
return unless @page_chain
|
|
47
55
|
return @page_chain if PAGE_CHAINS.include?(@page_chain)
|
|
@@ -55,8 +63,13 @@ module Mxrb
|
|
|
55
63
|
end
|
|
56
64
|
|
|
57
65
|
def stage_registry
|
|
66
|
+
files = @transaction.created.dup
|
|
67
|
+
if @kind == :demo_user
|
|
68
|
+
shared = [project_path('.env'), project_path('.env.example')]
|
|
69
|
+
files -= shared
|
|
70
|
+
end
|
|
58
71
|
Registry.stage(
|
|
59
|
-
@transaction, root: @root, key: "#{@kind}:#{@name}", files:
|
|
72
|
+
@transaction, root: @root, key: "#{@kind}:#{@name}", files:
|
|
60
73
|
)
|
|
61
74
|
end
|
|
62
75
|
|
|
@@ -125,6 +138,115 @@ module Mxrb
|
|
|
125
138
|
@transaction.write(path, lines.join)
|
|
126
139
|
end
|
|
127
140
|
|
|
141
|
+
def connect_project_demo_users
|
|
142
|
+
path = project_path('project.rb')
|
|
143
|
+
source = connect_project_dotenv(@transaction.content(path))
|
|
144
|
+
opening = source.lines.index { _1.match?(/^\s*security do\s*$/) }
|
|
145
|
+
raise ArgumentError, security_initialization_error unless opening
|
|
146
|
+
|
|
147
|
+
insert_demo_user_loader(path, source, opening)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def insert_demo_user_loader(path, source, opening)
|
|
151
|
+
line = 'evaluate_dir File.join(__dir__, "app", "security", "demo_users")'
|
|
152
|
+
return if source.include?(line)
|
|
153
|
+
|
|
154
|
+
lines = source.lines
|
|
155
|
+
indentation = "#{lines.fetch(opening)[/^\s*/]} "
|
|
156
|
+
lines.insert(opening + 1, "#{indentation}#{line}\n")
|
|
157
|
+
@transaction.write(path, lines.join)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def connect_project_dotenv(source)
|
|
161
|
+
dotenv_line = 'Dotenv.load(File.join(__dir__, ".env"))'
|
|
162
|
+
return source if source.include?(dotenv_line)
|
|
163
|
+
|
|
164
|
+
lines = source.lines
|
|
165
|
+
require_line = lines.index { _1.match?(%r{^require ["']dotenv/load["']\s*$}) }
|
|
166
|
+
raise ArgumentError, 'project.rb must require dotenv/load before scaffolding demo users' unless require_line
|
|
167
|
+
|
|
168
|
+
lines.insert(require_line + 1, "#{dotenv_line}\n").join
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def security_initialization_error
|
|
172
|
+
'project security is not initialized; run `mxrb security init Module` first'
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def create_demo_user_file(name, entity, roles, password_env)
|
|
176
|
+
path = project_path('app', 'security', 'demo_users', "#{Templates.snake_case(name)}.rb")
|
|
177
|
+
@transaction.create(
|
|
178
|
+
path,
|
|
179
|
+
Templates.render(
|
|
180
|
+
:demo_user, name:, entity:, roles:, password_env:
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def ensure_demo_user_secret(password_env)
|
|
186
|
+
path = project_path('.env')
|
|
187
|
+
source = @transaction.content(path)
|
|
188
|
+
if source
|
|
189
|
+
@transaction.write(path, append_env(source, password_env, generated_demo_password)) unless
|
|
190
|
+
env_key?(source, password_env)
|
|
191
|
+
else
|
|
192
|
+
create_demo_user_secret(path, password_env)
|
|
193
|
+
end
|
|
194
|
+
ensure_demo_user_env_example(password_env)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def create_demo_user_secret(path, password_env)
|
|
198
|
+
source = "# Local secrets; never commit this file.\n"
|
|
199
|
+
content = append_env(source, password_env, generated_demo_password)
|
|
200
|
+
@transaction.create(path, content, mode: 0o600)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def ensure_demo_user_env_example(password_env)
|
|
204
|
+
path = project_path('.env.example')
|
|
205
|
+
source = @transaction.content(path) || "# Copy to .env and keep real values local.\n"
|
|
206
|
+
@transaction.write(path, append_env(source, password_env, '')) unless env_key?(source, password_env)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def append_env(source, key, value)
|
|
210
|
+
"#{source.rstrip}\n#{key}=#{value}\n"
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def env_key?(source, key)
|
|
214
|
+
source.lines.any? { _1.match?(/\A#{Regexp.escape(key)}=/) }
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def generated_demo_password
|
|
218
|
+
"Mxrb#{SecureRandom.alphanumeric(16)}7"
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def validate_demo_user_references!(name, entity, roles)
|
|
222
|
+
sources = project_ruby_sources
|
|
223
|
+
errors = []
|
|
224
|
+
missing_roles = roles - known_user_roles(sources)
|
|
225
|
+
errors << "missing user role(s): #{missing_roles.join(', ')}" unless missing_roles.empty?
|
|
226
|
+
errors << "missing user entity: #{entity}" unless
|
|
227
|
+
entity == 'System.User' || known_entities(sources).include?(entity)
|
|
228
|
+
return if errors.empty?
|
|
229
|
+
|
|
230
|
+
raise ArgumentError, "demo user #{name}: #{errors.join('; ')}"
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def project_ruby_sources
|
|
234
|
+
Dir.glob(project_path('**', '*.rb')).to_h { [_1, File.read(_1)] }
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def known_user_roles(sources)
|
|
238
|
+
pattern = /\buser_role\s+(?::([A-Za-z][A-Za-z0-9_]*)|["']([^"']+)["'])/
|
|
239
|
+
sources.values.flat_map { _1.scan(pattern).map { |match| match.compact.first } }.uniq
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def known_entities(sources)
|
|
243
|
+
pattern = /\bentity\s+(?::([A-Za-z][A-Za-z0-9_]*)|["']([^"']+)["'])/
|
|
244
|
+
sources.flat_map do |path, source|
|
|
245
|
+
mod = path.match(%r{/modules/([^/]+)/})&.[](1)
|
|
246
|
+
mod ? source.scan(pattern).map { "#{mod}.#{_1.compact.first}" } : []
|
|
247
|
+
end.uniq
|
|
248
|
+
end
|
|
249
|
+
|
|
128
250
|
def insert_before_closing(source, line, path)
|
|
129
251
|
lines = source.lines
|
|
130
252
|
closing = lines.rindex { _1.strip == 'end' }
|
data/lib/mxrb/scaffold/help.rb
CHANGED
|
@@ -23,6 +23,11 @@ module Mxrb
|
|
|
23
23
|
'nanoflow' => ['new Module.Flow', 'Create a client nanoflow', 'presentation/client_actions'],
|
|
24
24
|
'repository' => ['new Module.Repository', 'Create repository port and adapter', 'application/infrastructure'],
|
|
25
25
|
'security' => ['init Module', 'Create module roles template', 'security'],
|
|
26
|
+
'demo-user' => [
|
|
27
|
+
'new NAME [--entity Module.Entity] [--role ROLE]',
|
|
28
|
+
'Create a local Mendix demo user backed by an ignored .env secret',
|
|
29
|
+
'app/security/demo_users'
|
|
30
|
+
],
|
|
26
31
|
'scheduled-event' => ['new Module.Event', 'Create scheduled event and handler', 'application/jobs'],
|
|
27
32
|
'constant' => ['new Module.Constant', 'Create a string constant', 'domain/constants'],
|
|
28
33
|
'integration' => ['new Module.Adapter', 'Create integration adapter flow', 'infrastructure/integrations'],
|
|
@@ -54,6 +59,14 @@ module Mxrb
|
|
|
54
59
|
mxrb page templates [--json]
|
|
55
60
|
--template starter|blank|dashboard|form-vertical
|
|
56
61
|
HELP
|
|
62
|
+
DEMO_USER_OPTIONS = <<~HELP
|
|
63
|
+
Demo user options:
|
|
64
|
+
--entity Module.Entity User entity (default: System.User)
|
|
65
|
+
--role ROLE App user role (repeatable; default: User)
|
|
66
|
+
|
|
67
|
+
`new` is optional, so `mxrb demo-user manager ...` is also accepted.
|
|
68
|
+
The generated password is stored in the ignored .env file.
|
|
69
|
+
HELP
|
|
57
70
|
|
|
58
71
|
def text(command)
|
|
59
72
|
usage, description, destination = COMMANDS.fetch(command)
|
|
@@ -71,9 +84,10 @@ module Mxrb
|
|
|
71
84
|
end
|
|
72
85
|
|
|
73
86
|
def page_options(command)
|
|
74
|
-
return
|
|
87
|
+
return PAGE_OPTIONS if command == 'page'
|
|
88
|
+
return DEMO_USER_OPTIONS if command == 'demo-user'
|
|
75
89
|
|
|
76
|
-
|
|
90
|
+
''
|
|
77
91
|
end
|
|
78
92
|
end
|
|
79
93
|
end
|
|
@@ -129,6 +129,33 @@ module Mxrb
|
|
|
129
129
|
connect_project_security(module_name)
|
|
130
130
|
end
|
|
131
131
|
|
|
132
|
+
def scaffold_demo_user
|
|
133
|
+
ensure_project
|
|
134
|
+
name = identifier(@name, label: 'demo user')
|
|
135
|
+
entity = demo_user_entity
|
|
136
|
+
roles = demo_user_roles
|
|
137
|
+
validate_demo_user_references!(name, entity, roles)
|
|
138
|
+
password_env = "MXRB_DEMO_USER_#{name.gsub(/[^A-Za-z0-9]/, '_').upcase}_PASSWORD"
|
|
139
|
+
|
|
140
|
+
connect_project_demo_users
|
|
141
|
+
create_demo_user_file(name, entity, roles, password_env)
|
|
142
|
+
ensure_demo_user_secret(password_env) unless @dry_run
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def demo_user_entity
|
|
146
|
+
entity = (@demo_entity || 'System.User').to_s
|
|
147
|
+
pattern = /\A[A-Za-z][A-Za-z0-9_]*\.[A-Za-z][A-Za-z0-9_]*\z/
|
|
148
|
+
raise ArgumentError, 'demo user entity must be qualified as Module.Entity' unless entity.match?(pattern)
|
|
149
|
+
|
|
150
|
+
entity
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def demo_user_roles
|
|
154
|
+
roles = Array(@demo_roles)
|
|
155
|
+
roles = ['User'] if roles.empty?
|
|
156
|
+
roles.map { identifier(_1.to_s, label: 'user role') }.uniq
|
|
157
|
+
end
|
|
158
|
+
|
|
132
159
|
def scaffold_functional_test
|
|
133
160
|
module_name, artifact_name = qualified_name
|
|
134
161
|
ensure_project
|
|
@@ -293,6 +293,17 @@ module Mxrb
|
|
|
293
293
|
RUBY
|
|
294
294
|
end
|
|
295
295
|
|
|
296
|
+
def demo_user(_module_name, name, entity:, roles:, password_env:)
|
|
297
|
+
<<~RUBY
|
|
298
|
+
# frozen_string_literal: true
|
|
299
|
+
|
|
300
|
+
demo_user #{name.inspect},
|
|
301
|
+
entity: #{entity.inspect},
|
|
302
|
+
roles: #{roles.inspect},
|
|
303
|
+
password: ENV.fetch(#{password_env.inspect})
|
|
304
|
+
RUBY
|
|
305
|
+
end
|
|
306
|
+
|
|
296
307
|
def scheduled_event(module_name, name)
|
|
297
308
|
<<~RUBY
|
|
298
309
|
# frozen_string_literal: true
|
|
@@ -21,11 +21,11 @@ module Mxrb
|
|
|
21
21
|
nil
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
def create(path, content)
|
|
24
|
+
def create(path, content, mode: 0o644)
|
|
25
25
|
abort "#{path}: file already exists" if File.exist?(path) || @changes.key?(path)
|
|
26
26
|
|
|
27
27
|
@created << path
|
|
28
|
-
@changes[path] = Change.new(path, content, nil,
|
|
28
|
+
@changes[path] = Change.new(path, content, nil, mode)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
def write(path, new_content)
|
data/lib/mxrb/version.rb
CHANGED
data/lib/mxrb/writer.rb
CHANGED
|
@@ -441,6 +441,7 @@ module Mxrb
|
|
|
441
441
|
end
|
|
442
442
|
{
|
|
443
443
|
demo_users_enabled: "EnableDemoUsers",
|
|
444
|
+
demo_users: "DemoUsers",
|
|
444
445
|
guest_access_enabled: "EnableGuestAccess",
|
|
445
446
|
guest_user_role: "GuestUserRole",
|
|
446
447
|
sign_in_microflow: "SignInMicroflow",
|
|
@@ -1923,7 +1924,9 @@ module Mxrb
|
|
|
1923
1924
|
"StrictMode" => false,
|
|
1924
1925
|
"StrictPageUrlCheck" => true,
|
|
1925
1926
|
"UserRoles" => IO::BsonCodec.build_array(roles, marker: 2),
|
|
1926
|
-
"DemoUsers" => IO::BsonCodec.build_array(
|
|
1927
|
+
"DemoUsers" => IO::BsonCodec.build_array(
|
|
1928
|
+
Array(security[:demo_users]).map { demo_user_doc(_1) }, marker: 2
|
|
1929
|
+
),
|
|
1927
1930
|
"FileDocumentAccess" => access_container("Security$FileDocumentAccessRuleContainer"),
|
|
1928
1931
|
"ImageAccess" => access_container("Security$ImageAccessRuleContainer"),
|
|
1929
1932
|
"PasswordPolicySettings" => password_policy
|
|
@@ -1945,6 +1948,17 @@ module Mxrb
|
|
|
1945
1948
|
}
|
|
1946
1949
|
end
|
|
1947
1950
|
|
|
1951
|
+
def demo_user_doc(user)
|
|
1952
|
+
{
|
|
1953
|
+
"$ID" => SecureRandom.uuid,
|
|
1954
|
+
"$Type" => "Security$DemoUserImpl",
|
|
1955
|
+
"UserName" => user.fetch(:name),
|
|
1956
|
+
"Password" => user.fetch(:password),
|
|
1957
|
+
"Entity" => user.fetch(:entity),
|
|
1958
|
+
"UserRoles" => IO::BsonCodec.build_array(user.fetch(:roles), marker: 1)
|
|
1959
|
+
}
|
|
1960
|
+
end
|
|
1961
|
+
|
|
1948
1962
|
def module_security_doc(roles, legacy: false)
|
|
1949
1963
|
{
|
|
1950
1964
|
"$ID" => SecureRandom.uuid,
|
data/lib/mxrb.rb
CHANGED
|
@@ -126,6 +126,7 @@ require_relative "mxrb/runtime/docker_workspace"
|
|
|
126
126
|
require_relative "mxrb/runtime/executor"
|
|
127
127
|
require_relative "mxrb/runtime/native"
|
|
128
128
|
require_relative "mxrb/runtime/docker_executor"
|
|
129
|
+
require_relative "mxrb/runtime/clipboard"
|
|
129
130
|
require_relative "mxrb/runtime/database_workspace"
|
|
130
131
|
require_relative "mxrb/runtime/sql_server_database"
|
|
131
132
|
require_relative "mxrb/oql/server"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mxrb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lucas Moura
|
|
@@ -375,6 +375,7 @@ files:
|
|
|
375
375
|
- lib/mxrb/project_lifecycle.rb
|
|
376
376
|
- lib/mxrb/protocols.rb
|
|
377
377
|
- lib/mxrb/protocols/plan.rb
|
|
378
|
+
- lib/mxrb/runtime/clipboard.rb
|
|
378
379
|
- lib/mxrb/runtime/database_workspace.rb
|
|
379
380
|
- lib/mxrb/runtime/docker_executor.rb
|
|
380
381
|
- lib/mxrb/runtime/docker_workspace.rb
|