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 +4 -4
- data/lib/atomic_tenant/current_application_instance_middleware.rb +7 -1
- data/lib/atomic_tenant/deployment_manager/abstract_auto_create_platform_guid_strategy.rb +124 -0
- data/lib/atomic_tenant/deployment_manager/deployment_manager.rb +47 -50
- data/lib/atomic_tenant/exceptions.rb +1 -0
- data/lib/atomic_tenant/version.rb +1 -1
- data/lib/atomic_tenant.rb +4 -0
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b96d3efc1008755065959ca9dbceff1b93296f11ea0289664119e3875d60a64d
|
|
4
|
+
data.tar.gz: 2c728035c438789a0c79bdde191392e1c7c6ae47464009a7533aaa963a11826b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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(
|
|
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
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30
|
+
to_link = nil
|
|
31
|
+
strategy_name = nil
|
|
45
32
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
raise AtomicTenant::Exceptions::UnableToLinkDeploymentError
|
|
54
|
-
end
|
|
44
|
+
raise AtomicTenant::Exceptions::UnableToLinkDeploymentError if to_link.nil?
|
|
55
45
|
|
|
56
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
58
|
+
private
|
|
62
59
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
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
|
+
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-
|
|
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: '
|
|
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: '
|
|
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
|