atlas_rb 1.8.10 → 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/collection.rb +17 -14
- data/lib/atlas_rb/community.rb +2 -2
- data/lib/atlas_rb/configuration.rb +11 -2
- data/lib/atlas_rb/faraday_helper.rb +19 -11
- data/lib/atlas_rb/resource.rb +2 -2
- data/lib/atlas_rb/system/user.rb +24 -15
- data/lib/atlas_rb/user.rb +55 -9
- data/lib/atlas_rb/version.rb +5 -2
- data/lib/atlas_rb/work.rb +2 -2
- 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.
|
data/lib/atlas_rb/collection.rb
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module AtlasRb
|
|
4
|
-
# A grouping of {Work}s
|
|
4
|
+
# A grouping of {Work}s and nested {Collection}s, parented by a {Community}
|
|
5
|
+
# or another Collection.
|
|
5
6
|
#
|
|
6
|
-
# Collections
|
|
7
|
-
#
|
|
8
|
-
#
|
|
7
|
+
# Collections nest, exactly as they did in DRS v1: a Collection holds Works
|
|
8
|
+
# and/or child Collections, and its own parent may be a Community or a
|
|
9
|
+
# Collection (Community → Community|Collection, Collection → Collection|Work,
|
|
10
|
+
# Work → leaf). A consumer that flattens a subtree must recurse into
|
|
11
|
+
# descendant Collections rather than stopping at direct children.
|
|
9
12
|
#
|
|
10
13
|
# See also: {Community}, {Work}.
|
|
11
14
|
class Collection < Resource
|
|
@@ -35,13 +38,13 @@ module AtlasRb
|
|
|
35
38
|
body && AtlasRb::Mash.new(body)["collection"]
|
|
36
39
|
end
|
|
37
40
|
|
|
38
|
-
# Create a new Collection under an existing Community.
|
|
41
|
+
# Create a new Collection under an existing Community or parent Collection.
|
|
39
42
|
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
+
# The `id` parameter is the parent's ID — a **Community or a Collection**,
|
|
44
|
+
# since Collections nest (a Collection may hold child Collections as well as
|
|
45
|
+
# Works).
|
|
43
46
|
#
|
|
44
|
-
# @param id [String] the parent Community ID.
|
|
47
|
+
# @param id [String] the parent Community or Collection ID.
|
|
45
48
|
# @param xml_path [String, nil] optional path to a MODS XML file used to
|
|
46
49
|
# seed metadata. When given, the Collection is created and immediately
|
|
47
50
|
# patched with the metadata in the file.
|
|
@@ -63,7 +66,7 @@ module AtlasRb
|
|
|
63
66
|
result = AtlasRb::Mash.new(JSON.parse(
|
|
64
67
|
connection({ parent_id: id, featured: featured }, nuid, on_behalf_of: on_behalf_of).post(ROUTE)&.body
|
|
65
68
|
))["collection"]
|
|
66
|
-
return result
|
|
69
|
+
return result if xml_path.to_s.empty?
|
|
67
70
|
|
|
68
71
|
update(result["id"], xml_path, nuid: nuid, on_behalf_of: on_behalf_of)
|
|
69
72
|
find(result["id"], nuid: nuid, on_behalf_of: on_behalf_of)
|
|
@@ -90,10 +93,10 @@ module AtlasRb
|
|
|
90
93
|
))["collection"]
|
|
91
94
|
end
|
|
92
95
|
|
|
93
|
-
# Move a Collection to a different parent Community.
|
|
96
|
+
# Move a Collection to a different parent (Community or Collection).
|
|
94
97
|
#
|
|
95
98
|
# Wraps `PATCH /collections/<id>/parent` with a `parent_id` of the new
|
|
96
|
-
#
|
|
99
|
+
# parent. Atlas re-parents the Collection and synchronously cascades
|
|
97
100
|
# the ancestry index over its Works; the structural rules (type, cycle,
|
|
98
101
|
# tombstone guards) are enforced server-side and surface as a `422`.
|
|
99
102
|
#
|
|
@@ -101,7 +104,7 @@ module AtlasRb
|
|
|
101
104
|
# the only difference is the verb and that the Collection already exists.
|
|
102
105
|
#
|
|
103
106
|
# @param id [String] the Collection ID to move.
|
|
104
|
-
# @param new_parent_id [String] the destination Community ID.
|
|
107
|
+
# @param new_parent_id [String] the destination Community or Collection ID.
|
|
105
108
|
# @param nuid [String, nil] optional acting user's NUID. On the relay-signing
|
|
106
109
|
# path it is signed into the assertion `sub`; on the BYO-JWT (`ATLAS_JWT`)
|
|
107
110
|
# path it is ignored (identity lives in the token).
|
|
@@ -274,7 +277,7 @@ module AtlasRb
|
|
|
274
277
|
def self.mods(id, kind = nil, nuid: nil, on_behalf_of: nil)
|
|
275
278
|
# json default, html, xml
|
|
276
279
|
connection({}, nuid, on_behalf_of: on_behalf_of).get(
|
|
277
|
-
ROUTE + id + '/mods' + (kind.
|
|
280
|
+
ROUTE + id + '/mods' + (kind.to_s.empty? ? '' : ".#{kind}")
|
|
278
281
|
)&.body
|
|
279
282
|
end
|
|
280
283
|
end
|
data/lib/atlas_rb/community.rb
CHANGED
|
@@ -64,7 +64,7 @@ module AtlasRb
|
|
|
64
64
|
result = AtlasRb::Mash.new(JSON.parse(
|
|
65
65
|
connection({ parent_id: id }, nuid, on_behalf_of: on_behalf_of).post(ROUTE)&.body
|
|
66
66
|
))["community"]
|
|
67
|
-
return result
|
|
67
|
+
return result if xml_path.to_s.empty?
|
|
68
68
|
|
|
69
69
|
update(result["id"], xml_path, nuid: nuid, on_behalf_of: on_behalf_of)
|
|
70
70
|
find(result["id"], nuid: nuid, on_behalf_of: on_behalf_of)
|
|
@@ -263,7 +263,7 @@ module AtlasRb
|
|
|
263
263
|
def self.mods(id, kind = nil, nuid: nil, on_behalf_of: nil)
|
|
264
264
|
# json default, html, xml
|
|
265
265
|
connection({}, nuid, on_behalf_of: on_behalf_of).get(
|
|
266
|
-
ROUTE + id + '/mods' + (kind.
|
|
266
|
+
ROUTE + id + '/mods' + (kind.to_s.empty? ? '' : ".#{kind}")
|
|
267
267
|
)&.body
|
|
268
268
|
end
|
|
269
269
|
end
|
|
@@ -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/resource.rb
CHANGED
|
@@ -244,7 +244,7 @@ module AtlasRb
|
|
|
244
244
|
# end
|
|
245
245
|
def self.mods(id, kind = nil, nuid: nil, on_behalf_of: nil)
|
|
246
246
|
connection({}, nuid, on_behalf_of: on_behalf_of).get(
|
|
247
|
-
'/resources/' + id + '/mods' + (kind.
|
|
247
|
+
'/resources/' + id + '/mods' + (kind.to_s.empty? ? '' : ".#{kind}")
|
|
248
248
|
)&.body
|
|
249
249
|
end
|
|
250
250
|
|
|
@@ -320,7 +320,7 @@ module AtlasRb
|
|
|
320
320
|
def self.mods_version(id, version_id, kind: nil, nuid: nil, on_behalf_of: nil)
|
|
321
321
|
connection({}, nuid, on_behalf_of: on_behalf_of).get(
|
|
322
322
|
'/resources/' + id + '/mods/versions/' + version_id +
|
|
323
|
-
(kind.
|
|
323
|
+
(kind.to_s.empty? ? '' : ".#{kind}")
|
|
324
324
|
)&.body
|
|
325
325
|
end
|
|
326
326
|
|
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
|
data/lib/atlas_rb/version.rb
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module AtlasRb
|
|
4
|
-
# Current gem version, read from the `.version` file at the
|
|
5
|
-
|
|
4
|
+
# Current gem version, read from the `.version` file at the gem root at load
|
|
5
|
+
# time. Resolved relative to this file (not the process CWD) so a standalone
|
|
6
|
+
# consumer — a headless BYO-JWT script run from anywhere — loads the gem's own
|
|
7
|
+
# version rather than raising ENOENT (or reading some unrelated `.version`).
|
|
8
|
+
VERSION = File.read(File.expand_path("../../.version", __dir__)).strip
|
|
6
9
|
end
|
data/lib/atlas_rb/work.rb
CHANGED
|
@@ -124,7 +124,7 @@ module AtlasRb
|
|
|
124
124
|
connection(params, nuid,
|
|
125
125
|
on_behalf_of: on_behalf_of, idempotency_key: idempotency_key).post(ROUTE)&.body
|
|
126
126
|
))["work"]
|
|
127
|
-
return result
|
|
127
|
+
return result if xml_path.to_s.empty?
|
|
128
128
|
|
|
129
129
|
update(result["id"], xml_path, nuid: nuid, on_behalf_of: on_behalf_of)
|
|
130
130
|
find(result["id"], nuid: nuid, on_behalf_of: on_behalf_of)
|
|
@@ -559,7 +559,7 @@ module AtlasRb
|
|
|
559
559
|
def self.mods(id, kind = nil, nuid: nil, on_behalf_of: nil)
|
|
560
560
|
# json default, html, xml
|
|
561
561
|
connection({}, nuid, on_behalf_of: on_behalf_of).get(
|
|
562
|
-
ROUTE + id + '/mods' + (kind.
|
|
562
|
+
ROUTE + id + '/mods' + (kind.to_s.empty? ? '' : ".#{kind}")
|
|
563
563
|
)&.body
|
|
564
564
|
end
|
|
565
565
|
|
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
|