Class: AceMQ::AMQP::Credentials
- Inherits:
-
Object
- Object
- AceMQ::AMQP::Credentials
- Defined in:
- lib/acemq/amqp/credentials.rb
Overview
A username and a secret the broker will accept.
This exists so a password does not have to live in a connection string.
A URL is the one piece of configuration that gets printed: it goes in
error messages, in structured logs, in ps output when it arrives as an
argument, and in whatever the deployment tool echoes back. Once a password
has been through any of those it has to be rotated, and the rotation is
the expensive part. Credentials passed separately never take that trip.
mq = Connection.open("amqp://broker:5672",
credentials: Credentials.from_env)
The second reason this is a type rather than two strings is #inspect. A
bare string reaches a log the moment anything dumps the object holding it
— an exception, a p left in during debugging, a structured logger that
serialises its context. This class renders as its username and the word
[REDACTED], whichever of inspect, to_s or %p asks, so the accident
produces something useless rather than something to rotate.
Constant Summary collapse
- REDACTED =
What replaces the secret in every rendering of this object.
"[REDACTED]"
Instance Attribute Summary collapse
-
#secret ⇒ Object
readonly
The password or token.
-
#username ⇒ Object
readonly
The account name, or an empty string for a bearer token.
Class Method Summary collapse
-
.describe(value) ⇒ Object
private
A short description of a value for an error message, naming its class rather than printing it — the thing that was passed in place of credentials is quite likely to be the password itself.
-
.from_env(username_variable: "ACEMQ_USERNAME", password_variable: "ACEMQ_PASSWORD") ⇒ Object
Credentials read from the environment.
-
.from_file(path, username: nil) ⇒ Object
Credentials read from a file, which is how a mounted Kubernetes secret or a Docker secret arrives.
-
.of(username:, password:) ⇒ Object
A username and password.
-
.resolve(source) ⇒ Object
Whatever a caller supplied, as Credentials or nil.
-
.token(token) ⇒ Object
A bearer token with no username, which is how OAuth 2 against RabbitMQ is presented: the token is the password and the username is ignored.
Instance Method Summary collapse
-
#==(other) ⇒ Object
(also: #eql?)
Two credentials are the same when both halves match.
-
#empty? ⇒ Boolean
Whether these carry nothing at all.
- #hash ⇒ Object
-
#initialize(username: nil, password: nil) ⇒ Credentials
constructor
A new instance of Credentials.
-
#inspect ⇒ Object
Never the secret.
-
#to_s ⇒ Object
Never the secret either.
-
#to_transport_options ⇒ Hash
The bunny options these describe.
-
#token? ⇒ Boolean
Whether these are a token rather than an account.
Constructor Details
#initialize(username: nil, password: nil) ⇒ Credentials
Returns a new instance of Credentials.
58 59 60 61 62 |
# File 'lib/acemq/amqp/credentials.rb', line 58 def initialize(username: nil, password: nil) @username = username.to_s @secret = password.to_s freeze end |
Instance Attribute Details
#secret ⇒ Object (readonly)
The password or token. Do not log the result.
Named secret rather than password because it is also where a bearer
token lives, and a reader who sees password on a token is entitled to
assume the token was the wrong thing to put there.
182 183 184 |
# File 'lib/acemq/amqp/credentials.rb', line 182 def secret @secret end |
#username ⇒ Object (readonly)
The account name, or an empty string for a bearer token.
54 55 56 |
# File 'lib/acemq/amqp/credentials.rb', line 54 def username @username end |
Class Method Details
.describe(value) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
A short description of a value for an error message, naming its class rather than printing it — the thing that was passed in place of credentials is quite likely to be the password itself.
154 155 156 |
# File 'lib/acemq/amqp/credentials.rb', line 154 def self.describe(value) value.nil? ? "nil" : "a #{value.class}" end |
.from_env(username_variable: "ACEMQ_USERNAME", password_variable: "ACEMQ_PASSWORD") ⇒ Object
Credentials read from the environment.
The environment is where an orchestrator puts a secret, and reading it here rather than interpolating it into a URL somewhere in a start-up script is the difference between a password that stays in the process and one that ends up in the command line.
85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/acemq/amqp/credentials.rb', line 85 def self.from_env(username_variable: "ACEMQ_USERNAME", password_variable: "ACEMQ_PASSWORD") username = ENV.fetch(username_variable, nil) password = ENV.fetch(password_variable, nil) if username.nil? && password.nil? raise ConfigurationError, "neither #{username_variable} nor #{password_variable} is set in the " \ "environment, so there are no credentials to read" end new(username: username, password: password) end |
.from_file(path, username: nil) ⇒ Object
Credentials read from a file, which is how a mounted Kubernetes secret or a Docker secret arrives.
The file holds the password alone, or username:password when it
carries both. Trailing whitespace is trimmed, because a file written by
an editor almost always ends in a newline, and a password with a newline
on the end is refused by the broker with an error that says nothing
about newlines.
Read now rather than remembered as a path: a caller that wants the file re-read on every connection — a sidecar rotating the secret underneath it — passes a block instead, which resolve calls at connection time.
114 115 116 117 118 119 120 121 122 |
# File 'lib/acemq/amqp/credentials.rb', line 114 def self.from_file(path, username: nil) contents = begin File.read(path) rescue SystemCallError, IOError => e raise ConfigurationError, "cannot read the credentials file #{path}: #{e.}" end from_file_contents(contents.sub(/[ \t\r\n]+\z/, ""), path, username) end |
.of(username:, password:) ⇒ Object
A username and password.
65 66 67 |
# File 'lib/acemq/amqp/credentials.rb', line 65 def self.of(username:, password:) new(username: username, password: password) end |
.resolve(source) ⇒ Object
Whatever a caller supplied, as AceMQ::AMQP::Credentials or nil.
A block or anything else answering call is invoked here, at the moment
the connection is made. That is the whole point of accepting one: a
password rotated by a sidecar is only useful if something asks for it
again rather than remembering what it read at start-up.
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
# File 'lib/acemq/amqp/credentials.rb', line 133 def self.resolve(source) case source when nil then nil when Credentials then source else resolved = source.respond_to?(:call) ? source.call : source unless resolved.is_a?(Credentials) raise ConfigurationError, "credentials must be an AceMQ::AMQP::Credentials, or something " \ "answering #call that returns one, not #{describe(source)}" end resolved end end |
.token(token) ⇒ Object
A bearer token with no username, which is how OAuth 2 against RabbitMQ is presented: the token is the password and the username is ignored.
71 72 73 |
# File 'lib/acemq/amqp/credentials.rb', line 71 def self.token(token) new(password: token) end |
Instance Method Details
#==(other) ⇒ Object Also known as: eql?
Two credentials are the same when both halves match.
206 207 208 |
# File 'lib/acemq/amqp/credentials.rb', line 206 def ==(other) other.is_a?(Credentials) && other.username == username && other.secret == secret end |
#empty? ⇒ Boolean
Whether these carry nothing at all.
185 |
# File 'lib/acemq/amqp/credentials.rb', line 185 def empty? = username.empty? && secret.empty? |
#hash ⇒ Object
211 |
# File 'lib/acemq/amqp/credentials.rb', line 211 def hash = [self.class, username, secret].hash |
#inspect ⇒ Object
Never the secret. See the class comment for why this matters more than it looks like it should.
215 216 217 |
# File 'lib/acemq/amqp/credentials.rb', line 215 def inspect "#<AceMQ::AMQP::Credentials #{self}>" end |
#to_s ⇒ Object
Never the secret either. Both are overridden because interpolation uses
one and p uses the other, and a leak needs only whichever was missed.
221 222 223 |
# File 'lib/acemq/amqp/credentials.rb', line 221 def to_s token? ? "token=#{REDACTED}" : "username=#{username.inspect} secret=#{REDACTED}" end |
#to_transport_options ⇒ Hash
The bunny options these describe.
The username is left out when there is none, rather than sent as an empty string: a token is authenticated by the token, and overwriting whatever the URL said with "" would take a working connection apart for no reason.
198 199 200 201 202 203 |
# File 'lib/acemq/amqp/credentials.rb', line 198 def = {} [:username] = username unless username.empty? [:password] = secret unless secret.empty? end |
#token? ⇒ Boolean
Whether these are a token rather than an account.
188 |
# File 'lib/acemq/amqp/credentials.rb', line 188 def token? = username.empty? && !secret.empty? |