1
use crate::{
2
    auth::{load_jwks, Claims, Jwks},
3
    config::Config,
4
    data_format::{Rejection, Report, ReviewRequest, ReviewType, Task},
5
    error::{Error, Result},
6
    mailer::Mailer,
7
    util::load_from_json,
8
};
9
use sha2::Digest;
10
use std::{
11
    collections::BTreeMap,
12
    fs::{create_dir_all, read_dir},
13
    path::{Path, PathBuf},
14
};
15
use tokio::sync::Mutex;
16

            
17
struct DataInner {
18
    requests: BTreeMap<String, ReviewRequest>,
19
    tasks: BTreeMap<String, Task>,
20
    rejections: BTreeMap<String, Rejection>,
21
    reports: BTreeMap<String, Report>,
22
    mailer: Mailer,
23
}
24

            
25
pub struct Data {
26
    inner: Mutex<DataInner>,
27
    data_path: PathBuf,
28
    jwks: Jwks,
29
    pub(crate) config: Config,
30
}
31

            
32
impl Data {
33
9
    pub fn new(data_path: PathBuf) -> Result<Self> {
34
9
        load(data_path)
35
9
    }
36
9
    pub fn data_path(&self) -> &Path {
37
9
        &self.data_path
38
9
    }
39
    pub fn selectors(&self) -> &[String] {
40
        &self.config.selectors
41
    }
42
    pub fn is_selector(&self, claims: &Claims) -> bool {
43
        self.config.selectors.contains(&claims.organization)
44
    }
45
    pub fn review_types(&self) -> &BTreeMap<String, ReviewType> {
46
        &self.config.review_types
47
    }
48
    fn is_participating_organization(&self, claims: &Claims) -> bool {
49
        self.config
50
            .review_types
51
            .values()
52
            .any(|rt| rt.organization == claims.organization)
53
    }
54
    pub async fn get_requests(&self, claims: &Claims) -> BTreeMap<String, ReviewRequest> {
55
        let data = self.inner.lock().await;
56
        if self.is_selector(claims) {
57
            return data.requests.clone();
58
        }
59
        if !self.is_participating_organization(claims) {
60
            return BTreeMap::new();
61
        }
62
        // For a reviewer, get all requests for which there is at least one
63
        // task.
64
        data.requests
65
            .iter()
66
            .filter(|r| data.tasks.iter().any(|t| &t.1.request == r.0))
67
            .map(|(k, v)| {
68
                let mut v = v.clone();
69
                // If the organization has no work on this request,
70
                // remove the personal information from the request.
71
                if !data
72
                    .tasks
73
                    .iter()
74
                    .any(|t| &t.1.request == k && t.1.assigned_organization == claims.organization)
75
                {
76
                    v.remove_personal_information();
77
                }
78
                (k.clone(), v)
79
            })
80
            .collect()
81
    }
82
3
    pub async fn add_request(&self, bytes: &[u8]) -> Result<ReviewRequest> {
83
1
        let mut data = self.inner.lock().await;
84
1
        let (name, request) = add_request(&mut data, bytes, None)?;
85
1
        let path = self.data_path.join("requests").join(name);
86
1
        tokio::fs::write(&path, bytes)
87
1
            .await
88
1
            .map_err(|e| Error::Io { source: e, path })?;
89
1
        let subject = "Your request to NGI Review";
90
1
        let body = format!(
91
1
            "A request was made to NGI Review with this e-mail address. Here is a structured version of that request.\n\n{}",
92
1
            serde_json::to_string_pretty(&request).map_err(|e| Error::Serde { source: e, path: None })?
93
        );
94
1
        data.mailer
95
1
            .send_mail(
96
1
                subject,
97
1
                &body,
98
1
                &request.email,
99
1
                &request.name,
100
1
                &["ngizero-review-coordinator@nlnet.nl"],
101
1
            )
102
1
            .map_err(|e| {
103
                eprintln!("{:?}", e);
104
                e
105
1
            })?;
106
1
        Ok(request)
107
1
    }
108
    pub async fn get_rejections(&self, claims: &Claims) -> BTreeMap<String, Rejection> {
109
        let data = self.inner.lock().await;
110
        if self.is_selector(claims) {
111
            return data.rejections.clone();
112
        }
113
        Default::default()
114
    }
115
    pub async fn add_rejection(&self, bytes: &[u8]) -> Result<Rejection> {
116
        let mut data = self.inner.lock().await;
117
        let (name, request, rejection) = add_rejection(&mut data, bytes, None)?;
118
        let path = self.data_path.join("rejections").join(name);
119
        tokio::fs::write(&path, bytes)
120
            .await
121
            .map_err(|e| Error::Io { source: e, path })?;
122
        if rejection.send_mail {
123
            let subject = "Your request to NGI Review";
124
            let body = format!(
125
                "Your request of {} was denied.\n\n{}",
126
                request.date, rejection.rejection_comment
127
            );
128
            data.mailer
129
                .send_mail(
130
                    subject,
131
                    &body,
132
                    &request.email,
133
                    &request.name,
134
                    &["webmaster@nlnet.nl"],
135
                )
136
                .map_err(|e| {
137
                    eprintln!("{:?}", e);
138
                    e
139
                })?;
140
        }
141
        Ok(rejection)
142
    }
143
    pub async fn tasks(&self, claims: &Claims) -> BTreeMap<String, Task> {
144
        if !self.is_selector(claims) && !self.is_participating_organization(claims) {
145
            return BTreeMap::new();
146
        }
147
        self.inner.lock().await.tasks.clone()
148
    }
149
    pub async fn add_task(&self, config: &Config, bytes: &[u8]) -> Result<Task> {
150
        let mut data = self.inner.lock().await;
151
        let (name, task) = add_task(&mut data, bytes, None)?;
152
        let path = self.data_path.join("tasks").join(name);
153
        tokio::fs::write(&path, bytes)
154
            .await
155
            .map_err(|e| Error::Io { source: e, path })?;
156
        let org = if let Some(org) = config.review_types.get(&task.review_type) {
157
            org
158
        } else {
159
            return Err(Error::Msg(format!(
160
                "{} is not a known review type.",
161
                task.review_type
162
            )));
163
        };
164
        let subject = "There is a new task for NGI Review";
165
        let body = "Please visit https://dashboard.nlnet.nl/ to take a look.";
166
        // the first contact mail gets 'to', any others are 'cc'
167
        let cc: Vec<_> = org.emails.iter().skip(1).map(|s| s.as_str()).collect();
168
        data.mailer
169
            .send_mail(subject, body, &org.emails[0], &org.name, &cc)?;
170
        Ok(task)
171
    }
172
    pub async fn reports(&self, claims: &Claims) -> BTreeMap<String, Report> {
173
        if !self.is_selector(claims) && !self.is_participating_organization(claims) {
174
            return BTreeMap::new();
175
        }
176
        self.inner.lock().await.reports.clone()
177
    }
178
    pub async fn add_report(&self, bytes: &[u8], claims: &Claims) -> Result<Report> {
179
        let mut data = self.inner.lock().await;
180
        let (name, report) = add_report_with_claims(&mut data, bytes, None, Some(claims))?;
181
        let path = self.data_path.join("reports").join(name);
182
        tokio::fs::write(&path, bytes)
183
            .await
184
            .map_err(|e| Error::Io { source: e, path })?;
185
        Ok(report)
186
    }
187
    pub async fn add_file(&self, bytes: &[u8]) -> Result<String> {
188
        let name = format!("{:x}", sha2::Sha256::digest(bytes));
189
        let path = self.data_path.join("files").join(&name);
190
        if !path.try_exists().unwrap_or(false) {
191
            tokio::fs::write(&path, bytes)
192
                .await
193
                .map_err(|e| Error::Io { source: e, path })?;
194
        }
195
        Ok(name)
196
    }
197
    pub fn jwks(&self) -> &Jwks {
198
        &self.jwks
199
    }
200
9
    pub fn config(&self) -> &Config {
201
9
        &self.config
202
9
    }
203
}
204

            
205
9
fn load(data_path: PathBuf) -> Result<Data> {
206
9
    let jwks = load_jwks(&data_path)?;
207
9
    let config_path = data_path.join("config.json");
208
9
    let config: Config = load_from_json(config_path)?;
209
108
    for (name, rt) in config.review_types.iter() {
210
108
        if rt.emails.is_empty() {
211
            return Err(Error::Msg(format!(
212
                "There are no emails defined for {}.",
213
                name
214
            )));
215
108
        }
216
    }
217
9
    let path = data_path.join("mail_config.json");
218
9
    let mail_config = load_from_json(path)?;
219
9
    let mailer = Mailer::new(mail_config)?;
220
9
    let mut data = DataInner {
221
9
        requests: BTreeMap::new(),
222
9
        rejections: BTreeMap::new(),
223
9
        tasks: BTreeMap::new(),
224
9
        reports: BTreeMap::new(),
225
9
        mailer,
226
9
    };
227
9
    load_items(&data_path, &mut data, "requests", add_request)?;
228
9
    load_items(&data_path, &mut data, "rejections", add_rejection)?;
229
9
    load_items(&data_path, &mut data, "tasks", add_task)?;
230
9
    load_items(&data_path, &mut data, "reports", add_report)?;
231
9
    let path = data_path.join("files");
232
9
    create_dir_all(&path).map_err(|e| Error::Io { source: e, path })?;
233
9
    Ok(Data {
234
9
        inner: Mutex::new(data),
235
9
        data_path,
236
9
        jwks,
237
9
        config,
238
9
    })
239
9
}
240

            
241
36
fn load_items<T>(
242
36
    path: &Path,
243
36
    data: &mut DataInner,
244
36
    dir: &str,
245
36
    add: fn(&mut DataInner, &[u8], Option<PathBuf>) -> Result<T>,
246
36
) -> Result<()> {
247
36
    let path = path.join(dir);
248
36
    create_dir_all(&path).map_err(|e| Error::Io {
249
        source: e,
250
        path: path.clone(),
251
36
    })?;
252
36
    for entry in read_dir(&path).map_err(|e| Error::Io {
253
        source: e,
254
        path: path.clone(),
255
36
    })? {
256
        let entry = entry.map_err(|e| Error::Io {
257
            source: e,
258
            path: path.clone(),
259
        })?;
260
        let path = entry.path();
261
        let bytes = std::fs::read(&path).map_err(|e| Error::Io {
262
            source: e,
263
            path: path.clone(),
264
        })?;
265
        add(data, &bytes, Some(path))?;
266
    }
267
36
    Ok(())
268
36
}
269

            
270
3
fn add_request(
271
3
    data: &mut DataInner,
272
3
    bytes: &[u8],
273
3
    path: Option<PathBuf>,
274
3
) -> Result<(String, ReviewRequest)> {
275
3
    let name = format!("{:x}", sha2::Sha256::digest(bytes));
276
3
    let request: ReviewRequest =
277
3
        serde_json::from_slice(bytes).map_err(|e| Error::Serde { source: e, path })?;
278
3
    data.requests.insert(name.clone(), request.clone());
279
3
    Ok((name, request))
280
3
}
281

            
282
fn ensure_new(data: &mut DataInner, request_id: &str, review_type: &str) -> Result<ReviewRequest> {
283
    // check the references
284
    let request = if let Some(request) = data.requests.get(request_id) {
285
        request
286
    } else {
287
        return Err(Error::Msg(format!(
288
            "request '{}' does not exist.",
289
            request_id
290
        )));
291
    };
292
    // check that there is not another task or rejection for the same request
293
    if data
294
        .tasks
295
        .iter()
296
        .any(|t| t.1.request == request_id && t.1.review_type == review_type)
297
    {
298
        return Err(Error::Msg(format!(
299
            "There is already a task for request {} and type {}.",
300
            request_id, review_type
301
        )));
302
    }
303
    if data
304
        .rejections
305
        .iter()
306
        .any(|t| t.1.request == request_id && t.1.review_type == review_type)
307
    {
308
        return Err(Error::Msg(format!(
309
            "There is already a rejection for request {} and type {}.",
310
            request_id, review_type
311
        )));
312
    }
313
    Ok(request.clone())
314
}
315

            
316
fn add_rejection(
317
    data: &mut DataInner,
318
    bytes: &[u8],
319
    path: Option<PathBuf>,
320
) -> Result<(String, ReviewRequest, Rejection)> {
321
    let name = format!("{:x}", sha2::Sha256::digest(bytes));
322
    let rejection: Rejection =
323
        serde_json::from_slice(bytes).map_err(|e| Error::Serde { source: e, path })?;
324
    let request = ensure_new(data, &rejection.request, &rejection.review_type)?;
325
    data.rejections.insert(name.clone(), rejection.clone());
326
    Ok((name, request, rejection))
327
}
328

            
329
fn add_task(data: &mut DataInner, bytes: &[u8], path: Option<PathBuf>) -> Result<(String, Task)> {
330
    let name = format!("{:x}", sha2::Sha256::digest(bytes));
331
    let task: Task = serde_json::from_slice(bytes).map_err(|e| Error::Serde { source: e, path })?;
332
    ensure_new(data, &task.request, &task.review_type)?;
333
    data.tasks.insert(name.clone(), task.clone());
334
    Ok((name, task))
335
}
336

            
337
fn add_report(
338
    data: &mut DataInner,
339
    bytes: &[u8],
340
    path: Option<PathBuf>,
341
) -> Result<(String, Report)> {
342
    add_report_with_claims(data, bytes, path, None)
343
}
344

            
345
fn add_report_with_claims(
346
    data: &mut DataInner,
347
    bytes: &[u8],
348
    path: Option<PathBuf>,
349
    claims: Option<&Claims>,
350
) -> Result<(String, Report)> {
351
    let name = format!("{:x}", sha2::Sha256::digest(bytes));
352
    let report: Report =
353
        serde_json::from_slice(bytes).map_err(|e| Error::Serde { source: e, path })?;
354
    // check the references
355
    if let Some(task) = data.tasks.get(&report.task) {
356
        if let Some(claims) = claims {
357
            if task.assigned_organization != claims.organization {
358
                return Err(Error::Msg(format!(
359
                    "task '{}' is not assigned to {}.",
360
                    report.task, claims.organization
361
                )));
362
            }
363
        }
364
    } else {
365
        println!("tasks: {:?}", data.tasks.keys());
366
        return Err(Error::Msg(format!(
367
            "Task '{}' does not exist.",
368
            report.task
369
        )));
370
    }
371
    // TODO: check that the previous report is referenced
372
    /*
373
    // check that there is no report yet for this task
374
    if data.reports.iter().any(|r| r.1.task == report.task) {
375
        return Err(Error::Msg(format!(
376
            "Task '{}' was already reported on.",
377
            report.task
378
        )));
379
    }*/
380
    data.reports.insert(name.clone(), report.clone());
381
    Ok((name, report))
382
}