1use std::path::Path;
4
5use crate::client::{Api, Pre, Warning};
6use crate::keys::TokenKeys;
7use crate::proto::FormatError;
8use crate::proto::api::{Scope, VaultStatus, VersionMeta};
9use crate::proto::audit::ChainHead;
10use crate::proto::children::is_reserved_name;
11use crate::proto::ids::TokenId;
12use zeroize::Zeroizing;
13
14use crate::audit::{self, AuditReport};
15use crate::check::{scan_literals, validate};
16use crate::error::{Error, code};
17use crate::state::StateStore;
18use crate::value::{ConfigDocument, Entry, Expect, Expiry, NewConfig, SecretSet, SecretValue};
19use crate::vault::{Handle, Item, Pins};
20
21pub struct Vault {
25 pub(crate) inner: Handle,
26 scope: Scope,
27}
28
29impl std::fmt::Debug for Vault {
30 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31 f.debug_struct("Vault")
32 .field("scope", &self.scope())
33 .field("vault_id", &self.vault_id())
34 .finish()
35 }
36}
37
38fn refuse_reserved(name: &str) -> Result<(), Error> {
39 if is_reserved_name(name) || name.is_empty() {
40 return Err(Error::local(
41 code::INVALID_NAME,
42 "a name must not be empty, and names beginning with \"gv:\" are reserved for galata-vault's own records",
43 ));
44 }
45 Ok(())
46}
47
48#[cfg(feature = "http")]
49fn set(var: &str) -> Option<std::ffi::OsString> {
50 std::env::var_os(var).filter(|v| !v.is_empty())
51}
52
53pub(crate) fn parse_token(token: &str) -> Result<TokenKeys, Error> {
55 TokenKeys::parse(token).map_err(|e| Error::Auth {
56 code: code::INVALID_TOKEN.to_owned(),
57 message: match e {
58 FormatError::UnknownVersion { found } => {
59 format!("the token carries version {found}, which this build does not know")
60 }
61 _ => "the token is not a valid gvt1_ token (its checksum does not match)".to_owned(),
62 },
63 })
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68#[non_exhaustive]
69pub enum TokenFileCheck {
70 Permissions,
73 CallerVerified,
76}
77
78#[cfg_attr(not(feature = "http"), allow(dead_code))]
79fn read_token_file(path: &Path, check: TokenFileCheck) -> Result<Zeroizing<String>, Error> {
80 let unreadable = |e: std::io::Error| {
81 Error::local(
82 code::INVALID_TOKEN_FILE,
83 format!(
84 "cannot read the token file {}: {}",
85 path.display(),
86 e.kind()
87 ),
88 )
89 };
90 let meta = std::fs::metadata(path).map_err(unreadable)?;
91 #[cfg(unix)]
92 {
93 use std::os::unix::fs::PermissionsExt;
94 let _ = check;
95 let mode = meta.permissions().mode() & 0o777;
96 if mode != 0o600 && mode != 0o400 {
97 return Err(Error::local(
98 code::INVALID_TOKEN_FILE,
99 format!(
100 "the token file {} has mode {mode:04o}; it must be 0600 or 0400",
101 path.display()
102 ),
103 ));
104 }
105 }
106 #[cfg(not(unix))]
107 {
108 let _ = meta;
109 if check != TokenFileCheck::CallerVerified {
110 return Err(Error::local(
111 code::INVALID_TOKEN_FILE,
112 format!(
113 "the token file {} is refused: its permissions cannot be verified on this \
114 platform. Check its ACL yourself and open it with \
115 TokenFileCheck::CallerVerified, or pass the token another way",
116 path.display()
117 ),
118 ));
119 }
120 }
121 let text = Zeroizing::new(std::fs::read_to_string(path).map_err(unreadable)?);
122 let mut words = text.split_whitespace();
123 let (Some(token), None) = (words.next(), words.next()) else {
124 return Err(Error::local(
125 code::INVALID_TOKEN_FILE,
126 format!(
127 "the token file {} must hold exactly one gvt1_ token and nothing else",
128 path.display()
129 ),
130 ));
131 };
132 Ok(Zeroizing::new(token.to_owned()))
133}
134
135fn entries(items: Vec<Item>, with_deleted: bool) -> Vec<Entry> {
136 items
137 .into_iter()
138 .filter(|i| (with_deleted || !i.tombstone) && !is_reserved_name(&i.name))
139 .map(|i| Entry {
140 name: i.name,
141 version: i.version,
142 updated_at: i.updated_at,
143 size: i.size,
144 deleted: i.tombstone,
145 })
146 .collect()
147}
148
149impl Vault {
150 pub(crate) fn from_handle(inner: Handle) -> Vault {
151 let scope = inner.scope().unwrap_or(Scope::Admin);
153 Vault { inner, scope }
154 }
155
156 #[cfg(feature = "http")]
161 pub fn new(token: &str, server: &str) -> Result<Vault, Error> {
162 let keys = parse_token(token)?;
163 let api = crate::client::ClientBuilder::new(server)
164 .build()
165 .map_err(Error::build)?;
166 Vault::open(keys, &api)
167 }
168
169 pub fn with_api(token: &str, api: &Api) -> Result<Vault, Error> {
173 Vault::open(parse_token(token)?, api)
174 }
175
176 fn open(keys: TokenKeys, api: &Api) -> Result<Vault, Error> {
177 let inner = Handle::open_token(api, keys, "the token's vault")?;
178 Ok(Vault::from_handle(inner))
179 }
180
181 #[cfg(feature = "http")]
186 pub fn from_token_file(path: impl AsRef<Path>, server: &str) -> Result<Vault, Error> {
187 Vault::from_token_file_with(path, server, TokenFileCheck::Permissions)
188 }
189
190 #[cfg(feature = "http")]
194 pub fn from_token_file_with(
195 path: impl AsRef<Path>,
196 server: &str,
197 check: TokenFileCheck,
198 ) -> Result<Vault, Error> {
199 let token = read_token_file(path.as_ref(), check)?;
200 Vault::new(&token, server)
201 }
202
203 #[cfg(feature = "http")]
206 pub fn from_env() -> Result<Vault, Error> {
207 let (token, file) = (set("GV_TOKEN"), set("GV_TOKEN_FILE"));
208 if token.is_some() && file.is_some() {
209 return Err(Error::local(
210 code::INVALID_ENVIRONMENT,
211 "GV_TOKEN and GV_TOKEN_FILE are both set; set exactly one",
212 ));
213 }
214 let server = set("GV_SERVER").ok_or_else(|| {
215 Error::local(
216 code::MISSING_SERVER,
217 "GV_SERVER is not set; a token needs the server it belongs to",
218 )
219 })?;
220 let server = server.to_string_lossy().into_owned();
221 match (token, file) {
222 (Some(token), None) => {
223 let token = Zeroizing::new(token.to_string_lossy().into_owned());
224 Vault::new(&token, &server)
225 }
226 (None, Some(file)) => Vault::from_token_file(file, &server),
227 _ => Err(Error::Auth {
228 code: code::INVALID_TOKEN.to_owned(),
229 message: "neither GV_TOKEN nor GV_TOKEN_FILE is set".to_owned(),
230 }),
231 }
232 }
233
234 pub fn scope(&self) -> Scope {
238 self.scope
239 }
240
241 pub fn vault_id(&self) -> String {
243 self.inner.vault_id.to_hex()
244 }
245
246 pub fn label(&self) -> &str {
249 &self.inner.label
250 }
251
252 pub fn expiry(&self) -> Expiry {
254 Expiry {
255 token_expires_at: self.inner.token_expires_at(),
256 vault_expires_at: self.inner.last_expires_at(),
257 }
258 }
259
260 pub fn refresh(&self) -> Result<(), Error> {
265 self.inner.refresh()
266 }
267
268 pub fn status(&self) -> Result<VaultStatus, Error> {
270 self.inner.refresh_status()
271 }
272
273 pub fn tokens(&self) -> Result<Vec<crate::owner::TokenInfo>, Error> {
282 if self.scope != Scope::Admin {
283 return Err(Error::forbidden(format!(
284 "{}: listing tokens needs an admin token; this one is {}",
285 self.label(),
286 self.scope
287 )));
288 }
289 let status = self.inner.refresh_status()?;
290 let summaries = status.tokens.ok_or_else(|| {
294 Error::other(format!(
295 "{}: the server did not send this vault's token list",
296 self.label()
297 ))
298 })?;
299 let names: std::collections::HashMap<_, _> = self
300 .inner
301 .list()?
302 .into_iter()
303 .map(|i| (i.hmac, i.name))
304 .collect();
305 Ok(summaries
306 .into_iter()
307 .map(|t| crate::owner::TokenInfo {
308 id: t.token_id,
309 scope: t.scope,
310 created_at: t.created_at,
311 expires_at: t.expires_at,
312 only: t.allow_list.map(|l| {
313 l.iter()
314 .map(|h| names.get(h).cloned().unwrap_or_else(|| "?".into()))
315 .collect()
316 }),
317 })
318 .collect())
319 }
320
321 pub fn revoke(&self, id: &TokenId) -> Result<crate::owner::Revocation, Error> {
332 if self.scope != Scope::Admin {
333 return Err(Error::forbidden(format!(
334 "{}: revoking a token needs an admin token; this one is {}",
335 self.label(),
336 self.scope
337 )));
338 }
339 let status = self.inner.refresh_status()?;
340 let scope = status
341 .tokens
342 .as_ref()
343 .and_then(|ts| ts.iter().find(|t| t.token_id == *id))
344 .map(|t| t.scope.clone())
345 .ok_or_else(|| Error::not_found(format!("{}: has no token {id}", self.label())))?;
346 self.inner.revoke(id)?;
347 let revocation = crate::owner::Revocation {
348 scope,
349 rotated_to: None,
350 };
351 if revocation.forward_only() {
352 self.inner
353 .api()
354 .events()
355 .warning(&Warning::ForwardOnlyRevocation {
356 path: self.label().to_owned(),
357 token: id.to_hex(),
358 scope: revocation.scope.to_string(),
359 held: revocation.held_keys(),
360 });
361 }
362 Ok(revocation)
363 }
364
365 pub fn verify_audit(&self, known: Option<ChainHead>) -> Result<AuditReport, Error> {
373 audit::verify(&self.inner, known, 0)
374 }
375
376 pub fn audit(&self, store: &dyn StateStore) -> Result<AuditReport, Error> {
380 let mut state = store.load().map_err(Error::store)?;
381 let report = audit::verify_recorded(&self.inner, &mut state, 0)?;
382 store.save(&state).map_err(Error::store)?;
383 Ok(report)
384 }
385
386 pub fn pins(&self) -> Pins {
390 self.inner.pins()
391 }
392
393 pub fn set_pins(&self, pins: Pins) -> Result<(), Error> {
397 self.inner.set_pins(pins)
398 }
399
400 fn read(&self, name: &str, version: Option<u64>) -> Result<SecretValue, Error> {
403 let v = self.inner.view();
404 if !self.inner.may_read(&v, &v.bundle.name_key().hmac(name)) {
405 return Err(Error::forbidden(format!(
406 "{}: this token may not read {name}",
407 self.inner.label
408 )));
409 }
410 let (version, opened) = self.inner.get(name, version)?;
411 Ok(SecretValue {
412 name: name.to_owned(),
413 version,
414 value: opened.value,
415 })
416 }
417
418 pub fn secret(&self, name: &str) -> Result<SecretValue, Error> {
420 self.read(name, None)
421 }
422
423 pub fn secret_version(&self, name: &str, version: u64) -> Result<SecretValue, Error> {
425 self.read(name, Some(version))
426 }
427
428 pub fn secrets(&self, names: &[&str]) -> Result<SecretSet, Error> {
431 let mut values = Vec::with_capacity(names.len());
432 for name in names {
433 values.push(self.read(name, None)?);
434 }
435 Ok(SecretSet { values })
436 }
437
438 pub fn readable(&self, only: Option<&[&str]>) -> Result<SecretSet, Error> {
443 let v = self.inner.view();
444 self.inner.vault_secret(&v)?;
445 let all = self.inner.list()?;
446 let only = only.unwrap_or(&[]);
447 for want in only {
448 if !all.iter().any(|i| i.name == *want && !i.tombstone) {
449 return Err(Error::not_found(format!(
450 "{}: no secret named {want}",
451 self.inner.label
452 )));
453 }
454 }
455 let mut values = Vec::new();
456 for item in all {
457 if item.tombstone
458 || is_reserved_name(&item.name)
459 || (!only.is_empty() && !only.contains(&item.name.as_str()))
460 || !self.inner.may_read(&v, &item.hmac)
461 {
462 continue;
463 }
464 let (version, opened) = self.inner.get(&item.name, Some(item.version))?;
465 values.push(SecretValue {
466 name: item.name,
467 version,
468 value: opened.value,
469 });
470 }
471 Ok(SecretSet { values })
472 }
473
474 pub fn list(&self) -> Result<Vec<Entry>, Error> {
477 Ok(entries(self.inner.list()?, false))
478 }
479
480 pub fn list_all(&self) -> Result<Vec<Entry>, Error> {
483 Ok(entries(self.inner.list()?, true))
484 }
485
486 pub fn history(&self, name: &str) -> Result<Vec<VersionMeta>, Error> {
489 self.inner.versions(name)
490 }
491
492 pub fn set_secret(&self, name: &str, value: &[u8]) -> Result<u64, Error> {
495 refuse_reserved(name)?;
496 let pre = self.inner.current(name)?;
497 self.inner.put(name, value, pre)
498 }
499
500 pub fn set_secret_expecting(
503 &self,
504 name: &str,
505 value: &[u8],
506 expect: Expect,
507 ) -> Result<u64, Error> {
508 refuse_reserved(name)?;
509 let pre = match expect {
510 Expect::Version(v) => Pre::Update(v),
511 Expect::Absent => match self.inner.current(name)? {
513 Pre::Update(v) => Pre::Revive(v),
514 other => other,
515 },
516 };
517 self.inner.put(name, value, pre)
518 }
519
520 pub fn delete_secret(&self, name: &str, expected: Option<u64>) -> Result<u64, Error> {
524 refuse_reserved(name)?;
525 let label = &self.inner.label;
526 let version = match expected {
527 Some(v) => v,
528 None => match self.inner.current(name)? {
529 Pre::Create => {
530 return Err(Error::not_found(format!("{label}: no secret named {name}")));
531 }
532 Pre::Revive(v) => {
533 return Err(Error::not_found(format!(
534 "{label}: {name} is already deleted (version {v})"
535 )));
536 }
537 Pre::Update(v) => v,
538 },
539 };
540 self.inner.delete(name, version)
541 }
542
543 fn read_config(&self, name: &str, version: Option<u64>) -> Result<ConfigDocument, Error> {
546 let (version, opened) = self.inner.get_config(name, version)?;
547 Ok(ConfigDocument {
548 name: name.to_owned(),
549 version,
550 format: opened.format,
551 body: opened.body,
552 })
553 }
554
555 pub fn config(&self, name: &str) -> Result<ConfigDocument, Error> {
557 self.read_config(name, None)
558 }
559
560 pub fn config_version(&self, name: &str, version: u64) -> Result<ConfigDocument, Error> {
562 self.read_config(name, Some(version))
563 }
564
565 pub fn list_configs(&self) -> Result<Vec<Entry>, Error> {
567 Ok(entries(self.inner.list_configs()?, false))
568 }
569
570 pub fn list_configs_all(&self) -> Result<Vec<Entry>, Error> {
572 Ok(entries(self.inner.list_configs()?, true))
573 }
574
575 pub fn config_history(&self, name: &str) -> Result<Vec<VersionMeta>, Error> {
578 self.inner.config_versions(name)
579 }
580
581 pub fn set_config(&self, name: &str, config: NewConfig) -> Result<u64, Error> {
587 refuse_reserved(name)?;
588 validate(config.format, &config.body)?;
589 if !config.allow_literals {
590 scan_literals(&config.body)?;
591 }
592 let pre = match config.expect {
593 Some(Expect::Version(v)) => Pre::Update(v),
594 Some(Expect::Absent) => match self.inner.config_current(name)? {
596 Pre::Update(v) => Pre::Revive(v),
597 other => other,
598 },
599 None => self.inner.config_current(name)?,
600 };
601 self.inner
602 .put_config(name, config.format, &config.body, pre)
603 }
604
605 pub fn delete_config(&self, name: &str, expected: Option<u64>) -> Result<u64, Error> {
609 refuse_reserved(name)?;
610 let label = &self.inner.label;
611 let version = match expected {
612 Some(v) => v,
613 None => match self.inner.config_current(name)? {
614 Pre::Create => {
615 return Err(Error::not_found(format!("{label}: no config named {name}")));
616 }
617 Pre::Revive(v) => {
618 return Err(Error::not_found(format!(
619 "{label}: config {name} is already deleted (version {v})"
620 )));
621 }
622 Pre::Update(v) => v,
623 },
624 };
625 self.inner.delete_config(name, version)
626 }
627}
628
629#[derive(Debug, Clone, PartialEq, Eq)]
631#[non_exhaustive]
632pub struct LeakReport {
633 pub token_id: String,
635 pub revoked: bool,
637}
638
639pub fn report_leaked_token(api: &Api, token: &str) -> Result<LeakReport, Error> {
642 let keys = parse_token(token)?;
643 let id = keys.id();
644 let response = Handle::report_token(api, &keys)?;
645 Ok(LeakReport {
646 token_id: id.to_string(),
647 revoked: response.revoked.contains(&id),
648 })
649}
650
651#[cfg(test)]
652mod tests {
653 use super::*;
654
655 #[test]
656 fn a_vault_is_send_and_sync() {
657 fn shared<T: Send + Sync>() {}
658 shared::<Vault>();
659 }
660
661 #[cfg(feature = "http")]
662 #[test]
663 fn construction_checks_before_any_request() {
664 use crate::proto::ids::VaultId;
665 let e = Vault::new("gvt1_nonsense", "https://vault.example").unwrap_err();
666 assert_eq!(e.code(), code::INVALID_TOKEN);
667 let token = TokenKeys::generate(VaultId([1; 16])).token_string();
668 let e = Vault::new(&token, "http://vault.example").unwrap_err();
669 assert_eq!(e.code(), code::INVALID_SERVER);
670 assert!(e.message().contains("https://"), "{e}");
671 assert!(!e.message().contains(token.as_str()));
672 }
673
674 #[test]
675 fn a_token_of_another_version_is_refused() {
676 let other = crate::proto::codec::encode_checked("gvt2_", &[7u8; 48]);
677 let e = parse_token(&other).unwrap_err();
678 assert_eq!(
679 (e.code(), e.kind()),
680 (code::INVALID_TOKEN, crate::ErrorKind::Auth)
681 );
682 assert!(e.message().contains("version 2"), "{e}");
683 assert!(!e.message().contains(&other[5..]), "{e}");
684 }
685
686 #[cfg(not(unix))]
689 #[test]
690 fn a_token_file_fails_closed_where_permissions_cannot_be_checked() {
691 let dir = tempfile::tempdir().unwrap();
692 let path = dir.path().join("token");
693 let token = TokenKeys::generate(crate::proto::ids::VaultId([2; 16])).token_string();
694 std::fs::write(&path, token.as_str()).unwrap();
695 let e = read_token_file(&path, TokenFileCheck::Permissions).unwrap_err();
696 assert_eq!(e.code(), code::INVALID_TOKEN_FILE);
697 assert!(e.message().contains("cannot be verified"), "{e}");
698 assert!(!e.message().contains(token.as_str()));
699 let read = read_token_file(&path, TokenFileCheck::CallerVerified).unwrap();
700 assert_eq!(read.as_str(), token.as_str());
701 }
702
703 #[cfg(unix)]
704 #[test]
705 fn a_token_file_mode_is_checked_whatever_the_caller_says() {
706 use std::os::unix::fs::PermissionsExt;
707 let dir = tempfile::tempdir().unwrap();
708 let path = dir.path().join("token");
709 std::fs::write(&path, "gvt1_x").unwrap();
710 std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).unwrap();
711 for check in [TokenFileCheck::Permissions, TokenFileCheck::CallerVerified] {
712 let e = read_token_file(&path, check).unwrap_err();
713 assert!(e.message().contains("0644"), "{e}");
714 }
715 }
716}