module CanvasSync::Api module V1 class LiveEventsController < ActionController::Base around_action :switch_tenant def process_event if params[:payload].present? process_eventsmanager_event else process_dataservices_event end rescue => e Rails.logger.error("Live Events Error: #{e.message} - #{e.backtrace}") render json: { error: "Live Events Error: #{e.message}" }, status: 422 end private def process_eventsmanager_event event = SymmetricEncryption.decrypt(params[:payload]) event = JSON.parse(event).with_indifferent_access event[:metadata] = event[:attributes] event.delete(:attributes) Rails.logger.debug("Processing event type: #{event['metadata']['event_name']}") Rails.logger.debug("Payload: #{event}") validate_tenant!(event.dig(:metadata, :root_account_uuid)) transform_ids!(event) event[:via] = "eventsmanager" dispatch_event(event) head :ok end def process_dataservices_event require "json/jwt" require "httparty" event = nil # Only allow unsigned events during devleopment if Rails.env.development? event = JSON.parse(request.raw_post) rescue nil end event ||= JSON::JWT.decode(params[:_json], dataservices_jwks) event = event.to_h.with_indifferent_access Rails.logger.debug("Processing event type: #{event['metadata']['event_name']}") Rails.logger.debug("Payload: #{event}") validate_tenant!(event.dig(:metadata, :root_account_uuid)) transform_ids!(event) event[:via] = "dataservices" dispatch_event(event) head :ok end def dispatch_event(event) CanvasSync::LiveEvents::ProcessEventJob.perform_later(event) end # Live events will use a canvas global ID (cross shard) for any ID's provided. This method will return the local ID. def local_canvas_id(id) # TODO: Don't apply this to cross-shard entries id.to_i % 10_000_000_000_000 end def transform_ids!(data) transformed = {} data.each do |k, v| if k.ends_with?("_id") && (v.is_a?(String) || v.is_a?(Numeric)) transformed["sharded_#{k}"] = v transformed[k] = local_canvas_id(v) rescue v elsif v.is_a?(Hash) transform_ids!(v) end end data.merge!(transformed) end def dataservices_jwks require "httparty" jwk_json = Rails.cache.fetch("canvas_sync/dataservices_jwks", expires_in: 24.hours) do jkws_url = ENV["DATASERVICES_JWK_URL"].presence || "https://8axpcl50e4.execute-api.us-east-1.amazonaws.com/main/jwks" response = HTTParty.get(jkws_url) JSON.parse(response.body) end JSON::JWK::Set.new(jwk_json) end def validate_tenant!(event_uuid) if defined?(PandaPal) root_info = current_organization.root_account_info root_uuid = root_info[:uuid] if !root_uuid.present? || root_uuid != event_uuid render json: { error: "Invalid Organization/UUID" }, status: 403 return end else raise "No way to validate LiveEvent is genuinely from the correct Canvas instance! Monkey-patch CanvasSync::Api::V1::LiveEventsController#validate_tenant!" end end def switch_tenant(&block) if defined?(PandaPal) && (org = params[:organization] || params[:org]).present? Apartment::Tenant.switch(PandaPal::Organization.find(org).name, &block) else yield end end end end end