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.
- checksums.yaml +4 -4
- data/bin/mxrb +458 -10
- data/docs/de-DE/README.md +1 -0
- data/docs/de-DE/oql-sql.md +42 -0
- data/docs/de-DE/project-structure.md +201 -2
- data/docs/de-DE/runtime-ruby.md +104 -0
- data/docs/de-DE/runtime-schema-migrations.md +44 -0
- data/docs/de-DE/validation-matrix.md +42 -4
- data/docs/en-US/README.md +1 -0
- data/docs/en-US/oql-sql.md +41 -0
- data/docs/en-US/project-structure.md +192 -2
- data/docs/en-US/runtime-ruby.md +100 -0
- data/docs/en-US/runtime-schema-migrations.md +41 -0
- data/docs/en-US/validation-matrix.md +39 -4
- data/docs/pt-BR/README.md +1 -0
- data/docs/pt-BR/entity-dsl.md +8 -0
- data/docs/pt-BR/oql-sql.md +41 -0
- data/docs/pt-BR/project-structure.md +197 -2
- data/docs/pt-BR/runtime-ruby.md +108 -0
- data/docs/pt-BR/runtime-schema-migrations.md +40 -0
- data/docs/pt-BR/validation-matrix.md +42 -5
- data/docs/pt-BR/writing.md +28 -0
- data/lib/mxrb/cli/help.rb +426 -0
- data/lib/mxrb/cli/release.rb +137 -0
- data/lib/mxrb/compiler/compatibility_analyzer.rb +1 -1
- data/lib/mxrb/compiler/data_grid_bundle_compiler.rb +8 -1
- data/lib/mxrb/compiler/domain_document_compiler.rb +3 -0
- data/lib/mxrb/compiler/gallery_bundle_compiler.rb +11 -1
- data/lib/mxrb/compiler/generic_widget_bundle_compiler.rb +76 -19
- data/lib/mxrb/compiler/legacy_custom_widget_compiler.rb +159 -0
- data/lib/mxrb/compiler/legacy_data_grid_compiler.rb +43 -7
- data/lib/mxrb/compiler/legacy_page_builder.rb +1248 -23
- data/lib/mxrb/compiler/packager.rb +27 -13
- data/lib/mxrb/compiler/page_bundle_compiler.rb +13 -4
- data/lib/mxrb/compiler/portable_packager.rb +16 -11
- data/lib/mxrb/compiler/web_bundle_builder.rb +5 -1
- data/lib/mxrb/compiler/web_shell_materializer.rb +42 -2
- data/lib/mxrb/compiler/widget_package_extractor.rb +3 -0
- data/lib/mxrb/domain_diagram/lifecycle.rb +272 -0
- data/lib/mxrb/domain_diagram.rb +409 -0
- data/lib/mxrb/dsl/builder.rb +62 -11
- data/lib/mxrb/environment.rb +226 -0
- data/lib/mxrb/exporter.rb +265 -45
- data/lib/mxrb/http/server.rb +107 -0
- data/lib/mxrb/initializer.rb +80 -9
- data/lib/mxrb/integrity/validator.rb +32 -7
- data/lib/mxrb/io/bson_codec.rb +42 -1
- data/lib/mxrb/io/mpr_file.rb +146 -3
- data/lib/mxrb/model/attribute.rb +3 -0
- data/lib/mxrb/model/entity.rb +44 -2
- data/lib/mxrb/model/microflow.rb +3 -1
- data/lib/mxrb/model/module.rb +71 -0
- data/lib/mxrb/model/page.rb +126 -15
- data/lib/mxrb/model/project.rb +3 -0
- data/lib/mxrb/modeler/catalog.rb +178 -0
- data/lib/mxrb/official_marketplace/module_package_importer.rb +23 -12
- data/lib/mxrb/official_marketplace/widget_package_installer.rb +15 -11
- data/lib/mxrb/official_marketplace.rb +9 -3
- data/lib/mxrb/oql/reverse_translator.rb +394 -0
- data/lib/mxrb/oql/server.rb +4 -8
- data/lib/mxrb/oql.rb +32 -4
- data/lib/mxrb/progress.rb +242 -0
- data/lib/mxrb/project_lifecycle.rb +5 -2
- data/lib/mxrb/ruby_app/exporter.rb +1749 -0
- data/lib/mxrb/ruby_app/preset.rb +506 -0
- data/lib/mxrb/ruby_app/session_manager.rb +122 -0
- data/lib/mxrb/ruby_app.rb +1361 -0
- data/lib/mxrb/runtime/access_control.rb +408 -0
- data/lib/mxrb/runtime/native.rb +1015 -93
- data/lib/mxrb/runtime/scheduler.rb +396 -0
- data/lib/mxrb/runtime/schema_migrator.rb +598 -0
- data/lib/mxrb/runtime/shared_store.rb +335 -0
- data/lib/mxrb/runtime/sqlite_store.rb +729 -0
- data/lib/mxrb/scaffold/generator.rb +11 -3
- data/lib/mxrb/team_server.rb +10 -7
- data/lib/mxrb/uml/activity_diagram.rb +118 -0
- data/lib/mxrb/uml/class_diagram.rb +118 -0
- data/lib/mxrb/uml/sequence_diagram.rb +112 -0
- data/lib/mxrb/uml/server.rb +146 -0
- data/lib/mxrb/uml/support.rb +35 -0
- data/lib/mxrb/version.rb +1 -1
- data/lib/mxrb/web_ui/assets/abnfDiagram-N423BO3Z-C4JD3cEC.js +1 -0
- data/lib/mxrb/web_ui/assets/arc-3Z53kgFp.js +1 -0
- data/lib/mxrb/web_ui/assets/architecture-TIHT7OUA-BStBTLea.js +1 -0
- data/lib/mxrb/web_ui/assets/architectureDiagram-T3A2C74G-DBwqnM4J.js +36 -0
- data/lib/mxrb/web_ui/assets/array-BifhSqXX.js +1 -0
- data/lib/mxrb/web_ui/assets/blockDiagram-VBNYF7ZC-DLtuHt-j.js +132 -0
- data/lib/mxrb/web_ui/assets/c4Diagram-5PPSVZJV-ConSk_pe.js +10 -0
- data/lib/mxrb/web_ui/assets/channel-BhPvD7Dt.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-2GRJ4B5K-D2HpWsf_.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-4I5QYGJK-CiIdNwaF.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-5RXB4S5H-Bu_AaEkE.js +231 -0
- data/lib/mxrb/web_ui/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- data/lib/mxrb/web_ui/assets/chunk-6Q2QTUOP-wQkNmt_X.js +88 -0
- data/lib/mxrb/web_ui/assets/chunk-7BUUIJ7U-Bb538aSH.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-GF5L2VYU-G8MmiBzU.js +206 -0
- data/lib/mxrb/web_ui/assets/chunk-I66GZJ75-CulcrLPh.js +127 -0
- data/lib/mxrb/web_ui/assets/chunk-JQJVKLGR-ukjzcdlL.js +156 -0
- data/lib/mxrb/web_ui/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-KBJHAD2P-Wm35lYov.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-KEIR6QF5-BICK3FdT.js +161 -0
- data/lib/mxrb/web_ui/assets/chunk-NSK5VX7P-D2nYrCY4.js +2 -0
- data/lib/mxrb/web_ui/assets/chunk-QR6OTTB3-BrdIzg4Q.js +62 -0
- data/lib/mxrb/web_ui/assets/chunk-RYQCIY6F-CCfZMcW6.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-UBXNYLIW-BPEVYsFK.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-W5SLKNZC-2E6OPhA4.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-WRU74C26-CBIvOyWp.js +70 -0
- data/lib/mxrb/web_ui/assets/chunk-XXDRQBXY-pH58XAyl.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- data/lib/mxrb/web_ui/assets/classDiagram-JCYQIIEL-xiqo4D89.js +1 -0
- data/lib/mxrb/web_ui/assets/classDiagram-v2-OCEON4UE-xiqo4D89.js +1 -0
- data/lib/mxrb/web_ui/assets/cose-bilkent-JH36ORCC-t_flPpii.js +1 -0
- data/lib/mxrb/web_ui/assets/cynefin-VYW2F7L2-BIqktTEv.js +1 -0
- data/lib/mxrb/web_ui/assets/cynefinDiagram-MW4NZA55-C2d--lut.js +62 -0
- data/lib/mxrb/web_ui/assets/cytoscape.esm-B-NFISlW.js +321 -0
- data/lib/mxrb/web_ui/assets/dagre-Buvkdvvj.js +1 -0
- data/lib/mxrb/web_ui/assets/dagre-VZM6K2ZE-CSz2_tb5.js +4 -0
- data/lib/mxrb/web_ui/assets/defaultLocale-BFoDCU3G.js +1 -0
- data/lib/mxrb/web_ui/assets/diagram-7IWD3JNH-DCt7u7V_.js +30 -0
- data/lib/mxrb/web_ui/assets/diagram-B4RE2ZJO-DqZBqkMu.js +3 -0
- data/lib/mxrb/web_ui/assets/diagram-LBJQPF4R-ClIYbIsI.js +24 -0
- data/lib/mxrb/web_ui/assets/diagram-Q27KOJAE-DFxhvthJ.js +24 -0
- data/lib/mxrb/web_ui/assets/diagram-UB23O5K3-BSUTXptv.js +41 -0
- data/lib/mxrb/web_ui/assets/dist-D2qOEbeJ.js +1 -0
- data/lib/mxrb/web_ui/assets/domain-mGOZ1KCG.css +1 -0
- data/lib/mxrb/web_ui/assets/domain-qsjXhy4v.js +1 -0
- data/lib/mxrb/web_ui/assets/ebnfDiagram-BXEA7PRR-B5reICEi.js +1 -0
- data/lib/mxrb/web_ui/assets/erDiagram-JOGREHBK-Co5Mxn2p.js +85 -0
- data/lib/mxrb/web_ui/assets/eventmodeling-45OFAUF4-C1ylhSks.js +1 -0
- data/lib/mxrb/web_ui/assets/flowDiagram-UKHOOZJN-CHXaJIaS.js +1 -0
- data/lib/mxrb/web_ui/assets/ganttDiagram-PKOTCBZU-DPPPaWaz.js +292 -0
- data/lib/mxrb/web_ui/assets/gitGraph-TEB2WS4Q-qS77ywA9.js +1 -0
- data/lib/mxrb/web_ui/assets/gitGraphDiagram-DS77QQ5N-DpAv6-BT.js +106 -0
- data/lib/mxrb/web_ui/assets/graphlib-DS17s2tU.js +1 -0
- data/lib/mxrb/web_ui/assets/info-DKCQHKI2-C9VD1Y4e.js +1 -0
- data/lib/mxrb/web_ui/assets/infoDiagram-6WML65LV-Bk1kbcCy.js +2 -0
- data/lib/mxrb/web_ui/assets/init-C-OQMol4.js +1 -0
- data/lib/mxrb/web_ui/assets/ishikawaDiagram-WSZJBQD7-0gcISBFR.js +70 -0
- data/lib/mxrb/web_ui/assets/journeyDiagram-NVQOT4AX-Dt0RQnlh.js +139 -0
- data/lib/mxrb/web_ui/assets/jsx-runtime-DV0a5kSb.js +9 -0
- data/lib/mxrb/web_ui/assets/kanban-definition-27J2QSJJ-DIPQM7w9.js +89 -0
- data/lib/mxrb/web_ui/assets/katex-CXMH3UgJ.js +257 -0
- data/lib/mxrb/web_ui/assets/line-C0rYD-tL.js +1 -0
- data/lib/mxrb/web_ui/assets/linear-BPPxWorj.js +1 -0
- data/lib/mxrb/web_ui/assets/map-BaFkSB1l.js +1 -0
- data/lib/mxrb/web_ui/assets/mermaid-parser.core-B_UPzTxa.js +7 -0
- data/lib/mxrb/web_ui/assets/mindmap-definition-FAOFIHXS-CdJRSMuk.js +96 -0
- data/lib/mxrb/web_ui/assets/modeler-BNqHthwk.css +1 -0
- data/lib/mxrb/web_ui/assets/modeler-CQmqflbg.js +1 -0
- data/lib/mxrb/web_ui/assets/ordinal-BDEzSJ7C.js +1 -0
- data/lib/mxrb/web_ui/assets/packet-7NZHBO7P-CwQ4NSBs.js +1 -0
- data/lib/mxrb/web_ui/assets/path-fybaL0A-.js +1 -0
- data/lib/mxrb/web_ui/assets/pegDiagram-VL7TDLO6-D40S7ppx.js +1 -0
- data/lib/mxrb/web_ui/assets/pie-RZYD4A2V-CJroHMKh.js +1 -0
- data/lib/mxrb/web_ui/assets/pieDiagram-7S7Q4E2Y-QB54__BV.js +39 -0
- data/lib/mxrb/web_ui/assets/quadrantDiagram-CIZ2JOQS-DBhDS6ag.js +7 -0
- data/lib/mxrb/web_ui/assets/radar-I7S5WNFK-DdIUwd2d.js +1 -0
- data/lib/mxrb/web_ui/assets/railroad-3IZDKUUU-B1Lw6Xyq.js +1 -0
- data/lib/mxrb/web_ui/assets/railroad-abnf-AHOZXSZD-CQgQoeak.js +1 -0
- data/lib/mxrb/web_ui/assets/railroad-ebnf-EBAXGLYW-D7qEQ2x8.js +1 -0
- data/lib/mxrb/web_ui/assets/railroad-peg-LSFZ7HO6-C005Vxbb.js +1 -0
- data/lib/mxrb/web_ui/assets/railroadDiagram-AXF67PYL-BBP2qk7M.js +1 -0
- data/lib/mxrb/web_ui/assets/requirementDiagram-LRYGKXZP-D2cLsAoI.js +84 -0
- data/lib/mxrb/web_ui/assets/rough.esm-Dy-Kn_BL.js +1 -0
- data/lib/mxrb/web_ui/assets/sankeyDiagram-W5VNT64P-Bpi3FiP2.js +40 -0
- data/lib/mxrb/web_ui/assets/sequenceDiagram-SI44F4Z6-DCRcDlVe.js +162 -0
- data/lib/mxrb/web_ui/assets/sizeCapture-X5ZJPWSS-B0uUizjq.js +1 -0
- data/lib/mxrb/web_ui/assets/src-CvxfUak2.js +1 -0
- data/lib/mxrb/web_ui/assets/stateDiagram-OKZ733FA-C2GCEoxl.js +1 -0
- data/lib/mxrb/web_ui/assets/stateDiagram-v2-UEYNNEHI-D2gWgedh.js +1 -0
- data/lib/mxrb/web_ui/assets/swimlanes-SLNWSIFB-CRI9irEW.js +2 -0
- data/lib/mxrb/web_ui/assets/swimlanesDiagram-ULZ7WXOC-C5Wb_-hZ.js +8 -0
- data/lib/mxrb/web_ui/assets/theme-_mxc7BdY.css +1 -0
- data/lib/mxrb/web_ui/assets/timeline-definition-Z64GVDOM-B1-mNg-h.js +120 -0
- data/lib/mxrb/web_ui/assets/treeView-QDETBFTQ-7CTvIwKq.js +1 -0
- data/lib/mxrb/web_ui/assets/treemap-6X3UGDF4-CtL22pq5.js +1 -0
- data/lib/mxrb/web_ui/assets/uml-C3KAFGTq.js +51 -0
- data/lib/mxrb/web_ui/assets/uml-Dn-qd01X.css +1 -0
- data/lib/mxrb/web_ui/assets/vennDiagram-T6HMQDX7-BzVnUEAy.js +34 -0
- data/lib/mxrb/web_ui/assets/wardley-OPB4EBWU-JNi-QNag.js +1 -0
- data/lib/mxrb/web_ui/assets/wardleyDiagram-T6FBY63Y-BUwmAQ-B.js +78 -0
- data/lib/mxrb/web_ui/assets/xychartDiagram-ELKLHX3M-DEQS8sKp.js +7 -0
- data/lib/mxrb/web_ui/domain.html +15 -0
- data/lib/mxrb/web_ui/modeler.html +16 -0
- data/lib/mxrb/web_ui/uml.html +31 -0
- data/lib/mxrb/web_ui.rb +89 -0
- data/lib/mxrb/widget_certification.rb +208 -0
- data/lib/mxrb/writer.rb +278 -41
- data/lib/mxrb.rb +38 -8
- 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
|