Class: ApiKeys::ApiKey

Inherits:
ActiveRecord::Base
  • Object
show all
Defined in:
lib/api_keys/models/api_key.rb

Overview

The core ActiveRecord model representing an API key.

Constant Summary collapse

MAX_SCOPES =
100
MAX_SCOPE_BYTESIZE =
128
MAX_METADATA_BYTESIZE =
16_384
IMMUTABLE_IDENTITY_ATTRIBUTES =
%w[
  token_digest digest_algorithm prefix last4 owner_type owner_id key_type environment
].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#tokenObject (readonly)

Attributes & Serialization ==

Expose the plaintext token only immediately after creation



28
29
30
# File 'lib/api_keys/models/api_key.rb', line 28

def token
  @token
end

Instance Method Details

#active?Boolean

Returns:

  • (Boolean)


155
156
157
# File 'lib/api_keys/models/api_key.rb', line 155

def active?
  !revoked? && !expired?
end

#allows_permission?(required_permission) ⇒ Boolean

Check if this key has a specific permission (alias for allows_scope?)

Parameters:

  • required_permission (String, Symbol)

    The permission to check

Returns:

  • (Boolean)

    true if the key has this permission or has no restrictions



289
290
291
# File 'lib/api_keys/models/api_key.rb', line 289

def allows_permission?(required_permission)
  allows_scope?(required_permission)
end

#allows_scope?(required_scope) ⇒ Boolean

Basic scope check. Assumes scopes are stored as an array of strings.

Behavior depends on whether key_types mode is enabled:

  • Simple mode (no key_types): blank scopes means "unrestricted" (all scopes allowed). This preserves backwards compatibility for apps that don't use scopes at all.
  • Key types mode: blank scopes means "no permissions". When you've configured key types with permission ceilings, an empty scope list should deny access, not silently bypass the entire permission system.

Returns:

  • (Boolean)


260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# File 'lib/api_keys/models/api_key.rb', line 260

def allows_scope?(required_scope)
  return false unless respond_to?(:scopes)
  return false unless required_scope.present?
  return false unless scopes.is_a?(Array)

  if scopes.blank?
    return !scope_policy_enabled?
  end

  required = required_scope.to_s
  return false unless scopes.all? { |scope| valid_scope_value?(scope) }
  return false unless scopes.include?(required)

  ceiling = permission_ceiling
  ceiling == :all || ceiling.include?(required)
end

#destroyObject

Override destroy to prevent destroying non-revocable keys



242
243
244
245
# File 'lib/api_keys/models/api_key.rb', line 242

def destroy
  raise ApiKeys::Errors::KeyNotRevocableError unless revocable?
  super
end

#destroy!Object



247
248
249
250
# File 'lib/api_keys/models/api_key.rb', line 247

def destroy!
  raise ApiKeys::Errors::KeyNotRevocableError unless revocable?
  super
end

#environment_configObject

Returns the configuration hash for this key's environment



217
218
219
220
221
# File 'lib/api_keys/models/api_key.rb', line 217

def environment_config
  return nil if environment.blank?
  configured_pair = ApiKeys.configuration.environments&.find { |name, _settings| name.to_s == environment.to_s }
  configured_pair&.last
end

#expired?Boolean

Returns:

  • (Boolean)


151
152
153
# File 'lib/api_keys/models/api_key.rb', line 151

def expired?
  expires_at? && expires_at <= Time.current
end

#inspectObject

Keep credentials and credential-derived values out of logs and consoles.



167
168
169
170
171
172
173
174
# File 'lib/api_keys/models/api_key.rb', line 167

def inspect
  attributes = %w[id prefix last4 name owner_type owner_id key_type environment expires_at revoked_at]
               .select { |attribute_name| has_attribute?(attribute_name) }
               .map { |attribute_name| "#{attribute_name}: #{attribute_for_inspect(attribute_name)}" }
  "#<#{self.class.name} #{attributes.join(', ')}>"
rescue StandardError
  "#<#{self.class.name}>"
end

#key_type_configObject

Returns the configuration hash for this key's type



210
211
212
213
214
# File 'lib/api_keys/models/api_key.rb', line 210

def key_type_config
  return nil if key_type.blank?
  configured_pair = ApiKeys.configuration.key_types&.find { |type, _settings| type.to_s == key_type.to_s }
  configured_pair&.last
end

#masked_tokenObject

Provides a masked version of the token for display (e.g., ak_live_••••rj4p) Requires the plaintext token to be available (only right after creation).



295
296
297
298
299
300
301
302
303
304
# File 'lib/api_keys/models/api_key.rb', line 295

def masked_token
  # return "[Token not available]" unless token # No longer needed
  # Show prefix, 4 bullets, last 4 chars of the random part
  # random_part = token.delete_prefix(prefix) # No longer needed
  # "#{prefix}••••#{random_part.last(4)}" # No longer needed

  # Use the stored prefix and last4 attributes
  return "[Invalid Key Data]" unless prefix.present? && last4.present?
  "#{prefix}••••#{last4}"
end

#permissionsArray<String>

Alias for scopes - provides a more user-friendly API that matches the configuration DSL where key types use permissions for scope ceiling. Note: We use a method instead of alias_method because scopes is defined dynamically via the attribute API in the engine initializer.

Returns:

  • (Array<String>)

    The permissions (scopes) assigned to this key



282
283
284
# File 'lib/api_keys/models/api_key.rb', line 282

def permissions
  scopes
end

#pretty_print(printer) ⇒ Object



176
177
178
# File 'lib/api_keys/models/api_key.rb', line 176

def pretty_print(printer)
  printer.text(inspect)
end

#public_key_type?Boolean

Returns true if this key type is configured as public AND non-revocable. Only these keys have their plaintext token stored in metadata for later viewing. This is used for publishable keys that are designed to be embedded in distributed apps.

Returns:

  • (Boolean)


226
227
228
229
230
231
# File 'lib/api_keys/models/api_key.rb', line 226

def public_key_type?
  return false if key_type.blank?
  config = key_type_config
  return false if config.nil?
  config[:public] == true && config[:revocable] == false
end

#reloadObject

The plaintext token is an ephemeral creation-time value. Active Record's reload does not clear arbitrary instance variables, so clear it explicitly.



161
162
163
164
# File 'lib/api_keys/models/api_key.rb', line 161

def reload(...)
  @token = nil
  super
end

#revocable?Boolean

Returns true if this key can be revoked/destroyed Keys without a key_type (legacy) are always revocable Keys with a key_type check the configuration

Returns:

  • (Boolean)


202
203
204
205
206
207
# File 'lib/api_keys/models/api_key.rb', line 202

def revocable?
  return true if key_type.blank?
  config = key_type_config
  return false if config.nil?
  config.fetch(:revocable, true)
end

#revoke!Object

Instance Methods ==



142
143
144
145
# File 'lib/api_keys/models/api_key.rb', line 142

def revoke!
  raise ApiKeys::Errors::KeyNotRevocableError unless revocable?
  update!(revoked_at: Time.current)
end

#revoked?Boolean

Returns:

  • (Boolean)


147
148
149
# File 'lib/api_keys/models/api_key.rb', line 147

def revoked?
  revoked_at.present?
end

#scopes=(value) ⇒ Object

Override scopes setter to auto-clean blank values. This handles the common case where form checkboxes submit empty strings. Works for both create and update operations.



36
37
38
39
40
41
42
43
44
45
46
# File 'lib/api_keys/models/api_key.rb', line 36

def scopes=(value)
  cleaned = if value.is_a?(Array)
              value
                .map { |scope| scope.is_a?(Symbol) ? scope.to_s : scope }
                .reject { |scope| scope.respond_to?(:blank?) && scope.blank? }
                .uniq
            else
              value
            end
  super(cleaned)
end

#serializable_hash(options = nil) ⇒ Object

Rendering a model as JSON must never expose the verification digest or a public token stored in the reserved metadata field. Call #viewable_token explicitly when an authenticated UI intentionally needs a public token.



183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/api_keys/models/api_key.rb', line 183

def serializable_hash(options = nil)
  serialized = super(options)
  serialized.delete("token_digest")
  serialized.delete(:token_digest)

   = serialized["metadata"] || serialized[:metadata]
  if .is_a?(Hash)
     = .dup
    .delete("token")
    .delete(:token)
    serialized[serialized.key?("metadata") ? "metadata" : :metadata] = 
  end

  serialized
end

#viewable_tokenString?

Returns the stored plaintext token for public, non-revocable keys. Returns nil for all other key types (the token is only available at creation time).

Returns:

  • (String, nil)

    The full plaintext token, or nil if not stored



236
237
238
239
# File 'lib/api_keys/models/api_key.rb', line 236

def viewable_token
  return nil unless public_key_type?
  &.dig("token")
end