atlas_rb 0.0.101 → 1.0.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 +131 -0
- data/Gemfile.lock +1 -1
- data/README.md +92 -6
- data/lib/atlas_rb/admin/collection.rb +54 -0
- data/lib/atlas_rb/admin/community.rb +54 -0
- data/lib/atlas_rb/admin/work.rb +66 -0
- data/lib/atlas_rb/admin.rb +29 -0
- data/lib/atlas_rb/blob.rb +30 -10
- data/lib/atlas_rb/collection.rb +50 -48
- data/lib/atlas_rb/community.rb +50 -48
- data/lib/atlas_rb/configuration.rb +48 -0
- data/lib/atlas_rb/delegate.rb +7 -2
- data/lib/atlas_rb/faraday_helper.rb +70 -11
- data/lib/atlas_rb/file_set.rb +24 -8
- data/lib/atlas_rb/resource.rb +20 -6
- data/lib/atlas_rb/system/user.rb +70 -0
- data/lib/atlas_rb/work.rb +64 -54
- data/lib/atlas_rb.rb +39 -3
- metadata +9 -3
- data/lib/atlas_rb/user.rb +0 -44
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7f8b62042df9f4d715469ba7cf23dcf352485552aa9d73c55290312c3055800c
|
|
4
|
+
data.tar.gz: d435ed7a4fb953f2d82dd6f304232731802b2e8fe68a5e055fecd51e972c560a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3dffc895e6bee5bce6dbe907488a72a240c55b7d5ae5a506bff61d81a451a97490090d576c575fa07cf82ffbaf023109a9938aa66adf872e892b3a2fc37f3e13
|
|
7
|
+
data.tar.gz: 7076d8304048de5f20b8560875fbaddcac8ab29a4c3cf3d37ac54b95c3bac4cb6aff56bc503967ec3a5e41289bf4173055b9d8ced278ac9b63849231e0e09ea2
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.0
|
|
1
|
+
1.0.0
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.0 — major restructure: namespace gradient + ambient identity
|
|
4
|
+
|
|
5
|
+
This release reshapes the gem's API surface. Downstream consumers
|
|
6
|
+
(Cerberus) need to update call sites — see the migration section
|
|
7
|
+
below.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`AtlasRb.configure { |c| ... }`** — configurable defaults for
|
|
12
|
+
ambient identity:
|
|
13
|
+
- `config.default_nuid` — callable invoked when a resource method
|
|
14
|
+
is called without an explicit `nuid:` kwarg. Lets host apps
|
|
15
|
+
register `-> { Current.nuid }` once instead of threading
|
|
16
|
+
`nuid: Current.nuid` at every call site.
|
|
17
|
+
- `config.default_on_behalf_of` — callable for the
|
|
18
|
+
`On-Behalf-Of:` header, used by acting-as / view-as flows.
|
|
19
|
+
- **`on_behalf_of:` kwarg** on every resource method that already
|
|
20
|
+
took `nuid:`. Sent as the `On-Behalf-Of: NUID <nuid>` header when
|
|
21
|
+
set. Falls through to `config.default_on_behalf_of` when omitted.
|
|
22
|
+
- **`AtlasRb::Admin::*` namespace** — destructive lifecycle ops:
|
|
23
|
+
- `AtlasRb::Admin::Work.destroy` / `.restore`
|
|
24
|
+
- `AtlasRb::Admin::Collection.destroy` / `.restore`
|
|
25
|
+
- `AtlasRb::Admin::Community.destroy` / `.restore`
|
|
26
|
+
|
|
27
|
+
Every `destroy` requires `confirm: :i_understand`. Missing or
|
|
28
|
+
wrong value raises `ArgumentError` before any wire request.
|
|
29
|
+
- **`AtlasRb::System::*` namespace** — system-context calls:
|
|
30
|
+
- `AtlasRb::System::User.find_or_create` (moved from
|
|
31
|
+
`AtlasRb::User.find_or_create`).
|
|
32
|
+
- `AtlasRb::System::NUID` constant — the seeded `:system` fixture's
|
|
33
|
+
NUID (`"000000000"`).
|
|
34
|
+
- **`FaradayHelper#system_connection`** — Faraday factory that
|
|
35
|
+
authenticates with `Rails.application.credentials.atlas_system_token`
|
|
36
|
+
and the system NUID. Never consults the configured defaults. Used
|
|
37
|
+
exclusively by `AtlasRb::System::*`.
|
|
38
|
+
|
|
39
|
+
### Removed (breaking)
|
|
40
|
+
|
|
41
|
+
- `AtlasRb::Work.destroy`, `AtlasRb::Work.restore` → move to
|
|
42
|
+
`AtlasRb::Admin::Work`.
|
|
43
|
+
- `AtlasRb::Collection.destroy`, `AtlasRb::Collection.restore` →
|
|
44
|
+
move to `AtlasRb::Admin::Collection`.
|
|
45
|
+
- `AtlasRb::Community.destroy`, `AtlasRb::Community.restore` →
|
|
46
|
+
move to `AtlasRb::Admin::Community`.
|
|
47
|
+
- `AtlasRb::User.find_or_create` → moves to
|
|
48
|
+
`AtlasRb::System::User.find_or_create`. The class
|
|
49
|
+
`AtlasRb::User` is gone.
|
|
50
|
+
|
|
51
|
+
### Changed
|
|
52
|
+
|
|
53
|
+
- `Work.tombstone`, `Collection.tombstone`, `Community.tombstone`
|
|
54
|
+
relax `nuid:` from required-kwarg to `nuid: nil`. The fall-through
|
|
55
|
+
resolution in `FaradayHelper#connection` handles the lookup. Atlas
|
|
56
|
+
still requires a real NUID on the wire for tombstone audit; if
|
|
57
|
+
neither the call site nor the configured default supplies one, the
|
|
58
|
+
request will hit Atlas without a `User:` header and Atlas will
|
|
59
|
+
reject it.
|
|
60
|
+
|
|
61
|
+
### Migration
|
|
62
|
+
|
|
63
|
+
For consumers (Cerberus piece 6):
|
|
64
|
+
|
|
65
|
+
1. Register the ambient defaults in `config/initializers/atlas_rb.rb`:
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
AtlasRb.configure do |config|
|
|
69
|
+
config.default_nuid = -> { Current.nuid }
|
|
70
|
+
config.default_on_behalf_of = -> { Current.on_behalf_of }
|
|
71
|
+
end
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
2. Drop `nuid: Current.nuid` from regular call sites:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
# Before
|
|
78
|
+
AtlasRb::Work.find(id, nuid: Current.nuid)
|
|
79
|
+
AtlasRb::Blob.create(work_id, path, name, nuid: Current.nuid)
|
|
80
|
+
|
|
81
|
+
# After
|
|
82
|
+
AtlasRb::Work.find(id)
|
|
83
|
+
AtlasRb::Blob.create(work_id, path, name)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Sites that need a *different* NUID than `Current.nuid` keep their
|
|
87
|
+
explicit kwarg — caller value always wins.
|
|
88
|
+
|
|
89
|
+
3. Rewrite destructive call sites:
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
# Before
|
|
93
|
+
AtlasRb::Work.destroy(id, nuid: Current.nuid)
|
|
94
|
+
AtlasRb::Work.restore(id, nuid: Current.nuid)
|
|
95
|
+
|
|
96
|
+
# After
|
|
97
|
+
AtlasRb::Admin::Work.destroy(id, confirm: :i_understand)
|
|
98
|
+
AtlasRb::Admin::Work.restore(id)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
4. Rewrite the SSO callback's user-provisioning call:
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
# Before
|
|
105
|
+
AtlasRb::User.find_or_create(nuid: ..., groups: ...)
|
|
106
|
+
|
|
107
|
+
# After
|
|
108
|
+
AtlasRb::System::User.find_or_create(nuid: ..., groups: ...)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
5. Add the system token to encrypted credentials:
|
|
112
|
+
|
|
113
|
+
```yaml
|
|
114
|
+
# config/credentials.yml.enc
|
|
115
|
+
atlas_system_token: <value Atlas's require_auth recognises as :system>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
This is paired on the Atlas side with
|
|
119
|
+
`Rails.application.credentials.system_token` (Atlas 0.6.20+).
|
|
120
|
+
|
|
121
|
+
## 0.0.101
|
|
122
|
+
|
|
123
|
+
- Threaded `nuid:` through the remaining gaps in `Work` / `Collection`
|
|
124
|
+
/ `Community` / `FileSet` / `Delegate`.
|
|
125
|
+
- Fixed `multipart({})` bug at `file_set.rb:83` — the literal `{}`
|
|
126
|
+
bound to the `nuid` positional arg, so the gem emitted
|
|
127
|
+
`User: NUID {}` on the wire for `FileSet.update`.
|
|
128
|
+
|
|
129
|
+
## 0.0.100 and earlier
|
|
130
|
+
|
|
131
|
+
See `git log` for pre-1.0 history.
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -23,22 +23,77 @@ Then `bundle install`, or install standalone with `gem install atlas_rb`.
|
|
|
23
23
|
|
|
24
24
|
## Configuration
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
### Environment variables
|
|
27
|
+
|
|
28
|
+
Every regular-path request reads two environment variables:
|
|
27
29
|
|
|
28
30
|
| Variable | Purpose |
|
|
29
31
|
|---------------|---------------------------------------------------------------|
|
|
30
32
|
| `ATLAS_URL` | Base URL of the Atlas API (e.g. `https://atlas.example.edu`). |
|
|
31
|
-
| `ATLAS_TOKEN` | Bearer token used in the `Authorization` header.
|
|
32
|
-
|
|
33
|
-
User-scoped calls (currently only `AtlasRb::Authentication`) additionally
|
|
34
|
-
accept an NUID — the Northeastern University ID — which is forwarded in a
|
|
35
|
-
`User: NUID <nuid>` header.
|
|
33
|
+
| `ATLAS_TOKEN` | Bearer token used in the `Authorization` header. |
|
|
36
34
|
|
|
37
35
|
```ruby
|
|
38
36
|
ENV["ATLAS_URL"] = "https://atlas.example.edu"
|
|
39
37
|
ENV["ATLAS_TOKEN"] = "..."
|
|
40
38
|
```
|
|
41
39
|
|
|
40
|
+
### Ambient identity (`default_nuid` / `default_on_behalf_of`)
|
|
41
|
+
|
|
42
|
+
Every resource method that talks to Atlas accepts a `nuid:` kwarg (the
|
|
43
|
+
acting user) and an `on_behalf_of:` kwarg (the user the call is being
|
|
44
|
+
made *for*, used by acting-as / view-as flows). Both are forwarded as
|
|
45
|
+
`User: NUID <nuid>` and `On-Behalf-Of: NUID <nuid>` headers
|
|
46
|
+
respectively.
|
|
47
|
+
|
|
48
|
+
Rather than threading them at every call site, register callables
|
|
49
|
+
once on app boot and let the gem read them as defaults:
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
# config/initializers/atlas_rb.rb (Rails)
|
|
53
|
+
AtlasRb.configure do |config|
|
|
54
|
+
config.default_nuid = -> { Current.nuid }
|
|
55
|
+
config.default_on_behalf_of = -> { Current.on_behalf_of }
|
|
56
|
+
end
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The lambdas run **at request time** in whatever thread / fiber is
|
|
60
|
+
making the call, so they pick up per-request `Current.*` values that
|
|
61
|
+
`ApplicationController` set up via Devise + Rails 7's
|
|
62
|
+
`ActiveSupport::CurrentAttributes`. Background jobs work the same way
|
|
63
|
+
(ActiveJob ↔ CurrentAttributes integration restores the values on
|
|
64
|
+
`perform`).
|
|
65
|
+
|
|
66
|
+
Caller-passed kwargs always win over the configured defaults:
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
# Uses Current.nuid:
|
|
70
|
+
AtlasRb::Work.find("w-789")
|
|
71
|
+
|
|
72
|
+
# Uses "X" — explicit kwarg overrides the default:
|
|
73
|
+
AtlasRb::Work.find("w-789", nuid: "X")
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
If neither the call site nor the registered default supplies a value,
|
|
77
|
+
no header is sent (legacy bearer-only path preserved).
|
|
78
|
+
|
|
79
|
+
### System-path credentials
|
|
80
|
+
|
|
81
|
+
Calls under `AtlasRb::System::*` (currently just SSO user provisioning)
|
|
82
|
+
authenticate as the seeded Atlas `:system` fixture, not as a real user.
|
|
83
|
+
They use a **separate** bearer token, looked up from
|
|
84
|
+
`Rails.application.credentials.atlas_system_token`. Storing it in
|
|
85
|
+
encrypted credentials rather than `ENV` halves the blast radius of a
|
|
86
|
+
`.env` leak — the user token and the system token can't both leak
|
|
87
|
+
through the same channel.
|
|
88
|
+
|
|
89
|
+
```yaml
|
|
90
|
+
# config/credentials.yml.enc (Cerberus side)
|
|
91
|
+
atlas_system_token: <token-Atlas-side-recognises-as-:system>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The system NUID itself is hardcoded as `AtlasRb::System::NUID =
|
|
95
|
+
"000000000"`, matching Atlas's seeded `:system` fixture row.
|
|
96
|
+
|
|
42
97
|
## Resource hierarchy
|
|
43
98
|
|
|
44
99
|
```
|
|
@@ -60,6 +115,37 @@ Community → Collection → Work
|
|
|
60
115
|
| `AtlasRb::Resource` | Generic resolver and permissions lookup. |
|
|
61
116
|
| `AtlasRb::Reset` | Test-only — wipes Atlas state via `GET /reset`. |
|
|
62
117
|
|
|
118
|
+
## Namespace gradient: regular / Admin / System
|
|
119
|
+
|
|
120
|
+
Operations are split across three namespaces, calibrated to the blast
|
|
121
|
+
radius and the kind of authentication they need:
|
|
122
|
+
|
|
123
|
+
| Namespace | What it does | Auth | Friction |
|
|
124
|
+
|----------------------|------------------------------------------------------------------------------------|-----------------------------------------------------|---------------------------------------|
|
|
125
|
+
| `AtlasRb::*` | Regular CRUD (find / list / create / update / tombstone / metadata, etc.) | User token (`ATLAS_TOKEN`) + acting user's NUID | None — these are the daily-use paths. |
|
|
126
|
+
| `AtlasRb::Admin::*` | Hard delete (`destroy`) and un-tombstone (`restore`) for Work / Collection / Community. | Same as regular — a real operator is acting. | `destroy` requires `confirm: :i_understand`. |
|
|
127
|
+
| `AtlasRb::System::*` | System-context provisioning (currently just SSO user find-or-create). | System token (`Rails.application.credentials.atlas_system_token`) + `User: NUID 000000000`. | The namespace itself is the marker — there is no way to call these as a non-system principal. |
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
# Regular daily use — picks up Current.nuid via the configured default:
|
|
131
|
+
AtlasRb::Work.find("w-789")
|
|
132
|
+
AtlasRb::Work.tombstone("w-789") # withdrawal (reversible)
|
|
133
|
+
|
|
134
|
+
# Operator-only, with a friction marker:
|
|
135
|
+
AtlasRb::Admin::Work.destroy("w-789", confirm: :i_understand)
|
|
136
|
+
AtlasRb::Admin::Work.restore("w-789")
|
|
137
|
+
|
|
138
|
+
# System-only — authenticates as Atlas's :system fixture:
|
|
139
|
+
AtlasRb::System::User.find_or_create(
|
|
140
|
+
nuid: "001234567",
|
|
141
|
+
groups: ["northeastern:staff", "drs:editors"]
|
|
142
|
+
)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The `AtlasRb::System::*` path never consults `AtlasRb.config.default_nuid`
|
|
146
|
+
or `default_on_behalf_of` — there is no ambient user context on system
|
|
147
|
+
calls.
|
|
148
|
+
|
|
63
149
|
### A note on `create` argument shapes
|
|
64
150
|
|
|
65
151
|
The CRUD-twin classes look the same but pass different parent IDs:
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AtlasRb
|
|
4
|
+
module Admin
|
|
5
|
+
# Destructive lifecycle operations on a {AtlasRb::Collection}.
|
|
6
|
+
#
|
|
7
|
+
# See {AtlasRb::Admin} for the rationale behind the namespace and the
|
|
8
|
+
# `confirm: :i_understand` friction marker.
|
|
9
|
+
class Collection
|
|
10
|
+
extend AtlasRb::FaradayHelper
|
|
11
|
+
|
|
12
|
+
# Atlas REST endpoint prefix.
|
|
13
|
+
# @api private
|
|
14
|
+
ROUTE = "/collections/"
|
|
15
|
+
|
|
16
|
+
# Hard-delete a Collection.
|
|
17
|
+
#
|
|
18
|
+
# Unrecoverable — prefer {AtlasRb::Collection.tombstone} for
|
|
19
|
+
# user-visible withdrawal. Operator-only.
|
|
20
|
+
#
|
|
21
|
+
# @param id [String] the Collection ID.
|
|
22
|
+
# @param confirm [Symbol] must be `:i_understand`. Any other value
|
|
23
|
+
# raises `ArgumentError`.
|
|
24
|
+
# @param nuid [String, nil] optional acting user's NUID.
|
|
25
|
+
# @param on_behalf_of [String, nil] optional `On-Behalf-Of` NUID.
|
|
26
|
+
# @return [Faraday::Response] the raw delete response.
|
|
27
|
+
# @raise [ArgumentError] if `confirm:` is missing or not the
|
|
28
|
+
# sentinel value.
|
|
29
|
+
#
|
|
30
|
+
# @example
|
|
31
|
+
# AtlasRb::Admin::Collection.destroy("col-456", confirm: :i_understand)
|
|
32
|
+
def self.destroy(id, confirm:, nuid: nil, on_behalf_of: nil)
|
|
33
|
+
unless confirm == :i_understand
|
|
34
|
+
raise ArgumentError,
|
|
35
|
+
"AtlasRb::Admin::Collection.destroy requires confirm: :i_understand"
|
|
36
|
+
end
|
|
37
|
+
connection({}, nuid, on_behalf_of: on_behalf_of).delete(ROUTE + id)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Restore a previously-tombstoned Collection.
|
|
41
|
+
#
|
|
42
|
+
# @param id [String] the Collection ID.
|
|
43
|
+
# @param nuid [String, nil] optional acting user's NUID.
|
|
44
|
+
# @param on_behalf_of [String, nil] optional `On-Behalf-Of` NUID.
|
|
45
|
+
# @return [Faraday::Response] the raw response.
|
|
46
|
+
#
|
|
47
|
+
# @example
|
|
48
|
+
# AtlasRb::Admin::Collection.restore("col-456")
|
|
49
|
+
def self.restore(id, nuid: nil, on_behalf_of: nil)
|
|
50
|
+
connection({}, nuid, on_behalf_of: on_behalf_of).post(ROUTE + id + '/restore')
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AtlasRb
|
|
4
|
+
module Admin
|
|
5
|
+
# Destructive lifecycle operations on a {AtlasRb::Community}.
|
|
6
|
+
#
|
|
7
|
+
# See {AtlasRb::Admin} for the rationale behind the namespace and the
|
|
8
|
+
# `confirm: :i_understand` friction marker.
|
|
9
|
+
class Community
|
|
10
|
+
extend AtlasRb::FaradayHelper
|
|
11
|
+
|
|
12
|
+
# Atlas REST endpoint prefix.
|
|
13
|
+
# @api private
|
|
14
|
+
ROUTE = "/communities/"
|
|
15
|
+
|
|
16
|
+
# Hard-delete a Community.
|
|
17
|
+
#
|
|
18
|
+
# Unrecoverable — prefer {AtlasRb::Community.tombstone} for
|
|
19
|
+
# user-visible withdrawal. Operator-only.
|
|
20
|
+
#
|
|
21
|
+
# @param id [String] the Community ID.
|
|
22
|
+
# @param confirm [Symbol] must be `:i_understand`. Any other value
|
|
23
|
+
# raises `ArgumentError`.
|
|
24
|
+
# @param nuid [String, nil] optional acting user's NUID.
|
|
25
|
+
# @param on_behalf_of [String, nil] optional `On-Behalf-Of` NUID.
|
|
26
|
+
# @return [Faraday::Response] the raw delete response.
|
|
27
|
+
# @raise [ArgumentError] if `confirm:` is missing or not the
|
|
28
|
+
# sentinel value.
|
|
29
|
+
#
|
|
30
|
+
# @example
|
|
31
|
+
# AtlasRb::Admin::Community.destroy("c-123", confirm: :i_understand)
|
|
32
|
+
def self.destroy(id, confirm:, nuid: nil, on_behalf_of: nil)
|
|
33
|
+
unless confirm == :i_understand
|
|
34
|
+
raise ArgumentError,
|
|
35
|
+
"AtlasRb::Admin::Community.destroy requires confirm: :i_understand"
|
|
36
|
+
end
|
|
37
|
+
connection({}, nuid, on_behalf_of: on_behalf_of).delete(ROUTE + id)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Restore a previously-tombstoned Community.
|
|
41
|
+
#
|
|
42
|
+
# @param id [String] the Community ID.
|
|
43
|
+
# @param nuid [String, nil] optional acting user's NUID.
|
|
44
|
+
# @param on_behalf_of [String, nil] optional `On-Behalf-Of` NUID.
|
|
45
|
+
# @return [Faraday::Response] the raw response.
|
|
46
|
+
#
|
|
47
|
+
# @example
|
|
48
|
+
# AtlasRb::Admin::Community.restore("c-123")
|
|
49
|
+
def self.restore(id, nuid: nil, on_behalf_of: nil)
|
|
50
|
+
connection({}, nuid, on_behalf_of: on_behalf_of).post(ROUTE + id + '/restore')
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AtlasRb
|
|
4
|
+
module Admin
|
|
5
|
+
# Destructive lifecycle operations on a {AtlasRb::Work}.
|
|
6
|
+
#
|
|
7
|
+
# See {AtlasRb::Admin} for the rationale behind the namespace and the
|
|
8
|
+
# `confirm: :i_understand` friction marker.
|
|
9
|
+
class Work
|
|
10
|
+
extend AtlasRb::FaradayHelper
|
|
11
|
+
|
|
12
|
+
# Atlas REST endpoint prefix.
|
|
13
|
+
# @api private
|
|
14
|
+
ROUTE = "/works/"
|
|
15
|
+
|
|
16
|
+
# Hard-delete a Work.
|
|
17
|
+
#
|
|
18
|
+
# Removes the Work, its FileSets, and their Blobs from Atlas
|
|
19
|
+
# storage. Unrecoverable — prefer {AtlasRb::Work.tombstone} for
|
|
20
|
+
# the user-visible withdrawal path. This is operator-only.
|
|
21
|
+
#
|
|
22
|
+
# @param id [String] the Work ID.
|
|
23
|
+
# @param confirm [Symbol] must be `:i_understand`. Any other value
|
|
24
|
+
# (including the kwarg being omitted) raises `ArgumentError`.
|
|
25
|
+
# @param nuid [String, nil] optional acting user's NUID. Falls
|
|
26
|
+
# through to {AtlasRb.config}.default_nuid when omitted.
|
|
27
|
+
# @param on_behalf_of [String, nil] optional NUID for the
|
|
28
|
+
# `On-Behalf-Of` header. Falls through to
|
|
29
|
+
# {AtlasRb.config}.default_on_behalf_of when omitted.
|
|
30
|
+
# @return [Faraday::Response] the raw delete response.
|
|
31
|
+
# @raise [ArgumentError] if `confirm:` is missing or not the
|
|
32
|
+
# sentinel value.
|
|
33
|
+
#
|
|
34
|
+
# @example
|
|
35
|
+
# AtlasRb::Admin::Work.destroy("w-789", confirm: :i_understand)
|
|
36
|
+
def self.destroy(id, confirm:, nuid: nil, on_behalf_of: nil)
|
|
37
|
+
unless confirm == :i_understand
|
|
38
|
+
raise ArgumentError,
|
|
39
|
+
"AtlasRb::Admin::Work.destroy requires confirm: :i_understand"
|
|
40
|
+
end
|
|
41
|
+
connection({}, nuid, on_behalf_of: on_behalf_of).delete(ROUTE + id)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Restore a previously-tombstoned Work.
|
|
45
|
+
#
|
|
46
|
+
# Reverses a withdrawal: search and show pages stop returning a
|
|
47
|
+
# withdrawn stub. Operator-only; typically driven from a Rails
|
|
48
|
+
# console or a future admin panel after the library has decided a
|
|
49
|
+
# withdrawn Work should come back.
|
|
50
|
+
#
|
|
51
|
+
# @param id [String] the Work ID.
|
|
52
|
+
# @param nuid [String, nil] optional acting user's NUID. Falls
|
|
53
|
+
# through to {AtlasRb.config}.default_nuid when omitted.
|
|
54
|
+
# @param on_behalf_of [String, nil] optional NUID for the
|
|
55
|
+
# `On-Behalf-Of` header. Falls through to
|
|
56
|
+
# {AtlasRb.config}.default_on_behalf_of when omitted.
|
|
57
|
+
# @return [Faraday::Response] the raw response.
|
|
58
|
+
#
|
|
59
|
+
# @example
|
|
60
|
+
# AtlasRb::Admin::Work.restore("w-789")
|
|
61
|
+
def self.restore(id, nuid: nil, on_behalf_of: nil)
|
|
62
|
+
connection({}, nuid, on_behalf_of: on_behalf_of).post(ROUTE + id + '/restore')
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AtlasRb
|
|
4
|
+
# Operator-only destructive lifecycle operations.
|
|
5
|
+
#
|
|
6
|
+
# Houses methods whose blast radius warrants more friction than the
|
|
7
|
+
# regular CRUD surface: `destroy` (hard delete — content and metadata
|
|
8
|
+
# are unrecoverable) and `restore` (un-tombstone — reverses a withdraw,
|
|
9
|
+
# typically driven from a Rails console session or a future admin UI).
|
|
10
|
+
#
|
|
11
|
+
# ## Why a separate namespace
|
|
12
|
+
#
|
|
13
|
+
# The class itself is the marker: `AtlasRb::Admin::Work.destroy(...)`
|
|
14
|
+
# is structurally distinct from `AtlasRb::Work.update(...)`. Mass-edits
|
|
15
|
+
# and code-search across a consumer codebase can quickly find every
|
|
16
|
+
# destructive call site by grepping `AtlasRb::Admin::`.
|
|
17
|
+
#
|
|
18
|
+
# ## `confirm: :i_understand`
|
|
19
|
+
#
|
|
20
|
+
# Every `destroy` method requires a `confirm: :i_understand` kwarg.
|
|
21
|
+
# Forgetting (or misspelling) it raises `ArgumentError` before any
|
|
22
|
+
# request goes out. The value is arbitrary — the point is that
|
|
23
|
+
# boilerplate-generated or copy-pasted call sites can't accidentally
|
|
24
|
+
# delete production data. `restore` does **not** require the marker;
|
|
25
|
+
# restoring tombstoned content is reversible (by tombstoning again)
|
|
26
|
+
# so the same friction isn't warranted.
|
|
27
|
+
module Admin
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/atlas_rb/blob.rb
CHANGED
|
@@ -20,14 +20,19 @@ module AtlasRb
|
|
|
20
20
|
# @param nuid [String, nil] optional acting user's NUID, forwarded as the
|
|
21
21
|
# `User:` header. Required for cerberus-token requests; legacy bearer
|
|
22
22
|
# tokens still resolve without it.
|
|
23
|
+
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
24
|
+
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
25
|
+
# omitted.
|
|
23
26
|
# @return [Hash] the `"blob"` object, already unwrapped — typically
|
|
24
27
|
# includes `"id"`, `"original_filename"`, `"size"`, and a download URL.
|
|
25
28
|
#
|
|
26
29
|
# @example
|
|
27
30
|
# AtlasRb::Blob.find("b-321")
|
|
28
31
|
# # => { "id" => "b-321", "original_filename" => "scan.pdf", ... }
|
|
29
|
-
def self.find(id, nuid: nil)
|
|
30
|
-
AtlasRb::Mash.new(JSON.parse(
|
|
32
|
+
def self.find(id, nuid: nil, on_behalf_of: nil)
|
|
33
|
+
AtlasRb::Mash.new(JSON.parse(
|
|
34
|
+
connection({}, nuid, on_behalf_of: on_behalf_of).get(ROUTE + id)&.body
|
|
35
|
+
))['blob']
|
|
31
36
|
end
|
|
32
37
|
|
|
33
38
|
# Stream the Blob's binary content through a caller-supplied block.
|
|
@@ -41,6 +46,9 @@ module AtlasRb
|
|
|
41
46
|
# @param nuid [String, nil] optional acting user's NUID, forwarded as the
|
|
42
47
|
# `User:` header. Required for cerberus-token requests; legacy bearer
|
|
43
48
|
# tokens still resolve without it.
|
|
49
|
+
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
50
|
+
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
51
|
+
# omitted.
|
|
44
52
|
# @yieldparam chunk [String] the next chunk of binary data.
|
|
45
53
|
# @return [Hash] the response headers from `GET /files/<id>/content`.
|
|
46
54
|
#
|
|
@@ -49,9 +57,9 @@ module AtlasRb
|
|
|
49
57
|
# headers = AtlasRb::Blob.content("b-321") { |chunk| f.write(chunk) }
|
|
50
58
|
# puts headers["content-type"]
|
|
51
59
|
# end
|
|
52
|
-
def self.content(id, nuid: nil, &chunk_handler)
|
|
60
|
+
def self.content(id, nuid: nil, on_behalf_of: nil, &chunk_handler)
|
|
53
61
|
headers = {}
|
|
54
|
-
connection({}, nuid).get("#{ROUTE}#{id}/content") do |req|
|
|
62
|
+
connection({}, nuid, on_behalf_of: on_behalf_of).get("#{ROUTE}#{id}/content") do |req|
|
|
55
63
|
req.options.on_data = proc do |chunk, _bytes_received, env|
|
|
56
64
|
headers = env.response_headers if headers.empty? && env
|
|
57
65
|
chunk_handler.call(chunk)
|
|
@@ -77,6 +85,9 @@ module AtlasRb
|
|
|
77
85
|
# @param nuid [String, nil] optional acting user's NUID, forwarded as the
|
|
78
86
|
# `User:` header. Required for cerberus-token requests; legacy bearer
|
|
79
87
|
# tokens still resolve without it.
|
|
88
|
+
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
89
|
+
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
90
|
+
# omitted.
|
|
80
91
|
# @return [Hash] the created `"blob"` payload, including its `"id"`.
|
|
81
92
|
#
|
|
82
93
|
# @example
|
|
@@ -87,7 +98,7 @@ module AtlasRb
|
|
|
87
98
|
# key = SecureRandom.uuid
|
|
88
99
|
# AtlasRb::Blob.create("w-789", "/tmp/upload.tmp", "thesis.pdf",
|
|
89
100
|
# idempotency_key: key)
|
|
90
|
-
def self.create(id, blob_path, original_filename, idempotency_key: nil, nuid: nil)
|
|
101
|
+
def self.create(id, blob_path, original_filename, idempotency_key: nil, nuid: nil, on_behalf_of: nil)
|
|
91
102
|
payload = { work_id: id,
|
|
92
103
|
original_filename: original_filename,
|
|
93
104
|
binary: Faraday::Multipart::FilePart.new(File.open(blob_path),
|
|
@@ -95,7 +106,8 @@ module AtlasRb
|
|
|
95
106
|
File.basename(blob_path)) }
|
|
96
107
|
|
|
97
108
|
AtlasRb::Mash.new(JSON.parse(
|
|
98
|
-
multipart(nuid, idempotency_key: idempotency_key)
|
|
109
|
+
multipart(nuid, on_behalf_of: on_behalf_of, idempotency_key: idempotency_key)
|
|
110
|
+
.post(ROUTE, payload)&.body
|
|
99
111
|
))['blob']
|
|
100
112
|
end
|
|
101
113
|
|
|
@@ -105,12 +117,15 @@ module AtlasRb
|
|
|
105
117
|
# @param nuid [String, nil] optional acting user's NUID, forwarded as the
|
|
106
118
|
# `User:` header. Required for cerberus-token requests; legacy bearer
|
|
107
119
|
# tokens still resolve without it.
|
|
120
|
+
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
121
|
+
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
122
|
+
# omitted.
|
|
108
123
|
# @return [Faraday::Response] the raw delete response.
|
|
109
124
|
#
|
|
110
125
|
# @example
|
|
111
126
|
# AtlasRb::Blob.destroy("b-321")
|
|
112
|
-
def self.destroy(id, nuid: nil)
|
|
113
|
-
connection({}, nuid).delete(ROUTE + id)
|
|
127
|
+
def self.destroy(id, nuid: nil, on_behalf_of: nil)
|
|
128
|
+
connection({}, nuid, on_behalf_of: on_behalf_of).delete(ROUTE + id)
|
|
114
129
|
end
|
|
115
130
|
|
|
116
131
|
# Replace the bytes of an existing Blob in-place.
|
|
@@ -124,15 +139,20 @@ module AtlasRb
|
|
|
124
139
|
# @param nuid [String, nil] optional acting user's NUID, forwarded as the
|
|
125
140
|
# `User:` header. Required for cerberus-token requests; legacy bearer
|
|
126
141
|
# tokens still resolve without it.
|
|
142
|
+
# @param on_behalf_of [String, nil] optional NUID for the `On-Behalf-Of`
|
|
143
|
+
# header. Falls through to {AtlasRb.config}.default_on_behalf_of when
|
|
144
|
+
# omitted.
|
|
127
145
|
# @return [Hash] the parsed JSON response from the patch.
|
|
128
146
|
#
|
|
129
147
|
# @example
|
|
130
148
|
# AtlasRb::Blob.update("b-321", "/tmp/revised.pdf")
|
|
131
|
-
def self.update(id, blob_path, nuid: nil)
|
|
149
|
+
def self.update(id, blob_path, nuid: nil, on_behalf_of: nil)
|
|
132
150
|
payload = { binary: Faraday::Multipart::FilePart.new(File.open(blob_path),
|
|
133
151
|
"application/octet-stream",
|
|
134
152
|
File.basename(blob_path)) }
|
|
135
|
-
AtlasRb::Mash.new(JSON.parse(
|
|
153
|
+
AtlasRb::Mash.new(JSON.parse(
|
|
154
|
+
multipart(nuid, on_behalf_of: on_behalf_of).patch(ROUTE + id, payload)&.body
|
|
155
|
+
))
|
|
136
156
|
end
|
|
137
157
|
end
|
|
138
158
|
end
|