atomic_tenant 1.4.1 → 1.5.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: 62aaa4fe481b26b696c0e6f441a16eb26377d402c764d85ce6e515d4b05fa89f
4
- data.tar.gz: 6d92c3dc99883ad2a2648b9c311779c9ed364d578d1a1a87d717b269f00a2df3
3
+ metadata.gz: 1040533dd529e7e1f40ddeb45dd404ded2fa9fa493ecf0befc9580b118698c89
4
+ data.tar.gz: c614b1b32f09c29987d88a77929b9e0f90dc8f9fc9fc1b29f6cb8aaa6097a932
5
5
  SHA512:
6
- metadata.gz: 7fc4a3a440ea08f99dd22aec7f05005b5a514e22d7ae1805c0dbd7fbedafde9b6ba0fc2c446c41dfb4d6d74b0ad2c0a700f9a0deed994d9671202393dfaec561
7
- data.tar.gz: 04ea249d9df9c4db70a206be70d838197950f4e7e38383be600049dd7c63da02242f64cef812ca8dac88f009b889b22bc0ea9ad4866f84ae6d2790404d22bf65
6
+ metadata.gz: 2d7de5baefd5dd1a04ccc52323db67706ec7548b1aad81a0695e968adad9139f0419d68247000584d9c1339d7bb98de56ed6aa5ad721df51dfb0addab373b40b
7
+ data.tar.gz: 3ef28df192ac02f32d0b72dfd441d91864b036ea11014317a14d927de545eb20664788509460b4775c5158c293eba154175997938a116bc54b89368c1ed16a2e
@@ -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
@@ -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.0'
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.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: 2025-01-29 00:00:00.000000000 Z
11
+ date: 2025-10-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: atomic_lti
@@ -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
@@ -118,7 +119,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
118
119
  - !ruby/object:Gem::Version
119
120
  version: '0'
120
121
  requirements: []
121
- rubygems_version: 3.5.16
122
+ rubygems_version: 3.5.11
122
123
  signing_key:
123
124
  specification_version: 4
124
125
  summary: Summary of AtomicTenant.