atomic_tenant 1.3.1 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5475a908ae3732b52948ad107eb43a20da9b97f010e262f1f66b124cc453e3ee
4
- data.tar.gz: 7999c4ab89bc4f10d325513ed5099af32342b9759c59218ad448950c01a9bb14
3
+ metadata.gz: d2d3e98335db46cdd62a93e555d5efa5b56741bdeb2bb7a7fc3a1954d0c111ce
4
+ data.tar.gz: 260711640f716ff03f2b699ee5b54c7004ad657bd835afaef5fd8e2884ebdbf4
5
5
  SHA512:
6
- metadata.gz: 32414a833dda4bc89564f2d1178cd3395f8a2b234d6e3159098b94e755e0b87d2f49512d0fa7696738bff2ff0e6fbfdcc142589b0b8a9151887a76b17f453ea4
7
- data.tar.gz: 4f8e5c8ac4c8014bd698d03dc217d2c4a5e345f568c3430ce7fec141001296fab35f37adc95da992d4a8530443c63b0c2fca5afdfb7bb9046ce14d411a5b78cf
6
+ metadata.gz: f4d38d829399bdd173ed68567f73bed3f45cfc4559e52a2db710369c28d6374a28bd8d7be3c0de87867dc8da76ecde743cf142615f1836c8969bd442e1eec3b8
7
+ data.tar.gz: 120d52e713b17c86be15ef2583c3cd08194adb21967cc677f53aef6af0d36217fdd311d1d605ae3a193f780c2afaaac61f0c0541859e709bce07893b36b3adb1
data/README.md CHANGED
@@ -34,5 +34,93 @@ With the following content:
34
34
  AtomicTenant.admin_subdomain = "admin".freeze
35
35
  ```
36
36
 
37
+ ### Row Level Security Tenanting
38
+ This gem also includes modules and helpers for a row level security tenanting solution.
39
+
40
+ Configure the settings in the initializer:
41
+
42
+ ```ruby
43
+ AtomicTenant.tenants_table = :tenants
44
+ AtomicTenant.db_tenant_restricted_user = Rails.application.credentials.db_tenant_restricted_user
45
+ ```
46
+
47
+ This example configures AtomicTenant to use the Tenant model as the tenants table, and will expect tenanted models to have a `tenant_id` field on them. db_tenant_restricted_user is the database user that will have row level security enforced.
48
+
49
+ Add row level security to each tenanted table in a migration:
50
+
51
+ ```ruby
52
+ dir.up do
53
+ # Enable row level security and add row level security policies for the users table
54
+ AtomicTenant::RowLevelSecurity.add_row_level_security(:users)
55
+ end
56
+ dir.down do
57
+ # Remove row level security and remove row level security policies for the users table
58
+ AtomicTenant::RowLevelSecurity.remove_row_level_security(:users)
59
+ end
60
+ ```
61
+
62
+ #### Tenantable
63
+ Include the `AtomicTenant::Tenantable` module in your base model to default all models private:
64
+ ```ruby
65
+ class ApplicationRecord < ActiveRecord::Base
66
+ include AtomicTenant::Tenantable
67
+ end
68
+ ```
69
+
70
+ If you default all models to private, non tenanted models can be marked public with `set_public_tenanted`:
71
+ ```ruby
72
+ class Tenant < ApplicationRecord
73
+ set_public_tenanted
74
+ end
75
+ ```
76
+
77
+ Alternatively you can include `AtomicTenant::Tenantable` in just the models you want to be tenanted.
78
+
79
+ There is a helper to verify that row level security is set on a model. If you default all models private, it's a good idea to have a test that verifies that all private models do actually have row level security enabled on them:
80
+ ```ruby
81
+ require "rails_helper"
82
+
83
+ RSpec.describe Tenant do
84
+ describe "private models have row level security enabled" do
85
+ it "ensures row level security is enabled for private tenanted models" do
86
+ Rails.application.eager_load!
87
+ private_models = AtomicTenant::Tenantable.private_tenanted_models.map(&:table_name)
88
+ expect(private_models).not_to be_empty
89
+
90
+ AtomicTenant::Tenantable.private_tenanted_models.each do |model|
91
+ expect do
92
+ AtomicTenant::Tenantable.verify_tenanted(model)
93
+ end.not_to raise_error
94
+ end
95
+ end
96
+ end
97
+ end
98
+ ```
99
+
100
+ #### TenantSwitching
101
+ To use `TenantSwitching`, include the module in your tenant model:
102
+ ```ruby
103
+ class Tenant < ApplicationRecord
104
+ set_public_tenanted
105
+ include AtomicTenant::TenantSwitching
106
+ end
107
+ ```
108
+
109
+ The `Tenant` model must have a key field.
110
+
111
+ Switching tenants can then be done via Apartment-esque tenant switching methods:
112
+ 1. ```ruby
113
+ Tenant.switch!(Tenant.find_by(key: "admin"))
114
+ ```
115
+ 2. ```ruby
116
+ Tenant.switch(Tenant.find_by(key: "admin")) do
117
+ Tenant.current_key
118
+ end
119
+ # => "admin"
120
+
121
+ Tenant.current_key
122
+ # => "public"
123
+ ```
124
+
37
125
  ## License
38
126
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -1,4 +1,5 @@
1
1
  module AtomicTenant
2
2
  class LtiDeployment < ApplicationRecord
3
+ belongs_to :application_instance
3
4
  end
4
5
  end
@@ -1,4 +1,5 @@
1
1
  module AtomicTenant
2
2
  class PinnedClientId < ApplicationRecord
3
+ belongs_to :application_instance
3
4
  end
4
5
  end
@@ -1,4 +1,6 @@
1
1
  module AtomicTenant
2
2
  class PinnedPlatformGuid < ApplicationRecord
3
+ belongs_to :application
4
+ belongs_to :application_instance
3
5
  end
4
6
  end
@@ -0,0 +1,24 @@
1
+ module AtomicTenant
2
+ module ActiveJob
3
+ extend ActiveSupport::Concern
4
+
5
+ class_methods do
6
+ def execute(job_data)
7
+ tenant_key = job_data.delete("tenant")
8
+ tenant = AtomicTenant.tenant_model.find_by(key: tenant_key)
9
+ AtomicTenant.tenant_model.switch(tenant) do
10
+ super
11
+ end
12
+ end
13
+ end
14
+
15
+ def initialize(*_args, **_kargs)
16
+ @tenant = AtomicTenant.tenant_model.current_key
17
+ super
18
+ end
19
+
20
+ def serialize
21
+ super.merge('tenant' => @tenant)
22
+ end
23
+ end
24
+ end
@@ -2,19 +2,18 @@ module AtomicTenant
2
2
  module CanvasContentMigration
3
3
  class InvalidTokenError < StandardError; end
4
4
 
5
- ALGORITHM = "HS256".freeze
5
+ ALGORITHM = 'HS256'.freeze
6
6
  HEADER = 1
7
7
 
8
8
  # Decode Canvas content migration JWT
9
9
  # https://canvas.instructure.com/doc/api/file.tools_xml.html#content-migrations-support
10
10
 
11
- def self.decode(token, algorithm = ALGORITHM)
11
+ def self.decode(token, algorithm = ALGORITHM)
12
12
  unverified = JWT.decode(token, nil, false)
13
- kid = unverified[HEADER]["kid"]
14
- app_instance = ApplicationInstance.find_by!(lti_key: kid)
13
+ kid = unverified[HEADER]['kid']
14
+ ApplicationInstance.find_by!(lti_key: kid)
15
15
  # We don't validate because we're only setting the tenant for the request. The app
16
16
  # must validate the JWT.
17
- app_instance
18
17
  end
19
18
  end
20
19
  end
@@ -35,15 +35,16 @@ module AtomicTenant
35
35
  env['atomic.validated.application_instance_id'] = deployment.application_instance_id
36
36
  else
37
37
  deployment = deployment_manager.link_deployment_id(decoded_id_token: decoded_token)
38
- env['atomic.validated.application_instance_id'] = deployment.application_instance_id
38
+ env['atomic.validated.application_instance_id'] = deployment.application_instance_id
39
39
  end
40
- elsif env.dig("oauth_state", "application_instance_id").present?
41
- env['atomic.validated.application_instance_id'] = env["oauth_state"]["application_instance_id"]
40
+ elsif env.dig('oauth_state', 'application_instance_id').present?
41
+ env['atomic.validated.application_instance_id'] = env['oauth_state']['application_instance_id']
42
42
  elsif is_admin?(request)
43
43
  admin_app_key = AtomicTenant.admin_subdomain
44
44
  admin_app = Application.find_by(key: admin_app_key)
45
45
 
46
46
  raise Exceptions::NoAdminApp if admin_app.nil?
47
+
47
48
  app_instances = admin_app.application_instances
48
49
 
49
50
  raise Exceptions::NonUniqueAdminApp if app_instances.count > 1
@@ -61,13 +62,10 @@ module AtomicTenant
61
62
  # the tenant for the request. If the token is invalid or expired the app must
62
63
  # return 401 or take other action.
63
64
  decoded_token = AtomicTenant::JwtToken.decode(token, validate: false)
64
- if decoded_token.present? && decoded_token.first.present?
65
- if app_instance_id = decoded_token.first['application_instance_id']
66
- env['atomic.validated.application_instance_id'] = app_instance_id
67
- end
65
+ if decoded_token.present? && decoded_token.first.present? && app_instance_id = decoded_token.first['application_instance_id']
66
+ env['atomic.validated.application_instance_id'] = app_instance_id
68
67
  end
69
68
  end
70
-
71
69
  rescue StandardError => e
72
70
  Rails.logger.error("Error in current app instance middleware: #{e}, #{e.backtrace}")
73
71
  end
@@ -76,10 +74,10 @@ module AtomicTenant
76
74
  end
77
75
 
78
76
  def is_admin?(request)
79
- return true if request.path == "/readiness"
77
+ return true if request.path == '/readiness'
80
78
 
81
79
  host = request.host_with_port
82
- subdomain = host&.split(".")&.first
80
+ subdomain = host&.split('.')&.first
83
81
 
84
82
  return false if subdomain.nil?
85
83
 
@@ -87,16 +85,16 @@ module AtomicTenant
87
85
  end
88
86
 
89
87
  def canvas_migration_hook?(request)
90
- return true if request.path.match?(%r{^/api/ims_(import|export)})
88
+ true if request.path.match?(%r{^/api/ims_(import|export)})
91
89
  end
92
90
 
93
91
  def encoded_token(req)
94
92
  return req.params['jwt'] if req.params['jwt']
95
93
 
96
94
  # TODO: verify HTTP_AUTORIZAITON is the same as "Authorization"
97
- if header = req.get_header('HTTP_AUTHORIZATION') # || req.headers[:authorization]
98
- header.split(' ').last
99
- end
95
+ return unless header = req.get_header('HTTP_AUTHORIZATION') # || req.headers[:authorization]
96
+
97
+ header.split(' ').last
100
98
  end
101
99
  end
102
100
  end
@@ -7,5 +7,8 @@ module AtomicTenant
7
7
 
8
8
  class NonUniqueAdminApp < StandardError; end
9
9
  class NoAdminApp < StandardError; end
10
+ class InvalidTenantKeyError < StandardError; end
11
+ class TenantNotFoundError < StandardError; end
12
+ class TenantNotSet < StandardError; end
10
13
  end
11
- end
14
+ end
@@ -2,60 +2,18 @@ module AtomicTenant
2
2
  module JwtToken
3
3
  class InvalidTokenError < StandardError; end
4
4
 
5
- ALGORITHM = "HS512".freeze
5
+ ALGORITHM = 'HS512'.freeze
6
6
 
7
- def self.decode(token, algorithm = ALGORITHM, validate: true)
7
+ def self.decode(token, algorithm = ALGORITHM, validate: true)
8
8
  decoded_token = JWT.decode(
9
9
  token,
10
10
  AtomicTenant.jwt_secret,
11
11
  validate,
12
- { algorithm: algorithm },
12
+ { algorithm: algorithm }
13
13
  )
14
- if AtomicTenant.jwt_aud != decoded_token[0]["aud"]
15
- return nil
16
- end
14
+ return nil if AtomicTenant.jwt_aud != decoded_token[0]['aud']
17
15
 
18
16
  decoded_token
19
17
  end
20
-
21
- def self.valid?(token, algorithm = ALGORITHM)
22
- decode(token, algorithm)
23
- end
24
-
25
- def decoded_jwt_token(req)
26
- token = valid?(encoded_token(req))
27
- raise InvalidTokenError, 'Unable to decode jwt token' if token.blank?
28
- raise InvalidTokenError, 'Invalid token payload' if token.empty?
29
-
30
- token[0]
31
- end
32
-
33
- def validate_token_with_secret(aud, secret, req = request)
34
- token = decoded_jwt_token(req, secret)
35
- raise InvalidTokenError if aud != token['aud']
36
- rescue JWT::DecodeError, InvalidTokenError => e
37
- Rails.logger.error "JWT Error occured: #{e.inspect}"
38
- render json: { error: 'Unauthorized: Invalid token.' }, status: :unauthorized
39
- end
40
-
41
- def encoded_token!(req)
42
- return req.params[:jwt] if req.params[:jwt]
43
-
44
- header = req.headers['Authorization'] || req.headers[:authorization]
45
- raise InvalidTokenError, 'No authorization header found' if header.nil?
46
-
47
- token = header.split(' ').last
48
- raise InvalidTokenError, 'Invalid authorization header string' if token.nil?
49
-
50
- token
51
- end
52
-
53
- def encoded_token(req)
54
- return req.params[:jwt] if req.params[:jwt]
55
-
56
- if header = req.headers['Authorization'] || req.headers[:authorization]
57
- header.split(' ').last
58
- end
59
- end
60
18
  end
61
19
  end
@@ -0,0 +1,24 @@
1
+ module AtomicTenant::RowLevelSecurity
2
+ def self.add_row_level_security(table_name)
3
+ app_username = ActiveRecord::Base.connection.quote_column_name(AtomicTenant.db_tenant_restricted_user)
4
+ safe_table_name = ActiveRecord::Base.connection.quote_table_name(table_name)
5
+ policy_name = ActiveRecord::Base.connection.quote_table_name("#{table_name}_tenant_enforcement")
6
+ rls_setting_name = ActiveRecord::Base.connection.quote("rls.#{AtomicTenant.tenanted_by}")
7
+ tenanted_by = ActiveRecord::Base.connection.quote_column_name(AtomicTenant.tenanted_by)
8
+
9
+ ActiveRecord::Base.connection.execute("ALTER TABLE #{safe_table_name} ENABLE ROW LEVEL SECURITY")
10
+ ActiveRecord::Base.connection.execute <<~SQL
11
+ CREATE POLICY #{policy_name}
12
+ ON #{safe_table_name}
13
+ TO #{app_username}
14
+ USING (#{tenanted_by} = NULLIF(current_setting(#{rls_setting_name}, TRUE), '')::bigint)
15
+ SQL
16
+ end
17
+
18
+ def self.remove_row_level_security(table_name)
19
+ safe_table_name = ActiveRecord::Base.connection.quote_table_name(table_name)
20
+ policy_name = ActiveRecord::Base.connection.quote_table_name("#{table_name}_tenant_enforcement")
21
+ ActiveRecord::Base.connection.execute("DROP POLICY #{policy_name} ON #{safe_table_name}")
22
+ ActiveRecord::Base.connection.execute("ALTER TABLE #{safe_table_name} DISABLE ROW LEVEL SECURITY")
23
+ end
24
+ end
@@ -0,0 +1,73 @@
1
+ module AtomicTenant::TenantSwitching
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ validates :key, presence: true, uniqueness: true
6
+
7
+ def self.switch!(tenant = nil)
8
+ if tenant
9
+ connection.clear_query_cache
10
+ Thread.current[:tenant] = tenant
11
+
12
+ variable = ActiveRecord::Base.connection.quote_column_name("rls.#{AtomicTenant.tenanted_by}")
13
+ query = "SET #{variable} = %s"
14
+ ActiveRecord::Base.connection.exec_query(query % connection.quote(tenant.id), 'SQL')
15
+ else
16
+ reset!
17
+ end
18
+ end
19
+
20
+ def self.reset!
21
+ connection.clear_query_cache
22
+ Thread.current[:tenant] = nil
23
+
24
+ variable = ActiveRecord::Base.connection.quote_column_name("rls.#{AtomicTenant.tenanted_by}")
25
+ query = "RESET #{variable}"
26
+ ActiveRecord::Base.connection.exec_query(query, 'SQL')
27
+ end
28
+
29
+ def self.switch_tenant_legacy!(tenant_key = nil)
30
+ if tenant_key
31
+ tenant = tenant_from_key!(tenant_key)
32
+ switch!(tenant)
33
+ else
34
+ reset!
35
+ end
36
+ end
37
+
38
+ def self.current_key
39
+ Thread.current[:tenant]&.key || 'public'
40
+ end
41
+
42
+ def self.current
43
+ Thread.current[:tenant]
44
+ end
45
+
46
+ def self.switch_tenant_legacy(tenant_key, &block)
47
+ tenant = tenant_from_key!(tenant_key)
48
+ switch(tenant, &block)
49
+ end
50
+
51
+ def self.tenant_from_key!(tenant_key)
52
+ tenant = AtomicTenant.tenant_model.find_by(key: tenant_key)
53
+ raise AtomicTenant::Exceptions::InvalidTenantKeyError, tenant_key unless tenant.present?
54
+
55
+ tenant
56
+ end
57
+
58
+ def self.switch(tenant, &block)
59
+ previous_tenant = Thread.current[:tenant]
60
+
61
+ begin
62
+ switch!(tenant)
63
+ block.call
64
+ ensure
65
+ if previous_tenant.present?
66
+ switch!(previous_tenant)
67
+ else
68
+ reset!
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,91 @@
1
+ module AtomicTenant::Tenantable
2
+ extend ActiveSupport::Concern
3
+
4
+ @public_tenanted_models = Set.new
5
+ @private_tenanted_models = Set.new
6
+
7
+ class << self
8
+ attr_reader :public_tenanted_models, :private_tenanted_models
9
+
10
+ def register_public_tenanted_model(model)
11
+ @public_tenanted_models.add(model)
12
+ @private_tenanted_models.delete(model)
13
+ end
14
+
15
+ def register_private_tenanted_model(model)
16
+ @private_tenanted_models.add(model)
17
+ @public_tenanted_models.delete(model)
18
+ end
19
+
20
+ def verify_tenanted(model)
21
+ query = <<~SQL
22
+ SELECT relrowsecurity
23
+ FROM pg_class
24
+ WHERE relname = $1;
25
+ SQL
26
+
27
+ result = ActiveRecord::Base.connection.exec_query(
28
+ query,
29
+ 'SQL',
30
+ [
31
+ ActiveRecord::Relation::QueryAttribute.new(
32
+ 'relname',
33
+ model.table_name,
34
+ ActiveRecord::Type::String.new
35
+ )
36
+ ]
37
+ )
38
+
39
+ return if result.first['relrowsecurity']
40
+
41
+ raise "Model #{model.name} is not public but does not have row level security. Did you forget to add row level security in your migration?"
42
+ end
43
+ end
44
+
45
+ included do
46
+ class_attribute :is_tenanted, instance_writer: false, default: true
47
+
48
+ before_create :set_tenant_id
49
+ before_validation :set_tenant_id, on: :create
50
+ validate :in_current_tenant
51
+
52
+ def self.inherited(subclass)
53
+ super
54
+
55
+ return unless subclass <= ActiveRecord::Base && !subclass.abstract_class?
56
+
57
+ AtomicTenant::Tenantable.register_private_tenanted_model(subclass)
58
+ end
59
+
60
+ private
61
+
62
+ def set_tenant_id
63
+ return unless self.class.is_tenanted?
64
+
65
+ tenant = AtomicTenant.tenant_model.current
66
+ raise AtomicTenant::Exceptions::TenantNotSet unless tenant.present?
67
+
68
+ self[AtomicTenant.tenanted_by] = tenant.id
69
+ end
70
+
71
+ def in_current_tenant
72
+ return unless self.class.is_tenanted?
73
+
74
+ tenant = AtomicTenant.tenant_model.current
75
+ raise AtomicTenant::Exceptions::TenantNotSet unless tenant.present?
76
+
77
+ return unless self[AtomicTenant.tenanted_by] != tenant.id
78
+
79
+ errors.add(AtomicTenant.tenanted_by, "must be set to the current tenant's id")
80
+ end
81
+ end
82
+
83
+ class_methods do
84
+ private
85
+
86
+ def set_public_tenanted
87
+ AtomicTenant::Tenantable.register_public_tenanted_model(self)
88
+ self.is_tenanted = false
89
+ end
90
+ end
91
+ end
@@ -1,3 +1,3 @@
1
1
  module AtomicTenant
2
- VERSION = '1.3.1'
2
+ VERSION = '1.4.0'
3
3
  end
data/lib/atomic_tenant.rb CHANGED
@@ -5,6 +5,10 @@ require 'atomic_tenant/deployment_manager/client_id_strategy'
5
5
  require 'atomic_tenant/deployment_manager/deployment_manager_strategy'
6
6
  require 'atomic_tenant/engine'
7
7
  require 'atomic_tenant/current_application_instance_middleware'
8
+ require 'atomic_tenant/tenant_switching'
9
+ require 'atomic_tenant/row_level_security'
10
+ require 'atomic_tenant/tenantable'
11
+ require 'atomic_tenant/active_job'
8
12
 
9
13
  module AtomicTenant
10
14
  mattr_accessor :custom_strategies
@@ -13,9 +17,18 @@ module AtomicTenant
13
17
  mattr_accessor :jwt_aud
14
18
 
15
19
  mattr_accessor :admin_subdomain
16
-
20
+ mattr_accessor :tenants_table
21
+ mattr_accessor :db_tenant_restricted_user
17
22
 
18
23
  def self.get_application_instance(iss:, deployment_id:)
19
24
  AtomicTenant::LtiDeployment.find_by(iss: iss, deployment_id: deployment_id)
20
25
  end
26
+
27
+ def self.tenant_model
28
+ AtomicTenant.tenants_table.to_s.classify.constantize
29
+ end
30
+
31
+ def self.tenanted_by
32
+ "#{AtomicTenant.tenants_table.to_s.singularize}_id"
33
+ end
21
34
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: atomic_tenant
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.1
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Benoit
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-10-03 00:00:00.000000000 Z
11
+ date: 2024-11-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: atomic_lti
@@ -19,7 +19,7 @@ dependencies:
19
19
  version: '1.3'
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
- version: '3'
22
+ version: '4'
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
@@ -29,21 +29,41 @@ dependencies:
29
29
  version: '1.3'
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
- version: '3'
32
+ version: '4'
33
33
  - !ruby/object:Gem::Dependency
34
34
  name: rails
35
35
  requirement: !ruby/object:Gem::Requirement
36
36
  requirements:
37
- - - "~>"
37
+ - - ">="
38
38
  - !ruby/object:Gem::Version
39
39
  version: '7.0'
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: '9'
40
43
  type: :runtime
41
44
  prerelease: false
42
45
  version_requirements: !ruby/object:Gem::Requirement
43
46
  requirements:
44
- - - "~>"
47
+ - - ">="
45
48
  - !ruby/object:Gem::Version
46
49
  version: '7.0'
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: '9'
53
+ - !ruby/object:Gem::Dependency
54
+ name: rspec
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '2.0'
60
+ type: :development
61
+ prerelease: false
62
+ version_requirements: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '2.0'
47
67
  description: Description of AtomicTenant.
48
68
  email:
49
69
  - nick.benoit@atomicjolt.com
@@ -64,6 +84,7 @@ files:
64
84
  - db/migrate/20220816223258_create_atomic_tenant_pinned_client_ids.rb
65
85
  - db/migrate/20240704002449_add_atomic_tenant_lti_deployment_platform_notification_status.rb
66
86
  - lib/atomic_tenant.rb
87
+ - lib/atomic_tenant/active_job.rb
67
88
  - lib/atomic_tenant/canvas_content_migration.rb
68
89
  - lib/atomic_tenant/current_application_instance_middleware.rb
69
90
  - lib/atomic_tenant/deployment_manager/client_id_strategy.rb
@@ -73,6 +94,9 @@ files:
73
94
  - lib/atomic_tenant/engine.rb
74
95
  - lib/atomic_tenant/exceptions.rb
75
96
  - lib/atomic_tenant/jwt_token.rb
97
+ - lib/atomic_tenant/row_level_security.rb
98
+ - lib/atomic_tenant/tenant_switching.rb
99
+ - lib/atomic_tenant/tenantable.rb
76
100
  - lib/atomic_tenant/version.rb
77
101
  - lib/tasks/atomic_tenant_tasks.rake
78
102
  homepage: https://example.com
@@ -94,7 +118,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
94
118
  - !ruby/object:Gem::Version
95
119
  version: '0'
96
120
  requirements: []
97
- rubygems_version: 3.4.19
121
+ rubygems_version: 3.5.16
98
122
  signing_key:
99
123
  specification_version: 4
100
124
  summary: Summary of AtomicTenant.