-
Notifications
You must be signed in to change notification settings - Fork 1
Closed
Labels
priority:highHigh priority issueHigh priority issuestatus:doneCompletedCompletedtype:enhancementNew feature or requestNew feature or request
Description
Summary
Implement a file transfer filtering infrastructure that can allow, deny, or log file transfers based on configurable policies.
Parent Epic
- Implement bssh-server with SFTP/SCP support #123 - bssh-server 추가 구현
- Depends on: Create shared module structure for client/server code reuse #124 (shared module structure)
Implementation Details
1. Filter Trait and Types
// src/server/filter/mod.rs
pub mod policy;
pub mod path;
pub mod pattern;
use std::path::Path;
/// File transfer operation type
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Operation {
Upload,
Download,
Delete,
Rename,
CreateDir,
ListDir,
}
/// Result of filter check
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum FilterResult {
/// Allow the operation
Allow,
/// Deny the operation
Deny,
/// Allow but log the operation
Log,
}
/// Trait for file transfer filters
pub trait TransferFilter: Send + Sync {
/// Check if operation is allowed
fn check(&self, path: &Path, operation: Operation, user: &str) -> FilterResult;
/// Check with destination (for rename/copy)
fn check_with_dest(
&self,
src: &Path,
dest: &Path,
operation: Operation,
user: &str,
) -> FilterResult {
// Default: check both paths
let src_result = self.check(src, operation, user);
let dest_result = self.check(dest, operation, user);
match (src_result, dest_result) {
(FilterResult::Deny, _) | (_, FilterResult::Deny) => FilterResult::Deny,
(FilterResult::Log, _) | (_, FilterResult::Log) => FilterResult::Log,
_ => FilterResult::Allow,
}
}
}2. Policy Engine
// src/server/filter/policy.rs
use super::*;
/// Filter policy engine
pub struct FilterPolicy {
rules: Vec<FilterRule>,
default_action: FilterResult,
}
#[derive(Debug, Clone)]
pub struct FilterRule {
/// Rule name (for logging)
pub name: Option<String>,
/// Pattern matcher
pub matcher: Box<dyn Matcher>,
/// Action to take
pub action: FilterResult,
/// Operations this rule applies to
pub operations: Option<Vec<Operation>>,
/// Users this rule applies to (None = all users)
pub users: Option<Vec<String>>,
}
pub trait Matcher: Send + Sync {
fn matches(&self, path: &Path) -> bool;
fn clone_box(&self) -> Box<dyn Matcher>;
}
impl Clone for Box<dyn Matcher> {
fn clone(&self) -> Self {
self.clone_box()
}
}
impl FilterPolicy {
pub fn new() -> Self {
Self {
rules: Vec::new(),
default_action: FilterResult::Allow,
}
}
pub fn with_default(mut self, action: FilterResult) -> Self {
self.default_action = action;
self
}
pub fn add_rule(mut self, rule: FilterRule) -> Self {
self.rules.push(rule);
self
}
/// Load policy from configuration
pub fn from_config(config: &FilterConfig) -> Result<Self> {
let mut policy = Self::new();
for rule_config in &config.rules {
let rule = Self::rule_from_config(rule_config)?;
policy.rules.push(rule);
}
Ok(policy)
}
fn rule_from_config(config: &FilterRuleConfig) -> Result<FilterRule> {
let matcher: Box<dyn Matcher> = if let Some(ref pattern) = config.pattern {
Box::new(GlobMatcher::new(pattern)?)
} else if let Some(ref prefix) = config.path_prefix {
Box::new(PrefixMatcher::new(prefix))
} else {
anyhow::bail!("Rule must have pattern or path_prefix");
};
Ok(FilterRule {
name: config.name.clone(),
matcher,
action: match config.action {
FilterActionConfig::Allow => FilterResult::Allow,
FilterActionConfig::Deny => FilterResult::Deny,
FilterActionConfig::Log => FilterResult::Log,
},
operations: config.operations.clone(),
users: config.users.clone(),
})
}
}
impl TransferFilter for FilterPolicy {
fn check(&self, path: &Path, operation: Operation, user: &str) -> FilterResult {
for rule in &self.rules {
// Check if operation matches
if let Some(ref ops) = rule.operations {
if !ops.contains(&operation) {
continue;
}
}
// Check if user matches
if let Some(ref users) = rule.users {
if !users.iter().any(|u| u == user) {
continue;
}
}
// Check if path matches
if rule.matcher.matches(path) {
tracing::debug!(
"Filter rule {:?} matched path {:?}, action: {:?}",
rule.name,
path,
rule.action
);
return rule.action;
}
}
self.default_action
}
}3. Built-in Matchers
// src/server/filter/pattern.rs
use glob::Pattern;
/// Glob pattern matcher
#[derive(Debug, Clone)]
pub struct GlobMatcher {
pattern: Pattern,
raw: String,
}
impl GlobMatcher {
pub fn new(pattern: &str) -> Result<Self> {
let pattern = Pattern::new(pattern)
.context("Invalid glob pattern")?;
Ok(Self {
pattern,
raw: pattern.to_string(),
})
}
}
impl Matcher for GlobMatcher {
fn matches(&self, path: &Path) -> bool {
self.pattern.matches_path(path)
}
fn clone_box(&self) -> Box<dyn Matcher> {
Box::new(self.clone())
}
}
// src/server/filter/path.rs
/// Path prefix matcher
#[derive(Debug, Clone)]
pub struct PrefixMatcher {
prefix: PathBuf,
}
impl PrefixMatcher {
pub fn new(prefix: &str) -> Self {
Self {
prefix: PathBuf::from(prefix),
}
}
}
impl Matcher for PrefixMatcher {
fn matches(&self, path: &Path) -> bool {
path.starts_with(&self.prefix)
}
fn clone_box(&self) -> Box<dyn Matcher> {
Box::new(self.clone())
}
}
/// Exact path matcher
#[derive(Debug, Clone)]
pub struct ExactMatcher {
path: PathBuf,
}
impl Matcher for ExactMatcher {
fn matches(&self, path: &Path) -> bool {
path == self.path
}
fn clone_box(&self) -> Box<dyn Matcher> {
Box::new(self.clone())
}
}
/// Regex matcher
#[derive(Debug, Clone)]
pub struct RegexMatcher {
regex: regex::Regex,
}
impl RegexMatcher {
pub fn new(pattern: &str) -> Result<Self> {
let regex = regex::Regex::new(pattern)?;
Ok(Self { regex })
}
}
impl Matcher for RegexMatcher {
fn matches(&self, path: &Path) -> bool {
self.regex.is_match(&path.to_string_lossy())
}
fn clone_box(&self) -> Box<dyn Matcher> {
Box::new(self.clone())
}
}Configuration
filter:
enabled: true
default_action: allow # allow, deny, or log
rules:
# Deny access to sensitive files
- name: "block-secrets"
pattern: "*.key"
action: deny
- name: "block-shadow"
path_prefix: "/etc/shadow"
action: deny
# Log large file downloads
- name: "log-archives"
pattern: "*.{tar,tar.gz,zip}"
action: log
operations: [download]
# Restrict certain users
- name: "restrict-deploy"
path_prefix: "/etc"
action: deny
users: [deploy, www-data]Files to Create/Modify
| File | Action |
|---|---|
src/server/filter/mod.rs |
Create - Filter trait and types |
src/server/filter/policy.rs |
Create - Policy engine |
src/server/filter/path.rs |
Create - Path matchers |
src/server/filter/pattern.rs |
Create - Pattern matchers |
src/server/mod.rs |
Modify - Add filter module |
Testing Requirements
- Unit test: Glob matching
- Unit test: Prefix matching
- Unit test: Regex matching
- Unit test: Policy rule evaluation order
- Unit test: User-specific rules
- Unit test: Operation-specific rules
Acceptance Criteria
- TransferFilter trait defined
- FilterPolicy implementation
- Glob pattern matcher
- Path prefix matcher
- Regex matcher (optional)
- Per-user rules
- Per-operation rules
- Configurable default action
- Integration with SFTP/SCP
- Tests passing
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
priority:highHigh priority issueHigh priority issuestatus:doneCompletedCompletedtype:enhancementNew feature or requestNew feature or request