mxrb 0.1.2 → 0.1.4

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.
Files changed (191) hide show
  1. checksums.yaml +4 -4
  2. data/bin/mxrb +458 -10
  3. data/docs/de-DE/README.md +1 -0
  4. data/docs/de-DE/oql-sql.md +42 -0
  5. data/docs/de-DE/project-structure.md +201 -2
  6. data/docs/de-DE/runtime-ruby.md +104 -0
  7. data/docs/de-DE/runtime-schema-migrations.md +44 -0
  8. data/docs/de-DE/validation-matrix.md +42 -4
  9. data/docs/en-US/README.md +1 -0
  10. data/docs/en-US/oql-sql.md +41 -0
  11. data/docs/en-US/project-structure.md +192 -2
  12. data/docs/en-US/runtime-ruby.md +100 -0
  13. data/docs/en-US/runtime-schema-migrations.md +41 -0
  14. data/docs/en-US/validation-matrix.md +39 -4
  15. data/docs/pt-BR/README.md +1 -0
  16. data/docs/pt-BR/entity-dsl.md +8 -0
  17. data/docs/pt-BR/oql-sql.md +41 -0
  18. data/docs/pt-BR/project-structure.md +197 -2
  19. data/docs/pt-BR/runtime-ruby.md +108 -0
  20. data/docs/pt-BR/runtime-schema-migrations.md +40 -0
  21. data/docs/pt-BR/validation-matrix.md +42 -5
  22. data/docs/pt-BR/writing.md +28 -0
  23. data/lib/mxrb/cli/help.rb +426 -0
  24. data/lib/mxrb/cli/release.rb +137 -0
  25. data/lib/mxrb/compiler/compatibility_analyzer.rb +1 -1
  26. data/lib/mxrb/compiler/data_grid_bundle_compiler.rb +8 -1
  27. data/lib/mxrb/compiler/domain_document_compiler.rb +3 -0
  28. data/lib/mxrb/compiler/gallery_bundle_compiler.rb +11 -1
  29. data/lib/mxrb/compiler/generic_widget_bundle_compiler.rb +76 -19
  30. data/lib/mxrb/compiler/legacy_custom_widget_compiler.rb +159 -0
  31. data/lib/mxrb/compiler/legacy_data_grid_compiler.rb +43 -7
  32. data/lib/mxrb/compiler/legacy_page_builder.rb +1248 -23
  33. data/lib/mxrb/compiler/packager.rb +27 -13
  34. data/lib/mxrb/compiler/page_bundle_compiler.rb +13 -4
  35. data/lib/mxrb/compiler/portable_packager.rb +16 -11
  36. data/lib/mxrb/compiler/web_bundle_builder.rb +5 -1
  37. data/lib/mxrb/compiler/web_shell_materializer.rb +42 -2
  38. data/lib/mxrb/compiler/widget_package_extractor.rb +3 -0
  39. data/lib/mxrb/domain_diagram/lifecycle.rb +272 -0
  40. data/lib/mxrb/domain_diagram.rb +409 -0
  41. data/lib/mxrb/dsl/builder.rb +62 -11
  42. data/lib/mxrb/environment.rb +226 -0
  43. data/lib/mxrb/exporter.rb +265 -45
  44. data/lib/mxrb/http/server.rb +107 -0
  45. data/lib/mxrb/initializer.rb +80 -9
  46. data/lib/mxrb/integrity/validator.rb +32 -7
  47. data/lib/mxrb/io/bson_codec.rb +42 -1
  48. data/lib/mxrb/io/mpr_file.rb +146 -3
  49. data/lib/mxrb/model/attribute.rb +3 -0
  50. data/lib/mxrb/model/entity.rb +44 -2
  51. data/lib/mxrb/model/microflow.rb +3 -1
  52. data/lib/mxrb/model/module.rb +71 -0
  53. data/lib/mxrb/model/page.rb +126 -15
  54. data/lib/mxrb/model/project.rb +3 -0
  55. data/lib/mxrb/modeler/catalog.rb +178 -0
  56. data/lib/mxrb/official_marketplace/module_package_importer.rb +23 -12
  57. data/lib/mxrb/official_marketplace/widget_package_installer.rb +15 -11
  58. data/lib/mxrb/official_marketplace.rb +9 -3
  59. data/lib/mxrb/oql/reverse_translator.rb +394 -0
  60. data/lib/mxrb/oql/server.rb +4 -8
  61. data/lib/mxrb/oql.rb +32 -4
  62. data/lib/mxrb/progress.rb +242 -0
  63. data/lib/mxrb/project_lifecycle.rb +5 -2
  64. data/lib/mxrb/ruby_app/exporter.rb +1749 -0
  65. data/lib/mxrb/ruby_app/preset.rb +506 -0
  66. data/lib/mxrb/ruby_app/session_manager.rb +122 -0
  67. data/lib/mxrb/ruby_app.rb +1361 -0
  68. data/lib/mxrb/runtime/access_control.rb +408 -0
  69. data/lib/mxrb/runtime/native.rb +1015 -93
  70. data/lib/mxrb/runtime/scheduler.rb +396 -0
  71. data/lib/mxrb/runtime/schema_migrator.rb +598 -0
  72. data/lib/mxrb/runtime/shared_store.rb +335 -0
  73. data/lib/mxrb/runtime/sqlite_store.rb +729 -0
  74. data/lib/mxrb/scaffold/generator.rb +11 -3
  75. data/lib/mxrb/team_server.rb +10 -7
  76. data/lib/mxrb/uml/activity_diagram.rb +118 -0
  77. data/lib/mxrb/uml/class_diagram.rb +118 -0
  78. data/lib/mxrb/uml/sequence_diagram.rb +112 -0
  79. data/lib/mxrb/uml/server.rb +146 -0
  80. data/lib/mxrb/uml/support.rb +35 -0
  81. data/lib/mxrb/version.rb +1 -1
  82. data/lib/mxrb/web_ui/assets/abnfDiagram-N423BO3Z-C4JD3cEC.js +1 -0
  83. data/lib/mxrb/web_ui/assets/arc-3Z53kgFp.js +1 -0
  84. data/lib/mxrb/web_ui/assets/architecture-TIHT7OUA-BStBTLea.js +1 -0
  85. data/lib/mxrb/web_ui/assets/architectureDiagram-T3A2C74G-DBwqnM4J.js +36 -0
  86. data/lib/mxrb/web_ui/assets/array-BifhSqXX.js +1 -0
  87. data/lib/mxrb/web_ui/assets/blockDiagram-VBNYF7ZC-DLtuHt-j.js +132 -0
  88. data/lib/mxrb/web_ui/assets/c4Diagram-5PPSVZJV-ConSk_pe.js +10 -0
  89. data/lib/mxrb/web_ui/assets/channel-BhPvD7Dt.js +1 -0
  90. data/lib/mxrb/web_ui/assets/chunk-2GRJ4B5K-D2HpWsf_.js +1 -0
  91. data/lib/mxrb/web_ui/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  92. data/lib/mxrb/web_ui/assets/chunk-4I5QYGJK-CiIdNwaF.js +1 -0
  93. data/lib/mxrb/web_ui/assets/chunk-5RXB4S5H-Bu_AaEkE.js +231 -0
  94. data/lib/mxrb/web_ui/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  95. data/lib/mxrb/web_ui/assets/chunk-6Q2QTUOP-wQkNmt_X.js +88 -0
  96. data/lib/mxrb/web_ui/assets/chunk-7BUUIJ7U-Bb538aSH.js +1 -0
  97. data/lib/mxrb/web_ui/assets/chunk-GF5L2VYU-G8MmiBzU.js +206 -0
  98. data/lib/mxrb/web_ui/assets/chunk-I66GZJ75-CulcrLPh.js +127 -0
  99. data/lib/mxrb/web_ui/assets/chunk-JQJVKLGR-ukjzcdlL.js +156 -0
  100. data/lib/mxrb/web_ui/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  101. data/lib/mxrb/web_ui/assets/chunk-KBJHAD2P-Wm35lYov.js +1 -0
  102. data/lib/mxrb/web_ui/assets/chunk-KEIR6QF5-BICK3FdT.js +161 -0
  103. data/lib/mxrb/web_ui/assets/chunk-NSK5VX7P-D2nYrCY4.js +2 -0
  104. data/lib/mxrb/web_ui/assets/chunk-QR6OTTB3-BrdIzg4Q.js +62 -0
  105. data/lib/mxrb/web_ui/assets/chunk-RYQCIY6F-CCfZMcW6.js +1 -0
  106. data/lib/mxrb/web_ui/assets/chunk-UBXNYLIW-BPEVYsFK.js +1 -0
  107. data/lib/mxrb/web_ui/assets/chunk-W5SLKNZC-2E6OPhA4.js +1 -0
  108. data/lib/mxrb/web_ui/assets/chunk-WRU74C26-CBIvOyWp.js +70 -0
  109. data/lib/mxrb/web_ui/assets/chunk-XXDRQBXY-pH58XAyl.js +1 -0
  110. data/lib/mxrb/web_ui/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  111. data/lib/mxrb/web_ui/assets/classDiagram-JCYQIIEL-xiqo4D89.js +1 -0
  112. data/lib/mxrb/web_ui/assets/classDiagram-v2-OCEON4UE-xiqo4D89.js +1 -0
  113. data/lib/mxrb/web_ui/assets/cose-bilkent-JH36ORCC-t_flPpii.js +1 -0
  114. data/lib/mxrb/web_ui/assets/cynefin-VYW2F7L2-BIqktTEv.js +1 -0
  115. data/lib/mxrb/web_ui/assets/cynefinDiagram-MW4NZA55-C2d--lut.js +62 -0
  116. data/lib/mxrb/web_ui/assets/cytoscape.esm-B-NFISlW.js +321 -0
  117. data/lib/mxrb/web_ui/assets/dagre-Buvkdvvj.js +1 -0
  118. data/lib/mxrb/web_ui/assets/dagre-VZM6K2ZE-CSz2_tb5.js +4 -0
  119. data/lib/mxrb/web_ui/assets/defaultLocale-BFoDCU3G.js +1 -0
  120. data/lib/mxrb/web_ui/assets/diagram-7IWD3JNH-DCt7u7V_.js +30 -0
  121. data/lib/mxrb/web_ui/assets/diagram-B4RE2ZJO-DqZBqkMu.js +3 -0
  122. data/lib/mxrb/web_ui/assets/diagram-LBJQPF4R-ClIYbIsI.js +24 -0
  123. data/lib/mxrb/web_ui/assets/diagram-Q27KOJAE-DFxhvthJ.js +24 -0
  124. data/lib/mxrb/web_ui/assets/diagram-UB23O5K3-BSUTXptv.js +41 -0
  125. data/lib/mxrb/web_ui/assets/dist-D2qOEbeJ.js +1 -0
  126. data/lib/mxrb/web_ui/assets/domain-mGOZ1KCG.css +1 -0
  127. data/lib/mxrb/web_ui/assets/domain-qsjXhy4v.js +1 -0
  128. data/lib/mxrb/web_ui/assets/ebnfDiagram-BXEA7PRR-B5reICEi.js +1 -0
  129. data/lib/mxrb/web_ui/assets/erDiagram-JOGREHBK-Co5Mxn2p.js +85 -0
  130. data/lib/mxrb/web_ui/assets/eventmodeling-45OFAUF4-C1ylhSks.js +1 -0
  131. data/lib/mxrb/web_ui/assets/flowDiagram-UKHOOZJN-CHXaJIaS.js +1 -0
  132. data/lib/mxrb/web_ui/assets/ganttDiagram-PKOTCBZU-DPPPaWaz.js +292 -0
  133. data/lib/mxrb/web_ui/assets/gitGraph-TEB2WS4Q-qS77ywA9.js +1 -0
  134. data/lib/mxrb/web_ui/assets/gitGraphDiagram-DS77QQ5N-DpAv6-BT.js +106 -0
  135. data/lib/mxrb/web_ui/assets/graphlib-DS17s2tU.js +1 -0
  136. data/lib/mxrb/web_ui/assets/info-DKCQHKI2-C9VD1Y4e.js +1 -0
  137. data/lib/mxrb/web_ui/assets/infoDiagram-6WML65LV-Bk1kbcCy.js +2 -0
  138. data/lib/mxrb/web_ui/assets/init-C-OQMol4.js +1 -0
  139. data/lib/mxrb/web_ui/assets/ishikawaDiagram-WSZJBQD7-0gcISBFR.js +70 -0
  140. data/lib/mxrb/web_ui/assets/journeyDiagram-NVQOT4AX-Dt0RQnlh.js +139 -0
  141. data/lib/mxrb/web_ui/assets/jsx-runtime-DV0a5kSb.js +9 -0
  142. data/lib/mxrb/web_ui/assets/kanban-definition-27J2QSJJ-DIPQM7w9.js +89 -0
  143. data/lib/mxrb/web_ui/assets/katex-CXMH3UgJ.js +257 -0
  144. data/lib/mxrb/web_ui/assets/line-C0rYD-tL.js +1 -0
  145. data/lib/mxrb/web_ui/assets/linear-BPPxWorj.js +1 -0
  146. data/lib/mxrb/web_ui/assets/map-BaFkSB1l.js +1 -0
  147. data/lib/mxrb/web_ui/assets/mermaid-parser.core-B_UPzTxa.js +7 -0
  148. data/lib/mxrb/web_ui/assets/mindmap-definition-FAOFIHXS-CdJRSMuk.js +96 -0
  149. data/lib/mxrb/web_ui/assets/modeler-BNqHthwk.css +1 -0
  150. data/lib/mxrb/web_ui/assets/modeler-CQmqflbg.js +1 -0
  151. data/lib/mxrb/web_ui/assets/ordinal-BDEzSJ7C.js +1 -0
  152. data/lib/mxrb/web_ui/assets/packet-7NZHBO7P-CwQ4NSBs.js +1 -0
  153. data/lib/mxrb/web_ui/assets/path-fybaL0A-.js +1 -0
  154. data/lib/mxrb/web_ui/assets/pegDiagram-VL7TDLO6-D40S7ppx.js +1 -0
  155. data/lib/mxrb/web_ui/assets/pie-RZYD4A2V-CJroHMKh.js +1 -0
  156. data/lib/mxrb/web_ui/assets/pieDiagram-7S7Q4E2Y-QB54__BV.js +39 -0
  157. data/lib/mxrb/web_ui/assets/quadrantDiagram-CIZ2JOQS-DBhDS6ag.js +7 -0
  158. data/lib/mxrb/web_ui/assets/radar-I7S5WNFK-DdIUwd2d.js +1 -0
  159. data/lib/mxrb/web_ui/assets/railroad-3IZDKUUU-B1Lw6Xyq.js +1 -0
  160. data/lib/mxrb/web_ui/assets/railroad-abnf-AHOZXSZD-CQgQoeak.js +1 -0
  161. data/lib/mxrb/web_ui/assets/railroad-ebnf-EBAXGLYW-D7qEQ2x8.js +1 -0
  162. data/lib/mxrb/web_ui/assets/railroad-peg-LSFZ7HO6-C005Vxbb.js +1 -0
  163. data/lib/mxrb/web_ui/assets/railroadDiagram-AXF67PYL-BBP2qk7M.js +1 -0
  164. data/lib/mxrb/web_ui/assets/requirementDiagram-LRYGKXZP-D2cLsAoI.js +84 -0
  165. data/lib/mxrb/web_ui/assets/rough.esm-Dy-Kn_BL.js +1 -0
  166. data/lib/mxrb/web_ui/assets/sankeyDiagram-W5VNT64P-Bpi3FiP2.js +40 -0
  167. data/lib/mxrb/web_ui/assets/sequenceDiagram-SI44F4Z6-DCRcDlVe.js +162 -0
  168. data/lib/mxrb/web_ui/assets/sizeCapture-X5ZJPWSS-B0uUizjq.js +1 -0
  169. data/lib/mxrb/web_ui/assets/src-CvxfUak2.js +1 -0
  170. data/lib/mxrb/web_ui/assets/stateDiagram-OKZ733FA-C2GCEoxl.js +1 -0
  171. data/lib/mxrb/web_ui/assets/stateDiagram-v2-UEYNNEHI-D2gWgedh.js +1 -0
  172. data/lib/mxrb/web_ui/assets/swimlanes-SLNWSIFB-CRI9irEW.js +2 -0
  173. data/lib/mxrb/web_ui/assets/swimlanesDiagram-ULZ7WXOC-C5Wb_-hZ.js +8 -0
  174. data/lib/mxrb/web_ui/assets/theme-_mxc7BdY.css +1 -0
  175. data/lib/mxrb/web_ui/assets/timeline-definition-Z64GVDOM-B1-mNg-h.js +120 -0
  176. data/lib/mxrb/web_ui/assets/treeView-QDETBFTQ-7CTvIwKq.js +1 -0
  177. data/lib/mxrb/web_ui/assets/treemap-6X3UGDF4-CtL22pq5.js +1 -0
  178. data/lib/mxrb/web_ui/assets/uml-C3KAFGTq.js +51 -0
  179. data/lib/mxrb/web_ui/assets/uml-Dn-qd01X.css +1 -0
  180. data/lib/mxrb/web_ui/assets/vennDiagram-T6HMQDX7-BzVnUEAy.js +34 -0
  181. data/lib/mxrb/web_ui/assets/wardley-OPB4EBWU-JNi-QNag.js +1 -0
  182. data/lib/mxrb/web_ui/assets/wardleyDiagram-T6FBY63Y-BUwmAQ-B.js +78 -0
  183. data/lib/mxrb/web_ui/assets/xychartDiagram-ELKLHX3M-DEQS8sKp.js +7 -0
  184. data/lib/mxrb/web_ui/domain.html +15 -0
  185. data/lib/mxrb/web_ui/modeler.html +16 -0
  186. data/lib/mxrb/web_ui/uml.html +31 -0
  187. data/lib/mxrb/web_ui.rb +89 -0
  188. data/lib/mxrb/widget_certification.rb +208 -0
  189. data/lib/mxrb/writer.rb +278 -41
  190. data/lib/mxrb.rb +38 -8
  191. metadata +157 -5
@@ -0,0 +1,408 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../io/bson_codec'
4
+
5
+ # Public authorization calls intentionally accept complete security contexts,
6
+ # and the XPath interpreter is clearer as small decision-oriented methods.
7
+ # rubocop:disable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity
8
+ # rubocop:disable Metrics/MethodLength, Metrics/ParameterLists, Metrics/PerceivedComplexity
9
+
10
+ module Mxrb
11
+ module Runtime
12
+ class AuthorizationError < StandardError; end
13
+
14
+ # Explicit security context used by the pure-Ruby runtime. User roles are
15
+ # expanded to module roles by AccessControl; callers may also supply module
16
+ # roles directly for system-to-system execution.
17
+ class SecurityContext
18
+ attr_reader :user, :user_roles, :module_roles, :attributes, :variables
19
+
20
+ def initialize(user: nil, roles: [], user_roles: nil, module_roles: [],
21
+ attributes: {}, variables: {})
22
+ @user = user
23
+ @user_roles = strings(user_roles || roles)
24
+ @module_roles = strings(module_roles)
25
+ @attributes = attributes.to_h.freeze
26
+ @variables = variables.to_h.freeze
27
+ end
28
+
29
+ def roles = @user_roles
30
+
31
+ private
32
+
33
+ def strings(values) = Array(values).map(&:to_s).reject(&:empty?).uniq.freeze
34
+ end
35
+
36
+ # Enforces Mendix project, document, entity, and member access rules.
37
+ # Unsupported XPath constructs are never treated as a successful match.
38
+ class AccessControl # rubocop:disable Metrics/ClassLength
39
+ RIGHTS = {
40
+ 'none' => :none,
41
+ 'readonly' => :read,
42
+ 'readwrite' => :write
43
+ }.freeze
44
+ attr_reader :project, :security_level
45
+
46
+ def initialize(project)
47
+ @project = project
48
+ @security = project_security
49
+ @security_level = value(@security, 'SecurityLevel').to_s
50
+ @role_map, @admin_roles = build_role_map
51
+ end
52
+
53
+ def security_enabled?
54
+ !@security.nil? && !%w[CheckNothing Off None].include?(@security_level)
55
+ end
56
+
57
+ def context(user: nil, roles: [], user_roles: nil, module_roles: [],
58
+ attributes: {}, variables: {})
59
+ supplied = SecurityContext.new(
60
+ user:, roles:, user_roles:, module_roles:, attributes:, variables:
61
+ )
62
+ expanded = supplied.user_roles.flat_map { @role_map.fetch(_1, []) }
63
+ SecurityContext.new(
64
+ user: supplied.user, user_roles: supplied.user_roles,
65
+ module_roles: supplied.module_roles + expanded,
66
+ attributes: supplied.attributes, variables: supplied.variables
67
+ )
68
+ end
69
+
70
+ def admin?(context)
71
+ !(normalized_context(context).user_roles & @admin_roles).empty?
72
+ end
73
+
74
+ def microflow_allowed?(microflow, context:)
75
+ document_allowed?(:microflow, microflow, context:)
76
+ end
77
+
78
+ def page_allowed?(page, context:)
79
+ document_allowed?(:page, page, context:)
80
+ end
81
+
82
+ def entity_allowed?(entity, action:, context:, member: nil, record: nil)
83
+ entity = resolve(:entity, entity)
84
+ return false unless entity
85
+
86
+ ctx = normalized_context(context)
87
+ return true unless security_enabled?
88
+ return true if admin?(ctx)
89
+
90
+ rules = applicable_rules(entity, ctx, record:)
91
+ return false if rules.empty?
92
+
93
+ entity_action_allowed?(rules, action, member)
94
+ end
95
+
96
+ def member_allowed?(entity, member, action:, context:, record: nil)
97
+ entity_allowed?(entity, action:, context:, member:, record:)
98
+ end
99
+
100
+ def authorized?(resource, action:, context:, kind: nil, member: nil, record: nil)
101
+ inferred_kind = kind || resource_kind(resource)
102
+ case inferred_kind&.to_sym
103
+ when :microflow then microflow_allowed?(resource, context:)
104
+ when :page then page_allowed?(resource, context:)
105
+ when :entity
106
+ entity_allowed?(resource, action:, context:, member:, record:)
107
+ else
108
+ false
109
+ end
110
+ end
111
+
112
+ def authorize!(resource, action:, context:, kind: nil, member: nil, record: nil)
113
+ return true if authorized?(resource, action:, context:, kind:, member:, record:)
114
+
115
+ name = resource.respond_to?(:name) ? resource.name : resource
116
+ raise AuthorizationError, "not authorized to #{action} #{name}"
117
+ end
118
+
119
+ # Returns the constraints contributed by roles in the context. An empty
120
+ # string is a valid unconstrained rule and therefore remains in the list.
121
+ def xpath_constraints(entity, context:)
122
+ entity = resolve(:entity, entity)
123
+ return [] unless entity
124
+
125
+ applicable_rules(entity, normalized_context(context), evaluate_xpath: false)
126
+ .map { value(_1, :xpath, 'XPathConstraint').to_s }.uniq.freeze
127
+ end
128
+
129
+ def filter_readable(entity, records, context:)
130
+ Array(records).select do |record|
131
+ entity_allowed?(entity, action: :read, context:, record:)
132
+ end
133
+ end
134
+
135
+ # Evaluates the deliberately small, safe XPath subset understood by the
136
+ # runtime. nil means that the expression is unsupported.
137
+ def evaluate_xpath(expression, record:, context:)
138
+ source = expression.to_s.strip
139
+ return true if source.empty?
140
+
141
+ source = source[1...-1].strip if source.start_with?('[') && source.end_with?(']')
142
+ evaluate_boolean(source, record, normalized_context(context))
143
+ end
144
+
145
+ private
146
+
147
+ def normalized_context(context)
148
+ if context.is_a?(Hash)
149
+ keys = context.to_h { |key, item| [key.to_sym, item] }
150
+ return self.context(**keys)
151
+ end
152
+ return context unless context.is_a?(SecurityContext)
153
+
154
+ expanded = context.user_roles.flat_map { @role_map.fetch(_1, []) }
155
+ return context if (expanded - context.module_roles).empty?
156
+
157
+ SecurityContext.new(
158
+ user: context.user, user_roles: context.user_roles,
159
+ module_roles: context.module_roles + expanded,
160
+ attributes: context.attributes, variables: context.variables
161
+ )
162
+ end
163
+
164
+ def project_security
165
+ raw = @project.all_units.find do |unit|
166
+ value(@project.parse_bson(unit), '$Type') == 'Security$ProjectSecurity'
167
+ rescue StandardError
168
+ false
169
+ end
170
+ raw && @project.parse_bson(raw)
171
+ rescue NoMethodError
172
+ nil
173
+ end
174
+
175
+ def build_role_map
176
+ roles = parse_array(value(@security, 'UserRoles'))
177
+ map = roles.to_h do |role|
178
+ [value(role, 'Name').to_s, parse_array(value(role, 'ModuleRoles')).map(&:to_s)]
179
+ end
180
+ admins = roles.filter_map do |role|
181
+ name = value(role, 'Name').to_s
182
+ configured = name == value(@security, 'AdminUserRole').to_s
183
+ name if configured || value(role, 'ManageAllRoles') == true
184
+ end
185
+ [map.freeze, admins.freeze]
186
+ end
187
+
188
+ def document_allowed?(kind, resource, context:)
189
+ artifact = resolve(kind, resource)
190
+ return false unless artifact
191
+
192
+ ctx = normalized_context(context)
193
+ return true unless security_enabled?
194
+ return true if admin?(ctx)
195
+
196
+ allowed = Array(artifact.allowed_module_roles).map(&:to_s)
197
+ !allowed.empty? && !(allowed & ctx.module_roles).empty?
198
+ end
199
+
200
+ def applicable_rules(entity, context, record: nil, evaluate_xpath: true)
201
+ Array(entity.access_rules).select do |rule|
202
+ roles = Array(value(rule, :roles, 'AllowedModuleRoles', 'ModuleRoles')).map(&:to_s)
203
+ next false if roles.empty? || (roles & context.module_roles).empty?
204
+ next true unless evaluate_xpath && record
205
+
206
+ evaluate_xpath(value(rule, :xpath, 'XPathConstraint'), record:, context:) == true
207
+ end
208
+ end
209
+
210
+ def entity_action_allowed?(rules, action, member)
211
+ case action.to_sym
212
+ when :create then rules.any? { value(_1, :create, 'AllowCreate') == true }
213
+ when :delete then rules.any? { value(_1, :delete, 'AllowDelete') == true }
214
+ when :read, :retrieve
215
+ member ? rules.any? { member_right(_1, member) != :none } : true
216
+ when :write, :update
217
+ if member
218
+ rules.any? { member_right(_1, member) == :write }
219
+ else
220
+ rules.any? { writable_rule?(_1) }
221
+ end
222
+ else false
223
+ end
224
+ end
225
+
226
+ def writable_rule?(rule)
227
+ return true if right(value(rule, :default_rights, 'DefaultMemberAccessRights')) == :write
228
+
229
+ Array(value(rule, :members, 'MemberAccesses')).any? do |member|
230
+ right(value(member, :rights, 'AccessRights')) == :write
231
+ end
232
+ end
233
+
234
+ def member_right(rule, member_name)
235
+ name = member_name.respond_to?(:name) ? member_name.name.to_s : member_name.to_s
236
+ override = Array(value(rule, :members, 'MemberAccesses')).find do |entry|
237
+ reference = value(entry, :name, :reference, 'Attribute', 'Association').to_s
238
+ reference == name || reference.split(%r{[/.]}).last == name
239
+ end
240
+ right(override ? value(override, :rights,
241
+ 'AccessRights') : value(rule, :default_rights, 'DefaultMemberAccessRights'))
242
+ end
243
+
244
+ def right(raw) = RIGHTS.fetch(raw.to_s.downcase, :none)
245
+
246
+ def resource_kind(resource)
247
+ name = resource.class.name.to_s
248
+ return :microflow if name.end_with?('Microflow')
249
+ return :page if name.end_with?('Page')
250
+ return :entity if name.end_with?('Entity')
251
+
252
+ nil
253
+ end
254
+
255
+ def resolve(kind, resource)
256
+ return resource unless resource.is_a?(String) || resource.is_a?(Symbol)
257
+
258
+ name = resource.to_s
259
+ matches = resource_catalog(kind).select do |qualified, artifact|
260
+ qualified == name || artifact.name.to_s == name
261
+ end
262
+ matches.size == 1 ? matches.first.last : nil
263
+ end
264
+
265
+ def resource_catalog(kind)
266
+ @resource_catalog ||= {}
267
+ @resource_catalog[kind] ||= @project.modules.flat_map do |mod|
268
+ collection = kind == :entity ? mod.entities : mod.public_send("#{kind}s")
269
+ collection.map do |artifact|
270
+ qualified = if artifact.respond_to?(:qualified_name) && artifact.qualified_name
271
+ artifact.qualified_name.to_s
272
+ else
273
+ "#{mod.name}.#{artifact.name}"
274
+ end
275
+ [qualified, artifact]
276
+ end
277
+ end
278
+ end
279
+
280
+ def evaluate_boolean(source, record, context)
281
+ expression = strip_parentheses(source.strip)
282
+ parts = split_logical(expression, 'or')
283
+ return combine(parts, record, context, :or) if parts.size > 1
284
+
285
+ parts = split_logical(expression, 'and')
286
+ return combine(parts, record, context, :and) if parts.size > 1
287
+
288
+ return true if expression.match?(/\Atrue\(\)\z/i)
289
+ return false if expression.match?(/\Afalse\(\)\z/i)
290
+
291
+ if (match = expression.match(/\Anot\s*\((.*)\)\z/im))
292
+ result = evaluate_boolean(match[1], record, context)
293
+ return result.nil? ? nil : !result
294
+ end
295
+
296
+ comparison(expression, record, context)
297
+ end
298
+
299
+ def combine(parts, record, context, operator)
300
+ values = parts.map { evaluate_boolean(_1, record, context) }
301
+ return values.any?(true) if operator == :or && values.none?(&:nil?)
302
+ return values.all?(true) if operator == :and && values.none?(&:nil?)
303
+
304
+ nil
305
+ end
306
+
307
+ def comparison(expression, record, context)
308
+ match = expression.match(/\A(.+?)\s*(>=|<=|!=|=|>|<)\s*(.+)\z/m)
309
+ return nil unless match
310
+
311
+ left = operand(match[1], record, context, record_side: true)
312
+ right = operand(match[3], record, context, record_side: false)
313
+ return nil if left.equal?(:unsupported) || right.equal?(:unsupported)
314
+
315
+ case match[2]
316
+ when '=' then left == right
317
+ when '!=' then left != right
318
+ when '>' then left && right && left > right
319
+ when '<' then left && right && left < right
320
+ when '>=' then left && right && left >= right
321
+ when '<=' then left && right && left <= right
322
+ end
323
+ rescue ArgumentError
324
+ false
325
+ end
326
+
327
+ def operand(source, record, context, record_side:)
328
+ token = source.strip
329
+ return token[1...-1].gsub("''", "'") if token.start_with?("'") && token.end_with?("'")
330
+ return true if token.match?(/\Atrue\(\)\z/i)
331
+ return false if token.match?(/\Afalse\(\)\z/i)
332
+ return nil if token.match?(/\A(empty|null)\z/i)
333
+ return token.to_f if token.match?(/\A-?\d+\.\d+\z/)
334
+ return token.to_i if token.match?(/\A-?\d+\z/)
335
+ return user_identity(context.user) if token == '[%CurrentUser%]'
336
+ return variable(token.delete_prefix('$'), context) if token.start_with?('$')
337
+ return lookup(record, token) if record_side
338
+
339
+ :unsupported
340
+ end
341
+
342
+ def variable(name, context)
343
+ return user_identity(context.user) if name.match?(/\Acurrentuser\z/i)
344
+
345
+ found, result = lookup_with_presence(context.variables, name)
346
+ found ? result : :unsupported
347
+ end
348
+
349
+ def user_identity(user)
350
+ return nil if user.nil?
351
+
352
+ if user.respond_to?(:key?)
353
+ return user['id'] if user.key?('id')
354
+ return user[:id] if user.key?(:id)
355
+ return user['name'] if user.key?('name')
356
+ return user[:name] if user.key?(:name)
357
+ end
358
+ return user.id if user.respond_to?(:id)
359
+
360
+ user
361
+ end
362
+
363
+ def lookup(record, path)
364
+ found, result = lookup_with_presence(record, path)
365
+ found ? result : :unsupported
366
+ end
367
+
368
+ def lookup_with_presence(object, path)
369
+ object = object.members if object.respond_to?(:members)
370
+ candidates = [path, path.split(%r{[/.]}).last].uniq
371
+ candidates.each do |candidate|
372
+ if object.respond_to?(:key?)
373
+ return [true, object[candidate]] if object.key?(candidate)
374
+
375
+ symbol = candidate.to_sym
376
+ return [true, object[symbol]] if object.key?(symbol)
377
+ end
378
+ return [true, object.public_send(candidate)] if object.respond_to?(candidate)
379
+ end
380
+ [false, nil]
381
+ end
382
+
383
+ def split_logical(source, operator)
384
+ source.split(/\s+#{operator}\s+/i)
385
+ end
386
+
387
+ def strip_parentheses(source)
388
+ source = source[1...-1].strip while source.start_with?('(') && source.end_with?(')')
389
+ source
390
+ end
391
+
392
+ def parse_array(raw)
393
+ Mxrb::IO::BsonCodec.parse_array(raw)[:items]
394
+ rescue TypeError
395
+ Array(raw)
396
+ end
397
+
398
+ def value(hash, *keys)
399
+ return nil unless hash.respond_to?(:key?)
400
+
401
+ keys.each { return hash[_1] if hash.key?(_1) }
402
+ nil
403
+ end
404
+ end
405
+ end
406
+ end
407
+ # rubocop:enable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity
408
+ # rubocop:enable Metrics/MethodLength, Metrics/ParameterLists, Metrics/PerceivedComplexity