3scale/porta

View on GitHub
app/lib/api_authentication/by_access_token.rb

Summary

Maintainability
A
1 hr
Test Coverage
# frozen_string_literal: true

module ApiAuthentication
  module ByAccessToken
    extend ActiveSupport::Concern

    def current_user
      @current_user ||= User.current = authenticated_token&.owner
    end

    included do
      include ApiAuthentication::HttpAuthentication

      class_attribute :_access_token_scopes, instance_accessor: false
      self._access_token_scopes = []

      before_action :verify_access_token_scopes
      around_action :enforce_access_token_permission
      rescue_from ApiAuthentication::ByAccessToken::Error,
                  with: :show_access_key_permission_error
    end

    module ReadOnlyTransaction
      def read_only_transaction?
        ::ApiAuthentication::ByAccessToken::PermissionEnforcer.read_only?
      end
    end

    module ConnectionExtension
      extend ActiveSupport::Concern

      included do
        prepend TransactionMethods
      end

      module TransactionMethods
        include ReadOnlyTransaction

        def begin_db_transaction
          transaction = ::ApiAuthentication::ByAccessToken::PermissionEnforcer.start_transaction
          transaction ? execute(transaction) : super
        end
      end
    end

    module OracleEnhancedConnectionExtension
      extend ActiveSupport::Concern

      included do
        prepend TransactionMethods
      end

      module TransactionMethods
        include ReadOnlyTransaction

        def begin_db_transaction
          super

          transaction = ::ApiAuthentication::ByAccessToken::PermissionEnforcer.set_transaction
          execute(transaction) if transaction
        end
      end
    end

    protected

    module ClassMethods
      def authenticate_access_token(status: 401, **options)
        define_method :authenticate! do
          render status: status, **options unless logged_in?
        end
      end

      def access_token_scopes=(*scopes)
        flattened_scopes = scopes.flatten
        validate_scopes!(flattened_scopes)
        self._access_token_scopes = flattened_scopes
      end

      def access_token_scopes
        _access_token_scopes
      end

      def validate_scopes!(scopes)
        available_scopes = AccessToken::SCOPES.values
        invalid_scopes   = scopes.map(&:to_s) - available_scopes

        raise(ScopeError, "scopes #{invalid_scopes} do not exist") if invalid_scopes.any?
      end
    end

    def access_token_scopes
      self.class.access_token_scopes
    end

    def allowed_scopes
      access_token_scopes.map(&:to_s) & user_allowed_scopes
    end

    def show_access_key_permission_error
      self.response_body = nil # prevent double render errors
      render_error "Your access token does not have the correct permissions", status: 403
    end

    def authenticated_token
      return @authenticated_token if instance_variable_defined?(:@authenticated_token)

      @authenticated_token = domain_account.access_tokens.find_from_value(access_token) if access_token
    end

    def enforce_access_token_permission(&block)
      PermissionEnforcer.enforce(authenticated_token, &block)
    end

    def verify_access_token_scopes
      return true unless authenticated_token

      raise PermissionError if !authenticated_token || allowed_scopes.blank?
      raise ScopeError if (allowed_scopes & authenticated_token.scopes).blank?

      true
    end

    def verify_write_permission
      return true unless authenticated_token
      raise PermissionError unless authenticated_token.try(:permission) == PermissionEnforcer::READ_WRITE
    end

    Error = Class.new(StandardError)

    ScopeError = Class.new(Error)
    PermissionError = Class.new(Error)


    module PermissionEnforcer

      extend self

      READ_ONLY = 'ro'
      READ_WRITE = 'rw'

      def set_transaction
        case level
        when READ_ONLY then 'SET TRANSACTION READ ONLY'
        when READ_WRITE then 'SET TRANSACTION READ WRITE'
        end
      end

      def start_transaction
        case level
        when READ_ONLY then 'START TRANSACTION READ ONLY'
        when READ_WRITE then 'START TRANSACTION READ WRITE'
        end
      end

      class EnforceError < StandardError
      end

      def enforce(access_token, &block)
        self.level = access_token&.permission

        return yield unless requires_transaction?

        if connection.transaction_open?
          raise "Can't use read-only Access Token with transactional fixtures" if Rails.env.test?

          error = EnforceError.new("couldn't open new transaction to enforce read-only access token")
          System::ErrorReporting.report_error(error)
        end

        connection.transaction(requires_new: true, &block)
      rescue ActiveRecord::StatementInvalid => error
        if error.message =~ /read(-|\s)only transaction/i
          raise PermissionError, error.message, caller
        else
          raise
        end
      ensure
        Rails.logger.info "PermissionEnforcer#ensure clear level"
        self.level = nil
      end

      def read_only?
        level == READ_ONLY
      end

      private

      def requires_transaction?
        case level
        when READ_ONLY then true
        when READ_WRITE then false
        end
      end

      THREAD_VARIABLE = :__permission_enforcer_level

      def level=(level)
        Rails.logger.info "PermissionEnforcer: level = #{level}"
        Thread.current[THREAD_VARIABLE] = level
      end

      def level
        Thread.current[THREAD_VARIABLE]
      end

      def connection
        ActiveRecord::Base.connection
      end
    end

    private

    def access_token
      @access_token ||= params.fetch(:access_token, &method(:http_authentication))
    end

    def user_allowed_scopes
      @user_allowed_scopes ||= current_user.allowed_access_token_scopes.values
    end
  end
end