Class: Railspress::AgentBootstrapKey

Inherits:
ApplicationRecord show all
Defined in:
app/models/railspress/agent_bootstrap_key.rb

Defined Under Namespace

Classes: ExchangeError

Constant Summary collapse

DEFAULT_TTL =
1.hour

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.authenticate(raw_token) ⇒ Object



60
61
62
63
64
65
66
67
68
69
# File 'app/models/railspress/agent_bootstrap_key.rb', line 60

def authenticate(raw_token)
  parsed = parse_token(raw_token)
  return nil unless parsed

  candidate = active.where(token_prefix: parsed[:prefix]).recent.first
  return nil unless candidate
  return nil unless secure_digest_match?(candidate.token_digest, digest(parsed[:secret]))

  candidate
end

.build_token(prefix, raw_secret) ⇒ Object



71
72
73
# File 'app/models/railspress/agent_bootstrap_key.rb', line 71

def build_token(prefix, raw_secret)
  "rpb_#{Rails.env}_#{prefix}_#{raw_secret}"
end

.digest(raw_secret) ⇒ Object



75
76
77
# File 'app/models/railspress/agent_bootstrap_key.rb', line 75

def digest(raw_secret)
  OpenSSL::HMAC.hexdigest("SHA256", digest_key, raw_secret.to_s)
end

.issue!(name:, actor:, owner: actor, expires_at: DEFAULT_TTL.from_now) ⇒ Object



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'app/models/railspress/agent_bootstrap_key.rb', line 42

def issue!(name:, actor:, owner: actor, expires_at: DEFAULT_TTL.from_now)
  prefix = generate_prefix
  raw_secret = generate_secret
  token = build_token(prefix, raw_secret)

  bootstrap_key = create!(
    name: name,
    token_prefix: prefix,
    token_digest: digest(raw_secret),
    secret_ciphertext: raw_secret,
    owner: owner,
    created_by: actor,
    expires_at: expires_at
  )

  [ bootstrap_key, token ]
end

.parse_token(raw_token) ⇒ Object



79
80
81
82
83
84
85
86
# File 'app/models/railspress/agent_bootstrap_key.rb', line 79

def parse_token(raw_token)
  return nil if raw_token.blank?

  match = raw_token.match(/\Arpb_[a-z0-9_]+_([a-f0-9]{12})_([a-f0-9]{64})\z/)
  return nil unless match

  { prefix: match[1], secret: match[2] }
end

Instance Method Details

#active?Boolean

Returns:

  • (Boolean)


141
142
143
# File 'app/models/railspress/agent_bootstrap_key.rb', line 141

def active?
  revoked_at.nil? && used_at.nil? && expires_at > Time.current
end

#exchange!(ip_address: nil, api_key_name: default_api_key_name, api_key_expires_at: nil) ⇒ Object

Raises:



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'app/models/railspress/agent_bootstrap_key.rb', line 110

def exchange!(ip_address: nil, api_key_name: default_api_key_name, api_key_expires_at: nil)
  raise ExchangeError, "Bootstrap key is not active." unless active?

  transaction do
    api_key_actor = owner || created_by
    api_key_owner = owner || api_key_actor
    api_key, plain_token = Railspress::ApiKey.issue!(
      name: api_key_name,
      actor: api_key_actor,
      owner: api_key_owner,
      expires_at: api_key_expires_at
    )

    update!(
      used_at: Time.current,
      used_ip: ip_address,
      exchanged_api_key: api_key
    )

    [ api_key, plain_token ]
  end
end

#revoke!(actor:, reason: "revoked") ⇒ Object



133
134
135
136
137
138
139
# File 'app/models/railspress/agent_bootstrap_key.rb', line 133

def revoke!(actor:, reason: "revoked")
  update!(
    revoked_at: Time.current,
    revoke_reason: reason,
    revoked_by: actor
  )
end

#statusObject



145
146
147
148
149
150
151
# File 'app/models/railspress/agent_bootstrap_key.rb', line 145

def status
  return "revoked" if revoked_at.present?
  return "used" if used_at.present?
  return "expired" if expires_at <= Time.current

  "active"
end