mod mutations;
mod reads;
mod zone;
use crate::capabilities::{render, CapabilityContext, View};
use crate::cli::{Cli, FirewallAction};
use crate::error::{is_service_unknown, FezError, Result};
use crate::protocol::client::BridgeClient;
use serde_json::Value;
const FW_NAME: &str = "org.fedoraproject.FirewallD1";
const FW_PATH: &str = "/org/fedoraproject/FirewallD1";
const FW_IFACE: &str = "org.fedoraproject.FirewallD1";
const FW_ZONE_IFACE: &str = "org.fedoraproject.FirewallD1.zone";
const FW_CONFIG_PATH: &str = "/org/fedoraproject/FirewallD1/config";
const FW_CONFIG_IFACE: &str = "org.fedoraproject.FirewallD1.config";
const FW_CONFIG_ZONE_IFACE: &str = "org.fedoraproject.FirewallD1.config.zone";
pub fn dispatch(cli: &Cli, action: &FirewallAction) -> i32 {
render(cli, run(cli, action))
}
fn dependency_missing() -> FezError {
FezError::DependencyMissing {
component: "firewalld".into(),
dbus_name: FW_NAME.into(),
remediation: "Check if firewalld is running: fez services status firewalld.service --json. If absent or stopped, install and start it: dnf install firewalld && systemctl enable --now firewalld.service.".into(),
}
}
#[derive(Debug, PartialEq, Eq)]
enum ReadAction<'a> {
Status,
List,
Show { zone: &'a str },
Services,
}
#[derive(Debug, PartialEq, Eq)]
enum Mutation<'a> {
AddService {
service: &'a str,
zone: Option<&'a str>,
timeout: Option<u32>,
},
RemoveService {
service: &'a str,
zone: Option<&'a str>,
},
AddPort {
port: &'a str,
zone: Option<&'a str>,
timeout: Option<u32>,
},
RemovePort {
port: &'a str,
zone: Option<&'a str>,
},
SetDefaultZone {
zone: &'a str,
},
Reload,
Confirm,
Panic {
state: &'a str,
},
Masquerade {
state: &'a str,
zone: Option<&'a str>,
timeout: Option<u32>,
},
}
#[derive(Debug, PartialEq, Eq)]
enum Plan<'a> {
Read(ReadAction<'a>),
Mutate(Mutation<'a>),
}
fn classify(action: &FirewallAction) -> Plan<'_> {
match action {
FirewallAction::Status => Plan::Read(ReadAction::Status),
FirewallAction::List => Plan::Read(ReadAction::List),
FirewallAction::Show { zone } => Plan::Read(ReadAction::Show { zone }),
FirewallAction::Services => Plan::Read(ReadAction::Services),
FirewallAction::AddService {
service,
zone,
timeout,
} => Plan::Mutate(Mutation::AddService {
service,
zone: zone.as_deref(),
timeout: *timeout,
}),
FirewallAction::RemoveService { service, zone } => Plan::Mutate(Mutation::RemoveService {
service,
zone: zone.as_deref(),
}),
FirewallAction::AddPort {
port,
zone,
timeout,
} => Plan::Mutate(Mutation::AddPort {
port,
zone: zone.as_deref(),
timeout: *timeout,
}),
FirewallAction::RemovePort { port, zone } => Plan::Mutate(Mutation::RemovePort {
port,
zone: zone.as_deref(),
}),
FirewallAction::SetDefaultZone { zone } => Plan::Mutate(Mutation::SetDefaultZone { zone }),
FirewallAction::Reload => Plan::Mutate(Mutation::Reload),
FirewallAction::Confirm => Plan::Mutate(Mutation::Confirm),
FirewallAction::Panic { state } => Plan::Mutate(Mutation::Panic { state }),
FirewallAction::Masquerade {
state,
zone,
timeout,
} => Plan::Mutate(Mutation::Masquerade {
state,
zone: zone.as_deref(),
timeout: *timeout,
}),
}
}
fn run(cli: &Cli, action: &FirewallAction) -> Result<View> {
let mut client = crate::capabilities::connect(cli)?;
let host = client.host().to_string();
match classify(action) {
Plan::Read(read) => {
let ch = open_channel(&mut client, false)?;
let mut ctx = CapabilityContext {
client: &mut client,
channel: &ch,
host: &host,
};
match read {
ReadAction::Status => reads::status(&mut ctx),
ReadAction::List => reads::list(&mut ctx),
ReadAction::Show { zone } => reads::show(&mut ctx, zone),
ReadAction::Services => reads::services(&mut ctx),
}
}
Plan::Mutate(mutation) => mutations::mutate(cli, &mut client, &host, mutation),
}
}
fn open_channel(client: &mut BridgeClient, privileged: bool) -> Result<String> {
if privileged {
client.dbus_open_privileged(FW_NAME)
} else {
client.dbus_open(FW_NAME)
}
}
fn fw_call(
client: &mut BridgeClient,
channel: &str,
iface: &str,
method: &str,
args: Value,
) -> Result<Value> {
fw_call_path(client, channel, FW_PATH, iface, method, args)
}
fn fw_call_path(
client: &mut BridgeClient,
channel: &str,
path: &str,
iface: &str,
method: &str,
args: Value,
) -> Result<Value> {
client
.dbus_call(channel, path, iface, method, args)
.map_err(|e| map_fw_error(e, method))
}
fn map_fw_error(e: FezError, method: &str) -> FezError {
match e {
FezError::Dbus { ref name, .. } if is_service_unknown(name) => dependency_missing(),
FezError::Dbus { ref name, .. } if name.contains("UnknownMethod") => {
FezError::UnsupportedApi(method.to_string())
}
FezError::Problem(ref p) if p == "not-found" || p == "not-supported" => {
dependency_missing()
}
other => other,
}
}
fn arg_str_vec(out: &Value) -> Vec<String> {
out.get(0)
.and_then(Value::as_array)
.map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect()
})
.unwrap_or_default()
}
fn arg_str(out: &Value) -> String {
out.get(0).and_then(Value::as_str).unwrap_or("").to_string()
}
fn arg_bool(out: &Value) -> bool {
out.get(0).and_then(Value::as_bool).unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::FirewallAction;
use crate::error::FezError;
fn dbus(name: &str) -> FezError {
FezError::Dbus {
name: name.into(),
message: "boom".into(),
}
}
#[test]
fn map_fw_error_service_unknown_is_dependency_missing() {
let mapped = map_fw_error(
dbus("org.freedesktop.DBus.Error.ServiceUnknown"),
"getZones",
);
assert_eq!(mapped.code(), "dependency-missing");
assert_eq!(mapped.exit_code(), 9);
assert_eq!(
map_fw_error(
dbus("org.freedesktop.DBus.Error.NameHasNoOwner"),
"getZones"
)
.code(),
"dependency-missing"
);
}
#[test]
fn map_fw_error_unknown_method_is_unsupported_api() {
let mapped = map_fw_error(
dbus("org.freedesktop.DBus.Error.UnknownMethod"),
"getMasquerade",
);
assert_eq!(mapped.code(), "unsupported-api");
assert_eq!(mapped.exit_code(), 12);
assert!(matches!(
mapped,
FezError::UnsupportedApi(ref m) if m == "getMasquerade"
));
}
#[test]
fn map_fw_error_channel_problem_is_dependency_missing() {
for problem in ["not-found", "not-supported"] {
let mapped = map_fw_error(FezError::Problem(problem.into()), "getZones");
assert_eq!(
mapped.code(),
"dependency-missing",
"Problem({problem}) should map to dependency-missing"
);
}
}
#[test]
fn map_fw_error_passes_through_unrelated_errors() {
assert_eq!(
map_fw_error(
FezError::Problem("authentication-failed".into()),
"getZones"
)
.code(),
"auth-failed"
);
let denied = FezError::AccessDenied {
remediation: "enable sudo".into(),
};
assert_eq!(map_fw_error(denied, "getZones").code(), "access-denied");
}
#[test]
fn classify_routes_reads_and_mutations_to_typed_plans() {
assert!(matches!(
classify(&FirewallAction::Status),
Plan::Read(ReadAction::Status)
));
assert!(matches!(
classify(&FirewallAction::List),
Plan::Read(ReadAction::List)
));
assert!(matches!(
classify(&FirewallAction::Show {
zone: "public".into()
}),
Plan::Read(ReadAction::Show { zone: "public" })
));
assert!(matches!(
classify(&FirewallAction::Services),
Plan::Read(ReadAction::Services)
));
assert!(matches!(
classify(&FirewallAction::AddService {
service: "ssh".into(),
zone: Some("public".into()),
timeout: Some(60),
}),
Plan::Mutate(Mutation::AddService {
service: "ssh",
zone: Some("public"),
timeout: Some(60),
})
));
assert!(matches!(
classify(&FirewallAction::RemoveService {
service: "ssh".into(),
zone: Some("public".into()),
}),
Plan::Mutate(Mutation::RemoveService {
service: "ssh",
zone: Some("public"),
})
));
assert!(matches!(
classify(&FirewallAction::AddPort {
port: "8080/tcp".into(),
zone: Some("public".into()),
timeout: Some(60),
}),
Plan::Mutate(Mutation::AddPort {
port: "8080/tcp",
zone: Some("public"),
timeout: Some(60),
})
));
assert!(matches!(
classify(&FirewallAction::RemovePort {
port: "8080/tcp".into(),
zone: Some("public".into()),
}),
Plan::Mutate(Mutation::RemovePort {
port: "8080/tcp",
zone: Some("public"),
})
));
assert!(matches!(
classify(&FirewallAction::SetDefaultZone {
zone: "internal".into(),
}),
Plan::Mutate(Mutation::SetDefaultZone { zone: "internal" })
));
assert!(matches!(
classify(&FirewallAction::Reload),
Plan::Mutate(Mutation::Reload)
));
assert!(matches!(
classify(&FirewallAction::Confirm),
Plan::Mutate(Mutation::Confirm)
));
assert!(matches!(
classify(&FirewallAction::Panic { state: "on".into() }),
Plan::Mutate(Mutation::Panic { state: "on" })
));
assert!(matches!(
classify(&FirewallAction::Masquerade {
state: "on".into(),
zone: Some("public".into()),
timeout: Some(60),
}),
Plan::Mutate(Mutation::Masquerade {
state: "on",
zone: Some("public"),
timeout: Some(60),
})
));
}
}