atlas_rb 1.8.3 → 1.8.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d25a6bf4fdadf6bacf1de67390aeb15343547dba8384c511d8c34b62c1fb058e
4
- data.tar.gz: f002dfcb1638bd8c5c5cb668121b9a8515f85765aaab4b229770d8d7b6b3587c
3
+ metadata.gz: 6b34244827cff750075d39c45f1fd628840b6b1637a06f6bfefc876f285eafbe
4
+ data.tar.gz: d3f429989a1b983ed13a39c8363b17f1eff439d829f3694244327a38cfc2ca7e
5
5
  SHA512:
6
- metadata.gz: 9b7f605825d8d4ebb6c44c93c16bc8a7f16a6ee5cb3d7a83c2cba05c7de59e9c25fc92811f35a332ff326dc929b63c057c2743ed8d5fdeb807288f49f2cb2c79
7
- data.tar.gz: 20066bd2c7f096a70d3ace6d3c36ed888d135f91e2e31dec7f7828f6eb1f0298e46e4fb0b06fe112be840846675240f6a026787f2479e8e7c73599414f3830d9
6
+ metadata.gz: 70c4cce5eacaabca34495bee0cd8ed0ada54998e7a90f3b39b26b601d4d20be9e16355d64ac68e75fe39fce8f28d3e25e275d5627afd3644e296c18fdca3453e
7
+ data.tar.gz: b46212f70bfbec6a343654f89e73e33a13a372abb2e42ff0bf88a0a667d2d5033cee6e221dbdc63022b9feca6f0fd7000c12eb90172a76feaa11540696c04daa
data/.version CHANGED
@@ -1 +1 @@
1
- 1.8.3
1
+ 1.8.5
data/CHANGELOG.md CHANGED
@@ -1,5 +1,44 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.8.5
4
+
5
+ ### Fixed — `find` returns a tombstone (`410`) instead of raising
6
+
7
+ The status-aware `find` (1.8.3) raised `AtlasRb::ResourceError` on any non-2xx
8
+ other than `404`. But Atlas returns a **tombstoned** resource as `410 Gone`
9
+ **with its full body** (carrying `tombstoned` / `tombstoned_at` / `tombstoned_by`)
10
+ — a "gone, but here it is" tombstone, not an error envelope. `fetch_resource`
11
+ now parses a `410` like a `2xx`, so every typed `find` (`Work` / `Collection` /
12
+ `Community` / `FileSet` / `Person` / `Compilation` / `Blob` / `Delegate` and the
13
+ polymorphic `Resource.find`) yields the tombstone rather than raising. Genuine
14
+ error statuses (400/401/403/422) still raise; a `404` still returns `nil`.
15
+
16
+ ## 1.8.4
17
+
18
+ ### Added — per-tier derivative-visibility policy (`Work.set_derivative_permissions`)
19
+
20
+ New binding for the departmental "each download rendition has its own permissions"
21
+ model: `AtlasRb::Work.set_derivative_permissions(id, policy:, …)` PATCHes a per-tier
22
+ read policy to `PATCH /works/:id/derivative_permissions`. `policy` is a map of tier
23
+ (`small` / `medium` / `large` / `service`) => array of read-group tokens (the resource
24
+ vocabulary: `"public"`, Grouper group names, `[]` = private). Whole-object replace;
25
+ omitted tiers inherit by cascade (an absent tier inherits the next lower-resolution
26
+ tier; `small` inherits the Work). Atlas enforces two invariants — a tier may not be
27
+ more visible than the Work, and visibility must narrow as resolution grows
28
+ (`service` ⊆ `large` ⊆ `medium` ⊆ `small`).
29
+
30
+ The effective per-Delegate gate surfaces on `Work.assets` entries as `gated` (must be
31
+ authorized, not linked directly) and `permission` (the effective read-group set;
32
+ `null` for guests). No `assets` change is needed — the new fields flow through the
33
+ existing polymorphic Mash. The gate is advisory: Cerberus and the IIIF auth layer
34
+ enforce it.
35
+
36
+ A `422` rejection (`tier_exceeds_resource` / `tier_ordering_violation` /
37
+ `unknown_tier`) now raises the typed `AtlasRb::DerivativePermissionsError` (with `code`
38
+ / `resource_id`) via `RaiseOnResourceError`, mirroring the re-parent / linked-member /
39
+ Compilation mappings; a `403` raises `AtlasRb::ForbiddenError`; a `409` raises
40
+ `AtlasRb::StaleResourceError`.
41
+
3
42
  ## 1.8.3
4
43
 
5
44
  ### Changed — `find` is status-aware (stops swallowing Atlas error envelopes)
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- atlas_rb (1.8.3)
4
+ atlas_rb (1.8.5)
5
5
  faraday (~> 2.7)
6
6
  faraday-follow_redirects (~> 0.3.0)
7
7
  faraday-multipart (~> 1)
data/lib/atlas_rb/blob.rb CHANGED
@@ -29,7 +29,7 @@ module AtlasRb
29
29
  # held bytes — reconciliation compares this against the v1 manifest
30
30
  # without re-downloading), and a download URL — or `nil` when the Blob
31
31
  # does not exist (`404`).
32
- # @raise [AtlasRb::ResourceError] on any non-2xx other than `404` (e.g. an
32
+ # @raise [AtlasRb::ResourceError] on any non-2xx other than `404` / `410` (e.g. an
33
33
  # auth/validation error envelope), carrying Atlas's status + body.
34
34
  #
35
35
  # @example
@@ -24,7 +24,7 @@ module AtlasRb
24
24
  # omitted.
25
25
  # @return [Hash, nil] the `"collection"` object, already unwrapped from the
26
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
27
+ # @raise [AtlasRb::ResourceError] on any non-2xx other than `404` / `410` (e.g. an
28
28
  # auth/validation error envelope), carrying Atlas's status + body.
29
29
  #
30
30
  # @example
@@ -25,7 +25,7 @@ module AtlasRb
25
25
  # omitted.
26
26
  # @return [Hash, nil] the `"community"` object from the JSON response,
27
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
28
+ # @raise [AtlasRb::ResourceError] on any non-2xx other than `404` / `410` (e.g. an
29
29
  # auth/validation error envelope), carrying Atlas's status + body.
30
30
  #
31
31
  # @example
@@ -33,7 +33,7 @@ module AtlasRb
33
33
  # includes `id`, `valkyrie_id`, `use`, `uri`, `mime_type`,
34
34
  # `original_filename`, `label`, and tombstone fields — or `nil` when the
35
35
  # Delegate does not exist (`404`).
36
- # @raise [AtlasRb::ResourceError] on any non-2xx other than `404` (e.g. an
36
+ # @raise [AtlasRb::ResourceError] on any non-2xx other than `404` / `410` (e.g. an
37
37
  # auth/validation error envelope), carrying Atlas's status + body.
38
38
  #
39
39
  # @example
@@ -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
@@ -203,7 +235,7 @@ module AtlasRb
203
235
  # Raised by the typed single-resource readers ({Resource.find} and the
204
236
  # `Work` / `Collection` / `Community` / `FileSet` / `Person` / `Compilation`
205
237
  # / `Blob` / `Delegate` overrides) when Atlas answers the `GET` with a
206
- # non-2xx that is **not** a `404` — i.e. an error envelope
238
+ # non-2xx that is **not** a `404` or a `410` — i.e. an error envelope
207
239
  # (`{ "error" => ... }`, status 400/401/403/422) on what the caller treated
208
240
  # as a plain read.
209
241
  #
@@ -217,7 +249,10 @@ module AtlasRb
217
249
  # attributable everywhere `find` is used.
218
250
  #
219
251
  # A genuine `404` is **not** this — it stays a clean `nil` return, since
220
- # "not found" is a normal `find` outcome callers already nil-check.
252
+ # "not found" is a normal `find` outcome callers already nil-check. A `410`
253
+ # is also **not** this — a tombstoned resource comes back as `410 Gone` with
254
+ # its full body, which `find` returns (the caller nil-checks / reads
255
+ # `tombstoned`), rather than an error to raise.
221
256
  #
222
257
  # @note Authorization failures on the narrow re-parent / linked-member /
223
258
  # Compilation write paths surface as {ForbiddenError} via
@@ -25,7 +25,7 @@ module AtlasRb
25
25
  # omitted.
26
26
  # @return [Hash, nil] the `"file_set"` object, already unwrapped, or `nil`
27
27
  # when the FileSet does not exist (`404`).
28
- # @raise [AtlasRb::ResourceError] on any non-2xx other than `404` (e.g. an
28
+ # @raise [AtlasRb::ResourceError] on any non-2xx other than `404` / `410` (e.g. an
29
29
  # auth/validation error envelope), carrying Atlas's status + body.
30
30
  #
31
31
  # @example
@@ -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...`), and binary uploads
20
- # (`/files...`, `/file_sets...`), and only on `403` / `422` bodies carrying
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)",
@@ -37,7 +37,7 @@ module AtlasRb
37
37
  # server-side `nuid` for callers that need it, e.g. depositor gating, and
38
38
  # `personal_root_id` for the publish-conduit parent), or `nil` when the
39
39
  # Person does not exist (`404`).
40
- # @raise [AtlasRb::ResourceError] on any non-2xx other than `404` (e.g. an
40
+ # @raise [AtlasRb::ResourceError] on any non-2xx other than `404` / `410` (e.g. an
41
41
  # auth/validation error envelope), carrying Atlas's status + body.
42
42
  def self.find(id, nuid: nil, on_behalf_of: nil)
43
43
  body = fetch_resource(ROUTE + id, nuid: nuid, on_behalf_of: on_behalf_of)
@@ -36,7 +36,7 @@ module AtlasRb
36
36
  # when the id resolves to nothing (`404`):
37
37
  # - `"klass"` — the resource type, capitalized (e.g. `"Work"`).
38
38
  # - `"resource"` — the resource payload as a Hash.
39
- # @raise [AtlasRb::ResourceError] on any non-2xx other than `404` (e.g. an
39
+ # @raise [AtlasRb::ResourceError] on any non-2xx other than `404` / `410` (e.g. an
40
40
  # auth/validation error envelope), carrying Atlas's status + body.
41
41
  #
42
42
  # @example Polymorphic lookup
@@ -260,6 +260,10 @@ module AtlasRb
260
260
  # - `404` → `nil` (clean "not found"; also avoids the
261
261
  # `JSON::ParserError` the old code raised on the empty `head :not_found`
262
262
  # body).
263
+ # - `410` → the parsed JSON Hash. A tombstoned resource is
264
+ # returned as `410 Gone` WITH its full body (carrying tombstoned /
265
+ # tombstoned_at / tombstoned_by), so it is a returnable tombstone, not an
266
+ # error envelope — `find` yields the tombstone rather than raising.
263
267
  # - any other non-2xx → {AtlasRb::ResourceError} carrying Atlas's status +
264
268
  # body, so the failure is attributable at the boundary.
265
269
  # - `2xx` → the parsed JSON Hash, for the caller to unwrap.
@@ -267,13 +271,19 @@ module AtlasRb
267
271
  # @param path [String] the resource path to GET (e.g. `"/works/abc123"`).
268
272
  # @param nuid [String, nil] optional acting user's NUID (see {find}).
269
273
  # @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`.
274
+ # @return [Hash, nil] the parsed JSON envelope (incl. a `410` tombstone), or
275
+ # `nil` on `404`.
276
+ # @raise [AtlasRb::ResourceError] on any non-2xx other than `404` / `410`.
272
277
  # @api private
273
278
  def self.fetch_resource(path, nuid: nil, on_behalf_of: nil)
274
279
  resp = connection({}, nuid, on_behalf_of: on_behalf_of).get(path)
275
280
  return nil if resp.status == 404
276
- unless resp.success?
281
+
282
+ # A tombstoned resource comes back as 410 Gone WITH its full body — a
283
+ # "gone, but here it is" tombstone, not an error envelope — so parse it
284
+ # like a 2xx and let callers read the tombstone. Only genuine error
285
+ # statuses raise.
286
+ unless resp.success? || resp.status == 410
277
287
  raise AtlasRb::ResourceError.new("GET #{path} → #{resp.status}: #{resp.body}", response: resp)
278
288
  end
279
289
 
data/lib/atlas_rb/work.rb CHANGED
@@ -24,7 +24,7 @@ module AtlasRb
24
24
  # {AtlasRb.config}.default_on_behalf_of when omitted.
25
25
  # @return [Hash, nil] the `"work"` object, already unwrapped from the JSON
26
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
27
+ # @raise [AtlasRb::ResourceError] on any non-2xx other than `404` / `410` (e.g. an
28
28
  # auth/validation error envelope), carrying Atlas's status + body.
29
29
  #
30
30
  # @example
@@ -345,6 +345,53 @@ module AtlasRb
345
345
  ))
346
346
  end
347
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
+
348
395
  # Store a Work's derived full-document text for search indexing.
349
396
  #
350
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.3
4
+ version: 1.8.5
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-27 00:00:00.000000000 Z
11
+ date: 2026-07-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday