1use crate::{
7 PublicUuid,
8 refs::{SessionSelector, StudyRef},
9};
10use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12
13pub const BROWSER_ACCESS_REQUEST_ERROR_TARGET_DATASTORE: &str = "browser_access_request.datastore";
14pub const BROWSER_ACCESS_REQUEST_ERROR_TARGET_DATASTORE_UNAVAILABLE: &str =
15 "browser_access_request.datastore_unavailable";
16pub const BROWSER_ACCESS_REQUEST_ERROR_TARGET_STUDY: &str = "browser_access_request.study";
17pub const BROWSER_ACCESS_REQUEST_ERROR_TARGET_REQUEST: &str = "browser_access_request.request";
18pub const BROWSER_ACCESS_REQUEST_ERROR_TARGET_INTENDED_USE: &str =
19 "browser_access_request.intended_use";
20pub const BROWSER_ACCESS_REQUEST_ERROR_TARGET_DUPLICATE: &str =
21 "browser_access_request.duplicate_pending";
22pub const BROWSER_ACCESS_REQUEST_ERROR_TARGET_WITHDRAWAL_STATE: &str =
23 "browser_access_request.withdrawal_state";
24pub const BROWSER_ACCESS_REQUEST_ERROR_TARGET_LIFECYCLE: &str = "browser_access_request.lifecycle";
25pub const BROWSER_ACCESS_REQUEST_ERROR_TARGET_SERVICE: &str = "browser_access_request.service";
26pub const BROWSER_CUSTODIAN_ACCESS_REQUEST_ERROR_TARGET_DATASTORE: &str =
27 "browser_custodian_access_request.datastore";
28pub const BROWSER_CUSTODIAN_ACCESS_REQUEST_ERROR_TARGET_SESSION: &str =
29 "browser_custodian_access_request.session";
30pub const BROWSER_CUSTODIAN_ACCESS_REQUEST_ERROR_TARGET_CUSTODIANSHIP: &str =
31 "browser_custodian_access_request.custodianship";
32pub const BROWSER_CUSTODIAN_ACCESS_REQUEST_ERROR_TARGET_REQUEST: &str =
33 "browser_custodian_access_request.request";
34pub const BROWSER_CUSTODIAN_ACCESS_REQUEST_ERROR_TARGET_DECISION_STATE: &str =
35 "browser_custodian_access_request.decision_state";
36pub const BROWSER_CUSTODIAN_ACCESS_REQUEST_ERROR_TARGET_SERVICE: &str =
37 "browser_custodian_access_request.service";
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
43#[serde(deny_unknown_fields)]
44pub struct BrowserVisitorIdentity {
45 pub issuer: String,
46 pub subject: String,
47 pub orcid: String,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
51#[serde(deny_unknown_fields)]
52pub struct StudyAccessRequestSubmission {
53 pub datastore_id: PublicUuid,
54 pub study: StudyRef,
55 pub intended_use: String,
56 #[serde(default, skip_serializing_if = "Option::is_none")]
57 pub contact: Option<String>,
58 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub affiliation: Option<String>,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
63#[serde(deny_unknown_fields)]
64pub struct BrowserStudyAccessRequestSubmissionRequest {
65 pub visitor: BrowserVisitorIdentity,
66 pub submission: StudyAccessRequestSubmission,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
70#[serde(deny_unknown_fields)]
71pub struct BrowserRequesterStudyAccessRequestListRequest {
72 pub visitor: BrowserVisitorIdentity,
73 pub datastore_id: PublicUuid,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
77#[serde(deny_unknown_fields)]
78pub struct BrowserRequesterStudyAccessRequestDetailRequest {
79 pub visitor: BrowserVisitorIdentity,
80 pub datastore_id: PublicUuid,
81 pub request_id: crate::refs::ObjectRef,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
85#[serde(deny_unknown_fields)]
86pub struct BrowserRequesterStudyAccessRequestWithdrawRequest {
87 pub visitor: BrowserVisitorIdentity,
88 pub datastore_id: PublicUuid,
89 pub request_id: crate::refs::ObjectRef,
90}
91
92#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
97#[serde(rename_all = "snake_case")]
98pub enum CustodianStudyAccessRequestStatusFilter {
99 #[default]
100 Pending,
101 Approved,
102 Rejected,
103}
104
105#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
109#[serde(deny_unknown_fields)]
110pub struct BrowserCustodianStudyAccessRequestListRequest {
111 pub session: SessionSelector,
112 pub datastore_id: PublicUuid,
113 #[serde(default)]
114 pub status: CustodianStudyAccessRequestStatusFilter,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
120#[serde(deny_unknown_fields)]
121pub struct BrowserCustodianStudyAccessRequestApproveRequest {
122 pub session: SessionSelector,
123 pub datastore_id: PublicUuid,
124 pub request_id: crate::refs::ObjectRef,
125}
126
127#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
130#[serde(deny_unknown_fields)]
131pub struct BrowserCustodianStudyAccessRequestRejectRequest {
132 pub session: SessionSelector,
133 pub datastore_id: PublicUuid,
134 pub request_id: crate::refs::ObjectRef,
135 #[serde(default, skip_serializing_if = "Option::is_none")]
136 pub reason: Option<String>,
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
140#[serde(rename_all = "snake_case")]
141pub enum StudyAccessRequestStatus {
142 Pending,
143 Approved,
144 Rejected,
145 Withdrawn,
146 Expired,
147}
148
149#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
150pub struct StudyAccessRequestSubmissionResponse {
151 pub authority: String,
152 pub request_id: crate::refs::ObjectRef,
153 pub datastore_id: PublicUuid,
154 pub study: StudyRef,
155 pub status: StudyAccessRequestStatus,
156 pub submitted_at: DateTime<Utc>,
157}
158
159#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
164pub struct RequesterStudyAccessRequestRecord {
165 pub authority: String,
166 pub request_id: crate::refs::ObjectRef,
167 pub datastore_id: PublicUuid,
168 pub study: StudyRef,
169 pub intended_use: String,
170 #[serde(default, skip_serializing_if = "Option::is_none")]
171 pub contact: Option<String>,
172 #[serde(default, skip_serializing_if = "Option::is_none")]
173 pub affiliation: Option<String>,
174 pub status: StudyAccessRequestStatus,
175 pub submitted_at: DateTime<Utc>,
176 #[serde(default, skip_serializing_if = "Option::is_none")]
177 pub decided_at: Option<DateTime<Utc>>,
178 #[serde(default, skip_serializing_if = "Option::is_none")]
179 pub rejection_reason: Option<String>,
180}
181
182#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
183pub struct RequesterStudyAccessRequestListResponse {
184 pub authority: String,
185 pub datastore_id: PublicUuid,
186 #[serde(default)]
187 pub requests: Vec<RequesterStudyAccessRequestRecord>,
188}
189
190#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
193pub struct CustodianStudyAccessRequestRecord {
194 pub authority: String,
195 pub request_id: crate::refs::ObjectRef,
196 pub datastore_id: PublicUuid,
197 pub study: StudyRef,
198 pub requester_orcid: String,
199 pub intended_use: String,
200 #[serde(default, skip_serializing_if = "Option::is_none")]
201 pub contact: Option<String>,
202 #[serde(default, skip_serializing_if = "Option::is_none")]
203 pub affiliation: Option<String>,
204 pub status: StudyAccessRequestStatus,
205 pub submitted_at: DateTime<Utc>,
206 #[serde(default, skip_serializing_if = "Option::is_none")]
207 pub decided_at: Option<DateTime<Utc>>,
208 #[serde(default, skip_serializing_if = "Option::is_none")]
209 pub rejection_reason: Option<String>,
210}
211
212#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
213pub struct CustodianStudyAccessRequestListResponse {
214 pub authority: String,
215 pub datastore_id: PublicUuid,
216 #[serde(default)]
217 pub requests: Vec<CustodianStudyAccessRequestRecord>,
218}
219
220#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
221#[serde(deny_unknown_fields)]
222pub struct RejectStudyAccessRequest {
223 #[serde(default, skip_serializing_if = "Option::is_none")]
224 pub reason: Option<String>,
225}
226
227#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
230#[serde(deny_unknown_fields)]
231pub struct ApproveStudyAccessRequest {}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236 use crate::request::{ProtocolRequest, kind};
237 use uuid::Uuid;
238
239 #[test]
240 fn requester_operations_carry_server_derived_visitor_identity() {
241 let datastore_id = PublicUuid::from_uuid(Uuid::from_u128(1));
242 let study = StudyRef {
243 datastore_id: crate::PublicUuid::from_uuid(uuid::Uuid::from_u128(5)),
244 kind: crate::refs::ObjectKind::Study,
245 id: PublicUuid::from_uuid(Uuid::from_u128(2)),
246 };
247 let request = ProtocolRequest::BrowserAccessRequestSubmit(
248 BrowserStudyAccessRequestSubmissionRequest {
249 visitor: BrowserVisitorIdentity {
250 issuer: "https://orcid.org".to_string(),
251 subject: "subject-123".to_string(),
252 orcid: "0000-0002-1825-0097".to_string(),
253 },
254 submission: StudyAccessRequestSubmission {
255 datastore_id,
256 study,
257 intended_use: "Evaluate the cohort".to_string(),
258 contact: None,
259 affiliation: Some("AHRI".to_string()),
260 },
261 },
262 );
263
264 assert_eq!(request.kind(), kind::BROWSER_ACCESS_REQUEST_SUBMIT);
265 let encoded = serde_json::to_value(&request).unwrap();
266 assert_eq!(encoded["body"]["visitor"]["subject"], "subject-123");
267 assert_eq!(
268 encoded["body"]["submission"]["datastore_id"],
269 datastore_id.to_string()
270 );
271 assert_eq!(
272 serde_json::from_value::<ProtocolRequest>(encoded).unwrap(),
273 request
274 );
275 }
276
277 #[test]
278 fn custodian_queue_operation_carries_a_supported_status_filter() {
279 let datastore_id = PublicUuid::from_uuid(Uuid::from_u128(1));
280 let session = crate::refs::SessionSelector::Name {
281 name: crate::PublicName::new("web_custodian").unwrap(),
282 };
283 let request = ProtocolRequest::BrowserCustodianAccessRequestList(
284 BrowserCustodianStudyAccessRequestListRequest {
285 session: session.clone(),
286 datastore_id,
287 status: CustodianStudyAccessRequestStatusFilter::Approved,
288 },
289 );
290
291 assert_eq!(request.kind(), kind::BROWSER_CUSTODIAN_ACCESS_REQUEST_LIST);
292 let encoded = serde_json::to_value(&request).unwrap();
293 assert_eq!(encoded["body"]["session"]["name"], "web_custodian");
294 assert_eq!(encoded["body"]["datastore_id"], datastore_id.to_string());
295 assert_eq!(encoded["body"]["status"], "approved");
296 assert_eq!(
297 serde_json::from_value::<ProtocolRequest>(encoded).unwrap(),
298 request
299 );
300
301 for status in ["pending", "approved", "rejected"] {
302 let decoded: CustodianStudyAccessRequestStatusFilter =
303 serde_json::from_str(&format!("\"{status}\"")).unwrap();
304 assert_eq!(
305 serde_json::to_string(&decoded).unwrap(),
306 format!("\"{status}\"")
307 );
308 }
309 assert!(
310 serde_json::from_str::<CustodianStudyAccessRequestStatusFilter>("\"withdrawn\"")
311 .is_err()
312 );
313
314 let legacy = serde_json::json!({
315 "session": {"name": "web_custodian"},
316 "datastore_id": datastore_id,
317 });
318 assert_eq!(
319 serde_json::from_value::<BrowserCustodianStudyAccessRequestListRequest>(legacy)
320 .unwrap()
321 .status,
322 CustodianStudyAccessRequestStatusFilter::Pending
323 );
324 }
325
326 #[test]
327 fn custodian_approval_operation_binds_the_request_to_a_live_session() {
328 let datastore_id = PublicUuid::from_uuid(Uuid::from_u128(1));
329 let request_id = crate::refs::ObjectRef {
330 datastore_id,
331 kind: crate::refs::ObjectKind::StudyAccessRequest,
332 id: PublicUuid::from_uuid(Uuid::from_u128(2)),
333 };
334 let session = crate::refs::SessionSelector::Name {
335 name: crate::PublicName::new("web_custodian").unwrap(),
336 };
337 let request = ProtocolRequest::BrowserCustodianAccessRequestApprove(
338 BrowserCustodianStudyAccessRequestApproveRequest {
339 session: session.clone(),
340 datastore_id,
341 request_id,
342 },
343 );
344
345 assert_eq!(
346 request.kind(),
347 kind::BROWSER_CUSTODIAN_ACCESS_REQUEST_APPROVE
348 );
349 let encoded = serde_json::to_value(&request).unwrap();
350 assert_eq!(encoded["body"]["session"]["name"], "web_custodian");
351 assert_eq!(encoded["body"]["datastore_id"], datastore_id.to_string());
352 assert_eq!(
353 encoded["body"]["request_id"],
354 serde_json::to_value(request_id).unwrap()
355 );
356 assert_eq!(
357 serde_json::from_value::<ProtocolRequest>(encoded).unwrap(),
358 request
359 );
360 }
361
362 #[test]
363 fn custodian_rejection_operation_preserves_the_optional_reason() {
364 let datastore_id = PublicUuid::from_uuid(Uuid::from_u128(1));
365 let request_id = crate::refs::ObjectRef {
366 datastore_id,
367 kind: crate::refs::ObjectKind::StudyAccessRequest,
368 id: PublicUuid::from_uuid(Uuid::from_u128(2)),
369 };
370 let session = crate::refs::SessionSelector::Name {
371 name: crate::PublicName::new("web_custodian").unwrap(),
372 };
373 let request = ProtocolRequest::BrowserCustodianAccessRequestReject(
374 BrowserCustodianStudyAccessRequestRejectRequest {
375 session,
376 datastore_id,
377 request_id,
378 reason: Some("Outside the Study scope".to_string()),
379 },
380 );
381
382 assert_eq!(
383 request.kind(),
384 kind::BROWSER_CUSTODIAN_ACCESS_REQUEST_REJECT
385 );
386 let encoded = serde_json::to_value(&request).unwrap();
387 assert_eq!(encoded["body"]["reason"], "Outside the Study scope");
388 assert_eq!(
389 serde_json::from_value::<ProtocolRequest>(encoded).unwrap(),
390 request
391 );
392 }
393}