1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
#![deny(warnings)]
#![warn(unused_extern_crates)]
#![deny(clippy::todo)]
#![deny(clippy::unimplemented)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::expect_used)]
#![deny(clippy::panic)]
#![deny(clippy::unreachable)]
#![deny(clippy::await_holding_lock)]
#![deny(clippy::needless_pass_by_value)]
#![deny(clippy::trivially_copy_pass_by_ref)]

#[cfg(all(jemallocator, not(test)))]
#[global_allocator]
static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc;

use users::{get_current_gid, get_current_uid, get_effective_gid, get_effective_uid};

use serde::Deserialize;
use std::fs::{metadata, File, Metadata};

#[cfg(target_family = "unix")]
use std::os::unix::fs::MetadataExt;

use std::io::Read;
use std::path::Path;
use std::path::PathBuf;
use std::str::FromStr;

use kanidm::audit::LogLevel;
use kanidm::config::{Configuration, OnlineBackup, ServerRole};
use kanidm::tracing_tree;
use kanidm::utils::file_permissions_readonly;
use score::{
    backup_server_core, create_server_core, dbscan_get_id2entry_core, dbscan_list_id2entry_core,
    dbscan_list_index_analysis_core, dbscan_list_index_core, dbscan_list_indexes_core,
    domain_rename_core, recover_account_core, reindex_server_core, restore_server_core,
    vacuum_server_core, verify_server_core,
};

use structopt::StructOpt;

include!("./opt.rs");

#[derive(Debug, Deserialize)]
struct ServerConfig {
    pub bindaddress: Option<String>,
    pub ldapbindaddress: Option<String>,
    // pub threads: Option<usize>,
    pub db_path: String,
    pub db_fs_type: Option<String>,
    pub db_arc_size: Option<usize>,
    pub tls_chain: Option<String>,
    pub tls_key: Option<String>,
    pub log_level: Option<String>,
    pub online_backup: Option<OnlineBackup>,
    pub domain: String,
    pub origin: String,
    #[serde(default)]
    pub role: ServerRole,
}

impl ServerConfig {
    pub fn new<P: AsRef<Path>>(config_path: P) -> Result<Self, ()> {
        let mut f = File::open(config_path).map_err(|e| {
            eprintln!("Unable to open config file [{:?}] 🥺", e);
        })?;

        let mut contents = String::new();
        f.read_to_string(&mut contents)
            .map_err(|e| eprintln!("unable to read contents {:?}", e))?;

        toml::from_str(contents.as_str()).map_err(|e| eprintln!("unable to parse config {:?}", e))
    }
}

impl KanidmdOpt {
    fn commonopt(&self) -> &CommonOpt {
        match self {
            KanidmdOpt::Server(sopt)
            | KanidmdOpt::ConfigTest(sopt)
            | KanidmdOpt::Verify(sopt)
            | KanidmdOpt::Reindex(sopt)
            | KanidmdOpt::Vacuum(sopt)
            | KanidmdOpt::DomainChange(sopt)
            | KanidmdOpt::DbScan(DbScanOpt::ListIndexes(sopt))
            | KanidmdOpt::DbScan(DbScanOpt::ListId2Entry(sopt))
            | KanidmdOpt::DbScan(DbScanOpt::ListIndexAnalysis(sopt)) => &sopt,
            KanidmdOpt::Backup(bopt) => &bopt.commonopts,
            KanidmdOpt::Restore(ropt) => &ropt.commonopts,
            KanidmdOpt::RecoverAccount(ropt) => &ropt.commonopts,
            KanidmdOpt::DbScan(DbScanOpt::ListIndex(dopt)) => &dopt.commonopts,
            // KanidmdOpt::DbScan(DbScanOpt::GetIndex(dopt)) => &dopt.commonopts,
            KanidmdOpt::DbScan(DbScanOpt::GetId2Entry(dopt)) => &dopt.commonopts,
        }
    }
}

fn read_file_metadata(path: &PathBuf) -> Metadata {
    match metadata(path) {
        Ok(m) => m,
        Err(e) => {
            eprintln!(
                "Unable to read metadata for {} - {:?}",
                path.to_str().unwrap_or("invalid file path"),
                e
            );
            std::process::exit(1);
        }
    }
}

#[tokio::main]
async fn main() {
    tracing_tree::main_init();

    // Get info about who we are.
    let cuid = get_current_uid();
    let ceuid = get_effective_uid();
    let cgid = get_current_gid();
    let cegid = get_effective_gid();

    if cuid == 0 || ceuid == 0 || cgid == 0 || cegid == 0 {
        eprintln!("WARNING: This is running as uid == 0 (root) which may be a security risk.");
        // eprintln!("ERROR: Refusing to run - this process must not operate as root.");
        // std::process::exit(1);
    }

    if cuid != ceuid || cgid != cegid {
        eprintln!("{} != {} || {} != {}", cuid, ceuid, cgid, cegid);
        eprintln!("ERROR: Refusing to run - uid and euid OR gid and egid must be consistent.");
        std::process::exit(1);
    }

    // Read cli args, determine if we should backup/restore
    let opt = KanidmdOpt::from_args();

    let mut config = Configuration::new();
    // Check the permissions are sane.
    let cfg_meta = read_file_metadata(&(opt.commonopt().config_path));
    if !file_permissions_readonly(&cfg_meta) {
        eprintln!("WARNING: permissions on {} may not be secure. Should be readonly to running uid. This could be a security risk ...",
            opt.commonopt().config_path.to_str().unwrap_or("invalid file path"));
    }

    if cfg_meta.mode() & 0o007 != 0 {
        eprintln!("WARNING: {} has 'everyone' permission bits in the mode. This could be a security risk ...",
            opt.commonopt().config_path.to_str().unwrap_or("invalid file path")
        );
    }

    if cfg_meta.uid() == cuid || cfg_meta.uid() == ceuid {
        eprintln!("WARNING: {} owned by the current uid, which may allow file permission changes. This could be a security risk ...",
            opt.commonopt().config_path.to_str().unwrap_or("invalid file path")
        );
    }

    // Read our config
    let sconfig = match ServerConfig::new(&(opt.commonopt().config_path)) {
        Ok(c) => c,
        Err(e) => {
            eprintln!("Config Parse failure {:?}", e);
            std::process::exit(1);
        }
    };
    // Apply the file requirements
    let ll = sconfig
        .log_level
        .map(|ll| match LogLevel::from_str(ll.as_str()) {
            Ok(v) => v as u32,
            Err(e) => {
                eprintln!("{:?}", e);
                std::process::exit(1);
            }
        });

    // Check the permissions of the files from the configuration.

    let db_path = PathBuf::from(sconfig.db_path.as_str());
    // We can't check the db_path permissions because it may not exist yet!
    if let Some(db_parent_path) = db_path.parent() {
        if !db_parent_path.exists() {
            eprintln!(
                "DB folder {} may not exist, server startup may FAIL!",
                db_parent_path.to_str().unwrap_or("invalid file path")
            );
        }

        let db_par_path_buf = db_parent_path.to_path_buf();
        let i_meta = read_file_metadata(&db_par_path_buf);
        if !i_meta.is_dir() {
            eprintln!(
                "ERROR: Refusing to run - DB folder {} may not be a directory",
                db_par_path_buf.to_str().unwrap_or("invalid file path")
            );
            std::process::exit(1);
        }
        if !file_permissions_readonly(&i_meta) {
            eprintln!("WARNING: DB folder permissions on {} indicate it may not be RW. This could cause the server start up to fail!", db_par_path_buf.to_str().unwrap_or("invalid file path"));
        }

        if i_meta.mode() & 0o007 != 0 {
            eprintln!("WARNING: DB folder {} has 'everyone' permission bits in the mode. This could be a security risk ...", db_par_path_buf.to_str().unwrap_or("invalid file path"));
        }
    }

    config.update_log_level(ll);
    config.update_db_path(&sconfig.db_path.as_str());
    config.update_db_fs_type(&sconfig.db_fs_type);
    config.update_origin(&sconfig.origin.as_str());
    config.update_domain(&sconfig.domain.as_str());
    config.update_db_arc_size(sconfig.db_arc_size);
    config.update_role(sconfig.role);

    // Apply any cli overrides, normally debug level.
    if let Some(dll) = opt.commonopt().debug.as_ref() {
        config.update_log_level(Some(dll.clone() as u32));
    }

    // ::std::env::set_var("RUST_LOG", "tide=info,kanidm=info,webauthn=debug");
    // env_logger::builder()
    //     .format_timestamp(None)
    //     .format_level(false)
    //     .init();

    match &opt {
        KanidmdOpt::Server(_sopt) | KanidmdOpt::ConfigTest(_sopt) => {
            let config_test = matches!(&opt, KanidmdOpt::ConfigTest(_));
            if config_test {
                eprintln!("Running in server configuration test mode ...");
            } else {
                eprintln!("Running in server mode ...");
            };

            // configuration options that only relate to server mode
            config.update_tls(&sconfig.tls_chain, &sconfig.tls_key);
            config.update_bind(&sconfig.bindaddress);
            config.update_ldapbind(&sconfig.ldapbindaddress);
            config.update_online_backup(&sconfig.online_backup);

            if let Some(i_str) = &(sconfig.tls_chain) {
                let i_path = PathBuf::from(i_str.as_str());
                let i_meta = read_file_metadata(&i_path);
                if !file_permissions_readonly(&i_meta) {
                    eprintln!("WARNING: permissions on {} may not be secure. Should be readonly to running uid. This could be a security risk ...", i_str);
                }
            }

            if let Some(i_str) = &(sconfig.tls_key) {
                let i_path = PathBuf::from(i_str.as_str());
                let i_meta = read_file_metadata(&i_path);
                if !file_permissions_readonly(&i_meta) {
                    eprintln!("WARNING: permissions on {} may not be secure. Should be readonly to running uid. This could be a security risk ...", i_str);
                }

                if i_meta.mode() & 0o007 != 0 {
                    eprintln!("WARNING: {} has 'everyone' permission bits in the mode. This could be a security risk ...", i_str);
                }
            }

            let sctx = create_server_core(config, config_test).await;
            if !config_test {
                match sctx {
                    Ok(_sctx) => match tokio::signal::ctrl_c().await {
                        Ok(_) => {
                            eprintln!("Ctrl-C received, shutting down");
                        }
                        Err(_) => {
                            eprintln!("Invalid signal received, shutting down as a precaution ...");
                        }
                    },
                    Err(_) => {
                        eprintln!("Failed to start server core!");
                        // We may need to return an exit code here, but that may take some re-architecting
                        // to ensure we drop everything cleanly.
                        return;
                    }
                }
                eprintln!("stopped 🛑 ");
            }
        }
        KanidmdOpt::Backup(bopt) => {
            eprintln!("Running in backup mode ...");
            let p = match bopt.path.to_str() {
                Some(p) => p,
                None => {
                    eprintln!("Invalid backup path");
                    std::process::exit(1);
                }
            };
            backup_server_core(&config, p);
        }
        KanidmdOpt::Restore(ropt) => {
            eprintln!("Running in restore mode ...");
            let p = match ropt.path.to_str() {
                Some(p) => p,
                None => {
                    eprintln!("Invalid restore path");
                    std::process::exit(1);
                }
            };
            restore_server_core(&config, p);
        }
        KanidmdOpt::Verify(_vopt) => {
            eprintln!("Running in db verification mode ...");
            verify_server_core(&config);
        }
        KanidmdOpt::RecoverAccount(raopt) => {
            eprintln!("Running account recovery ...");
            recover_account_core(&config, &raopt.name);
        }
        KanidmdOpt::Reindex(_copt) => {
            eprintln!("Running in reindex mode ...");
            reindex_server_core(&config);
        }
        KanidmdOpt::Vacuum(_copt) => {
            eprintln!("Running in vacuum mode ...");
            vacuum_server_core(&config);
        }
        KanidmdOpt::DomainChange(_dopt) => {
            eprintln!("Running in domain name change mode ... this may take a long time ...");
            domain_rename_core(&config);
        }
        KanidmdOpt::DbScan(DbScanOpt::ListIndexes(_)) => {
            eprintln!("👀 db scan - list indexes");
            dbscan_list_indexes_core(&config);
        }
        KanidmdOpt::DbScan(DbScanOpt::ListId2Entry(_)) => {
            eprintln!("👀 db scan - list id2entry");
            dbscan_list_id2entry_core(&config);
        }
        KanidmdOpt::DbScan(DbScanOpt::ListIndexAnalysis(_)) => {
            eprintln!("👀 db scan - list index analysis");
            dbscan_list_index_analysis_core(&config);
        }
        KanidmdOpt::DbScan(DbScanOpt::ListIndex(dopt)) => {
            eprintln!("👀 db scan - list index content - {}", dopt.index_name);
            dbscan_list_index_core(&config, dopt.index_name.as_str());
        }
        KanidmdOpt::DbScan(DbScanOpt::GetId2Entry(dopt)) => {
            eprintln!("👀 db scan - get id2 entry - {}", dopt.id);
            dbscan_get_id2entry_core(&config, dopt.id);
        }
    }
}