atomic_tenant 1.2.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1fb823e9a69ae301f4c54a2fdd0212a1ab1a722d8a29693eb19ab368bfe20426
4
+ data.tar.gz: 5dfc41443847da7d515de0aea9303d44cacac5ba424fbc9e2837335221c90a59
5
+ SHA512:
6
+ metadata.gz: 178f0e154893bed333c5d1a308b6598c795b5516b9884083fc4303e57949cc623725a282bf7c7d6a01ae1bb334eb9d73bdc1e8fc5ad5986cb015f377eb0bfd11
7
+ data.tar.gz: de989225aac86e31b254e23c514bf1714bae793f319e63f48bcb140b3eb1e233e591c481d7b03e4c20325ac66153c147df9243ec0ddfb87051a2b816f1cd6f2c
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2022 Nick Benoit
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # AtomicTenant
2
+ This gem handles figuring out which tenant is being used and adds that information .
3
+
4
+ ## Installation
5
+ Add this line to your application's Gemfile:
6
+
7
+ ```ruby
8
+ gem "atomic_tenant"
9
+ ```
10
+
11
+ And then execute:
12
+ ```bash
13
+ $ bundle
14
+ ```
15
+
16
+ Or install it yourself as:
17
+ ```bash
18
+ $ gem install atomic_tenant
19
+ ```
20
+
21
+ Then install the migrations:
22
+ ./bin/rails atomic_tenant:install:migrations
23
+
24
+ ## Usage
25
+ Create a new initializer:
26
+ ```
27
+ config/initializers/atomic_tenant.rb
28
+ ```
29
+
30
+ With the following content:
31
+ ```
32
+ AtomicTenant.jwt_secret = Rails.application.secrets.auth0_client_secret
33
+ AtomicTenant.jwt_aud = Rails.application.secrets.auth0_client_id
34
+ AtomicTenant.admin_subdomain = "admin".freeze
35
+ ```
36
+
37
+ ## License
38
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require 'bundler/setup'
2
+
3
+ APP_RAKEFILE = File.expand_path('test/dummy/Rakefile', __dir__)
4
+ load 'rails/tasks/engine.rake'
5
+
6
+ load 'rails/tasks/statistics.rake'
7
+
8
+ require 'bundler/gem_tasks'
@@ -0,0 +1,5 @@
1
+ module AtomicTenant
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,4 @@
1
+ module AtomicTenant
2
+ class LtiDeployment < ApplicationRecord
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module AtomicTenant
2
+ class PinnedClientId < ApplicationRecord
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module AtomicTenant
2
+ class PinnedPlatformGuid < ApplicationRecord
3
+ end
4
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,2 @@
1
+ AtomicTenant::Engine.routes.draw do
2
+ end
@@ -0,0 +1,13 @@
1
+ class CreateAtomicTenantLtiDeployments < ActiveRecord::Migration[7.0]
2
+ def change
3
+ create_table :atomic_tenant_lti_deployments do |t|
4
+ t.string :iss, null: false
5
+ t.string :deployment_id, null: false
6
+ t.bigint :application_instance_id, null: false
7
+ t.timestamps
8
+ end
9
+
10
+
11
+ add_index :atomic_tenant_lti_deployments, [:iss, :deployment_id], unique: true
12
+ end
13
+ end
@@ -0,0 +1,16 @@
1
+
2
+ class CreateAtomicTenantPinnedPlatformGuids < ActiveRecord::Migration[7.0]
3
+ def change
4
+ create_table :atomic_tenant_pinned_platform_guids do |t|
5
+ t.string :iss, null: false
6
+ t.string :platform_guid, null: false
7
+ t.bigint :application_id, null: false
8
+
9
+ t.bigint :application_instance_id, null: false
10
+
11
+ t.timestamps
12
+ end
13
+
14
+ add_index :atomic_tenant_pinned_platform_guids, [:iss, :platform_guid, :application_id], unique: true, name: 'index_pinned_platform_guids'
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ class CreateAtomicTenantPinnedClientIds < ActiveRecord::Migration[7.0]
2
+ def change
3
+ create_table :atomic_tenant_pinned_client_ids do |t|
4
+ t.string :iss, null: false
5
+ t.string :client_id, null: false
6
+
7
+ t.bigint :application_instance_id, null: false
8
+
9
+ t.timestamps
10
+ end
11
+
12
+ add_index :atomic_tenant_pinned_client_ids, [:iss, :client_id], unique: true
13
+ end
14
+ end
15
+
16
+
@@ -0,0 +1,24 @@
1
+ module AtomicTenant
2
+ module CanvasContentMigration
3
+ class InvalidTokenError < StandardError; end
4
+
5
+ ALGORITHM = "HS256".freeze
6
+ HEADER = 1
7
+
8
+ # Decode Canvas content migration JWT
9
+ # https://canvas.instructure.com/doc/api/file.tools_xml.html#content-migrations-support
10
+
11
+ def self.decode(token, algorithm = ALGORITHM)
12
+ unverified = JWT.decode(token, nil, false)
13
+ kid = unverified[HEADER]["kid"]
14
+ app_instance = ApplicationInstance.find_by!(lti_key: kid)
15
+ decoded_token = JWT.decode(
16
+ token,
17
+ app_instance.lti_secret,
18
+ true,
19
+ { algorithm: algorithm },
20
+ )
21
+ [decoded_token, app_instance]
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,101 @@
1
+ require_relative 'jwt_token'
2
+ require_relative 'canvas_content_migration'
3
+ require_relative 'exceptions'
4
+ module AtomicTenant
5
+ class CurrentApplicationInstanceMiddleware
6
+ include JwtToken
7
+
8
+ def initialize(app)
9
+ @app = app
10
+ end
11
+
12
+ def call(env)
13
+ begin
14
+ request = Rack::Request.new(env)
15
+
16
+ if env['atomic.validated.oauth_consumer_key'].present?
17
+ oauth_consumer_key = env['atomic.validated.oauth_consumer_key']
18
+ if ai = ApplicationInstance.find_by(lti_key: oauth_consumer_key)
19
+ env['atomic.validated.application_instance_id'] = ai.id
20
+ end
21
+ elsif env['atomic.validated.id_token'].present?
22
+
23
+ custom_strategies = AtomicTenant.custom_strategies || []
24
+ default_strategies = [
25
+ AtomicTenant::DeploymentManager::PlatformGuidStrategy.new,
26
+ AtomicTenant::DeploymentManager::ClientIdStrategy.new
27
+ ]
28
+
29
+ deployment_manager = AtomicTenant::DeploymentManager::DeploymentManager.new(custom_strategies.concat(default_strategies))
30
+ decoded_token = env['atomic.validated.decoded_id_token']
31
+ iss = env['atomic.validated.decoded_id_token']['iss']
32
+ deployment_id = env['atomic.validated.decoded_id_token'][AtomicLti::Definitions::DEPLOYMENT_ID]
33
+
34
+ if deployment = AtomicTenant::LtiDeployment.find_by(iss: iss, deployment_id: deployment_id)
35
+ env['atomic.validated.application_instance_id'] = deployment.application_instance_id
36
+ else
37
+ deployment = deployment_manager.link_deployment_id(decoded_id_token: decoded_token)
38
+ env['atomic.validated.application_instance_id'] = deployment.application_instance_id
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"]
42
+ elsif is_admin?(request)
43
+ admin_app_key = AtomicTenant.admin_subdomain
44
+ admin_app = Application.find_by(key: admin_app_key)
45
+
46
+ raise Exceptions::NoAdminApp if admin_app.nil?
47
+ app_instances = admin_app.application_instances
48
+
49
+ raise Exceptions::NonUniqueAdminApp if app_instances.count > 1
50
+ raise Exceptions::NoAdminApp if app_instances.empty?
51
+
52
+ if instance = app_instances.first
53
+ env['atomic.validated.application_instance_id'] = instance.id
54
+ end
55
+ elsif canvas_migration_hook?(request)
56
+ _token, app_instance = AtomicTenant::CanvasContentMigration.decode(encoded_token(request))
57
+ env['atomic.validated.application_instance_id'] = app_instance.id
58
+ elsif encoded_token(request).present?
59
+ token = encoded_token(request)
60
+ # TODO: decoded token should be put on request
61
+ decoded_token = AtomicTenant::JwtToken.decode(token)
62
+ if decoded_token.present? && decoded_token.first.present?
63
+ if app_instance_id = decoded_token.first['application_instance_id']
64
+ env['atomic.validated.application_instance_id'] = app_instance_id
65
+ end
66
+ end
67
+
68
+ end
69
+
70
+ rescue StandardError => e
71
+ Rails.logger.error("Error in current app instance middleware: #{e}, #{e.backtrace}")
72
+ end
73
+
74
+ @app.call(env)
75
+ end
76
+
77
+ def is_admin?(request)
78
+ return true if request.path == "/readiness"
79
+
80
+ host = request.host_with_port
81
+ subdomain = host&.split(".")&.first
82
+
83
+ return false if subdomain.nil?
84
+
85
+ subdomain == AtomicTenant.admin_subdomain
86
+ end
87
+
88
+ def canvas_migration_hook?(request)
89
+ return true if request.path.match?(%r{^/api/ims_(import|export)})
90
+ end
91
+
92
+ def encoded_token(req)
93
+ return req.params['jwt'] if req.params['jwt']
94
+
95
+ # TODO: verify HTTP_AUTORIZAITON is the same as "Authorization"
96
+ if header = req.get_header('HTTP_AUTHORIZATION') # || req.headers[:authorization]
97
+ header.split(' ').last
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,22 @@
1
+ require 'uri'
2
+
3
+ module AtomicTenant
4
+ module DeploymentManager
5
+ class ClientIdStrategy < DeploymentManagerStrategy
6
+ def name
7
+ 'ClientIdStrategy'
8
+ end
9
+
10
+ def call(decoded_id_token:)
11
+ client_id = AtomicLti::Lti.client_id(decoded_id_token)
12
+ iss = decoded_id_token["iss"]
13
+
14
+ if (pinned = AtomicTenant::PinnedClientId.find_by(iss: iss, client_id: client_id))
15
+ DeploymentStrategyResult.new(application_instance_id: pinned.application_instance_id)
16
+ else
17
+ DeploymentStrategyResult.new()
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,72 @@
1
+ module AtomicTenant
2
+
3
+ module DeploymentManager
4
+ class DeploymentStrategyResult
5
+ attr_accessor :application_instance_id
6
+ attr_accessor :details
7
+
8
+ def initialize(application_instance_id: nil, details: nil)
9
+ @application_instance_id = application_instance_id
10
+ @details = details
11
+ end
12
+
13
+ end
14
+
15
+ class DeploymentManagerStrategy
16
+ def name; end
17
+ def call(decoded_id_token:); end
18
+ end
19
+
20
+
21
+
22
+ # Associate deployment
23
+ class DeploymentManager
24
+
25
+ def initialize(strageties)
26
+ @strageties = strageties || []
27
+ end
28
+
29
+ def link_deployment_id(decoded_id_token:)
30
+ deployment_id = decoded_id_token[AtomicLti::Definitions::DEPLOYMENT_ID]
31
+ iss = decoded_id_token["iss"]
32
+
33
+ results = @strageties.flat_map do |strategy|
34
+ begin
35
+ [{name: strategy.name, result: strategy.call(decoded_id_token: decoded_id_token)}]
36
+ rescue StandardError => e
37
+ Rails.logger.error("Error in lti deployment linking strategy: #{strategy.name}, #{e}")
38
+ []
39
+ end
40
+ end
41
+
42
+ Rails.logger.debug("Linking Results: #{results}")
43
+
44
+ matched = results.filter { |r| r[:result].application_instance_id.present? }
45
+
46
+ to_link = if matched.size == 1
47
+ matched.first[:result]
48
+ elsif matched.size > 1
49
+ matched.first[:result]
50
+ Rails.logger.info("Colliding strategies, Linking iss / deployment id: #{iss} / #{deployment_id} to application instance: #{to_link.application_instance_id}, all results: #{results}")
51
+
52
+ else
53
+ raise AtomicTenant::Exceptions::UnableToLinkDeploymentError
54
+ end
55
+
56
+ Rails.logger.info("Linking iss / deployment id: #{iss} / #{deployment_id} to application instance: #{to_link.application_instance_id}")
57
+
58
+ associate_deployment(iss: iss, deployment_id: deployment_id,application_instance_id: to_link.application_instance_id)
59
+ end
60
+
61
+ private
62
+
63
+ def associate_deployment(iss:, deployment_id:, application_instance_id:)
64
+ AtomicTenant::LtiDeployment.create!(
65
+ iss: iss,
66
+ deployment_id: deployment_id,
67
+ application_instance_id: application_instance_id
68
+ )
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,20 @@
1
+ module AtomicTenant
2
+
3
+ module DeploymentManager
4
+ class DeploymentStrategyResult
5
+ attr_accessor :application_instance_id
6
+ attr_accessor :details
7
+
8
+ def initialize(application_instance_id: nil, details: nil)
9
+ @application_instance_id = application_instance_id
10
+ @details = details
11
+ end
12
+
13
+ end
14
+
15
+ class DeploymentManagerStrategy
16
+ def name; end
17
+ def call(id_token:); end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,37 @@
1
+ require 'uri'
2
+
3
+ module AtomicTenant
4
+ module DeploymentManager
5
+ class PlatformGuidStrategy < DeploymentManagerStrategy
6
+ def name
7
+ "PlatformGuidStrategy"
8
+ end
9
+
10
+ # return DeploymentStrategyResult
11
+ def call(decoded_id_token:)
12
+ iss = decoded_id_token["iss"]
13
+ platform_guid = decoded_id_token.dig(AtomicLti::Definitions::TOOL_PLATFORM_CLAIM, "guid")
14
+ target_link_uri = decoded_id_token[AtomicLti::Definitions::TARGET_LINK_URI_CLAIM]
15
+
16
+
17
+ return DeploymentStrategyResult.new() if !platform_guid.present? || !target_link_uri.present?
18
+
19
+
20
+ uri = URI.parse(target_link_uri)
21
+ application_key = uri.host&.split('.')&.first
22
+
23
+ return DeploymentStrategyResult.new() if !application_key.present?
24
+
25
+ app = Application.find_by(key: application_key)
26
+
27
+ return DeploymentStrategyResult.new() if !app.present?
28
+
29
+ if(pinned = AtomicTenant::PinnedPlatformGuid.find_by(iss: iss, platform_guid: platform_guid, application_id: app.id))
30
+ DeploymentStrategyResult.new(application_instance_id: pinned.application_instance_id)
31
+ else
32
+ DeploymentStrategyResult.new()
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,5 @@
1
+ module AtomicTenant
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace AtomicTenant
4
+ end
5
+ end
@@ -0,0 +1,11 @@
1
+
2
+ module AtomicTenant
3
+ module Exceptions
4
+
5
+ class UnableToLinkDeploymentError < StandardError
6
+ end
7
+
8
+ class NonUniqueAdminApp < StandardError; end
9
+ class NoAdminApp < StandardError; end
10
+ end
11
+ end
@@ -0,0 +1,59 @@
1
+ module AtomicTenant
2
+ module JwtToken
3
+ class InvalidTokenError < StandardError; end
4
+
5
+ ALGORITHM = "HS512".freeze
6
+
7
+ def self.decode(token, algorithm = ALGORITHM)
8
+ decoded_token = JWT.decode(
9
+ token,
10
+ AtomicTenant.jwt_secret,
11
+ true,
12
+ { algorithm: algorithm },
13
+ )
14
+ raise InvalidTokenError if AtomicTenant.jwt_aud != decoded_token[0]["aud"]
15
+
16
+ decoded_token
17
+ end
18
+
19
+ def self.valid?(token, algorithm = ALGORITHM)
20
+ decode(token, algorithm)
21
+ end
22
+
23
+ def decoded_jwt_token(req)
24
+ token = valid?(encoded_token(req))
25
+ raise InvalidTokenError, 'Unable to decode jwt token' if token.blank?
26
+ raise InvalidTokenError, 'Invalid token payload' if token.empty?
27
+
28
+ token[0]
29
+ end
30
+
31
+ def validate_token_with_secret(aud, secret, req = request)
32
+ token = decoded_jwt_token(req, secret)
33
+ raise InvalidTokenError if aud != token['aud']
34
+ rescue JWT::DecodeError, InvalidTokenError => e
35
+ Rails.logger.error "JWT Error occured: #{e.inspect}"
36
+ render json: { error: 'Unauthorized: Invalid token.' }, status: :unauthorized
37
+ end
38
+
39
+ def encoded_token!(req)
40
+ return req.params[:jwt] if req.params[:jwt]
41
+
42
+ header = req.headers['Authorization'] || req.headers[:authorization]
43
+ raise InvalidTokenError, 'No authorization header found' if header.nil?
44
+
45
+ token = header.split(' ').last
46
+ raise InvalidTokenError, 'Invalid authorization header string' if token.nil?
47
+
48
+ token
49
+ end
50
+
51
+ def encoded_token(req)
52
+ return req.params[:jwt] if req.params[:jwt]
53
+
54
+ if header = req.headers['Authorization'] || req.headers[:authorization]
55
+ header.split(' ').last
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,3 @@
1
+ module AtomicTenant
2
+ VERSION = '1.2.0'
3
+ end
@@ -0,0 +1,21 @@
1
+ require 'atomic_tenant/version'
2
+ require 'atomic_tenant/deployment_manager/deployment_manager'
3
+ require 'atomic_tenant/deployment_manager/platform_guid_strategy'
4
+ require 'atomic_tenant/deployment_manager/client_id_strategy'
5
+ require 'atomic_tenant/deployment_manager/deployment_manager_strategy'
6
+ require 'atomic_tenant/engine'
7
+ require 'atomic_tenant/current_application_instance_middleware'
8
+
9
+ module AtomicTenant
10
+ mattr_accessor :custom_strategies
11
+
12
+ mattr_accessor :jwt_secret
13
+ mattr_accessor :jwt_aud
14
+
15
+ mattr_accessor :admin_subdomain
16
+
17
+
18
+ def self.get_application_instance(iss:, deployment_id:)
19
+ AtomicTenant::LtiDeployment.find_by(iss: iss, deployment_id: deployment_id)
20
+ end
21
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :atomic_tenant do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,94 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: atomic_tenant
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Nick Benoit
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-08-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: atomic_lti
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rails
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '7.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '7.0'
41
+ description: Description of AtomicTenant.
42
+ email:
43
+ - nick.benoit@atomicjolt.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - MIT-LICENSE
49
+ - README.md
50
+ - Rakefile
51
+ - app/models/atomic_tenant/application_record.rb
52
+ - app/models/atomic_tenant/lti_deployment.rb
53
+ - app/models/atomic_tenant/pinned_client_id.rb
54
+ - app/models/atomic_tenant/pinned_platform_guid.rb
55
+ - config/routes.rb
56
+ - db/migrate/20220816154357_create_atomic_tenant_lti_deployments.rb
57
+ - db/migrate/20220816174344_create_atomic_tenant_pinned_platform_guids.rb
58
+ - db/migrate/20220816223258_create_atomic_tenant_pinned_client_ids.rb
59
+ - lib/atomic_tenant.rb
60
+ - lib/atomic_tenant/canvas_content_migration.rb
61
+ - lib/atomic_tenant/current_application_instance_middleware.rb
62
+ - lib/atomic_tenant/deployment_manager/client_id_strategy.rb
63
+ - lib/atomic_tenant/deployment_manager/deployment_manager.rb
64
+ - lib/atomic_tenant/deployment_manager/deployment_manager_strategy.rb
65
+ - lib/atomic_tenant/deployment_manager/platform_guid_strategy.rb
66
+ - lib/atomic_tenant/engine.rb
67
+ - lib/atomic_tenant/exceptions.rb
68
+ - lib/atomic_tenant/jwt_token.rb
69
+ - lib/atomic_tenant/version.rb
70
+ - lib/tasks/atomic_tenant_tasks.rake
71
+ homepage: https://example.com
72
+ licenses:
73
+ - MIT
74
+ metadata: {}
75
+ post_install_message:
76
+ rdoc_options: []
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ requirements: []
90
+ rubygems_version: 3.4.15
91
+ signing_key:
92
+ specification_version: 4
93
+ summary: Summary of AtomicTenant.
94
+ test_files: []