Class: ShortLivedData

Inherits:
RailsBase::ApplicationRecord show all
Defined in:
app/models/short_lived_data.rb

Overview

Schema Information

Table name: short_lived_data

id                      :bigint           not null, primary key
user_id                 :integer          not null
data                    :string(255)      not null
reason                  :string(255)
death_time              :datetime         not null
extra                   :string(255)
created_at              :datetime         not null
updated_at              :datetime         not null
exclusive_use_count     :integer          default(0)
exclusive_use_count_max :integer

Constant Summary collapse

DEFAULT_TIME_TO_LIVE =
1.hour.freeze
LENGTH_OF_HEX =
64.freeze
MAX_ATTEMPTS =
10.freeze
VALID_DATA_USE_LENGTH =
[:numeric, :alphanumeric, :hex].freeze
VALID_DATA_NON_LENGTH =
[:uuid]
VALID_DATA_USE =
[VALID_DATA_NON_LENGTH, VALID_DATA_USE_LENGTH].flatten.freeze

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from RailsBase::ApplicationRecord

_magically_defined_time_objects

Class Method Details

.create_data_key(user:, max_use: nil, data: nil, data_use: :alphanumeric, expires_at: nil, ttl: DEFAULT_TIME_TO_LIVE, reason: 'default', length: LENGTH_OF_HEX, extra: nil) ⇒ Object

ShortLivedData.create_data_key(user: User.first, ttl: 5.hours)



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'app/models/short_lived_data.rb', line 29

def create_data_key(user:, max_use: nil, data: nil, data_use: :alphanumeric, expires_at: nil, ttl: DEFAULT_TIME_TO_LIVE, reason: 'default', length: LENGTH_OF_HEX, extra: nil)
  raise ":ttl is expected to be an ActiveSupport::Duration" unless ttl.is_a?(ActiveSupport::Duration)

  if data.nil?
    data = generate_secure_datum(data_use, length: length)
    attempt = 0
    while get_by_data(data: data, reason: reason)
      Rails.logger.warn "Data key already in use for #{data_use}. Attempt #{attempt} for reason #{reason} for user_id #{user.id}"
      data = generate_secure_hex(data_use, length: length)
      attempt +=1
      raise "Failed to generate unique id. Attempted #{attempt} times" if attempt >= MAX_ATTEMPTS
    end
  end

  # expires at takes precedence since this is a dangerous oeration and should only be done with caution
  # dangerous because of time zone checks --- we dont do that
  death_time = Time.now + ttl
  death_time = expires_at if expires_at

  create(user_id: user.id, data: data, death_time: death_time, reason: reason, extra: extra, exclusive_use_count_max: max_use)
end

.find_datum(data:, reason: nil, access_count: true) ⇒ Object



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'app/models/short_lived_data.rb', line 70

def find_datum(data:, reason: nil, access_count: true)
  datum = get_by_data(data: data, reason: reason)

  params = {
    user: datum&.user,
    use_count: datum&.exclusive_use_count,
    max_use_count: datum&.exclusive_use_count_max,
    valid: datum&.is_valid? || false,
    invalid_reason: datum&.invalid_reason || ['Forbidden. Invalid usecase'],
    found: !datum.nil?,
    extra: datum&.extra,
    access_count_proc: -> { datum&.add_access_count! }
  }
  datum&.add_access_count! if access_count

  return params unless params[:valid]

  if reason && (datum&.reason.to_sym != reason.to_sym)
    params[:valid] = false
    params[:invalid_reason] = ['Unknown reason for datum field']
  end

  params
end

.generate_secure_datum(data_use, length: LENGTH_OF_HEX) ⇒ Object



57
58
59
60
61
62
63
64
65
66
67
68
# File 'app/models/short_lived_data.rb', line 57

def generate_secure_datum(data_use, length: LENGTH_OF_HEX)
  case data_use.to_sym
  when :numeric
    return rand.to_s[2..(2+(length-1))]
  when *VALID_DATA_USE_LENGTH
    return SecureRandom.public_send(data_use, length)
  when *VALID_DATA_NON_LENGTH
    return SecureRandom.public_send(data_use)
  else
    raise ArgumentError, "Unexpected data_use: Expected #{VALID_DATA_USE}. given [#{data_use}]"
  end
end

.get_by_data(data:, reason: nil) ⇒ Object



51
52
53
54
55
# File 'app/models/short_lived_data.rb', line 51

def get_by_data(data:, reason: nil)
  # data is indexed and uniq
  params = { data: data, reason: reason }.compact
  where(params).first
end

Instance Method Details

#add_access_count!Object



96
97
98
99
100
101
# File 'app/models/short_lived_data.rb', line 96

def add_access_count!
  # only update if count is valid and we can add things -- save db call
  return false unless used_count_valid?

  update(exclusive_use_count: exclusive_use_count + 1)
end

#invalid_reasonObject



103
104
105
106
107
108
# File 'app/models/short_lived_data.rb', line 103

def invalid_reason
  arr = []
  arr << 'too many uses' unless used_count_valid?
  arr << 'expired' unless still_alive?
  arr
end

#is_valid?Boolean

Returns:

  • (Boolean)


110
111
112
# File 'app/models/short_lived_data.rb', line 110

def is_valid?
  used_count_valid? && still_alive?
end

#still_alive?Boolean

Returns:

  • (Boolean)


114
115
116
# File 'app/models/short_lived_data.rb', line 114

def still_alive?
  (death_time).to_f > Time.now.to_f
end

#used_count_valid?Boolean

Returns:

  • (Boolean)


118
119
120
121
122
# File 'app/models/short_lived_data.rb', line 118

def used_count_valid?
  return true if exclusive_use_count_max.nil?

  return exclusive_use_count < exclusive_use_count_max
end

#userObject



124
125
126
# File 'app/models/short_lived_data.rb', line 124

def user
  @user ||= User.find(user_id)
end

#user=(u) ⇒ Object



128
129
130
131
# File 'app/models/short_lived_data.rb', line 128

def user=(u)
  update(user_id: u.id)
  u.id
end