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
        let subject = "Your request to NGI Review";
123
        let body = format!(
124
            "Your request of {} was denied.\n\n{}",
125
            request.date, rejection.rejection_comment
126
        );
127
        data.mailer
128
            .send_mail(
129
                subject,
130
                &body,
131
                &request.email,
132
                &request.name,
133
                &["webmaster@nlnet.nl"],
134
            )
135
            .map_err(|e| {
136
                eprintln!("{:?}", e);
137
                e
138
            })?;
139
        Ok(rejection)
140
    }
141
    pub async fn tasks(&self, claims: &Claims) -> BTreeMap<String, Task> {
142
        if !self.is_participating_organization(claims) {
143
            return BTreeMap::new();
144
        }
145
        self.inner.lock().await.tasks.clone()
146
    }
147
    pub async fn add_task(&self, config: &Config, bytes: &[u8]) -> Result<Task> {
148
        let mut data = self.inner.lock().await;
149
        let (name, task) = add_task(&mut data, bytes, None)?;
150
        let path = self.data_path.join("tasks").join(name);
151
        tokio::fs::write(&path, bytes)
152
            .await
153
            .map_err(|e| Error::Io { source: e, path })?;
154
        let org = if let Some(org) = config.review_types.get(&task.review_type) {
155
            org
156
        } else {
157
            return Err(Error::Msg(format!(
158
                "{} is not a known review type.",
159
                task.review_type
160
            )));
161
        };
162
        let subject = "There is a new task for NGI Review";
163
        let body = "Please visit https://dashboard.nlnet.nl/ to take a look.";
164
        // the first contact mail gets 'to', any others are 'cc'
165
        let cc: Vec<_> = org.emails.iter().skip(1).map(|s| s.as_str()).collect();
166
        data.mailer
167
            .send_mail(subject, body, &org.emails[0], &org.name, &cc)?;
168
        Ok(task)
169
    }
170
    pub async fn reports(&self, claims: &Claims) -> BTreeMap<String, Report> {
171
        if !self.is_participating_organization(claims) {
172
            return BTreeMap::new();
173
        }
174
        self.inner.lock().await.reports.clone()
175
    }
176
    pub async fn add_report(&self, bytes: &[u8], claims: &Claims) -> Result<Report> {
177
        let mut data = self.inner.lock().await;
178
        let (name, report) = add_report_with_claims(&mut data, bytes, None, Some(claims))?;
179
        let path = self.data_path.join("reports").join(name);
180
        tokio::fs::write(&path, bytes)
181
            .await
182
            .map_err(|e| Error::Io { source: e, path })?;
183
        Ok(report)
184
    }
185
    pub async fn add_file(&self, bytes: &[u8]) -> Result<String> {
186
        let name = format!("{:x}", sha2::Sha256::digest(bytes));
187
        let path = self.data_path.join("files").join(&name);
188
        if !path.try_exists().unwrap_or(false) {
189
            tokio::fs::write(&path, bytes)
190
                .await
191
                .map_err(|e| Error::Io { source: e, path })?;
192
        }
193
        Ok(name)
194
    }
195
    pub fn jwks(&self) -> &Jwks {
196
        &self.jwks
197
    }
198
9
    pub fn config(&self) -> &Config {
199
9
        &self.config
200
9
    }
201
}
202

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

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

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

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

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

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

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

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