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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 80ac058134902cb839b9dacd80da635675e27fe0ac6fe3eb22dd833c551731ab
4
- data.tar.gz: 291c3ce744371e4f89a9b0b63ae37200831d566a140285e26c0d3199482c6775
3
+ metadata.gz: 571786ae22b838daaa4700d7a99816653046f645a5d4f49ee29d155f4a05fa99
4
+ data.tar.gz: 4366822b4731a3604eb2be09ee1f4408fe93108229486b6b45528772022c709f
5
5
  SHA512:
6
- metadata.gz: 37566da77719ce1537886b96ed5189c080da1d8668aea9d7e4e5a3de7ffd18a81de67318ffbc005d5ab2a5b3dfbacf5e903439cf884c94b09c6c09b6148e1a75
7
- data.tar.gz: f48190ec6af16e09d97246e7eb514f054ea7555b99560143766bb3df13f86fbcc47d8944a924bc3a70cd846410e14b845a4c64b5caec7e9f56866ec441b53355
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
@@ -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
 
@@ -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,
@@ -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
@@ -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,
@@ -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
@@ -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"],
@@ -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
@@ -152,6 +152,7 @@ module Mxrb
152
152
  # frozen_string_literal: true
153
153
 
154
154
  require "dotenv/load"
155
+ Dotenv.load(File.join(__dir__, ".env"))
155
156
  require "mxrb"
156
157
 
157
158
  output = ENV.fetch("MXRB_OUTPUT_PATH", File.join(__dir__, "#{@mpr_name}"))
@@ -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?
@@ -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
- @page_chain = page_options.delete(:page_chain)
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: @transaction.created.dup
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' }
@@ -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 '' unless command == 'page'
87
+ return PAGE_OPTIONS if command == 'page'
88
+ return DEMO_USER_OPTIONS if command == 'demo-user'
75
89
 
76
- PAGE_OPTIONS
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, 0o644)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mxrb
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end
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([], marker: 2),
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.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