atomic_tenant 1.3.1 → 1.4.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.
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.