1 use std::{env, fmt};
2
3 use serde::{de::DeserializeOwned, Serialize};
4
5 use rpki::uri;
6
7 use crate::{
8 cli::{
9 options::{BulkCaCommand, CaCommand, Command, KrillInitDetails, Options, PubServerCommand},
10 report::{ApiResponse, ReportError},
11 },
12 commons::{
13 api::{
14 AllCertAuthIssues, AspaDefinitionUpdates, CaRepoDetails, CertAuthIssues, ChildCaInfo,
15 ChildrenConnectionStats, ParentCaContact, ParentStatuses, PublisherDetails, PublisherList, RepoStatus,
16 Token,
17 },
18 bgp::BgpAnalysisAdvice,
19 error::KrillIoError,
20 remote::rfc8183,
21 util::{file, httpclient},
22 },
23 constants::KRILL_CLI_API_ENV,
24 daemon::config::Config,
25 };
26
27 #[cfg(feature = "multi-user")]
28 use crate::{
29 cli::options::KrillUserDetails,
30 constants::{PW_HASH_LOG_N, PW_HASH_P, PW_HASH_R},
31 };
32
resolve_uri(server: &uri::Https, path: &str) -> String33 fn resolve_uri(server: &uri::Https, path: &str) -> String {
34 format!("{}{}", server, path)
35 }
36
get_json<T: DeserializeOwned>(server: &uri::Https, token: &Token, path: &str) -> Result<T, Error>37 async fn get_json<T: DeserializeOwned>(server: &uri::Https, token: &Token, path: &str) -> Result<T, Error> {
38 let uri = resolve_uri(server, path);
39 httpclient::get_json(&uri, Some(token))
40 .await
41 .map_err(Error::HttpClientError)
42 }
43
post_empty(server: &uri::Https, token: &Token, path: &str) -> Result<(), Error>44 async fn post_empty(server: &uri::Https, token: &Token, path: &str) -> Result<(), Error> {
45 let uri = resolve_uri(server, path);
46 httpclient::post_empty(&uri, Some(token))
47 .await
48 .map_err(Error::HttpClientError)
49 }
50
post_json(server: &uri::Https, token: &Token, path: &str, data: impl Serialize) -> Result<(), Error>51 async fn post_json(server: &uri::Https, token: &Token, path: &str, data: impl Serialize) -> Result<(), Error> {
52 let uri = resolve_uri(server, path);
53 httpclient::post_json(&uri, data, Some(token))
54 .await
55 .map_err(Error::HttpClientError)
56 }
57
post_json_with_response<T: DeserializeOwned>( server: &uri::Https, token: &Token, path: &str, data: impl Serialize, ) -> Result<T, Error>58 async fn post_json_with_response<T: DeserializeOwned>(
59 server: &uri::Https,
60 token: &Token,
61 path: &str,
62 data: impl Serialize,
63 ) -> Result<T, Error> {
64 let uri = resolve_uri(server, path);
65 httpclient::post_json_with_response(&uri, data, Some(token))
66 .await
67 .map_err(Error::HttpClientError)
68 }
69
post_json_with_opt_response<T: DeserializeOwned>( server: &uri::Https, token: &Token, uri: &str, data: impl Serialize, ) -> Result<Option<T>, Error>70 async fn post_json_with_opt_response<T: DeserializeOwned>(
71 server: &uri::Https,
72 token: &Token,
73 uri: &str,
74 data: impl Serialize,
75 ) -> Result<Option<T>, Error> {
76 let uri = resolve_uri(server, uri);
77 httpclient::post_json_with_opt_response(&uri, data, Some(token))
78 .await
79 .map_err(Error::HttpClientError)
80 }
81
delete(server: &uri::Https, token: &Token, uri: &str) -> Result<(), Error>82 async fn delete(server: &uri::Https, token: &Token, uri: &str) -> Result<(), Error> {
83 let uri = resolve_uri(server, uri);
84 httpclient::delete(&uri, Some(token))
85 .await
86 .map_err(Error::HttpClientError)
87 }
88
89 /// Command line tool for Krill admin tasks
90 pub struct KrillClient {
91 server: uri::Https,
92 token: Token,
93 }
94
95 impl KrillClient {
96 /// Delegates the options to be processed, and reports the response
97 /// back to the user. Note that error reporting is handled by CLI.
report(options: Options) -> Result<(), Error>98 pub async fn report(options: Options) -> Result<(), Error> {
99 let format = options.format;
100 let res = Self::process(options).await?;
101
102 if let Some(string) = res.report(format)? {
103 println!("{}", string)
104 }
105 Ok(())
106 }
107
108 /// Processes the options, and returns a response ready for formatting.
109 /// Note that this function is public to help integration testing the API
110 /// and client.
process(options: Options) -> Result<ApiResponse, Error>111 pub async fn process(options: Options) -> Result<ApiResponse, Error> {
112 let client = KrillClient {
113 server: options.server,
114 token: options.token,
115 };
116
117 if options.api {
118 // passing the api option in the env, so that the call
119 // to the back-end will just print and exit.
120 env::set_var(KRILL_CLI_API_ENV, "1")
121 }
122
123 trace!("Sending command: {:?}", options.command);
124
125 match options.command {
126 Command::Health => client.health().await,
127 Command::Info => client.info().await,
128 Command::Bulk(cmd) => client.bulk(cmd).await,
129 Command::CertAuth(cmd) => client.certauth(cmd).await,
130 Command::PubServer(cmd) => client.publishers(cmd).await,
131 Command::Init(details) => client.init_config(details),
132 #[cfg(feature = "multi-user")]
133 Command::User(cmd) => client.user(cmd),
134 Command::NotSet => Err(Error::MissingCommand),
135 }
136 }
137
health(&self) -> Result<ApiResponse, Error>138 async fn health(&self) -> Result<ApiResponse, Error> {
139 httpclient::get_ok(&resolve_uri(&self.server, "api/v1/authorized"), Some(&self.token)).await?;
140 Ok(ApiResponse::Health)
141 }
142
info(&self) -> Result<ApiResponse, Error>143 async fn info(&self) -> Result<ApiResponse, Error> {
144 let info = httpclient::get_json(&resolve_uri(&self.server, "stats/info"), Some(&self.token)).await?;
145 Ok(ApiResponse::Info(info))
146 }
147
bulk(&self, command: BulkCaCommand) -> Result<ApiResponse, Error>148 async fn bulk(&self, command: BulkCaCommand) -> Result<ApiResponse, Error> {
149 match command {
150 BulkCaCommand::Refresh => {
151 post_empty(&self.server, &self.token, "api/v1/bulk/cas/sync/parent").await?;
152 Ok(ApiResponse::Empty)
153 }
154 BulkCaCommand::Publish => {
155 post_empty(&self.server, &self.token, "api/v1/bulk/cas/publish").await?;
156 Ok(ApiResponse::Empty)
157 }
158 BulkCaCommand::Sync => {
159 post_empty(&self.server, &self.token, "api/v1/bulk/cas/sync/repo").await?;
160 Ok(ApiResponse::Empty)
161 }
162 }
163 }
164
165 #[allow(clippy::cognitive_complexity)]
certauth(&self, command: CaCommand) -> Result<ApiResponse, Error>166 async fn certauth(&self, command: CaCommand) -> Result<ApiResponse, Error> {
167 match command {
168 CaCommand::Init(init) => {
169 post_json(&self.server, &self.token, "api/v1/cas", init).await?;
170 Ok(ApiResponse::Empty)
171 }
172
173 CaCommand::Delete(ca) => {
174 let uri = format!("api/v1/cas/{}", ca);
175 delete(&self.server, &self.token, &uri).await?;
176 Ok(ApiResponse::Empty)
177 }
178
179 CaCommand::UpdateId(handle) => {
180 let uri = format!("api/v1/cas/{}/id", handle);
181 post_empty(&self.server, &self.token, &uri).await?;
182 Ok(ApiResponse::Empty)
183 }
184
185 CaCommand::ParentResponse(handle, child) => {
186 let uri = format!("api/v1/cas/{}/children/{}/contact", handle, child);
187 let info: ParentCaContact = get_json(&self.server, &self.token, &uri).await?;
188 Ok(ApiResponse::ParentCaContact(info))
189 }
190
191 CaCommand::ChildRequest(handle) => {
192 let uri = format!("api/v1/cas/{}/id/child_request.json", handle);
193 let req = get_json(&self.server, &self.token, &uri).await?;
194 Ok(ApiResponse::Rfc8183ChildRequest(req))
195 }
196
197 CaCommand::RepoPublisherRequest(handle) => {
198 let uri = format!("api/v1/cas/{}/id/publisher_request.json", handle);
199 let req: rfc8183::PublisherRequest = get_json(&self.server, &self.token, &uri).await?;
200 Ok(ApiResponse::Rfc8183PublisherRequest(req))
201 }
202
203 CaCommand::RepoDetails(handle) => {
204 let uri = format!("api/v1/cas/{}/repo", handle);
205 let details: CaRepoDetails = get_json(&self.server, &self.token, &uri).await?;
206 Ok(ApiResponse::RepoDetails(details))
207 }
208
209 CaCommand::RepoStatus(ca) => {
210 let uri = format!("api/v1/cas/{}/repo/status", ca);
211 let status: RepoStatus = get_json(&self.server, &self.token, &uri).await?;
212 Ok(ApiResponse::RepoStatus(status))
213 }
214
215 CaCommand::RepoUpdate(handle, update) => {
216 let uri = format!("api/v1/cas/{}/repo", handle);
217 post_json(&self.server, &self.token, &uri, update).await?;
218 Ok(ApiResponse::Empty)
219 }
220
221 CaCommand::AddParent(handle, parent_req) => {
222 let uri = format!("api/v1/cas/{}/parents", handle);
223 post_json(&self.server, &self.token, &uri, parent_req).await?;
224 Ok(ApiResponse::Empty)
225 }
226
227 CaCommand::RemoveParent(handle, parent) => {
228 let uri = format!("api/v1/cas/{}/parents/{}", handle, parent);
229 delete(&self.server, &self.token, &uri).await?;
230 Ok(ApiResponse::Empty)
231 }
232
233 CaCommand::ParentStatuses(handle) => {
234 let uri = format!("api/v1/cas/{}/parents", handle);
235 let statuses: ParentStatuses = get_json(&self.server, &self.token, &uri).await?;
236 Ok(ApiResponse::ParentStatuses(statuses))
237 }
238
239 CaCommand::MyParentCaContact(handle, parent) => {
240 let uri = format!("api/v1/cas/{}/parents/{}", handle, parent);
241 let parent: ParentCaContact = get_json(&self.server, &self.token, &uri).await?;
242 Ok(ApiResponse::ParentCaContact(parent))
243 }
244
245 CaCommand::Refresh(handle) => {
246 let uri = format!("api/v1/cas/{}/sync/parents", handle);
247 post_empty(&self.server, &self.token, &uri).await?;
248 Ok(ApiResponse::Empty)
249 }
250
251 CaCommand::ChildInfo(handle, child) => {
252 let uri = format!("api/v1/cas/{}/children/{}", handle, child);
253 let info: ChildCaInfo = get_json(&self.server, &self.token, &uri).await?;
254 Ok(ApiResponse::ChildInfo(info))
255 }
256
257 CaCommand::ChildAdd(handle, req) => {
258 let uri = format!("api/v1/cas/{}/children", handle);
259 let info: ParentCaContact = post_json_with_response(&self.server, &self.token, &uri, req).await?;
260 Ok(ApiResponse::ParentCaContact(info))
261 }
262 CaCommand::ChildUpdate(handle, child, req) => {
263 let uri = format!("api/v1/cas/{}/children/{}", handle, child);
264 post_json(&self.server, &self.token, &uri, req).await?;
265 Ok(ApiResponse::Empty)
266 }
267 CaCommand::ChildDelete(handle, child) => {
268 let uri = format!("api/v1/cas/{}/children/{}", handle, child);
269 delete(&self.server, &self.token, &uri).await?;
270 Ok(ApiResponse::Empty)
271 }
272 CaCommand::ChildConnections(handle) => {
273 let uri = format!("api/v1/cas/{}/stats/children/connections", handle);
274 let stats: ChildrenConnectionStats = get_json(&self.server, &self.token, &uri).await?;
275 Ok(ApiResponse::ChildrenStats(stats))
276 }
277
278 CaCommand::KeyRollInit(handle) => {
279 let uri = format!("api/v1/cas/{}/keys/roll_init", handle);
280 post_empty(&self.server, &self.token, &uri).await?;
281 Ok(ApiResponse::Empty)
282 }
283 CaCommand::KeyRollActivate(handle) => {
284 let uri = format!("api/v1/cas/{}/keys/roll_activate", handle);
285 post_empty(&self.server, &self.token, &uri).await?;
286 Ok(ApiResponse::Empty)
287 }
288
289 CaCommand::RouteAuthorizationsList(handle) => {
290 let uri = format!("api/v1/cas/{}/routes", handle);
291 let roas = get_json(&self.server, &self.token, &uri).await?;
292 Ok(ApiResponse::RouteAuthorizations(roas))
293 }
294
295 CaCommand::RouteAuthorizationsUpdate(handle, updates) => {
296 let uri = format!("api/v1/cas/{}/routes", handle);
297 post_json(&self.server, &self.token, &uri, updates).await?;
298 Ok(ApiResponse::Empty)
299 }
300
301 CaCommand::RouteAuthorizationsTryUpdate(handle, updates) => {
302 let uri = format!("api/v1/cas/{}/routes/try", handle);
303 let advice_opt: Option<BgpAnalysisAdvice> =
304 post_json_with_opt_response(&self.server, &self.token, &uri, updates).await?;
305 match advice_opt {
306 None => Ok(ApiResponse::Empty),
307 Some(advice) => Ok(ApiResponse::BgpAnalysisAdvice(advice)),
308 }
309 }
310
311 CaCommand::RouteAuthorizationsDryRunUpdate(handle, updates) => {
312 let uri = format!("api/v1/cas/{}/routes/analysis/dryrun", handle);
313 let report = post_json_with_response(&self.server, &self.token, &uri, updates).await?;
314 Ok(ApiResponse::BgpAnalysisFull(report))
315 }
316
317 CaCommand::BgpAnalysisFull(handle) => {
318 let uri = format!("api/v1/cas/{}/routes/analysis/full", handle);
319 let report = get_json(&self.server, &self.token, &uri).await?;
320 Ok(ApiResponse::BgpAnalysisFull(report))
321 }
322
323 CaCommand::BgpAnalysisSuggest(handle, resources) => {
324 let uri = format!("api/v1/cas/{}/routes/analysis/suggest", handle);
325
326 let suggestions = if let Some(resources) = resources {
327 post_json_with_response(&self.server, &self.token, &uri, resources).await?
328 } else {
329 get_json(&self.server, &self.token, &uri).await?
330 };
331
332 Ok(ApiResponse::BgpAnalysisSuggestions(suggestions))
333 }
334
335 CaCommand::AspasList(handle) => {
336 let uri = format!("api/v1/cas/{}/aspas", handle);
337 let aspas = get_json(&self.server, &self.token, &uri).await?;
338 Ok(ApiResponse::AspaDefinitions(aspas))
339 }
340
341 CaCommand::AspasAddOrReplace(handle, aspa) => {
342 let uri = format!("api/v1/cas/{}/aspas", handle);
343 let updates = AspaDefinitionUpdates::new(vec![aspa], vec![]);
344 post_json(&self.server, &self.token, &uri, updates).await?;
345 Ok(ApiResponse::Empty)
346 }
347
348 CaCommand::AspasRemove(handle, customer) => {
349 let uri = format!("api/v1/cas/{}/aspas", handle);
350 let updates = AspaDefinitionUpdates::new(vec![], vec![customer]);
351 post_json(&self.server, &self.token, &uri, updates).await?;
352 Ok(ApiResponse::Empty)
353 }
354
355 CaCommand::AspasUpdate(handle, customer, update) => {
356 let uri = format!("api/v1/cas/{}/aspas/as/{}", handle, customer);
357 post_json(&self.server, &self.token, &uri, update).await?;
358 Ok(ApiResponse::Empty)
359 }
360
361 CaCommand::Show(handle) => {
362 let uri = format!("api/v1/cas/{}", handle);
363 let ca_info = get_json(&self.server, &self.token, &uri).await?;
364
365 Ok(ApiResponse::CertAuthInfo(ca_info))
366 }
367
368 CaCommand::ShowHistoryCommands(handle, options) => {
369 let uri = format!(
370 "api/v1/cas/{}/history/commands/{}",
371 handle,
372 options.url_path_parameters()
373 );
374 let history = get_json(&self.server, &self.token, &uri).await?;
375
376 Ok(ApiResponse::CertAuthHistory(history))
377 }
378
379 CaCommand::ShowHistoryDetails(handle, key) => {
380 let uri = format!("api/v1/cas/{}/history/details/{}", handle, key);
381 let action = get_json(&self.server, &self.token, &uri).await?;
382
383 Ok(ApiResponse::CertAuthAction(action))
384 }
385
386 CaCommand::Issues(ca_opt) => match ca_opt {
387 Some(ca) => {
388 let uri = format!("api/v1/cas/{}/issues", ca);
389 let issues: CertAuthIssues = get_json(&self.server, &self.token, &uri).await?;
390 Ok(ApiResponse::CertAuthIssues(issues))
391 }
392 None => {
393 let issues: AllCertAuthIssues =
394 get_json(&self.server, &self.token, "api/v1/bulk/cas/issues").await?;
395 Ok(ApiResponse::AllCertAuthIssues(issues))
396 }
397 },
398
399 CaCommand::RtaList(ca) => {
400 let uri = format!("api/v1/cas/{}/rta/", ca);
401 let list = get_json(&self.server, &self.token, &uri).await?;
402 Ok(ApiResponse::RtaList(list))
403 }
404
405 CaCommand::RtaShow(ca, name, out) => {
406 let uri = format!("api/v1/cas/{}/rta/{}", ca, name);
407 let rta = get_json(&self.server, &self.token, &uri).await?;
408
409 match out {
410 None => Ok(ApiResponse::Rta(rta)),
411 Some(out) => {
412 file::save(rta.as_ref(), &out)?;
413 Ok(ApiResponse::Empty)
414 }
415 }
416 }
417
418 CaCommand::RtaSign(ca, name, request) => {
419 let uri = format!("api/v1/cas/{}/rta/{}/sign", ca, name);
420 post_json(&self.server, &self.token, &uri, request).await?;
421 Ok(ApiResponse::Empty)
422 }
423
424 CaCommand::RtaMultiPrep(ca, name, resources) => {
425 let uri = format!("api/v1/cas/{}/rta/{}/multi/prep", ca, name);
426 let response = post_json_with_response(&self.server, &self.token, &uri, resources).await?;
427 Ok(ApiResponse::RtaMultiPrep(response))
428 }
429
430 CaCommand::RtaMultiCoSign(ca, name, rta) => {
431 let uri = format!("api/v1/cas/{}/rta/{}/multi/cosign", ca, name);
432 post_json(&self.server, &self.token, &uri, rta).await?;
433 Ok(ApiResponse::Empty)
434 }
435
436 CaCommand::List => {
437 let cas = get_json(&self.server, &self.token, "api/v1/cas").await?;
438 Ok(ApiResponse::CertAuths(cas))
439 }
440 }
441 }
442
443 /// Processes the options, and returns a response ready for formatting.
444 /// Note that this function is public to help integration testing the API
445 /// and client.
publishers(&self, command: PubServerCommand) -> Result<ApiResponse, Error>446 pub async fn publishers(&self, command: PubServerCommand) -> Result<ApiResponse, Error> {
447 match command {
448 PubServerCommand::PublisherList => {
449 let list: PublisherList = get_json(&self.server, &self.token, "api/v1/pubd/publishers").await?;
450 Ok(ApiResponse::PublisherList(list))
451 }
452 PubServerCommand::StalePublishers(seconds) => {
453 let uri = format!("api/v1/pubd/stale/{}", seconds);
454 let stales = get_json(&self.server, &self.token, &uri).await?;
455 Ok(ApiResponse::PublisherList(stales))
456 }
457 PubServerCommand::RepositoryStats => {
458 let stats = get_json(&self.server, &self.token, "stats/repo").await?;
459 Ok(ApiResponse::RepoStats(stats))
460 }
461 PubServerCommand::RepositoryInit(uris) => {
462 let uri = "api/v1/pubd/init";
463 post_json(&self.server, &self.token, uri, uris).await?;
464 Ok(ApiResponse::Empty)
465 }
466 PubServerCommand::RepositoryClear => {
467 let uri = "api/v1/pubd/init";
468 delete(&self.server, &self.token, uri).await?;
469 Ok(ApiResponse::Empty)
470 }
471 PubServerCommand::AddPublisher(req) => {
472 let res = post_json_with_response(&self.server, &self.token, "api/v1/pubd/publishers", req).await?;
473 Ok(ApiResponse::Rfc8183RepositoryResponse(res))
474 }
475 PubServerCommand::RemovePublisher(handle) => {
476 let uri = format!("api/v1/pubd/publishers/{}", handle);
477 delete(&self.server, &self.token, &uri).await?;
478 Ok(ApiResponse::Empty)
479 }
480 PubServerCommand::ShowPublisher(handle) => {
481 let uri = format!("api/v1/pubd/publishers/{}", handle);
482 let details: PublisherDetails = get_json(&self.server, &self.token, &uri).await?;
483 Ok(ApiResponse::PublisherDetails(details))
484 }
485 PubServerCommand::RepositoryResponse(handle) => {
486 let uri = format!("api/v1/pubd/publishers/{}/response.json", handle);
487 let res = get_json(&self.server, &self.token, &uri).await?;
488 Ok(ApiResponse::Rfc8183RepositoryResponse(res))
489 }
490 }
491 }
492
init_config(&self, details: KrillInitDetails) -> Result<ApiResponse, Error>493 fn init_config(&self, details: KrillInitDetails) -> Result<ApiResponse, Error> {
494 let defaults = include_str!("../../defaults/krill.conf");
495 let multi_add_on = include_str!("../../defaults/krill-multi-user.conf");
496
497 let mut config = defaults.to_string();
498 config = config.replace("### admin_token =", &format!("admin_token = \"{}\"", self.token));
499
500 config = config.replace(
501 "### service_uri = \"https://localhost:3000/\"",
502 &format!("service_uri = \"{}\"", self.server),
503 );
504
505 if let Some(data_dir) = details.data_dir() {
506 config = config.replace("### data_dir = \"./data\"", &format!("data_dir = \"{}\"", data_dir))
507 }
508
509 if let Some(log_file) = details.log_file() {
510 config = config.replace(
511 "### log_file = \"./krill.log\"",
512 &format!("log_file = \"{}\"", log_file),
513 )
514 }
515
516 if details.multi_user() {
517 config.push_str("\n\n\n");
518 config.push_str(multi_add_on);
519 }
520
521 let c: Config = toml::from_slice(config.as_ref()).map_err(Error::init)?;
522 c.verify().map_err(Error::init)?;
523
524 Ok(ApiResponse::GenericBody(config))
525 }
526
527 #[cfg(feature = "multi-user")]
528 #[allow(clippy::unnecessary_wraps)]
user(&self, details: KrillUserDetails) -> Result<ApiResponse, Error>529 fn user(&self, details: KrillUserDetails) -> Result<ApiResponse, Error> {
530 let (password_hash, salt) = {
531 use scrypt::scrypt;
532
533 let password = rpassword::read_password_from_tty(Some("Enter the password to hash: ")).unwrap();
534
535 // The scrypt-js NPM documentation (https://www.npmjs.com/package/scrypt-js) says:
536 // "TL;DR - either only allow ASCII characters in passwords, or use
537 // String.prototype.normalize('NFKC') on any password"
538 // So in Lagosta we do the NFKC normalization and thus we need to do the same here.
539 use unicode_normalization::UnicodeNormalization;
540
541 let user_id = details.id().nfkc().collect::<String>();
542 let password = password.trim().nfkc().collect::<String>();
543 let params = scrypt::Params::new(PW_HASH_LOG_N, PW_HASH_R, PW_HASH_P).unwrap();
544
545 // hash twice with two different salts
546 // hash first with a salt the client browser knows how to construct based on the users id and a site
547 // specific string.
548
549 let weak_salt = format!("krill-lagosta-{}", user_id);
550 let weak_salt = weak_salt.nfkc().collect::<String>();
551
552 let mut interim_hash: [u8; 32] = [0; 32];
553 scrypt(password.as_bytes(), weak_salt.as_bytes(), ¶ms, &mut interim_hash).unwrap();
554
555 // hash again using a strong random salt only known to the server
556 let mut strong_salt: [u8; 32] = [0; 32];
557 openssl::rand::rand_bytes(&mut strong_salt).unwrap();
558 let mut final_hash: [u8; 32] = [0; 32];
559 scrypt(&interim_hash, &strong_salt, ¶ms, &mut final_hash).unwrap();
560
561 (final_hash, strong_salt)
562 };
563
564 // Due to https://github.com/alexcrichton/toml-rs/issues/406 we cannot
565 // produce inline table style TOML by serializing from config structs to
566 // a string using the toml crate. Instead we build it up ourselves.
567 let attrs = details.attrs();
568 let attrs_fragment = match attrs.is_empty() {
569 false => format!(
570 "attributes={{ {} }}, ",
571 attrs
572 .iter()
573 // quote the key if needed
574 .map(|(k, v)| match k.contains(' ') {
575 true => (format!(r#""{}""#, k), v),
576 false => (k.clone(), v),
577 })
578 // quote the value
579 .map(|(k, v)| format!(r#"{}="{}""#, k, v))
580 .collect::<Vec<String>>()
581 .join(", ")
582 ),
583 true => String::new(),
584 };
585
586 let toml = format!(
587 r#"
588 [auth_users]
589 "{id}" = {{ {attrs}password_hash="{ph}", salt="{salt}" }}"#,
590 id = details.id(),
591 attrs = attrs_fragment,
592 ph = hex::encode(password_hash),
593 salt = hex::encode(salt),
594 );
595
596 Ok(ApiResponse::GenericBody(toml))
597 }
598 }
599
600 //------------ Error ---------------------------------------------------------
601
602 #[derive(Debug)]
603 #[allow(clippy::large_enum_variant)]
604 pub enum Error {
605 MissingCommand,
606 ServerDown,
607 HttpClientError(httpclient::Error),
608 ReportError(ReportError),
609 IoError(KrillIoError),
610 EmptyResponse,
611 Rfc8183(rfc8183::Error),
612 InitError(String),
613 InputError(String),
614 }
615
616 impl fmt::Display for Error {
fmt(&self, f: &mut fmt::Formatter) -> fmt::Result617 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
618 match self {
619 Error::MissingCommand => write!(f, "No valid command given, see --help"),
620 Error::ServerDown => write!(f, "Server is not available."),
621 Error::HttpClientError(e) => write!(f, "Http client error: {}", e),
622 Error::ReportError(e) => e.fmt(f),
623 Error::IoError(e) => write!(f, "I/O error: {}", e),
624 Error::EmptyResponse => write!(f, "Empty response received from server"),
625 Error::Rfc8183(e) => e.fmt(f),
626 Error::InitError(s) => s.fmt(f),
627 Error::InputError(s) => s.fmt(f),
628 }
629 }
630 }
631
632 impl Error {
init(msg: impl fmt::Display) -> Self633 fn init(msg: impl fmt::Display) -> Self {
634 Error::InitError(msg.to_string())
635 }
636 }
637
638 impl From<httpclient::Error> for Error {
from(e: httpclient::Error) -> Self639 fn from(e: httpclient::Error) -> Self {
640 Error::HttpClientError(e)
641 }
642 }
643
644 impl From<KrillIoError> for Error {
from(e: KrillIoError) -> Self645 fn from(e: KrillIoError) -> Self {
646 Error::IoError(e)
647 }
648 }
649
650 impl From<ReportError> for Error {
from(e: ReportError) -> Self651 fn from(e: ReportError) -> Self {
652 Error::ReportError(e)
653 }
654 }
655
656 impl From<rfc8183::Error> for Error {
from(e: rfc8183::Error) -> Error657 fn from(e: rfc8183::Error) -> Error {
658 Error::Rfc8183(e)
659 }
660 }
661
662 //------------ Tests ---------------------------------------------------------
663
664 #[cfg(test)]
665 mod tests {
666
667 use super::*;
668 use crate::cli::options::KrillInitDetails;
669 use crate::test;
670
671 #[test]
init_config_file()672 fn init_config_file() {
673 let mut details = KrillInitDetails::default();
674 details.with_data_dir("/var/lib/krill/data/");
675 details.with_log_file("/var/log/krill/krill.log");
676
677 let client = KrillClient {
678 server: test::https("https://localhost:3001/"),
679 token: Token::from("secret"),
680 };
681
682 let res = client.init_config(details).unwrap();
683
684 match res {
685 ApiResponse::GenericBody(body) => {
686 let expected = include_str!("../../test-resources/krill-init.conf");
687 assert_eq!(expected, &body)
688 }
689 _ => panic!("Expected body"),
690 }
691 }
692
693 #[test]
init_multi_user_config_file()694 fn init_multi_user_config_file() {
695 let mut details = KrillInitDetails::multi_user_dflt();
696 details.with_data_dir("/var/lib/krill/data/");
697 details.with_log_file("/var/log/krill/krill.log");
698
699 let client = KrillClient {
700 server: test::https("https://localhost:3001/"),
701 token: Token::from("secret"),
702 };
703
704 let res = client.init_config(details).unwrap();
705
706 match res {
707 ApiResponse::GenericBody(body) => {
708 let expected = include_str!("../../test-resources/krill-init-multi-user.conf");
709 assert_eq!(expected, &body)
710 }
711 _ => panic!("Expected body"),
712 }
713 }
714 }
715