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
MAX_RESTRICTION_ENTRIES =
100
MAX_RESTRICTION_ENTRY_BYTESIZE =
255
RESTRICTIONS_COLUMN =
"restrictions"
IMMUTABLE_IDENTITY_ATTRIBUTES =

Deliberately excludes restrictions: owners must be able to tighten a request policy even when the key itself is non-revocable.

%w[
  token_digest digest_algorithm prefix last4 owner_type owner_id key_type environment
].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#expires_at_presetObject

Returns the value of attribute expires_at_preset.



35
36
37
# File 'lib/api_keys/models/api_key.rb', line 35

def expires_at_preset
  @expires_at_preset
end

#tokenObject (readonly)

Attributes & Serialization ==

Expose the plaintext token only immediately after creation



34
35
36
# File 'lib/api_keys/models/api_key.rb', line 34

def token
  @token
end

Class Method Details

.restriction_kinds_for(type_config) ⇒ Object

Single source of truth for a key type's request-restriction ceiling. Omitting the setting allows every supported restriction kind.



396
397
398
399
400
# File 'lib/api_keys/models/api_key.rb', line 396

def self.restriction_kinds_for(type_config)
  return ApiKeys::Restrictions::KINDS.dup unless type_config.is_a?(Hash) && type_config.key?(:restrictions)

  Array(type_config[:restrictions]).map(&:to_sym)
end

.restrictions_column?Boolean

Whether the restrictions column exists. Installations that predate v0.5.0 keep working untouched until they run the generator.

Returns:

  • (Boolean)


388
389
390
391
392
# File 'lib/api_keys/models/api_key.rb', line 388

def self.restrictions_column?
  column_names.include?(RESTRICTIONS_COLUMN)
rescue StandardError
  false
end

.revocable_for(type_config) ⇒ Object

Single source of truth for lifecycle policy in model and dashboard code.



403
404
405
# File 'lib/api_keys/models/api_key.rb', line 403

def self.revocable_for(type_config)
  type_config.is_a?(Hash) && type_config.fetch(:revocable, true)
end

Instance Method Details

#active?Boolean

Returns:

  • (Boolean)


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

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

#allowed_ipsArray<String>

Returns Allowed IP addresses and CIDR ranges.

Returns:

  • (Array<String>)

    Allowed IP addresses and CIDR ranges.



91
92
93
# File 'lib/api_keys/models/api_key.rb', line 91

def allowed_ips
  restrictions.ips
end

#allowed_ips=(value) ⇒ Object

Accepts an array or a raw string ("203.0.113.7, 10.0.0.0/8").



102
103
104
# File 'lib/api_keys/models/api_key.rb', line 102

def allowed_ips=(value)
  self.restrictions = restrictions.to_h.merge("ips" => ApiKeys::Restrictions.normalize_ips(value))
end

#allowed_originsArray<String>

Returns Allowed web origins (hosts and *.host wildcards).

Returns:

  • (Array<String>)

    Allowed web origins (hosts and *.host wildcards).



86
87
88
# File 'lib/api_keys/models/api_key.rb', line 86

def allowed_origins
  restrictions.origins
end

#allowed_origins=(value) ⇒ Object

Accepts an array or a raw string ("example.com, *.example.com") and normalizes it, so host applications never need their own parser.



97
98
99
# File 'lib/api_keys/models/api_key.rb', line 97

def allowed_origins=(value)
  self.restrictions = restrictions.to_h.merge("origins" => ApiKeys::Restrictions.normalize_origins(value))
end

#allowed_restriction_kindsArray<Symbol>

Returns Restriction kinds this key's type permits.

Returns:

  • (Array<Symbol>)

    Restriction kinds this key's type permits.



408
409
410
# File 'lib/api_keys/models/api_key.rb', line 408

def allowed_restriction_kinds
  self.class.restriction_kinds_for(key_type_config)
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



365
366
367
# File 'lib/api_keys/models/api_key.rb', line 365

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)


336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
# File 'lib/api_keys/models/api_key.rb', line 336

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



318
319
320
321
# File 'lib/api_keys/models/api_key.rb', line 318

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

#destroy!Object



323
324
325
326
# File 'lib/api_keys/models/api_key.rb', line 323

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

#environment_configObject

Returns the configuration hash for this key's environment



293
294
295
296
297
# File 'lib/api_keys/models/api_key.rb', line 293

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

#expirable?Boolean

Non-revocable keys are permanent by design; other key types may expire.

Returns:

  • (Boolean)


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

def expirable?
  revocable?
end

#expired?Boolean

Returns:

  • (Boolean)


224
225
226
# File 'lib/api_keys/models/api_key.rb', line 224

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

#inspectObject

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



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

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



286
287
288
289
290
# File 'lib/api_keys/models/api_key.rb', line 286

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).



371
372
373
374
375
376
377
378
379
380
# File 'lib/api_keys/models/api_key.rb', line 371

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



358
359
360
# File 'lib/api_keys/models/api_key.rb', line 358

def permissions
  scopes
end

#pretty_print(printer) ⇒ Object



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

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

#public_key_type?Boolean

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

Returns:

  • (Boolean)


302
303
304
305
306
307
# File 'lib/api_keys/models/api_key.rb', line 302

def public_key_type?
  return false if key_type.blank?
  config = key_type_config
  return false if config.nil?
  config[:public] == true
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.



234
235
236
237
# File 'lib/api_keys/models/api_key.rb', line 234

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

#restricted?Boolean

Returns true when this key carries any request restriction.

Returns:

  • (Boolean)

    true when this key carries any request restriction.



107
108
109
# File 'lib/api_keys/models/api_key.rb', line 107

def restricted?
  restrictions.restricted?
end

#restrictionsApiKeys::Restrictions

Request Restrictions ==

Where this key may be used from. Reads always answer with a value object, so key.restrictions.origins works even on a key that has none.



60
61
62
63
64
# File 'lib/api_keys/models/api_key.rb', line 60

def restrictions
  return ApiKeys::Restrictions.none unless self.class.restrictions_column?

  ApiKeys::Restrictions.wrap(self[:restrictions])
end

#restrictions=(value) ⇒ Object

Accepts a Restrictions instance, a hash of lists, or nil. Hashes are normalized into the storage shape; anything else is stored untouched so the validation, rather than a silent coercion, is what reports it.



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/api_keys/models/api_key.rb', line 69

def restrictions=(value)
  ensure_restrictions_column!

  normalized = if value.nil?
                 {}
               elsif value.is_a?(Hash)
                 wrapped = ApiKeys::Restrictions.wrap(value)
                 wrapped.malformed? ? value : wrapped.to_h
               elsif value.is_a?(ApiKeys::Restrictions)
                 value.malformed? ? { "__malformed__" => true } : value.to_h
               else
                 value
               end
  super(normalized)
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)


275
276
277
278
# File 'lib/api_keys/models/api_key.rb', line 275

def revocable?
  return true if key_type.blank?
  self.class.revocable_for(key_type_config)
end

#revoke!Object

Instance Methods ==



215
216
217
218
# File 'lib/api_keys/models/api_key.rb', line 215

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

#revoked?Boolean

Returns:

  • (Boolean)


220
221
222
# File 'lib/api_keys/models/api_key.rb', line 220

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.



43
44
45
46
47
48
49
50
51
52
53
# File 'lib/api_keys/models/api_key.rb', line 43

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.



256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
# File 'lib/api_keys/models/api_key.rb', line 256

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 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



312
313
314
315
# File 'lib/api_keys/models/api_key.rb', line 312

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