Skip to main content

galata_vault/
token.rs

1//! The token client: one vault, opened with an access token.
2
3use 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
21/// One environment's vault. Opened with a token it is a token client; an
22/// [`crate::owner::Environment`] derefs to one for its record operations.
23/// `Send + Sync`: one handle serves every thread.
24pub 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
53/// Check a token string before any request.
54pub(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/// How a token file's permissions are established before it is read.
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68#[non_exhaustive]
69pub enum TokenFileCheck {
70    /// This crate checks them: on Unix the mode must be 0600 or 0400. Where
71    /// it cannot check them (Windows), the file is refused.
72    Permissions,
73    /// The caller has verified the file's access control itself (a Windows
74    /// ACL, say). On Unix the mode is still checked.
75    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        // An owner-opened handle holds every key an admin token holds.
152        let scope = inner.scope().unwrap_or(Scope::Admin);
153        Vault { inner, scope }
154    }
155
156    /// Open the vault `token` belongs to, over HTTP. The token's checksum is
157    /// checked before any request, and the server must be `https://`, or
158    /// `http://` on loopback. The vault is then verified from the vault id
159    /// the token carries.
160    #[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    /// Open the vault `token` belongs to over `api`: any transport, with the
170    /// observer the API carries (a proxy, a test transport, a
171    /// [`crate::client::ClientBuilder`] with a private CA, …).
172    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    /// Open the vault of the token in `path`. On Unix the file's mode must
182    /// be 0600 or 0400, checked before its contents are read; on other
183    /// platforms the file is refused (see [`Vault::from_token_file_with`]).
184    /// It must hold one token and nothing else.
185    #[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    /// As [`Vault::from_token_file`], stating how the file's permissions
191    /// were established. [`TokenFileCheck::CallerVerified`] is the only way
192    /// to read a token file where this crate cannot check its permissions.
193    #[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    /// Open the vault named by the environment: `GV_SERVER`, and exactly one
204    /// of `GV_TOKEN` or `GV_TOKEN_FILE`. Both set is refused, not ranked.
205    #[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    /// The token's scope, stored when the vault was opened. An owner-opened
235    /// handle reports [`Scope::Admin`]: the owner holds every key an admin
236    /// token holds.
237    pub fn scope(&self) -> Scope {
238        self.scope
239    }
240
241    /// The vault id (hex), pinned from the token string or the owner key.
242    pub fn vault_id(&self) -> String {
243        self.inner.vault_id.to_hex()
244    }
245
246    /// How errors and notices name this vault: the environment path, or
247    /// "the token's vault".
248    pub fn label(&self) -> &str {
249        &self.inner.label
250    }
251
252    /// The token's expiry, and the latest vault expiry the server announced.
253    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    /// Fetch the vault's current generation again (after a rotation):
261    /// the token's bundle and the descriptor, verified from the pinned vault
262    /// id. A handle that met [`Error::StaleGeneration`] works again after
263    /// this, without being rebuilt.
264    pub fn refresh(&self) -> Result<(), Error> {
265        self.inner.refresh()
266    }
267
268    /// The vault's status, fetched now, its descriptor verified.
269    pub fn status(&self) -> Result<VaultStatus, Error> {
270        self.inner.refresh_status()
271    }
272
273    /// This vault's tokens, for an `admin` token: the same view the owner
274    /// gets from [`crate::owner::Environment::tokens`], with a read token's
275    /// allow-list names decrypted where this credential can read them.
276    ///
277    /// The server sends the list to the owner and to `admin` and to nobody
278    /// else (`docs/spec/http-api.md#2`), so every other scope is refused
279    /// here by name rather than shown an empty list, which would say the
280    /// vault has no tokens.
281    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        // Absent is not empty: an admin credential should have been sent the
291        // list, so a missing one is the server withholding it, not a vault
292        // without tokens.
293        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    /// Revoke token `id`, for an `admin` token. The server drops it at once;
322    /// this cannot rotate, so the revocation is always forward-only and the
323    /// keys the token's bundle held keep working on whatever its holder
324    /// already copied. Rotation needs the owner key
325    /// ([`crate::owner::Environment::revoke`] with `rotate`).
326    ///
327    /// The scope is read from the vault's token list first, so the returned
328    /// [`crate::owner::Revocation`] can name what the revoked token held,
329    /// and a [`Warning::ForwardOnlyRevocation`] is raised for every scope
330    /// but `meta`, exactly as the owner path raises it.
331    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    // ------------------------------------------------------------ integrity
366
367    /// Fetch and verify this vault's audit chain. Pass the head a previous
368    /// call returned (kept by you, across handles or runs) to check that the
369    /// server's history only grew since; `None` verifies from the first row
370    /// served. Every scope may do this. A chain that does not continue from
371    /// `known` is an error with code `audit_mismatch`.
372    pub fn verify_audit(&self, known: Option<ChainHead>) -> Result<AuditReport, Error> {
373        audit::verify(&self.inner, known, 0)
374    }
375
376    /// Verify the audit chain from the head recorded in `store` for this
377    /// vault, and record the new head. The recorded head moves only when the
378    /// whole fetched range verifies.
379    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    /// What this handle has seen: the descriptor it verified and the latest
387    /// version of every record it read. Persist it and pass it to
388    /// [`Vault::set_pins`] on a later handle to detect rollback.
389    pub fn pins(&self) -> Pins {
390        self.inner.pins()
391    }
392
393    /// Adopt pins kept from earlier. An older or different generation than
394    /// the pinned one fails with `generation_rollback`; later reads of a
395    /// record older than its pinned version fail with `version_rollback`.
396    pub fn set_pins(&self, pins: Pins) -> Result<(), Error> {
397        self.inner.set_pins(pins)
398    }
399
400    // ------------------------------------------------------------ secrets
401
402    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    /// The latest version of secret `name`.
419    pub fn secret(&self, name: &str) -> Result<SecretValue, Error> {
420        self.read(name, None)
421    }
422
423    /// Version `version` of secret `name`.
424    pub fn secret_version(&self, name: &str, version: u64) -> Result<SecretValue, Error> {
425        self.read(name, Some(version))
426    }
427
428    /// Every named secret, or an error naming the first that could not be
429    /// returned. Never a partial set.
430    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    /// Every live secret this credential may read, or, with `only`, the
439    /// readable ones among those names, each of which must exist. All or
440    /// nothing: the first failure is the result. This is what `gv run` and
441    /// the Python package's `load_env` inject.
442    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    /// Every live secret, sorted by name. Each listed record's signature is
475    /// verified.
476    pub fn list(&self) -> Result<Vec<Entry>, Error> {
477        Ok(entries(self.inner.list()?, false))
478    }
479
480    /// Every secret, deleted ones included ([`Entry::deleted`]), sorted by
481    /// name.
482    pub fn list_all(&self) -> Result<Vec<Entry>, Error> {
483        Ok(entries(self.inner.list()?, true))
484    }
485
486    /// A secret's versions, oldest first, each verified; empty if it never
487    /// existed.
488    pub fn history(&self, name: &str) -> Result<Vec<VersionMeta>, Error> {
489        self.inner.versions(name)
490    }
491
492    /// Create `name`, or update it from its current version; returns the new
493    /// version. A write that loses a race fails with a conflict.
494    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    /// Write `name` only if it stands as `expect` says; returns the new
501    /// version. [`Expect::Absent`] also revives a deleted secret.
502    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            // `If-None-Match: *`: the server refuses it if the secret is live.
512            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    /// Delete secret `name` (a signed tombstone; history is kept). With
521    /// `expected`, only if its latest version is still that one. Returns the
522    /// tombstone's version.
523    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    // ------------------------------------------------------------ configs
544
545    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    /// The latest version of config `name`.
556    pub fn config(&self, name: &str) -> Result<ConfigDocument, Error> {
557        self.read_config(name, None)
558    }
559
560    /// Version `version` of config `name`.
561    pub fn config_version(&self, name: &str, version: u64) -> Result<ConfigDocument, Error> {
562        self.read_config(name, Some(version))
563    }
564
565    /// Every live config, sorted by name.
566    pub fn list_configs(&self) -> Result<Vec<Entry>, Error> {
567        Ok(entries(self.inner.list_configs()?, false))
568    }
569
570    /// Every config, deleted ones included, sorted by name.
571    pub fn list_configs_all(&self) -> Result<Vec<Entry>, Error> {
572        Ok(entries(self.inner.list_configs()?, true))
573    }
574
575    /// A config's versions, oldest first, each verified; empty if it never
576    /// existed.
577    pub fn config_history(&self, name: &str) -> Result<Vec<VersionMeta>, Error> {
578        self.inner.config_versions(name)
579    }
580
581    /// Write config `name`. Before any request the body is checked against
582    /// its declared format, and refused if it holds a credential literal
583    /// (unless the config allows literals). Without an expectation it
584    /// updates from the current version; with one it succeeds only against
585    /// the version the caller read.
586    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            // `If-None-Match: *`: the server refuses it if the config is live.
595            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    /// Delete config `name` (a signed tombstone; history is kept). With
606    /// `expected`, only if its latest version is still that one. Returns the
607    /// tombstone's version.
608    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/// What reporting a leaked token did.
630#[derive(Debug, Clone, PartialEq, Eq)]
631#[non_exhaustive]
632pub struct LeakReport {
633    /// The token's id.
634    pub token_id: String,
635    /// Whether the server revoked it now (false: it had nothing to revoke).
636    pub revoked: bool,
637}
638
639/// Report a leaked token so its server revokes it, by proving possession.
640/// The token string is never sent: only a signature made with it is.
641pub 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    /// Where file permissions cannot be checked, a token file is refused
687    /// unless the caller vouches for it, and its contents are not read.
688    #[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}