Update to account recovery UX (#859)

* JSON-formatted output for recover_account, moved a bunch of logs to debug instead of info
* updated documentation
This commit is contained in:
James Hodgkinson 2022-06-26 18:02:16 +10:00 committed by GitHub
parent 4b1989ee22
commit 57f8fa9d2b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 152 additions and 48 deletions

View file

@ -148,7 +148,7 @@ in `/tmp/kanidm.db`.
Create the initial database and generate an `admin` username:
cargo run --bin kanidmd recover_account -c ./examples/insecure_server.toml -n admin
cargo run --bin kanidmd recover_account -c ./examples/insecure_server.toml admin
<snip>
Success - password reset to -> Et8QRJgQkMJu3v1AQxcbxRWW44qRUZPpr6BJ9fCGapAB9cT4

View file

@ -119,7 +119,7 @@ You should test your configuration is valid before you proceed.
Then you can setup the initial admin account and initialise the database into your volume.
docker run --rm -i -t -v kanidmd:/data \
kanidm/server:latest /sbin/kanidmd recover_account -c /data/server.toml -n admin
kanidm/server:latest /sbin/kanidmd recover_account -c /data/server.toml admin
### Run the Server

View file

@ -15,7 +15,7 @@ tracing = "^0.1.35"
reqwest = { version = "^0.11.11", features=["cookies", "json", "native-tls"] }
kanidm_proto = { path = "../kanidm_proto", version = "1.1.0-alpha.8" }
serde = { version = "^1.0.137", features = ["derive"] }
serde_json = "^1.0.80"
serde_json = "^1.0.81"
toml = "^0.5.9"
uuid = { version = "^1.1.2", features = ["serde", "v4"] }
url = { version = "^2.2.2", features = ["serde"] }

View file

@ -12,7 +12,7 @@ repository = "https://github.com/kanidm/kanidm/"
[dependencies]
serde = { version = "^1.0.137", features = ["derive"] }
serde_json = "^1.0.80"
serde_json = "^1.0.81"
uuid = { version = "^1.1.2", features = ["serde"] }
base32 = "^0.4.0"
webauthn-rs = { version = "^0.3.2", default-features = false, features = ["wasm"] }

View file

@ -37,7 +37,7 @@ rpassword = "^6.0.1"
clap = { version = "^3.2", features = ["derive", "env"] }
libc = "^0.2.126"
serde = { version = "^1.0.137", features = ["derive"] }
serde_json = "^1.0.80"
serde_json = "^1.0.81"
shellexpand = "^2.1.0"
rayon = "^1.5.3"
time = { version = "=0.2.27", features = ["serde", "std"] }

View file

@ -60,7 +60,7 @@ bytes = "^1.1.0"
libc = "^0.2.126"
serde = { version = "^1.0.137", features = ["derive"] }
serde_json = "^1.0.80"
serde_json = "^1.0.81"
clap = { version = "^3.2", features = ["derive"] }
libsqlite3-sys = "0.24.2"

View file

@ -28,7 +28,7 @@ use std::path::PathBuf;
use std::str::FromStr;
use kanidm::audit::LogLevel;
use kanidm::config::{Configuration, OnlineBackup, ServerRole};
use kanidm::config::{Configuration, ConsoleOutputMode, OnlineBackup, ServerRole};
use kanidm::tracing_tree;
use kanidm::utils::file_permissions_readonly;
use score::{
@ -221,6 +221,9 @@ async fn main() {
config.update_domain(&sconfig.domain.as_str());
config.update_db_arc_size(sconfig.db_arc_size);
config.update_role(sconfig.role);
config.update_output_mode(
ConsoleOutputMode::from_str(opt.commands.commonopt().output_mode.as_str()).unwrap(),
);
// Apply any cli overrides, normally debug level.
if let Some(dll) = opt.commands.commonopt().debug.as_ref() {

View file

@ -6,6 +6,10 @@ struct CommonOpt {
#[clap(parse(from_os_str), short, long = "config", env = "KANIDM_CONFIG")]
/// Path to the server's configuration file. If it does not exist, it will be created.
config_path: PathBuf,
//TODO: remove this once we work out the format
/// Log format (still in very early development)
#[clap(short, long = "output", env = "KANIDM_OUTPUT", default_value="text")]
output_mode: String,
}
#[derive(Debug, Args)]
@ -28,7 +32,7 @@ struct RestoreOpt {
#[derive(Debug, Args)]
struct RecoverAccountOpt {
#[clap(short)]
#[clap(value_parser)]
/// The account name to recover credentials for.
name: String,
#[clap(flatten)]

View file

@ -48,7 +48,7 @@ uuid = { version = "^1.1.2", features = ["serde", "v4" ] }
compiled-uuid = "0.1.2"
serde = { version = "^1.0.137", features = ["derive"] }
serde_cbor = "^0.11.2"
serde_json = "^1.0.80"
serde_json = "^1.0.81"
libsqlite3-sys = "0.24.2"
rusqlite = "^0.27.0"

View file

@ -1255,7 +1255,7 @@ impl<'a> BackendWriteTransaction<'a> {
pub fn upgrade_reindex(&self, v: i64) -> Result<(), OperationError> {
let dbv = self.get_db_index_version();
admin_info!(?dbv, ?v, "upgrade_reindex");
admin_debug!(?dbv, ?v, "upgrade_reindex");
if dbv < v {
limmediate_warning!(
"NOTICE: A system reindex is required. This may take a long time ...\n"
@ -1514,9 +1514,9 @@ impl Backend {
idxkeys: Vec<IdxKey>,
vacuum: bool,
) -> Result<Self, OperationError> {
info!("DB tickets -> {:?}", cfg.pool_size);
info!("Profile -> {}", env!("KANIDM_PROFILE_NAME"));
info!("CPU Flags -> {}", env!("KANIDM_CPU_FLAGS"));
debug!("DB tickets -> {:?}", cfg.pool_size);
debug!("Profile -> {}", env!("KANIDM_PROFILE_NAME"));
debug!("CPU Flags -> {}", env!("KANIDM_CPU_FLAGS"));
// If in memory, reduce pool to 1
if cfg.path.is_empty() {

View file

@ -74,6 +74,31 @@ impl FromStr for ServerRole {
}
}
// TODO: this should probably be in the kanidm crate
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
#[serde(rename_all = "lowercase")]
pub enum ConsoleOutputMode {
Text,
JSON,
}
impl Default for ConsoleOutputMode {
fn default() -> Self {
ConsoleOutputMode::Text
}
}
impl FromStr for ConsoleOutputMode {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"json" => Ok(ConsoleOutputMode::JSON),
"text" => Ok(ConsoleOutputMode::Text),
_ => Err("Must be one of json, text"),
}
}
}
#[derive(Serialize, Deserialize, Debug, Default)]
pub struct Configuration {
pub address: String,
@ -93,6 +118,7 @@ pub struct Configuration {
pub domain: String,
pub origin: String,
pub role: ServerRole,
pub output_mode: ConsoleOutputMode,
}
impl fmt::Display for Configuration {
@ -112,21 +138,23 @@ impl fmt::Display for Configuration {
.and_then(|_| write!(f, "secure cookies: {}, ", self.secure_cookies))
.and_then(|_| write!(f, "with TLS: {}, ", self.tls_config.is_some()))
.and_then(|_| match self.log_level {
Some(u) => write!(f, "with log_level: {:x}, ", u),
None => write!(f, "with log_level: default, "),
Some(u) => write!(f, "log_level: {:x}, ", u),
None => write!(f, "log_level: default, "),
})
// TODO: include the backup timings
.and_then(|_| match &self.online_backup {
Some(_) => write!(f, "with online_backup: enabled, "),
None => write!(f, "with online_backup: disabled, "),
Some(_) => write!(f, "online_backup: enabled, "),
None => write!(f, "online_backup: disabled, "),
})
.and_then(|_| write!(f, "role: {}, ", self.role.to_string()))
.and_then(|_| {
write!(
f,
"integration mode: {}",
"integration mode: {}, ",
self.integration_test_config.is_some()
)
})
.and_then(|_| write!(f, "console output format: {:?} ", self.output_mode))
}
}
@ -144,7 +172,7 @@ impl Configuration {
db_path: String::from(""),
db_fs_type: None,
db_arc_size: None,
maximum_request: 262_144, // 256k
maximum_request: 256 * 1024, // 256k
// log type
// log path
// TODO #63: default true in prd
@ -157,6 +185,7 @@ impl Configuration {
domain: "idm.example.com".to_string(),
origin: "https://idm.example.com".to_string(),
role: ServerRole::WriteReplica,
output_mode: ConsoleOutputMode::default(),
};
let mut rng = StdRng::from_entropy();
rng.fill(&mut c.cookie_key);
@ -218,6 +247,11 @@ impl Configuration {
self.role = r;
}
/// Sets the output mode for writing to the console
pub fn update_output_mode(&mut self, om: ConsoleOutputMode) {
self.output_mode = om;
}
pub fn update_tls(&mut self, chain: &Option<String>, key: &Option<String>) {
match (chain, key) {
(None, None) => {}

View file

@ -93,9 +93,9 @@ pub mod prelude {
ValueSetUuid,
};
pub use crate::{
admin_error, admin_info, admin_warn, filter_error, filter_info, filter_trace, filter_warn,
perf_trace, request_error, request_info, request_trace, request_warn, security_access,
security_critical, security_error, security_info, spanned,
admin_debug, admin_error, admin_info, admin_warn, filter_error, filter_info, filter_trace,
filter_warn, perf_trace, request_error, request_info, request_trace, request_warn,
security_access, security_critical, security_error, security_info, spanned,
};
}

View file

@ -1354,10 +1354,10 @@ impl<'a> SchemaWriteTransaction<'a> {
let r = self.validate();
if r.is_empty() {
admin_info!("schema validate -> passed");
admin_debug!("schema validate -> passed");
Ok(())
} else {
admin_info!(err = ?r, "schema validate -> errors");
admin_error!(err = ?r, "schema validate -> errors");
Err(OperationError::ConsistencyError(r))
}
})

View file

@ -1094,7 +1094,7 @@ impl QueryServer {
Err(OperationError::NoMatchingEntries) => Ok(0),
Err(r) => Err(r),
}?;
admin_info!(?system_info_version);
admin_debug!(?system_info_version);
if system_info_version < 3 {
migrate_txn.migrate_2_to_3()?;
@ -1119,8 +1119,8 @@ impl QueryServer {
ts_write_3
.initialise_idm()
.and_then(|_| ts_write_3.commit())?;
admin_info!("migrations success! ☀️ ");
// TODO: work out if we've actually done any migrations before printing this
admin_debug!("Database version check and migrations success! ☀️ ");
Ok(())
}
@ -2267,7 +2267,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
*/
pub fn initialise_schema_core(&self) -> Result<(), OperationError> {
admin_info!("initialise_schema_core -> start ...");
admin_debug!("initialise_schema_core -> start ...");
// Load in all the "core" schema, that we already have in "memory".
let entries = self.schema.to_entries();
@ -2277,9 +2277,9 @@ impl<'a> QueryServerWriteTransaction<'a> {
self.internal_migrate_or_create(e)
});
if r.is_ok() {
admin_info!("initialise_schema_core -> Ok!");
admin_debug!("initialise_schema_core -> Ok!");
} else {
admin_info!(?r, "initialise_schema_core -> Error");
admin_error!(?r, "initialise_schema_core -> Error");
}
// why do we have error handling if it's always supposed to be `Ok`?
debug_assert!(r.is_ok());
@ -2287,7 +2287,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
}
pub fn initialise_schema_idm(&self) -> Result<(), OperationError> {
admin_info!("initialise_schema_idm -> start ...");
admin_debug!("initialise_schema_idm -> start ...");
// List of IDM schemas to init.
let idm_schema: Vec<&str> = vec![
JSON_SCHEMA_ATTR_DISPLAYNAME,
@ -2338,7 +2338,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
.try_for_each(|e_str| self.internal_migrate_or_create_str(e_str));
if r.is_ok() {
admin_info!("initialise_schema_idm -> Ok!");
admin_debug!("initialise_schema_idm -> Ok!");
} else {
admin_error!(res = ?r, "initialise_schema_idm -> Error");
}
@ -2467,7 +2467,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
.iter()
.try_for_each(|e_str| self.internal_migrate_or_create_str(e_str));
if res.is_ok() {
admin_info!("initialise_idm -> result Ok!");
admin_debug!("initialise_idm -> result Ok!");
} else {
admin_error!(?res, "initialise_idm p3 -> result");
}

View file

@ -3,6 +3,7 @@ use num_enum::{IntoPrimitive, TryFromPrimitive};
#[derive(Debug, Clone, Copy, IntoPrimitive, TryFromPrimitive)]
#[repr(u64)]
pub enum EventTag {
AdminDebug,
AdminError,
AdminWarn,
AdminInfo,
@ -24,6 +25,7 @@ pub enum EventTag {
impl EventTag {
pub fn pretty(self) -> &'static str {
match self {
EventTag::AdminDebug => "admin.debug",
EventTag::AdminError => "admin.error",
EventTag::AdminWarn => "admin.warn",
EventTag::AdminInfo => "admin.info",
@ -46,8 +48,9 @@ impl EventTag {
pub fn emoji(self) -> &'static str {
use EventTag::*;
match self {
AdminDebug => "🐛",
AdminError | FilterError | RequestError | SecurityError => "🚨",
AdminWarn | FilterWarn | RequestWarn => "⚠️ ",
AdminWarn | FilterWarn | RequestWarn => "⚠️",
AdminInfo | FilterInfo | RequestInfo | SecurityInfo => " ",
RequestTrace | FilterTrace | PerfTrace => "📍",
SecurityCritical => "🔐",

View file

@ -25,6 +25,11 @@ macro_rules! tagged_event {
}}
}
#[macro_export]
macro_rules! admin_debug {
($($arg:tt)*) => { tagged_event!(DEBUG, EventTag::AdminDebug, $($arg)*) }
}
#[macro_export]
macro_rules! admin_error {
($($arg:tt)*) => { tagged_event!(ERROR, EventTag::AdminError, $($arg)*) }

View file

@ -31,6 +31,8 @@ ldap3_proto = "^0.2.3"
tracing = { version = "^0.1.35", features = ["attributes"] }
serde = { version = "^1.0.137", features = ["derive"] }
serde_json = "^1.0.81"
async-trait = "^0.1.53"
async-std = { version = "^1.12.0", features = ["tokio1"] }
compact_jwt = "^0.2.1"
@ -44,7 +46,6 @@ tracing-subscriber = "^0.3.11"
# kanidm = { path = "../kanidmd" }
# score = { path = "../kanidmd/score" }
futures = "^0.3.21"
serde_json = "^1.0.80"
# async-std = { version = "1.6", features = ["tokio1"] }
webauthn-authenticator-rs = "^0.3.2"

View file

@ -27,19 +27,17 @@ extern crate kanidm;
mod https;
mod ldaps;
use libc::umask;
// use crossbeam::channel::unbounded;
use async_std::task;
use compact_jwt::JwsSigner;
use kanidm::prelude::*;
use std::sync::Arc;
use libc::umask;
use kanidm::config::Configuration;
// SearchResult
// use self::ctx::ServerCtx;
use kanidm::actors::v1_read::QueryServerReadV1;
use kanidm::actors::v1_write::QueryServerWriteV1;
use kanidm::be::{Backend, BackendConfig, BackendTransaction, FsType};
use kanidm::config::{Configuration, ConsoleOutputMode};
use kanidm::crypto::setup_tls;
use kanidm::idm::server::{IdmServer, IdmServerDelayed};
use kanidm::interval::IntervalActor;
@ -47,11 +45,12 @@ use kanidm::ldap::LdapServer;
use kanidm::schema::Schema;
use kanidm::status::StatusActor;
use kanidm::utils::{duration_from_epoch_now, touch_file_or_quit};
use kanidm_proto::v1::OperationError;
use async_std::task;
use compact_jwt::JwsSigner;
use serde::{Deserialize, Serialize};
use serde_json;
use std::fmt;
use std::sync::Arc;
// === internal setup helpers
@ -484,6 +483,42 @@ pub fn verify_server_core(config: &Configuration) {
// Now add IDM server verifications?
}
// TODO: should this go somewhere else
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
enum MessageStatus {
Failure,
Success,
}
impl fmt::Display for MessageStatus {
fn fmt(&self, f: &mut fmt::Formatter) -> ::std::result::Result<(), ::std::fmt::Error> {
match *self {
MessageStatus::Failure => f.write_str("failure"),
MessageStatus::Success => f.write_str("status"),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
struct AccountChangeMessage {
action: String,
result: String,
status: MessageStatus,
src_user: String,
dest_user: String,
}
impl fmt::Display for AccountChangeMessage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
serde_json::to_string(self).unwrap_or(format!("{:?}", self))
)
}
}
pub fn recover_account_core(config: &Configuration, name: &str) {
let schema = match Schema::new() {
Ok(s) => s,
@ -527,7 +562,26 @@ pub fn recover_account_core(config: &Configuration, name: &str) {
std::process::exit(1);
}
};
eprintln!("Success - password reset to -> {}", new_pw);
match config.output_mode {
ConsoleOutputMode::JSON => {
println!(
"{}",
AccountChangeMessage {
status: MessageStatus::Success,
src_user: String::from("command-line invocation"),
dest_user: name.to_string(),
result: new_pw,
action: String::from("recover_account"),
}
);
}
ConsoleOutputMode::Text => {
println!(
"Successfully recovered account '{}' - password reset to -> {}",
name, new_pw
);
}
}
}
pub async fn create_server_core(config: Configuration, config_test: bool) -> Result<(), ()> {

View file

@ -18,7 +18,7 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
serde = { version = "^1.0.137", features = ["derive"] }
serde_json = "^1.0.80"
serde_json = "^1.0.81"
wasm-bindgen = { version = "^0.2.81", features = ["serde-serialize"] }
wasm-bindgen-futures = { version = "^0.4.30" }

View file

@ -91,7 +91,7 @@ impl Component for DeleteApp {
type Message = Msg;
type Properties = ModalProps;
fn create(ctx: &Context<Self>) -> Self {
fn create(_ctx: &Context<Self>) -> Self {
console::log!("delete modal create");
DeleteApp { state: State::Init }

View file

@ -24,7 +24,7 @@ clap = { version = "^3.2", features = ["derive"] }
uuid = { version = "^1.1.2", features = ["serde", "v4" ] }
csv = "1.1.6"
serde = { version = "^1.0.137", features = ["derive"] }
serde_json = "^1.0.80"
serde_json = "^1.0.81"
rand = "^0.8.5"
toml = "^0.5.9"