1mod cli;
2
3use std::io::IsTerminal;
4
5pub use cli::{Options, ParseCommandError, ReportFromStr};
6use dhttp::{
7 access::{
8 action::RequestAction,
9 db::{
10 identity::Name,
11 identity_access_db_path, init_access_database_for, open_access_database,
12 service::{
13 error::{
14 AppendRuleError, ListAllRulesError, ListRuleSetsError, ListRulesError,
15 RemoveRuleSetError, RemoveRulesError,
16 },
17 location_service::LocationService,
18 },
19 },
20 },
21 home::{DhttpHome, LocateDhttpHomeError, identity::settings::LoadDhttpSettingsError},
22};
23use snafu::{IntoError, OptionExt, ResultExt, Snafu};
24use tracing_subscriber::prelude::*;
25
26use crate::cli::{Command, PathOperation};
27
28#[derive(Debug, Snafu)]
31#[snafu(module)]
32pub enum Error {
33 #[snafu(transparent)]
34 ParseCommand { source: ParseCommandError },
35
36 #[snafu(display("failed to locate DHTTP_CONFIG"))]
37 LocateHome { source: LocateDhttpHomeError },
38
39 #[snafu(display("failed to load default identity config"))]
40 LoadDefaultIdentityConfig { source: LoadDhttpSettingsError },
41
42 #[snafu(display(
43 "no default identity configured, use `genmeta identity default <name>` to set one"
44 ))]
45 MissingDefaultIdentity,
46
47 #[snafu(display("failed to initialize identity access database"))]
48 InitDatabase {
49 source: dhttp::access::db::AccessDbError,
50 },
51
52 #[snafu(display("failed to open identity access database"))]
53 OpenDatabase {
54 source: dhttp::access::db::AccessDbError,
55 },
56
57 #[snafu(display("failed to list access path rules"))]
58 ListRules { source: ListRulesError },
59
60 #[snafu(display("failed to remove access path"))]
61 RemovePath { source: RemoveRuleSetError },
62
63 #[snafu(display("failed to remove rules"))]
64 RemoveRules { source: RemoveRulesError },
65
66 #[snafu(display("failed to add rule"))]
67 AddRule { source: AppendRuleError },
68
69 #[snafu(display("failed to list access paths"))]
70 ListPaths { source: ListRuleSetsError },
71
72 #[snafu(display("failed to list access paths"))]
73 ListAllPaths { source: ListAllRulesError },
74}
75
76fn init_tracing() -> tracing_appender::non_blocking::WorkerGuard {
79 let (stderr, guard) = tracing_appender::non_blocking(std::io::stderr());
80 tracing_subscriber::registry()
81 .with(
82 tracing_subscriber::fmt::layer()
83 .with_ansi(std::io::stderr().is_terminal())
84 .with_timer(tracing_subscriber::fmt::time::LocalTime::rfc_3339())
85 .with_writer(stderr),
86 )
87 .with(
88 tracing_subscriber::EnvFilter::builder()
89 .with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into())
90 .from_env_lossy(),
91 )
92 .init();
93 guard
94}
95
96pub async fn run(options: Options) -> Result<(), Error> {
97 let _guard = init_tracing();
98
99 let home = DhttpHome::load_from_environment().context(error::LocateHomeSnafu)?;
100 let output = run_for_home(&home, options).await?;
101
102 if !output.is_empty() {
103 println!("{}", output.trim_end());
104 }
105 Ok(())
106}
107
108pub async fn run_for_home(home: &DhttpHome, options: Options) -> Result<String, Error> {
109 let (identity, command) = options.into_parts()?;
110 if let Command::Print { output } = command {
111 return Ok(output);
112 }
113
114 let identity = resolve_identity(home, identity).await?;
115 let identity_profile = home.identity_profile(identity.borrow());
116 let db_path = identity_access_db_path(&identity_profile);
117 let db = if db_path.is_file() {
118 open_access_database(&identity_profile)
119 .await
120 .context(error::OpenDatabaseSnafu)?
121 } else {
122 tracing::warn!(
123 "access store not found, initializing at `{}`",
124 db_path.display()
125 );
126 init_access_database_for(&identity_profile)
127 .await
128 .context(error::InitDatabaseSnafu)?
129 };
130 run_with(command, &db).await
131}
132
133async fn resolve_identity(
134 home: &DhttpHome,
135 identity: Option<Name<'static>>,
136) -> Result<Name<'static>, Error> {
137 if let Some(identity) = identity {
138 return Ok(identity);
139 }
140
141 let config = match home.load_settings().await {
142 Ok(config) => config,
143 Err(LoadDhttpSettingsError::Io { source, .. })
144 if source.kind() == std::io::ErrorKind::NotFound =>
145 {
146 return error::MissingDefaultIdentitySnafu.fail();
147 }
148 Err(source) => return Err(error::LoadDefaultIdentityConfigSnafu.into_error(source)),
149 };
150
151 config
152 .settings()
153 .default_identity_name()
154 .cloned()
155 .context(error::MissingDefaultIdentitySnafu)
156}
157
158async fn run_with(command: Command, db: &sea_orm::DatabaseConnection) -> Result<String, Error> {
159 let location_service = LocationService::new(db);
160
161 match command {
162 Command::Print { output } => return Ok(output),
163 Command::Path { pattern, operation } => match operation {
164 PathOperation::List => match location_service.list_rules(&pattern).await {
165 Ok(rules) => return Ok(rules.to_string()),
166 Err(ListRulesError::NoMatchedLocation { source }) => {
167 return Ok(source.to_string());
168 }
169 result => _ = result.context(error::ListRulesSnafu)?,
170 },
171 PathOperation::Remove { all, sequence } => match all {
172 true => location_service
173 .remove_rule_set(&pattern)
174 .await
175 .context(error::RemovePathSnafu)?,
176 false => location_service
177 .remove_rules(&pattern, sequence)
178 .await
179 .context(error::RemoveRulesSnafu)?,
180 },
181 PathOperation::Clear => location_service
182 .remove_rule_set(&pattern)
183 .await
184 .context(error::RemovePathSnafu)?,
185 PathOperation::Allow { expr } => location_service
186 .append_rule(&pattern, RequestAction::Allow, expr)
187 .await
188 .context(error::AddRuleSnafu)?,
189 PathOperation::Deny { expr } => location_service
190 .append_rule(&pattern, RequestAction::Deny, expr)
191 .await
192 .context(error::AddRuleSnafu)?,
193 },
194 Command::List { wide } => match wide {
195 true => {
196 return Ok(location_service
197 .list_all_rules()
198 .await
199 .context(error::ListAllPathsSnafu)?
200 .to_string());
201 }
202 false => {
203 return Ok(location_service
204 .list_rule_sets()
205 .await
206 .context(error::ListPathsSnafu)?
207 .to_string());
208 }
209 },
210 Command::RemovePaths { patterns } => {
211 for pattern in patterns {
212 location_service
213 .remove_rule_set(&pattern)
214 .await
215 .context(error::RemovePathSnafu)?;
216 }
217 }
218 }
219
220 Ok(String::new())
221}