atomic_tenant 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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: []