scim_users_controller.rb 7.18 KB
module ScimRails
  class ScimUsersController < ScimRails::ApplicationController
    def index
      if params[:filter].present?
        query = ScimRails::ScimQueryParser.new(params[:filter])

        users = @company
          .public_send(ScimRails.config.scim_users_scope)
          .where(
            "#{ScimRails.config.scim_users_model.connection.quote_column_name(query.attribute)} #{query.operator} ?",
            query.parameter
          )
          .order(ScimRails.config.scim_users_list_order)
      else
        users = @company
          .public_send(ScimRails.config.scim_users_scope)
          .order(ScimRails.config.scim_users_list_order)
      end

      counts = ScimCount.new(
        start_index: params[:startIndex],
        limit: params[:count],
        total: users.count,
      )

      json_scim_response(object: users, counts: counts)
    end

    def create
      Rails.logger.error("scim create_params: " + params.to_json)

      if ScimRails.config.scim_user_prevent_update_on_create
        user = @company.public_send(ScimRails.config.scim_users_scope).create!(permitted_user_params)
      else
        username_key = ScimRails.config.queryable_user_attributes[:userName]
        find_by_username = Hash.new
        user_params = permitted_user_params
        find_by_username[username_key.to_sym] = permitted_user_params[username_key.to_sym].downcase
        user = @company
          .public_send(ScimRails.config.scim_users_scope)
          .find_or_create_by(find_by_username)
        user.update!(permitted_user_params)
        user.reload
        update_user(user) rescue ""
      end
      update_status(user) unless put_active_param.nil?
      json_scim_response(object: user, status: :created)
    end

    def show
      user = @company.public_send(ScimRails.config.scim_users_scope).find(params[:id])
      json_scim_response(object: user)
    end

    def put_update
      Rails.logger.error("scim put_update: " + params.to_json)
      user = @company.public_send(ScimRails.config.scim_users_scope).find(params[:id])
      update_user(user) rescue ""
      update_status(user) unless put_active_param.nil?
      user.update!(permitted_user_params)
      json_scim_response(object: user)
    end

    # TODO: PATCH will only deprovision or reprovision users.
    # This will work just fine for Okta but is not SCIM compliant.
    def patch_update
      Rails.logger.error("scim patch_update: " + params.to_json)
      user = @company.public_send(ScimRails.config.scim_users_scope).find(params[:id])
      update_user(user) rescue ""
      update_status(user) rescue "" # Azure AD uses PATCH to update user, not just status.
      json_scim_response(object: user)
    end

    def update_user(user)
      user_params = {}
      operations = params["Operations"] || params[:Operations] || []
      postal_code = params[:addresses][0][:postalCode] || params["addresses"][0]["addresses"] rescue ""
      if postal_code.blank?
        postal_code = operations.find { |operation| (operation["path"] || operation[:path]).to_s.downcase == 'addresses[type eq "work"].postalcode' }[:value] rescue ""
      end
      user_params.merge!({ postal_code: postal_code }) if postal_code.present?

      office_location = params[:addresses][0][:formatted] || params["addresses"][0]["formatted"] rescue ""
      if office_location.blank?
        operation = operations.find { |operation| (operation["path"] || operation[:path]).to_s.downcase == 'addresses[type eq "work"].formatted' } || {}
        office_location = operation[:value] || operation["value"]
      end
      user_params.merge!({ office_location: office_location }) if office_location.present?

      if ScimRails.config.constant_user_attributes
        user_params.merge!(ScimRails.config.constant_user_attributes)
      end

      ScimRails.config.mutable_user_attributes.each do |attribute|
        param = operations.find { |operation| operation["path"] == path_for(attribute, ScimRails.config.patch_user_attributes_schema).map { |x| x.to_s }.join(".") } rescue nil
        user_params[attribute] = param["value"] if param.present?
      end

      Rails.logger.error("scim update_user params: #{user_params.inspect}")
      user.update!(user_params)
    end

    private

    def permitted_user_params
      hash = {}
      if ScimRails.config.constant_user_attributes
        hash.merge!(ScimRails.config.constant_user_attributes)
      end

      ScimRails.config.mutable_user_attributes.each do |attribute|
        hash[attribute] ||= find_value_for(attribute)
      end
      hash
    end

    def find_value_for(attribute)
      params.dig(*path_for(attribute).map { |x| x.to_s }) rescue nil
    end

    # `path_for` is a recursive method used to find the "path" for
    # `.dig` to take when looking for a given attribute in the
    # params.
    #
    # Example: `path_for(:name)` should return an array that looks
    # like [:names, 0, :givenName]. `.dig` can then use that path
    # against the params to translate the :name attribute to "John".

    def path_for(attribute, object = ScimRails.config.mutable_user_attributes_schema, path = [])
      at_path = path.empty? ? object : object.dig(*path)
      return path if at_path == attribute
      result = nil
      case at_path
      when Hash
        at_path.each do |key, value|
          found_path = path_for(attribute, object, [*path, key])
          if found_path.present?
            result = found_path
            break
          end
        end
      when Array
        at_path.each_with_index do |value, index|
          found_path = path_for(attribute, object, [*path, index])
          if found_path.present?
            result = found_path
            break
          end
        end
      end
      return result
    end

    def update_status(user)
      user.public_send(ScimRails.config.user_reprovision_method) if active?
      user.public_send(ScimRails.config.user_deprovision_method) unless active?
    end

    def active?
      active = put_active_param
      Rails.logger.error("scim active: " + active.to_s)
      active = patch_active_param if active.nil?
      Rails.logger.error("scim active: " + active.to_s)
      case active
      when true, "true", 1
        true
      when false, "false", 0
        false
      else
        Rails.logger.error("scim active: error")
        raise ActiveRecord::RecordInvalid
      end
    end

    def put_active_param
      params[:active]
    end

    def patch_active_param
      handle_invalid = lambda do
        raise ScimRails::ExceptionHandler::UnsupportedPatchRequest
      end
      operations = params["Operations"] || params[:Operations] || {}
      valid_operation = operations.find { |operation| (operation["op"] || operation[:op]).to_s.downcase == "replace" && (operation["path"] || operation[:path]).to_s.downcase == "active" } rescue {}
      return valid_operation["value"].to_s.downcase == "true" if valid_operation.present?

      valid_operation = operations.find(handle_invalid) do |operation|
        valid_patch_operation?(operation)
      end

      valid_operation.dig("value", "active")
    end

    def valid_patch_operation?(operation)
      operation["op"].casecmp("replace") &&
        operation["value"] &&
        [true, false].include?(operation["value"]["active"])
    end
  end
end