Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/atomic_tenant.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require 'atomic_tenant/deployment_manager/platform_guid_strategy'
require 'atomic_tenant/deployment_manager/client_id_strategy'
require 'atomic_tenant/deployment_manager/deployment_manager_strategy'
require 'atomic_tenant/deployment_manager/abstract_auto_create_platform_guid_strategy'
require 'atomic_tenant/engine'
require 'atomic_tenant/current_application_instance_middleware'
require 'atomic_tenant/tenant_switching'
Expand All @@ -12,6 +13,9 @@

module AtomicTenant
mattr_accessor :custom_strategies
mattr_accessor :custom_fallback_strategies

mattr_accessor :untrusted_iss_tenant_limit, default: 100

mattr_accessor :jwt_secret
mattr_accessor :jwt_aud
Expand Down
8 changes: 7 additions & 1 deletion lib/atomic_tenant/current_application_instance_middleware.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,18 @@ def call(env)
elsif env['atomic.validated.id_token'].present?

custom_strategies = AtomicTenant.custom_strategies || []
custom_fallback_strategies = AtomicTenant.custom_fallback_strategies || []
default_strategies = [
AtomicTenant::DeploymentManager::PlatformGuidStrategy.new,
AtomicTenant::DeploymentManager::ClientIdStrategy.new
]

deployment_manager = AtomicTenant::DeploymentManager::DeploymentManager.new(custom_strategies.concat(default_strategies))
deployment_manager = AtomicTenant::DeploymentManager::DeploymentManager.new([
*custom_strategies,
*default_strategies,
*custom_fallback_strategies
])

decoded_token = env['atomic.validated.decoded_id_token']
iss = env['atomic.validated.decoded_id_token']['iss']
deployment_id = env['atomic.validated.decoded_id_token'][AtomicLti::Definitions::DEPLOYMENT_ID]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
require 'uri'

module AtomicTenant
module DeploymentManager
class AbstractAutoCreatePlatformGuidStrategy < DeploymentManagerStrategy
TRUSTED_ISSUERS = [
%r|^https://canvas\.instructure\.com$|,
%r|^https://[a-z0-9.-]+\.brightspace\.com$|,
%r|^https://blackboard\.com$|,
].freeze

def name
raise NotImplementedError, "Subclasses must implement #name"
end

def call(decoded_id_token:)
issuer = decoded_id_token["iss"]
platform_guid = decoded_id_token.dig(AtomicLti::Definitions::TOOL_PLATFORM_CLAIM, "guid")
target_link_uri = decoded_id_token[AtomicLti::Definitions::TARGET_LINK_URI_CLAIM]

if !platform_guid.present? || !target_link_uri.present?
return AtomicTenant::DeploymentManager::DeploymentStrategyResult.new()
end

uri = URI.parse(target_link_uri)
application_key = uri.host&.split('.')&.first
return AtomicTenant::DeploymentManager::DeploymentStrategyResult.new() if !application_key.present?

app = Application.find_by(key: application_key)
return AtomicTenant::DeploymentManager::DeploymentStrategyResult.new() if app.nil?

if !TRUSTED_ISSUERS.any? { |pattern| issuer.match?(pattern) }
existing_app_instance_count = AtomicTenant::LtiDeployment
.joins(:application_instance)
.where(
iss: issuer,
application_instances: { application_id: app.id },
).distinct.count(:application_instance_id)

if existing_app_instance_count >= AtomicTenant.untrusted_iss_tenant_limit
raise AtomicTenant::Exceptions::OnboardingException, "The issuer #{issuer} has reached the limit of #{AtomicTenant.untrusted_iss_tenant_limit} unique tenants for the application #{application_key}."
end
end

site_url = extract_site_url(decoded_id_token)

app_inst = find_application_instance(app, site_url, issuer, platform_guid)
app_inst ||= maybe_create_application_instance(app, site_url, issuer, platform_guid)
pin = pin_platform_guid(issuer, platform_guid, app.id, app_inst.id)
AtomicTenant::DeploymentManager::DeploymentStrategyResult.new(application_instance_id: pin.application_instance_id)
end

private

def find_application_instance(current_application, site_url, issuer, platform_guid)
raise NotImplementedError, "Subclasses must implement #find_application_instance"
end

def create_application_instance(app, site_url, issuer, platform_guid)
raise NotImplementedError, "Subclasses must implement #create_application_instance"
end

def maybe_create_application_instance(app, site_url, issuer, platform_guid)
ActiveRecord::Base.transaction do
create_application_instance(app, site_url, issuer, platform_guid)
rescue ActiveRecord::RecordNotUnique
# If we get a RecordNotUnique error, it means another process created the instance concurrently.
find_application_instance(app, site_url, issuer, platform_guid)
end
end

# Pin platform guid, handling concurrent launches both trying to pin the same
# platform guid at the same time.
def pin_platform_guid(iss, platform_guid, application_id, application_instance_id)
begin
AtomicTenant::PinnedPlatformGuid.create!(
iss:,
platform_guid:,
application_id:,
application_instance_id:,
)
rescue ActiveRecord::RecordNotUnique
AtomicTenant::PinnedPlatformGuid.find_by!(
iss:,
platform_guid:,
application_id:,
application_instance_id:,
)
end
end

def extract_site_url(decoded_id_token)
platform_claim = decoded_id_token[AtomicLti::Definitions::TOOL_PLATFORM_CLAIM]
product_family_code = platform_claim["product_family_code"]

if product_family_code == "canvas"
canvas_domain = decoded_id_token.dig(AtomicLti::Definitions::CUSTOM_CLAIM, "canvas_api_domain")
if canvas_domain.blank?
raise AtomicTenant::Exceptions::OnboardingException, "Missing canvas_api_domain claim from canvas launch"
end

ensure_https(canvas_domain)
elsif product_family_code == "BlackboardLearn"
blackboard_url = platform_claim["url"]

if blackboard_url.blank?
raise AtomicTenant::Exceptions::OnboardingException, "Missing url in platform claim from blackboard launch"
end

ensure_https(blackboard_url)
else
decoded_id_token["iss"]
end
end

def ensure_https(url)
return nil if url.blank?

url = "https://#{url}" unless url.start_with?("http")
url.gsub("http://", "https://")
end
end
end
end
1 change: 1 addition & 0 deletions lib/atomic_tenant/exceptions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ class NoAdminApp < StandardError; end
class InvalidTenantKeyError < StandardError; end
class TenantNotFoundError < StandardError; end
class TenantNotSet < StandardError; end
class OnboardingException < StandardError; end
end
end
Loading
Loading