atlas_rb 1.9.2 → 1.9.3
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 +42 -0
- data/Gemfile.lock +1 -1
- data/README.md +5 -0
- data/lib/atlas_rb/collection.rb +14 -2
- data/lib/atlas_rb/community.rb +14 -2
- data/lib/atlas_rb/errors.rb +48 -5
- data/lib/atlas_rb/middleware/raise_on_resource_error.rb +41 -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: 25a2216a5796eae1514b062b18135d9f071c731f006b50e8f72d6064380f9923
|
|
4
|
+
data.tar.gz: d8d564ef0d476aa32046c5a4294776b73217e0c4a932b5b9895f22f6843c4371
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 685a8bf54eeffcf7590a26147455deeca64751e7161155978607c6bf6b68b29b9c982b9b6ffbca1bdbc004dcdeec0b9d1f5fe5955b17e5e9009b74c709e91f51
|
|
7
|
+
data.tar.gz: ef296b4c28970b3ec6d3b3ec35647a90407636d174d4a6ee9d1663640fd8c9b3853f4b0371ae6c76d89774fcfe7947d86152b6e717d1ae6b0fe8a509ea8392d0
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.9.
|
|
1
|
+
1.9.3
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.3
|
|
4
|
+
|
|
5
|
+
### Added — `depositor:` on `Collection.create` and `Community.create`
|
|
6
|
+
|
|
7
|
+
`AtlasRb::Collection.create(parent_id, depositor: "000000099")` (and the same
|
|
8
|
+
on `Community.create`) stamps a container's intellectual owner, matching what
|
|
9
|
+
`Work.create` has always supported. Atlas already read the param; only the
|
|
10
|
+
bindings couldn't pass it.
|
|
11
|
+
|
|
12
|
+
The case it unblocks is seeding an institutional tree: the caller acts as an
|
|
13
|
+
admin (the only identity whose wildcard carries a whole seed sequence) while
|
|
14
|
+
attributing the containers to the anonymous NUID, since nobody personally owns
|
|
15
|
+
them and access is via Grouper groups. That matters now that a depositor
|
|
16
|
+
carries edit rights on their own resource — a container stamped with a real
|
|
17
|
+
person's NUID would hand them edit over that whole subtree.
|
|
18
|
+
|
|
19
|
+
Omitted, the depositor still falls through to the acting user, so existing
|
|
20
|
+
callers are unaffected.
|
|
21
|
+
|
|
22
|
+
### Added — typed errors for Atlas's container-create `403` and ACL-write `422`
|
|
23
|
+
|
|
24
|
+
Atlas gained two refusals that the bindings previously swallowed, because
|
|
25
|
+
`RaiseOnResourceError` was scoped to the re-parent / linked-member /
|
|
26
|
+
Compilation / derivative-permissions / upload paths:
|
|
27
|
+
|
|
28
|
+
- **A refused create.** `POST /{works,collections,communities}` now `403`s when
|
|
29
|
+
the caller holds no edit rights on the destination container. The binding's
|
|
30
|
+
`["collection"]` unwrap returned `nil`, so the caller's next `.id` raised
|
|
31
|
+
`NoMethodError` — an unhandled 500 instead of an authorization failure. Now
|
|
32
|
+
{AtlasRb::ForbiddenError}, carrying `action: "create_child"` and the parent
|
|
33
|
+
container's class as `subject`.
|
|
34
|
+
- **A refused ACL write.** `PATCH /{type}/:id` with `metadata[permissions]`
|
|
35
|
+
now `422`s with `visibility_exceeds_parent` when the read audience would
|
|
36
|
+
exceed the structural container. The parsed envelope looked like a success
|
|
37
|
+
payload, so a user's visibility edit was discarded silently. Now
|
|
38
|
+
{AtlasRb::PermissionsError}, keyed on the `error` code rather than the path —
|
|
39
|
+
the same endpoint's other `422`s (tombstone's `has_live_children`) still pass
|
|
40
|
+
through untouched, which a path rule could not distinguish.
|
|
41
|
+
|
|
42
|
+
The create branch matches the three paths exactly and only on `POST`, so member
|
|
43
|
+
actions under the same prefix and the index `GET` are unaffected.
|
|
44
|
+
|
|
3
45
|
## 1.9.2
|
|
4
46
|
|
|
5
47
|
### Added — `read_only:` on `System::Token.mint`
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -222,6 +222,11 @@ AtlasRb::FileSet.create("w-789", "primary") # file_set under work w-789, classif
|
|
|
222
222
|
AtlasRb::Blob.create("w-789", path, name) # blob under work w-789 with original filename preserved
|
|
223
223
|
```
|
|
224
224
|
|
|
225
|
+
`Community.create`, `Collection.create`, and `Work.create` each accept an
|
|
226
|
+
optional `depositor:` kwarg — the NUID to stamp as the resource's intellectual
|
|
227
|
+
owner, independent of who authorizes the call. Omitted, Atlas falls through to
|
|
228
|
+
the acting user.
|
|
229
|
+
|
|
225
230
|
`Work.create`, `FileSet.create`, and `Blob.create` each accept an optional
|
|
226
231
|
`idempotency_key:` kwarg for retry-safe bulk-deposit jobs. The caller
|
|
227
232
|
generates the UUID; the Atlas server enforces uniqueness scoped to the
|
data/lib/atlas_rb/collection.rb
CHANGED
|
@@ -57,14 +57,26 @@ module AtlasRb
|
|
|
57
57
|
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
58
58
|
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
59
59
|
# omitted.
|
|
60
|
+
# @param depositor [String, nil] NUID to stamp as the Collection's
|
|
61
|
+
# intellectual owner. Omit it and Atlas falls through to the acting user.
|
|
62
|
+
# Supply it to attribute a container to someone other than whoever is
|
|
63
|
+
# authorizing the call — e.g. seeding an institutional tree as an admin
|
|
64
|
+
# while attributing it to the anonymous NUID, since nobody personally owns
|
|
65
|
+
# those containers and access to them is via Grouper groups. The depositor
|
|
66
|
+
# is immutable post-create; there is no setter on the update surface.
|
|
60
67
|
# @return [Hash] the created Collection payload (post-update if
|
|
61
68
|
# `xml_path` was supplied).
|
|
62
69
|
#
|
|
63
70
|
# @example A featured showcase collection
|
|
64
71
|
# AtlasRb::Collection.create("c-123", featured: true)
|
|
65
|
-
|
|
72
|
+
#
|
|
73
|
+
# @example An institutional container owned by nobody
|
|
74
|
+
# AtlasRb::Collection.create("c-123", depositor: "000000099")
|
|
75
|
+
def self.create(id, xml_path = nil, featured: false, nuid: nil, on_behalf_of: nil, depositor: nil)
|
|
76
|
+
params = { parent_id: id, featured: featured }
|
|
77
|
+
params[:depositor] = depositor if depositor
|
|
66
78
|
result = AtlasRb::Mash.new(JSON.parse(
|
|
67
|
-
connection(
|
|
79
|
+
connection(params, nuid, on_behalf_of: on_behalf_of).post(ROUTE)&.body
|
|
68
80
|
))["collection"]
|
|
69
81
|
return result if xml_path.to_s.empty?
|
|
70
82
|
|
data/lib/atlas_rb/community.rb
CHANGED
|
@@ -52,6 +52,13 @@ module AtlasRb
|
|
|
52
52
|
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
53
53
|
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
54
54
|
# omitted.
|
|
55
|
+
# @param depositor [String, nil] NUID to stamp as the Community's
|
|
56
|
+
# intellectual owner. Omit it and Atlas falls through to the acting user.
|
|
57
|
+
# Supply it to attribute a container to someone other than whoever is
|
|
58
|
+
# authorizing the call — e.g. seeding an institutional tree as an admin
|
|
59
|
+
# while attributing it to the anonymous NUID, since nobody personally owns
|
|
60
|
+
# those containers and access to them is via Grouper groups. The depositor
|
|
61
|
+
# is immutable post-create; there is no setter on the update surface.
|
|
55
62
|
# @return [Hash] the created Community payload (post-update if `xml_path`
|
|
56
63
|
# was supplied).
|
|
57
64
|
#
|
|
@@ -60,9 +67,14 @@ module AtlasRb
|
|
|
60
67
|
#
|
|
61
68
|
# @example Sub-community seeded from MODS
|
|
62
69
|
# AtlasRb::Community.create("c-parent", "/tmp/dept-mods.xml")
|
|
63
|
-
|
|
70
|
+
#
|
|
71
|
+
# @example An institutional container owned by nobody
|
|
72
|
+
# AtlasRb::Community.create("c-parent", depositor: "000000099")
|
|
73
|
+
def self.create(id = nil, xml_path = nil, nuid: nil, on_behalf_of: nil, depositor: nil)
|
|
74
|
+
params = { parent_id: id }
|
|
75
|
+
params[:depositor] = depositor if depositor
|
|
64
76
|
result = AtlasRb::Mash.new(JSON.parse(
|
|
65
|
-
connection(
|
|
77
|
+
connection(params, nuid, on_behalf_of: on_behalf_of).post(ROUTE)&.body
|
|
66
78
|
))["community"]
|
|
67
79
|
return result if xml_path.to_s.empty?
|
|
68
80
|
|
data/lib/atlas_rb/errors.rb
CHANGED
|
@@ -201,15 +201,58 @@ module AtlasRb
|
|
|
201
201
|
end
|
|
202
202
|
end
|
|
203
203
|
|
|
204
|
-
# Raised when Atlas refuses
|
|
205
|
-
#
|
|
204
|
+
# Raised when Atlas refuses an ACL write on a resource
|
|
205
|
+
# (`PATCH /{works,collections,communities}/:id` with `metadata[permissions]`)
|
|
206
|
+
# because it breaks a rights invariant. Today the one code is
|
|
207
|
+
# `visibility_exceeds_parent`: a resource may be no more visible than its
|
|
208
|
+
# structural container, so opening a Work to `public` inside a restricted
|
|
209
|
+
# Collection is refused (the fix is to widen the container).
|
|
210
|
+
#
|
|
211
|
+
# This is NOT an authorization failure — the caller may well hold full edit
|
|
212
|
+
# rights on the resource, and a depositor choosing "Public" on their own item
|
|
213
|
+
# under a private collection trips it in ordinary use. Without the typed
|
|
214
|
+
# error the binding's `["collection"]` / `["work"]` unwrap returns an envelope
|
|
215
|
+
# the caller reads as success, so the user's visibility edit is silently
|
|
216
|
+
# discarded.
|
|
217
|
+
#
|
|
218
|
+
# rescue AtlasRb::PermissionsError => e
|
|
219
|
+
# flash.now[:alert] = t("permissions.errors.#{e.code}", default: e.message)
|
|
220
|
+
#
|
|
221
|
+
# @note Keyed on the envelope's `error` code, not the request path, so other
|
|
222
|
+
# `422`s on the same endpoint (e.g. `tombstone`'s `has_live_children`) pass
|
|
223
|
+
# through untouched.
|
|
224
|
+
class PermissionsError < Error
|
|
225
|
+
# @return [String, nil] the machine-readable error code from the envelope,
|
|
226
|
+
# suitable for keying an i18n map.
|
|
227
|
+
attr_reader :code
|
|
228
|
+
|
|
229
|
+
# @return [String, nil] the rejected resource's ID, from the envelope.
|
|
230
|
+
attr_reader :resource_id
|
|
231
|
+
|
|
232
|
+
# @param message [String] human-readable rejection description.
|
|
233
|
+
# @param code [String, nil] the envelope's `error` discriminator.
|
|
234
|
+
# @param resource_id [String, nil] the rejected resource's ID.
|
|
235
|
+
def initialize(message, code: nil, resource_id: nil)
|
|
236
|
+
super(message)
|
|
237
|
+
@code = code
|
|
238
|
+
@resource_id = resource_id
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Raised when Atlas refuses a re-parent, linked-member, Compilation, or
|
|
243
|
+
# container-create request with an HTTP `403`, whose envelope is
|
|
206
244
|
# `{ "error", "action", "subject" }`. Lets callers distinguish "you may
|
|
207
245
|
# not do this" from a structural rejection ({ReparentError} /
|
|
208
246
|
# {LinkedMemberError} / {CompilationError}) or a not-found.
|
|
209
247
|
#
|
|
210
|
-
#
|
|
211
|
-
#
|
|
212
|
-
#
|
|
248
|
+
# On a create the `action` is `create_child` and the `subject` is the parent
|
|
249
|
+
# container's class: the caller holds no edit rights on the container the new
|
|
250
|
+
# child would land in.
|
|
251
|
+
#
|
|
252
|
+
# @note Scoped to the re-parent / linked-member write paths, the Compilation
|
|
253
|
+
# surface, and the container-create endpoints — `403`s on other endpoints
|
|
254
|
+
# still surface as raw responses for the caller's own rescue layer,
|
|
255
|
+
# unchanged.
|
|
213
256
|
class ForbiddenError < Error
|
|
214
257
|
# @return [String, nil] the envelope's `error` value.
|
|
215
258
|
attr_reader :code
|
|
@@ -17,22 +17,29 @@ module AtlasRb
|
|
|
17
17
|
# It is intentionally narrow — it only fires on the re-parent
|
|
18
18
|
# (`.../parent`) and linked-member (`.../linked_members...`) write paths,
|
|
19
19
|
# the Compilation surface (`/compilations...`), the derivative-permissions
|
|
20
|
-
# write (`.../derivative_permissions`),
|
|
21
|
-
#
|
|
22
|
-
# an `error` discriminator. The upload
|
|
23
|
-
# discriminator ({FIXITY_CODES}), so a
|
|
24
|
-
# `error` (or `403`s on uploads, which
|
|
20
|
+
# write (`.../derivative_permissions`), the container-create endpoints
|
|
21
|
+
# ({CREATE_PATHS}), and binary uploads (`/files...`, `/file_sets...`), and
|
|
22
|
+
# only on `403` / `422` bodies carrying an `error` discriminator. The upload
|
|
23
|
+
# branch is further gated on a fixity discriminator ({FIXITY_CODES}), so a
|
|
24
|
+
# `422` on those paths with any other `error` (or `403`s on uploads, which
|
|
25
|
+
# stay raw) passes through untouched.
|
|
25
26
|
# Everything else (other paths, other statuses, a `422` whose body uses a
|
|
26
27
|
# different discriminator such as `tombstone`'s `code: "has_live_children"`)
|
|
27
28
|
# passes through untouched, so atlas_rb stays a thin Faraday binding that
|
|
28
29
|
# translates only the wire signals callers genuinely need to discriminate.
|
|
29
30
|
#
|
|
31
|
+
# The ACL branch is keyed on the `error` code ({PERMISSIONS_CODES}) rather
|
|
32
|
+
# than a path, because the write it guards is the plain resource `PATCH`
|
|
33
|
+
# whose other rejections (tombstone's `has_live_children`) must keep passing
|
|
34
|
+
# through — a path rule couldn't tell them apart.
|
|
35
|
+
#
|
|
30
36
|
# Mapping:
|
|
31
|
-
# - `403` on a re-parent/linked/Compilation/derivative-permissions path → {AtlasRb::ForbiddenError}
|
|
37
|
+
# - `403` on a re-parent/linked/Compilation/derivative-permissions/create path → {AtlasRb::ForbiddenError}
|
|
32
38
|
# - `422` on `.../parent` → {AtlasRb::ReparentError} (`error`/`resource_id`)
|
|
33
39
|
# - `422` on `.../linked_members...` → {AtlasRb::LinkedMemberError}
|
|
34
40
|
# - `422` on `/compilations...` → {AtlasRb::CompilationError}
|
|
35
41
|
# - `422` on `.../derivative_permissions` → {AtlasRb::DerivativePermissionsError}
|
|
42
|
+
# - `422` + an ACL discriminator anywhere → {AtlasRb::PermissionsError}
|
|
36
43
|
# - `422` + a fixity discriminator on `/files...` / `/file_sets...` →
|
|
37
44
|
# {AtlasRb::FixityMismatchError}
|
|
38
45
|
class RaiseOnResourceError < Faraday::Middleware
|
|
@@ -40,12 +47,25 @@ module AtlasRb
|
|
|
40
47
|
# `error` on those paths passes through (Atlas owns these as a wire contract).
|
|
41
48
|
FIXITY_CODES = %w[fixity_mismatch unsupported_digest_algorithm].freeze
|
|
42
49
|
|
|
50
|
+
# ACL-invariant `422` discriminators, translated wherever they appear:
|
|
51
|
+
# Atlas raises them from the shared metadata-PATCH funnel, so they can
|
|
52
|
+
# arrive on any resource's `PATCH /{type}/:id`.
|
|
53
|
+
PERMISSIONS_CODES = %w[visibility_exceeds_parent].freeze
|
|
54
|
+
|
|
55
|
+
# The container-create endpoints, matched exactly (no trailing id) and only
|
|
56
|
+
# on `POST`, so neither a member action under the same prefix
|
|
57
|
+
# (`/collections/:id/tombstone`, whose `422` must pass through) nor the
|
|
58
|
+
# index `GET` on the same path can be caught by mistake. A `403` here means
|
|
59
|
+
# the caller holds no edit rights on the destination container.
|
|
60
|
+
CREATE_PATHS = %w[/works /collections /communities].freeze
|
|
61
|
+
|
|
43
62
|
# @param env [Faraday::Env] the completed response environment.
|
|
44
|
-
# @raise [AtlasRb::ForbiddenError] on a 403 to a re-parent/linked/Compilation path.
|
|
63
|
+
# @raise [AtlasRb::ForbiddenError] on a 403 to a re-parent/linked/Compilation/create path.
|
|
45
64
|
# @raise [AtlasRb::ReparentError] on a 422 to a re-parent path.
|
|
46
65
|
# @raise [AtlasRb::LinkedMemberError] on a 422 to a linked-member path.
|
|
47
66
|
# @raise [AtlasRb::CompilationError] on a 422 to a Compilation path.
|
|
48
67
|
# @raise [AtlasRb::DerivativePermissionsError] on a 422 to a derivative-permissions path.
|
|
68
|
+
# @raise [AtlasRb::PermissionsError] on a 422 carrying an ACL-invariant discriminator.
|
|
49
69
|
# @raise [AtlasRb::FixityMismatchError] on a 422 + fixity discriminator to an upload path.
|
|
50
70
|
# @return [void]
|
|
51
71
|
def on_complete(env)
|
|
@@ -56,15 +76,27 @@ module AtlasRb
|
|
|
56
76
|
linked = path.include?("/linked_members")
|
|
57
77
|
compilation = path.start_with?("/compilations")
|
|
58
78
|
deriv_perms = path.end_with?("/derivative_permissions")
|
|
79
|
+
create = env.method.to_s == "post" && CREATE_PATHS.include?(path.chomp("/"))
|
|
59
80
|
upload = path.start_with?("/files") || path.start_with?("/file_sets")
|
|
60
|
-
return unless reparent || linked || compilation || deriv_perms || upload
|
|
61
81
|
|
|
62
82
|
body = parse_json(env.body)
|
|
63
83
|
return unless body.is_a?(Hash) && body["error"]
|
|
64
84
|
|
|
85
|
+
# Path-independent: the ACL invariants ride the shared metadata PATCH,
|
|
86
|
+
# so the code is the only reliable signal.
|
|
87
|
+
if env.status == 422 && PERMISSIONS_CODES.include?(body["error"])
|
|
88
|
+
raise AtlasRb::PermissionsError.new(
|
|
89
|
+
body["message"] || "Atlas rejected the permissions write",
|
|
90
|
+
code: body["error"],
|
|
91
|
+
resource_id: body["resource_id"]
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
return unless reparent || linked || compilation || deriv_perms || create || upload
|
|
96
|
+
|
|
65
97
|
if env.status == 403
|
|
66
98
|
# 403s on upload paths stay raw — acting-as/authz isn't an upload concern here.
|
|
67
|
-
return unless reparent || linked || compilation || deriv_perms
|
|
99
|
+
return unless reparent || linked || compilation || deriv_perms || create
|
|
68
100
|
|
|
69
101
|
raise AtlasRb::ForbiddenError.new(
|
|
70
102
|
body["message"] || "Atlas refused the request",
|
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.9.
|
|
4
|
+
version: 1.9.3
|
|
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-31 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|