1use super::*;
2use ahri_tre_protocol::{acquisition::AcquisitionRequest, request::ProtocolRequest};
3use ahri_tre_runtime::acquisition::{AcquiredSource, AcquisitionAdmission, AcquisitionInput};
4
5pub struct AcquisitionAuthorization<'a> {
6 pub shared: Option<&'a ahri_tre_config::SharedSource>,
7 pub identity: &'a ahri_tre_runtime::AuthenticationIdentityBinding,
8 pub deadline: std::time::Instant,
9 pub cancelled: std::sync::Arc<std::sync::atomic::AtomicBool>,
10}
11
12impl AppService {
13 pub async fn authorize_acquisition(
16 &self,
17 session: &DataStoreSession,
18 request: &mut AcquisitionRequest,
19 authority: AcquisitionAuthorization<'_>,
20 ) -> Result<(), AppError> {
21 let invalid =
22 || AppError::Validation("Source acquisition is unavailable or unauthorized".into());
23 let _deadline = session
24 .dataset_admission_repository()
25 .ok_or_else(invalid)?
26 .upload_deadline(authority.deadline, authority.cancelled)
27 .map_err(|_| invalid())?;
28 if !request.valid() {
29 return Err(invalid());
30 }
31 let (explicit, classification) = match &request.request.request {
32 ProtocolRequest::IngestFile(value) => (value.study.clone(), &value.classification),
33 ProtocolRequest::IngestDatasetFile(value) => {
34 (Some(value.study.clone()), &value.classification)
35 }
36 ProtocolRequest::IngestRedcap(value) => {
37 (Some(value.study.clone()), &value.classification)
38 }
39 ProtocolRequest::IngestSql(value)
40 if !matches!(
41 value.source,
42 ahri_tre_protocol::ingest::SqlSource::Duckdb { .. }
43 | ahri_tre_protocol::ingest::SqlSource::Sqlite { .. }
44 ) =>
45 {
46 (Some(value.study.clone()), &value.classification)
47 }
48 _ => return Err(invalid()),
49 };
50 let study = self
51 .resolve_session_study(
52 self.catalogue_datastore_id()?.as_uuid(),
53 explicit,
54 session.current_study().map(|study| study.0),
55 )
56 .await
57 .map_err(|_| invalid())?;
58 let repository = session.dataset_admission_repository().ok_or_else(invalid)?;
59 repository
60 .authorize_acquisition_destination(
61 ahri_tre_types::StudyId(study.id.as_uuid()),
62 classification,
63 )
64 .map_err(|_| invalid())?;
65 if let ProtocolRequest::IngestDatasetFile(value) = &request.request.request {
66 let domain = self.catalogue_domain_selector(value.domain.clone())?;
67 self.get_domain(GetDomainRequest { domain })
68 .await?
69 .ok_or_else(invalid)?;
70 }
71 if let ProtocolRequest::IngestSql(value) = &mut request.request.request {
72 let domain = self
73 .get_domain(GetDomainRequest {
74 domain: self.catalogue_domain_selector(value.domain.clone())?,
75 })
76 .await?
77 .ok_or_else(invalid)?;
78 self.ensure_study_domain(
79 ahri_tre_types::StudyId(study.id.as_uuid()),
80 domain.domain_id,
81 "SQL acquisition",
82 )
83 .await?;
84 value.domain = ahri_tre_protocol::domain::DomainSelector::Id {
85 domain: crate::projections::domain_ref(
86 self.catalogue_datastore_id()?,
87 domain.domain_id,
88 ),
89 };
90 DatasetOutputIntent::parse(
91 value.materialization.replace,
92 value.materialization.new_version.as_deref(),
93 )?;
94 ahri_tre_lake::SqlSourcePlan::parse(&value.sql).map_err(|_| invalid())?;
95 }
96 if let ProtocolRequest::IngestRedcap(value) = &mut request.request.request {
97 let domain = self
98 .get_domain(GetDomainRequest {
99 domain: self.catalogue_domain_selector(value.domain.clone())?,
100 })
101 .await?
102 .ok_or_else(invalid)?;
103 self.ensure_study_domain(
104 ahri_tre_types::StudyId(study.id.as_uuid()),
105 domain.domain_id,
106 "REDCap acquisition",
107 )
108 .await?;
109 value.domain = ahri_tre_protocol::domain::DomainSelector::Id {
110 domain: crate::projections::domain_ref(
111 self.catalogue_datastore_id()?,
112 domain.domain_id,
113 ),
114 };
115 }
116 match (&request.shared_source, authority.shared) {
117 (None, None) => {}
118 (Some(_), Some(source))
119 if request.path == ahri_tre_types::AcquisitionPath::Trusted
120 && source.endpoint == request.endpoint.as_str()
121 && source.authentication == request.authentication
122 && matches!(
123 source.authentication,
124 ahri_tre_types::SourceAuthentication::Basic
125 | ahri_tre_types::SourceAuthentication::Bearer
126 | ahri_tre_types::SourceAuthentication::PostgreSql
127 | ahri_tre_types::SourceAuthentication::MsSql
128 )
129 && source.grants.iter().any(|grant| {
130 grant.issuer.as_str() == authority.identity.issuer()
131 && grant.subject.as_str() == authority.identity.subject()
132 && grant.study == study.id.as_uuid()
133 }) => {}
134 _ => return Err(invalid()),
135 }
136 let selector = ahri_tre_protocol::study::StudySelector::Id { study };
137 match &mut request.request.request {
138 ProtocolRequest::IngestFile(value) => value.study = Some(selector),
139 ProtocolRequest::IngestDatasetFile(value) => value.study = selector,
140 ProtocolRequest::IngestRedcap(value) => value.study = selector,
141 ProtocolRequest::IngestSql(value) => value.study = selector,
142 _ => unreachable!(),
143 }
144 Ok(())
145 }
146
147 pub async fn ingest_acquisition(
148 &self,
149 session: &mut DataStoreSession,
150 status: ahri_tre_protocol::session::SessionStatusPayload,
151 admission: &AcquisitionAdmission,
152 input: AcquisitionInput,
153 outbound: &ahri_tre_config::OutboundPolicy,
154 ) -> Result<serde_json::Value, AppError> {
155 let invalid =
156 || AppError::Validation("Source acquisition is unavailable or rejected".into());
157 admission.check().map_err(|_| invalid())?;
158 let (source, metadata): (Box<dyn std::io::Read + Send>, _) = match input {
159 AcquisitionInput::Trusted(credentials) => {
160 let scratch = session.shared_scratch_attempt().ok_or_else(invalid)?;
161 let request = admission.request.clone();
162 let policy = outbound.clone();
163 let budgets = admission.budgets;
164 let deadline = admission.deadline;
165 let cancelled = admission.cancelled.clone();
166 let acquired = run_blocking_app_work(move || {
167 AcquiredSource::fetch(
168 &request,
169 credentials,
170 &policy,
171 budgets,
172 deadline,
173 cancelled,
174 scratch.path(),
175 )
176 .map_err(|_| AppError::Validation("Source acquisition failed".into()))
177 })
178 .await?;
179 (Box::new(acquired.file), acquired.metadata)
180 }
181 AcquisitionInput::Client { metadata, source } => (source, *metadata),
182 };
183 if metadata.upload.filename != admission.request.filename
184 || metadata.upload.media_type != admission.request.media_type
185 {
186 return Err(invalid());
187 }
188 let mut request = admission.request.request.clone();
189 let redcap = matches!(&request.request, ProtocolRequest::IngestRedcap(_));
190 match (&mut request.request, metadata.redcap) {
191 (ProtocolRequest::IngestRedcap(value), Some(roles)) => {
192 value.source = roles.into();
193 value.materialization.response_encoding = None;
194 value.materialization.max_attempts = None;
195 value.materialization.timeout_secs = None;
196 }
197 (ProtocolRequest::IngestRedcap(_), None) | (_, Some(_)) => return Err(invalid()),
198 (_, None) => {}
199 }
200 let upload = ahri_tre_protocol::upload::UploadRequest {
201 request,
202 metadata: metadata.upload,
203 budgets: admission.request.budgets.clone(),
204 };
205 let input = ahri_tre_runtime::upload::UploadInput {
206 source,
207 budgets: admission.budgets,
208 deadline: admission.deadline,
209 cancelled: admission.cancelled.clone(),
210 };
211 if redcap {
212 self.ingest_redcap_upload(session, status, upload, input, Some(admission.request.path))
213 .await
214 } else {
215 self.ingest_upload_with_acquisition(
216 session,
217 status,
218 upload,
219 input,
220 Some(admission.request.path),
221 )
222 .await
223 }
224 }
225}