Feature: kanidm CLI pulling OpenAPI schema ()

* diag is super noisy when you actually turn on logging... even though it wasn't an error?
* adding api download-schema to the CLI
* docs
This commit is contained in:
James Hodgkinson 2023-11-03 17:37:27 +10:00 committed by GitHub
parent cf35a7e667
commit 7025a9ff55
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 163 additions and 49 deletions
book/src/developers/designs
libs/client/src
server
core/src/https/apidocs
daemon/src
testkit/tests
tools/cli/src

View file

@ -17,3 +17,5 @@ The Swagger UI is available at `/docs/swagger-ui` on your server (ie, if your or
`https://example.com:8443`, visit `https://example.com:8443/docs/swagger-ui`).
The OpenAPI schema is similarly available at `/docs/v1/openapi.json`.
You can download the schema file using `kanidm api download-schema <filename>` - it defaults to `./kanidm-openapi.json`.

View file

@ -275,7 +275,7 @@ impl KanidmClientBuilder {
if !config_path.as_ref().exists() {
debug!("{:?} does not exist", config_path);
let diag = kanidm_lib_file_permissions::diagnose_path(config_path.as_ref());
info!(%diag);
debug!(%diag);
return Ok(self);
};

View file

@ -1,4 +1,4 @@
use axum::{response::Redirect, routing::get, Router};
use axum::{middleware::from_fn, response::Redirect, routing::get, Router};
use kanidm_proto::{scim_v1::ScimSyncState, v1};
use utoipa::{
openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme},
@ -242,4 +242,6 @@ pub(crate) fn router() -> Router<ServerState> {
.route("/docs", get(Redirect::temporary("/docs/swagger-ui")))
.route("/docs/", get(Redirect::temporary("/docs/swagger-ui")))
.merge(SwaggerUi::new("/docs/swagger-ui").url("/docs/v1/openapi.json", ApiDoc::openapi()))
// overlay the version middleware because the client is sad without it
.layer(from_fn(super::middleware::version_middleware))
}

View file

@ -37,9 +37,9 @@ use kanidmd_core::{
dbscan_restore_quarantined_core, domain_rename_core, reindex_server_core, restore_server_core,
vacuum_server_core, verify_server_core,
};
use sketching::tracing_forest;
use sketching::tracing_forest::traits::*;
use sketching::tracing_forest::util::*;
use sketching::tracing_forest::{self};
use tokio::net::UnixStream;
use tokio_util::codec::Framed;
#[cfg(target_family = "windows")] // for windows builds
@ -255,7 +255,6 @@ async fn kanidm_main() -> ExitCode {
// Fall back to stderr
.map_sender(|sender| {
sender.or_stderr()
})
.build_on(|subscriber|{
subscriber.with(log_filter)

View file

@ -1,15 +1,12 @@
//! Integration tests using browser automation
use std::process::Output;
// use std::process::Output;
use tempfile::tempdir;
// use tempfile::tempdir;
use kanidm_client::KanidmClient;
use kanidmd_testkit::{
login_put_admin_idm_admins, ADMIN_TEST_PASSWORD, IDM_ADMIN_TEST_PASSWORD, IDM_ADMIN_TEST_USER,
NOT_ADMIN_TEST_USERNAME,
};
use testkit_macros::cli_kanidm;
use kanidmd_testkit::login_put_admin_idm_admins;
// use testkit_macros::cli_kanidm;
/// Tries to handle closing the webdriver session if there's an error
#[allow(unused_macros)]
@ -228,43 +225,43 @@ async fn test_idm_domain_set_ldap_basedn(rsclient: KanidmClient) {
.is_err());
}
/// run a test command as the admin user
fn test_cmd_admin(token_cache_path: &str, rsclient: &KanidmClient, cmd: &str) -> Output {
let split_cmd: Vec<&str> = cmd.split_ascii_whitespace().collect();
test_cmd_admin_split(token_cache_path, rsclient, &split_cmd)
}
/// run a test command as the admin user
fn test_cmd_admin_split(token_cache_path: &str, rsclient: &KanidmClient, cmd: &[&str]) -> Output {
println!(
"##################################\nrunning {}\n##################################",
cmd.join(" ")
);
let res = cli_kanidm!()
.env("KANIDM_PASSWORD", ADMIN_TEST_PASSWORD)
.args(cmd)
.output()
.unwrap();
println!("############ result ##################");
println!("status: {:?}", res.status);
println!("stdout: {}", String::from_utf8_lossy(&res.stdout));
println!("stderr: {}", String::from_utf8_lossy(&res.stderr));
println!("######################################");
assert!(res.status.success());
res
}
// /// run a test command as the admin user
// fn test_cmd_admin(token_cache_path: &str, rsclient: &KanidmClient, cmd: &str) -> Output {
// let split_cmd: Vec<&str> = cmd.split_ascii_whitespace().collect();
// test_cmd_admin_split(token_cache_path, rsclient, &split_cmd)
// }
// /// run a test command as the admin user
// fn test_cmd_admin_split(token_cache_path: &str, rsclient: &KanidmClient, cmd: &[&str]) -> Output {
// println!(
// "##################################\nrunning {}\n##################################",
// cmd.join(" ")
// );
// let res = cli_kanidm!()
// .env("KANIDM_PASSWORD", ADMIN_TEST_PASSWORD)
// .args(cmd)
// .output()
// .unwrap();
// println!("############ result ##################");
// println!("status: {:?}", res.status);
// println!("stdout: {}", String::from_utf8_lossy(&res.stdout));
// println!("stderr: {}", String::from_utf8_lossy(&res.stderr));
// println!("######################################");
// assert!(res.status.success());
// res
// }
/// run a test command as the idm_admin user
fn test_cmd_idm_admin(token_cache_path: &str, rsclient: &KanidmClient, cmd: &str) -> Output {
println!("##############################\nrunning {}", cmd);
let res = cli_kanidm!()
.env("KANIDM_PASSWORD", IDM_ADMIN_TEST_PASSWORD)
.args(cmd.split(" "))
.output()
.unwrap();
println!("##############################\n{} result: {:?}", cmd, res);
assert!(res.status.success());
res
}
// /// run a test command as the idm_admin user
// fn test_cmd_idm_admin(token_cache_path: &str, rsclient: &KanidmClient, cmd: &str) -> Output {
// println!("##############################\nrunning {}", cmd);
// let res = cli_kanidm!()
// .env("KANIDM_PASSWORD", IDM_ADMIN_TEST_PASSWORD)
// .args(cmd.split(" "))
// .output()
// .unwrap();
// println!("##############################\n{} result: {:?}", cmd, res);
// assert!(res.status.success());
// res
// }
// Disabled due to inconsistent test failures and blocking
/*
@ -362,4 +359,3 @@ async fn test_integration_with_assert_cmd(rsclient: KanidmClient) {
println!("Success!");
}
*/

View file

@ -143,6 +143,7 @@ impl SelfOpt {
impl SystemOpt {
pub fn debug(&self) -> bool {
match self {
SystemOpt::Api { commands } => commands.debug(),
SystemOpt::PwBadlist { commands } => commands.debug(),
SystemOpt::DeniedNames { commands } => commands.debug(),
SystemOpt::Oauth2 { commands } => commands.debug(),
@ -153,6 +154,7 @@ impl SystemOpt {
pub async fn exec(&self) {
match self {
SystemOpt::Api { commands } => commands.exec().await,
SystemOpt::PwBadlist { commands } => commands.exec().await,
SystemOpt::DeniedNames { commands } => commands.exec().await,
SystemOpt::Oauth2 { commands } => commands.exec().await,

View file

@ -15,6 +15,7 @@ use clap::Parser;
use kanidm_cli::KanidmClientParser;
use std::thread;
use tokio::runtime;
use tracing_subscriber::filter::LevelFilter;
use tracing_subscriber::prelude::*;
use tracing_subscriber::{fmt, EnvFilter};
@ -35,7 +36,9 @@ fn main() {
} else {
match EnvFilter::try_from_default_env() {
Ok(f) => f,
Err(_) => EnvFilter::new("kanidm_client=warn,kanidm_cli=warn"),
Err(_) => EnvFilter::builder()
.with_default_directive(LevelFilter::INFO.into())
.parse_lossy("kanidm_client=warn,kanidm_cli=info"),
}
};

View file

@ -0,0 +1,84 @@
use crate::ApiOpt;
use std::io::IsTerminal;
impl ApiOpt {
pub fn debug(&self) -> bool {
match self {
ApiOpt::DownloadSchema(asdo) => asdo.copt.debug,
}
}
pub async fn exec(&self) {
match self {
ApiOpt::DownloadSchema(aopt) => {
let client = aopt.copt.to_unauth_client();
// check if the output file already exists
if aopt.filename.exists() {
debug!("Output file {} already exists", aopt.filename.display());
let mut bail = false;
if !aopt.force {
// check if we're in a terminal
if std::io::stdout().is_terminal()
&& std::io::stderr().is_terminal()
&& std::io::stdin().is_terminal()
{
// validate with the user that it's OK to overwrite
let response = dialoguer::Confirm::new()
.with_prompt(format!(
"Output file {} already exists, overwrite?",
aopt.filename.display()
))
.interact()
.unwrap();
if !response {
bail = true;
}
} else {
debug!("stdin is not a terminal, bailing!");
bail = true;
}
if bail {
error!("Output file {} already exists and user hasn't forced overwrite, can't continue!", aopt.filename.display());
std::process::exit(1);
}
}
}
let url = client.make_url("/docs/v1/openapi.json");
debug!(
"Downloading schema from {} to {}",
url,
aopt.filename.display()
);
let jsondata: serde_json::Value =
match client.perform_get_request("/docs/v1/openapi.json").await {
Ok(val) => val,
Err(err) => {
error!("Failed to download: {:?}", err);
std::process::exit(1);
}
};
let serialized = match serde_json::to_string_pretty(&jsondata) {
Ok(val) => val,
Err(err) => {
error!("Failed to serialize schema: {:?}", err);
std::process::exit(1);
}
};
match std::fs::write(&aopt.filename, serialized.as_bytes()) {
Ok(_) => {
info!("Wrote schema to {}", aopt.filename.display());
}
Err(err) => {
error!(
"Failed to write schema to {}: {:?}",
aopt.filename.display(),
err
);
std::process::exit(1);
}
}
}
}
}
}

View file

@ -1,2 +1,3 @@
pub mod api;
pub mod badlist;
pub mod denied_names;

View file

@ -1033,6 +1033,25 @@ pub enum PrivilegedSessionExpiryOpt {
},
}
#[derive(Args, Debug)]
pub struct ApiSchemaDownloadOpt {
#[clap(flatten)]
copt: CommonOpt,
/// Where to put the file, defaults to ./kanidm-openapi.json
#[clap(name = "filename", env, default_value = "./kanidm-openapi.json")]
filename: PathBuf,
/// Force overwriting the file if it exists
#[clap(short, long, env)]
force: bool,
}
#[derive(Debug, Subcommand)]
pub enum ApiOpt {
/// Download the OpenAPI schema file
#[clap(name = "download-schema")]
DownloadSchema(ApiSchemaDownloadOpt),
}
#[derive(Debug, Subcommand)]
pub enum SystemOpt {
#[clap(name = "pw-badlist")]
@ -1065,6 +1084,12 @@ pub enum SystemOpt {
#[clap(subcommand)]
commands: SynchOpt,
},
#[clap(name = "api")]
/// API related things
Api {
#[clap(subcommand)]
commands: ApiOpt,
},
}
#[derive(Debug, Subcommand)]