atomic_tenant 1.4.1 → 1.5.1

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: 62aaa4fe481b26b696c0e6f441a16eb26377d402c764d85ce6e515d4b05fa89f
4
- data.tar.gz: 6d92c3dc99883ad2a2648b9c311779c9ed364d578d1a1a87d717b269f00a2df3
3
+ metadata.gz: b96d3efc1008755065959ca9dbceff1b93296f11ea0289664119e3875d60a64d
4
+ data.tar.gz: 2c728035c438789a0c79bdde191392e1c7c6ae47464009a7533aaa963a11826b
5
5
  SHA512:
6
- metadata.gz: 7fc4a3a440ea08f99dd22aec7f05005b5a514e22d7ae1805c0dbd7fbedafde9b6ba0fc2c446c41dfb4d6d74b0ad2c0a700f9a0deed994d9671202393dfaec561
7
- data.tar.gz: 04ea249d9df9c4db70a206be70d838197950f4e7e38383be600049dd7c63da02242f64cef812ca8dac88f009b889b22bc0ea9ad4866f84ae6d2790404d22bf65
6
+ metadata.gz: 98ff0964945704bd35f93cb5a825df117b3722de817c57957c2668c54ded2e766cb715221b62180a1530eb46e1b23938a9b78093430396c5c16d729f54b6a9a3
7
+ data.tar.gz: 229e7facd2f8e460e2ccdbcc534274da1d6814871b4b6c402fc2ae89273fdb17db87c8b44f56ebc52446b3528925ddac1bcacf16fcaab4a0642e1c24b6aebfd2
@@ -21,12 +21,18 @@ module AtomicTenant
21
21
  elsif env['atomic.validated.id_token'].present?
22
22
 
23
23
  custom_strategies = AtomicTenant.custom_strategies || []
24
+ custom_fallback_strategies = AtomicTenant.custom_fallback_strategies || []
24
25
  default_strategies = [
25
26
  AtomicTenant::DeploymentManager::PlatformGuidStrategy.new,
26
27
  AtomicTenant::DeploymentManager::ClientIdStrategy.new
27
28
  ]
28
29
 
29
- deployment_manager = AtomicTenant::DeploymentManager::DeploymentManager.new(custom_strategies.concat(default_strategies))
30
+ deployment_manager = AtomicTenant::DeploymentManager::DeploymentManager.new([
31
+ *custom_strategies,
32
+ *default_strategies,
33
+ *custom_fallback_strategies
34
+ ])
35
+
30
36
  decoded_token = env['atomic.validated.decoded_id_token']
31
37
  iss = env['atomic.validated.decoded_id_token']['iss']
32
38
  deployment_id = env['atomic.validated.decoded_id_token'][AtomicLti::Definitions::DEPLOYMENT_ID]
@@ -0,0 +1,124 @@
1
+ require 'uri'
2
+
3
+ module AtomicTenant
4
+ module DeploymentManager
5
+ class AbstractAutoCreatePlatformGuidStrategy < DeploymentManagerStrategy
6
+ TRUSTED_ISSUERS = [
7
+ %r|^https://canvas\.instructure\.com$|,
8
+ %r|^https://[a-z0-9.-]+\.brightspace\.com$|,
9
+ %r|^https://blackboard\.com$|,
10
+ ].freeze
11
+
12
+ def name
13
+ raise NotImplementedError, "Subclasses must implement #name"
14
+ end
15
+
16
+ def call(decoded_id_token:)
17
+ issuer = decoded_id_token["iss"]
18
+ platform_guid = decoded_id_token.dig(AtomicLti::Definitions::TOOL_PLATFORM_CLAIM, "guid")
19
+ target_link_uri = decoded_id_token[AtomicLti::Definitions::TARGET_LINK_URI_CLAIM]
20
+
21
+ if !platform_guid.present? || !target_link_uri.present?
22
+ return AtomicTenant::DeploymentManager::DeploymentStrategyResult.new()
23
+ end
24
+
25
+ uri = URI.parse(target_link_uri)
26
+ application_key = uri.host&.split('.')&.first
27
+ return AtomicTenant::DeploymentManager::DeploymentStrategyResult.new() if !application_key.present?
28
+
29
+ app = Application.find_by(key: application_key)
30
+ return AtomicTenant::DeploymentManager::DeploymentStrategyResult.new() if app.nil?
31
+
32
+ if !TRUSTED_ISSUERS.any? { |pattern| issuer.match?(pattern) }
33
+ existing_app_instance_count = AtomicTenant::LtiDeployment
34
+ .joins(:application_instance)
35
+ .where(
36
+ iss: issuer,
37
+ application_instances: { application_id: app.id },
38
+ ).distinct.count(:application_instance_id)
39
+
40
+ if existing_app_instance_count >= AtomicTenant.untrusted_iss_tenant_limit
41
+ raise AtomicTenant::Exceptions::OnboardingException, "The issuer #{issuer} has reached the limit of #{AtomicTenant.untrusted_iss_tenant_limit} unique tenants for the application #{application_key}."
42
+ end
43
+ end
44
+
45
+ site_url = extract_site_url(decoded_id_token)
46
+
47
+ app_inst = find_application_instance(app, site_url, issuer, platform_guid)
48
+ app_inst ||= maybe_create_application_instance(app, site_url, issuer, platform_guid)
49
+ pin = pin_platform_guid(issuer, platform_guid, app.id, app_inst.id)
50
+ AtomicTenant::DeploymentManager::DeploymentStrategyResult.new(application_instance_id: pin.application_instance_id)
51
+ end
52
+
53
+ private
54
+
55
+ def find_application_instance(current_application, site_url, issuer, platform_guid)
56
+ raise NotImplementedError, "Subclasses must implement #find_application_instance"
57
+ end
58
+
59
+ def create_application_instance(app, site_url, issuer, platform_guid)
60
+ raise NotImplementedError, "Subclasses must implement #create_application_instance"
61
+ end
62
+
63
+ def maybe_create_application_instance(app, site_url, issuer, platform_guid)
64
+ ActiveRecord::Base.transaction do
65
+ create_application_instance(app, site_url, issuer, platform_guid)
66
+ rescue ActiveRecord::RecordNotUnique
67
+ # If we get a RecordNotUnique error, it means another process created the instance concurrently.
68
+ find_application_instance(app, site_url, issuer, platform_guid)
69
+ end
70
+ end
71
+
72
+ # Pin platform guid, handling concurrent launches both trying to pin the same
73
+ # platform guid at the same time.
74
+ def pin_platform_guid(iss, platform_guid, application_id, application_instance_id)
75
+ begin
76
+ AtomicTenant::PinnedPlatformGuid.create!(
77
+ iss:,
78
+ platform_guid:,
79
+ application_id:,
80
+ application_instance_id:,
81
+ )
82
+ rescue ActiveRecord::RecordNotUnique
83
+ AtomicTenant::PinnedPlatformGuid.find_by!(
84
+ iss:,
85
+ platform_guid:,
86
+ application_id:,
87
+ application_instance_id:,
88
+ )
89
+ end
90
+ end
91
+
92
+ def extract_site_url(decoded_id_token)
93
+ platform_claim = decoded_id_token[AtomicLti::Definitions::TOOL_PLATFORM_CLAIM]
94
+ product_family_code = platform_claim["product_family_code"]
95
+
96
+ if product_family_code == "canvas"
97
+ canvas_domain = decoded_id_token.dig(AtomicLti::Definitions::CUSTOM_CLAIM, "canvas_api_domain")
98
+ if canvas_domain.blank?
99
+ raise AtomicTenant::Exceptions::OnboardingException, "Missing canvas_api_domain claim from canvas launch"
100
+ end
101
+
102
+ ensure_https(canvas_domain)
103
+ elsif product_family_code == "BlackboardLearn"
104
+ blackboard_url = platform_claim["url"]
105
+
106
+ if blackboard_url.blank?
107
+ raise AtomicTenant::Exceptions::OnboardingException, "Missing url in platform claim from blackboard launch"
108
+ end
109
+
110
+ ensure_https(blackboard_url)
111
+ else
112
+ decoded_id_token["iss"]
113
+ end
114
+ end
115
+
116
+ def ensure_https(url)
117
+ return nil if url.blank?
118
+
119
+ url = "https://#{url}" unless url.start_with?("http")
120
+ url.gsub("http://", "https://")
121
+ end
122
+ end
123
+ end
124
+ end
@@ -1,72 +1,69 @@
1
1
  module AtomicTenant
2
2
 
3
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
4
+ class DeploymentStrategyResult
5
+ attr_accessor :application_instance_id, :details
12
6
 
7
+ def initialize(application_instance_id: nil, details: nil)
8
+ @application_instance_id = application_instance_id
9
+ @details = details
13
10
  end
14
11
 
15
- class DeploymentManagerStrategy
16
- def name; end
17
- def call(decoded_id_token:); end
18
- end
19
-
12
+ end
20
13
 
14
+ class DeploymentManagerStrategy
15
+ def name; end
16
+ def call(decoded_id_token:); end
17
+ end
21
18
 
22
19
  # Associate deployment
23
20
  class DeploymentManager
24
21
 
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
22
+ def initialize(strageties)
23
+ @strageties = strageties || []
24
+ end
41
25
 
42
- Rails.logger.debug("Linking Results: #{results}")
26
+ def link_deployment_id(decoded_id_token:)
27
+ deployment_id = decoded_id_token[AtomicLti::Definitions::DEPLOYMENT_ID]
28
+ iss = decoded_id_token["iss"]
43
29
 
44
- matched = results.filter { |r| r[:result].application_instance_id.present? }
30
+ to_link = nil
31
+ strategy_name = nil
45
32
 
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}")
33
+ @strageties.each do |strategy|
34
+ result = strategy.call(decoded_id_token: decoded_id_token)
35
+ if result.application_instance_id.present?
36
+ to_link = result
37
+ strategy_name = strategy.name
38
+ break
39
+ end
40
+ rescue StandardError => e
41
+ Rails.logger.error("Error in lti deployment linking strategy: #{strategy.name}, #{e}")
42
+ end
51
43
 
52
- else
53
- raise AtomicTenant::Exceptions::UnableToLinkDeploymentError
54
- end
44
+ raise AtomicTenant::Exceptions::UnableToLinkDeploymentError if to_link.nil?
55
45
 
56
- Rails.logger.info("Linking iss / deployment id: #{iss} / #{deployment_id} to application instance: #{to_link.application_instance_id}")
46
+ Rails.logger.info(
47
+ "Linking iss / deployment id: #{iss} / #{deployment_id} to application instance: " \
48
+ "#{to_link.application_instance_id} using strategy: #{strategy_name}"
49
+ )
57
50
 
58
- associate_deployment(iss: iss, deployment_id: deployment_id,application_instance_id: to_link.application_instance_id)
59
- end
51
+ associate_deployment(
52
+ iss: iss,
53
+ deployment_id: deployment_id,
54
+ application_instance_id: to_link.application_instance_id
55
+ )
56
+ end
60
57
 
61
- private
58
+ private
62
59
 
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
60
+ def associate_deployment(iss:, deployment_id:, application_instance_id:)
61
+ AtomicTenant::LtiDeployment.create!(
62
+ iss: iss,
63
+ deployment_id: deployment_id,
64
+ application_instance_id: application_instance_id
65
+ )
66
+ end
70
67
  end
71
68
  end
72
69
  end
@@ -10,5 +10,6 @@ module AtomicTenant
10
10
  class InvalidTenantKeyError < StandardError; end
11
11
  class TenantNotFoundError < StandardError; end
12
12
  class TenantNotSet < StandardError; end
13
+ class OnboardingException < StandardError; end
13
14
  end
14
15
  end
@@ -1,3 +1,3 @@
1
1
  module AtomicTenant
2
- VERSION = '1.4.1'
2
+ VERSION = "1.5.1".freeze
3
3
  end
data/lib/atomic_tenant.rb CHANGED
@@ -3,6 +3,7 @@ require 'atomic_tenant/deployment_manager/deployment_manager'
3
3
  require 'atomic_tenant/deployment_manager/platform_guid_strategy'
4
4
  require 'atomic_tenant/deployment_manager/client_id_strategy'
5
5
  require 'atomic_tenant/deployment_manager/deployment_manager_strategy'
6
+ require 'atomic_tenant/deployment_manager/abstract_auto_create_platform_guid_strategy'
6
7
  require 'atomic_tenant/engine'
7
8
  require 'atomic_tenant/current_application_instance_middleware'
8
9
  require 'atomic_tenant/tenant_switching'
@@ -12,6 +13,9 @@ require 'atomic_tenant/active_job'
12
13
 
13
14
  module AtomicTenant
14
15
  mattr_accessor :custom_strategies
16
+ mattr_accessor :custom_fallback_strategies
17
+
18
+ mattr_accessor :untrusted_iss_tenant_limit, default: 100
15
19
 
16
20
  mattr_accessor :jwt_secret
17
21
  mattr_accessor :jwt_aud
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.4.1
4
+ version: 1.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Benoit
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-01-29 00:00:00.000000000 Z
11
+ date: 2025-11-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: atomic_lti
@@ -56,14 +56,14 @@ dependencies:
56
56
  requirements:
57
57
  - - "~>"
58
58
  - !ruby/object:Gem::Version
59
- version: '2.0'
59
+ version: '3.0'
60
60
  type: :development
61
61
  prerelease: false
62
62
  version_requirements: !ruby/object:Gem::Requirement
63
63
  requirements:
64
64
  - - "~>"
65
65
  - !ruby/object:Gem::Version
66
- version: '2.0'
66
+ version: '3.0'
67
67
  description: Description of AtomicTenant.
68
68
  email:
69
69
  - nick.benoit@atomicjolt.com
@@ -87,6 +87,7 @@ files:
87
87
  - lib/atomic_tenant/active_job.rb
88
88
  - lib/atomic_tenant/canvas_content_migration.rb
89
89
  - lib/atomic_tenant/current_application_instance_middleware.rb
90
+ - lib/atomic_tenant/deployment_manager/abstract_auto_create_platform_guid_strategy.rb
90
91
  - lib/atomic_tenant/deployment_manager/client_id_strategy.rb
91
92
  - lib/atomic_tenant/deployment_manager/deployment_manager.rb
92
93
  - lib/atomic_tenant/deployment_manager/deployment_manager_strategy.rb