Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

53 changes: 53 additions & 0 deletions crates/auths-cli/src/adapters/allowed_signers_store.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//! File-based adapter for [`AllowedSignersStore`].

use std::path::Path;

use auths_sdk::ports::allowed_signers::AllowedSignersStore;
use auths_sdk::workflows::allowed_signers::AllowedSignersError;

/// Reads and writes allowed_signers files using the local filesystem.
/// Uses atomic writes via `tempfile::NamedTempFile::persist`.
pub struct FileAllowedSignersStore;

impl AllowedSignersStore for FileAllowedSignersStore {
fn read(&self, path: &Path) -> Result<Option<String>, AllowedSignersError> {
match std::fs::read_to_string(path) {
Ok(content) => Ok(Some(content)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(AllowedSignersError::FileRead {
path: path.to_path_buf(),
source: e,
}),
}
}

#[allow(clippy::expect_used)] // INVARIANT: path always has a parent (caller provides full file paths)
fn write(&self, path: &Path, content: &str) -> Result<(), AllowedSignersError> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| AllowedSignersError::FileWrite {
path: path.to_path_buf(),
source: e,
})?;
}

use std::io::Write;
let dir = path.parent().expect("path has parent");
let tmp =
tempfile::NamedTempFile::new_in(dir).map_err(|e| AllowedSignersError::FileWrite {
path: path.to_path_buf(),
source: e,
})?;
(&tmp)
.write_all(content.as_bytes())
.map_err(|e| AllowedSignersError::FileWrite {
path: path.to_path_buf(),
source: e,
})?;
tmp.persist(path)
.map_err(|e| AllowedSignersError::FileWrite {
path: path.to_path_buf(),
source: e.error,
})?;
Ok(())
}
}
34 changes: 34 additions & 0 deletions crates/auths-cli/src/adapters/config_store.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//! File-based adapter for the `ConfigStore` port.

use std::path::Path;

use auths_core::ports::config_store::{ConfigStore, ConfigStoreError};

/// Reads and writes config files from the local filesystem.
pub struct FileConfigStore;

impl ConfigStore for FileConfigStore {
fn read(&self, path: &Path) -> Result<Option<String>, ConfigStoreError> {
match std::fs::read_to_string(path) {
Ok(content) => Ok(Some(content)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(ConfigStoreError::Read {
path: path.to_path_buf(),
source: e,
}),
}
}

fn write(&self, path: &Path, content: &str) -> Result<(), ConfigStoreError> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| ConfigStoreError::Write {
path: path.to_path_buf(),
source: e,
})?;
}
std::fs::write(path, content).map_err(|e| ConfigStoreError::Write {
path: path.to_path_buf(),
source: e,
})
}
}
5 changes: 3 additions & 2 deletions crates/auths-cli/src/adapters/doctor_fixes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,14 @@ impl DiagnosticFix for AllowedSignersFix {
let signers_path = ssh_dir.join("allowed_signers");

let storage = RegistryAttestationStorage::new(&self.repo_path);
let mut signers = AllowedSigners::load(&signers_path)
let store = super::allowed_signers_store::FileAllowedSignersStore;
let mut signers = AllowedSigners::load(&signers_path, &store)
.unwrap_or_else(|_| AllowedSigners::new(&signers_path));
let report = signers
.sync(&storage)
.map_err(|e| DiagnosticError::ExecutionFailed(format!("sync signers: {e}")))?;
signers
.save()
.save(&store)
.map_err(|e| DiagnosticError::ExecutionFailed(format!("save signers: {e}")))?;

let signers_str = signers_path
Expand Down
2 changes: 2 additions & 0 deletions crates/auths-cli/src/adapters/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
pub mod agent;
pub mod allowed_signers_store;
pub mod config_store;
pub mod doctor_fixes;
pub mod git_config;
pub mod local_file;
Expand Down
3 changes: 2 additions & 1 deletion crates/auths-cli/src/bin/sign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ use std::sync::Arc;
use anyhow::{Context, Result, anyhow, bail};
use clap::Parser;

use auths_cli::adapters::config_store::FileConfigStore;
use auths_cli::core::pubkey_cache::get_cached_pubkey;
use auths_cli::factories::build_agent_provider;
use auths_core::config::{EnvironmentConfig, load_config};
Expand Down Expand Up @@ -117,7 +118,7 @@ fn build_signing_context(alias: &str) -> Result<CommitSigningContext> {
if let Some(passphrase) = env_config.keychain.passphrase.clone() {
Arc::new(auths_core::PrefilledPassphraseProvider::new(&passphrase))
} else {
let config = load_config();
let config = load_config(&FileConfigStore);
let cache = get_passphrase_cache(config.passphrase.biometric);
let ttl_secs = config
.passphrase
Expand Down
16 changes: 10 additions & 6 deletions crates/auths-cli/src/commands/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ use crate::commands::executable::ExecutableCommand;
use crate::config::CliConfig;
use anyhow::{Result, bail};
use auths_core::config::{AuthsConfig, PassphraseCachePolicy, load_config, save_config};

use crate::adapters::config_store::FileConfigStore;
use clap::{Parser, Subcommand};

/// Manage Auths configuration.
Expand Down Expand Up @@ -44,7 +46,8 @@ impl ExecutableCommand for ConfigCommand {
}

fn execute_set(key: &str, value: &str) -> Result<()> {
let mut config = load_config();
let store = FileConfigStore;
let mut config = load_config(&store);

match key {
"passphrase.cache" => {
Expand All @@ -68,13 +71,13 @@ fn execute_set(key: &str, value: &str) -> Result<()> {
),
}

save_config(&config)?;
save_config(&config, &store)?;
println!("Set {} = {}", key, value);
Ok(())
}

fn execute_get(key: &str) -> Result<()> {
let config = load_config();
let config = load_config(&FileConfigStore);

match key {
"passphrase.cache" => {
Expand All @@ -100,7 +103,7 @@ fn execute_get(key: &str) -> Result<()> {
}

fn execute_show() -> Result<()> {
let config = load_config();
let config = load_config(&FileConfigStore);
let toml_str = toml::to_string_pretty(&config)
.map_err(|e| anyhow::anyhow!("Failed to serialize config: {}", e))?;
println!("{}", toml_str);
Expand Down Expand Up @@ -138,7 +141,8 @@ fn parse_bool(s: &str) -> Result<bool> {
}

fn _ensure_default_config_exists() -> Result<AuthsConfig> {
let config = load_config();
save_config(&config)?;
let store = FileConfigStore;
let config = load_config(&store);
save_config(&config, &store)?;
Ok(config)
}
5 changes: 4 additions & 1 deletion crates/auths-cli/src/commands/doctor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,10 @@ fn check_allowed_signers_file() -> Check {
Some(path_str) => {
let file_path = std::path::Path::new(&path_str);
if file_path.exists() {
match AllowedSigners::load(file_path) {
match AllowedSigners::load(
file_path,
&crate::adapters::allowed_signers_store::FileAllowedSignersStore,
) {
Ok(signers) => {
let entries = signers.list();
let attestation_count = entries
Expand Down
4 changes: 3 additions & 1 deletion crates/auths-cli/src/commands/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,9 @@ fn handle_install_hooks(
let mut signers = AllowedSigners::new(&cmd.allowed_signers_path);
match signers.sync(&storage) {
Ok(report) => {
if let Err(e) = signers.save() {
if let Err(e) =
signers.save(&crate::adapters::allowed_signers_store::FileAllowedSignersStore)
{
eprintln!("Warning: Could not write allowed_signers: {}", e);
} else {
println!(
Expand Down
7 changes: 4 additions & 3 deletions crates/auths-cli/src/commands/init/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,14 @@ pub(crate) fn write_allowed_signers(key_alias: &str, out: &Output) -> Result<()>
std::fs::create_dir_all(&ssh_dir)?;
let signers_path = ssh_dir.join("allowed_signers");

let mut signers =
AllowedSigners::load(&signers_path).unwrap_or_else(|_| AllowedSigners::new(&signers_path));
let store = crate::adapters::allowed_signers_store::FileAllowedSignersStore;
let mut signers = AllowedSigners::load(&signers_path, &store)
.unwrap_or_else(|_| AllowedSigners::new(&signers_path));
let report = signers
.sync(&storage)
.map_err(|e| anyhow!("Failed to sync allowed signers: {}", e))?;
signers
.save()
.save(&store)
.map_err(|e| anyhow!("Failed to write allowed signers: {}", e))?;

let signers_str = signers_path
Expand Down
19 changes: 10 additions & 9 deletions crates/auths-cli/src/commands/signers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use ssh_key::PublicKey as SshPublicKey;
use std::path::PathBuf;

use super::git::expand_tilde;
use crate::adapters::allowed_signers_store::FileAllowedSignersStore;

#[derive(Parser, Debug, Clone)]
#[command(about = "Manage allowed signers for Git commit verification.")]
Expand Down Expand Up @@ -98,7 +99,7 @@ fn resolve_signers_path() -> Result<PathBuf> {

fn handle_list(args: &SignersListArgs) -> Result<()> {
let path = resolve_signers_path()?;
let signers = AllowedSigners::load(&path)
let signers = AllowedSigners::load(&path, &FileAllowedSignersStore)
.with_context(|| format!("Failed to load {}", path.display()))?;

if args.json {
Expand Down Expand Up @@ -134,7 +135,7 @@ fn handle_list(args: &SignersListArgs) -> Result<()> {

fn handle_add(args: &SignersAddArgs) -> Result<()> {
let path = resolve_signers_path()?;
let mut signers = AllowedSigners::load(&path)
let mut signers = AllowedSigners::load(&path, &FileAllowedSignersStore)
.with_context(|| format!("Failed to load {}", path.display()))?;

let principal = SignerPrincipal::Email(
Expand All @@ -147,7 +148,7 @@ fn handle_add(args: &SignersAddArgs) -> Result<()> {
.add(principal, pubkey, SignerSource::Manual)
.map_err(|e| anyhow::anyhow!("{}", e))?;
signers
.save()
.save(&FileAllowedSignersStore)
.with_context(|| format!("Failed to write {}", path.display()))?;

println!("Added {} to {}", args.email, path.display());
Expand All @@ -156,7 +157,7 @@ fn handle_add(args: &SignersAddArgs) -> Result<()> {

fn handle_remove(args: &SignersRemoveArgs) -> Result<()> {
let path = resolve_signers_path()?;
let mut signers = AllowedSigners::load(&path)
let mut signers = AllowedSigners::load(&path, &FileAllowedSignersStore)
.with_context(|| format!("Failed to load {}", path.display()))?;

let principal = SignerPrincipal::Email(
Expand All @@ -166,7 +167,7 @@ fn handle_remove(args: &SignersRemoveArgs) -> Result<()> {
match signers.remove(&principal) {
Ok(true) => {
signers
.save()
.save(&FileAllowedSignersStore)
.with_context(|| format!("Failed to write {}", path.display()))?;
println!("Removed {} from {}", args.email, path.display());
}
Expand Down Expand Up @@ -196,15 +197,15 @@ fn handle_sync(args: &SignersSyncArgs) -> Result<()> {
resolve_signers_path()?
};

let mut signers = AllowedSigners::load(&path)
let mut signers = AllowedSigners::load(&path, &FileAllowedSignersStore)
.with_context(|| format!("Failed to load {}", path.display()))?;

let report = signers
.sync(&storage)
.context("Failed to sync attestations")?;

signers
.save()
.save(&FileAllowedSignersStore)
.with_context(|| format!("Failed to write {}", path.display()))?;

println!(
Expand Down Expand Up @@ -244,7 +245,7 @@ fn handle_add_from_github(args: &SignersAddFromGithubArgs) -> Result<()> {
}

let path = resolve_signers_path()?;
let mut signers = AllowedSigners::load(&path)
let mut signers = AllowedSigners::load(&path, &FileAllowedSignersStore)
.with_context(|| format!("Failed to load {}", path.display()))?;

let email = format!("{}@github.com", args.username);
Expand Down Expand Up @@ -282,7 +283,7 @@ fn handle_add_from_github(args: &SignersAddFromGithubArgs) -> Result<()> {

if added > 0 {
signers
.save()
.save(&FileAllowedSignersStore)
.with_context(|| format!("Failed to write {}", path.display()))?;
println!(
"Added {} key(s) for {} to {}",
Expand Down
51 changes: 51 additions & 0 deletions crates/auths-core/clippy.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Duplicated from workspace clippy.toml — keep in sync
# Clippy does NOT merge per-crate configs with workspace config.
# Any changes to the workspace clippy.toml must be replicated here.

allow-unwrap-in-tests = true
allow-expect-in-tests = true

disallowed-methods = [
# === Workspace rules (duplicated from root clippy.toml) ===
{ path = "chrono::offset::Utc::now", reason = "inject ClockProvider instead of calling Utc::now() directly", allow-invalid = true },
{ path = "std::time::SystemTime::now", reason = "inject ClockProvider instead of calling SystemTime::now() directly", allow-invalid = true },
{ path = "std::env::var", reason = "use EnvironmentConfig abstraction instead of reading env vars directly", allow-invalid = true },
{ path = "uuid::Uuid::new_v4", reason = "Use UuidProvider::new_id() instead. Inject SystemUuidProvider in production and DeterministicUuidProvider in tests." },

# === Sans-IO: filesystem ===
{ path = "std::fs::read", reason = "sans-IO crate — use a port trait" },
{ path = "std::fs::read_to_string", reason = "sans-IO crate — use a port trait" },
{ path = "std::fs::write", reason = "sans-IO crate — use a port trait" },
{ path = "std::fs::create_dir", reason = "sans-IO crate — use a port trait" },
{ path = "std::fs::create_dir_all", reason = "sans-IO crate — use a port trait" },
{ path = "std::fs::remove_file", reason = "sans-IO crate — use a port trait" },
{ path = "std::fs::remove_dir", reason = "sans-IO crate — use a port trait" },
{ path = "std::fs::remove_dir_all", reason = "sans-IO crate — use a port trait" },
{ path = "std::fs::copy", reason = "sans-IO crate — use a port trait" },
{ path = "std::fs::rename", reason = "sans-IO crate — use a port trait" },
{ path = "std::fs::metadata", reason = "sans-IO crate — use a port trait" },
{ path = "std::fs::read_dir", reason = "sans-IO crate — use a port trait" },
{ path = "std::fs::canonicalize", reason = "sans-IO crate — use a port trait" },

# === Sans-IO: process ===
{ path = "std::process::Command::new", reason = "sans-IO crate — use a port trait" },
{ path = "std::process::exit", reason = "sans-IO crate — return errors instead" },

# === Sans-IO: dirs ===
{ path = "dirs::home_dir", reason = "sans-IO crate — inject paths via config", allow-invalid = true },
{ path = "dirs::config_dir", reason = "sans-IO crate — inject paths via config", allow-invalid = true },
{ path = "dirs::data_dir", reason = "sans-IO crate — inject paths via config", allow-invalid = true },
{ path = "dirs::data_local_dir", reason = "sans-IO crate — inject paths via config", allow-invalid = true },

# === Sans-IO: network ===
{ path = "reqwest::Client::new", reason = "sans-IO crate — use a port trait for HTTP", allow-invalid = true },
{ path = "reqwest::get", reason = "sans-IO crate — use a port trait for HTTP", allow-invalid = true },
]

disallowed-types = [
{ path = "std::fs::File", reason = "sans-IO crate — use a port trait" },
{ path = "std::fs::OpenOptions", reason = "sans-IO crate — use a port trait" },
{ path = "std::process::Command", reason = "sans-IO crate — use a port trait" },
{ path = "std::net::TcpStream", reason = "sans-IO crate — use a port trait" },
{ path = "std::net::TcpListener", reason = "sans-IO crate — use a port trait" },
]
2 changes: 2 additions & 0 deletions crates/auths-core/src/agent/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ impl AgentHandle {
/// 1. Clears all keys from the agent core (zeroizing sensitive data)
/// 2. Marks the agent as not running
/// 3. Optionally removes the socket file and PID file
#[allow(clippy::disallowed_methods)] // INVARIANT: daemon lifecycle — socket/PID cleanup is inherently I/O
pub fn shutdown(&self) -> Result<(), AgentError> {
log::info!("Shutting down agent at {:?}", self.socket_path);

Expand Down Expand Up @@ -332,6 +333,7 @@ impl Clone for AgentHandle {
}

#[cfg(test)]
#[allow(clippy::disallowed_methods)]
mod tests {
use super::*;
use ring::rand::SystemRandom;
Expand Down
1 change: 1 addition & 0 deletions crates/auths-core/src/api/ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ static FFI_AGENT: LazyLock<RwLock<Option<Arc<AgentHandle>>>> = LazyLock::new(||
/// - 1 if the socket path is invalid
/// - FFI_ERR_PANIC (-127) if a panic occurred
#[unsafe(no_mangle)]
#[allow(clippy::disallowed_methods)] // INVARIANT: FFI boundary — home-dir fallback for default socket path
pub unsafe extern "C" fn ffi_init_agent(socket_path: *const c_char) -> c_int {
let result = panic::catch_unwind(|| {
let path_str = match unsafe { c_str_to_str_safe(socket_path) } {
Expand Down
Loading
Loading