atlas_rb 1.3.7 → 1.3.9
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/.version +1 -1
- data/Gemfile.lock +5 -1
- data/README.md +35 -0
- data/lib/atlas_rb/configuration.rb +23 -0
- data/lib/atlas_rb/faraday_helper.rb +79 -13
- data/lib/atlas_rb.rb +3 -0
- metadata +16 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cf2f8c7f9ef316468a2544efe2f4471c23c547ae36d50b9abfee2efd03db38c8
|
|
4
|
+
data.tar.gz: 9da90557d04f84ddad26c05b6c50eab0d9a7378ccf57fbb37ba6b3e7e4355cd4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 640d113ae6693aacb7d7d2a943634561281ebb5324d7db5284429673a48ff1ae9cbedda4d55b058710c497573e4c7c379dee18b08119925c5a0e7e73f7105088
|
|
7
|
+
data.tar.gz: 8e7209552ebc5b7b8db138809bd89ef077ac282342c39d483f1c125f68d757c155ff12865f7a1c24bb65040bf287eb13c7ecd81549117cb0cf01d3a1bf58cc79
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.3.
|
|
1
|
+
1.3.9
|
data/Gemfile.lock
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
atlas_rb (1.3.
|
|
4
|
+
atlas_rb (1.3.9)
|
|
5
5
|
faraday (~> 2.7)
|
|
6
6
|
faraday-follow_redirects (~> 0.3.0)
|
|
7
7
|
faraday-multipart (~> 1)
|
|
8
8
|
hashie (~> 5.0)
|
|
9
|
+
jwt (~> 2.7)
|
|
9
10
|
|
|
10
11
|
GEM
|
|
11
12
|
remote: https://rubygems.org/
|
|
12
13
|
specs:
|
|
14
|
+
base64 (0.3.0)
|
|
13
15
|
diff-lcs (1.6.1)
|
|
14
16
|
faraday (2.14.2)
|
|
15
17
|
faraday-net_http (>= 2.0, < 3.5)
|
|
@@ -24,6 +26,8 @@ GEM
|
|
|
24
26
|
hashie (5.1.0)
|
|
25
27
|
logger
|
|
26
28
|
json (2.19.9)
|
|
29
|
+
jwt (2.10.3)
|
|
30
|
+
base64
|
|
27
31
|
logger (1.7.0)
|
|
28
32
|
multipart-post (2.4.1)
|
|
29
33
|
net-http (0.9.1)
|
data/README.md
CHANGED
|
@@ -106,6 +106,41 @@ In BYO-JWT mode:
|
|
|
106
106
|
To rotate or revoke, ask Cerberus to regenerate your token (Atlas rotates
|
|
107
107
|
the user's `jti`, invalidating outstanding tokens — single-token model).
|
|
108
108
|
|
|
109
|
+
### Relay-signing mode (the `ATLAS_TOKEN` replacement)
|
|
110
|
+
|
|
111
|
+
The default relay authenticates with the shared `ATLAS_TOKEN` and *asserts* the
|
|
112
|
+
acting user via a `User: NUID` header. Relay-signing replaces that with a
|
|
113
|
+
**proven** identity: the relay **signs** a short-lived assertion with its own
|
|
114
|
+
private key (`iss=cerberus`, `aud=atlas`, `sub` = the acting nuid, ES256), which
|
|
115
|
+
Atlas verifies against the matching public key. No shared secret, no asserted
|
|
116
|
+
header.
|
|
117
|
+
|
|
118
|
+
Configure a signing key (and the `kid` Atlas indexes its public key by) — value
|
|
119
|
+
or callable, so a Rails host reads it from credentials at request time:
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
AtlasRb.configure do |config|
|
|
123
|
+
config.assertion_signing_key = -> { Rails.application.credentials.cerberus_signing_key }
|
|
124
|
+
config.assertion_signing_kid = -> { Rails.application.credentials.cerberus_signing_kid }
|
|
125
|
+
end
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
When a signing key is configured, the regular relay (`connection` / `multipart`)
|
|
129
|
+
signs instead of sending `ATLAS_TOKEN` + `User:`. Otherwise it behaves exactly as
|
|
130
|
+
before — so signing **coexists with `ATLAS_TOKEN` during cutover** (turn it on by
|
|
131
|
+
configuring the key; roll back by clearing it).
|
|
132
|
+
|
|
133
|
+
Two things to know:
|
|
134
|
+
|
|
135
|
+
- **Acting-as rides a signed `obo` claim.** An `on_behalf_of` request is signed
|
|
136
|
+
with `sub` = the operator and `obo` = the target, inside the signature — so the
|
|
137
|
+
target can't be forged onto a stolen assertion, and no `On-Behalf-Of` header is
|
|
138
|
+
sent. Atlas admin-gates the operator and ignores any header obo on this path.
|
|
139
|
+
(Requires an Atlas on the signed-obo release; older Atlas would silently ignore
|
|
140
|
+
the claim — don't enable signing for acting-as traffic until Atlas is current.)
|
|
141
|
+
- **`ATLAS_JWT` still wins.** A personal token (BYO-JWT) takes precedence over
|
|
142
|
+
relay-signing.
|
|
143
|
+
|
|
109
144
|
### System-path credentials
|
|
110
145
|
|
|
111
146
|
Calls under `AtlasRb::System::*` (currently just SSO user provisioning)
|
|
@@ -44,5 +44,28 @@ module AtlasRb
|
|
|
44
44
|
# @return [Proc, nil] callable returning the on-behalf-of NUID, or nil
|
|
45
45
|
# to send no `On-Behalf-Of:` header.
|
|
46
46
|
attr_accessor :default_on_behalf_of
|
|
47
|
+
|
|
48
|
+
# Relay signing (the cerberus_token replacement). When set, the regular
|
|
49
|
+
# relay path *signs* a short-lived assertion (ES256, `sub` = acting nuid)
|
|
50
|
+
# instead of sending `ATLAS_TOKEN` + a `User:` header — identity becomes
|
|
51
|
+
# proven, not asserted. Leave nil (the default) to keep the legacy relay.
|
|
52
|
+
#
|
|
53
|
+
# Accepts either a value or a callable (resolved per request, so a Rails
|
|
54
|
+
# host can read it from request-scoped state / credentials). The value may
|
|
55
|
+
# be a PEM string or an `OpenSSL::PKey`; a PEM is parsed for you.
|
|
56
|
+
#
|
|
57
|
+
# @example Rails host (initializer), reading the EC private key from credentials
|
|
58
|
+
# AtlasRb.configure do |config|
|
|
59
|
+
# config.assertion_signing_key = -> { Rails.application.credentials.cerberus_signing_key }
|
|
60
|
+
# config.assertion_signing_kid = -> { Rails.application.credentials.cerberus_signing_kid }
|
|
61
|
+
# end
|
|
62
|
+
#
|
|
63
|
+
# @return [String, OpenSSL::PKey, Proc, nil] EC private key (PEM/key/callable), or nil.
|
|
64
|
+
attr_accessor :assertion_signing_key
|
|
65
|
+
|
|
66
|
+
# @return [String, Proc, nil] the `kid` stamped in the assertion header so
|
|
67
|
+
# Atlas selects the matching public key. Value or callable. Required when
|
|
68
|
+
# {#assertion_signing_key} is set.
|
|
69
|
+
attr_accessor :assertion_signing_kid
|
|
47
70
|
end
|
|
48
71
|
end
|
|
@@ -30,9 +30,31 @@ module AtlasRb
|
|
|
30
30
|
# precedence over `ATLAS_TOKEN`. This is the standalone-script path: a
|
|
31
31
|
# librarian exports their minted token and runs headless against the API.
|
|
32
32
|
#
|
|
33
|
+
# **Relay-signing mode ({AtlasRb.config#assertion_signing_key} set).** The
|
|
34
|
+
# cryptographic replacement for the `ATLAS_TOKEN` relay: instead of a shared
|
|
35
|
+
# secret + an asserted `User:` header, the regular relay path **signs** a
|
|
36
|
+
# short-lived assertion (ES256, `iss=cerberus`, `aud=atlas`, `sub` = the
|
|
37
|
+
# acting nuid) with Cerberus's private key — Atlas verifies it with the
|
|
38
|
+
# public key. No `User:` header; identity is the proven `sub`. **Acting-as
|
|
39
|
+
# rides a signed `obo` claim** inside the assertion (the target can't be forged
|
|
40
|
+
# onto a stolen assertion; Atlas admin-gates the operator and ignores any
|
|
41
|
+
# header obo on this path) — it is no longer punted to the legacy relay. Off
|
|
42
|
+
# unless a signing key is configured (so it coexists with `ATLAS_TOKEN`
|
|
43
|
+
# during cutover). `ATLAS_JWT`, if set, still wins over signing.
|
|
44
|
+
#
|
|
45
|
+
# Requires an Atlas that verifies the signed `obo` claim (the signed-obo
|
|
46
|
+
# release); against an older Atlas an `obo` would be silently ignored, so
|
|
47
|
+
# don't enable signing for acting-as traffic until Atlas is on that version.
|
|
48
|
+
#
|
|
33
49
|
# The module is mixed in via `extend`, so its methods become class methods on
|
|
34
50
|
# the host (e.g. `AtlasRb::Work.connection({})`).
|
|
35
51
|
module FaradayHelper
|
|
52
|
+
# Wire contract Atlas enforces for relay-signing assertions (see Atlas
|
|
53
|
+
# ApplicationController#verify_cerberus_assertion). iss/aud are fixed; the
|
|
54
|
+
# short TTL bounds replay (Atlas allows 30s leeway on exp).
|
|
55
|
+
ASSERTION_ISSUER = "cerberus"
|
|
56
|
+
ASSERTION_AUDIENCE = "atlas"
|
|
57
|
+
ASSERTION_TTL = 30 # seconds
|
|
36
58
|
# Build a JSON-content Faraday connection to the Atlas API.
|
|
37
59
|
#
|
|
38
60
|
# @param params [Hash] query-string / body params to attach to the request.
|
|
@@ -151,30 +173,74 @@ module AtlasRb
|
|
|
151
173
|
|
|
152
174
|
private
|
|
153
175
|
|
|
154
|
-
# Build the auth + identity headers shared by {#connection} and
|
|
155
|
-
#
|
|
156
|
-
#
|
|
157
|
-
#
|
|
158
|
-
# `ATLAS_TOKEN` and names the acting user, falling through to the
|
|
159
|
-
# configured `default_nuid` / `default_on_behalf_of` callables when the
|
|
160
|
-
# caller passes neither.
|
|
176
|
+
# Build the auth + identity headers shared by {#connection} and {#multipart}.
|
|
177
|
+
# Precedence: ATLAS_JWT (BYO-JWT) > relay-signing > ATLAS_TOKEN relay. The
|
|
178
|
+
# acting nuid / on_behalf_of fall through to the configured `default_nuid` /
|
|
179
|
+
# `default_on_behalf_of` callables here, once, for whichever mode applies.
|
|
161
180
|
def auth_headers(nuid, on_behalf_of)
|
|
162
181
|
jwt = ENV.fetch("ATLAS_JWT", nil)
|
|
163
182
|
return { "Authorization" => "Bearer #{jwt}" } if jwt
|
|
164
183
|
|
|
165
|
-
relay_headers(nuid, on_behalf_of)
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
# Relay-path headers: ATLAS_TOKEN bearer + the acting-user identity headers,
|
|
169
|
-
# falling through to the configured default_nuid / default_on_behalf_of.
|
|
170
|
-
def relay_headers(nuid, on_behalf_of)
|
|
171
184
|
nuid ||= AtlasRb.config.default_nuid&.call
|
|
172
185
|
on_behalf_of ||= AtlasRb.config.default_on_behalf_of&.call
|
|
173
186
|
|
|
187
|
+
signed_relay_headers(nuid, on_behalf_of) || relay_headers(nuid, on_behalf_of)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# A signed-assertion Authorization header (sub = acting nuid), or nil to
|
|
191
|
+
# defer to the legacy relay. nil when signing isn't configured or there is no
|
|
192
|
+
# acting nuid to put in `sub`. Acting-as is carried IN the assertion as a
|
|
193
|
+
# signed `obo` claim (Atlas honours it on the assertion path as of the
|
|
194
|
+
# signed-obo release), so it is no longer punted to the cerberus_token relay.
|
|
195
|
+
def signed_relay_headers(nuid, on_behalf_of)
|
|
196
|
+
return nil unless nuid
|
|
197
|
+
|
|
198
|
+
key = assertion_signing_key
|
|
199
|
+
return nil unless key
|
|
200
|
+
|
|
201
|
+
{ "Authorization" => "Bearer #{signed_assertion(nuid.to_s, key, on_behalf_of)}" }
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Legacy relay headers: ATLAS_TOKEN bearer + acting-user identity headers.
|
|
205
|
+
# `nuid` / `on_behalf_of` are already resolved by {#auth_headers}.
|
|
206
|
+
def relay_headers(nuid, on_behalf_of)
|
|
174
207
|
headers = { "Authorization" => "Bearer #{ENV.fetch("ATLAS_TOKEN", nil)}" }
|
|
175
208
|
headers["User"] = "NUID #{nuid}" if nuid
|
|
176
209
|
headers["On-Behalf-Of"] = "NUID #{on_behalf_of}" if on_behalf_of
|
|
177
210
|
headers
|
|
178
211
|
end
|
|
212
|
+
|
|
213
|
+
# Mint a Cerberus relay assertion for `nuid`, signed ES256 with `key`. The
|
|
214
|
+
# `kid` header tells Atlas which public key to verify against; iss/aud are
|
|
215
|
+
# the fixed contract; the short TTL bounds replay; `jti` is forward-compat
|
|
216
|
+
# for an Atlas-side one-time cache. When `on_behalf_of` is given, it rides as
|
|
217
|
+
# a SIGNED `obo` claim — acting-as that can't be forged onto a stolen
|
|
218
|
+
# assertion (Atlas admin-gates the operator and ignores any header obo here).
|
|
219
|
+
def signed_assertion(nuid, key, on_behalf_of = nil)
|
|
220
|
+
now = Time.now.to_i
|
|
221
|
+
payload = { "iss" => ASSERTION_ISSUER, "aud" => ASSERTION_AUDIENCE, "sub" => nuid,
|
|
222
|
+
"iat" => now, "exp" => now + ASSERTION_TTL, "jti" => SecureRandom.uuid,
|
|
223
|
+
"obo" => on_behalf_of&.to_s }.compact # obo only when acting-as
|
|
224
|
+
JWT.encode(payload, key, "ES256", { kid: assertion_signing_kid })
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Resolve the configured signing key to an OpenSSL::PKey, or nil if signing
|
|
228
|
+
# is not configured. Accepts a callable (resolved per request), a PEM
|
|
229
|
+
# string (parsed), or an already-built key.
|
|
230
|
+
def assertion_signing_key
|
|
231
|
+
raw = config_value(AtlasRb.config.assertion_signing_key)
|
|
232
|
+
return nil if raw.nil?
|
|
233
|
+
|
|
234
|
+
raw.is_a?(OpenSSL::PKey::PKey) ? raw : OpenSSL::PKey.read(raw)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def assertion_signing_kid
|
|
238
|
+
config_value(AtlasRb.config.assertion_signing_kid)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Config slots may hold a value or a callable resolved at request time.
|
|
242
|
+
def config_value(value)
|
|
243
|
+
value.respond_to?(:call) ? value.call : value
|
|
244
|
+
end
|
|
179
245
|
end
|
|
180
246
|
end
|
data/lib/atlas_rb.rb
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
require "faraday"
|
|
4
4
|
require "faraday/multipart"
|
|
5
5
|
require "faraday/follow_redirects"
|
|
6
|
+
require "jwt"
|
|
7
|
+
require "openssl"
|
|
8
|
+
require "securerandom"
|
|
6
9
|
require_relative "atlas_rb/version"
|
|
7
10
|
require_relative "atlas_rb/errors"
|
|
8
11
|
require_relative "atlas_rb/configuration"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: atlas_rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.3.
|
|
4
|
+
version: 1.3.9
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Cliff
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -24,6 +24,20 @@ dependencies:
|
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
26
|
version: '2.7'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: jwt
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '2.7'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '2.7'
|
|
27
41
|
- !ruby/object:Gem::Dependency
|
|
28
42
|
name: faraday-multipart
|
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|