Class: Secret
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- Secret
- Defined in:
- app/models/secret.rb
Overview
Encrypted key-value storage for runtime secrets (API tokens, credentials). Replaces Rails encrypted credentials for secrets that must be readable across forked Solid Queue workers without cache-busting hacks.
Secrets are organized by namespace (e.g. “anthropic”, “mcp”) and key (e.g. “subscription_token”). Values are encrypted at rest using Active Record Encryption — only the value column is encrypted; namespace and key are plain text for queryability.
Instance Attribute Summary collapse
-
#key ⇒ String
Credential identifier within the namespace.
-
#namespace ⇒ String
Grouping key (e.g. “anthropic”, “mcp”).
-
#value ⇒ String
The secret value (encrypted at rest).
Class Method Summary collapse
-
.for_namespace(ns) ⇒ ActiveRecord::Relation
Secrets in the given namespace.
-
.list(namespace) ⇒ Array<String>
Lists all keys under a namespace (not values).
-
.read(namespace, key) ⇒ String?
Reads a single secret value.
-
.remove(namespace, key) ⇒ void
Removes a single key from a namespace.
-
.write(namespace, pairs) ⇒ void
Writes one or more key-value pairs under a namespace.
Instance Attribute Details
#key ⇒ String
Returns credential identifier within the namespace.
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
# File 'app/models/secret.rb', line 18 class Secret < ApplicationRecord encrypts :value validates :namespace, presence: true validates :key, presence: true validates :value, presence: true validates :key, uniqueness: {scope: :namespace} # @!method self.for_namespace(ns) # @param ns [String] namespace to filter by # @return [ActiveRecord::Relation] secrets in the given namespace scope :for_namespace, ->(ns) { where(namespace: ns) } # Reads a single secret value. # # @param namespace [String] top-level grouping key # @param key [String] credential key within the namespace # @return [String, nil] decrypted value or nil if not found def self.read(namespace, key) find_by(namespace: namespace, key: key)&.value end # Writes one or more key-value pairs under a namespace. # Each pair is upserted (insert or update). The entire batch is wrapped # in a transaction so partial writes cannot occur. # # @param namespace [String] top-level grouping key # @param pairs [Hash<String, String>] key-value pairs to store # @return [void] def self.write(namespace, pairs) transaction do pairs.each do |secret_key, secret_value| record = find_or_initialize_by(namespace: namespace, key: secret_key) record.update!(value: secret_value) end end end # Lists all keys under a namespace (not values). # # @param namespace [String] top-level grouping key # @return [Array<String>] credential keys def self.list(namespace) for_namespace(namespace).pluck(:key) end # Removes a single key from a namespace. # # @param namespace [String] top-level grouping key # @param key [String] credential key to remove # @return [void] def self.remove(namespace, key) find_by(namespace: namespace, key: key)&.destroy! end end |
#namespace ⇒ String
Returns grouping key (e.g. “anthropic”, “mcp”).
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
# File 'app/models/secret.rb', line 18 class Secret < ApplicationRecord encrypts :value validates :namespace, presence: true validates :key, presence: true validates :value, presence: true validates :key, uniqueness: {scope: :namespace} # @!method self.for_namespace(ns) # @param ns [String] namespace to filter by # @return [ActiveRecord::Relation] secrets in the given namespace scope :for_namespace, ->(ns) { where(namespace: ns) } # Reads a single secret value. # # @param namespace [String] top-level grouping key # @param key [String] credential key within the namespace # @return [String, nil] decrypted value or nil if not found def self.read(namespace, key) find_by(namespace: namespace, key: key)&.value end # Writes one or more key-value pairs under a namespace. # Each pair is upserted (insert or update). The entire batch is wrapped # in a transaction so partial writes cannot occur. # # @param namespace [String] top-level grouping key # @param pairs [Hash<String, String>] key-value pairs to store # @return [void] def self.write(namespace, pairs) transaction do pairs.each do |secret_key, secret_value| record = find_or_initialize_by(namespace: namespace, key: secret_key) record.update!(value: secret_value) end end end # Lists all keys under a namespace (not values). # # @param namespace [String] top-level grouping key # @return [Array<String>] credential keys def self.list(namespace) for_namespace(namespace).pluck(:key) end # Removes a single key from a namespace. # # @param namespace [String] top-level grouping key # @param key [String] credential key to remove # @return [void] def self.remove(namespace, key) find_by(namespace: namespace, key: key)&.destroy! end end |
#value ⇒ String
Returns the secret value (encrypted at rest).
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
# File 'app/models/secret.rb', line 18 class Secret < ApplicationRecord encrypts :value validates :namespace, presence: true validates :key, presence: true validates :value, presence: true validates :key, uniqueness: {scope: :namespace} # @!method self.for_namespace(ns) # @param ns [String] namespace to filter by # @return [ActiveRecord::Relation] secrets in the given namespace scope :for_namespace, ->(ns) { where(namespace: ns) } # Reads a single secret value. # # @param namespace [String] top-level grouping key # @param key [String] credential key within the namespace # @return [String, nil] decrypted value or nil if not found def self.read(namespace, key) find_by(namespace: namespace, key: key)&.value end # Writes one or more key-value pairs under a namespace. # Each pair is upserted (insert or update). The entire batch is wrapped # in a transaction so partial writes cannot occur. # # @param namespace [String] top-level grouping key # @param pairs [Hash<String, String>] key-value pairs to store # @return [void] def self.write(namespace, pairs) transaction do pairs.each do |secret_key, secret_value| record = find_or_initialize_by(namespace: namespace, key: secret_key) record.update!(value: secret_value) end end end # Lists all keys under a namespace (not values). # # @param namespace [String] top-level grouping key # @return [Array<String>] credential keys def self.list(namespace) for_namespace(namespace).pluck(:key) end # Removes a single key from a namespace. # # @param namespace [String] top-level grouping key # @param key [String] credential key to remove # @return [void] def self.remove(namespace, key) find_by(namespace: namespace, key: key)&.destroy! end end |
Class Method Details
.for_namespace(ns) ⇒ ActiveRecord::Relation
Returns secrets in the given namespace.
29 |
# File 'app/models/secret.rb', line 29 scope :for_namespace, ->(ns) { where(namespace: ns) } |
.list(namespace) ⇒ Array<String>
Lists all keys under a namespace (not values).
60 61 62 |
# File 'app/models/secret.rb', line 60 def self.list(namespace) for_namespace(namespace).pluck(:key) end |
.read(namespace, key) ⇒ String?
Reads a single secret value.
36 37 38 |
# File 'app/models/secret.rb', line 36 def self.read(namespace, key) find_by(namespace: namespace, key: key)&.value end |
.remove(namespace, key) ⇒ void
This method returns an undefined value.
Removes a single key from a namespace.
69 70 71 |
# File 'app/models/secret.rb', line 69 def self.remove(namespace, key) find_by(namespace: namespace, key: key)&.destroy! end |
.write(namespace, pairs) ⇒ void
This method returns an undefined value.
Writes one or more key-value pairs under a namespace. Each pair is upserted (insert or update). The entire batch is wrapped in a transaction so partial writes cannot occur.
47 48 49 50 51 52 53 54 |
# File 'app/models/secret.rb', line 47 def self.write(namespace, pairs) transaction do pairs.each do |secret_key, secret_value| record = find_or_initialize_by(namespace: namespace, key: secret_key) record.update!(value: secret_value) end end end |