bxcore, gatewayd, loopd: the secret store moves to bxcore::secrets

Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
2026-09-27 19:08:42 -07:00
co-authored by ornith-1.5-35b-a3b
parent de60c9e13a
commit f594e0d9d4
20 changed files with 385 additions and 146 deletions
+1 -2
View File
@@ -9,13 +9,12 @@ publish.workspace = true
workspace = true
[dependencies]
bxcore.workspace = true
bxcore = { workspace = true, features = ["secrets"] }
rustls.workspace = true
rustls-native-certs.workspace = true
serde.workspace = true
serde_json.workspace = true
toml.workspace = true
zeroize.workspace = true
[dev-dependencies]
rustls.workspace = true
+1 -80
View File
@@ -3,6 +3,7 @@
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use bxcore::secrets::{SecretSource, SecretSpec};
use serde::Deserialize;
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
@@ -39,26 +40,6 @@ pub struct MattermostConfig {
pub ca_file: Option<PathBuf>,
}
/// Where one secret comes from: exactly one of the three is set (checked by `Config::load`).
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct SecretSpec {
#[serde(default)]
pub credential: Option<String>,
#[serde(default)]
pub env: Option<String>,
#[serde(default)]
pub file: Option<PathBuf>,
}
/// A checked `SecretSpec`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SecretSource {
Credential(String),
Env(String),
File(PathBuf),
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AllowConfig {
@@ -267,66 +248,6 @@ impl Config {
}
}
impl SecretSpec {
/// Exactly one source, well formed.
pub fn source(&self) -> Result<SecretSource, String> {
// Exactly one of the three set, else "needs exactly one of credential, env and file".
// credential: not empty, only ASCII letters, digits, _ . -. env: not empty, only A-Z, 0-9,
// _. file: an absolute path. The messages are in the task.
let mut count = 0;
if self.credential.is_some() {
count += 1;
}
if self.env.is_some() {
count += 1;
}
if self.file.is_some() {
count += 1;
}
if count != 1 {
return Err("needs exactly one of credential, env and file".to_string());
}
if let Some(credential) = &self.credential {
if credential.is_empty()
|| !credential.bytes().all(|b| {
b.is_ascii_alphabetic() || b.is_ascii_digit() || matches!(b, b'_' | b'.' | b'-')
})
{
return Err(format!(
"credential {:?} is not a credential name (letters, digits, _ . -)",
credential
));
}
return Ok(SecretSource::Credential(credential.clone()));
}
if let Some(env) = &self.env {
if env.is_empty()
|| !env
.bytes()
.all(|b| matches!(b, b'A'..=b'Z' | b'0'..=b'9' | b'_'))
{
return Err(format!(
"env {:?} is not a variable name (A-Z, 0-9, _)",
env
));
}
return Ok(SecretSource::Env(env.clone()));
}
// `count == 1` and the two above are `None`, so this is `Some`.
let file = match &self.file {
Some(file) => file,
None => return Err("needs exactly one of credential, env and file".to_string()),
};
if !file.is_absolute() {
return Err(format!(
"file {:?} must be an absolute path",
file.display()
));
}
Ok(SecretSource::File(file.clone()))
}
}
/// A Mattermost id: 26 characters of `a-z0-9`.
pub fn valid_id(id: &str) -> bool {
// 26 bytes, each a-z or 0-9.
-1
View File
@@ -8,7 +8,6 @@ pub mod gateway;
pub mod http;
pub mod mm;
pub mod net;
pub mod secrets;
pub mod serve;
pub mod sessions;
pub mod state;
+6 -4
View File
@@ -7,8 +7,8 @@ use std::process::ExitCode;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use bxcore::secrets;
use gatewayd::config::{Config, MATTERMOST_TOKEN};
use gatewayd::secrets;
use gatewayd::serve::{START_FAILED, Tuning, run};
fn main() -> ExitCode {
@@ -30,8 +30,9 @@ fn serve(path: &Path) -> ExitCode {
// "gatewayd: {e}\n{START_FAILED}".
// 2. `let source = match config.token_source() { ... }`: an Err(why) prints
// "gatewayd: {}: {why}\n{START_FAILED}" with `path.display()`.
// 3. `let loaded = match secrets::load(MATTERMOST_TOKEN, &source, &|k| std::env::var_os(k))`:
// an Err(e) prints "gatewayd: {e}" (the error carries its own pointer).
// 3. `let env = |k: &str| std::env::var_os(k);`, then
// `let loaded = match secrets::load("gatewayd", MATTERMOST_TOKEN, &source, &env)`: an Err(e)
// prints "gatewayd: {e}" (the error carries its own pointer).
// Then `if let Some(warning) = &loaded.warning { eprintln!("{warning}"); }`.
// 4. The state file's directory:
// `let dir = config.state_path().parent().map(Path::to_path_buf).unwrap_or_default();`
@@ -54,7 +55,8 @@ fn serve(path: &Path) -> ExitCode {
return ExitCode::from(1);
}
};
let loaded = match secrets::load(MATTERMOST_TOKEN, &source, &|k| std::env::var_os(k)) {
let env = |k: &str| std::env::var_os(k);
let loaded = match secrets::load("gatewayd", MATTERMOST_TOKEN, &source, &env) {
Ok(loaded) => loaded,
Err(e) => {
eprintln!("gatewayd: {e}");
+1 -1
View File
@@ -3,13 +3,13 @@
use std::time::{Duration, SystemTime};
use bxcore::secrets::Secret;
use serde::Deserialize;
use crate::config::valid_id;
use crate::http::{self, Response, rate_limit_wait};
use crate::mm::{Me, MmError, Post, Since, json, since_list};
use crate::net::Connector;
use crate::secrets::Secret;
use crate::ws::conn::host_header;
/// The pause before trying a call again after a 5xx.
-233
View File
@@ -1,233 +0,0 @@
//! The `SecretStore`: a secret from a systemd credential, an environment variable, or an owner-only
//! file (M4a spec, section 4; the brief after P15). A `Secret` cannot be printed.
use std::ffi::OsString;
use std::io::Read;
use std::os::unix::fs::MetadataExt;
use std::path::Path;
use zeroize::Zeroizing;
use crate::config::SecretSource;
pub const RUNBOOK: &str = "see docs/runbook.md#secret-unavailable";
pub const RUNBOOK_FILE: &str = "see docs/runbook.md#secret-in-a-file";
/// A secret's text. No `Display`; `Debug` shows nothing of it; wiped when dropped.
pub struct Secret(Zeroizing<String>);
impl Secret {
pub fn new(text: String) -> Secret {
Secret(Zeroizing::new(text))
}
/// The text, for the one place that must send it.
pub fn expose(&self) -> &str {
&self.0
}
}
impl std::fmt::Debug for Secret {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Secret(…)")
}
}
/// A loaded secret, and the warning to print for it, if any.
#[derive(Debug)]
pub struct Loaded {
pub secret: Secret,
pub warning: Option<String>,
}
#[derive(Debug)]
pub struct SecretError {
pub name: String,
pub why: String,
}
impl std::fmt::Display for SecretError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "secret {}: {}\n{RUNBOOK}", self.name, self.why)
}
}
impl std::error::Error for SecretError {}
/// Load secret `name` from `source`. `env` reads an environment variable (in `gatewayd`,
/// `std::env::var_os`); tests pass their own.
pub fn load(
name: &str,
source: &SecretSource,
env: &dyn Fn(&str) -> Option<OsString>,
) -> Result<Loaded, SecretError> {
// By source (spec section 4). Credential: read $CREDENTIALS_DIRECTORY/<name> (through `env`,
// not std::env); an unset variable, or a file that cannot be read, is an error. Env: the
// variable, UTF-8; unset is an error. File: `read_checked` checks the path, opens and re-checks
// the same file handle, reads it, and set `warning`
// to the exact text in the task. Every value goes through `value`. Every error is a
// `SecretError` naming the secret, never the value.
match source {
SecretSource::Credential(cred) => {
let dir = match env("CREDENTIALS_DIRECTORY") {
Some(dir) => dir,
None => return Err(SecretError {
name: name.to_string(),
why: "CREDENTIALS_DIRECTORY is not set: gatewayd was not started by systemd with LoadCredentialEncrypted=".to_string(),
}),
};
let path = Path::new(dir.as_os_str()).join(cred);
let bytes = match std::fs::read(&path) {
Ok(bytes) => bytes,
Err(e) => {
return Err(SecretError {
name: name.to_string(),
why: format!("cannot read the credential {}: {}", path.display(), e),
});
}
};
let secret = value(bytes).map_err(|why| SecretError {
name: name.to_string(),
why,
})?;
Ok(Loaded {
secret,
warning: None,
})
}
SecretSource::Env(var) => {
let val = match env(var) {
Some(val) => val,
None => {
return Err(SecretError {
name: name.to_string(),
why: format!("the environment variable {} is not set", var),
});
}
};
let text = match val.to_str() {
Some(text) => text,
None => {
return Err(SecretError {
name: name.to_string(),
why: format!("the environment variable {} is not UTF-8", var),
});
}
};
let secret = value(text.as_bytes().to_vec()).map_err(|why| SecretError {
name: name.to_string(),
why,
})?;
Ok(Loaded {
secret,
warning: None,
})
}
SecretSource::File(path) => {
let mut bytes = match read_checked(path, &|| {}) {
Ok(bytes) => bytes,
Err(why) => {
return Err(SecretError {
name: name.to_string(),
why,
});
}
};
let secret = value(std::mem::take(&mut *bytes)).map_err(|why| SecretError {
name: name.to_string(),
why,
})?;
Ok(Loaded {
secret,
warning: Some(format!(
"gatewayd: warning: secret {} is read in plaintext from {}; a systemd credential keeps it encrypted at rest ({RUNBOOK_FILE})",
name,
path.display()
)),
})
}
}
}
/// The bytes of an owner-only regular file, read from the very file that was checked. `between`
/// runs after the path is checked and before it is opened: `load` passes `&|| {}`; tests use it
/// to swap the file.
pub fn read_checked(path: &Path, between: &dyn Fn()) -> Result<Zeroizing<Vec<u8>>, String> {
// 1. `path.is_absolute()`, else "<path> is not an absolute path".
// 2. `let named = std::fs::symlink_metadata(path)`, an Err(e) is "cannot read <path>: <e>";
// `named.file_type().is_symlink()` is "<path> is a symbolic link";
// `!named.file_type().is_file()` is "<path> is not a regular file".
// 3. `between();`
// 4. `let mut file = std::fs::File::open(path)`, an Err(e) is "cannot read <path>: <e>";
// `let opened = file.metadata()`, the same error text.
// 5. `if (opened.dev(), opened.ino()) != (named.dev(), named.ino())`:
// "<path> changed while it was read".
// 6. The owner and mode checks from the original file check, word for word, on `opened` (not
// on `named`): the uid of `user_id()`; "<path> is not owned by the user gatewayd runs as";
// "<path> has mode <mode:03o>; only the owner may read it (0600 or 0400)".
// 7. `let mut bytes = Zeroizing::new(Vec::new());` then `file.read_to_end(&mut bytes)`, an
// Err(e) is "cannot read <path>: <e>". Ok(bytes).
if !path.is_absolute() {
return Err(format!("{} is not an absolute path", path.display()));
}
let named = std::fs::symlink_metadata(path)
.map_err(|e| format!("cannot read {}: {}", path.display(), e))?;
if named.file_type().is_symlink() {
return Err(format!("{} is a symbolic link", path.display()));
}
if !named.file_type().is_file() {
return Err(format!("{} is not a regular file", path.display()));
}
between();
let mut file =
std::fs::File::open(path).map_err(|e| format!("cannot read {}: {}", path.display(), e))?;
let opened = file
.metadata()
.map_err(|e| format!("cannot read {}: {}", path.display(), e))?;
if (opened.dev(), opened.ino()) != (named.dev(), named.ino()) {
return Err(format!("{} changed while it was read", path.display()));
}
let owner = user_id()?;
if opened.uid() != owner {
return Err(format!(
"{} is not owned by the user gatewayd runs as",
path.display()
));
}
let mode = opened.mode() & 0o777;
if mode & 0o077 != 0 {
return Err(format!(
"{} has mode {:03o}; only the owner may read it (0600 or 0400)",
path.display(),
mode
));
}
let mut bytes = Zeroizing::new(Vec::new());
file.read_to_end(&mut bytes)
.map_err(|e| format!("cannot read {}: {}", path.display(), e))?;
Ok(bytes)
}
/// The user id this process runs as: the owner of a pipe it has just made. `std` has no call for
/// it, and `/proc/self` is Linux only.
fn user_id() -> Result<u32, String> {
let (reader, _writer) = std::io::pipe().map_err(|e| format!("cannot make a pipe: {e}"))?;
let pipe = std::fs::File::from(std::os::fd::OwnedFd::from(reader));
let meta = pipe
.metadata()
.map_err(|e| format!("cannot read a pipe: {e}"))?;
Ok(meta.uid())
}
/// The text without one trailing newline; not empty; UTF-8.
fn value(bytes: Vec<u8>) -> Result<Secret, String> {
// UTF-8 (else an error), one trailing newline removed, not empty. Keep the bytes in `Zeroizing`
// until they are inside the `Secret`.
let bytes = Zeroizing::new(bytes);
let text = std::str::from_utf8(&bytes).map_err(|_| "the value is not UTF-8".to_string())?;
let text = text.strip_suffix('\n').unwrap_or(text);
if text.is_empty() {
return Err("the value is empty".to_string());
}
Ok(Secret::new(text.to_string()))
}
+1 -1
View File
@@ -13,6 +13,7 @@ use std::sync::mpsc::{self, Receiver, Sender};
use std::time::{Duration, Instant};
use bxcore::SessionId;
use bxcore::secrets::Secret;
use crate::approvals::Registry;
use crate::config::Config;
@@ -20,7 +21,6 @@ use crate::gateway::{self, ToLoop};
use crate::mm::rest::Client;
use crate::mm::{Event, Me, MmError, parse_event};
use crate::net::Connector;
use crate::secrets::Secret;
use crate::sessions::{Queues, Router};
use crate::state::{State, StateError};
use crate::ws::conn::{Timing, Ws};
+2 -1
View File
@@ -5,7 +5,8 @@ mod tmp;
use std::path::PathBuf;
use gatewayd::config::{Config, ConfigError, SecretSource, ServerUrl, parse_url, valid_id};
use bxcore::secrets::SecretSource;
use gatewayd::config::{Config, ConfigError, ServerUrl, parse_url, valid_id};
use tmp::TempDir;
const OWNER: &str = "abcdefghijklmnopqrstuvwxyz";
+1 -1
View File
@@ -9,11 +9,11 @@ mod tls_server;
use std::net::SocketAddr;
use std::time::Duration;
use bxcore::secrets::Secret;
use gatewayd::config::ServerUrl;
use gatewayd::mm::MmError;
use gatewayd::mm::rest::Client;
use gatewayd::net::Connector;
use gatewayd::secrets::Secret;
use http_server::{reply, serve_http};
use serde_json::json;
+1 -1
View File
@@ -10,11 +10,11 @@ use std::net::SocketAddr;
use std::sync::mpsc::Receiver;
use std::time::{Duration, Instant};
use bxcore::secrets::Secret;
use gatewayd::config::ServerUrl;
use gatewayd::mm::MmError;
use gatewayd::mm::rest::Client;
use gatewayd::net::Connector;
use gatewayd::secrets::Secret;
use http_server::{Request, reply, serve_http};
use serde_json::json;
use tls_server::{fixture, server_config};
-200
View File
@@ -1,200 +0,0 @@
//! The secret store (M4a spec, section 4). The environment is passed in as a function, so no test
//! changes the process's environment. Do not edit.
#[path = "support/tmp.rs"]
mod tmp;
use std::collections::HashMap;
use std::ffi::OsString;
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use gatewayd::config::SecretSource;
use gatewayd::secrets::{RUNBOOK, RUNBOOK_FILE, load};
use tmp::TempDir;
const TOKEN: &str = "s3cret-t0ken-value";
fn env_of(pairs: &[(&str, &str)]) -> impl Fn(&str) -> Option<OsString> + use<> {
let map: HashMap<String, OsString> = pairs
.iter()
.map(|(k, v)| (k.to_string(), OsString::from(v)))
.collect();
move |k| map.get(k).cloned()
}
fn owner_file(dir: &TempDir, name: &str, text: &str, mode: u32) -> PathBuf {
let path = dir.write(name, text);
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(mode)).unwrap();
path
}
fn refused(source: &SecretSource, env: &dyn Fn(&str) -> Option<OsString>, word: &str) {
let e = load("mattermost_token", source, env).expect_err(word);
let text = e.to_string();
assert!(text.contains(word), "{word}: {text}");
assert!(text.starts_with("secret mattermost_token: "), "{text}");
assert!(text.ends_with(RUNBOOK), "{text}");
assert!(
!text.contains(TOKEN),
"a refusal never shows the value: {text}"
);
}
#[test]
fn a_systemd_credential() {
let dir = TempDir::new("cred");
dir.write("creds/mattermost-token", &format!("{TOKEN}\n"));
let creds = dir.path().join("creds");
let env = env_of(&[("CREDENTIALS_DIRECTORY", creds.to_str().unwrap())]);
let got = load(
"mattermost_token",
&SecretSource::Credential("mattermost-token".into()),
&env,
)
.unwrap();
assert_eq!(
got.secret.expose(),
TOKEN,
"one trailing newline is removed"
);
assert_eq!(got.warning, None);
}
#[test]
fn a_credential_outside_systemd_or_missing_is_refused() {
let src = SecretSource::Credential("mattermost-token".into());
refused(&src, &env_of(&[]), "CREDENTIALS_DIRECTORY is not set");
let dir = TempDir::new("cred-missing");
refused(
&src,
&env_of(&[("CREDENTIALS_DIRECTORY", dir.path().to_str().unwrap())]),
"cannot read the credential",
);
}
#[test]
fn an_environment_variable() {
let got = load(
"mattermost_token",
&SecretSource::Env("MM".into()),
&env_of(&[("MM", TOKEN)]),
)
.unwrap();
assert_eq!(got.secret.expose(), TOKEN);
assert_eq!(got.warning, None, "only a file warns");
refused(&SecretSource::Env("MM".into()), &env_of(&[]), "is not set");
refused(
&SecretSource::Env("MM".into()),
&env_of(&[("MM", "")]),
"empty",
);
}
#[test]
fn an_owner_only_file_is_read_with_a_warning() {
let dir = TempDir::new("file");
for mode in [0o600, 0o400] {
let path = owner_file(
&dir,
&format!("token-{mode:o}"),
&format!("{TOKEN}\n"),
mode,
);
let got = load(
"mattermost_token",
&SecretSource::File(path.clone()),
&env_of(&[]),
)
.unwrap();
assert_eq!(got.secret.expose(), TOKEN);
let warning = got.warning.expect("a file secret warns");
assert!(
warning.starts_with(
"gatewayd: warning: secret mattermost_token is read in plaintext from "
),
"{warning}"
);
assert!(warning.contains(path.to_str().unwrap()), "{warning}");
assert!(warning.contains(RUNBOOK_FILE), "{warning}");
assert!(!warning.contains(TOKEN));
}
}
#[test]
fn a_file_anyone_else_can_read_or_that_is_not_a_plain_file_is_refused() {
let dir = TempDir::new("file-bad");
for (mode, _) in [
(0o640, "group"),
(0o604, "other"),
(0o644, "both"),
(0o660, "group write"),
] {
let path = owner_file(&dir, &format!("t-{mode:o}"), TOKEN, mode);
refused(
&SecretSource::File(path),
&env_of(&[]),
"only the owner may read it",
);
}
let target = owner_file(&dir, "real", TOKEN, 0o600);
let link = dir.path().join("link");
std::os::unix::fs::symlink(&target, &link).unwrap();
refused(&SecretSource::File(link), &env_of(&[]), "symbolic link");
std::fs::create_dir(dir.path().join("adir")).unwrap();
refused(
&SecretSource::File(dir.path().join("adir")),
&env_of(&[]),
"not a regular file",
);
refused(
&SecretSource::File(dir.path().join("missing")),
&env_of(&[]),
"cannot read",
);
refused(
&SecretSource::File(PathBuf::from("relative/token")),
&env_of(&[]),
"absolute",
);
}
#[test]
fn empty_and_non_utf8_values_are_refused() {
let dir = TempDir::new("value");
refused(
&SecretSource::File(owner_file(&dir, "empty", "", 0o600)),
&env_of(&[]),
"empty",
);
refused(
&SecretSource::File(owner_file(&dir, "nl", "\n", 0o600)),
&env_of(&[]),
"empty",
);
let path = dir.path().join("bin");
std::fs::write(&path, [0xff, 0xfe]).unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).unwrap();
refused(&SecretSource::File(path), &env_of(&[]), "not UTF-8");
}
#[test]
fn only_one_trailing_newline_is_removed_and_spaces_stay() {
let dir = TempDir::new("trim");
let path = owner_file(&dir, "t", " a b \n\n", 0o600);
let got = load("x", &SecretSource::File(path), &env_of(&[])).unwrap();
assert_eq!(got.secret.expose(), " a b \n");
}
#[test]
fn a_secret_prints_nothing_of_itself() {
let got = load(
"mattermost_token",
&SecretSource::Env("MM".into()),
&env_of(&[("MM", TOKEN)]),
)
.unwrap();
let shown = format!("{:?} {:?}", got.secret, got);
assert!(!shown.contains(TOKEN), "{shown}");
assert!(shown.contains("Secret(…)"), "{shown}");
}
-91
View File
@@ -1,91 +0,0 @@
//! A secret file is read from the file that was checked, never from whatever the path names a
//! moment later (M4a review, finding 2). `read_checked` runs `between` after checking the path and
//! before opening it; each test swaps something there. Do not edit.
#[path = "support/tmp.rs"]
mod tmp;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use gatewayd::secrets::read_checked;
use tmp::TempDir;
const TOKEN: &str = "the-real-token";
const OTHER: &str = "a-file-the-owner-never-chose";
fn owner_file(dir: &TempDir, name: &str, text: &str) -> PathBuf {
let path = dir.write(name, text);
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).unwrap();
path
}
fn refused(path: &Path, between: &dyn Fn(), word: &str) {
let why = read_checked(path, between).expect_err(word);
assert!(why.contains(word), "{word}: {why}");
assert!(why.contains(&path.display().to_string()), "{why}");
assert!(
!why.contains(TOKEN) && !why.contains(OTHER),
"never a value: {why}"
);
}
#[test]
fn an_untouched_file_is_read() {
let dir = TempDir::new("race-ok");
let path = owner_file(&dir, "token", &format!("{TOKEN}\n"));
let bytes = read_checked(&path, &|| {}).unwrap();
assert_eq!(bytes.as_slice(), format!("{TOKEN}\n").as_bytes());
}
#[test]
fn a_file_swapped_for_a_link_is_refused() {
let dir = TempDir::new("race-link");
let path = owner_file(&dir, "token", TOKEN);
let other = owner_file(&dir, "other", OTHER);
let swap = || {
std::fs::remove_file(&path).unwrap();
std::os::unix::fs::symlink(&other, &path).unwrap();
};
refused(&path, &swap, "changed while it was read");
}
#[test]
fn a_file_swapped_for_another_file_is_refused() {
let dir = TempDir::new("race-rename");
let path = owner_file(&dir, "token", TOKEN);
let other = owner_file(&dir, "other", OTHER);
let swap = || std::fs::rename(&other, &path).unwrap();
refused(&path, &swap, "changed while it was read");
}
#[test]
fn the_checks_hold_for_the_file_that_is_read() {
let dir = TempDir::new("race-mode");
let path = owner_file(&dir, "token", TOKEN);
let widen = || {
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).unwrap();
};
refused(&path, &widen, "has mode 644");
}
#[test]
fn the_path_checks_still_come_first() {
let dir = TempDir::new("race-first");
let other = owner_file(&dir, "other", OTHER);
let link = dir.path().join("link");
std::os::unix::fs::symlink(&other, &link).unwrap();
refused(
&link,
&|| panic!("never reached for a link"),
"is a symbolic link",
);
refused(
dir.path(),
&|| panic!("never reached for a directory"),
"is not a regular file",
);
let relative = Path::new("relative/token");
let why = read_checked(relative, &|| panic!("never reached")).unwrap_err();
assert!(why.contains("is not an absolute path"), "{why}");
}
+1 -1
View File
@@ -9,8 +9,8 @@ use std::thread::JoinHandle;
use std::time::Duration;
use bxcore::Turn;
use bxcore::secrets::Secret;
use gatewayd::config::Config;
use gatewayd::secrets::Secret;
use gatewayd::serve::{Stop, Tuning, run};
use crate::fake_loop::{done, serve_loop};