atlas_rb 1.8.11 → 1.9.0
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 +1 -1
- data/lib/atlas_rb/authentication.rb +9 -4
- data/lib/atlas_rb/configuration.rb +11 -2
- data/lib/atlas_rb/faraday_helper.rb +19 -11
- data/lib/atlas_rb/system/user.rb +24 -15
- data/lib/atlas_rb/user.rb +55 -9
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9bfa2bcb90b5d30ebbf8a4a2612b1a6aafab5fc4eb07df8b12c3ba904ab1fb48
|
|
4
|
+
data.tar.gz: 9afb80d4f4cecfdfa5204ad101967fe4360057a2222ca573153e00bfc3a3e281
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2dbd56463086ce26fc3380eaee4e850dfe6c66a15d6e716c22271d8a13230fbb9db122195f368b41dad8edf96eb153540c699c567f0d43b50b96bc95e8d5c1b7
|
|
7
|
+
data.tar.gz: cfba3517bd027ae9ff972b3316da4a3bd941e28a080961a69d16625b874bd6b6f5ececd503327d5f0b9ebc56ffc118bd36c04823e2a94ba07aa45a4179fcdc4b
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.
|
|
1
|
+
1.9.0
|
data/Gemfile.lock
CHANGED
|
@@ -17,7 +17,14 @@ module AtlasRb
|
|
|
17
17
|
|
|
18
18
|
# Look up the Atlas user record for an NUID.
|
|
19
19
|
#
|
|
20
|
+
# A NUID can hold several accounts (a person's staff/student logins). Pass
|
|
21
|
+
# `email:` to act as — and read back — a specific one; it rides as a signed
|
|
22
|
+
# `acct` claim so `GET /user` resolves that account. Omit it and Atlas
|
|
23
|
+
# returns the person's preferred account.
|
|
24
|
+
#
|
|
20
25
|
# @param nuid [String] the user's Northeastern University ID.
|
|
26
|
+
# @param email [String, nil] optional account email to act as (the `acct`
|
|
27
|
+
# selector); nil resolves the preferred account.
|
|
21
28
|
# @return [Hash] the user record returned by `GET /user`, including at
|
|
22
29
|
# minimum `"id"`, `"name"`, and `"groups"`.
|
|
23
30
|
# @raise [JSON::ParserError] if the response body is not valid JSON
|
|
@@ -26,10 +33,8 @@ module AtlasRb
|
|
|
26
33
|
# @example
|
|
27
34
|
# AtlasRb::Authentication.login("001234567")
|
|
28
35
|
# # => { "id" => 42, "name" => "Jane Doe", "groups" => [...] }
|
|
29
|
-
def self.login(nuid)
|
|
30
|
-
|
|
31
|
-
# need hash - id, name, token => ...
|
|
32
|
-
AtlasRb::Mash.new(JSON.parse(connection({}, nuid).get('/user')&.body))
|
|
36
|
+
def self.login(nuid, email: nil)
|
|
37
|
+
AtlasRb::Mash.new(JSON.parse(connection({}, nuid, account: email).get('/user')&.body))
|
|
33
38
|
end
|
|
34
39
|
|
|
35
40
|
# Fetch only the group memberships for an NUID.
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
module AtlasRb
|
|
4
4
|
# Holds gem-wide configuration registered via {AtlasRb.configure}.
|
|
5
5
|
#
|
|
6
|
-
# The configuration model is deliberately tiny:
|
|
7
|
-
# accept callables. The gem stays consumer-agnostic — it knows nothing
|
|
6
|
+
# The configuration model is deliberately tiny: a handful of slots, all of
|
|
7
|
+
# which accept callables. The gem stays consumer-agnostic — it knows nothing
|
|
8
8
|
# about Rails, Devise, or any host application's request lifecycle — and
|
|
9
9
|
# instead lets the consumer hand it lambdas that resolve the per-request
|
|
10
10
|
# ambient context when a request is about to go out.
|
|
@@ -45,6 +45,15 @@ module AtlasRb
|
|
|
45
45
|
# to send no `On-Behalf-Of:` header.
|
|
46
46
|
attr_accessor :default_on_behalf_of
|
|
47
47
|
|
|
48
|
+
# @return [Proc, nil] callable returning the ambient account email when a
|
|
49
|
+
# call is made without an explicit `account:` kwarg. A person's NUID can
|
|
50
|
+
# hold several accounts (staff/student logins); this names which one is
|
|
51
|
+
# acting, signed into the assertion as an `acct` claim (companion to
|
|
52
|
+
# {#default_nuid}). Typically `-> { Current.account_email }` in a Rails
|
|
53
|
+
# host. `nil` (the default) signs no `acct` — Atlas then resolves the
|
|
54
|
+
# person's preferred account.
|
|
55
|
+
attr_accessor :default_account
|
|
56
|
+
|
|
48
57
|
# Relay signing. When set, the regular relay path *signs* a short-lived
|
|
49
58
|
# assertion (ES256, `sub` = acting nuid) — identity is proven, not asserted.
|
|
50
59
|
# This is the relay credential: with no signing key configured (and no
|
|
@@ -76,6 +76,10 @@ module AtlasRb
|
|
|
76
76
|
# claim (acting-as / view-as). When `nil`, falls through to
|
|
77
77
|
# `AtlasRb.config.default_on_behalf_of&.call`; if that is also nil, no
|
|
78
78
|
# `obo` claim is added.
|
|
79
|
+
# @param account [String, nil] optional account email carried as a signed
|
|
80
|
+
# `acct` claim, naming which of the NUID's accounts is acting. When `nil`,
|
|
81
|
+
# falls through to `AtlasRb.config.default_account&.call`; if that is also
|
|
82
|
+
# nil, no `acct` claim is added and Atlas resolves the preferred account.
|
|
79
83
|
# @param idempotency_key [String, nil] optional UUID to send in the
|
|
80
84
|
# `Idempotency-Key` header. Used by retry-safe create flows (currently
|
|
81
85
|
# `POST /works`, `POST /file_sets`, `POST /files`) to deduplicate replays
|
|
@@ -92,8 +96,8 @@ module AtlasRb
|
|
|
92
96
|
#
|
|
93
97
|
# @example Fetching a community
|
|
94
98
|
# AtlasRb::Community.connection({}).get('/communities/abc123')
|
|
95
|
-
def connection(params, nuid=nil, on_behalf_of: nil, idempotency_key: nil, auth: :required)
|
|
96
|
-
headers = auth_headers(nuid, on_behalf_of, optional: auth == :optional)
|
|
99
|
+
def connection(params, nuid=nil, on_behalf_of: nil, account: nil, idempotency_key: nil, auth: :required)
|
|
100
|
+
headers = auth_headers(nuid, on_behalf_of, account: account, optional: auth == :optional)
|
|
97
101
|
.merge("Content-Type" => "application/json")
|
|
98
102
|
headers["Idempotency-Key"] = idempotency_key if idempotency_key
|
|
99
103
|
|
|
@@ -135,8 +139,8 @@ module AtlasRb
|
|
|
135
139
|
# "scan.pdf")
|
|
136
140
|
# }
|
|
137
141
|
# AtlasRb::Blob.multipart.post('/files/', payload)
|
|
138
|
-
def multipart(nuid=nil, on_behalf_of: nil, idempotency_key: nil)
|
|
139
|
-
headers = auth_headers(nuid, on_behalf_of)
|
|
142
|
+
def multipart(nuid=nil, on_behalf_of: nil, account: nil, idempotency_key: nil)
|
|
143
|
+
headers = auth_headers(nuid, on_behalf_of, account: account)
|
|
140
144
|
headers["Idempotency-Key"] = idempotency_key if idempotency_key
|
|
141
145
|
|
|
142
146
|
Faraday.new(
|
|
@@ -243,14 +247,15 @@ module AtlasRb
|
|
|
243
247
|
# is only for endpoints Atlas serves with `require_auth` skipped (`GET
|
|
244
248
|
# /reset`); every normal endpoint leaves `optional` false so a
|
|
245
249
|
# misconfiguration fails loudly rather than silently going unauthenticated.
|
|
246
|
-
def auth_headers(nuid, on_behalf_of, optional: false)
|
|
250
|
+
def auth_headers(nuid, on_behalf_of, account: nil, optional: false)
|
|
247
251
|
jwt = ENV.fetch("ATLAS_JWT", nil)
|
|
248
252
|
return { "Authorization" => "Bearer #{jwt}" } if jwt
|
|
249
253
|
|
|
250
254
|
nuid ||= AtlasRb.config.default_nuid&.call
|
|
251
255
|
on_behalf_of ||= AtlasRb.config.default_on_behalf_of&.call
|
|
256
|
+
account ||= AtlasRb.config.default_account&.call
|
|
252
257
|
|
|
253
|
-
headers = signed_relay_headers(nuid, on_behalf_of)
|
|
258
|
+
headers = signed_relay_headers(nuid, on_behalf_of, account)
|
|
254
259
|
return headers if headers
|
|
255
260
|
return {} if optional
|
|
256
261
|
|
|
@@ -262,14 +267,16 @@ module AtlasRb
|
|
|
262
267
|
# A signed-assertion Authorization header (sub = acting nuid), or nil when
|
|
263
268
|
# signing isn't configured or there is no acting nuid to put in `sub`.
|
|
264
269
|
# Acting-as is carried IN the assertion as a signed `obo` claim (Atlas
|
|
265
|
-
# honours it on the assertion path; a header obo is ignored there).
|
|
266
|
-
|
|
270
|
+
# honours it on the assertion path; a header obo is ignored there). When a
|
|
271
|
+
# NUID holds several accounts, `account` (email) rides as a signed `acct`
|
|
272
|
+
# claim naming which one is acting; absent, Atlas picks the preferred.
|
|
273
|
+
def signed_relay_headers(nuid, on_behalf_of, account = nil)
|
|
267
274
|
return nil unless nuid
|
|
268
275
|
|
|
269
276
|
key = assertion_signing_key
|
|
270
277
|
return nil unless key
|
|
271
278
|
|
|
272
|
-
{ "Authorization" => "Bearer #{signed_assertion(nuid.to_s, key, on_behalf_of)}" }
|
|
279
|
+
{ "Authorization" => "Bearer #{signed_assertion(nuid.to_s, key, on_behalf_of, account)}" }
|
|
273
280
|
end
|
|
274
281
|
|
|
275
282
|
# Mint a Cerberus relay assertion for `nuid`, signed ES256 with `key`. The
|
|
@@ -278,11 +285,12 @@ module AtlasRb
|
|
|
278
285
|
# for an Atlas-side one-time cache. When `on_behalf_of` is given, it rides as
|
|
279
286
|
# a SIGNED `obo` claim — acting-as that can't be forged onto a stolen
|
|
280
287
|
# assertion (Atlas admin-gates the operator and ignores any header obo here).
|
|
281
|
-
def signed_assertion(nuid, key, on_behalf_of = nil)
|
|
288
|
+
def signed_assertion(nuid, key, on_behalf_of = nil, account = nil)
|
|
282
289
|
now = Time.now.to_i
|
|
283
290
|
payload = { "iss" => ASSERTION_ISSUER, "aud" => ASSERTION_AUDIENCE, "sub" => nuid,
|
|
284
291
|
"iat" => now, "exp" => now + ASSERTION_TTL, "jti" => SecureRandom.uuid,
|
|
285
|
-
"obo"
|
|
292
|
+
"obo" => on_behalf_of&.to_s, # obo only when acting-as
|
|
293
|
+
"acct" => account&.to_s }.compact # acct only when a specific account is named
|
|
286
294
|
JWT.encode(payload, key, "ES256", { kid: assertion_signing_kid })
|
|
287
295
|
end
|
|
288
296
|
|
data/lib/atlas_rb/system/user.rb
CHANGED
|
@@ -27,9 +27,10 @@ module AtlasRb
|
|
|
27
27
|
NUID = "000000000"
|
|
28
28
|
|
|
29
29
|
# SSO-callback user provisioning. Finds the Atlas `User` row keyed on
|
|
30
|
-
# the supplied
|
|
31
|
-
# `groups` with the IdP-asserted set. Full replace, not
|
|
32
|
-
# the IdP assertion is authoritative.
|
|
30
|
+
# the supplied email — the account key — (creating it if missing) and
|
|
31
|
+
# replaces its `groups` with the IdP-asserted set. Full replace, not
|
|
32
|
+
# merge — the IdP assertion is authoritative. Keying on email keeps a
|
|
33
|
+
# person's staff/student logins as distinct accounts under one NUID.
|
|
33
34
|
#
|
|
34
35
|
# Always authenticates via {FaradayHelper#system_connection}, so the
|
|
35
36
|
# caller has no way to act as a non-system principal. Atlas allows
|
|
@@ -37,32 +38,40 @@ module AtlasRb
|
|
|
37
38
|
class User
|
|
38
39
|
extend AtlasRb::FaradayHelper
|
|
39
40
|
|
|
40
|
-
# Find-or-create the User keyed on
|
|
41
|
+
# Find-or-create the User keyed on email and replace its groups.
|
|
41
42
|
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
43
|
+
# Email is the account key: a person's staff and student logins share a
|
|
44
|
+
# NUID but present a different email each, so keying on email keeps them
|
|
45
|
+
# as distinct accounts instead of collapsing (last-write-wins) on NUID.
|
|
46
|
+
#
|
|
47
|
+
# @param email [String] the account key — the login's `mail`.
|
|
48
|
+
# This is the *subject* of the operation, not the actor; the actor is
|
|
49
|
+
# always the system fixture.
|
|
50
|
+
# @param nuid [String, nil] the person's NUID (the grouping thread that
|
|
51
|
+
# ties a person's accounts together). Forwarded when available.
|
|
45
52
|
# @param groups [Array<String>] full group set; replaces, not merges.
|
|
46
53
|
# @param name [String, nil] forwarded if the SSO callback has it;
|
|
47
54
|
# Atlas treats this field as optional.
|
|
48
|
-
# @param
|
|
49
|
-
#
|
|
55
|
+
# @param affiliation [String, nil] the login's unscoped-affiliation, a
|
|
56
|
+
# human label for the account (e.g. "staff"/"student"); optional.
|
|
50
57
|
# @return [AtlasRb::Mash] the resulting User record (`id`, `nuid`,
|
|
51
|
-
# `name`, `email`, `role`, `groups`).
|
|
58
|
+
# `name`, `email`, `role`, `groups`, `affiliation`, `preferred`).
|
|
52
59
|
#
|
|
53
60
|
# @example From Cerberus's SSO callback
|
|
54
61
|
# AtlasRb::System::User.find_or_create(
|
|
62
|
+
# email: "j.doe@northeastern.edu",
|
|
55
63
|
# nuid: "001234567",
|
|
56
64
|
# groups: ["northeastern:staff", "drs:editors"],
|
|
57
65
|
# name: "Jane Doe",
|
|
58
|
-
#
|
|
66
|
+
# affiliation: "staff"
|
|
59
67
|
# )
|
|
60
|
-
def self.find_or_create(
|
|
68
|
+
def self.find_or_create(email:, nuid: nil, groups: [], name: nil, affiliation: nil)
|
|
61
69
|
body = { groups: groups }
|
|
62
|
-
body[:
|
|
63
|
-
body[:
|
|
70
|
+
body[:nuid] = nuid if nuid
|
|
71
|
+
body[:name] = name if name
|
|
72
|
+
body[:affiliation] = affiliation if affiliation
|
|
64
73
|
|
|
65
|
-
response = system_connection.put("/users/
|
|
74
|
+
response = system_connection.put("/users/by_email/#{email}", body.to_json)
|
|
66
75
|
AtlasRb::Mash.new(JSON.parse(response.body))["user"]
|
|
67
76
|
end
|
|
68
77
|
end
|
data/lib/atlas_rb/user.rb
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module AtlasRb
|
|
4
|
-
#
|
|
5
|
-
#
|
|
4
|
+
# User directory lookups (typeahead search, NUID → name resolution) plus a
|
|
5
|
+
# person's own account management — enumerating the accounts that share their
|
|
6
|
+
# NUID and choosing a preferred (default) one.
|
|
6
7
|
#
|
|
7
8
|
# This is a **user-context** binding: calls authenticate as the acting
|
|
8
9
|
# user via the standard relay-signing path (the acting NUID is signed into
|
|
9
10
|
# the assertion `sub`), like every other top-level class. It is
|
|
10
11
|
# deliberately *not* part of
|
|
11
12
|
# {AtlasRb::System} — that namespace is structurally reserved for
|
|
12
|
-
# system-token calls ({System::User.find_or_create}), and
|
|
13
|
-
#
|
|
13
|
+
# system-token calls ({System::User.find_or_create}), and these are ordinary
|
|
14
|
+
# logged-in-user capabilities.
|
|
14
15
|
#
|
|
15
|
-
#
|
|
16
|
-
# only (no email, role, or groups), and rows with role
|
|
17
|
-
# `guest`, or `system` are never returned.
|
|
18
|
-
#
|
|
19
|
-
#
|
|
16
|
+
# The directory lookups enforce minimal disclosure: every entry carries
|
|
17
|
+
# `nuid` + `name` only (no email, role, or groups), and rows with role
|
|
18
|
+
# `anonymous`, `guest`, or `system` are never returned. The account methods
|
|
19
|
+
# ({.accounts} / {.set_preferred}) *do* disclose email/groups/affiliation, so
|
|
20
|
+
# Atlas limits them to the person themselves (matching NUID), an admin, or the
|
|
21
|
+
# system principal. Per the layering principle the gem adds nothing on top —
|
|
22
|
+
# no caching, no name parsing, no result shaping; presentation belongs to the
|
|
23
|
+
# host application.
|
|
20
24
|
class User
|
|
21
25
|
extend AtlasRb::FaradayHelper
|
|
22
26
|
|
|
@@ -88,5 +92,47 @@ module AtlasRb
|
|
|
88
92
|
connection({ nuids: Array(nuids).join(",") }, nuid).get(ROUTE)&.body
|
|
89
93
|
).map { |entry| AtlasRb::Mash.new(entry) }
|
|
90
94
|
end
|
|
95
|
+
|
|
96
|
+
# List the accounts sharing a NUID — a person's staff/student logins.
|
|
97
|
+
#
|
|
98
|
+
# Unlike the directory lookups this discloses each account's email,
|
|
99
|
+
# affiliation label, role, stored group set, and which one is preferred —
|
|
100
|
+
# the data the login "you have more than one account" check and the My DRS
|
|
101
|
+
# accounts panel render from. Atlas limits it to the person themselves, an
|
|
102
|
+
# admin, or the system principal; others get a `403`.
|
|
103
|
+
#
|
|
104
|
+
# @param target_nuid [String] the NUID whose accounts to list — the
|
|
105
|
+
# *subject*, distinct from the acting `nuid:` kwarg.
|
|
106
|
+
# @param nuid [String, nil] optional acting user's NUID (signed into the
|
|
107
|
+
# assertion `sub`); on the BYO-JWT path identity lives in the token.
|
|
108
|
+
# @return [AtlasRb::Mash] the envelope: `nuid` plus an `accounts` array,
|
|
109
|
+
# each entry carrying `email`, `name`, `affiliation`, `role`, `groups`,
|
|
110
|
+
# and `preferred`.
|
|
111
|
+
#
|
|
112
|
+
# @example Login-time multi-account detection
|
|
113
|
+
# AtlasRb::User.accounts("001234567", nuid: "001234567")["accounts"].size # => 2
|
|
114
|
+
def self.accounts(target_nuid, nuid: nil)
|
|
115
|
+
AtlasRb::Mash.new(JSON.parse(
|
|
116
|
+
connection({}, nuid).get("#{ROUTE}/by_nuid/#{target_nuid}/accounts")&.body
|
|
117
|
+
))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Set the preferred (default) account for a NUID — the account chosen when
|
|
121
|
+
# a login names no specific one. Same self/admin/system scope as {.accounts}.
|
|
122
|
+
#
|
|
123
|
+
# @param target_nuid [String] the NUID whose default is being set.
|
|
124
|
+
# @param email [String] the account to make preferred; must be one of the
|
|
125
|
+
# NUID's accounts (else Atlas returns `404`).
|
|
126
|
+
# @param nuid [String, nil] optional acting user's NUID (signed into `sub`).
|
|
127
|
+
# @return [AtlasRb::Mash] the updated account record under `"user"`.
|
|
128
|
+
#
|
|
129
|
+
# @example
|
|
130
|
+
# AtlasRb::User.set_preferred("001234567", email: "j.doe@husky.neu.edu",
|
|
131
|
+
# nuid: "001234567")
|
|
132
|
+
def self.set_preferred(target_nuid, email:, nuid: nil)
|
|
133
|
+
response = connection({}, nuid)
|
|
134
|
+
.put("#{ROUTE}/by_nuid/#{target_nuid}/preferred_account", { email: email }.to_json)
|
|
135
|
+
AtlasRb::Mash.new(JSON.parse(response.body))["user"]
|
|
136
|
+
end
|
|
91
137
|
end
|
|
92
138
|
end
|
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.
|
|
4
|
+
version: 1.9.0
|
|
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-07-
|
|
11
|
+
date: 2026-07-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|