class Softwear::Auth::StandardModel

Constants

REMOTE_ATTRIBUTES
INSTANCE METHODS ======================

Attributes

auth_server_went_down_at[RW]
email_when_down_after[RW]
expire_query_cache_every[W]
query_cache[W]
query_cache_expiry[W]
sent_auth_server_down_email[RW]
time_before_down_email[RW]
total_query_cache[RW]
persisted[R]
persisted?[R]

Public Class Methods

abstract_class?() click to toggle source
# File lib/softwear/auth/standard_model.rb, line 18
def abstract_class?
  true
end
all() click to toggle source

Returns an array of all registered users

# File lib/softwear/auth/standard_model.rb, line 375
def all
  json = validate_response query "all"

  objects = JSON.parse(json).map(&method(:new))
  objects.each { |u| u.instance_variable_set(:@persisted, true) }
end
arel_table() click to toggle source
# File lib/softwear/auth/standard_model.rb, line 132
def arel_table
  @arel_table ||= Arel::Table.new(model_name.plural, self)
end
auth(token, app_name = nil) click to toggle source

Given a valid signin token:

Returns the authenticated user for the given token

Given an invalid signin token:

Returns false
# File lib/softwear/auth/standard_model.rb, line 401
def auth(token, app_name = nil)
  response = validate_response query "auth #{app_name || Figaro.env.hub_app_name} #{token}"

  return false unless response =~ /^yes .+$/

  _yes, json = response.split(' ', 2)
  object = new(JSON.parse(json))
  object.instance_variable_set(:@persisted, true)
  object
end
auth_server_down?() click to toggle source

Returns true if the authentication server was unreachable for the previous query.

# File lib/softwear/auth/standard_model.rb, line 34
def auth_server_down?
  !!auth_server_went_down_at
end
auth_server_down_mailer() click to toggle source

Override this in your subclasses! The mailer should have auth_server_down(time) and auth_server_up(time)

# File lib/softwear/auth/standard_model.rb, line 55
def auth_server_down_mailer
  # override me
end
auth_server_host() click to toggle source

Host of the auth server, from 'auth_server_endpoint' env variable. Defaults to localhost.

# File lib/softwear/auth/standard_model.rb, line 149
def auth_server_host
  endpoint = Figaro.env.auth_server_endpoint
  if endpoint.blank?
    'localhost'
  elsif endpoint.include?(':')
    endpoint.split(':').first
  else
    endpoint
  end
end
auth_server_port() click to toggle source

Port of the auth server, from 'auth_server_endpoint' env variable. Defaults to 2900.

# File lib/softwear/auth/standard_model.rb, line 164
def auth_server_port
  endpoint = Figaro.env.auth_server_endpoint
  if endpoint.try(:include?, ':')
    endpoint.split(':').last
  else
    2900
  end
end
base_class() click to toggle source
# File lib/softwear/auth/standard_model.rb, line 64
def base_class
  self
end
default_socket() click to toggle source
# File lib/softwear/auth/standard_model.rb, line 173
def default_socket
  @default_socket ||= TCPSocket.open(auth_server_host, auth_server_port)
end
expire_query_cache() click to toggle source

Expires the query cache, setting a new expiration time as well as merging with the previous query cache, in case of an auth server outage.

# File lib/softwear/auth/standard_model.rb, line 209
def expire_query_cache
  before = Time.now
  if total_query_cache
    query_cache.each_pair do |key, value|
      total_query_cache[key] = value
    end
  else
    self.total_query_cache = query_cache.clone
  end

  query_cache.clear
  query_cache['_expire_at'] = (query_cache_expiry || 1.hour).from_now
  after = Time.now

  record(before, after, "Authentication Expire Cache", "")
end
filter_all(method, options) click to toggle source
# File lib/softwear/auth/standard_model.rb, line 352
def filter_all(method, options)
  all.send(method) do |user|
    options.all? { |field, wanted_value| user.send(field) == wanted_value }
  end
end
find(target_id) click to toggle source

Finds a user with the given ID

# File lib/softwear/auth/standard_model.rb, line 336
def find(target_id)
  json = validate_response query "get #{target_id}"

  if json == 'nosuchuser'
    nil
  else
    object = new(JSON.parse(json))
    object.instance_variable_set(:@persisted, true)
    object
  end

rescue JSON::ParserError => e
  Rails.logger.error "Bad user model JSON: ``` #{json} ```"
  nil
end
find_by(options) click to toggle source

Finds a user with the given attributes (just queries for 'all' and uses ruby filters)

# File lib/softwear/auth/standard_model.rb, line 361
def find_by(options)
  filter_all(:find, options)
end
force_query(message) click to toggle source

Runs a query through the server without error or cache checking.

# File lib/softwear/auth/standard_model.rb, line 304
def force_query(message)
  before = Time.now
  response = raw_query(message)
  after = Time.now

  record(before, after, "Authentication Query (forced)", message)
  response
end
has_many(assoc, options = {}) click to toggle source

Not a fully featured has_many - must specify foreign_key if the association doesn't match the model name, through is inefficient.

# File lib/softwear/auth/standard_model.rb, line 93
        def has_many(assoc, options = {})
          assoc = assoc.to_s

          if through = options[:through]
            source = options[:source] || assoc

            class_eval <<-RUBY, __FILE__, __LINE__ + 1
              def #{assoc}
                #{through}.flat_map(&:#{source})
              end
            RUBY

          else
            class_name  = options[:class_name]  || assoc.singularize.camelize
            foreign_key = options[:foreign_key] || 'user_id'

            class_eval <<-RUBY, __FILE__, __LINE__ + 1
              def #{assoc}
                #{class_name}.where(#{foreign_key}: id)
              end
            RUBY
          end
        end
logger() click to toggle source

Overridable logger method used when recording query benchmarks

# File lib/softwear/auth/standard_model.rb, line 415
def logger
  Rails.logger
end
new(*args) click to toggle source
Calls superclass method
# File lib/softwear/auth/standard_model.rb, line 76
def new(*args)
  if args.size == 3
    assoc_class = args[2].owner.class.name
    assoc_name = args[2].reflection.name
    raise "Unsupported user association: #{assoc_class}##{assoc_name}. If this is a belongs_to "\
          "association, you may have #{assoc_class} include Softwear::Auth::BelongsToUser and call "\
          "`belongs_to_user_called :#{assoc_name}' instead of the traditional rails method."
  else
    super
  end
end
new(attributes = {}) click to toggle source
# File lib/softwear/auth/standard_model.rb, line 448
def initialize(attributes = {})
  update_attributes(attributes)
end
of_role(*roles) click to toggle source

Returns array of all users with the given roles

# File lib/softwear/auth/standard_model.rb, line 385
def of_role(*roles)
  roles = roles.flatten.compact
  return [] if roles.empty?

  json = validate_response query "ofrole #{Figaro.env.hub_app_name} #{roles.split(' ')}"

  objects = JSON.parse(json).map(&method(:new))
  objects.each { |u| u.instance_variable_set(:@persisted, true) }
end
pluck(*attrs) click to toggle source

Pretty much a map function - for activerecord compatibility.

# File lib/softwear/auth/standard_model.rb, line 120
def pluck(*attrs)
  if attrs.size == 1
    all.map do |user|
      user.send(attrs.first)
    end
  else
    all.map do |user|
      attrs.map { |a| user.send(a) }
    end
  end
end
primary_key() click to toggle source
# File lib/softwear/auth/standard_model.rb, line 60
def primary_key
  :id
end
query(message) click to toggle source

Queries the authentication server only if there isn't a cached response. Also keeps track of whether or not the server is reachable, and sends emails when the server goes down and back up.

# File lib/softwear/auth/standard_model.rb, line 231
def query(message)
  before = Time.now

  expire_at = query_cache['_expire_at']
  expire_query_cache if expire_at.blank? || Time.now > expire_at

  if cached_response = query_cache[message]
    response = cached_response
    action = "Authentication Cache"
  else
    begin
      response = raw_query(message)
      action = "Authentication Query"
      query_cache[message] = response

      if auth_server_went_down_at
        self.auth_server_went_down_at = nil

        if sent_auth_server_down_email
          self.sent_auth_server_down_email = false
          if (mailer = auth_server_down_mailer) && mailer.respond_to?(:auth_server_up)
            mailer.auth_server_up(Time.now).deliver_now
          end
        end
      end

    rescue AuthServerError => e
      raise unless total_query_cache

      old_response = total_query_cache[message]
      if old_response
        response = old_response
        action = "Authentication Cache (due to error)"
        Rails.logger.error "AUTHENTICATION: The authentication server encountered an error. "\
                           "You should probably check the auth server's logs. "\
                           "A cached response was used."
      else
        raise
      end

    rescue AuthServerDown => e
      if auth_server_went_down_at.nil?
        self.auth_server_went_down_at = Time.now
        expire_query_cache

      elsif auth_server_went_down_at > (time_before_down_email || 5.minutes).ago
        unless sent_auth_server_down_email
          self.sent_auth_server_down_email = true

          if (mailer = auth_server_down_mailer) && mailer.respond_to?(:auth_server_down)
            mailer.auth_server_down(auth_server_went_down_at).deliver_now
          end
        end
      end

      old_response = total_query_cache[message]
      if old_response
        response = old_response
        action = "Authentication Cache (server down)"
      else
        raise AuthServerDown, "An uncached query was attempted, and the authentication server is down."
      end
    end
  end
  after = Time.now

  record(before, after, action, message)
  response
end
query_cache() click to toggle source

The query cache takes message keys (such as “get 12”) with response values straight from the server. So yes, this will cache error responses. You can clear this with <User Class>.query_cache.clear or <User Class>.query_cache = nil

# File lib/softwear/auth/standard_model.rb, line 43
def query_cache
  @query_cache ||= ThreadSafe::Cache.new
end
query_cache_expiry() click to toggle source
# File lib/softwear/auth/standard_model.rb, line 47
def query_cache_expiry
  @query_cache_expiry || Figaro.env.query_cache_expiry.try(:to_f) || 1.hour
end
raw_query(message) click to toggle source

Bare minimum query function - sends a message and returns the response, and handles a broken socket. query and force_query call this function.

# File lib/softwear/auth/standard_model.rb, line 181
def raw_query(message)
  begin
    default_socket.puts message

  rescue Errno::EPIPE => e
    @default_socket = TCPSocket.open(auth_server_host, auth_server_port)
    @default_socket.puts message
  end

  response = default_socket.gets.try(:chomp)
  if response.nil?
    @default_socket.close rescue nil
    @default_socket = nil
    return raw_query(message)
  end
  response

rescue Errno::ECONNREFUSED => e
  raise AuthServerDown, "Unable to connect to the authentication server."

rescue Errno::ETIMEDOUT => e
  raise AuthServerDown, "Connection to authentication server timed out."
end
record(before, after, type, body) click to toggle source

This is only used to record how long it takes to perform queries for development.

# File lib/softwear/auth/standard_model.rb, line 139
def record(before, after, type, body)
  ms = (after - before) * 1000
  # The garbage in this string gives us the bold and color
  Rails.logger.info "  \033[1m\033[33m#{type} (#{'%.1f' % ms}ms)\033[0m #{body}"
end
relation_delegate_class(*) click to toggle source
# File lib/softwear/auth/standard_model.rb, line 68
def relation_delegate_class(*)
  self
end
unscoped() click to toggle source
# File lib/softwear/auth/standard_model.rb, line 72
def unscoped
  self
end
validate_response(response_string) click to toggle source

Expects a response string returned from query and raises an error for the following cases:

# File lib/softwear/auth/standard_model.rb, line 321
def validate_response(response_string)
  case response_string
  when 'denied'  then raise AccessDeniedError,   "Denied"
  when 'invalid' then raise InvalidCommandError, "Invalid command"
  when 'sorry'
    expire_query_cache
    raise AuthServerError, "Authentication server encountered an error"
  else
    response_string
  end
end
where(options) click to toggle source

Finds users with the given attributes (just queries for 'all' and uses ruby filters)

# File lib/softwear/auth/standard_model.rb, line 368
def where(options)
  filter_all(:select, options)
end

Public Instance Methods

force_query(*a) click to toggle source
# File lib/softwear/auth/standard_model.rb, line 440
def force_query(*a)
  self.class.force_query(*a)
end
full_name() click to toggle source
# File lib/softwear/auth/standard_model.rb, line 479
def full_name
  "#{@first_name} #{@last_name}"
end
logger() click to toggle source
# File lib/softwear/auth/standard_model.rb, line 443
def logger
  self.class.logger
end
query(*a) click to toggle source

Various class methods accessible on instances

# File lib/softwear/auth/standard_model.rb, line 434
def query(*a)
  self.class.query(*a)
end
raw_query(*a) click to toggle source
# File lib/softwear/auth/standard_model.rb, line 437
def raw_query(*a)
  self.class.raw_query(*a)
end
reload() click to toggle source
# File lib/softwear/auth/standard_model.rb, line 471
def reload
  json = validate_response query "get #{id}"

  update_attributes(JSON.parse(json))
  @persisted = true
  self
end
role?(*wanted_roles) click to toggle source
# File lib/softwear/auth/standard_model.rb, line 487
def role?(*wanted_roles)
  return true if wanted_roles.empty?

  if @roles.nil?
    query("role #{Figaro.env.hub_app_name} #{id} #{wanted_roles.join(' ')}") == 'yes'
  else
    wanted_roles.any? { |r| @roles.include?(r.to_s) }
  end
end
to_json() click to toggle source
# File lib/softwear/auth/standard_model.rb, line 461
def to_json
  {
    id:         @id,
    email:      @email,
    first_name: @first_name,
    last_name:  @last_name
  }
    .to_json
end
update_attributes(attributes={}) click to toggle source
# File lib/softwear/auth/standard_model.rb, line 452
def update_attributes(attributes={})
  return if attributes.blank?
  attributes = attributes.with_indifferent_access

  REMOTE_ATTRIBUTES.each do |attr|
    instance_variable_set("@#{attr}", attributes[attr])
  end
end
valid_password?(pass) click to toggle source
# File lib/softwear/auth/standard_model.rb, line 483
def valid_password?(pass)
  query("pass #{id} #{pass}") == 'yes'
end