module PandaPal module OrganizationConcerns; end # Newer versions of SymmetricEncryption introduced it's own version of attr_encrypted # that is completely incompatible with the attr_encrypted Gem that PandaPal uses. if defined?(::SymmetricEncryption::ActiveRecord::AttrEncrypted) module SkipSymmetricEncAttrEncrypted extend ActiveSupport::Concern module ::SymmetricEncryption::ActiveRecord::AttrEncrypted::ClassMethods alias_method :panda_pal_se_attr_encrypted, :attr_encrypted def attr_encrypted(*args, **kwargs) if self <= SkipSymmetricEncAttrEncrypted super else panda_pal_se_attr_encrypted(*args, **kwargs) end end end end end class SettingsMarshaler def self.load(data) return nil unless data.present? loaded = Marshal.load(data) loaded = loaded.with_indifferent_access if loaded.is_a?(Hash) && !loaded.is_a?(HashWithIndifferentAccess) loaded end def self.dump(obj) Marshal.dump(obj) end end class Organization < PandaPalRecord include SkipSymmetricEncAttrEncrypted if defined?(SkipSymmetricEncAttrEncrypted) include OrganizationConcerns::SettingsValidation include OrganizationConcerns::OrganizationBuilder if $stdin&.tty? include OrganizationConcerns::TaskScheduling if defined?(Sidekiq.schedule) attr_encrypted :settings, marshal: true, key: :encryption_key, marshaler: SettingsMarshaler before_save {|a| a.settings = a.settings} # this is a hacky work-around to a bug where attr_encrypted is not saving settings in place alias_method "settings_panda_pal_super=", "settings=" def settings=(settings) settings = settings.with_indifferent_access if settings.is_a?(Hash) && !settings.is_a?(HashWithIndifferentAccess) self.settings_panda_pal_super = settings end validates :key, uniqueness: { case_sensitive: false }, presence: true validates :secret, presence: true validates :name, uniqueness: { case_sensitive: false }, presence: true, format: { with: /\A[a-z0-9_]+\z/i } validates :canvas_account_id, presence: true validates :salesforce_id, presence: true, uniqueness: true after_create :create_schema after_commit :destroy_schema, on: :destroy define_setting("lti", { type: 'Hash', required: false, properties: {}, }) if defined?(scheduled_task) scheduled_task '0 0 3 * * *', :clean_old_sessions do PandaPal::Session.where(panda_pal_organization: self).where('updated_at < ?', 1.week.ago).delete_all end end before_validation on: [:update] do errors.add(:name, 'should not be changed after creation') if name_changed? end def encryption_key # production environment might not have loaded secret_key_base yet. # In that case, just read it from env. if (Rails.application.secrets.secret_key_base) Rails.application.secrets.secret_key_base[0,32] else ENV["SECRET_KEY_BASE"][0,32] end end def switch_tenant(&block) if block_given? Apartment::Tenant.switch(name, &block) else Apartment::Tenant.switch!(name) end end def self.current find_by_name(Apartment::Tenant.current) end def create_api(logic, expiration: nil, uses: nil, host: nil) switch_tenant do logic = "current_organization.#{logic}" if logic.is_a?(Symbol) ac = ApiCall.create!( logic: logic, expiration: expiration, uses_remaining: uses, ) ac.call_url(host: host) end end def rename!(new_name) do_switch = Apartment::Tenant.current == name ActiveRecord::Base.connection.execute( "ALTER SCHEMA \"#{name}\" RENAME TO \"#{new_name}\";" ) self.class.where(id: id).update_all(name: new_name) reload switch_tenant if do_switch end if !PandaPal.lti_options[:platform].present? || PandaPal.lti_options[:platform].is_a?(String) CONST_PLATFORM_TYPE = Platform.resolve_platform_class(nil) rescue nil else CONST_PLATFORM_TYPE = nil end # Include the Platform API... if CONST_PLATFORM_TYPE # ... directly if this is a single-platform tool include CONST_PLATFORM_TYPE::OrgExtension if defined?(CONST_PLATFORM_TYPE::OrgExtension) else # ... via method_missing/delegation if this is a multi-platform tool def respond_to_missing?(name, include_private = false) if (self.class == PandaPal::Organization) && (plat_api = platform_api).present? plat_api.respond_to?(name, include_private) else super end end def method_missing(method, *args, **kwargs, &block) if (self.class == PandaPal::Organization) && (plat_api = platform_api).present? plat_api.send(method, *args, **kwargs, &block) else super end end end # Extend a particular type of Platform API. def self.extend_platform_api(platform_type = CONST_PLATFORM_TYPE, &blk) scl = platform_type.organization_api scl.class_eval(&blk) end # Retrieve the specified Platform API for this Organization. # If only a single Platform is used (when lti_options[:platform] is a constant string or nil), passing the platform_type is unnecessary # # The API is currently an Organization subclass (using `becomes()`), but such may change. def platform_api(platform_type = primary_platform) return nil if platform_type.nil? scl = platform_type.organization_api return self if scl == self.class becomes(scl) end define_setting("lti.trusted_platforms", { type: 'Array', required: false, description: "Additional trusted JWT issuers when validating the LTI 1.3 OIDC handshake", }) # Determines if the specified platform can create sessions in this organization def trusted_platform?(platform) # Trust Instructure-hosted Canvas return true if platform.is_a?(PandaPal::Platform::Canvas) && platform.is_trusted_env? # Trust issuers added to the Org settings if (issuer = platform.platform_uri rescue nil).present? trusted_issuers = settings.dig(:lti, :trusted_platforms) || [] return true if trusted_issuers.include?(issuer) end false end protected # OrgExtension-Overridable method to allow multi-platform tool Orgs to implicitly include a Platform API def primary_platform CONST_PLATFORM_TYPE end private def create_schema Apartment::Tenant.create name end def destroy_schema Apartment::Tenant.drop name end public PandaPal.resolved_extensions_for(self).each do |ext| include ext end end end