atlas_rb 1.8.2 → 1.8.4
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/CHANGELOG.md +63 -0
- data/Gemfile.lock +1 -1
- data/lib/atlas_rb/blob.rb +7 -5
- data/lib/atlas_rb/collection.rb +6 -5
- data/lib/atlas_rb/community.rb +6 -5
- data/lib/atlas_rb/compilation.rb +6 -5
- data/lib/atlas_rb/delegate.rb +7 -5
- data/lib/atlas_rb/errors.rb +76 -0
- data/lib/atlas_rb/file_set.rb +6 -4
- data/lib/atlas_rb/middleware/raise_on_resource_error.rb +15 -5
- data/lib/atlas_rb/person.rb +7 -5
- data/lib/atlas_rb/resource.rb +42 -4
- data/lib/atlas_rb/work.rb +53 -5
- 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: a319cfd0891ed913d8c08f8138d7e11cc12a3f13d79d1b41d9eab95a45e67824
|
|
4
|
+
data.tar.gz: a840bfac145c02f79f59b53e226d293d90a89a18466511d4b9dbb9bd90460f59
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ba9b973ba96cbf0f132617bafe8d0475c0bba7b3f76328fbd54adfa20175c96f97fab5e6c4a66a025ea92890d3d2d90f00616674d143a7442dc10da763052013
|
|
7
|
+
data.tar.gz: 0cc2ce817a804eb2925963f8aae21108bd4151e85cf437c0220574c067025a37544212dc298b2a97b197a063ac258ff8d759ee140f957e3e9ba10f7e3f6a15ce
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.8.
|
|
1
|
+
1.8.4
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,68 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.8.4
|
|
4
|
+
|
|
5
|
+
### Added — per-tier derivative-visibility policy (`Work.set_derivative_permissions`)
|
|
6
|
+
|
|
7
|
+
New binding for the departmental "each download rendition has its own permissions"
|
|
8
|
+
model: `AtlasRb::Work.set_derivative_permissions(id, policy:, …)` PATCHes a per-tier
|
|
9
|
+
read policy to `PATCH /works/:id/derivative_permissions`. `policy` is a map of tier
|
|
10
|
+
(`small` / `medium` / `large` / `service`) => array of read-group tokens (the resource
|
|
11
|
+
vocabulary: `"public"`, Grouper group names, `[]` = private). Whole-object replace;
|
|
12
|
+
omitted tiers inherit by cascade (an absent tier inherits the next lower-resolution
|
|
13
|
+
tier; `small` inherits the Work). Atlas enforces two invariants — a tier may not be
|
|
14
|
+
more visible than the Work, and visibility must narrow as resolution grows
|
|
15
|
+
(`service` ⊆ `large` ⊆ `medium` ⊆ `small`).
|
|
16
|
+
|
|
17
|
+
The effective per-Delegate gate surfaces on `Work.assets` entries as `gated` (must be
|
|
18
|
+
authorized, not linked directly) and `permission` (the effective read-group set;
|
|
19
|
+
`null` for guests). No `assets` change is needed — the new fields flow through the
|
|
20
|
+
existing polymorphic Mash. The gate is advisory: Cerberus and the IIIF auth layer
|
|
21
|
+
enforce it.
|
|
22
|
+
|
|
23
|
+
A `422` rejection (`tier_exceeds_resource` / `tier_ordering_violation` /
|
|
24
|
+
`unknown_tier`) now raises the typed `AtlasRb::DerivativePermissionsError` (with `code`
|
|
25
|
+
/ `resource_id`) via `RaiseOnResourceError`, mirroring the re-parent / linked-member /
|
|
26
|
+
Compilation mappings; a `403` raises `AtlasRb::ForbiddenError`; a `409` raises
|
|
27
|
+
`AtlasRb::StaleResourceError`.
|
|
28
|
+
|
|
29
|
+
## 1.8.3
|
|
30
|
+
|
|
31
|
+
### Changed — `find` is status-aware (stops swallowing Atlas error envelopes)
|
|
32
|
+
|
|
33
|
+
Every typed single-resource reader — `Resource.find` (the polymorphic resolver)
|
|
34
|
+
and the `Work` / `Collection` / `Community` / `FileSet` / `Person` /
|
|
35
|
+
`Compilation` / `Blob` / `Delegate` overrides — now routes through a shared
|
|
36
|
+
status-aware read path instead of blindly `JSON.parse`-ing the body and
|
|
37
|
+
unwrapping a fixed key.
|
|
38
|
+
|
|
39
|
+
Previously, any non-2xx that wasn't a re-parent/linked-member/Compilation/upload
|
|
40
|
+
signal (which `RaiseOnResourceError` already translates) passed straight through:
|
|
41
|
+
Atlas renders its auth/validation failures as a JSON envelope (`{ "error" => …}`,
|
|
42
|
+
status 400/401/403/422), so `JSON.parse(body)["work"]` found no `"work"` key and
|
|
43
|
+
returned **`nil`**. Callers then dereferenced that `nil` far from the cause — the
|
|
44
|
+
canonical symptom being `undefined method 'tombstoned' for nil`, a 500 surfaced
|
|
45
|
+
nowhere near the request that actually failed.
|
|
46
|
+
|
|
47
|
+
Now:
|
|
48
|
+
|
|
49
|
+
- **`404` → `nil`** — a clean "not found" (and no more `JSON::ParserError` on the
|
|
50
|
+
empty `head :not_found` body, which is what a missing id used to raise).
|
|
51
|
+
- **any other non-2xx → `AtlasRb::ResourceError`** — a new typed error carrying
|
|
52
|
+
Atlas's `status`, `body`, and `response`, raised **at the boundary** so the
|
|
53
|
+
real cause (e.g. `GET /works/abc → 401: {"error":"invalid bearer token"}`) is
|
|
54
|
+
attributable everywhere `find` is used.
|
|
55
|
+
- **`2xx`** → unchanged (the unwrapped resource).
|
|
56
|
+
|
|
57
|
+
Authorization failures on the narrow re-parent / linked-member / Compilation
|
|
58
|
+
write paths still surface as `AtlasRb::ForbiddenError` via
|
|
59
|
+
`RaiseOnResourceError`; `ResourceError` is the catch-all for the read path that
|
|
60
|
+
middleware intentionally does not cover.
|
|
61
|
+
|
|
62
|
+
**Behavior change to note:** `find` of a missing id now returns `nil` rather than
|
|
63
|
+
raising `JSON::ParserError`. Callers that relied on the parse error (none known)
|
|
64
|
+
should nil-check instead.
|
|
65
|
+
|
|
3
66
|
## 1.8.0
|
|
4
67
|
|
|
5
68
|
### Added — `Work.set_full_text` (full-text search seam)
|
data/Gemfile.lock
CHANGED
data/lib/atlas_rb/blob.rb
CHANGED
|
@@ -23,20 +23,22 @@ module AtlasRb
|
|
|
23
23
|
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
24
24
|
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
25
25
|
# omitted.
|
|
26
|
-
# @return [Hash] the `"blob"` object, already unwrapped — typically
|
|
26
|
+
# @return [Hash, nil] the `"blob"` object, already unwrapped — typically
|
|
27
27
|
# includes `"id"`, `"original_filename"`, `"size"`, `"digest"` (the
|
|
28
28
|
# recorded fixity digest `"sha512:<hex>"`, or `nil` for a Blob with no
|
|
29
29
|
# held bytes — reconciliation compares this against the v1 manifest
|
|
30
|
-
# without re-downloading), and a download URL
|
|
30
|
+
# without re-downloading), and a download URL — or `nil` when the Blob
|
|
31
|
+
# does not exist (`404`).
|
|
32
|
+
# @raise [AtlasRb::ResourceError] on any non-2xx other than `404` (e.g. an
|
|
33
|
+
# auth/validation error envelope), carrying Atlas's status + body.
|
|
31
34
|
#
|
|
32
35
|
# @example
|
|
33
36
|
# AtlasRb::Blob.find("b-321")
|
|
34
37
|
# # => { "id" => "b-321", "original_filename" => "scan.pdf",
|
|
35
38
|
# # "digest" => "sha512:9f86d0…", ... }
|
|
36
39
|
def self.find(id, nuid: nil, on_behalf_of: nil)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
))['blob']
|
|
40
|
+
body = fetch_resource(ROUTE + id, nuid: nuid, on_behalf_of: on_behalf_of)
|
|
41
|
+
body && AtlasRb::Mash.new(body)['blob']
|
|
40
42
|
end
|
|
41
43
|
|
|
42
44
|
# Resolve a content Blob to its parent FileSet and containing Work noids.
|
data/lib/atlas_rb/collection.rb
CHANGED
|
@@ -22,16 +22,17 @@ module AtlasRb
|
|
|
22
22
|
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
23
23
|
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
24
24
|
# omitted.
|
|
25
|
-
# @return [Hash] the `"collection"` object, already unwrapped from the
|
|
26
|
-
# JSON response.
|
|
25
|
+
# @return [Hash, nil] the `"collection"` object, already unwrapped from the
|
|
26
|
+
# JSON response, or `nil` when the Collection does not exist (`404`).
|
|
27
|
+
# @raise [AtlasRb::ResourceError] on any non-2xx other than `404` (e.g. an
|
|
28
|
+
# auth/validation error envelope), carrying Atlas's status + body.
|
|
27
29
|
#
|
|
28
30
|
# @example
|
|
29
31
|
# AtlasRb::Collection.find("col-456")
|
|
30
32
|
# # => { "id" => "col-456", "title" => "Faculty Publications", ... }
|
|
31
33
|
def self.find(id, nuid: nil, on_behalf_of: nil)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
))["collection"]
|
|
34
|
+
body = fetch_resource(ROUTE + id, nuid: nuid, on_behalf_of: on_behalf_of)
|
|
35
|
+
body && AtlasRb::Mash.new(body)["collection"]
|
|
35
36
|
end
|
|
36
37
|
|
|
37
38
|
# Create a new Collection under an existing Community.
|
data/lib/atlas_rb/community.rb
CHANGED
|
@@ -23,16 +23,17 @@ module AtlasRb
|
|
|
23
23
|
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
24
24
|
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
25
25
|
# omitted.
|
|
26
|
-
# @return [Hash] the `"community"` object from the JSON response,
|
|
27
|
-
# already unwrapped.
|
|
26
|
+
# @return [Hash, nil] the `"community"` object from the JSON response,
|
|
27
|
+
# already unwrapped, or `nil` when the Community does not exist (`404`).
|
|
28
|
+
# @raise [AtlasRb::ResourceError] on any non-2xx other than `404` (e.g. an
|
|
29
|
+
# auth/validation error envelope), carrying Atlas's status + body.
|
|
28
30
|
#
|
|
29
31
|
# @example
|
|
30
32
|
# AtlasRb::Community.find("c-123")
|
|
31
33
|
# # => { "id" => "c-123", "title" => "College of Engineering", ... }
|
|
32
34
|
def self.find(id, nuid: nil, on_behalf_of: nil)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
))["community"]
|
|
35
|
+
body = fetch_resource(ROUTE + id, nuid: nuid, on_behalf_of: on_behalf_of)
|
|
36
|
+
body && AtlasRb::Mash.new(body)["community"]
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
# Create a new Community, optionally seeded with MODS metadata.
|
data/lib/atlas_rb/compilation.rb
CHANGED
|
@@ -38,19 +38,20 @@ module AtlasRb
|
|
|
38
38
|
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
39
39
|
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
40
40
|
# omitted.
|
|
41
|
-
# @return [Hash] the `"compilation"` object, already unwrapped — `id`,
|
|
41
|
+
# @return [Hash, nil] the `"compilation"` object, already unwrapped — `id`,
|
|
42
42
|
# `title`, `description`, `depositor`, the three recipe arrays
|
|
43
43
|
# (`included_collections`, `included_works`, `excluded_works`), the
|
|
44
|
-
# ACL arrays, and timestamps.
|
|
44
|
+
# ACL arrays, and timestamps — or `nil` when the Set does not exist (`404`).
|
|
45
45
|
# @raise [AtlasRb::ForbiddenError] if the caller may not read this Set.
|
|
46
|
+
# @raise [AtlasRb::ResourceError] on any other non-2xx (e.g. `401`),
|
|
47
|
+
# carrying Atlas's status + body.
|
|
46
48
|
#
|
|
47
49
|
# @example
|
|
48
50
|
# AtlasRb::Compilation.find("c-123", nuid: "000000002")
|
|
49
51
|
# # => { "id" => "c-123", "title" => "Course readings", ... }
|
|
50
52
|
def self.find(id, nuid: nil, on_behalf_of: nil)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
))["compilation"]
|
|
53
|
+
body = fetch_resource(ROUTE + id, nuid: nuid, on_behalf_of: on_behalf_of)
|
|
54
|
+
body && AtlasRb::Mash.new(body)["compilation"]
|
|
54
55
|
end
|
|
55
56
|
|
|
56
57
|
# List Compilations, paginated (newest first), in one of three modes.
|
data/lib/atlas_rb/delegate.rb
CHANGED
|
@@ -29,16 +29,18 @@ module AtlasRb
|
|
|
29
29
|
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
30
30
|
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
31
31
|
# omitted.
|
|
32
|
-
# @return [AtlasRb::Mash] the `"delegate"` object, already unwrapped —
|
|
32
|
+
# @return [AtlasRb::Mash, nil] the `"delegate"` object, already unwrapped —
|
|
33
33
|
# includes `id`, `valkyrie_id`, `use`, `uri`, `mime_type`,
|
|
34
|
-
# `original_filename`, `label`, and tombstone fields
|
|
34
|
+
# `original_filename`, `label`, and tombstone fields — or `nil` when the
|
|
35
|
+
# Delegate does not exist (`404`).
|
|
36
|
+
# @raise [AtlasRb::ResourceError] on any non-2xx other than `404` (e.g. an
|
|
37
|
+
# auth/validation error envelope), carrying Atlas's status + body.
|
|
35
38
|
#
|
|
36
39
|
# @example
|
|
37
40
|
# AtlasRb::Delegate.find("d-555")
|
|
38
41
|
def self.find(id, nuid: nil, on_behalf_of: nil)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
))["delegate"]
|
|
42
|
+
body = fetch_resource(ROUTE + id, nuid: nuid, on_behalf_of: on_behalf_of)
|
|
43
|
+
body && AtlasRb::Mash.new(body)["delegate"]
|
|
42
44
|
end
|
|
43
45
|
end
|
|
44
46
|
end
|
data/lib/atlas_rb/errors.rb
CHANGED
|
@@ -169,6 +169,38 @@ module AtlasRb
|
|
|
169
169
|
end
|
|
170
170
|
end
|
|
171
171
|
|
|
172
|
+
# Raised when Atlas rejects a per-tier derivative-visibility policy
|
|
173
|
+
# (`PATCH /works/:id/derivative_permissions`) with a `422` carrying a
|
|
174
|
+
# machine-readable `error` discriminator — `tier_exceeds_resource` (a tier
|
|
175
|
+
# more visible than its Work), `tier_ordering_violation` (visibility not
|
|
176
|
+
# narrowing as resolution grows), or `unknown_tier`.
|
|
177
|
+
#
|
|
178
|
+
# The derivative-permissions sibling of {ReparentError} / {LinkedMemberError};
|
|
179
|
+
# same shape, same rationale (the binding's `["work"]` unwrap would otherwise
|
|
180
|
+
# discard the envelope on the 422). Atlas rejects before persisting.
|
|
181
|
+
#
|
|
182
|
+
# rescue AtlasRb::DerivativePermissionsError => e
|
|
183
|
+
# flash.now[:alert] = t("derivative_permissions.errors.#{e.code}", default: e.message)
|
|
184
|
+
#
|
|
185
|
+
# @note Authorization failures surface as {ForbiddenError} (HTTP 403).
|
|
186
|
+
class DerivativePermissionsError < Error
|
|
187
|
+
# @return [String, nil] the machine-readable error code from the envelope,
|
|
188
|
+
# suitable for keying an i18n map.
|
|
189
|
+
attr_reader :code
|
|
190
|
+
|
|
191
|
+
# @return [String, nil] the rejected Work's ID, from the envelope.
|
|
192
|
+
attr_reader :resource_id
|
|
193
|
+
|
|
194
|
+
# @param message [String] human-readable rejection description.
|
|
195
|
+
# @param code [String, nil] the envelope's `error` discriminator.
|
|
196
|
+
# @param resource_id [String, nil] the rejected Work's ID.
|
|
197
|
+
def initialize(message, code: nil, resource_id: nil)
|
|
198
|
+
super(message)
|
|
199
|
+
@code = code
|
|
200
|
+
@resource_id = resource_id
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
172
204
|
# Raised when Atlas refuses a re-parent, linked-member, or Compilation
|
|
173
205
|
# request with an HTTP `403`, whose envelope is
|
|
174
206
|
# `{ "error", "action", "subject" }`. Lets callers distinguish "you may
|
|
@@ -200,6 +232,50 @@ module AtlasRb
|
|
|
200
232
|
end
|
|
201
233
|
end
|
|
202
234
|
|
|
235
|
+
# Raised by the typed single-resource readers ({Resource.find} and the
|
|
236
|
+
# `Work` / `Collection` / `Community` / `FileSet` / `Person` / `Compilation`
|
|
237
|
+
# / `Blob` / `Delegate` overrides) when Atlas answers the `GET` with a
|
|
238
|
+
# non-2xx that is **not** a `404` — i.e. an error envelope
|
|
239
|
+
# (`{ "error" => ... }`, status 400/401/403/422) on what the caller treated
|
|
240
|
+
# as a plain read.
|
|
241
|
+
#
|
|
242
|
+
# Before this existed, `find` unwrapped the success body by a fixed key
|
|
243
|
+
# (`["work"]`, `["collection"]`, …); on an error envelope that key is
|
|
244
|
+
# absent, so `find` returned `nil` and silently discarded Atlas's status and
|
|
245
|
+
# message. The caller then dereferenced the `nil` far from the cause (the
|
|
246
|
+
# canonical symptom: `undefined method 'tombstoned' for nil`). This error
|
|
247
|
+
# keeps the failure **at the boundary**, carrying the status and body so the
|
|
248
|
+
# real cause (e.g. `… → 401: {"error":"invalid bearer token"}`) is
|
|
249
|
+
# attributable everywhere `find` is used.
|
|
250
|
+
#
|
|
251
|
+
# A genuine `404` is **not** this — it stays a clean `nil` return, since
|
|
252
|
+
# "not found" is a normal `find` outcome callers already nil-check.
|
|
253
|
+
#
|
|
254
|
+
# @note Authorization failures on the narrow re-parent / linked-member /
|
|
255
|
+
# Compilation write paths surface as {ForbiddenError} via
|
|
256
|
+
# {Middleware::RaiseOnResourceError}; this is the catch-all for the read
|
|
257
|
+
# path, which that middleware intentionally does not cover.
|
|
258
|
+
class ResourceError < Error
|
|
259
|
+
# @return [Faraday::Response, nil] the originating response, when available.
|
|
260
|
+
attr_reader :response
|
|
261
|
+
|
|
262
|
+
# @return [Integer, nil] Atlas's HTTP status.
|
|
263
|
+
attr_reader :status
|
|
264
|
+
|
|
265
|
+
# @return [String, nil] Atlas's raw response body (the error envelope).
|
|
266
|
+
attr_reader :body
|
|
267
|
+
|
|
268
|
+
# @param message [String] human-readable failure description.
|
|
269
|
+
# @param response [Faraday::Response, nil] the originating response; its
|
|
270
|
+
# status and body are captured for callers that rescue this.
|
|
271
|
+
def initialize(message, response: nil)
|
|
272
|
+
super(message)
|
|
273
|
+
@response = response
|
|
274
|
+
@status = response&.status
|
|
275
|
+
@body = response&.body
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
203
279
|
# Raised when the transport has no way to authenticate a relay request:
|
|
204
280
|
# neither `ATLAS_JWT` (BYO-JWT mode) nor a signing key
|
|
205
281
|
# ({AtlasRb.config#assertion_signing_key}, relay-signing mode) is configured.
|
data/lib/atlas_rb/file_set.rb
CHANGED
|
@@ -23,14 +23,16 @@ module AtlasRb
|
|
|
23
23
|
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
24
24
|
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
25
25
|
# omitted.
|
|
26
|
-
# @return [Hash] the `"file_set"` object, already unwrapped
|
|
26
|
+
# @return [Hash, nil] the `"file_set"` object, already unwrapped, or `nil`
|
|
27
|
+
# when the FileSet does not exist (`404`).
|
|
28
|
+
# @raise [AtlasRb::ResourceError] on any non-2xx other than `404` (e.g. an
|
|
29
|
+
# auth/validation error envelope), carrying Atlas's status + body.
|
|
27
30
|
#
|
|
28
31
|
# @example
|
|
29
32
|
# AtlasRb::FileSet.find("fs-001")
|
|
30
33
|
def self.find(id, nuid: nil, on_behalf_of: nil)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
))["file_set"]
|
|
34
|
+
body = fetch_resource(ROUTE + id, nuid: nuid, on_behalf_of: on_behalf_of)
|
|
35
|
+
body && AtlasRb::Mash.new(body)["file_set"]
|
|
34
36
|
end
|
|
35
37
|
|
|
36
38
|
# Create a new FileSet under a Work.
|
|
@@ -16,8 +16,9 @@ module AtlasRb
|
|
|
16
16
|
#
|
|
17
17
|
# It is intentionally narrow — it only fires on the re-parent
|
|
18
18
|
# (`.../parent`) and linked-member (`.../linked_members...`) write paths,
|
|
19
|
-
# the Compilation surface (`/compilations...`),
|
|
20
|
-
# (
|
|
19
|
+
# the Compilation surface (`/compilations...`), the derivative-permissions
|
|
20
|
+
# write (`.../derivative_permissions`), and binary uploads (`/files...`,
|
|
21
|
+
# `/file_sets...`), and only on `403` / `422` bodies carrying
|
|
21
22
|
# an `error` discriminator. The upload branch is further gated on a fixity
|
|
22
23
|
# discriminator ({FIXITY_CODES}), so a `422` on those paths with any other
|
|
23
24
|
# `error` (or `403`s on uploads, which stay raw) passes through untouched.
|
|
@@ -27,10 +28,11 @@ module AtlasRb
|
|
|
27
28
|
# translates only the wire signals callers genuinely need to discriminate.
|
|
28
29
|
#
|
|
29
30
|
# Mapping:
|
|
30
|
-
# - `403` on a re-parent/linked/Compilation path → {AtlasRb::ForbiddenError}
|
|
31
|
+
# - `403` on a re-parent/linked/Compilation/derivative-permissions path → {AtlasRb::ForbiddenError}
|
|
31
32
|
# - `422` on `.../parent` → {AtlasRb::ReparentError} (`error`/`resource_id`)
|
|
32
33
|
# - `422` on `.../linked_members...` → {AtlasRb::LinkedMemberError}
|
|
33
34
|
# - `422` on `/compilations...` → {AtlasRb::CompilationError}
|
|
35
|
+
# - `422` on `.../derivative_permissions` → {AtlasRb::DerivativePermissionsError}
|
|
34
36
|
# - `422` + a fixity discriminator on `/files...` / `/file_sets...` →
|
|
35
37
|
# {AtlasRb::FixityMismatchError}
|
|
36
38
|
class RaiseOnResourceError < Faraday::Middleware
|
|
@@ -43,6 +45,7 @@ module AtlasRb
|
|
|
43
45
|
# @raise [AtlasRb::ReparentError] on a 422 to a re-parent path.
|
|
44
46
|
# @raise [AtlasRb::LinkedMemberError] on a 422 to a linked-member path.
|
|
45
47
|
# @raise [AtlasRb::CompilationError] on a 422 to a Compilation path.
|
|
48
|
+
# @raise [AtlasRb::DerivativePermissionsError] on a 422 to a derivative-permissions path.
|
|
46
49
|
# @raise [AtlasRb::FixityMismatchError] on a 422 + fixity discriminator to an upload path.
|
|
47
50
|
# @return [void]
|
|
48
51
|
def on_complete(env)
|
|
@@ -52,15 +55,16 @@ module AtlasRb
|
|
|
52
55
|
reparent = path.end_with?("/parent")
|
|
53
56
|
linked = path.include?("/linked_members")
|
|
54
57
|
compilation = path.start_with?("/compilations")
|
|
58
|
+
deriv_perms = path.end_with?("/derivative_permissions")
|
|
55
59
|
upload = path.start_with?("/files") || path.start_with?("/file_sets")
|
|
56
|
-
return unless reparent || linked || compilation || upload
|
|
60
|
+
return unless reparent || linked || compilation || deriv_perms || upload
|
|
57
61
|
|
|
58
62
|
body = parse_json(env.body)
|
|
59
63
|
return unless body.is_a?(Hash) && body["error"]
|
|
60
64
|
|
|
61
65
|
if env.status == 403
|
|
62
66
|
# 403s on upload paths stay raw — acting-as/authz isn't an upload concern here.
|
|
63
|
-
return unless reparent || linked || compilation
|
|
67
|
+
return unless reparent || linked || compilation || deriv_perms
|
|
64
68
|
|
|
65
69
|
raise AtlasRb::ForbiddenError.new(
|
|
66
70
|
body["message"] || "Atlas refused the request",
|
|
@@ -86,6 +90,12 @@ module AtlasRb
|
|
|
86
90
|
code: body["error"],
|
|
87
91
|
resource_id: body["resource_id"]
|
|
88
92
|
)
|
|
93
|
+
elsif deriv_perms
|
|
94
|
+
raise AtlasRb::DerivativePermissionsError.new(
|
|
95
|
+
body["message"] || "Atlas rejected the derivative-permissions policy",
|
|
96
|
+
code: body["error"],
|
|
97
|
+
resource_id: body["resource_id"]
|
|
98
|
+
)
|
|
89
99
|
elsif FIXITY_CODES.include?(body["error"])
|
|
90
100
|
raise AtlasRb::FixityMismatchError.new(
|
|
91
101
|
body["message"] || "Atlas rejected the upload (fixity)",
|
data/lib/atlas_rb/person.rb
CHANGED
|
@@ -33,13 +33,15 @@ module AtlasRb
|
|
|
33
33
|
# @param id [String] the person's NOID.
|
|
34
34
|
# @param nuid [String, nil] acting principal (signed into the assertion sub).
|
|
35
35
|
# @param on_behalf_of [String, nil] acting-as target.
|
|
36
|
-
# @return [AtlasRb::Mash] the unwrapped `"person"` object (carries the
|
|
36
|
+
# @return [AtlasRb::Mash, nil] the unwrapped `"person"` object (carries the
|
|
37
37
|
# server-side `nuid` for callers that need it, e.g. depositor gating, and
|
|
38
|
-
# `personal_root_id` for the publish-conduit parent)
|
|
38
|
+
# `personal_root_id` for the publish-conduit parent), or `nil` when the
|
|
39
|
+
# Person does not exist (`404`).
|
|
40
|
+
# @raise [AtlasRb::ResourceError] on any non-2xx other than `404` (e.g. an
|
|
41
|
+
# auth/validation error envelope), carrying Atlas's status + body.
|
|
39
42
|
def self.find(id, nuid: nil, on_behalf_of: nil)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
))["person"]
|
|
43
|
+
body = fetch_resource(ROUTE + id, nuid: nuid, on_behalf_of: on_behalf_of)
|
|
44
|
+
body && AtlasRb::Mash.new(body)["person"]
|
|
43
45
|
end
|
|
44
46
|
|
|
45
47
|
# List people — the NOID-keyed People-index source. Returns the page's
|
data/lib/atlas_rb/resource.rb
CHANGED
|
@@ -32,17 +32,20 @@ module AtlasRb
|
|
|
32
32
|
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
33
33
|
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
34
34
|
# omitted.
|
|
35
|
-
# @return [Hash{String => String, Hash}] hash with two keys
|
|
35
|
+
# @return [Hash{String => String, Hash}, nil] hash with two keys, or `nil`
|
|
36
|
+
# when the id resolves to nothing (`404`):
|
|
36
37
|
# - `"klass"` — the resource type, capitalized (e.g. `"Work"`).
|
|
37
38
|
# - `"resource"` — the resource payload as a Hash.
|
|
39
|
+
# @raise [AtlasRb::ResourceError] on any non-2xx other than `404` (e.g. an
|
|
40
|
+
# auth/validation error envelope), carrying Atlas's status + body.
|
|
38
41
|
#
|
|
39
42
|
# @example Polymorphic lookup
|
|
40
43
|
# AtlasRb::Resource.find("abc123")
|
|
41
44
|
# # => { "klass" => "Work", "resource" => { "id" => "abc123", "title" => "..." } }
|
|
42
45
|
def self.find(id, nuid: nil, on_behalf_of: nil)
|
|
43
|
-
result =
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
result = fetch_resource('/resources/' + id, nuid: nuid, on_behalf_of: on_behalf_of)
|
|
47
|
+
return nil if result.nil?
|
|
48
|
+
|
|
46
49
|
AtlasRb::Mash.new("klass" => result.first[0].capitalize,
|
|
47
50
|
"resource" => result.first[1])
|
|
48
51
|
end
|
|
@@ -242,5 +245,40 @@ module AtlasRb
|
|
|
242
245
|
(kind.present? ? ".#{kind}" : '')
|
|
243
246
|
)&.body
|
|
244
247
|
end
|
|
248
|
+
|
|
249
|
+
# Shared read path behind every typed `find`: perform a single-resource
|
|
250
|
+
# `GET` and return the parsed JSON envelope, or `nil` when Atlas reports
|
|
251
|
+
# the resource is absent (`404`).
|
|
252
|
+
#
|
|
253
|
+
# Its job is to stop `find` from silently coercing an error *envelope*
|
|
254
|
+
# into `nil`. Atlas renders auth/validation failures as a JSON body
|
|
255
|
+
# (`{ "error" => ... }`, status 400/401/403/422); a naive
|
|
256
|
+
# `JSON.parse(body)["work"]` returns `nil` for the missing `"work"` key —
|
|
257
|
+
# losing the status and message, and surfacing as a baffling
|
|
258
|
+
# `NoMethodError` far from the cause. Mapping:
|
|
259
|
+
#
|
|
260
|
+
# - `404` → `nil` (clean "not found"; also avoids the
|
|
261
|
+
# `JSON::ParserError` the old code raised on the empty `head :not_found`
|
|
262
|
+
# body).
|
|
263
|
+
# - any other non-2xx → {AtlasRb::ResourceError} carrying Atlas's status +
|
|
264
|
+
# body, so the failure is attributable at the boundary.
|
|
265
|
+
# - `2xx` → the parsed JSON Hash, for the caller to unwrap.
|
|
266
|
+
#
|
|
267
|
+
# @param path [String] the resource path to GET (e.g. `"/works/abc123"`).
|
|
268
|
+
# @param nuid [String, nil] optional acting user's NUID (see {find}).
|
|
269
|
+
# @param on_behalf_of [String, nil] optional `On-Behalf-Of` NUID (see {find}).
|
|
270
|
+
# @return [Hash, nil] the parsed JSON envelope, or `nil` on `404`.
|
|
271
|
+
# @raise [AtlasRb::ResourceError] on any non-2xx other than `404`.
|
|
272
|
+
# @api private
|
|
273
|
+
def self.fetch_resource(path, nuid: nil, on_behalf_of: nil)
|
|
274
|
+
resp = connection({}, nuid, on_behalf_of: on_behalf_of).get(path)
|
|
275
|
+
return nil if resp.status == 404
|
|
276
|
+
unless resp.success?
|
|
277
|
+
raise AtlasRb::ResourceError.new("GET #{path} → #{resp.status}: #{resp.body}", response: resp)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
JSON.parse(resp.body)
|
|
281
|
+
end
|
|
282
|
+
private_class_method :fetch_resource
|
|
245
283
|
end
|
|
246
284
|
end
|
data/lib/atlas_rb/work.rb
CHANGED
|
@@ -22,16 +22,17 @@ module AtlasRb
|
|
|
22
22
|
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
23
23
|
# header (acting-as / view-as). Falls through to
|
|
24
24
|
# {AtlasRb.config}.default_on_behalf_of when omitted.
|
|
25
|
-
# @return [Hash] the `"work"` object, already unwrapped from the JSON
|
|
26
|
-
# response.
|
|
25
|
+
# @return [Hash, nil] the `"work"` object, already unwrapped from the JSON
|
|
26
|
+
# response, or `nil` when the Work does not exist (`404`).
|
|
27
|
+
# @raise [AtlasRb::ResourceError] on any non-2xx other than `404` (e.g. an
|
|
28
|
+
# auth/validation error envelope), carrying Atlas's status + body.
|
|
27
29
|
#
|
|
28
30
|
# @example
|
|
29
31
|
# AtlasRb::Work.find("w-789")
|
|
30
32
|
# # => { "id" => "w-789", "title" => "An Article", ... }
|
|
31
33
|
def self.find(id, nuid: nil, on_behalf_of: nil)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
))["work"]
|
|
34
|
+
body = fetch_resource(ROUTE + id, nuid: nuid, on_behalf_of: on_behalf_of)
|
|
35
|
+
body && AtlasRb::Mash.new(body)["work"]
|
|
35
36
|
end
|
|
36
37
|
|
|
37
38
|
# List Works, paginated.
|
|
@@ -344,6 +345,53 @@ module AtlasRb
|
|
|
344
345
|
))
|
|
345
346
|
end
|
|
346
347
|
|
|
348
|
+
# Replace a Work's per-tier derivative-visibility policy.
|
|
349
|
+
#
|
|
350
|
+
# Sets which read groups may fetch the Work's sized image derivatives —
|
|
351
|
+
# the `small` / `medium` / `large` / `service` (deep-zoom) tiers — reusing
|
|
352
|
+
# the resource read-group vocabulary (`"public"`, Grouper group names,
|
|
353
|
+
# `[]` = private). Unlike {.set_image_derivatives} (which upserts URIs) this
|
|
354
|
+
# is a whole-object REPLACE: the map you pass is the complete policy;
|
|
355
|
+
# omitted tiers inherit by cascade (an absent tier inherits the next
|
|
356
|
+
# lower-resolution tier; `small` inherits the Work's own visibility). Pass a
|
|
357
|
+
# tier as `[]` to make it private.
|
|
358
|
+
#
|
|
359
|
+
# Atlas enforces two invariants: a tier may not be more visible than the
|
|
360
|
+
# Work, and visibility must narrow as resolution grows
|
|
361
|
+
# (`service` ⊆ `large` ⊆ `medium` ⊆ `small`). The gate is advisory — it
|
|
362
|
+
# surfaces per Delegate on {.assets} as `gated` / `permission` for the
|
|
363
|
+
# display layer (Cerberus / the IIIF auth service) to enforce.
|
|
364
|
+
#
|
|
365
|
+
# @param id [String] the Work ID.
|
|
366
|
+
# @param policy [Hash] tier => Array(read groups), e.g.
|
|
367
|
+
# `{ large: ["northeastern:drs:repository:archives"], service: [...] }`.
|
|
368
|
+
# Keys may be strings or symbols; only `small` / `medium` / `large` /
|
|
369
|
+
# `service` are recognized.
|
|
370
|
+
# @param nuid [String, nil] optional acting user's NUID. On the relay-signing
|
|
371
|
+
# path it is signed into the assertion `sub`; on the BYO-JWT (`ATLAS_JWT`)
|
|
372
|
+
# path it is ignored (identity lives in the token).
|
|
373
|
+
# @param on_behalf_of [String, nil] optional On-Behalf-Of NUID.
|
|
374
|
+
# @return [AtlasRb::Mash] the updated Work; the stored map echoes back under
|
|
375
|
+
# `derivative_permissions`.
|
|
376
|
+
# @raise [AtlasRb::DerivativePermissionsError] if Atlas rejects the policy
|
|
377
|
+
# (422) — `tier_exceeds_resource` / `tier_ordering_violation` /
|
|
378
|
+
# `unknown_tier` (see {#code}).
|
|
379
|
+
# @raise [AtlasRb::StaleResourceError] on an optimistic-lock conflict (409).
|
|
380
|
+
#
|
|
381
|
+
# @example
|
|
382
|
+
# AtlasRb::Work.set_derivative_permissions(
|
|
383
|
+
# "w-789",
|
|
384
|
+
# policy: { small: ["public"],
|
|
385
|
+
# large: ["northeastern:drs:repository:archives"],
|
|
386
|
+
# service: ["northeastern:drs:repository:archives"] }
|
|
387
|
+
# )
|
|
388
|
+
def self.set_derivative_permissions(id, policy:, nuid: nil, on_behalf_of: nil)
|
|
389
|
+
AtlasRb::Mash.new(JSON.parse(
|
|
390
|
+
connection({}, nuid, on_behalf_of: on_behalf_of)
|
|
391
|
+
.patch(ROUTE + id + '/derivative_permissions', JSON.dump(policy))&.body
|
|
392
|
+
))
|
|
393
|
+
end
|
|
394
|
+
|
|
347
395
|
# Store a Work's derived full-document text for search indexing.
|
|
348
396
|
#
|
|
349
397
|
# Purpose-specific PATCH in the same "machine-set derived metadata" family
|
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.8.
|
|
4
|
+
version: 1.8.4
|
|
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-
|
|
11
|
+
date: 2026-07-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|