mecha_tenant 0.1.0

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.
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module MechaTenant
6
+ module Controller
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ if respond_to?(:helper_method)
11
+ helper_method :tenant_scope
12
+ end
13
+ end
14
+
15
+ class_methods do
16
+ def skip_tenant_enforcement!(only: nil, except: nil, reason:)
17
+ if reason.nil? || !reason.is_a?(String) || reason.strip.empty?
18
+ raise ArgumentError, "skip_tenant_enforcement! requires a non-empty 'reason:' string"
19
+ end
20
+
21
+ @mecha_tenant_skip_rules ||= []
22
+ rule = {
23
+ only: only ? Array(only).map(&:to_s) : nil,
24
+ except: except ? Array(except).map(&:to_s) : nil,
25
+ reason: reason.strip
26
+ }.freeze
27
+ @mecha_tenant_skip_rules << rule
28
+ end
29
+
30
+ def own_tenant_enforcement_rules
31
+ @mecha_tenant_skip_rules || []
32
+ end
33
+
34
+ def tenant_enforcement_rules
35
+ inherited = if superclass.respond_to?(:tenant_enforcement_rules)
36
+ superclass.tenant_enforcement_rules
37
+ else
38
+ []
39
+ end
40
+ inherited + own_tenant_enforcement_rules
41
+ end
42
+
43
+ def reset_tenant_enforcement_rules!
44
+ @mecha_tenant_skip_rules = []
45
+ end
46
+
47
+ def tenant_enforcement_skipped_for?(action_name)
48
+ !tenant_enforcement_reason_for(action_name).nil?
49
+ end
50
+
51
+ def tenant_enforcement_reason_for(action_name)
52
+ act = action_name.to_s
53
+ tenant_enforcement_rules.reverse_each do |rule|
54
+ if rule[:only]
55
+ return rule[:reason] if rule[:only].include?(act)
56
+ elsif rule[:except]
57
+ return rule[:reason] unless rule[:except].include?(act)
58
+ else
59
+ return rule[:reason]
60
+ end
61
+ end
62
+ nil
63
+ end
64
+ end
65
+
66
+ def tenant_scope(model_class)
67
+ tenant_method = MechaTenant.configuration.tenant_method
68
+ tenant = respond_to?(tenant_method, true) ? send(tenant_method) : nil
69
+ scope = MechaTenant.registry.scope_for(model_class, tenant)
70
+ @_mecha_tenant_scoped_models ||= []
71
+ @_mecha_tenant_scoped_models << model_class.to_s
72
+ scope
73
+ end
74
+
75
+ def mecha_tenant_scoped_models
76
+ @_mecha_tenant_scoped_models || []
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MechaTenant
4
+ module Errors
5
+ class Error < StandardError; end
6
+ class ConfigurationError < Error; end
7
+ class UnregisteredModelError < Error; end
8
+ class GlobalModelScopeError < Error; end
9
+ class TenantMissingError < Error; end
10
+ class UnscopedQueryError < Error; end
11
+ end
12
+
13
+ Error = Errors::Error
14
+ ConfigurationError = Errors::ConfigurationError
15
+ UnregisteredModelError = Errors::UnregisteredModelError
16
+ GlobalModelScopeError = Errors::GlobalModelScopeError
17
+ TenantMissingError = Errors::TenantMissingError
18
+ UnscopedQueryError = Errors::UnscopedQueryError
19
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ unless defined?(ActionDispatch::IntegrationTest)
4
+ app_test_helper = File.join(Dir.pwd, "test/test_helper.rb")
5
+ require app_test_helper if File.exist?(app_test_helper)
6
+ end
7
+ require "mecha_tenant"
8
+
9
+ module MechaTenant
10
+ class MatrixTest < ActionDispatch::IntegrationTest
11
+ setup do
12
+ if MechaTenant.configuration.test_setup
13
+ instance_exec(&MechaTenant.configuration.test_setup)
14
+ end
15
+ end
16
+
17
+ teardown do
18
+ if MechaTenant.configuration.test_teardown
19
+ instance_exec(&MechaTenant.configuration.test_teardown)
20
+ end
21
+ end
22
+
23
+ test "static model registry and controller AST audit passes with zero violations" do
24
+ result = MechaTenant::Auditor::Static.new.audit
25
+
26
+ assert_empty result.unmapped_models, "Found unmapped models missing from config/mecha_tenant.yml: #{result.unmapped_models.inspect}"
27
+ assert_empty result.dead_models, "Found dead models in config/mecha_tenant.yml: #{result.dead_models.inspect}"
28
+ assert_empty result.missing_scopes, "Found missing/invalid scopes: #{result.missing_scopes.inspect}"
29
+ assert_empty result.dead_skip_rules, "Found dead skip_tenant_enforcement! rules: #{result.dead_skip_rules.inspect}"
30
+ assert_empty result.violations, "Found controller AST tenant violations:\n" +
31
+ result.violations.map { |v| " - #{v.file}:#{v.line} [#{v.controller}##{v.method_name}] #{v.message}" }.join("\n")
32
+ assert result.success?
33
+ end
34
+
35
+ test "static AST auditor deterministically catches naked model queries and invalid tenant_scope calls" do
36
+ auditor = MechaTenant::Auditor::Static.new
37
+ tenant_model = MechaTenant.registry.tenant_models.keys.first
38
+ global_model = MechaTenant.registry.global_models.keys.first
39
+ return unless tenant_model && global_model
40
+
41
+ bad_source = <<~RUBY
42
+ class VulnerableController < ApplicationController
43
+ def show
44
+ @msg = #{tenant_model}.find(params[:id])
45
+ @opt = tenant_scope(#{global_model}).first
46
+ @ok_const = #{tenant_model}::SOME_CONSTANT
47
+ @ok_scoped = tenant_scope(#{tenant_model}).find(params[:id])
48
+ end
49
+ end
50
+ RUBY
51
+
52
+ dummy_class = Class.new(ApplicationController) do
53
+ def self.name
54
+ "VulnerableController"
55
+ end
56
+
57
+ def show; end
58
+ end
59
+
60
+ res = auditor.audit_source(bad_source, controller_class: dummy_class, file_path: "app/controllers/vulnerable_controller.rb")
61
+ violations = res[:violations]
62
+
63
+ assert_equal 2, violations.size
64
+ assert_equal :naked_model_reference, violations[0].kind
65
+ assert_equal tenant_model, violations[0].model
66
+ assert_equal 3, violations[0].line
67
+
68
+ assert_equal :global_model_in_tenant_scope, violations[1].kind
69
+ assert_equal global_model, violations[1].model
70
+ assert_equal 4, violations[1].line
71
+ end
72
+
73
+ def self.define_tenant_matrix_tests!
74
+ return if @tenant_matrix_tests_defined
75
+ @tenant_matrix_tests_defined = true
76
+
77
+ # 1. One test per registered tenant model for scope isolation
78
+ MechaTenant.registry.tenant_models.each_key do |model_name|
79
+ test "behavioral tenant scope isolation for model #{model_name}" do
80
+ ctx_proc = MechaTenant.configuration.test_matrix_context
81
+ assert_not_nil ctx_proc, "MechaTenant.configuration.test_matrix_context must be configured for behavioral matrix testing"
82
+
83
+ ctx = instance_exec(&ctx_proc)
84
+ prober = MechaTenant::Auditor::Prober.new(self)
85
+ result = prober.probe_single_model_scope(
86
+ model_name,
87
+ victim: ctx[:victim],
88
+ attacker: ctx[:attacker],
89
+ victim_records: ctx[:victim_records]
90
+ )
91
+
92
+ assert result.success?, "Model scope isolation failed for #{model_name}:\n" +
93
+ result.scope_failures.map { |f| " - [#{f.model}]: #{f.message}" }.join("\n")
94
+ end
95
+ end
96
+
97
+ # 2. One test per enforced parameterized (:id) route for cross-tenant IDOR protection
98
+ enforced_id_audits = MechaTenant::Auditor::Prober.collect_enforced_id_audits
99
+ enforced_id_audits.each do |audit|
100
+ test "behavioral IDOR route isolation for #{audit.verb} #{audit.path} (#{audit.controller}##{audit.action})" do
101
+ ctx_proc = MechaTenant.configuration.test_matrix_context
102
+ assert_not_nil ctx_proc, "MechaTenant.configuration.test_matrix_context must be configured for behavioral matrix testing"
103
+
104
+ ctx = instance_exec(&ctx_proc)
105
+ prober = MechaTenant::Auditor::Prober.new(self)
106
+ result = prober.probe_single_idor_route(
107
+ audit,
108
+ victim_records: ctx[:victim_records],
109
+ ui_auth: ctx[:ui_auth],
110
+ api_auth: ctx[:api_auth]
111
+ )
112
+
113
+ assert result.success?, "Cross-tenant IDOR route prober failed for #{audit.verb} #{audit.path}:\n" +
114
+ result.route_failures.map { |f| " - #{f.route} [#{f.target_model}]: #{f.message}" }.join("\n")
115
+ end
116
+ end
117
+ end
118
+
119
+ define_tenant_matrix_tests!
120
+ end
121
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/railtie"
4
+
5
+ module MechaTenant
6
+ class Railtie < Rails::Railtie
7
+ rake_tasks do
8
+ load File.expand_path("../tasks/mecha_tenant.rake", __dir__)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module MechaTenant
6
+ class Registry
7
+ VALID_CLASSIFICATIONS = %w[tenant global].freeze
8
+
9
+ attr_reader :models, :tenant_root, :colorization
10
+
11
+ def initialize(config_path_or_hash = nil, scopes = {})
12
+ @scopes = scopes.transform_keys(&:to_s)
13
+ @models = {}
14
+ @tenant_root = "User"
15
+ @colorization = {}
16
+
17
+ if config_path_or_hash.is_a?(Hash)
18
+ load_from_hash(config_path_or_hash)
19
+ elsif config_path_or_hash
20
+ load_from_file(config_path_or_hash)
21
+ end
22
+ end
23
+
24
+ def load_from_file(path)
25
+ raise ConfigurationError, "Tenancy configuration file missing: #{path}" unless File.exist?(path)
26
+
27
+ data = begin
28
+ YAML.load_file(path)
29
+ rescue => e
30
+ raise ConfigurationError, "Invalid YAML syntax in #{path}: #{e.message}"
31
+ end
32
+
33
+ load_from_hash(data)
34
+ end
35
+
36
+ def load_from_hash(data)
37
+ unless data.is_a?(Hash) && data.key?("models") && data["models"].is_a?(Array)
38
+ raise ConfigurationError, "Tenancy config must have a top-level 'models' list"
39
+ end
40
+
41
+ @tenant_root = data["tenant_root"].to_s.strip if data["tenant_root"].present?
42
+ @models = {}
43
+
44
+ data["models"].each_with_index do |entry, idx|
45
+ validate_model_entry!(entry, idx)
46
+ model_name = entry["model"].strip
47
+ if @models.key?(model_name)
48
+ raise ConfigurationError, "Duplicate model entry '#{model_name}' in tenancy configuration"
49
+ end
50
+
51
+ @models[model_name] = {
52
+ model: model_name,
53
+ classification: entry["classification"].to_sym,
54
+ ownership: entry["ownership"]&.to_s&.strip,
55
+ notes: entry["notes"]&.to_s&.strip
56
+ }.freeze
57
+ end
58
+
59
+ if data["debug_colorization"].is_a?(Hash)
60
+ @colorization = data["debug_colorization"].transform_keys(&:to_sym).transform_values do |v|
61
+ v.to_s.sub(/^:/, "").to_sym
62
+ end
63
+ end
64
+
65
+ @models.freeze
66
+ self
67
+ end
68
+
69
+ def registered?(model_or_name)
70
+ @models.key?(normalize_model_name(model_or_name))
71
+ end
72
+
73
+ def model_entry(model_or_name)
74
+ @models[normalize_model_name(model_or_name)]
75
+ end
76
+
77
+ def tenant_model?(model_or_name)
78
+ entry = model_entry(model_or_name)
79
+ entry ? entry[:classification] == :tenant : false
80
+ end
81
+
82
+ def global_model?(model_or_name)
83
+ entry = model_entry(model_or_name)
84
+ entry ? entry[:classification] == :global : false
85
+ end
86
+
87
+ def tenant_models
88
+ @models.select { |_k, v| v[:classification] == :tenant }
89
+ end
90
+
91
+ def global_models
92
+ @models.select { |_k, v| v[:classification] == :global }
93
+ end
94
+
95
+ def scope_defined?(model_or_name)
96
+ @scopes.key?(normalize_model_name(model_or_name))
97
+ end
98
+
99
+ def scope_for(model_or_name, tenant)
100
+ name = normalize_model_name(model_or_name)
101
+ entry = @models[name]
102
+
103
+ if entry.nil?
104
+ raise UnregisteredModelError, "Model '#{name}' is not registered in MechaTenant configuration"
105
+ end
106
+
107
+ if entry[:classification] == :global
108
+ raise GlobalModelScopeError, "Model '#{name}' is classified as :global and cannot be queried via tenant_scope"
109
+ end
110
+
111
+ raise TenantMissingError, "Cannot resolve tenant_scope(#{name}) when current tenant is nil" if tenant.nil?
112
+
113
+ resolver = @scopes[name]
114
+ if resolver.nil?
115
+ raise ConfigurationError, "No scope resolver defined for tenant model '#{name}'"
116
+ end
117
+
118
+ resolver.call(tenant)
119
+ end
120
+
121
+ private
122
+
123
+ def normalize_model_name(model_or_name)
124
+ case model_or_name
125
+ when Class, Module
126
+ model_or_name.name.to_s
127
+ else
128
+ model_or_name.to_s
129
+ end
130
+ end
131
+
132
+ def validate_model_entry!(entry, idx)
133
+ unless entry.is_a?(Hash)
134
+ raise ConfigurationError, "Tenancy model entry at index #{idx} must be a Hash"
135
+ end
136
+
137
+ model_name = entry["model"]
138
+ unless model_name.is_a?(String) && !model_name.strip.empty?
139
+ raise ConfigurationError, "Tenancy model entry at index #{idx} must have a non-empty string for 'model'"
140
+ end
141
+
142
+ classification = entry["classification"]
143
+ unless classification.is_a?(String) && VALID_CLASSIFICATIONS.include?(classification)
144
+ raise ConfigurationError,
145
+ "Tenancy model '#{model_name}' at index #{idx} must have classification in #{VALID_CLASSIFICATIONS.inspect}, got #{classification.inspect}"
146
+ end
147
+
148
+ if classification == "tenant"
149
+ ownership = entry["ownership"]
150
+ unless ownership.is_a?(String) && !ownership.strip.empty?
151
+ raise ConfigurationError,
152
+ "Tenancy model '#{model_name}' (classification: tenant) must explicitly declare 'ownership'"
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MechaTenant
4
+ VERSION = "0.1.0"
5
+ Version = VERSION
6
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "mecha_tenant/version"
4
+ require_relative "mecha_tenant/errors"
5
+ require_relative "mecha_tenant/configuration"
6
+ require_relative "mecha_tenant/registry"
7
+ require_relative "mecha_tenant/controller"
8
+ require_relative "mecha_tenant/auditor/static"
9
+ require_relative "mecha_tenant/auditor/prober"
10
+ require_relative "mecha_tenant/railtie" if defined?(Rails::Railtie)
11
+
12
+ module MechaTenant
13
+ class << self
14
+ def configuration
15
+ @configuration ||= Configuration.new
16
+ end
17
+
18
+ def configure
19
+ yield(configuration) if block_given?
20
+ reload!
21
+ end
22
+
23
+ def reset_configuration!
24
+ @configuration = Configuration.new
25
+ @registry = nil
26
+ end
27
+
28
+ def registry
29
+ @registry ||= Registry.new(configuration.config_path, configuration.scopes)
30
+ end
31
+
32
+ def reload!
33
+ @registry = Registry.new(configuration.config_path, configuration.scopes)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tiny_color"
4
+ require_relative "../mecha_tenant"
5
+
6
+ class String
7
+ def dark_grey
8
+ respond_to?(:grey) ? grey : self
9
+ end unless method_defined?(:dark_grey)
10
+
11
+ def dark_gray
12
+ respond_to?(:gray) ? gray : self
13
+ end unless method_defined?(:dark_gray)
14
+ end
15
+
16
+ desc "Print all models and controller actions with their MechaTenant isolation controls"
17
+ task mecha_tenant: :environment do
18
+ auditor = MechaTenant::Auditor::Static.new
19
+ result = auditor.audit
20
+ colors = MechaTenant.configuration.debug_colorization
21
+
22
+ colorize = ->(text, key) {
23
+ color = colors[key]
24
+ if color && text.respond_to?(color)
25
+ text.send(color)
26
+ else
27
+ text
28
+ end
29
+ }
30
+
31
+ puts "=== Model Tenancy Registry ==="
32
+ all_model_names = result.mapped_models.map { |m| m[:model] } + result.unmapped_models
33
+ max_model_len = [all_model_names.map(&:length).max || 5, 5].max
34
+ max_class_len = 14
35
+
36
+ puts sprintf("%-#{max_model_len}s %-#{max_class_len}s %s", "Model", "Classification", "Ownership")
37
+
38
+ result.mapped_models.each do |entry|
39
+ cls_sym = entry[:classification]
40
+ padded_model = sprintf("%-#{max_model_len}s", entry[:model])
41
+ padded_class = sprintf("%-#{max_class_len}s", cls_sym.to_s.upcase)
42
+ ownership = entry[:ownership] || (cls_sym == :global ? "(global/system)" : "-")
43
+
44
+ puts sprintf(
45
+ "%s %s %s",
46
+ colorize.call(padded_model, cls_sym),
47
+ colorize.call(padded_class, cls_sym),
48
+ ownership
49
+ )
50
+ end
51
+
52
+ result.unmapped_models.each do |model_name|
53
+ padded_model = sprintf("%-#{max_model_len}s", model_name)
54
+ padded_class = sprintf("%-#{max_class_len}s", "UNMAPPED")
55
+ puts sprintf(
56
+ "%s %s %s",
57
+ colorize.call(padded_model, :unmapped),
58
+ colorize.call(padded_class, :unmapped),
59
+ "MISSING FROM config/mecha_tenant.yml"
60
+ )
61
+ end
62
+
63
+ puts "\n=== Controller Action Tenancy Enforcement ==="
64
+ audits = result.action_audits
65
+ max_verb_len = [audits.map { |a| a.verb.to_s.length }.max || 4, 4].max
66
+ max_path_len = [audits.map { |a| a.path.to_s.length }.max || 11, 11].max
67
+ max_target_len = [audits.map { |a| "#{a.controller}##{a.action}".length }.max || 17, 17].max
68
+ max_status_len = 9
69
+
70
+ puts sprintf(
71
+ "%-#{max_verb_len}s %-#{max_path_len}s %-#{max_target_len}s %-#{max_status_len}s %s",
72
+ "Verb",
73
+ "URI Pattern",
74
+ "Controller#Action",
75
+ "Status",
76
+ "Scoping / Reason"
77
+ )
78
+
79
+ audits.each do |a|
80
+ padded_verb = sprintf("%-#{max_verb_len}s", a.verb)
81
+ padded_path = sprintf("%-#{max_path_len}s", a.path)
82
+ padded_target = sprintf("%-#{max_target_len}s", "#{a.controller}##{a.action}")
83
+ padded_status = sprintf("%-#{max_status_len}s", a.status.to_s.upcase)
84
+
85
+ detail = case a.status
86
+ when :skipped
87
+ "SKIP: #{a.skip_reason}"
88
+ when :violation
89
+ "VIOLATION"
90
+ else
91
+ a.scopes_used.any? ? a.scopes_used.join(", ") : "(no model queries)"
92
+ end
93
+
94
+ puts sprintf(
95
+ "%s %s %s %s %s",
96
+ padded_verb,
97
+ colorize.call(padded_path, a.status),
98
+ padded_target,
99
+ colorize.call(padded_status, a.status),
100
+ detail
101
+ )
102
+ end
103
+ end
104
+
105
+ namespace :mecha_tenant do
106
+ namespace :verify do
107
+ desc "Statically verify model tenancy registry parity and strict controller lookup scoping"
108
+ task static: :environment do
109
+ puts "Running MechaTenant static model & controller AST audit..."
110
+ auditor = MechaTenant::Auditor::Static.new
111
+ result = auditor.audit
112
+
113
+ tenant_count = result.mapped_models.count { |m| m[:classification] == :tenant }
114
+ global_count = result.mapped_models.count { |m| m[:classification] == :global }
115
+ enforced_count = result.action_audits.count { |a| a.status == :enforced }
116
+ skipped_count = result.action_audits.count { |a| a.status == :skipped }
117
+
118
+ puts " -> #{result.mapped_models.size} models registered (#{tenant_count} tenant, #{global_count} global)"
119
+ puts " -> #{result.action_audits.size} controller routes audited (#{enforced_count} enforced, #{skipped_count} explicitly skipped)"
120
+
121
+ if result.unmapped_models.any?
122
+ puts "\nERROR: Found #{result.unmapped_models.size} unmapped model(s) missing from config/mecha_tenant.yml:".light_red
123
+ result.unmapped_models.each { |m| puts " - #{m}" }
124
+ end
125
+
126
+ if result.dead_models.any?
127
+ puts "\nERROR: Found #{result.dead_models.size} dead/stale model(s) in config/mecha_tenant.yml:".light_red
128
+ result.dead_models.each { |m| puts " - #{m}" }
129
+ end
130
+
131
+ if result.missing_scopes.any?
132
+ puts "\nERROR: Found #{result.missing_scopes.size} model scope configuration error(s):".light_red
133
+ result.missing_scopes.each { |err| puts " - #{err}" }
134
+ end
135
+
136
+ if result.dead_skip_rules.any?
137
+ puts "\nERROR: Found #{result.dead_skip_rules.size} dead skip_tenant_enforcement! rule(s):".light_red
138
+ result.dead_skip_rules.each { |dr| puts " - #{dr[:file]}: #{dr[:message]}" }
139
+ end
140
+
141
+ if result.violations.any?
142
+ puts "\nERROR: Found #{result.violations.size} unscoped model reference violation(s) in controllers:".light_red
143
+ result.violations.each do |v|
144
+ puts " - #{v.file}:#{v.line} [#{v.controller}##{v.method_name}] #{v.message}"
145
+ puts " #{v.snippet}"
146
+ end
147
+ end
148
+
149
+ if result.success?
150
+ puts "\n✓ Static tenant isolation audit passed: 0 unmapped models, 0 dead rules, 0 controller violations.".green
151
+ else
152
+ abort("\n✗ Static tenant isolation audit failed. Please resolve the errors above.")
153
+ end
154
+ end
155
+
156
+ desc "Run behavioral cross-tenant scope and IDOR route matrix test suite"
157
+ task behavioral: :environment do
158
+ puts "Running MechaTenant behavioral cross-tenant isolation matrix test suite..."
159
+ matrix_test_path = File.expand_path("../mecha_tenant/matrix_test.rb", __dir__)
160
+ system({ "MECHA_MATRIX_STANDALONE" => "1" }, "bin/rails", "test", matrix_test_path) || abort("Behavioral tenant verification failed.")
161
+ end
162
+ end
163
+
164
+ desc "Run both static AST/registry verification and behavioral cross-tenant IDOR verification"
165
+ task verify: %w[verify:static verify:behavioral]
166
+ end
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mecha_tenant
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jeff Lunt
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activesupport
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '7.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: prism
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: tiny_color
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ description: MechaTenant enforces strict tenant isolation in Rails applications via
55
+ a YAML model classification registry, explicit tenant_scope helpers, Prism AST static
56
+ analysis of controllers, and behavioral IDOR route probing.
57
+ email:
58
+ - jefflunt@gmail.com
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - LICENSE
64
+ - README.md
65
+ - Rakefile
66
+ - lib/mecha_tenant.rb
67
+ - lib/mecha_tenant/auditor/prober.rb
68
+ - lib/mecha_tenant/auditor/static.rb
69
+ - lib/mecha_tenant/configuration.rb
70
+ - lib/mecha_tenant/controller.rb
71
+ - lib/mecha_tenant/errors.rb
72
+ - lib/mecha_tenant/matrix_test.rb
73
+ - lib/mecha_tenant/railtie.rb
74
+ - lib/mecha_tenant/registry.rb
75
+ - lib/mecha_tenant/version.rb
76
+ - lib/tasks/mecha_tenant.rake
77
+ homepage: https://github.com/jefflunt/mecha_tenant
78
+ licenses:
79
+ - MIT
80
+ metadata: {}
81
+ rdoc_options: []
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: 3.2.0
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ requirements: []
95
+ rubygems_version: 4.0.16
96
+ specification_version: 4
97
+ summary: Closed-by-default multi-tenant model registry, AST controller auditing, and
98
+ IDOR probing for Rails
99
+ test_files: []