atlas_rb 1.2.1 → 1.3.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/CHANGELOG.md +63 -0
- data/Gemfile.lock +1 -1
- data/README.md +61 -0
- data/lib/atlas_rb/audit_event.rb +109 -0
- data/lib/atlas_rb/resource.rb +38 -0
- data/lib/atlas_rb.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d65fea7329731bd0fa0a397a19f5764a05e88397c522baf28721e4a08282e2d3
|
|
4
|
+
data.tar.gz: 7b2db70c9e1a7d9e8fad5194bcb045baeed562561b1c36a60c174e2c1fc5d0aa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 335c7d4bd02d6bdb6f297cf86857c042d515680fb8e36991d98a7a7bf4fdd1e52470f0dda4b40605f79d0618c7260a64f2125af2a3100a693c920b51ebc1c1f5
|
|
7
|
+
data.tar.gz: ce1bc4b16ce482975af0785138574aa6b42fc2cc05ddfee777adbef2834b733bdb93df5390cfc4e9a5012af4b9483cf5676f7213edc8e79afc0fd886d2b84545
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.
|
|
1
|
+
1.3.0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,68 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.3.0
|
|
4
|
+
|
|
5
|
+
### Added — `AtlasRb::Resource.find_many` (batch resolve by NOID)
|
|
6
|
+
|
|
7
|
+
A binding for Atlas's `POST /resources/find_many`. Resolves a set of NOIDs
|
|
8
|
+
to lightweight digests in **one** round-trip, replacing the `find`-per-id
|
|
9
|
+
fan-out that several Cerberus surfaces (breadcrumbs, linked-member lists,
|
|
10
|
+
load-destination pickers) paid on every render.
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
nodes = AtlasRb::Resource.find_many(["col-456", "col-457", "missing"])
|
|
14
|
+
by_noid = nodes.index_by { |n| n["noid"] }
|
|
15
|
+
by_noid["col-456"].title # => "Some Collection"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
- Each digest is `{ "id", "noid", "klass", "title", "thumbnail",
|
|
19
|
+
"tombstoned" }` — not the full typed payload. `title` / `thumbnail` are
|
|
20
|
+
`null` for resources off the Modsable backbone (FileSet/Blob).
|
|
21
|
+
- The ids ride in the request **body**, so the list isn't bounded by URL
|
|
22
|
+
length. Returns one `AtlasRb::Mash` per resolved resource.
|
|
23
|
+
- The result is **unordered** and **may be shorter than the input**:
|
|
24
|
+
unresolvable ids are dropped, tombstoned ones come back flagged
|
|
25
|
+
(`"tombstoned" => true`). Index by `"noid"` — don't assume positional
|
|
26
|
+
correspondence with the input.
|
|
27
|
+
- Resolves NOIDs (alternate ids) only; raw Valkyrie ids are not a supported
|
|
28
|
+
input.
|
|
29
|
+
|
|
30
|
+
## 1.2.2
|
|
31
|
+
|
|
32
|
+
### Added — `AtlasRb::AuditEvent.emit` (session-scoped audit events)
|
|
33
|
+
|
|
34
|
+
A new binding for Atlas's `POST /audit_events` endpoint, which records an
|
|
35
|
+
AuditEvent with a **null `resource_id`** — an event not tied to any
|
|
36
|
+
resource write. This is the gem's half of the impersonation audit trail
|
|
37
|
+
(acting-as / view-as): the session lifecycle lives entirely in the calling
|
|
38
|
+
application, and a view-as session performs no writes, so neither leaves a
|
|
39
|
+
per-resource event for `Resource.history` to surface.
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
AtlasRb::AuditEvent.emit(
|
|
43
|
+
action: "impersonation_started", # or "impersonation_ended"
|
|
44
|
+
actor_nuid: admin_nuid,
|
|
45
|
+
on_behalf_of_nuid: target_nuid,
|
|
46
|
+
mode: "acting_as" # or "view_as"
|
|
47
|
+
)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
- The recorded principals (`actor_nuid`, `on_behalf_of_nuid`), `mode`, and
|
|
51
|
+
an optional free-form `payload:` travel in the request **body**, not in
|
|
52
|
+
ambient headers — so the call is self-describing even when fired as a
|
|
53
|
+
session is being torn down (e.g. `impersonation_ended`).
|
|
54
|
+
- The request authenticates via the standard `connection` (system token),
|
|
55
|
+
with the `User: NUID` header pinned to `actor_nuid` so the server-side
|
|
56
|
+
admin gate holds regardless of ambient `Current` state.
|
|
57
|
+
- `on_behalf_of_nuid`, `mode`, and `payload` are omitted from the body when
|
|
58
|
+
blank, leaving room for future, mode-less session events. Atlas stamps
|
|
59
|
+
`occurred_at` server-side.
|
|
60
|
+
- Authorization errors (`401` / `403`) surface as raw Faraday responses,
|
|
61
|
+
matching `Resource.history`.
|
|
62
|
+
|
|
63
|
+
Depends on the matching Atlas-side `POST /audit_events` emit endpoint
|
|
64
|
+
(nullable resource scope, admin-gated); see the impersonation gap report.
|
|
65
|
+
|
|
3
66
|
## 1.2.1
|
|
4
67
|
|
|
5
68
|
### Added — typed errors for re-parent / linked-member rejections
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -113,6 +113,7 @@ Community → Collection → Work
|
|
|
113
113
|
| `AtlasRb::Blob` | The binary bytes; supports streaming downloads. |
|
|
114
114
|
| `AtlasRb::Authentication` | NUID → user record / group lookup. |
|
|
115
115
|
| `AtlasRb::Resource` | Generic resolver, permissions lookup, and audit-event history. |
|
|
116
|
+
| `AtlasRb::AuditEvent` | Emit a session-scoped (resource-less) audit event, e.g. impersonation. |
|
|
116
117
|
| `AtlasRb::Reset` | Test-only — wipes Atlas state via `GET /reset`. |
|
|
117
118
|
|
|
118
119
|
## Namespace gradient: regular / Admin / System
|
|
@@ -205,6 +206,45 @@ result["events"].first["action"] # => "update"
|
|
|
205
206
|
Authorization errors (`401` / `403`) are not caught here — they surface as
|
|
206
207
|
raw Faraday responses for the calling application's rescue layer.
|
|
207
208
|
|
|
209
|
+
### Session-scoped audit events
|
|
210
|
+
|
|
211
|
+
`Resource.history` only reads events that hang off a resource write. Some
|
|
212
|
+
auditable events have no resource to attach to — most notably impersonation
|
|
213
|
+
sessions (acting-as / view-as): the session lifecycle lives in the calling
|
|
214
|
+
app, and a view-as session performs no writes at all, so it would otherwise
|
|
215
|
+
leave no audit trail. `AtlasRb::AuditEvent.emit` wraps Atlas's
|
|
216
|
+
`POST /audit_events` endpoint, which records an event with a **null
|
|
217
|
+
`resource_id`**.
|
|
218
|
+
|
|
219
|
+
The principals and metadata travel in the request body — not inferred from
|
|
220
|
+
ambient headers — so the call is self-describing even when fired as a
|
|
221
|
+
session is being torn down. The request is admin-gated server-side; the
|
|
222
|
+
`User: NUID` header is pinned to the `actor_nuid` you pass.
|
|
223
|
+
|
|
224
|
+
```ruby
|
|
225
|
+
# An admin starts acting as another user:
|
|
226
|
+
AtlasRb::AuditEvent.emit(
|
|
227
|
+
action: "impersonation_started",
|
|
228
|
+
actor_nuid: Current.nuid, # the admin (also the User: header)
|
|
229
|
+
on_behalf_of_nuid: target_nuid, # the impersonated user
|
|
230
|
+
mode: "acting_as" # or "view_as"
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# …and later ends the session:
|
|
234
|
+
AtlasRb::AuditEvent.emit(
|
|
235
|
+
action: "impersonation_ended",
|
|
236
|
+
actor_nuid: admin_nuid,
|
|
237
|
+
on_behalf_of_nuid: target_nuid,
|
|
238
|
+
mode: "acting_as"
|
|
239
|
+
)
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
`on_behalf_of_nuid`, `mode`, and `payload:` (free-form metadata) are
|
|
243
|
+
omitted from the body when blank, so `emit` can also carry future,
|
|
244
|
+
mode-less session events. Atlas stamps `occurred_at` server-side.
|
|
245
|
+
Authorization errors (`401` / `403`) surface as raw Faraday responses,
|
|
246
|
+
matching `Resource.history`.
|
|
247
|
+
|
|
208
248
|
### Re-parenting
|
|
209
249
|
|
|
210
250
|
`reparent` moves a resource to a new structural parent, binding Atlas's
|
|
@@ -272,6 +312,27 @@ The two mutations raise the same way `reparent` does — `LinkedMemberError`
|
|
|
272
312
|
on a structural `422` (carrying the envelope's `error` code as `#code`) and
|
|
273
313
|
`ForbiddenError` on a `403` — instead of swallowing the envelope.
|
|
274
314
|
|
|
315
|
+
### Batch resolve (`Resource.find_many`)
|
|
316
|
+
|
|
317
|
+
When you have a *set* of NOIDs and only need each one's title / klass /
|
|
318
|
+
thumbnail — breadcrumb chains, linked-member lists, load-destination
|
|
319
|
+
pickers — resolve them in one round-trip instead of a `find`-per-id
|
|
320
|
+
fan-out:
|
|
321
|
+
|
|
322
|
+
```ruby
|
|
323
|
+
nodes = AtlasRb::Resource.find_many(["col-456", "col-457", "missing"])
|
|
324
|
+
by_noid = nodes.index_by { |n| n["noid"] }
|
|
325
|
+
by_noid["col-456"].title # => "Some Collection"
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Each entry is a lightweight digest — `{ "id", "noid", "klass", "title",
|
|
329
|
+
"thumbnail", "tombstoned" }` — not the full typed payload. The ids travel
|
|
330
|
+
in the request body (no URL-length ceiling). The result is **unordered**
|
|
331
|
+
and **may be shorter than the input**: unresolvable ids are dropped and
|
|
332
|
+
tombstoned resources come back flagged (`"tombstoned" => true`), so index
|
|
333
|
+
by `"noid"` rather than assuming positional correspondence. NOIDs only —
|
|
334
|
+
raw Valkyrie ids are not a supported input.
|
|
335
|
+
|
|
275
336
|
## End-to-end example
|
|
276
337
|
|
|
277
338
|
JSON responses come back as `AtlasRb::Mash` (a `Hashie::Mash` subclass), so
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AtlasRb
|
|
4
|
+
# Session-scoped AuditEvent emit — record an audit event that is **not**
|
|
5
|
+
# tied to a resource write.
|
|
6
|
+
#
|
|
7
|
+
# ## Why this exists
|
|
8
|
+
#
|
|
9
|
+
# Every other AuditEvent in Atlas is a side effect of a resource mutation
|
|
10
|
+
# (`create`, `update`, `tombstone`, …) and is read back per-resource via
|
|
11
|
+
# {AtlasRb::Resource.history} (`GET /resources/<id>/history`). Some
|
|
12
|
+
# auditable events have **no resource to hang on**: impersonation sessions.
|
|
13
|
+
# An admin starting/ending an acting-as or view-as session is a security
|
|
14
|
+
# event worth recording, but view-as performs no writes at all, and the
|
|
15
|
+
# session lifecycle lives entirely in the calling application (a cookie),
|
|
16
|
+
# so there is no resource mutation to attach the event to.
|
|
17
|
+
#
|
|
18
|
+
# This binding wraps Atlas's `POST /audit_events` emit endpoint, which
|
|
19
|
+
# writes an AuditEvent with a **null `resource_id`** (a session-scoped
|
|
20
|
+
# event). The recorded principals are passed explicitly in the body rather
|
|
21
|
+
# than inferred from request headers, so the call is self-describing and
|
|
22
|
+
# does not depend on the ambient {AtlasRb.config} identity being intact at
|
|
23
|
+
# emit time (it may not be — e.g. an `impersonation_ended` emit fires as
|
|
24
|
+
# the session is being torn down).
|
|
25
|
+
#
|
|
26
|
+
# ## Authorization
|
|
27
|
+
#
|
|
28
|
+
# The endpoint is system-token + admin-gated. The request authenticates via
|
|
29
|
+
# the standard {FaradayHelper#connection} — the `Authorization: Bearer`
|
|
30
|
+
# token plus a `User: NUID <admin>` header pinned to `actor_nuid` — and
|
|
31
|
+
# Atlas gates the emit to admin operators. `actor_nuid` is passed
|
|
32
|
+
# explicitly (rather than left to {AtlasRb.config}.default_nuid) so the
|
|
33
|
+
# admin gate holds even when the ambient session identity is mid-teardown,
|
|
34
|
+
# as it is for an `impersonation_ended` emit. The same `actor_nuid` is the
|
|
35
|
+
# principal recorded on the event.
|
|
36
|
+
#
|
|
37
|
+
# @example Recording the start of an acting-as session (from Cerberus)
|
|
38
|
+
# AtlasRb::AuditEvent.emit(
|
|
39
|
+
# action: "impersonation_started",
|
|
40
|
+
# actor_nuid: Current.nuid, # the admin
|
|
41
|
+
# on_behalf_of_nuid: target_nuid, # the impersonated user
|
|
42
|
+
# mode: "acting_as"
|
|
43
|
+
# )
|
|
44
|
+
#
|
|
45
|
+
# @example Recording the end of a view-as session
|
|
46
|
+
# AtlasRb::AuditEvent.emit(
|
|
47
|
+
# action: "impersonation_ended",
|
|
48
|
+
# actor_nuid: admin_nuid,
|
|
49
|
+
# on_behalf_of_nuid: target_nuid,
|
|
50
|
+
# mode: "view_as"
|
|
51
|
+
# )
|
|
52
|
+
class AuditEvent
|
|
53
|
+
extend AtlasRb::FaradayHelper
|
|
54
|
+
|
|
55
|
+
# Atlas REST endpoint for the session-scoped emit.
|
|
56
|
+
# @api private
|
|
57
|
+
ROUTE = "/audit_events"
|
|
58
|
+
|
|
59
|
+
# Emit a session-scoped AuditEvent (one with no resource scope).
|
|
60
|
+
#
|
|
61
|
+
# Wraps `POST /audit_events`. The principals and metadata travel in the
|
|
62
|
+
# JSON body — not in request headers — so the recorded event does not
|
|
63
|
+
# depend on the ambient {AtlasRb.config} identity. The request still
|
|
64
|
+
# authenticates as the configured caller (system token + the admin
|
|
65
|
+
# `User:` header); Atlas admin-gates the endpoint server-side.
|
|
66
|
+
#
|
|
67
|
+
# Atlas stamps `occurred_at` server-side, so it is not part of the body.
|
|
68
|
+
#
|
|
69
|
+
# Authorization errors (`401` / `403`) are intentionally **not** caught
|
|
70
|
+
# here — they surface as raw Faraday responses for the calling
|
|
71
|
+
# application's rescue layer to translate, matching
|
|
72
|
+
# {AtlasRb::Resource.history}.
|
|
73
|
+
#
|
|
74
|
+
# @param action [String] the audit action, e.g. `"impersonation_started"`
|
|
75
|
+
# or `"impersonation_ended"`.
|
|
76
|
+
# @param actor_nuid [String] NUID of the principal performing the action
|
|
77
|
+
# (the admin, in the impersonation flow).
|
|
78
|
+
# @param on_behalf_of_nuid [String, nil] NUID of the attribution target
|
|
79
|
+
# (the impersonated user). Omitted from the body when `nil`.
|
|
80
|
+
# @param mode [String, nil] session mode, e.g. `"acting_as"` or
|
|
81
|
+
# `"view_as"`. Omitted from the body when `nil` so the binding can also
|
|
82
|
+
# carry future, mode-less session events.
|
|
83
|
+
# @param payload [Hash] optional free-form metadata to record alongside
|
|
84
|
+
# the event. Omitted from the body when empty.
|
|
85
|
+
# @return [AtlasRb::Mash] the parsed envelope returned by
|
|
86
|
+
# `POST /audit_events` (the created event).
|
|
87
|
+
#
|
|
88
|
+
# @example
|
|
89
|
+
# AtlasRb::AuditEvent.emit(
|
|
90
|
+
# action: "impersonation_started",
|
|
91
|
+
# actor_nuid: "000000001",
|
|
92
|
+
# on_behalf_of_nuid: "000000123",
|
|
93
|
+
# mode: "acting_as"
|
|
94
|
+
# )
|
|
95
|
+
def self.emit(action:, actor_nuid:, on_behalf_of_nuid: nil, mode: nil, payload: {})
|
|
96
|
+
body = {
|
|
97
|
+
action: action,
|
|
98
|
+
actor_nuid: actor_nuid,
|
|
99
|
+
on_behalf_of_nuid: on_behalf_of_nuid,
|
|
100
|
+
mode: mode,
|
|
101
|
+
payload: (payload unless payload.nil? || payload.empty?)
|
|
102
|
+
}.compact
|
|
103
|
+
|
|
104
|
+
AtlasRb::Mash.new(JSON.parse(
|
|
105
|
+
connection({}, actor_nuid).post(ROUTE, JSON.dump(body))&.body
|
|
106
|
+
))
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
data/lib/atlas_rb/resource.rb
CHANGED
|
@@ -47,6 +47,44 @@ module AtlasRb
|
|
|
47
47
|
"resource" => result.first[1])
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
# Resolve many resources by NOID in a single round-trip.
|
|
51
|
+
#
|
|
52
|
+
# Wraps Atlas's `POST /resources/find_many`, which returns one lightweight
|
|
53
|
+
# digest per resolvable resource — `{ "id", "noid", "klass", "title",
|
|
54
|
+
# "thumbnail", "tombstoned" }` — rather than full typed payloads. Use it
|
|
55
|
+
# anywhere a set of ids would otherwise be resolved with a `find`-per-id
|
|
56
|
+
# fan-out (breadcrumb chains, linked-member lists, load-destination
|
|
57
|
+
# pickers): one HTTP call instead of N.
|
|
58
|
+
#
|
|
59
|
+
# The ids travel in the request **body**, so the list is not bounded by
|
|
60
|
+
# URL length. The result is **unordered** and **may be shorter than the
|
|
61
|
+
# input** — unresolvable ids are dropped silently, and tombstoned
|
|
62
|
+
# resources come back flagged (`"tombstoned" => true`) rather than
|
|
63
|
+
# omitted. Index the result by `"noid"`; do not assume positional
|
|
64
|
+
# correspondence with `ids`.
|
|
65
|
+
#
|
|
66
|
+
# @param ids [Array<String>] resource NOIDs to resolve. (Raw Valkyrie ids
|
|
67
|
+
# are not a supported input — the endpoint resolves alternate ids only.)
|
|
68
|
+
# @param nuid [String, nil] optional acting user's NUID, forwarded as the
|
|
69
|
+
# `User:` header. Required for cerberus-token requests; legacy bearer
|
|
70
|
+
# tokens still resolve without it.
|
|
71
|
+
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
72
|
+
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
73
|
+
# omitted.
|
|
74
|
+
# @return [Array<AtlasRb::Mash>] one digest Mash per resolved resource
|
|
75
|
+
# (dot- or string-keyed access); empty when nothing resolved.
|
|
76
|
+
#
|
|
77
|
+
# @example Resolve a set of collection titles in one call
|
|
78
|
+
# nodes = AtlasRb::Resource.find_many(["col-456", "col-457", "missing"])
|
|
79
|
+
# by_noid = nodes.index_by { |n| n["noid"] }
|
|
80
|
+
# by_noid["col-456"].title # => "Some Collection"
|
|
81
|
+
def self.find_many(ids, nuid: nil, on_behalf_of: nil)
|
|
82
|
+
JSON.parse(
|
|
83
|
+
connection({}, nuid, on_behalf_of: on_behalf_of)
|
|
84
|
+
.post('/resources/find_many', JSON.dump(ids: Array(ids)))&.body
|
|
85
|
+
).map { |node| AtlasRb::Mash.new(node) }
|
|
86
|
+
end
|
|
87
|
+
|
|
50
88
|
# Validate a MODS XML document against Atlas's schema *without* persisting it.
|
|
51
89
|
#
|
|
52
90
|
# Useful for surfacing validation errors in UIs before the user commits.
|
data/lib/atlas_rb.rb
CHANGED
|
@@ -23,6 +23,7 @@ require_relative "atlas_rb/admin/work"
|
|
|
23
23
|
require_relative "atlas_rb/admin/collection"
|
|
24
24
|
require_relative "atlas_rb/admin/community"
|
|
25
25
|
require_relative "atlas_rb/system/user"
|
|
26
|
+
require_relative "atlas_rb/audit_event"
|
|
26
27
|
|
|
27
28
|
# Ruby client for the Atlas API — Northeastern University's institutional
|
|
28
29
|
# digital repository.
|
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.3.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-06-
|
|
11
|
+
date: 2026-06-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -119,6 +119,7 @@ files:
|
|
|
119
119
|
- lib/atlas_rb/admin/collection.rb
|
|
120
120
|
- lib/atlas_rb/admin/community.rb
|
|
121
121
|
- lib/atlas_rb/admin/work.rb
|
|
122
|
+
- lib/atlas_rb/audit_event.rb
|
|
122
123
|
- lib/atlas_rb/authentication.rb
|
|
123
124
|
- lib/atlas_rb/blob.rb
|
|
124
125
|
- lib/atlas_rb/collection.rb
|