atlas_rb 1.2.1 → 1.2.2

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: 30f433b53fc97128762eadbf01f12f975111c2b6e597469acc19831e3ef02bba
4
- data.tar.gz: 3604a876f6061a2a4bf45953abae93faca9b7fdae27a9b264ec5e10526acaa0c
3
+ metadata.gz: 29da2e41c70d17572c6c24002ba0ff25beb2ad6cdd940dee157a83133c86efaf
4
+ data.tar.gz: 9e16fed8e22867c37647a12de104135d78f57995ba5e6decd4929ac6a402bae7
5
5
  SHA512:
6
- metadata.gz: 62376e2b252c0269420ca89661bf7bb864b198f820076bd0c01210dc129adde111f218f2f33e7b3f88365a8b944cd5d2fda0758de050b9585c84ded5a1694309
7
- data.tar.gz: d498f94bff1f2e13fcdacbca9ca05dce38bbcfe80209de0f3339f31b69aff8e47492980079096f2f141748845aab30e271ba54fa19639368c005a37d1cc0a9d7
6
+ metadata.gz: 7ba5a0216a5cc5dacce4f6822078eb07d71aa4e14b31142c83dd823c0ec177efa92e1712eac0b235c125870611dcd4633817066c5ee70a98d6f85adbec614a7f
7
+ data.tar.gz: b176fc377d6d802e356c5f0a0fd487c6c899d4b5e323e3017eb773fd11e06dd8ebb415329a4c4e81aef5f55f0086602fc33a6cb82c4699709e0a3209c542a48d
data/.version CHANGED
@@ -1 +1 @@
1
- 1.2.1
1
+ 1.2.2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ### Added — `AtlasRb::AuditEvent.emit` (session-scoped audit events)
6
+
7
+ A new binding for Atlas's `POST /audit_events` endpoint, which records an
8
+ AuditEvent with a **null `resource_id`** — an event not tied to any
9
+ resource write. This is the gem's half of the impersonation audit trail
10
+ (acting-as / view-as): the session lifecycle lives entirely in the calling
11
+ application, and a view-as session performs no writes, so neither leaves a
12
+ per-resource event for `Resource.history` to surface.
13
+
14
+ ```ruby
15
+ AtlasRb::AuditEvent.emit(
16
+ action: "impersonation_started", # or "impersonation_ended"
17
+ actor_nuid: admin_nuid,
18
+ on_behalf_of_nuid: target_nuid,
19
+ mode: "acting_as" # or "view_as"
20
+ )
21
+ ```
22
+
23
+ - The recorded principals (`actor_nuid`, `on_behalf_of_nuid`), `mode`, and
24
+ an optional free-form `payload:` travel in the request **body**, not in
25
+ ambient headers — so the call is self-describing even when fired as a
26
+ session is being torn down (e.g. `impersonation_ended`).
27
+ - The request authenticates via the standard `connection` (system token),
28
+ with the `User: NUID` header pinned to `actor_nuid` so the server-side
29
+ admin gate holds regardless of ambient `Current` state.
30
+ - `on_behalf_of_nuid`, `mode`, and `payload` are omitted from the body when
31
+ blank, leaving room for future, mode-less session events. Atlas stamps
32
+ `occurred_at` server-side.
33
+ - Authorization errors (`401` / `403`) surface as raw Faraday responses,
34
+ matching `Resource.history`.
35
+
36
+ Depends on the matching Atlas-side `POST /audit_events` emit endpoint
37
+ (nullable resource scope, admin-gated); see the impersonation gap report.
38
+
3
39
  ## 1.2.1
4
40
 
5
41
  ### Added — typed errors for re-parent / linked-member rejections
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- atlas_rb (1.2.1)
4
+ atlas_rb (1.2.2)
5
5
  faraday (~> 2.7)
6
6
  faraday-follow_redirects (~> 0.3.0)
7
7
  faraday-multipart (~> 1)
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
@@ -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.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,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: atlas_rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.1
4
+ version: 1.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Cliff
@@ -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