api_keys 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +69 -0
- data/README.md +402 -13
- data/SECURITY.md +7 -6
- data/app/controllers/api_keys/keys_controller.rb +73 -46
- data/app/views/api_keys/keys/_form.html.erb +56 -10
- data/app/views/api_keys/keys/_key_row.html.erb +4 -2
- data/app/views/api_keys/keys/_refusal_notice.html.erb +24 -0
- data/app/views/api_keys/keys/_show_token.html.erb +4 -7
- data/app/views/api_keys/keys/edit.html.erb +2 -0
- data/app/views/layouts/api_keys/application.html.erb +6 -0
- data/docs/reviews/2026-10-18-0-6-0/01-astra-review-of-0-6-0.md +36 -0
- data/docs/reviews/2026-10-18-0-6-0/02-astra-re-review-of-0-6-0.md +34 -0
- data/docs/reviews/2026-10-18-0-6-0/03-sol-re-check-of-0-6-0.md +41 -0
- data/lib/api_keys/authentication.rb +174 -7
- data/lib/api_keys/configuration.rb +82 -0
- data/lib/api_keys/device_flow.rb +146 -0
- data/lib/api_keys/engine.rb +9 -0
- data/lib/api_keys/errors.rb +11 -0
- data/lib/api_keys/form_builder_extensions.rb +3 -1
- data/lib/api_keys/helpers/token_session.rb +183 -23
- data/lib/api_keys/helpers/view_helpers.rb +34 -0
- data/lib/api_keys/idempotency.rb +155 -0
- data/lib/api_keys/jobs/update_stats_job.rb +45 -1
- data/lib/api_keys/models/api_key.rb +261 -3
- data/lib/api_keys/models/concerns/has_api_keys.rb +9 -1
- data/lib/api_keys/models/device_authorization.rb +333 -0
- data/lib/api_keys/models/idempotency_key.rb +184 -0
- data/lib/api_keys/version.rb +1 -1
- data/lib/api_keys.rb +13 -0
- data/lib/generators/api_keys/add_created_by_generator.rb +50 -0
- data/lib/generators/api_keys/add_device_authorizations_generator.rb +51 -0
- data/lib/generators/api_keys/add_idempotency_keys_generator.rb +52 -0
- data/lib/generators/api_keys/add_key_activity_generator.rb +59 -0
- data/lib/generators/api_keys/templates/add_created_by_to_api_keys.rb.erb +33 -0
- data/lib/generators/api_keys/templates/add_key_activity_to_api_keys.rb.erb +37 -0
- data/lib/generators/api_keys/templates/create_api_keys_device_authorizations.rb.erb +70 -0
- data/lib/generators/api_keys/templates/create_api_keys_idempotency_keys.rb.erb +52 -0
- data/lib/generators/api_keys/templates/create_api_keys_table.rb.erb +16 -0
- data/lib/generators/api_keys/templates/initializer.rb +35 -0
- metadata +17 -1
|
@@ -10,15 +10,39 @@ module ApiKeys
|
|
|
10
10
|
# The core ActiveRecord model representing an API key.
|
|
11
11
|
class ApiKey < ActiveRecord::Base
|
|
12
12
|
MAX_SCOPES = 100
|
|
13
|
+
# A key name: letters, numbers, underscores, hyphens and spaces.
|
|
14
|
+
MAX_NAME_LENGTH = 60
|
|
15
|
+
NAME_FORMAT = /\A[a-zA-Z0-9_ -]+\z/
|
|
13
16
|
MAX_SCOPE_BYTESIZE = 128
|
|
14
17
|
MAX_METADATA_BYTESIZE = 16_384
|
|
15
18
|
MAX_RESTRICTION_ENTRIES = 100
|
|
16
19
|
MAX_RESTRICTION_ENTRY_BYTESIZE = 255
|
|
17
20
|
RESTRICTIONS_COLUMN = "restrictions"
|
|
21
|
+
# Refused-but-identified traffic (0.6.0). Kept apart from last_used_at and
|
|
22
|
+
# requests_count, which only ever describe successful authentications.
|
|
23
|
+
REFUSAL_STATS_COLUMNS = %w[last_refused_at refusals_count last_refusal_code].freeze
|
|
24
|
+
# Durable one-time token handoff (0.6.0): stamped by the first retrieval
|
|
25
|
+
# of the key's token through ApiKeys::TokenSession, and never cleared.
|
|
26
|
+
TOKEN_SHOWN_AT_COLUMN = "token_shown_at"
|
|
27
|
+
# Every refusal that happens after the presented token was verified, so the
|
|
28
|
+
# key is known: lifecycle, type/environment configuration, isolation,
|
|
29
|
+
# request restrictions, and scopes. Lookup failures (missing or invalid
|
|
30
|
+
# token, insecure transport) name no key and are never stamped on one.
|
|
31
|
+
REFUSAL_CODES = %w[
|
|
32
|
+
revoked_key expired_key unknown_key_type unknown_environment
|
|
33
|
+
environment_misconfigured environment_mismatch
|
|
34
|
+
origin_not_allowed ip_not_allowed restriction_misconfigured missing_scope
|
|
35
|
+
untrusted_key
|
|
36
|
+
].freeze
|
|
37
|
+
# The person who created the key (0.7.0). Optional columns.
|
|
38
|
+
CREATED_BY_COLUMNS = %w[created_by_type created_by_id].freeze
|
|
18
39
|
# Deliberately excludes `restrictions`: owners must be able to tighten a
|
|
19
|
-
# request policy even when the key itself is non-revocable.
|
|
40
|
+
# request policy even when the key itself is non-revocable. Includes the
|
|
41
|
+
# person who created the key: `config.trusted_key_check` may trust a key
|
|
42
|
+
# because of that person, so no update can name another one.
|
|
20
43
|
IMMUTABLE_IDENTITY_ATTRIBUTES = %w[
|
|
21
44
|
token_digest digest_algorithm prefix last4 owner_type owner_id key_type environment
|
|
45
|
+
created_by_type created_by_id
|
|
22
46
|
].freeze
|
|
23
47
|
|
|
24
48
|
self.table_name = "api_keys"
|
|
@@ -28,10 +52,24 @@ module ApiKeys
|
|
|
28
52
|
|
|
29
53
|
# == Associations ==
|
|
30
54
|
belongs_to :owner, polymorphic: true, optional: true
|
|
55
|
+
# The person behind the key, when the host names one at creation:
|
|
56
|
+
# `create_api_key!(created_by: current_user)`. Needs the created_by
|
|
57
|
+
# columns (`rails generate api_keys:add_created_by`).
|
|
58
|
+
belongs_to :created_by, polymorphic: true, optional: true
|
|
31
59
|
|
|
32
60
|
# == Attributes & Serialization ==
|
|
33
61
|
# Expose the plaintext token only immediately after creation
|
|
34
62
|
attr_reader :token
|
|
63
|
+
|
|
64
|
+
# Written only by the atomic claim in `claim_token_handoff!`. Ordinary
|
|
65
|
+
# saves can never set or clear it, so no update path (a form, mass
|
|
66
|
+
# assignment, a stray `update`) can reopen a handoff that was claimed.
|
|
67
|
+
attr_readonly :token_shown_at
|
|
68
|
+
# The person behind the key is fixed at creation. `config.trusted_key_check`
|
|
69
|
+
# may trust a key because of that person, so no save (with or without
|
|
70
|
+
# validations) and no update_column can name another one. A backfill of
|
|
71
|
+
# keys made before the columns existed writes them with SQL (update_all).
|
|
72
|
+
attr_readonly :created_by_type, :created_by_id
|
|
35
73
|
attr_accessor :expires_at_preset
|
|
36
74
|
|
|
37
75
|
# JSON attributes (:scopes, :metadata) are defined in the engine initializer
|
|
@@ -118,9 +156,9 @@ module ApiKeys
|
|
|
118
156
|
# validates :scopes, presence: true # Default handled by attribute def
|
|
119
157
|
# validates :metadata, presence: true # Default handled by attribute def
|
|
120
158
|
validates :name,
|
|
121
|
-
length: { maximum:
|
|
159
|
+
length: { maximum: MAX_NAME_LENGTH },
|
|
122
160
|
# Allow letters, numbers, underscores, hyphens, and spaces. No leading/trailing spaces.
|
|
123
|
-
format: { with:
|
|
161
|
+
format: { with: NAME_FORMAT, message: "can only contain letters, numbers, underscores, hyphens, and spaces (no leading / trailing spaces)" },
|
|
124
162
|
allow_blank: true # Apply length and format only if name is present
|
|
125
163
|
|
|
126
164
|
validates :name, presence: true, if: :name_required? # Only require presence conditionally
|
|
@@ -356,11 +394,23 @@ module ApiKeys
|
|
|
356
394
|
# - Key types mode: blank scopes means "no permissions". When you've configured
|
|
357
395
|
# key types with permission ceilings, an empty scope list should deny access,
|
|
358
396
|
# not silently bypass the entire permission system.
|
|
397
|
+
#
|
|
398
|
+
# Key types configured with `scopes: :ceiling` skip the stored list: the
|
|
399
|
+
# type's live permission ceiling is the contract, so a widened ceiling
|
|
400
|
+
# reaches existing keys and a narrowed one takes effect on the next request.
|
|
359
401
|
def allows_scope?(required_scope)
|
|
360
402
|
return false unless respond_to?(:scopes)
|
|
361
403
|
return false unless required_scope.present?
|
|
362
404
|
return false unless scopes.is_a?(Array)
|
|
363
405
|
|
|
406
|
+
if scopes_follow_ceiling?
|
|
407
|
+
# Stored scopes are metadata here, but a damaged list still means
|
|
408
|
+
# something wrote around validation: refuse rather than guess.
|
|
409
|
+
return false unless scopes.all? { |scope| valid_scope_value?(scope) }
|
|
410
|
+
|
|
411
|
+
return followed_ceiling.include?(required_scope.to_s)
|
|
412
|
+
end
|
|
413
|
+
|
|
364
414
|
if scopes.blank?
|
|
365
415
|
return !scope_policy_enabled?
|
|
366
416
|
end
|
|
@@ -401,6 +451,50 @@ module ApiKeys
|
|
|
401
451
|
ceiling == :all || ceiling.include?(scope.to_s)
|
|
402
452
|
end
|
|
403
453
|
|
|
454
|
+
# The scopes to save after a form that offered only some scopes as
|
|
455
|
+
# checkboxes was submitted: the rule the dashboard's edit form uses, for
|
|
456
|
+
# host forms built with `api_key_scopes_checkboxes`.
|
|
457
|
+
#
|
|
458
|
+
# - Offered scopes are set from the boxes: checked ones are granted,
|
|
459
|
+
# unchecked ones removed.
|
|
460
|
+
# - Every other scope the key holds is kept, in its current order.
|
|
461
|
+
# - A checked value that was never offered is ignored, so a crafted
|
|
462
|
+
# request cannot grant it.
|
|
463
|
+
# - A held scope outside the key's current ceiling is dropped, offered or
|
|
464
|
+
# not. Authentication already denies it, and keeping it would make every
|
|
465
|
+
# save of the key fail the ceiling validation. A newly checked scope
|
|
466
|
+
# outside the ceiling is kept, so the validation reports it.
|
|
467
|
+
# - `checked: nil` means the form submitted no scopes at all (no boxes and
|
|
468
|
+
# no sentinel): nothing is granted or removed except held scopes outside
|
|
469
|
+
# the ceiling.
|
|
470
|
+
# - A `scopes: :ceiling` key returns its stored scopes unchanged: they are
|
|
471
|
+
# metadata there, and the type's live ceiling authorizes the key.
|
|
472
|
+
#
|
|
473
|
+
# It only computes the list; assign it (`key.update(scopes: ...)`) to save.
|
|
474
|
+
#
|
|
475
|
+
# @param offered [Array<String, Symbol>] The scopes the form rendered as boxes.
|
|
476
|
+
# @param checked [Array<String, Symbol>, nil] The submitted values; blank
|
|
477
|
+
# entries (the sentinel) are ignored.
|
|
478
|
+
# @return [Array<String>]
|
|
479
|
+
# @raise [ArgumentError] when +offered+ or +checked+ is not an Array (or nil for +checked+).
|
|
480
|
+
def scopes_after_form_edit(offered:, checked:)
|
|
481
|
+
raise ArgumentError, "offered must be an Array of scope names" unless offered.is_a?(Array)
|
|
482
|
+
unless checked.nil? || checked.is_a?(Array)
|
|
483
|
+
raise ArgumentError, "checked must be an Array of scope names, or nil when no scopes were submitted"
|
|
484
|
+
end
|
|
485
|
+
return scopes.deep_dup if scopes_follow_ceiling?
|
|
486
|
+
|
|
487
|
+
offered = checked.nil? ? [] : offered.map(&:to_s)
|
|
488
|
+
checked = Array(checked).reject { |scope| scope.respond_to?(:blank?) && scope.blank? }.map(&:to_s)
|
|
489
|
+
held = Array(scopes)
|
|
490
|
+
|
|
491
|
+
kept = held.select do |scope|
|
|
492
|
+
scope_within_ceiling?(scope) && (!offered.include?(scope) || checked.include?(scope))
|
|
493
|
+
end
|
|
494
|
+
added = offered.select { |scope| checked.include?(scope) && !held.include?(scope) }
|
|
495
|
+
kept + added
|
|
496
|
+
end
|
|
497
|
+
|
|
404
498
|
# Alias for scopes - provides a more user-friendly API that matches
|
|
405
499
|
# the configuration DSL where key types use `permissions` for scope ceiling.
|
|
406
500
|
# Note: We use a method instead of alias_method because `scopes` is defined
|
|
@@ -417,6 +511,52 @@ module ApiKeys
|
|
|
417
511
|
allows_scope?(required_permission)
|
|
418
512
|
end
|
|
419
513
|
|
|
514
|
+
# Whether scope checks for this key read its type's live permission ceiling
|
|
515
|
+
# instead of the scopes stored at creation (`scopes: :ceiling` key types).
|
|
516
|
+
# Untyped keys, keys whose type is no longer configured, and every other
|
|
517
|
+
# type answer false and keep the stored-scope behavior.
|
|
518
|
+
# @return [Boolean]
|
|
519
|
+
def scopes_follow_ceiling?
|
|
520
|
+
return false if key_type.blank?
|
|
521
|
+
|
|
522
|
+
self.class.scopes_follow_ceiling_for?(key_type_config)
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
# == Refused-but-identified traffic ==
|
|
526
|
+
# Stamped asynchronously when a request presents this key's real token and a
|
|
527
|
+
# policy refuses it. Installations that have not run
|
|
528
|
+
# `rails generate api_keys:add_key_activity` read nil / 0 instead of raising.
|
|
529
|
+
|
|
530
|
+
# @return [Time, nil] When the most recently recorded refusal happened.
|
|
531
|
+
def last_refused_at
|
|
532
|
+
self.class.refusal_stats_columns? ? self[:last_refused_at] : nil
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
# @return [Integer] Refusals counted while `track_requests_count` is on.
|
|
536
|
+
def refusals_count
|
|
537
|
+
self.class.refusal_stats_columns? ? self[:refusals_count].to_i : 0
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
# @return [String, nil] The error code of the most recently recorded refusal.
|
|
541
|
+
def last_refusal_code
|
|
542
|
+
self.class.refusal_stats_columns? ? self[:last_refusal_code] : nil
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
# == One-time token handoff ==
|
|
546
|
+
|
|
547
|
+
# When this key's token was released through the one-time handoff
|
|
548
|
+
# (ApiKeys::TokenSession). Nil when it never was, and on installations that
|
|
549
|
+
# have not run `rails generate api_keys:add_key_activity` yet.
|
|
550
|
+
# @return [Time, nil]
|
|
551
|
+
def token_shown_at
|
|
552
|
+
self.class.token_shown_at_column? ? self[:token_shown_at] : nil
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
# @return [Boolean] Whether the one-time handoff already released this key's token.
|
|
556
|
+
def token_shown?
|
|
557
|
+
!token_shown_at.nil?
|
|
558
|
+
end
|
|
559
|
+
|
|
420
560
|
# Provides a masked version of the token for display (e.g., ak_live_••••rj4p)
|
|
421
561
|
# Requires the plaintext token to be available (only right after creation).
|
|
422
562
|
def masked_token
|
|
@@ -442,6 +582,114 @@ module ApiKeys
|
|
|
442
582
|
false
|
|
443
583
|
end
|
|
444
584
|
|
|
585
|
+
# Whether the `created_by` columns exist. Installations that predate
|
|
586
|
+
# v0.7.0 keep working untouched until they run the generator.
|
|
587
|
+
# @return [Boolean]
|
|
588
|
+
def self.created_by_columns?
|
|
589
|
+
(CREATED_BY_COLUMNS - column_names).empty?
|
|
590
|
+
rescue StandardError
|
|
591
|
+
false
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
# Whether the refused-traffic columns exist. Installations that predate
|
|
595
|
+
# v0.6.0 keep authenticating untouched until they run the generator.
|
|
596
|
+
# @return [Boolean]
|
|
597
|
+
def self.refusal_stats_columns?
|
|
598
|
+
(REFUSAL_STATS_COLUMNS - column_names).empty?
|
|
599
|
+
rescue StandardError
|
|
600
|
+
false
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
# Whether the `token_shown_at` column exists. Without it the one-time
|
|
604
|
+
# handoff falls back to the cache marker alone, as in 0.5.1.
|
|
605
|
+
# @return [Boolean]
|
|
606
|
+
def self.token_shown_at_column?
|
|
607
|
+
column_names.include?(TOKEN_SHOWN_AT_COLUMN)
|
|
608
|
+
rescue StandardError
|
|
609
|
+
false
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
# Durably claims the one-time token handoff of the key with +id+ in one
|
|
613
|
+
# conditional statement:
|
|
614
|
+
#
|
|
615
|
+
# UPDATE api_keys SET token_shown_at = now WHERE id = ? AND token_shown_at IS NULL
|
|
616
|
+
#
|
|
617
|
+
# The database applies it atomically, so exactly one caller ever gets true
|
|
618
|
+
# for a key: across processes, across copies of a session, and after the
|
|
619
|
+
# cache is flushed. Zero rows changed means the token was already shown or
|
|
620
|
+
# the key no longer exists, and answers false (fail closed).
|
|
621
|
+
#
|
|
622
|
+
# Answers nil when the column does not exist yet (nothing can be recorded
|
|
623
|
+
# durably). Database errors are raised, not rescued: the caller withholds
|
|
624
|
+
# the token.
|
|
625
|
+
#
|
|
626
|
+
# @param id [Integer, String] The key's primary key.
|
|
627
|
+
# @return [Boolean, nil]
|
|
628
|
+
def self.claim_token_handoff!(id)
|
|
629
|
+
return nil unless column_names.include?(TOKEN_SHOWN_AT_COLUMN)
|
|
630
|
+
return false if id.nil?
|
|
631
|
+
|
|
632
|
+
where(primary_key => id, TOKEN_SHOWN_AT_COLUMN => nil)
|
|
633
|
+
.update_all(TOKEN_SHOWN_AT_COLUMN => Time.current) == 1
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
# Read-only companion of `claim_token_handoff!`: whether a claim for +id+
|
|
637
|
+
# could still succeed. Nil without the column. Raises on database errors.
|
|
638
|
+
#
|
|
639
|
+
# @param id [Integer, String] The key's primary key.
|
|
640
|
+
# @return [Boolean, nil]
|
|
641
|
+
def self.token_handoff_claimable?(id)
|
|
642
|
+
return nil unless column_names.include?(TOKEN_SHOWN_AT_COLUMN)
|
|
643
|
+
return false if id.nil?
|
|
644
|
+
|
|
645
|
+
where(primary_key => id, TOKEN_SHOWN_AT_COLUMN => nil).exists?
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
# Single source of truth for `scopes: :ceiling`. The finite-ceiling check
|
|
649
|
+
# repeats the configuration validation on purpose: a ceiling of `:all` must
|
|
650
|
+
# never become the contract, however the configuration was produced.
|
|
651
|
+
def self.scopes_follow_ceiling_for?(type_config)
|
|
652
|
+
return false unless type_config.is_a?(Hash) && type_config[:scopes].to_s == "ceiling"
|
|
653
|
+
|
|
654
|
+
permissions = type_config[:permissions]
|
|
655
|
+
permissions.is_a?(Array) && permissions.any?
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
# Add-only merge of a key type's current permission ceiling into the stored
|
|
659
|
+
# scopes of every non-revoked key of that type: the backfill for types that
|
|
660
|
+
# keep stored scopes after their ceiling was widened. It only ever adds
|
|
661
|
+
# permissions the type already allows, and it re-adds any a user removed
|
|
662
|
+
# by hand, so run it only for types whose users do not curate scopes.
|
|
663
|
+
#
|
|
664
|
+
# @param key_type [Symbol, String] A configured type with a finite ceiling.
|
|
665
|
+
# @return [Integer] How many keys gained scopes.
|
|
666
|
+
def self.reconcile_scopes!(key_type:)
|
|
667
|
+
type_name, type_config = ApiKeys.configuration.key_types.find { |name, _settings| name.to_s == key_type.to_s }
|
|
668
|
+
raise ArgumentError, "Unknown key type '#{key_type}'" unless type_config
|
|
669
|
+
|
|
670
|
+
ceiling = type_config[:permissions]
|
|
671
|
+
unless ceiling.is_a?(Array) && ceiling.any?
|
|
672
|
+
raise ArgumentError, "Key type '#{key_type}' has no finite permission ceiling to reconcile"
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
ceiling = ceiling.map(&:to_s)
|
|
676
|
+
where(key_type: type_name.to_s, revoked_at: nil).find_each.count do |api_key|
|
|
677
|
+
api_key.with_lock do
|
|
678
|
+
# with_lock reloads the row: a key revoked (or retyped) after it was
|
|
679
|
+
# selected is skipped, so a revoked key never gains scopes.
|
|
680
|
+
next false if api_key.revoked_at.present? || api_key.key_type.to_s != type_name.to_s
|
|
681
|
+
|
|
682
|
+
stored = api_key.scopes
|
|
683
|
+
next false unless stored.is_a?(Array) && stored.all? { |scope| api_key.send(:valid_scope_value?, scope) }
|
|
684
|
+
|
|
685
|
+
merged = stored | ceiling
|
|
686
|
+
next false if merged == stored || merged.length > MAX_SCOPES
|
|
687
|
+
|
|
688
|
+
api_key.update_columns(scopes: merged, updated_at: Time.current)
|
|
689
|
+
end
|
|
690
|
+
end
|
|
691
|
+
end
|
|
692
|
+
|
|
445
693
|
# Single source of truth for a key type's request-restriction ceiling.
|
|
446
694
|
# Omitting the setting allows every supported restriction kind.
|
|
447
695
|
def self.restriction_kinds_for(type_config)
|
|
@@ -706,6 +954,11 @@ module ApiKeys
|
|
|
706
954
|
|
|
707
955
|
def scopes_respect_permission_ceiling
|
|
708
956
|
return unless scopes.is_a?(Array)
|
|
957
|
+
# On a `scopes: :ceiling` key the stored list authorizes nothing. Narrowing
|
|
958
|
+
# the ceiling must not leave every existing key of the type unsaveable
|
|
959
|
+
# (which would also block tightening its request restrictions), so only
|
|
960
|
+
# writes to the scopes themselves are held to the current ceiling.
|
|
961
|
+
return if persisted? && scopes_follow_ceiling? && !will_save_change_to_attribute?(:scopes)
|
|
709
962
|
|
|
710
963
|
ceiling = permission_ceiling
|
|
711
964
|
return if ceiling == :all
|
|
@@ -754,6 +1007,11 @@ module ApiKeys
|
|
|
754
1007
|
false
|
|
755
1008
|
end
|
|
756
1009
|
|
|
1010
|
+
# The live ceiling a `scopes: :ceiling` key is authorized for.
|
|
1011
|
+
def followed_ceiling
|
|
1012
|
+
Array(key_type_config&.[](:permissions)).map(&:to_s)
|
|
1013
|
+
end
|
|
1014
|
+
|
|
757
1015
|
def permission_ceiling
|
|
758
1016
|
if key_type.present?
|
|
759
1017
|
config = key_type_config
|
|
@@ -225,13 +225,20 @@ module ApiKeys
|
|
|
225
225
|
# Accepts the raw string a form field submits ("example.com, *.example.com").
|
|
226
226
|
# @param allowed_ips [String, Array, nil] Convenience form of the IP list
|
|
227
227
|
# ("203.0.113.7, 10.0.0.0/8").
|
|
228
|
+
# @param created_by [ActiveRecord::Base, nil] The person behind the key, for
|
|
229
|
+
# `config.trusted_key_check`. Needs the created_by columns.
|
|
228
230
|
# @return [ApiKeys::ApiKey] The newly created ApiKey instance. The plaintext token
|
|
229
231
|
# is available via the `#token` attribute on this instance
|
|
230
232
|
# *only until it's reloaded*.
|
|
231
233
|
def create_api_key!(name: nil, scopes: nil, expires_at: nil, expires_at_preset: nil, metadata: nil,
|
|
232
|
-
key_type: nil, environment: nil, restrictions: nil, allowed_origins: nil, allowed_ips: nil
|
|
234
|
+
key_type: nil, environment: nil, restrictions: nil, allowed_origins: nil, allowed_ips: nil,
|
|
235
|
+
created_by: nil)
|
|
233
236
|
config = ApiKeys.configuration
|
|
234
237
|
|
|
238
|
+
if created_by && !ApiKeys::ApiKey.created_by_columns?
|
|
239
|
+
raise ApiKeys::Errors::CreatedByMigrationRequiredError
|
|
240
|
+
end
|
|
241
|
+
|
|
235
242
|
# Parse expires_at_preset if provided (takes precedence over expires_at)
|
|
236
243
|
if expires_at_preset.present?
|
|
237
244
|
expires_at = ApiKeys::Helpers::ExpirationOptions.parse(expires_at_preset)
|
|
@@ -302,6 +309,7 @@ module ApiKeys
|
|
|
302
309
|
# prefix, token_digest, digest_algorithm are set by ApiKey callbacks
|
|
303
310
|
}
|
|
304
311
|
attributes[:restrictions] = requested_restrictions if requested_restrictions
|
|
312
|
+
attributes[:created_by] = created_by if created_by
|
|
305
313
|
|
|
306
314
|
api_key = self.class.transaction do
|
|
307
315
|
self.api_keys.create!(**attributes)
|