# (c) 2017 Ribose Inc. # # Adds attr_accessors that mask an object's attributes module AttrMasker autoload :Version, "attr_masker/version" autoload :Error, "attr_masker/error" autoload :Performer, "attr_masker/performer" module Maskers autoload :Replacing, "attr_masker/maskers/replacing" autoload :SIMPLE, "attr_masker/maskers/simple" end require "attr_masker/railtie" if defined?(Rails) def self.extended(base) # :nodoc: base.class_eval do # Only include the dangerous instance methods during the Rake task! include InstanceMethods attr_writer :attr_masker_options @attr_masker_options, @masker_attributes = {}, {} end end # Generates attr_accessors that mask attributes transparently # # Options (any other options you specify are passed to the masker's mask # methods) # # :marshal => If set to true, attributes will be marshaled as well as masker. This is useful if you're planning # on masking something other than a string. Defaults to false unless you're using it with ActiveRecord # or DataMapper. # # :marshaler => The object to use for marshaling. Defaults to Marshal. # # :dump_method => The dump method name to call on the :marshaler object to. Defaults to 'dump'. # # :load_method => The load method name to call on the :marshaler object. Defaults to 'load'. # # :masker => The object to use for masking. It must respond to +#mask+. Defaults to AttrMasker::Maskers::Simple. # # :if => Attributes are only masker if this option evaluates to true. If you pass a symbol representing an instance # method then the result of the method will be evaluated. Any objects that respond to :call are evaluated as well. # Defaults to true. # # :unless => Attributes are only masker if this option evaluates to false. If you pass a symbol representing an instance # method then the result of the method will be evaluated. Any objects that respond to :call are evaluated as well. # Defaults to false. # # You can specify your own default options # # class User # # now all attributes will be encoded and marshaled by default # attr_masker_options.merge!(:marshal => true, :some_other_option => true) # attr_masker :configuration # end # # # Example # # class User # attr_masker :email, :credit_card # attr_masker :configuration, :marshal => true # end # # @user = User.new # @user.masker_email # nil # @user.email? # false # @user.email = 'test@example.com' # @user.email? # true # @user.masker_email # returns the masker version of 'test@example.com' # # @user.configuration = { :time_zone => 'UTC' } # @user.masker_configuration # returns the masker version of configuration # # See README for more examples def attr_masker(*attributes) options = { :if => true, :unless => false, :column_name => nil, :marshal => false, :marshaler => Marshal, :dump_method => "dump", :load_method => "load", :masker => AttrMasker::Maskers::SIMPLE, }.merge!(attr_masker_options).merge!(attributes.last.is_a?(Hash) ? attributes.pop : {}) attributes.each do |attribute| masker_attributes[attribute.to_sym] = options.merge(attribute: attribute.to_sym) end end # Default options to use with calls to attr_masker # XXX:Keep # # It will inherit existing options from its superclass def attr_masker_options @attr_masker_options ||= superclass.attr_masker_options.dup end # Checks if an attribute is configured with attr_masker # XXX:Keep # # Example # # class User # attr_accessor :name # attr_masker :email # end # # User.attr_masker?(:name) # false # User.attr_masker?(:email) # true def attr_masker?(attribute) masker_attributes.has_key?(attribute.to_sym) end # masks a value for the attribute specified # XXX:modify # # Example # # class User # attr_masker :email # end # # masker_email = User.mask(:email, 'test@example.com') def mask(attribute, value, options = {}) options = masker_attributes[attribute.to_sym].merge(options) # if options[:if] && !options[:unless] && !value.nil? && !(value.is_a?(String) && value.empty?) if options[:if] && !options[:unless] value = options[:marshal] ? options[:marshaler].send(options[:dump_method], value) : value.to_s masker_value = options[:masker].call(options.merge!(value: value)) masker_value else value end end # Contains a hash of masker attributes with virtual attribute names as keys # and their corresponding options as values # XXX:Keep # # Example # # class User # attr_masker :email # end # # User.masker_attributes # { :email => { :attribute => 'masker_email' } } def masker_attributes @masker_attributes ||= superclass.masker_attributes.dup end # Forwards calls to :mask_#{attribute} to the corresponding mask method # if attribute was configured with attr_masker # # Example # # class User # attr_masker :email # end # # User.mask_email('SOME_masker_EMAIL_STRING') def method_missing(method, *arguments, &block) if method.to_s =~ /^mask_(.+)$/ && attr_masker?($1) send(:mask, $1, *arguments) else super end end module InstanceMethods # masks a value for the attribute specified using options evaluated in the current object's scope # # Example # # class User # attr_accessor :secret_key # attr_masker :email # # def initialize(secret_key) # self.secret_key = secret_key # end # end # # @user = User.new('some-secret-key') # @user.mask(:email, 'test@example.com') def mask(attribute, value=nil) value = self.send(attribute) if value.nil? self.class.mask(attribute, value, evaluated_attr_masker_options_for(attribute)) end protected # Returns attr_masker options evaluated in the current object's scope for the attribute specified # XXX:Keep def evaluated_attr_masker_options_for(attribute) self.class.masker_attributes[attribute.to_sym].inject({}) do |hash, (option, value)| if %i[if unless].include?(option) hash.merge!(option => evaluate_attr_masker_option(value)) else hash.merge!(option => value) end end end # Evaluates symbol (method reference) or proc (responds to call) options # XXX:Keep # # If the option is not a symbol or proc then the original option is returned def evaluate_attr_masker_option(option) if option.is_a?(Symbol) && respond_to?(option) send(option) elsif option.respond_to?(:call) option.call(self) else option end end end end Object.extend AttrMasker