Skip to main content

genmeta_access/
lib.rs

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// --- Error ---
29
30#[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
76// --- Logic ---
77
78fn 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}