# frozen_string_literal: true # rubocop:disable Rails/HelperInstanceVariable module Decidim # An authorization transfer object is created when the user authorizes # themselves with the same unique ID as some other user had used in the past. # Typically this can happen if the user first created an account and # authorized it, then deleted their account, and finally decided to register # again and authorize their account for a second time. # # To register an authorization transfer handler in a specific module, use the # following code example. # # @example Register authorization handler # Decidim::AuthorizationTransfer.register(:my_module) do |transfer, auth_handler| # # The move_records method updates the provided active record objects # # to be mapped to the new user for which the authorization is being # # transferred to. Provide the record class and the column name which # # maps the user records to these records as its arguments. # # # # If you need access to the authorization handler that caused the # # transfer to be initiated, it is available as the second yielded # # argument (auth_hander). # transfer.move_records(Decidim::MyModule::FooBar, :decidim_user_id) # end # # The handler registration needs a name for the specific module handling the # transfer and a block which handles the specific transfer. The block is # called with the transfer record, i.e. instance of this class with access to # all necessary information required for handling the transfer. class AuthorizationTransfer < ApplicationRecord belongs_to :authorization, class_name: "Decidim::Authorization" belongs_to :user, class_name: "Decidim::User" belongs_to :source_user, class_name: "Decidim::User" has_many :records, class_name: "Decidim::AuthorizationTransferRecord", foreign_key: :transfer_id, dependent: :destroy class DisabledError < StandardError; end class << self # Provides access to the registry instance that stores the transfer # handlers for each module. # # The only reason for the registry is defined at the Decidim core module # is to have it in the `lib` folder which is not reloaded on every request # at the development environment. If the registry was stored within the # model class itself, it would be empty after every code reload (i.e. # every request). # # @return [Decidim::BlockRegistry] The registry of the authorization # transfer handlers. def registry Decidim.authorization_transfer_registry end # Expose the methods provided by the registry singleton through the model # class. # # @!method register(name) # Registers an authorization transfer handler for a specific use case in # any modules that need to handle authorization transfers. # @example # Decidim::AuthorizationTransfer.register(:my_module) do |transfer, auth_handler| # transfer.move_records(Decidim::MyModule::FooBar, :decidim_user_id) # end # @param name [Symbol] The name for the block, e.g. `:proposals`. # @yield [transfer, authorization_handler] Handles the authorization # transfer for the given context. This is called before the # authorization is transferred over to the new user and granted. # @yieldparam [Decidim::AuthorizationTransfer] The authorization transfer # being processed which stores all the necessary information about the # transfer, related authorization and related user and source user (i.e. # the user from which the authorization is transferred from). # @yieldparam [Decidim::AuthorizationHandler] The authorization handler # in charge of the current authorization action which initiated the # transfer due to the duplicate unique ID it detected. This handler can # provide access to the metadata from the new authorization action that # caused the conflict to happen. # @return [Proc] The registered block itself. # # @!method unregister(name) # @see Decidim::BlockRegistry#unregister(name) # # @!method unregister(*names) # @see Decidim::BlockRegistry#unregister(*names) # # @!method registrations # @see Decidim::BlockRegistry#registrations delegate :register, :unregister, :registrations, to: :registry # Returns the enabled status for the authorization transfers. True by # default. # # @return [Boolean] True if the authorization transfers are enabled and # false if they are disabled. def enabled? enable! if @enabled.nil? @enabled end # Enables the authorization transfer functionality. By default the # functionality is already enabled, so this method is only needed in case # the enabled state is changed e.g. during tests. # # @return [Boolean] Returns the enabled status after the enabling, i.e. # true. def enable! @enabled = true end # Disables the authorization transfer functionality. # # @return [Boolean] Returns the enabled status after the disabling, i.e. # false. def disable! @enabled = false end # Performs the authorization transfer for the provided authorization object # with the provided handler which is authorizing the user. # # @param authorization [Decidim::Authorization] The authorization object # to be transferred over to the new user indicated by the authorization # handler. # @param handler [Decidim::AuthorizationHandler] The authorization handler # object with all the necessary information for authorizing the new # user. The target user for which the authorization is transferred over # to is fetched from the handler. # @raise [DisabledError] If the functionality is disabled. # @return [Decidim::AuthorizationTransfer] The created authorization # transfer object. def perform!(authorization, handler) raise DisabledError unless enabled? transaction do transfer = create!( authorization:, user: handler.user, source_user: authorization.user ) transfer.announce!(handler) # Update the metadata, transfer to the new user and grant. authorization.attributes = { metadata: handler.metadata, user: handler.user } authorization.grant! transfer end end end # Overwrites the method so that records cannot be modified. # # @return [Boolean] A boolean indicating whether the record is read only. def readonly? !new_record? && !destroyed_by_association end # This announces the transfer to external modules that can perform their own # actions during the authorization transfer. This is called before the # authorization is transferred to the new user allowing different modules to # transfer their records from the source user to the user that the # authorization is being transferred to. Note that during the publish event, # the authorization record is still pointing to the source user but the # transfer record itself available for the event is mapped correctly to the # target user. # # @param handler [Decidim::AuthorizationHandler] The authorization handler # for the transfer procedure which contains all the necessary information # about the data that was submitted from the authorization action. It is # yielded to the registered transfer handlers as the second argument of # the registered block. # @raise [DisabledError] If the functionality is disabled. # @return [Array] An array of the blocks that were processed during # the transfer. def announce!(handler) raise DisabledError unless self.class.enabled? self.class.registrations.values.each do |block| block.call(self, handler) end end # Creates a presenter instance for this record and returns it. # # @return [Decidim::AuthorizationTransferPresenter] The presenter object. def presenter AuthorizationTransferPresenter.new(self) end # Returns information about the transfer in the described format. The # returned hash contains information about the transferred records as the # record type (class name as string) as its keys and an informational hash # as its values with the following keys: # - :class - The class constant of the transferred records # - :count - Number of the records of this type that were transferred # - :name - An instance of ActiveModel::Name for the record class # # @example Format of the returned information hash # { # "Decidim::Foo" => { # class: Decidim::Foo, # count: 123, # name: ActiveModel::Name.new(Decidim::Foo) # }, # "Decidim::Bar" => { # class: Decidim::Bar, # count: 456, # name: ActiveModel::Name.new(Decidim::Bar) # } # } # # @return [Hash Hash Integer, ActiveModel::Name>>] The # information hash created for the transfer. def information {}.tap do |types| records.find_each do |record| resource_class = record.type.safe_constantize next unless resource_class types[record.type] ||= { class: resource_class, count: 0 } types[record.type][:count] += 1 end end end # Handles moving records from the source user to the user to which the # authorization is being transferred to. This updates the provided user # column of the provided class to the user being authorized. # # @param resource_class The resource class for which records should # be transferred for. # @param user_column The User column to be updated for the # records. It is updated with the user mapped to the transfer, i.e. the # target user. # @return [Array] An array of the # created authorization transfer records. def move_records(resource_class, user_column) transferrable_records = resource_class.where(user_column => source_user_id) transferrable_ids = transferrable_records.pluck(:id) # rubocop:disable Rails::SkipsModelValidations transferrable_records.update_all(user_column => user_id) # rubocop:enable Rails::SkipsModelValidations records.create!( transferrable_ids.map do |resource_id| { resource_type: resource_class.name, resource_id: } end ) end end end