nbulaj/doorkeeper-sequel

View on GitHub
lib/doorkeeper-sequel/mixins/application_mixin.rb

Summary

Maintainability
A
1 hr
Test Coverage
# frozen_string_literal: true

require_relative "../validators/redirect_uri_validator"

module DoorkeeperSequel
  module ApplicationMixin
    extend ActiveSupport::Concern

    include SequelCompat
    include Doorkeeper::OAuth::Helpers
    include Doorkeeper::Models::Scopes
    include Doorkeeper::Models::SecretStorable
    include DoorkeeperSequel::RedirectUriValidator

    included do
      plugin :validation_helpers
      plugin :timestamps
      plugin :association_dependencies

      one_to_many :access_grants, class: "Doorkeeper::AccessGrant"
      one_to_many :access_tokens, class: "Doorkeeper::AccessToken"

      add_association_dependencies access_grants: :delete, access_tokens: :delete

      set_allowed_columns :name, :redirect_uri, :scopes, :confidential

      def before_validation
        if new?
          generate_uid
          generate_secret
        end
        super
      end

      def validate
        super
        validates_presence %i[name secret uid]
        validates_unique [:uid]
        validates_redirect_uri :redirect_uri
        validates_includes [true, false], :confidential, allow_missing: true

        validate_scopes_match_configured if enforce_scopes?

        if respond_to?(:validate_owner?)
          validates_presence [:owner_id] if validate_owner?
        end
      end

      # In some cases, Doorkeeper used as a proxy app. In this case database does not have any fields.
      # Even table may not exists on source database.
      # Aliasing this method throws NoMethod Error. Due to this we need to explicitly
      # define confidential? here.
      def confidential?
        confidential.present? && !!confidential
      end
      
      def renew_secret
        @raw_secret = Doorkeeper::OAuth::Helpers::UniqueToken.generate
        secret_strategy.store_secret(self, :secret, @raw_secret)
      end
            
      def as_json(options = {})
        if (respond_to?(:owner) && owner && owner == options[:current_resource_owner]) ||
           options[:as_owner]
          hash = JSON.parse(to_json, symbolize_names: false)
        else
          only = extract_serializable_attributes(options)
          # TODO: Write our own serializer for Hash          
          hash = JSON.parse(to_json(options.merge(only: only)), symbolize_names: false)
        end
        hash["secret"] = plaintext_secret if hash.key?("secret")
        hash
      end
      
      def plaintext_secret
        if secret_strategy.allows_restoring_secrets?
          secret_strategy.restore_secret(self, :secret)
        else
          @raw_secret
        end
      end
      
      def authorized_for_resource_owner?(resource_owner)
        Doorkeeper.configuration.authorize_resource_owner_for_client.call(self, resource_owner)
      end
      
      protected

      def validate_scopes_match_configured
        if scopes.present? && !Doorkeeper::OAuth::Helpers::ScopeChecker.valid?(scope_str: scopes.to_s,
                                                                               server_scopes: Doorkeeper.configuration.scopes)
          scope = "sequel.errors.models.doorkeeper/application.attributes.scopes"
          errors.add(:scopes, I18n.t(:not_match_configured, scope: scope))
        end
      end

      def enforce_scopes?
        Doorkeeper.configuration.enforce_configured_scopes?
      end
    end

    module ClassMethods
      def by_uid_and_secret(uid, secret)
        app = by_uid(uid)
        return unless app
        return app if secret.blank? && !app.confidential?
        return unless app.secret_matches?(secret)

        app
      end

      def by_uid(uid)
        first(uid: uid.to_s)
      end

      def find_by(params)
        first(params)
      end

      def column_names
        columns.map(&:to_s)
      end

      def secret_strategy
        ::Doorkeeper.configuration.application_secret_strategy
      end

      def fallback_secret_strategy
        ::Doorkeeper.configuration.application_secret_fallback_strategy
      end
    end

    def secret_matches?(input)
      # return false if either is nil, since secure_compare depends on strings
      # but Application secrets MAY be nil depending on confidentiality.
      return false if input.nil? || secret.nil?

      # When matching the secret by comparer function, all is well.
      return true if secret_strategy.secret_matches?(input, secret)

      # When fallback lookup is enabled, ensure applications
      # with plain secrets can still be found
      if fallback_secret_strategy
        fallback_secret_strategy.secret_matches?(input, secret)
      else
        false
      end
    end

    private
    
    def extract_serializable_attributes(options = {})
      opts = options.try(:dup) || {}
      only = Array.wrap(opts[:only]).map(&:to_s)
    
      only = if only.blank?
               serializable_attributes
             else
               only & serializable_attributes
             end
    
      only -= Array.wrap(opts[:except]).map(&:to_s) if opts.key?(:except)
      only.uniq
    end
    
    def serializable_attributes
      attributes = %w[id name created_at]
      attributes << "uid" unless confidential?
      attributes
    end
    
    def has_scopes?
      Doorkeeper::Application.columns.include?("scopes")
    end

    def generate_uid
      self.uid = UniqueToken.generate if uid.blank? && new?
    end

    def generate_secret
      return unless secret.blank?

      @raw_secret = UniqueToken.generate
      secret_strategy.store_secret(self, :secret, @raw_secret)
    end
  end
end