Class: Secret

Inherits:
ApplicationRecord show all
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

Class Method Summary collapse

Instance Attribute Details

#keyString

Returns credential identifier within the namespace.

Returns:

  • (String)

    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

#namespaceString

Returns grouping key (e.g. “anthropic”, “mcp”).

Returns:

  • (String)

    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

#valueString

Returns the secret value (encrypted at rest).

Returns:

  • (String)

    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.

Parameters:

  • ns (String)

    namespace to filter by

Returns:

  • (ActiveRecord::Relation)

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

Parameters:

  • namespace (String)

    top-level grouping key

Returns:

  • (Array<String>)

    credential keys



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.

Parameters:

  • namespace (String)

    top-level grouping key

  • key (String)

    credential key within the namespace

Returns:

  • (String, nil)

    decrypted value or nil if not found



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.

Parameters:

  • namespace (String)

    top-level grouping key

  • key (String)

    credential key to remove



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.

Parameters:

  • namespace (String)

    top-level grouping key

  • pairs (Hash<String, String>)

    key-value pairs to store



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