Class: ApiKeys::ApiKey
- Inherits:
-
ActiveRecord::Base
- Object
- ActiveRecord::Base
- ApiKeys::ApiKey
- 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
-
#expires_at_preset ⇒ Object
Returns the value of attribute expires_at_preset.
-
#token ⇒ Object
readonly
Attributes & Serialization == Expose the plaintext token only immediately after creation.
Class Method Summary collapse
-
.restriction_kinds_for(type_config) ⇒ Object
Single source of truth for a key type's request-restriction ceiling.
-
.restrictions_column? ⇒ Boolean
Whether the
restrictionscolumn exists. -
.revocable_for(type_config) ⇒ Object
Single source of truth for lifecycle policy in model and dashboard code.
Instance Method Summary collapse
- #active? ⇒ Boolean
-
#allowed_ips ⇒ Array<String>
Allowed IP addresses and CIDR ranges.
-
#allowed_ips=(value) ⇒ Object
Accepts an array or a raw string ("203.0.113.7, 10.0.0.0/8").
-
#allowed_origins ⇒ Array<String>
Allowed web origins (hosts and
*.hostwildcards). -
#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.
-
#allowed_restriction_kinds ⇒ Array<Symbol>
Restriction kinds this key's type permits.
-
#allows_permission?(required_permission) ⇒ Boolean
Check if this key has a specific permission (alias for allows_scope?).
-
#allows_scope?(required_scope) ⇒ Boolean
Basic scope check.
-
#destroy ⇒ Object
Override destroy to prevent destroying non-revocable keys.
- #destroy! ⇒ Object
-
#environment_config ⇒ Object
Returns the configuration hash for this key's environment.
-
#expirable? ⇒ Boolean
Non-revocable keys are permanent by design; other key types may expire.
- #expired? ⇒ Boolean
-
#inspect ⇒ Object
Keep credentials and credential-derived values out of logs and consoles.
-
#key_type_config ⇒ Object
Returns the configuration hash for this key's type.
-
#masked_token ⇒ Object
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).
-
#permissions ⇒ Array<String>
Alias for scopes - provides a more user-friendly API that matches the configuration DSL where key types use
permissionsfor scope ceiling. - #pretty_print(printer) ⇒ Object
-
#public_key_type? ⇒ Boolean
Returns true if this key type is explicitly configured as public.
-
#reload ⇒ Object
The plaintext token is an ephemeral creation-time value.
-
#restricted? ⇒ Boolean
True when this key carries any request restriction.
-
#restrictions ⇒ ApiKeys::Restrictions
Request Restrictions == Where this key may be used from.
-
#restrictions=(value) ⇒ Object
Accepts a Restrictions instance, a hash of lists, or nil.
-
#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.
-
#revoke! ⇒ Object
Instance Methods ==.
- #revoked? ⇒ Boolean
-
#scopes=(value) ⇒ Object
Override scopes setter to auto-clean blank values.
-
#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.
-
#viewable_token ⇒ String?
Returns the stored plaintext token for public keys.
Instance Attribute Details
#expires_at_preset ⇒ Object
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 |
#token ⇒ Object (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.
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
228 229 230 |
# File 'lib/api_keys/models/api_key.rb', line 228 def active? !revoked? && !expired? end |
#allowed_ips ⇒ Array<String>
Returns 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_origins ⇒ Array<String>
Returns 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_kinds ⇒ Array<Symbol>
Returns 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?)
365 366 367 |
# File 'lib/api_keys/models/api_key.rb', line 365 def () allows_scope?() 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.
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 = ceiling == :all || ceiling.include?(required) end |
#destroy ⇒ Object
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_config ⇒ Object
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.
281 282 283 |
# File 'lib/api_keys/models/api_key.rb', line 281 def expirable? revocable? end |
#expired? ⇒ Boolean
224 225 226 |
# File 'lib/api_keys/models/api_key.rb', line 224 def expired? expires_at? && expires_at <= Time.current end |
#inspect ⇒ Object
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_config ⇒ Object
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_token ⇒ Object
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 |
#permissions ⇒ Array<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.
358 359 360 |
# File 'lib/api_keys/models/api_key.rb', line 358 def 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.
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 |
#reload ⇒ Object
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.
107 108 109 |
# File 'lib/api_keys/models/api_key.rb', line 107 def restricted? restrictions.restricted? end |
#restrictions ⇒ ApiKeys::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
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
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( = nil) serialized = super() 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_token ⇒ String?
Returns the stored plaintext token for public keys. Returns nil for all other key types (the token is only available at creation time).
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 |