mirror of
https://github.com/kanidm/kanidm.git
synced 2025-05-17 14:33:55 +02:00
Feature: kanidm CLI pulling OpenAPI schema (#2285)
* 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:
parent
cf35a7e667
commit
7025a9ff55
book/src/developers/designs
libs/client/src
server
tools/cli/src
|
@ -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`.
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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!");
|
||||
}
|
||||
*/
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"),
|
||||
}
|
||||
};
|
||||
|
||||
|
|
84
tools/cli/src/cli/system_config/api.rs
Normal file
84
tools/cli/src/cli/system_config/api.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,2 +1,3 @@
|
|||
pub mod api;
|
||||
pub mod badlist;
|
||||
pub mod denied_names;
|
||||
|
|
|
@ -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)]
|
||||
|
|
Loading…
Reference in a new issue