mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +01:00
383 170 164 authentication updates 3 (#723)
This commit is contained in:
parent
5eb9fa604e
commit
8dc0199380
5
Cargo.lock
generated
5
Cargo.lock
generated
|
@ -638,8 +638,6 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "concread"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65099306f247b9a7bdd2919bba79355f646e477bc59ff4e8c13a1af976ed86fc"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"crossbeam",
|
||||
|
@ -2033,6 +2031,7 @@ dependencies = [
|
|||
"shellexpand",
|
||||
"structopt",
|
||||
"time 0.2.27",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"webauthn-authenticator-rs",
|
||||
|
@ -2043,7 +2042,6 @@ dependencies = [
|
|||
name = "kanidm_unix_int"
|
||||
version = "1.1.0-alpha.7"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"bytes",
|
||||
"futures",
|
||||
"kanidm",
|
||||
|
@ -2527,7 +2525,6 @@ dependencies = [
|
|||
name = "orca"
|
||||
version = "1.1.0-alpha.7"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"crossbeam",
|
||||
"csv",
|
||||
"futures-util",
|
||||
|
|
|
@ -54,6 +54,48 @@ impl KanidmAsyncClient {
|
|||
*tguard = None;
|
||||
}
|
||||
|
||||
async fn perform_simple_post_request<R: Serialize, T: DeserializeOwned>(
|
||||
&self,
|
||||
dest: &str,
|
||||
request: &R,
|
||||
) -> Result<T, ClientError> {
|
||||
let dest = format!("{}{}", self.get_url(), dest);
|
||||
|
||||
let req_string = serde_json::to_string(request).map_err(ClientError::JsonEncode)?;
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.post(dest.as_str())
|
||||
.body(req_string)
|
||||
.header(CONTENT_TYPE, APPLICATION_JSON);
|
||||
|
||||
let response = response.send().await.map_err(ClientError::Transport)?;
|
||||
|
||||
let opid = response
|
||||
.headers()
|
||||
.get(KOPID)
|
||||
.and_then(|hv| hv.to_str().ok())
|
||||
.unwrap_or("missing_kopid")
|
||||
.to_string();
|
||||
debug!("opid -> {:?}", opid);
|
||||
|
||||
match response.status() {
|
||||
reqwest::StatusCode::OK => {}
|
||||
unexpect => {
|
||||
return Err(ClientError::Http(
|
||||
unexpect,
|
||||
response.json().await.ok(),
|
||||
opid,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| ClientError::JsonDecode(e, opid))
|
||||
}
|
||||
|
||||
async fn perform_auth_post_request<R: Serialize, T: DeserializeOwned>(
|
||||
&self,
|
||||
dest: &str,
|
||||
|
@ -1207,6 +1249,50 @@ impl KanidmAsyncClient {
|
|||
})
|
||||
}
|
||||
|
||||
// == new credential update session code.
|
||||
pub async fn idm_account_credential_update_intent(
|
||||
&self,
|
||||
id: &str,
|
||||
) -> Result<CUIntentToken, ClientError> {
|
||||
self.perform_get_request(format!("/v1/account/{}/_credential/_update_intent", id).as_str())
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn idm_account_credential_update_exchange(
|
||||
&self,
|
||||
intent_token: CUIntentToken,
|
||||
) -> Result<CUSessionToken, ClientError> {
|
||||
// We don't need to send the UAT with these, which is why we use the different path.
|
||||
self.perform_simple_post_request("/v1/credential/_exchange_intent", &intent_token)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn idm_account_credential_update_status(
|
||||
&self,
|
||||
session_token: &CUSessionToken,
|
||||
) -> Result<CUStatus, ClientError> {
|
||||
self.perform_simple_post_request("/v1/credential/_status", &session_token)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn idm_account_credential_update_set_password(
|
||||
&self,
|
||||
session_token: &CUSessionToken,
|
||||
pw: &str,
|
||||
) -> Result<CUStatus, ClientError> {
|
||||
let scr = SetCredentialRequest::Password(pw.to_string());
|
||||
self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn idm_account_credential_update_commit(
|
||||
&self,
|
||||
session_token: &CUSessionToken,
|
||||
) -> Result<(), ClientError> {
|
||||
self.perform_simple_post_request("/v1/credential/_commit", &session_token)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn idm_account_radius_credential_get(
|
||||
&self,
|
||||
id: &str,
|
||||
|
|
|
@ -15,7 +15,6 @@ extern crate tracing;
|
|||
|
||||
use serde::Deserialize;
|
||||
use serde_json::error::Error as SerdeJsonError;
|
||||
use std::collections::BTreeSet as Set;
|
||||
use std::fs::{metadata, File, Metadata};
|
||||
use std::io::ErrorKind;
|
||||
use std::io::Read;
|
||||
|
@ -31,17 +30,11 @@ use uuid::Uuid;
|
|||
|
||||
pub use reqwest::StatusCode;
|
||||
|
||||
use webauthn_rs::proto::{
|
||||
CreationChallengeResponse, PublicKeyCredential, RegisterPublicKeyCredential,
|
||||
RequestChallengeResponse,
|
||||
};
|
||||
// use users::{get_current_uid, get_effective_uid};
|
||||
|
||||
use kanidm_proto::v1::*;
|
||||
|
||||
pub mod asynchronous;
|
||||
|
||||
use crate::asynchronous::KanidmAsyncClient;
|
||||
pub use crate::asynchronous::KanidmAsyncClient;
|
||||
|
||||
pub const APPLICATION_JSON: &str = "application/json";
|
||||
pub const KOPID: &str = "X-KANIDM-OPID";
|
||||
|
@ -316,10 +309,12 @@ impl KanidmClientBuilder {
|
|||
APP_USER_AGENT
|
||||
}
|
||||
|
||||
/*
|
||||
/// Consume self and return an async client.
|
||||
pub fn build(self) -> Result<KanidmClient, reqwest::Error> {
|
||||
self.build_async().map(|asclient| KanidmClient { asclient })
|
||||
}
|
||||
*/
|
||||
|
||||
/// Async client
|
||||
pub fn build_async(self) -> Result<KanidmAsyncClient, reqwest::Error> {
|
||||
|
@ -375,640 +370,3 @@ impl KanidmClientBuilder {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct KanidmClient {
|
||||
asclient: KanidmAsyncClient,
|
||||
}
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
fn tokio_block_on<R, F>(f: F) -> R
|
||||
where
|
||||
F: std::future::Future + std::future::Future<Output = R>,
|
||||
{
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("failed to start tokio");
|
||||
rt.block_on(f)
|
||||
}
|
||||
|
||||
impl KanidmClient {
|
||||
pub fn get_origin(&self) -> &str {
|
||||
self.asclient.get_origin()
|
||||
}
|
||||
|
||||
pub fn get_url(&self) -> &str {
|
||||
self.asclient.get_url()
|
||||
}
|
||||
|
||||
pub fn new_session(&self) -> Result<Self, reqwest::Error> {
|
||||
// Copy our builder, and then just process it.
|
||||
self.asclient
|
||||
.new_session()
|
||||
.map(|asclient| KanidmClient { asclient })
|
||||
}
|
||||
|
||||
pub fn set_token(&self, new_token: String) {
|
||||
tokio_block_on(self.asclient.set_token(new_token));
|
||||
}
|
||||
|
||||
pub fn get_token(&self) -> Option<String> {
|
||||
tokio_block_on(self.asclient.get_token())
|
||||
}
|
||||
|
||||
pub fn logout(&self) {
|
||||
tokio_block_on(self.asclient.logout())
|
||||
}
|
||||
|
||||
// whoami
|
||||
// Can't use generic get due to possible un-auth case.
|
||||
pub fn whoami(&self) -> Result<Option<(Entry, UserAuthToken)>, ClientError> {
|
||||
tokio_block_on(self.asclient.whoami())
|
||||
}
|
||||
|
||||
// auth
|
||||
pub fn auth_step_init(&self, ident: &str) -> Result<Set<AuthMech>, ClientError> {
|
||||
tokio_block_on(self.asclient.auth_step_init(ident))
|
||||
}
|
||||
|
||||
pub fn auth_step_begin(&self, mech: AuthMech) -> Result<Vec<AuthAllowed>, ClientError> {
|
||||
tokio_block_on(self.asclient.auth_step_begin(mech))
|
||||
}
|
||||
|
||||
pub fn auth_step_anonymous(&self) -> Result<AuthResponse, ClientError> {
|
||||
tokio_block_on(self.asclient.auth_step_anonymous())
|
||||
}
|
||||
|
||||
pub fn auth_step_password(&self, password: &str) -> Result<AuthResponse, ClientError> {
|
||||
tokio_block_on(self.asclient.auth_step_password(password))
|
||||
}
|
||||
|
||||
pub fn auth_step_backup_code(&self, backup_code: &str) -> Result<AuthResponse, ClientError> {
|
||||
tokio_block_on(self.asclient.auth_step_backup_code(backup_code))
|
||||
}
|
||||
|
||||
pub fn auth_step_totp(&self, totp: u32) -> Result<AuthResponse, ClientError> {
|
||||
tokio_block_on(self.asclient.auth_step_totp(totp))
|
||||
}
|
||||
|
||||
pub fn auth_step_webauthn_complete(
|
||||
&self,
|
||||
pkc: PublicKeyCredential,
|
||||
) -> Result<AuthResponse, ClientError> {
|
||||
tokio_block_on(self.asclient.auth_step_webauthn_complete(pkc))
|
||||
}
|
||||
|
||||
pub fn auth_anonymous(&self) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.auth_anonymous())
|
||||
}
|
||||
|
||||
pub fn auth_simple_password(&self, ident: &str, password: &str) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.auth_simple_password(ident, password))
|
||||
}
|
||||
|
||||
pub fn auth_password_totp(
|
||||
&self,
|
||||
ident: &str,
|
||||
password: &str,
|
||||
totp: u32,
|
||||
) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.auth_password_totp(ident, password, totp))
|
||||
}
|
||||
|
||||
pub fn auth_password_backup_code(
|
||||
&self,
|
||||
ident: &str,
|
||||
password: &str,
|
||||
backup_code: &str,
|
||||
) -> Result<(), ClientError> {
|
||||
tokio_block_on(
|
||||
self.asclient
|
||||
.auth_password_backup_code(ident, password, backup_code),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn auth_webauthn_begin(
|
||||
&self,
|
||||
ident: &str,
|
||||
) -> Result<RequestChallengeResponse, ClientError> {
|
||||
tokio_block_on(self.asclient.auth_webauthn_begin(ident))
|
||||
}
|
||||
|
||||
pub fn auth_valid(&self) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.auth_valid())
|
||||
}
|
||||
|
||||
pub fn auth_webauthn_complete(&self, pkc: PublicKeyCredential) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.auth_webauthn_complete(pkc))
|
||||
}
|
||||
|
||||
// search
|
||||
pub fn search(&self, filter: Filter) -> Result<Vec<Entry>, ClientError> {
|
||||
tokio_block_on(self.asclient.search(filter))
|
||||
}
|
||||
|
||||
// create
|
||||
pub fn create(&self, entries: Vec<Entry>) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.create(entries))
|
||||
}
|
||||
|
||||
// modify
|
||||
pub fn modify(&self, filter: Filter, modlist: ModifyList) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.modify(filter, modlist))
|
||||
}
|
||||
|
||||
// delete
|
||||
pub fn delete(&self, filter: Filter) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.delete(filter))
|
||||
}
|
||||
|
||||
// === idm actions here ==
|
||||
|
||||
// ===== GROUPS
|
||||
pub fn idm_group_list(&self) -> Result<Vec<Entry>, ClientError> {
|
||||
tokio_block_on(self.asclient.idm_group_list())
|
||||
}
|
||||
|
||||
pub fn idm_group_get(&self, id: &str) -> Result<Option<Entry>, ClientError> {
|
||||
tokio_block_on(self.asclient.idm_group_get(id))
|
||||
}
|
||||
|
||||
pub fn idm_group_get_members(&self, id: &str) -> Result<Option<Vec<String>>, ClientError> {
|
||||
tokio_block_on(self.asclient.idm_group_get_members(id))
|
||||
}
|
||||
|
||||
pub fn idm_group_set_members(&self, id: &str, members: &[&str]) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_group_set_members(id, members))
|
||||
}
|
||||
|
||||
pub fn idm_group_add_members(&self, id: &str, members: &[&str]) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_group_add_members(id, members))
|
||||
}
|
||||
|
||||
pub fn idm_group_remove_members(
|
||||
&self,
|
||||
group: &str,
|
||||
members: &[&str],
|
||||
) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_group_remove_members(group, members))
|
||||
}
|
||||
|
||||
pub fn idm_group_purge_members(&self, id: &str) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_group_purge_members(id))
|
||||
}
|
||||
|
||||
pub fn idm_group_unix_token_get(&self, id: &str) -> Result<UnixGroupToken, ClientError> {
|
||||
tokio_block_on(self.asclient.idm_group_unix_token_get(id))
|
||||
}
|
||||
|
||||
pub fn idm_group_unix_extend(
|
||||
&self,
|
||||
id: &str,
|
||||
gidnumber: Option<u32>,
|
||||
) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_group_unix_extend(id, gidnumber))
|
||||
}
|
||||
|
||||
pub fn idm_group_delete(&self, id: &str) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_group_delete(id))
|
||||
}
|
||||
|
||||
pub fn idm_group_create(&self, name: &str) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_group_create(name))
|
||||
}
|
||||
|
||||
// ==== accounts
|
||||
pub fn idm_account_list(&self) -> Result<Vec<Entry>, ClientError> {
|
||||
tokio_block_on(self.asclient.idm_account_list())
|
||||
}
|
||||
|
||||
pub fn idm_account_create(&self, name: &str, dn: &str) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_account_create(name, dn))
|
||||
}
|
||||
|
||||
pub fn idm_account_set_password(&self, cleartext: String) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_account_set_password(cleartext))
|
||||
}
|
||||
|
||||
pub fn idm_account_set_displayname(&self, id: &str, dn: &str) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_account_set_displayname(id, dn))
|
||||
}
|
||||
|
||||
pub fn idm_account_delete(&self, id: &str) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_account_delete(id))
|
||||
}
|
||||
|
||||
pub fn idm_account_get(&self, id: &str) -> Result<Option<Entry>, ClientError> {
|
||||
tokio_block_on(self.asclient.idm_account_get(id))
|
||||
}
|
||||
|
||||
pub fn idm_account_get_attr(
|
||||
&self,
|
||||
id: &str,
|
||||
attr: &str,
|
||||
) -> Result<Option<Vec<String>>, ClientError> {
|
||||
tokio_block_on(self.asclient.idm_account_get_attr(id, attr))
|
||||
}
|
||||
|
||||
// different ways to set the primary credential?
|
||||
// not sure how to best expose this.
|
||||
pub fn idm_account_primary_credential_set_password(
|
||||
&self,
|
||||
id: &str,
|
||||
pw: &str,
|
||||
) -> Result<SetCredentialResponse, ClientError> {
|
||||
tokio_block_on(
|
||||
self.asclient
|
||||
.idm_account_primary_credential_set_password(id, pw),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn idm_account_purge_attr(&self, id: &str, attr: &str) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_account_purge_attr(id, attr))
|
||||
}
|
||||
|
||||
pub fn idm_account_add_attr(
|
||||
&self,
|
||||
id: &str,
|
||||
attr: &str,
|
||||
values: &[&str],
|
||||
) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_account_add_attr(id, attr, values))
|
||||
}
|
||||
|
||||
pub fn idm_account_set_attr(
|
||||
&self,
|
||||
id: &str,
|
||||
attr: &str,
|
||||
values: &[&str],
|
||||
) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_account_set_attr(id, attr, values))
|
||||
}
|
||||
|
||||
pub fn idm_account_primary_credential_import_password(
|
||||
&self,
|
||||
id: &str,
|
||||
pw: &str,
|
||||
) -> Result<(), ClientError> {
|
||||
tokio_block_on(
|
||||
self.asclient
|
||||
.idm_account_primary_credential_import_password(id, pw),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn idm_account_primary_credential_set_generated(
|
||||
&self,
|
||||
id: &str,
|
||||
) -> Result<String, ClientError> {
|
||||
tokio_block_on(
|
||||
self.asclient
|
||||
.idm_account_primary_credential_set_generated(id),
|
||||
)
|
||||
}
|
||||
|
||||
// Reg intent for totp
|
||||
pub fn idm_account_primary_credential_generate_totp(
|
||||
&self,
|
||||
id: &str,
|
||||
) -> Result<(Uuid, TotpSecret), ClientError> {
|
||||
tokio_block_on(
|
||||
self.asclient
|
||||
.idm_account_primary_credential_generate_totp(id),
|
||||
)
|
||||
}
|
||||
|
||||
// Verify the totp
|
||||
pub fn idm_account_primary_credential_verify_totp(
|
||||
&self,
|
||||
id: &str,
|
||||
otp: u32,
|
||||
session: Uuid,
|
||||
) -> Result<(), ClientError> {
|
||||
tokio_block_on(
|
||||
self.asclient
|
||||
.idm_account_primary_credential_verify_totp(id, otp, session),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn idm_account_primary_credential_accept_sha1_totp(
|
||||
&self,
|
||||
id: &str,
|
||||
session: Uuid,
|
||||
) -> Result<(), ClientError> {
|
||||
tokio_block_on(
|
||||
self.asclient
|
||||
.idm_account_primary_credential_accept_sha1_totp(id, session),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn idm_account_primary_credential_remove_totp(&self, id: &str) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_account_primary_credential_remove_totp(id))
|
||||
}
|
||||
|
||||
pub fn idm_account_primary_credential_register_webauthn(
|
||||
&self,
|
||||
id: &str,
|
||||
label: &str,
|
||||
) -> Result<(Uuid, CreationChallengeResponse), ClientError> {
|
||||
tokio_block_on(
|
||||
self.asclient
|
||||
.idm_account_primary_credential_register_webauthn(id, label),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn idm_account_primary_credential_complete_webuthn_registration(
|
||||
&self,
|
||||
id: &str,
|
||||
rego: RegisterPublicKeyCredential,
|
||||
session: Uuid,
|
||||
) -> Result<(), ClientError> {
|
||||
tokio_block_on(
|
||||
self.asclient
|
||||
.idm_account_primary_credential_complete_webuthn_registration(id, rego, session),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn idm_account_primary_credential_remove_webauthn(
|
||||
&self,
|
||||
id: &str,
|
||||
label: &str,
|
||||
) -> Result<(), ClientError> {
|
||||
tokio_block_on(
|
||||
self.asclient
|
||||
.idm_account_primary_credential_remove_webauthn(id, label),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn idm_account_primary_credential_generate_backup_code(
|
||||
&self,
|
||||
id: &str,
|
||||
) -> Result<Vec<String>, ClientError> {
|
||||
tokio_block_on(
|
||||
self.asclient
|
||||
.idm_account_primary_credential_generate_backup_code(id),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn idm_account_primary_credential_remove_backup_code(
|
||||
&self,
|
||||
id: &str,
|
||||
) -> Result<(), ClientError> {
|
||||
tokio_block_on(
|
||||
self.asclient
|
||||
.idm_account_primary_credential_remove_backup_code(id),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn idm_account_get_credential_status(
|
||||
&self,
|
||||
id: &str,
|
||||
) -> Result<CredentialStatus, ClientError> {
|
||||
tokio_block_on(self.asclient.idm_account_get_credential_status(id))
|
||||
}
|
||||
|
||||
pub fn idm_account_radius_credential_get(
|
||||
&self,
|
||||
id: &str,
|
||||
) -> Result<Option<String>, ClientError> {
|
||||
tokio_block_on(self.asclient.idm_account_radius_credential_get(id))
|
||||
}
|
||||
|
||||
pub fn idm_account_radius_credential_regenerate(
|
||||
&self,
|
||||
id: &str,
|
||||
) -> Result<String, ClientError> {
|
||||
tokio_block_on(self.asclient.idm_account_radius_credential_regenerate(id))
|
||||
}
|
||||
|
||||
pub fn idm_account_radius_credential_delete(&self, id: &str) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_account_radius_credential_delete(id))
|
||||
}
|
||||
|
||||
pub fn idm_account_radius_token_get(&self, id: &str) -> Result<RadiusAuthToken, ClientError> {
|
||||
tokio_block_on(self.asclient.idm_account_radius_token_get(id))
|
||||
}
|
||||
|
||||
pub fn idm_account_unix_extend(
|
||||
&self,
|
||||
id: &str,
|
||||
gidnumber: Option<u32>,
|
||||
shell: Option<&str>,
|
||||
) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_account_unix_extend(id, gidnumber, shell))
|
||||
}
|
||||
|
||||
pub fn idm_account_unix_token_get(&self, id: &str) -> Result<UnixUserToken, ClientError> {
|
||||
tokio_block_on(self.asclient.idm_account_unix_token_get(id))
|
||||
}
|
||||
|
||||
pub fn idm_account_unix_cred_put(&self, id: &str, cred: &str) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_account_unix_cred_put(id, cred))
|
||||
}
|
||||
|
||||
pub fn idm_account_unix_cred_delete(&self, id: &str) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_account_unix_cred_delete(id))
|
||||
}
|
||||
|
||||
pub fn idm_account_unix_cred_verify(
|
||||
&self,
|
||||
id: &str,
|
||||
cred: &str,
|
||||
) -> Result<Option<UnixUserToken>, ClientError> {
|
||||
tokio_block_on(self.asclient.idm_account_unix_cred_verify(id, cred))
|
||||
}
|
||||
|
||||
/*
|
||||
pub fn idm_account_orgperson_extend(
|
||||
&self,
|
||||
id: &str,
|
||||
mail: &str,
|
||||
) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_account_orgperson_extend(id, mail))
|
||||
}
|
||||
*/
|
||||
|
||||
pub fn idm_account_get_ssh_pubkeys(&self, id: &str) -> Result<Vec<String>, ClientError> {
|
||||
tokio_block_on(self.asclient.idm_account_get_ssh_pubkeys(id))
|
||||
}
|
||||
|
||||
pub fn idm_account_post_ssh_pubkey(
|
||||
&self,
|
||||
id: &str,
|
||||
tag: &str,
|
||||
pubkey: &str,
|
||||
) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_account_post_ssh_pubkey(id, tag, pubkey))
|
||||
}
|
||||
|
||||
pub fn idm_account_person_extend(
|
||||
&self,
|
||||
id: &str,
|
||||
mail: Option<&[String]>,
|
||||
legalname: Option<&str>,
|
||||
) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_account_person_extend(id, mail, legalname))
|
||||
}
|
||||
|
||||
pub fn idm_account_person_set(
|
||||
&self,
|
||||
id: &str,
|
||||
mail: Option<&[String]>,
|
||||
legalname: Option<&str>,
|
||||
) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_account_person_set(id, mail, legalname))
|
||||
}
|
||||
|
||||
/*
|
||||
pub fn idm_account_rename_ssh_pubkey(&self, id: &str, oldtag: &str, newtag: &str) -> Result<(), ClientError> {
|
||||
self.perform_put_request(format!("/v1/account/{}/_ssh_pubkeys/{}", id, oldtag).as_str(), newtag.to_string())
|
||||
}
|
||||
*/
|
||||
|
||||
pub fn idm_account_get_ssh_pubkey(
|
||||
&self,
|
||||
id: &str,
|
||||
tag: &str,
|
||||
) -> Result<Option<String>, ClientError> {
|
||||
tokio_block_on(self.asclient.idm_account_get_ssh_pubkey(id, tag))
|
||||
}
|
||||
|
||||
pub fn idm_account_delete_ssh_pubkey(&self, id: &str, tag: &str) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_account_delete_ssh_pubkey(id, tag))
|
||||
}
|
||||
|
||||
// ==== domain_info (aka domain)
|
||||
pub fn idm_domain_get(&self) -> Result<Entry, ClientError> {
|
||||
tokio_block_on(self.asclient.idm_domain_get())
|
||||
}
|
||||
|
||||
// pub fn idm_domain_get_attr
|
||||
pub fn idm_domain_get_ssid(&self) -> Result<String, ClientError> {
|
||||
tokio_block_on(self.asclient.idm_domain_get_ssid())
|
||||
}
|
||||
|
||||
// pub fn idm_domain_put_attr
|
||||
pub fn idm_domain_set_ssid(&self, ssid: &str) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_domain_set_ssid(ssid))
|
||||
}
|
||||
|
||||
pub fn idm_domain_reset_token_key(&self) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_domain_reset_token_key())
|
||||
}
|
||||
|
||||
// ==== schema
|
||||
pub fn idm_schema_list(&self) -> Result<Vec<Entry>, ClientError> {
|
||||
tokio_block_on(self.asclient.idm_schema_list())
|
||||
}
|
||||
|
||||
pub fn idm_schema_attributetype_list(&self) -> Result<Vec<Entry>, ClientError> {
|
||||
tokio_block_on(self.asclient.idm_schema_attributetype_list())
|
||||
}
|
||||
|
||||
pub fn idm_schema_attributetype_get(&self, id: &str) -> Result<Option<Entry>, ClientError> {
|
||||
tokio_block_on(self.asclient.idm_schema_attributetype_get(id))
|
||||
}
|
||||
|
||||
pub fn idm_schema_classtype_list(&self) -> Result<Vec<Entry>, ClientError> {
|
||||
tokio_block_on(self.asclient.idm_schema_classtype_list())
|
||||
}
|
||||
|
||||
pub fn idm_schema_classtype_get(&self, id: &str) -> Result<Option<Entry>, ClientError> {
|
||||
tokio_block_on(self.asclient.idm_schema_classtype_get(id))
|
||||
}
|
||||
|
||||
// ==== Oauth2 resource server configuration
|
||||
|
||||
pub fn idm_oauth2_rs_list(&self) -> Result<Vec<Entry>, ClientError> {
|
||||
tokio_block_on(self.asclient.idm_oauth2_rs_list())
|
||||
}
|
||||
|
||||
pub fn idm_oauth2_rs_basic_create(
|
||||
&self,
|
||||
name: &str,
|
||||
displayname: &str,
|
||||
origin: &str,
|
||||
) -> Result<(), ClientError> {
|
||||
tokio_block_on(
|
||||
self.asclient
|
||||
.idm_oauth2_rs_basic_create(name, displayname, origin),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn idm_oauth2_rs_get(&self, id: &str) -> Result<Option<Entry>, ClientError> {
|
||||
tokio_block_on(self.asclient.idm_oauth2_rs_get(id))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn idm_oauth2_rs_update(
|
||||
&self,
|
||||
id: &str,
|
||||
name: Option<&str>,
|
||||
displayname: Option<&str>,
|
||||
origin: Option<&str>,
|
||||
scopes: Option<Vec<&str>>,
|
||||
reset_secret: bool,
|
||||
reset_token_key: bool,
|
||||
reset_sign_key: bool,
|
||||
) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_oauth2_rs_update(
|
||||
id,
|
||||
name,
|
||||
displayname,
|
||||
origin,
|
||||
scopes,
|
||||
reset_secret,
|
||||
reset_token_key,
|
||||
reset_sign_key,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn idm_oauth2_rs_create_scope_map(
|
||||
&self,
|
||||
id: &str,
|
||||
group: &str,
|
||||
scopes: Vec<&str>,
|
||||
) -> Result<(), ClientError> {
|
||||
tokio_block_on(
|
||||
self.asclient
|
||||
.idm_oauth2_rs_create_scope_map(id, group, scopes),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn idm_oauth2_rs_delete_scope_map(&self, id: &str, group: &str) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_oauth2_rs_delete_scope_map(id, group))
|
||||
}
|
||||
|
||||
pub fn idm_oauth2_rs_delete(&self, id: &str) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_oauth2_rs_delete(id))
|
||||
}
|
||||
|
||||
pub fn idm_oauth2_rs_enable_pkce(&self, id: &str) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_oauth2_rs_enable_pkce(id))
|
||||
}
|
||||
|
||||
pub fn idm_oauth2_rs_disable_pkce(&self, id: &str) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_oauth2_rs_disable_pkce(id))
|
||||
}
|
||||
|
||||
pub fn idm_oauth2_rs_enable_legacy_crypto(&self, id: &str) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_oauth2_rs_enable_legacy_crypto(id))
|
||||
}
|
||||
|
||||
pub fn idm_oauth2_rs_disable_legacy_crypto(&self, id: &str) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_oauth2_rs_disable_legacy_crypto(id))
|
||||
}
|
||||
|
||||
// ==== recycle bin
|
||||
pub fn recycle_bin_list(&self) -> Result<Vec<Entry>, ClientError> {
|
||||
tokio_block_on(self.asclient.recycle_bin_list())
|
||||
}
|
||||
|
||||
pub fn recycle_bin_get(&self, id: &str) -> Result<Option<Entry>, ClientError> {
|
||||
tokio_block_on(self.asclient.recycle_bin_get(id))
|
||||
}
|
||||
|
||||
pub fn recycle_bin_revive(&self, id: &str) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.recycle_bin_revive(id))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -821,6 +821,46 @@ pub enum SetCredentialResponse {
|
|||
BackupCodes(Vec<String>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CUIntentToken {
|
||||
pub intent_token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CUSessionToken {
|
||||
pub session_token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum CURequest {
|
||||
PrimaryRemove,
|
||||
Password(String),
|
||||
TotpGenerate,
|
||||
TotpVerify(u32),
|
||||
TotpAcceptSha1,
|
||||
TotpRemove,
|
||||
BackupCodeGenerate,
|
||||
BackupCodeRemove,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum CURegState {
|
||||
// Nothing in progress.
|
||||
None,
|
||||
TotpCheck(TotpSecret),
|
||||
TotpTryAgain,
|
||||
TotpInvalidSha1,
|
||||
BackupCodes(Vec<String>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CUStatus {
|
||||
pub can_commit: bool,
|
||||
pub primary: Option<CredentialDetail>,
|
||||
pub mfaregstate: CURegState,
|
||||
}
|
||||
|
||||
/* Recycle Requests area */
|
||||
|
||||
// Only two actions on recycled is possible. Search and Revive.
|
||||
|
|
|
@ -49,6 +49,7 @@ zxcvbn = "^2.2.1"
|
|||
dialoguer = "^0.10.0"
|
||||
|
||||
webauthn-authenticator-rs = "^0.3.0-alpha.12"
|
||||
tokio = { version = "^1.17.0", features = ["rt", "macros"] }
|
||||
|
||||
[build-dependencies]
|
||||
structopt = { version = "^0.3.26", default-features = false }
|
||||
|
|
|
@ -59,12 +59,12 @@ impl AccountOpt {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn exec(&self) {
|
||||
pub async fn exec(&self) {
|
||||
match self {
|
||||
// id/cred/primary/set
|
||||
AccountOpt::Credential(acopt) => match acopt {
|
||||
AccountCredential::SetPassword(acsopt) => {
|
||||
let client = acsopt.copt.to_client();
|
||||
let client = acsopt.copt.to_client().await;
|
||||
let password = match password_prompt(
|
||||
format!("Enter new password for {}: ", acsopt.aopts.account_id).as_str(),
|
||||
) {
|
||||
|
@ -75,10 +75,13 @@ impl AccountOpt {
|
|||
}
|
||||
};
|
||||
|
||||
if let Err(e) = client.idm_account_primary_credential_set_password(
|
||||
acsopt.aopts.account_id.as_str(),
|
||||
password.as_str(),
|
||||
) {
|
||||
if let Err(e) = client
|
||||
.idm_account_primary_credential_set_password(
|
||||
acsopt.aopts.account_id.as_str(),
|
||||
password.as_str(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
match e {
|
||||
// TODO: once the password length is configurable at a system level (#498), pull from the configuration.
|
||||
ClientErrorHttp(_, Some(PasswordQuality(feedback)), _) => {
|
||||
|
@ -89,11 +92,14 @@ impl AccountOpt {
|
|||
}
|
||||
}
|
||||
AccountCredential::GeneratePassword(acsopt) => {
|
||||
let client = acsopt.copt.to_client();
|
||||
let client = acsopt.copt.to_client().await;
|
||||
|
||||
match client.idm_account_primary_credential_set_generated(
|
||||
acsopt.aopts.account_id.as_str(),
|
||||
) {
|
||||
match client
|
||||
.idm_account_primary_credential_set_generated(
|
||||
acsopt.aopts.account_id.as_str(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(npw) => {
|
||||
println!(
|
||||
"Generated password for {}: {}",
|
||||
|
@ -106,13 +112,15 @@ impl AccountOpt {
|
|||
}
|
||||
}
|
||||
AccountCredential::RegisterWebauthn(acsopt) => {
|
||||
let client = acsopt.copt.to_client();
|
||||
let client = acsopt.copt.to_client().await;
|
||||
|
||||
let (session, chal) = match client
|
||||
.idm_account_primary_credential_register_webauthn(
|
||||
acsopt.aopts.account_id.as_str(),
|
||||
acsopt.tag.as_str(),
|
||||
) {
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
error!("Error Starting Registration -> {:?}", e);
|
||||
|
@ -132,11 +140,14 @@ impl AccountOpt {
|
|||
}
|
||||
};
|
||||
|
||||
match client.idm_account_primary_credential_complete_webuthn_registration(
|
||||
acsopt.aopts.account_id.as_str(),
|
||||
rego,
|
||||
session,
|
||||
) {
|
||||
match client
|
||||
.idm_account_primary_credential_complete_webuthn_registration(
|
||||
acsopt.aopts.account_id.as_str(),
|
||||
rego,
|
||||
session,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
println!("Webauthn token registration success.");
|
||||
}
|
||||
|
@ -146,11 +157,14 @@ impl AccountOpt {
|
|||
}
|
||||
}
|
||||
AccountCredential::RemoveWebauthn(acsopt) => {
|
||||
let client = acsopt.copt.to_client();
|
||||
match client.idm_account_primary_credential_remove_webauthn(
|
||||
acsopt.aopts.account_id.as_str(),
|
||||
acsopt.tag.as_str(),
|
||||
) {
|
||||
let client = acsopt.copt.to_client().await;
|
||||
match client
|
||||
.idm_account_primary_credential_remove_webauthn(
|
||||
acsopt.aopts.account_id.as_str(),
|
||||
acsopt.tag.as_str(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
println!("Webauthn removal success.");
|
||||
}
|
||||
|
@ -160,10 +174,13 @@ impl AccountOpt {
|
|||
}
|
||||
}
|
||||
AccountCredential::RegisterTotp(acsopt) => {
|
||||
let client = acsopt.copt.to_client();
|
||||
let (session, tok) = match client.idm_account_primary_credential_generate_totp(
|
||||
acsopt.aopts.account_id.as_str(),
|
||||
) {
|
||||
let client = acsopt.copt.to_client().await;
|
||||
let (session, tok) = match client
|
||||
.idm_account_primary_credential_generate_totp(
|
||||
acsopt.aopts.account_id.as_str(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
error!("Error Starting Registration -> {:?}", e);
|
||||
|
@ -221,11 +238,14 @@ impl AccountOpt {
|
|||
}
|
||||
};
|
||||
|
||||
match client.idm_account_primary_credential_verify_totp(
|
||||
acsopt.aopts.account_id.as_str(),
|
||||
totp,
|
||||
session,
|
||||
) {
|
||||
match client
|
||||
.idm_account_primary_credential_verify_totp(
|
||||
acsopt.aopts.account_id.as_str(),
|
||||
totp,
|
||||
session,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
println!("TOTP registration success.");
|
||||
break;
|
||||
|
@ -243,10 +263,13 @@ impl AccountOpt {
|
|||
};
|
||||
|
||||
if confirm_input.to_lowercase().trim() == "i am sure" {
|
||||
match client.idm_account_primary_credential_accept_sha1_totp(
|
||||
acsopt.aopts.account_id.as_str(),
|
||||
session,
|
||||
) {
|
||||
match client
|
||||
.idm_account_primary_credential_accept_sha1_totp(
|
||||
acsopt.aopts.account_id.as_str(),
|
||||
session,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
println!("TOTP registration success.");
|
||||
}
|
||||
|
@ -272,10 +295,13 @@ impl AccountOpt {
|
|||
} // end loop
|
||||
}
|
||||
AccountCredential::RemoveTotp(acsopt) => {
|
||||
let client = acsopt.copt.to_client();
|
||||
match client.idm_account_primary_credential_remove_totp(
|
||||
acsopt.aopts.account_id.as_str(),
|
||||
) {
|
||||
let client = acsopt.copt.to_client().await;
|
||||
match client
|
||||
.idm_account_primary_credential_remove_totp(
|
||||
acsopt.aopts.account_id.as_str(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
println!("TOTP removal success.");
|
||||
}
|
||||
|
@ -285,10 +311,13 @@ impl AccountOpt {
|
|||
}
|
||||
}
|
||||
AccountCredential::BackupCodeGenerate(acsopt) => {
|
||||
let client = acsopt.copt.to_client();
|
||||
match client.idm_account_primary_credential_generate_backup_code(
|
||||
acsopt.aopts.account_id.as_str(),
|
||||
) {
|
||||
let client = acsopt.copt.to_client().await;
|
||||
match client
|
||||
.idm_account_primary_credential_generate_backup_code(
|
||||
acsopt.aopts.account_id.as_str(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(s) => {
|
||||
println!("Please store these Backup codes in a safe place");
|
||||
println!("---");
|
||||
|
@ -301,10 +330,13 @@ impl AccountOpt {
|
|||
}
|
||||
}
|
||||
AccountCredential::BackupCodeRemove(acsopt) => {
|
||||
let client = acsopt.copt.to_client();
|
||||
match client.idm_account_primary_credential_remove_backup_code(
|
||||
acsopt.aopts.account_id.as_str(),
|
||||
) {
|
||||
let client = acsopt.copt.to_client().await;
|
||||
match client
|
||||
.idm_account_primary_credential_remove_backup_code(
|
||||
acsopt.aopts.account_id.as_str(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
println!("BackupCodeRemove success.");
|
||||
}
|
||||
|
@ -314,8 +346,10 @@ impl AccountOpt {
|
|||
}
|
||||
}
|
||||
AccountCredential::Status(acsopt) => {
|
||||
let client = acsopt.copt.to_client();
|
||||
match client.idm_account_get_credential_status(acsopt.aopts.account_id.as_str())
|
||||
let client = acsopt.copt.to_client().await;
|
||||
match client
|
||||
.idm_account_get_credential_status(acsopt.aopts.account_id.as_str())
|
||||
.await
|
||||
{
|
||||
Ok(status) => {
|
||||
print!("{}", status);
|
||||
|
@ -328,10 +362,11 @@ impl AccountOpt {
|
|||
}, // end AccountOpt::Credential
|
||||
AccountOpt::Radius(aropt) => match aropt {
|
||||
AccountRadius::Show(aopt) => {
|
||||
let client = aopt.copt.to_client();
|
||||
let client = aopt.copt.to_client().await;
|
||||
|
||||
let rcred =
|
||||
client.idm_account_radius_credential_get(aopt.aopts.account_id.as_str());
|
||||
let rcred = client
|
||||
.idm_account_radius_credential_get(aopt.aopts.account_id.as_str())
|
||||
.await;
|
||||
|
||||
match rcred {
|
||||
Ok(Some(s)) => println!("Radius secret: {}", s),
|
||||
|
@ -342,17 +377,19 @@ impl AccountOpt {
|
|||
}
|
||||
}
|
||||
AccountRadius::Generate(aopt) => {
|
||||
let client = aopt.copt.to_client();
|
||||
let client = aopt.copt.to_client().await;
|
||||
if let Err(e) = client
|
||||
.idm_account_radius_credential_regenerate(aopt.aopts.account_id.as_str())
|
||||
.await
|
||||
{
|
||||
error!("Error -> {:?}", e);
|
||||
}
|
||||
}
|
||||
AccountRadius::Delete(aopt) => {
|
||||
let client = aopt.copt.to_client();
|
||||
if let Err(e) =
|
||||
client.idm_account_radius_credential_delete(aopt.aopts.account_id.as_str())
|
||||
let client = aopt.copt.to_client().await;
|
||||
if let Err(e) = client
|
||||
.idm_account_radius_credential_delete(aopt.aopts.account_id.as_str())
|
||||
.await
|
||||
{
|
||||
error!("Error -> {:?}", e);
|
||||
}
|
||||
|
@ -360,8 +397,11 @@ impl AccountOpt {
|
|||
}, // end AccountOpt::Radius
|
||||
AccountOpt::Posix(apopt) => match apopt {
|
||||
AccountPosix::Show(aopt) => {
|
||||
let client = aopt.copt.to_client();
|
||||
match client.idm_account_unix_token_get(aopt.aopts.account_id.as_str()) {
|
||||
let client = aopt.copt.to_client().await;
|
||||
match client
|
||||
.idm_account_unix_token_get(aopt.aopts.account_id.as_str())
|
||||
.await
|
||||
{
|
||||
Ok(token) => println!("{}", token),
|
||||
Err(e) => {
|
||||
error!("Error -> {:?}", e);
|
||||
|
@ -369,17 +409,20 @@ impl AccountOpt {
|
|||
}
|
||||
}
|
||||
AccountPosix::Set(aopt) => {
|
||||
let client = aopt.copt.to_client();
|
||||
if let Err(e) = client.idm_account_unix_extend(
|
||||
aopt.aopts.account_id.as_str(),
|
||||
aopt.gidnumber,
|
||||
aopt.shell.as_deref(),
|
||||
) {
|
||||
let client = aopt.copt.to_client().await;
|
||||
if let Err(e) = client
|
||||
.idm_account_unix_extend(
|
||||
aopt.aopts.account_id.as_str(),
|
||||
aopt.gidnumber,
|
||||
aopt.shell.as_deref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("Error -> {:?}", e);
|
||||
}
|
||||
}
|
||||
AccountPosix::SetPassword(aopt) => {
|
||||
let client = aopt.copt.to_client();
|
||||
let client = aopt.copt.to_client().await;
|
||||
let password = match password_prompt("Enter new posix (sudo) password: ") {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
|
@ -388,41 +431,53 @@ impl AccountOpt {
|
|||
}
|
||||
};
|
||||
|
||||
if let Err(e) = client.idm_account_unix_cred_put(
|
||||
aopt.aopts.account_id.as_str(),
|
||||
password.as_str(),
|
||||
) {
|
||||
if let Err(e) = client
|
||||
.idm_account_unix_cred_put(
|
||||
aopt.aopts.account_id.as_str(),
|
||||
password.as_str(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("Error -> {:?}", e);
|
||||
}
|
||||
}
|
||||
}, // end AccountOpt::Posix
|
||||
AccountOpt::Person(apopt) => match apopt {
|
||||
AccountPerson::Extend(aopt) => {
|
||||
let client = aopt.copt.to_client();
|
||||
if let Err(e) = client.idm_account_person_extend(
|
||||
aopt.aopts.account_id.as_str(),
|
||||
aopt.mail.as_deref(),
|
||||
aopt.legalname.as_deref(),
|
||||
) {
|
||||
let client = aopt.copt.to_client().await;
|
||||
if let Err(e) = client
|
||||
.idm_account_person_extend(
|
||||
aopt.aopts.account_id.as_str(),
|
||||
aopt.mail.as_deref(),
|
||||
aopt.legalname.as_deref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("Error -> {:?}", e);
|
||||
}
|
||||
}
|
||||
AccountPerson::Set(aopt) => {
|
||||
let client = aopt.copt.to_client();
|
||||
if let Err(e) = client.idm_account_person_set(
|
||||
aopt.aopts.account_id.as_str(),
|
||||
aopt.mail.as_deref(),
|
||||
aopt.legalname.as_deref(),
|
||||
) {
|
||||
let client = aopt.copt.to_client().await;
|
||||
if let Err(e) = client
|
||||
.idm_account_person_set(
|
||||
aopt.aopts.account_id.as_str(),
|
||||
aopt.mail.as_deref(),
|
||||
aopt.legalname.as_deref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("Error -> {:?}", e);
|
||||
}
|
||||
}
|
||||
}, // end AccountOpt::Person
|
||||
AccountOpt::Ssh(asopt) => match asopt {
|
||||
AccountSsh::List(aopt) => {
|
||||
let client = aopt.copt.to_client();
|
||||
let client = aopt.copt.to_client().await;
|
||||
|
||||
match client.idm_account_get_ssh_pubkeys(aopt.aopts.account_id.as_str()) {
|
||||
match client
|
||||
.idm_account_get_ssh_pubkeys(aopt.aopts.account_id.as_str())
|
||||
.await
|
||||
{
|
||||
Ok(pkeys) => pkeys.iter().for_each(|pkey| println!("{}", pkey)),
|
||||
Err(e) => {
|
||||
error!("Error -> {:?}", e);
|
||||
|
@ -430,113 +485,132 @@ impl AccountOpt {
|
|||
}
|
||||
}
|
||||
AccountSsh::Add(aopt) => {
|
||||
let client = aopt.copt.to_client();
|
||||
if let Err(e) = client.idm_account_post_ssh_pubkey(
|
||||
aopt.aopts.account_id.as_str(),
|
||||
aopt.tag.as_str(),
|
||||
aopt.pubkey.as_str(),
|
||||
) {
|
||||
let client = aopt.copt.to_client().await;
|
||||
if let Err(e) = client
|
||||
.idm_account_post_ssh_pubkey(
|
||||
aopt.aopts.account_id.as_str(),
|
||||
aopt.tag.as_str(),
|
||||
aopt.pubkey.as_str(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("Error -> {:?}", e);
|
||||
}
|
||||
}
|
||||
AccountSsh::Delete(aopt) => {
|
||||
let client = aopt.copt.to_client();
|
||||
if let Err(e) = client.idm_account_delete_ssh_pubkey(
|
||||
aopt.aopts.account_id.as_str(),
|
||||
aopt.tag.as_str(),
|
||||
) {
|
||||
let client = aopt.copt.to_client().await;
|
||||
if let Err(e) = client
|
||||
.idm_account_delete_ssh_pubkey(
|
||||
aopt.aopts.account_id.as_str(),
|
||||
aopt.tag.as_str(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("Error -> {:?}", e);
|
||||
}
|
||||
}
|
||||
}, // end AccountOpt::Ssh
|
||||
AccountOpt::List(copt) => {
|
||||
let client = copt.to_client();
|
||||
match client.idm_account_list() {
|
||||
let client = copt.to_client().await;
|
||||
match client.idm_account_list().await {
|
||||
Ok(r) => r.iter().for_each(|ent| println!("{}", ent)),
|
||||
Err(e) => error!("Error -> {:?}", e),
|
||||
}
|
||||
}
|
||||
AccountOpt::Get(aopt) => {
|
||||
let client = aopt.copt.to_client();
|
||||
match client.idm_account_get(aopt.aopts.account_id.as_str()) {
|
||||
let client = aopt.copt.to_client().await;
|
||||
match client.idm_account_get(aopt.aopts.account_id.as_str()).await {
|
||||
Ok(Some(e)) => println!("{}", e),
|
||||
Ok(None) => println!("No matching entries"),
|
||||
Err(e) => error!("Error -> {:?}", e),
|
||||
}
|
||||
}
|
||||
AccountOpt::Delete(aopt) => {
|
||||
let client = aopt.copt.to_client();
|
||||
if let Err(e) = client.idm_account_delete(aopt.aopts.account_id.as_str()) {
|
||||
let client = aopt.copt.to_client().await;
|
||||
if let Err(e) = client
|
||||
.idm_account_delete(aopt.aopts.account_id.as_str())
|
||||
.await
|
||||
{
|
||||
error!("Error -> {:?}", e)
|
||||
}
|
||||
}
|
||||
AccountOpt::Create(acopt) => {
|
||||
let client = acopt.copt.to_client();
|
||||
if let Err(e) = client.idm_account_create(
|
||||
acopt.aopts.account_id.as_str(),
|
||||
acopt.display_name.as_str(),
|
||||
) {
|
||||
let client = acopt.copt.to_client().await;
|
||||
if let Err(e) = client
|
||||
.idm_account_create(
|
||||
acopt.aopts.account_id.as_str(),
|
||||
acopt.display_name.as_str(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("Error -> {:?}", e)
|
||||
}
|
||||
}
|
||||
AccountOpt::Validity(avopt) => match avopt {
|
||||
AccountValidity::Show(ano) => {
|
||||
let client = ano.copt.to_client();
|
||||
let client = ano.copt.to_client().await;
|
||||
|
||||
let r = client
|
||||
let ex = match client
|
||||
.idm_account_get_attr(ano.aopts.account_id.as_str(), "account_expire")
|
||||
.and_then(|v1| {
|
||||
client
|
||||
.idm_account_get_attr(
|
||||
ano.aopts.account_id.as_str(),
|
||||
"account_valid_from",
|
||||
)
|
||||
.map(|v2| (v1, v2))
|
||||
});
|
||||
|
||||
match r {
|
||||
Ok((ex, vf)) => {
|
||||
if let Some(t) = vf {
|
||||
// Convert the time to local timezone.
|
||||
let t = OffsetDateTime::parse(&t[0], time::Format::Rfc3339)
|
||||
.map(|odt| {
|
||||
odt.to_offset(
|
||||
time::UtcOffset::try_current_local_offset()
|
||||
.unwrap_or(time::UtcOffset::UTC),
|
||||
)
|
||||
.format(time::Format::Rfc3339)
|
||||
})
|
||||
.unwrap_or_else(|_| "invalid timestamp".to_string());
|
||||
|
||||
println!("valid after: {}", t);
|
||||
} else {
|
||||
println!("valid after: any time");
|
||||
}
|
||||
|
||||
if let Some(t) = ex {
|
||||
let t = OffsetDateTime::parse(&t[0], time::Format::Rfc3339)
|
||||
.map(|odt| {
|
||||
odt.to_offset(
|
||||
time::UtcOffset::try_current_local_offset()
|
||||
.unwrap_or(time::UtcOffset::UTC),
|
||||
)
|
||||
.format(time::Format::Rfc3339)
|
||||
})
|
||||
.unwrap_or_else(|_| "invalid timestamp".to_string());
|
||||
println!("expire: {}", t);
|
||||
} else {
|
||||
println!("expire: never");
|
||||
}
|
||||
.await
|
||||
{
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
error!("Error -> {:?}", e);
|
||||
return;
|
||||
}
|
||||
Err(e) => error!("Error -> {:?}", e),
|
||||
};
|
||||
|
||||
let vf = match client
|
||||
.idm_account_get_attr(ano.aopts.account_id.as_str(), "account_valid_from")
|
||||
.await
|
||||
{
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
error!("Error -> {:?}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(t) = vf {
|
||||
// Convert the time to local timezone.
|
||||
let t = OffsetDateTime::parse(&t[0], time::Format::Rfc3339)
|
||||
.map(|odt| {
|
||||
odt.to_offset(
|
||||
time::UtcOffset::try_current_local_offset()
|
||||
.unwrap_or(time::UtcOffset::UTC),
|
||||
)
|
||||
.format(time::Format::Rfc3339)
|
||||
})
|
||||
.unwrap_or_else(|_| "invalid timestamp".to_string());
|
||||
|
||||
println!("valid after: {}", t);
|
||||
} else {
|
||||
println!("valid after: any time");
|
||||
}
|
||||
|
||||
if let Some(t) = ex {
|
||||
let t = OffsetDateTime::parse(&t[0], time::Format::Rfc3339)
|
||||
.map(|odt| {
|
||||
odt.to_offset(
|
||||
time::UtcOffset::try_current_local_offset()
|
||||
.unwrap_or(time::UtcOffset::UTC),
|
||||
)
|
||||
.format(time::Format::Rfc3339)
|
||||
})
|
||||
.unwrap_or_else(|_| "invalid timestamp".to_string());
|
||||
println!("expire: {}", t);
|
||||
} else {
|
||||
println!("expire: never");
|
||||
}
|
||||
}
|
||||
AccountValidity::ExpireAt(ano) => {
|
||||
let client = ano.copt.to_client();
|
||||
let client = ano.copt.to_client().await;
|
||||
if matches!(ano.datetime.as_str(), "never" | "clear") {
|
||||
// Unset the value
|
||||
match client
|
||||
.idm_account_purge_attr(ano.aopts.account_id.as_str(), "account_expire")
|
||||
.await
|
||||
{
|
||||
Err(e) => error!("Error -> {:?}", e),
|
||||
_ => println!("Success"),
|
||||
|
@ -549,24 +623,30 @@ impl AccountOpt {
|
|||
return;
|
||||
}
|
||||
|
||||
match client.idm_account_set_attr(
|
||||
ano.aopts.account_id.as_str(),
|
||||
"account_expire",
|
||||
&[ano.datetime.as_str()],
|
||||
) {
|
||||
match client
|
||||
.idm_account_set_attr(
|
||||
ano.aopts.account_id.as_str(),
|
||||
"account_expire",
|
||||
&[ano.datetime.as_str()],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Err(e) => error!("Error -> {:?}", e),
|
||||
_ => println!("Success"),
|
||||
}
|
||||
}
|
||||
}
|
||||
AccountValidity::BeginFrom(ano) => {
|
||||
let client = ano.copt.to_client();
|
||||
let client = ano.copt.to_client().await;
|
||||
if matches!(ano.datetime.as_str(), "any" | "clear" | "whenever") {
|
||||
// Unset the value
|
||||
match client.idm_account_purge_attr(
|
||||
ano.aopts.account_id.as_str(),
|
||||
"account_valid_from",
|
||||
) {
|
||||
match client
|
||||
.idm_account_purge_attr(
|
||||
ano.aopts.account_id.as_str(),
|
||||
"account_valid_from",
|
||||
)
|
||||
.await
|
||||
{
|
||||
Err(e) => error!("Error -> {:?}", e),
|
||||
_ => println!("Success"),
|
||||
}
|
||||
|
@ -579,11 +659,14 @@ impl AccountOpt {
|
|||
return;
|
||||
}
|
||||
|
||||
match client.idm_account_set_attr(
|
||||
ano.aopts.account_id.as_str(),
|
||||
"account_valid_from",
|
||||
&[ano.datetime.as_str()],
|
||||
) {
|
||||
match client
|
||||
.idm_account_set_attr(
|
||||
ano.aopts.account_id.as_str(),
|
||||
"account_valid_from",
|
||||
&[ano.datetime.as_str()],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Err(e) => error!("Error -> {:?}", e),
|
||||
_ => println!("Success"),
|
||||
}
|
||||
|
|
|
@ -2,12 +2,12 @@ use crate::session::read_tokens;
|
|||
use crate::CommonOpt;
|
||||
use compact_jwt::{Jws, JwsUnverified};
|
||||
use dialoguer::{theme::ColorfulTheme, Select};
|
||||
use kanidm_client::{KanidmClient, KanidmClientBuilder};
|
||||
use kanidm_client::{KanidmAsyncClient, KanidmClientBuilder};
|
||||
use kanidm_proto::v1::UserAuthToken;
|
||||
use std::str::FromStr;
|
||||
|
||||
impl CommonOpt {
|
||||
pub fn to_unauth_client(&self) -> KanidmClient {
|
||||
pub fn to_unauth_client(&self) -> KanidmAsyncClient {
|
||||
let config_path: String = shellexpand::tilde("~/.config/kanidm").into_owned();
|
||||
|
||||
let client_builder = KanidmClientBuilder::new()
|
||||
|
@ -46,13 +46,13 @@ impl CommonOpt {
|
|||
client_builder
|
||||
);
|
||||
|
||||
client_builder.build().unwrap_or_else(|e| {
|
||||
client_builder.build_async().unwrap_or_else(|e| {
|
||||
error!("Failed to build client instance -- {:?}", e);
|
||||
std::process::exit(1);
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_client(&self) -> KanidmClient {
|
||||
pub async fn to_client(&self) -> KanidmAsyncClient {
|
||||
let client = self.to_unauth_client();
|
||||
// Read the token file.
|
||||
let tokens = match read_tokens() {
|
||||
|
@ -132,7 +132,7 @@ impl CommonOpt {
|
|||
};
|
||||
|
||||
// Set it into the client
|
||||
client.set_token(token);
|
||||
client.set_token(token).await;
|
||||
|
||||
client
|
||||
}
|
||||
|
|
|
@ -7,18 +7,18 @@ impl DomainOpt {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn exec(&self) {
|
||||
pub async fn exec(&self) {
|
||||
match self {
|
||||
DomainOpt::Show(copt) => {
|
||||
let client = copt.to_client();
|
||||
match client.idm_domain_get() {
|
||||
let client = copt.to_client().await;
|
||||
match client.idm_domain_get().await {
|
||||
Ok(e) => println!("{}", e),
|
||||
Err(e) => error!("Error -> {:?}", e),
|
||||
}
|
||||
}
|
||||
DomainOpt::ResetTokenKey(copt) => {
|
||||
let client = copt.to_client();
|
||||
match client.idm_domain_reset_token_key() {
|
||||
let client = copt.to_client().await;
|
||||
match client.idm_domain_reset_token_key().await {
|
||||
Ok(_) => println!("Success"),
|
||||
Err(e) => error!("Error -> {:?}", e),
|
||||
}
|
||||
|
|
|
@ -19,41 +19,41 @@ impl GroupOpt {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn exec(&self) {
|
||||
pub async fn exec(&self) {
|
||||
match self {
|
||||
GroupOpt::List(copt) => {
|
||||
let client = copt.to_client();
|
||||
match client.idm_group_list() {
|
||||
let client = copt.to_client().await;
|
||||
match client.idm_group_list().await {
|
||||
Ok(r) => r.iter().for_each(|ent| println!("{}", ent)),
|
||||
Err(e) => error!("Error -> {:?}", e),
|
||||
}
|
||||
}
|
||||
GroupOpt::Get(gcopt) => {
|
||||
let client = gcopt.copt.to_client();
|
||||
let client = gcopt.copt.to_client().await;
|
||||
// idm_group_get
|
||||
match client.idm_group_get(gcopt.name.as_str()) {
|
||||
match client.idm_group_get(gcopt.name.as_str()).await {
|
||||
Ok(Some(e)) => println!("{}", e),
|
||||
Ok(None) => warn!("No matching group '{}'", gcopt.name.as_str()),
|
||||
Err(e) => error!("Error -> {:?}", e),
|
||||
}
|
||||
}
|
||||
GroupOpt::Create(gcopt) => {
|
||||
let client = gcopt.copt.to_client();
|
||||
match client.idm_group_create(gcopt.name.as_str()) {
|
||||
let client = gcopt.copt.to_client().await;
|
||||
match client.idm_group_create(gcopt.name.as_str()).await {
|
||||
Err(e) => error!("Error -> {:?}", e),
|
||||
Ok(_) => println!("Successfully created group '{}'", gcopt.name.as_str()),
|
||||
}
|
||||
}
|
||||
GroupOpt::Delete(gcopt) => {
|
||||
let client = gcopt.copt.to_client();
|
||||
match client.idm_group_delete(gcopt.name.as_str()) {
|
||||
let client = gcopt.copt.to_client().await;
|
||||
match client.idm_group_delete(gcopt.name.as_str()).await {
|
||||
Err(e) => error!("Error -> {:?}", e),
|
||||
Ok(_) => println!("Successfully deleted group {}", gcopt.name.as_str()),
|
||||
}
|
||||
}
|
||||
GroupOpt::PurgeMembers(gcopt) => {
|
||||
let client = gcopt.copt.to_client();
|
||||
match client.idm_group_purge_members(gcopt.name.as_str()) {
|
||||
let client = gcopt.copt.to_client().await;
|
||||
match client.idm_group_purge_members(gcopt.name.as_str()).await {
|
||||
Err(e) => error!("Error -> {:?}", e),
|
||||
Ok(_) => println!(
|
||||
"Successfully purged members of group {}",
|
||||
|
@ -62,53 +62,65 @@ impl GroupOpt {
|
|||
}
|
||||
}
|
||||
GroupOpt::ListMembers(gcopt) => {
|
||||
let client = gcopt.copt.to_client();
|
||||
match client.idm_group_get_members(gcopt.name.as_str()) {
|
||||
let client = gcopt.copt.to_client().await;
|
||||
match client.idm_group_get_members(gcopt.name.as_str()).await {
|
||||
Ok(Some(groups)) => groups.iter().for_each(|m| println!("{:?}", m)),
|
||||
Ok(None) => warn!("No members in group {}", gcopt.name.as_str()),
|
||||
Err(e) => error!("Error -> {:?}", e),
|
||||
}
|
||||
}
|
||||
GroupOpt::AddMembers(gcopt) => {
|
||||
let client = gcopt.copt.to_client();
|
||||
let client = gcopt.copt.to_client().await;
|
||||
let new_members: Vec<&str> = gcopt.members.iter().map(String::as_str).collect();
|
||||
|
||||
match client.idm_group_add_members(gcopt.name.as_str(), &new_members) {
|
||||
match client
|
||||
.idm_group_add_members(gcopt.name.as_str(), &new_members)
|
||||
.await
|
||||
{
|
||||
Err(e) => error!("Error -> {:?}", e),
|
||||
Ok(_) => warn!("Successfully added members to {}", gcopt.name.as_str()),
|
||||
}
|
||||
}
|
||||
|
||||
GroupOpt::RemoveMembers(gcopt) => {
|
||||
let client = gcopt.copt.to_client();
|
||||
let client = gcopt.copt.to_client().await;
|
||||
let remove_members: Vec<&str> = gcopt.members.iter().map(String::as_str).collect();
|
||||
|
||||
match client.idm_group_remove_members(gcopt.name.as_str(), &remove_members) {
|
||||
match client
|
||||
.idm_group_remove_members(gcopt.name.as_str(), &remove_members)
|
||||
.await
|
||||
{
|
||||
Err(e) => error!("Failed to remove members -> {:?}", e),
|
||||
Ok(_) => println!("Successfully removed members from {}", gcopt.name.as_str()),
|
||||
}
|
||||
}
|
||||
|
||||
GroupOpt::SetMembers(gcopt) => {
|
||||
let client = gcopt.copt.to_client();
|
||||
let client = gcopt.copt.to_client().await;
|
||||
let new_members: Vec<&str> = gcopt.members.iter().map(String::as_str).collect();
|
||||
|
||||
match client.idm_group_set_members(gcopt.name.as_str(), &new_members) {
|
||||
match client
|
||||
.idm_group_set_members(gcopt.name.as_str(), &new_members)
|
||||
.await
|
||||
{
|
||||
Err(e) => error!("Error -> {:?}", e),
|
||||
Ok(_) => println!("Successfully set members for group {}", gcopt.name.as_str()),
|
||||
}
|
||||
}
|
||||
GroupOpt::Posix(gpopt) => match gpopt {
|
||||
GroupPosix::Show(gcopt) => {
|
||||
let client = gcopt.copt.to_client();
|
||||
match client.idm_group_unix_token_get(gcopt.name.as_str()) {
|
||||
let client = gcopt.copt.to_client().await;
|
||||
match client.idm_group_unix_token_get(gcopt.name.as_str()).await {
|
||||
Ok(token) => println!("{}", token),
|
||||
Err(e) => error!("Error -> {:?}", e),
|
||||
}
|
||||
}
|
||||
GroupPosix::Set(gcopt) => {
|
||||
let client = gcopt.copt.to_client();
|
||||
match client.idm_group_unix_extend(gcopt.name.as_str(), gcopt.gidnumber) {
|
||||
let client = gcopt.copt.to_client().await;
|
||||
match client
|
||||
.idm_group_unix_extend(gcopt.name.as_str(), gcopt.gidnumber)
|
||||
.await
|
||||
{
|
||||
Err(e) => error!("Error -> {:?}", e),
|
||||
Ok(_) => println!(
|
||||
"Success adding POSIX configuration for group {}",
|
||||
|
|
|
@ -35,12 +35,12 @@ impl SelfOpt {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn exec(&self) {
|
||||
pub async fn exec(&self) {
|
||||
match self {
|
||||
SelfOpt::Whoami(copt) => {
|
||||
let client = copt.to_client();
|
||||
let client = copt.to_client().await;
|
||||
|
||||
match client.whoami() {
|
||||
match client.whoami().await {
|
||||
Ok(o_ent) => {
|
||||
match o_ent {
|
||||
Some((ent, uat)) => {
|
||||
|
@ -58,7 +58,7 @@ impl SelfOpt {
|
|||
}
|
||||
|
||||
SelfOpt::SetPassword(copt) => {
|
||||
let client = copt.to_client();
|
||||
let client = copt.to_client().await;
|
||||
|
||||
let password = match rpassword::prompt_password("Enter new password: ") {
|
||||
Ok(p) => p,
|
||||
|
@ -68,7 +68,7 @@ impl SelfOpt {
|
|||
}
|
||||
};
|
||||
|
||||
if let Err(e) = client.idm_account_set_password(password) {
|
||||
if let Err(e) = client.idm_account_set_password(password).await {
|
||||
error!("Error -> {:?}", e);
|
||||
}
|
||||
}
|
||||
|
@ -84,10 +84,10 @@ impl SystemOpt {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn exec(&self) {
|
||||
pub async fn exec(&self) {
|
||||
match self {
|
||||
SystemOpt::Oauth2(oopt) => oopt.exec(),
|
||||
SystemOpt::Domain(dopt) => dopt.exec(),
|
||||
SystemOpt::Oauth2(oopt) => oopt.exec().await,
|
||||
SystemOpt::Domain(dopt) => dopt.exec().await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -107,17 +107,17 @@ impl KanidmClientOpt {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn exec(&self) {
|
||||
pub async fn exec(&self) {
|
||||
match self {
|
||||
KanidmClientOpt::Raw(ropt) => ropt.exec(),
|
||||
KanidmClientOpt::Login(lopt) => lopt.exec(),
|
||||
KanidmClientOpt::Logout(lopt) => lopt.exec(),
|
||||
KanidmClientOpt::Session(sopt) => sopt.exec(),
|
||||
KanidmClientOpt::CSelf(csopt) => csopt.exec(),
|
||||
KanidmClientOpt::Account(aopt) => aopt.exec(),
|
||||
KanidmClientOpt::Group(gopt) => gopt.exec(),
|
||||
KanidmClientOpt::System(sopt) => sopt.exec(),
|
||||
KanidmClientOpt::Recycle(ropt) => ropt.exec(),
|
||||
KanidmClientOpt::Raw(ropt) => ropt.exec().await,
|
||||
KanidmClientOpt::Login(lopt) => lopt.exec().await,
|
||||
KanidmClientOpt::Logout(lopt) => lopt.exec().await,
|
||||
KanidmClientOpt::Session(sopt) => sopt.exec().await,
|
||||
KanidmClientOpt::CSelf(csopt) => csopt.exec().await,
|
||||
KanidmClientOpt::Account(aopt) => aopt.exec().await,
|
||||
KanidmClientOpt::Group(gopt) => gopt.exec().await,
|
||||
KanidmClientOpt::System(sopt) => sopt.exec().await,
|
||||
KanidmClientOpt::Recycle(ropt) => ropt.exec().await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,8 @@ use structopt::StructOpt;
|
|||
use tracing_subscriber::prelude::*;
|
||||
use tracing_subscriber::{fmt, EnvFilter};
|
||||
|
||||
fn main() {
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() {
|
||||
let opt = KanidmClientOpt::from_args();
|
||||
|
||||
let fmt_layer = fmt::layer().with_writer(std::io::stderr);
|
||||
|
@ -42,5 +43,5 @@ fn main() {
|
|||
.with(fmt_layer)
|
||||
.init();
|
||||
|
||||
opt.exec()
|
||||
opt.exec().await
|
||||
}
|
||||
|
|
|
@ -19,133 +19,155 @@ impl Oauth2Opt {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn exec(&self) {
|
||||
pub async fn exec(&self) {
|
||||
match self {
|
||||
Oauth2Opt::List(copt) => {
|
||||
let client = copt.to_client();
|
||||
match client.idm_oauth2_rs_list() {
|
||||
let client = copt.to_client().await;
|
||||
match client.idm_oauth2_rs_list().await {
|
||||
Ok(r) => r.iter().for_each(|ent| println!("{}", ent)),
|
||||
Err(e) => error!("Error -> {:?}", e),
|
||||
}
|
||||
}
|
||||
Oauth2Opt::Get(nopt) => {
|
||||
let client = nopt.copt.to_client();
|
||||
match client.idm_oauth2_rs_get(nopt.name.as_str()) {
|
||||
let client = nopt.copt.to_client().await;
|
||||
match client.idm_oauth2_rs_get(nopt.name.as_str()).await {
|
||||
Ok(Some(e)) => println!("{}", e),
|
||||
Ok(None) => println!("No matching entries"),
|
||||
Err(e) => error!("Error -> {:?}", e),
|
||||
}
|
||||
}
|
||||
Oauth2Opt::CreateBasic(cbopt) => {
|
||||
let client = cbopt.nopt.copt.to_client();
|
||||
match client.idm_oauth2_rs_basic_create(
|
||||
cbopt.nopt.name.as_str(),
|
||||
cbopt.displayname.as_str(),
|
||||
cbopt.origin.as_str(),
|
||||
) {
|
||||
let client = cbopt.nopt.copt.to_client().await;
|
||||
match client
|
||||
.idm_oauth2_rs_basic_create(
|
||||
cbopt.nopt.name.as_str(),
|
||||
cbopt.displayname.as_str(),
|
||||
cbopt.origin.as_str(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => println!("Success"),
|
||||
Err(e) => error!("Error -> {:?}", e),
|
||||
}
|
||||
}
|
||||
Oauth2Opt::SetImplictScopes(cbopt) => {
|
||||
let client = cbopt.nopt.copt.to_client();
|
||||
match client.idm_oauth2_rs_update(
|
||||
cbopt.nopt.name.as_str(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(cbopt.scopes.iter().map(|s| s.as_str()).collect()),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
) {
|
||||
let client = cbopt.nopt.copt.to_client().await;
|
||||
match client
|
||||
.idm_oauth2_rs_update(
|
||||
cbopt.nopt.name.as_str(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(cbopt.scopes.iter().map(|s| s.as_str()).collect()),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => println!("Success"),
|
||||
Err(e) => error!("Error -> {:?}", e),
|
||||
}
|
||||
}
|
||||
Oauth2Opt::CreateScopeMap(cbopt) => {
|
||||
let client = cbopt.nopt.copt.to_client();
|
||||
match client.idm_oauth2_rs_create_scope_map(
|
||||
cbopt.nopt.name.as_str(),
|
||||
cbopt.group.as_str(),
|
||||
cbopt.scopes.iter().map(|s| s.as_str()).collect(),
|
||||
) {
|
||||
let client = cbopt.nopt.copt.to_client().await;
|
||||
match client
|
||||
.idm_oauth2_rs_create_scope_map(
|
||||
cbopt.nopt.name.as_str(),
|
||||
cbopt.group.as_str(),
|
||||
cbopt.scopes.iter().map(|s| s.as_str()).collect(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => println!("Success"),
|
||||
Err(e) => error!("Error -> {:?}", e),
|
||||
}
|
||||
}
|
||||
Oauth2Opt::DeleteScopeMap(cbopt) => {
|
||||
let client = cbopt.nopt.copt.to_client();
|
||||
let client = cbopt.nopt.copt.to_client().await;
|
||||
match client
|
||||
.idm_oauth2_rs_delete_scope_map(cbopt.nopt.name.as_str(), cbopt.group.as_str())
|
||||
.await
|
||||
{
|
||||
Ok(_) => println!("Success"),
|
||||
Err(e) => error!("Error -> {:?}", e),
|
||||
}
|
||||
}
|
||||
Oauth2Opt::ResetSecrets(cbopt) => {
|
||||
let client = cbopt.copt.to_client();
|
||||
match client.idm_oauth2_rs_update(
|
||||
cbopt.name.as_str(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
) {
|
||||
let client = cbopt.copt.to_client().await;
|
||||
match client
|
||||
.idm_oauth2_rs_update(
|
||||
cbopt.name.as_str(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => println!("Success"),
|
||||
Err(e) => error!("Error -> {:?}", e),
|
||||
}
|
||||
}
|
||||
Oauth2Opt::Delete(nopt) => {
|
||||
let client = nopt.copt.to_client();
|
||||
match client.idm_oauth2_rs_delete(nopt.name.as_str()) {
|
||||
let client = nopt.copt.to_client().await;
|
||||
match client.idm_oauth2_rs_delete(nopt.name.as_str()).await {
|
||||
Ok(_) => println!("Success"),
|
||||
Err(e) => error!("Error -> {:?}", e),
|
||||
}
|
||||
}
|
||||
Oauth2Opt::SetDisplayname(cbopt) => {
|
||||
let client = cbopt.nopt.copt.to_client();
|
||||
match client.idm_oauth2_rs_update(
|
||||
cbopt.nopt.name.as_str(),
|
||||
None,
|
||||
Some(&cbopt.displayname.as_str()),
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
) {
|
||||
let client = cbopt.nopt.copt.to_client().await;
|
||||
match client
|
||||
.idm_oauth2_rs_update(
|
||||
cbopt.nopt.name.as_str(),
|
||||
None,
|
||||
Some(&cbopt.displayname.as_str()),
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => println!("Success"),
|
||||
Err(e) => error!("Error -> {:?}", e),
|
||||
}
|
||||
}
|
||||
Oauth2Opt::EnablePkce(nopt) => {
|
||||
let client = nopt.copt.to_client();
|
||||
match client.idm_oauth2_rs_enable_pkce(nopt.name.as_str()) {
|
||||
let client = nopt.copt.to_client().await;
|
||||
match client.idm_oauth2_rs_enable_pkce(nopt.name.as_str()).await {
|
||||
Ok(_) => println!("Success"),
|
||||
Err(e) => error!("Error -> {:?}", e),
|
||||
}
|
||||
}
|
||||
Oauth2Opt::DisablePkce(nopt) => {
|
||||
let client = nopt.copt.to_client();
|
||||
match client.idm_oauth2_rs_disable_pkce(nopt.name.as_str()) {
|
||||
let client = nopt.copt.to_client().await;
|
||||
match client.idm_oauth2_rs_disable_pkce(nopt.name.as_str()).await {
|
||||
Ok(_) => println!("Success"),
|
||||
Err(e) => error!("Error -> {:?}", e),
|
||||
}
|
||||
}
|
||||
Oauth2Opt::EnableLegacyCrypto(nopt) => {
|
||||
let client = nopt.copt.to_client();
|
||||
match client.idm_oauth2_rs_enable_legacy_crypto(nopt.name.as_str()) {
|
||||
let client = nopt.copt.to_client().await;
|
||||
match client
|
||||
.idm_oauth2_rs_enable_legacy_crypto(nopt.name.as_str())
|
||||
.await
|
||||
{
|
||||
Ok(_) => println!("Success"),
|
||||
Err(e) => error!("Error -> {:?}", e),
|
||||
}
|
||||
}
|
||||
Oauth2Opt::DisableLegacyCrypto(nopt) => {
|
||||
let client = nopt.copt.to_client();
|
||||
match client.idm_oauth2_rs_disable_legacy_crypto(nopt.name.as_str()) {
|
||||
let client = nopt.copt.to_client().await;
|
||||
match client
|
||||
.idm_oauth2_rs_disable_legacy_crypto(nopt.name.as_str())
|
||||
.await
|
||||
{
|
||||
Ok(_) => println!("Success"),
|
||||
Err(e) => error!("Error -> {:?}", e),
|
||||
}
|
||||
|
|
|
@ -26,10 +26,10 @@ impl RawOpt {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn exec(&self) {
|
||||
pub async fn exec(&self) {
|
||||
match self {
|
||||
RawOpt::Search(sopt) => {
|
||||
let client = sopt.commonopts.to_client();
|
||||
let client = sopt.commonopts.to_client().await;
|
||||
|
||||
let filter: Filter = match serde_json::from_str(sopt.filter.as_str()) {
|
||||
Ok(f) => f,
|
||||
|
@ -39,13 +39,13 @@ impl RawOpt {
|
|||
}
|
||||
};
|
||||
|
||||
match client.search(filter) {
|
||||
match client.search(filter).await {
|
||||
Ok(rset) => rset.iter().for_each(|e| println!("{}", e)),
|
||||
Err(e) => error!("Error -> {:?}", e),
|
||||
}
|
||||
}
|
||||
RawOpt::Create(copt) => {
|
||||
let client = copt.commonopts.to_client();
|
||||
let client = copt.commonopts.to_client().await;
|
||||
// Read the file?
|
||||
let r_entries: Vec<BTreeMap<String, Vec<String>>> = match read_file(&copt.file) {
|
||||
Ok(r) => r,
|
||||
|
@ -57,12 +57,12 @@ impl RawOpt {
|
|||
|
||||
let entries = r_entries.into_iter().map(|b| Entry { attrs: b }).collect();
|
||||
|
||||
if let Err(e) = client.create(entries) {
|
||||
if let Err(e) = client.create(entries).await {
|
||||
error!("Error -> {:?}", e);
|
||||
}
|
||||
}
|
||||
RawOpt::Modify(mopt) => {
|
||||
let client = mopt.commonopts.to_client();
|
||||
let client = mopt.commonopts.to_client().await;
|
||||
// Read the file?
|
||||
let filter: Filter = match serde_json::from_str(mopt.filter.as_str()) {
|
||||
Ok(f) => f,
|
||||
|
@ -81,12 +81,12 @@ impl RawOpt {
|
|||
};
|
||||
|
||||
let modlist = ModifyList::new_list(r_list);
|
||||
if let Err(e) = client.modify(filter, modlist) {
|
||||
if let Err(e) = client.modify(filter, modlist).await {
|
||||
error!("Error -> {:?}", e);
|
||||
}
|
||||
}
|
||||
RawOpt::Delete(dopt) => {
|
||||
let client = dopt.commonopts.to_client();
|
||||
let client = dopt.commonopts.to_client().await;
|
||||
let filter: Filter = match serde_json::from_str(dopt.filter.as_str()) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
|
@ -95,7 +95,7 @@ impl RawOpt {
|
|||
}
|
||||
};
|
||||
|
||||
if let Err(e) = client.delete(filter) {
|
||||
if let Err(e) = client.delete(filter).await {
|
||||
error!("Error -> {:?}", e);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,11 +9,11 @@ impl RecycleOpt {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn exec(&self) {
|
||||
pub async fn exec(&self) {
|
||||
match self {
|
||||
RecycleOpt::List(copt) => {
|
||||
let client = copt.to_client();
|
||||
match client.recycle_bin_list() {
|
||||
let client = copt.to_client().await;
|
||||
match client.recycle_bin_list().await {
|
||||
Ok(r) => r.iter().for_each(|e| println!("{}", e)),
|
||||
Err(e) => {
|
||||
error!("Error -> {:?}", e);
|
||||
|
@ -21,8 +21,8 @@ impl RecycleOpt {
|
|||
}
|
||||
}
|
||||
RecycleOpt::Get(nopt) => {
|
||||
let client = nopt.copt.to_client();
|
||||
match client.recycle_bin_get(nopt.name.as_str()) {
|
||||
let client = nopt.copt.to_client().await;
|
||||
match client.recycle_bin_get(nopt.name.as_str()).await {
|
||||
Ok(Some(e)) => println!("{}", e),
|
||||
Ok(None) => println!("No matching entries"),
|
||||
Err(e) => {
|
||||
|
@ -31,8 +31,8 @@ impl RecycleOpt {
|
|||
}
|
||||
}
|
||||
RecycleOpt::Revive(nopt) => {
|
||||
let client = nopt.copt.to_client();
|
||||
if let Err(e) = client.recycle_bin_revive(nopt.name.as_str()) {
|
||||
let client = nopt.copt.to_client().await;
|
||||
if let Err(e) = client.recycle_bin_revive(nopt.name.as_str()).await {
|
||||
error!("Error -> {:?}", e);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::common::prompt_for_username_get_username;
|
||||
use crate::{LoginOpt, LogoutOpt, SessionOpt};
|
||||
|
||||
use kanidm_client::{ClientError, KanidmClient};
|
||||
use kanidm_client::{ClientError, KanidmAsyncClient};
|
||||
use kanidm_proto::v1::{AuthAllowed, AuthResponse, AuthState, UserAuthToken};
|
||||
#[cfg(target_family = "unix")]
|
||||
use libc::umask;
|
||||
|
@ -142,15 +142,21 @@ impl LoginOpt {
|
|||
self.copt.debug
|
||||
}
|
||||
|
||||
fn do_password(&self, client: &mut KanidmClient) -> Result<AuthResponse, ClientError> {
|
||||
async fn do_password(
|
||||
&self,
|
||||
client: &mut KanidmAsyncClient,
|
||||
) -> Result<AuthResponse, ClientError> {
|
||||
let password = rpassword::prompt_password("Enter password: ").unwrap_or_else(|e| {
|
||||
error!("Failed to create password prompt -- {:?}", e);
|
||||
std::process::exit(1);
|
||||
});
|
||||
client.auth_step_password(password.as_str())
|
||||
client.auth_step_password(password.as_str()).await
|
||||
}
|
||||
|
||||
fn do_backup_code(&self, client: &mut KanidmClient) -> Result<AuthResponse, ClientError> {
|
||||
async fn do_backup_code(
|
||||
&self,
|
||||
client: &mut KanidmAsyncClient,
|
||||
) -> Result<AuthResponse, ClientError> {
|
||||
print!("Enter Backup Code: ");
|
||||
// We flush stdout so it'll write the buffer to screen, continuing operation. Without it, the application halts.
|
||||
#[allow(clippy::unwrap_used)]
|
||||
|
@ -165,10 +171,10 @@ impl LoginOpt {
|
|||
break;
|
||||
};
|
||||
}
|
||||
client.auth_step_backup_code(backup_code.trim())
|
||||
client.auth_step_backup_code(backup_code.trim()).await
|
||||
}
|
||||
|
||||
fn do_totp(&self, client: &mut KanidmClient) -> Result<AuthResponse, ClientError> {
|
||||
async fn do_totp(&self, client: &mut KanidmAsyncClient) -> Result<AuthResponse, ClientError> {
|
||||
let totp = loop {
|
||||
print!("Enter TOTP: ");
|
||||
// We flush stdout so it'll write the buffer to screen, continuing operation. Without it, the application halts.
|
||||
|
@ -187,12 +193,12 @@ impl LoginOpt {
|
|||
Err(_) => eprintln!("Invalid Number"),
|
||||
};
|
||||
};
|
||||
client.auth_step_totp(totp)
|
||||
client.auth_step_totp(totp).await
|
||||
}
|
||||
|
||||
fn do_webauthn(
|
||||
async fn do_webauthn(
|
||||
&self,
|
||||
client: &mut KanidmClient,
|
||||
client: &mut KanidmAsyncClient,
|
||||
pkr: RequestChallengeResponse,
|
||||
) -> Result<AuthResponse, ClientError> {
|
||||
let mut wa = WebauthnAuthenticator::new(U2FHid::new());
|
||||
|
@ -204,10 +210,10 @@ impl LoginOpt {
|
|||
std::process::exit(1);
|
||||
});
|
||||
|
||||
client.auth_step_webauthn_complete(auth)
|
||||
client.auth_step_webauthn_complete(auth).await
|
||||
}
|
||||
|
||||
pub fn exec(&self) {
|
||||
pub async fn exec(&self) {
|
||||
let mut client = self.copt.to_unauth_client();
|
||||
|
||||
// TODO: remove this anon, nobody should do default anonymous
|
||||
|
@ -216,6 +222,7 @@ impl LoginOpt {
|
|||
// What auth mechanisms exist?
|
||||
let mechs: Vec<_> = client
|
||||
.auth_step_init(username)
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
error!("Error during authentication init phase: {:?}", e);
|
||||
std::process::exit(1);
|
||||
|
@ -250,10 +257,13 @@ impl LoginOpt {
|
|||
}
|
||||
};
|
||||
|
||||
let mut allowed = client.auth_step_begin((*mech).clone()).unwrap_or_else(|e| {
|
||||
error!("Error during authentication begin phase: {:?}", e);
|
||||
std::process::exit(1);
|
||||
});
|
||||
let mut allowed = client
|
||||
.auth_step_begin((*mech).clone())
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
error!("Error during authentication begin phase: {:?}", e);
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
// We now have the first auth state, so we can proceed until complete.
|
||||
loop {
|
||||
|
@ -289,11 +299,11 @@ impl LoginOpt {
|
|||
};
|
||||
|
||||
let res = match choice {
|
||||
AuthAllowed::Anonymous => client.auth_step_anonymous(),
|
||||
AuthAllowed::Password => self.do_password(&mut client),
|
||||
AuthAllowed::BackupCode => self.do_backup_code(&mut client),
|
||||
AuthAllowed::Totp => self.do_totp(&mut client),
|
||||
AuthAllowed::Webauthn(chal) => self.do_webauthn(&mut client, chal.clone()),
|
||||
AuthAllowed::Anonymous => client.auth_step_anonymous().await,
|
||||
AuthAllowed::Password => self.do_password(&mut client).await,
|
||||
AuthAllowed::BackupCode => self.do_backup_code(&mut client).await,
|
||||
AuthAllowed::Totp => self.do_totp(&mut client).await,
|
||||
AuthAllowed::Webauthn(chal) => self.do_webauthn(&mut client, chal.clone()).await,
|
||||
};
|
||||
|
||||
// Now update state.
|
||||
|
@ -326,7 +336,7 @@ impl LoginOpt {
|
|||
std::process::exit(1);
|
||||
});
|
||||
// Add our new one
|
||||
match client.get_token() {
|
||||
match client.get_token().await {
|
||||
Some(t) => tokens.insert(username.to_string(), t),
|
||||
None => {
|
||||
error!("Error retrieving client session");
|
||||
|
@ -350,7 +360,7 @@ impl LogoutOpt {
|
|||
self.debug
|
||||
}
|
||||
|
||||
pub fn exec(&self) {
|
||||
pub async fn exec(&self) {
|
||||
// For now we just remove this from the token store.
|
||||
|
||||
let mut _tmp_username = String::new();
|
||||
|
@ -420,7 +430,7 @@ impl SessionOpt {
|
|||
.collect()
|
||||
}
|
||||
|
||||
pub fn exec(&self) {
|
||||
pub async fn exec(&self) {
|
||||
match self {
|
||||
SessionOpt::List(_) => {
|
||||
let tokens = Self::read_valid_tokens();
|
||||
|
|
|
@ -21,7 +21,8 @@ include!("opt/ssh_authorizedkeys.rs");
|
|||
//
|
||||
// usage: AuthorizedKeysCommand /usr/sbin/kanidm_ssh_authorizedkeys %u -H URL -D anonymous -C /etc/kanidm/ca.pem
|
||||
//
|
||||
fn main() {
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() {
|
||||
let opt = SshAuthorizedOpt::from_args();
|
||||
if opt.debug {
|
||||
::std::env::set_var("RUST_LOG", "kanidm=debug,kanidm_client=debug");
|
||||
|
@ -57,19 +58,21 @@ fn main() {
|
|||
None => client_builder,
|
||||
};
|
||||
|
||||
let client = client_builder.build().unwrap_or_else(|e| {
|
||||
let client = client_builder.build_async().unwrap_or_else(|e| {
|
||||
error!("Failed to build client instance -- {:?}", e);
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
let r = if opt.username == "anonymous" {
|
||||
client.auth_anonymous()
|
||||
client.auth_anonymous().await
|
||||
} else {
|
||||
let password = rpassword::prompt_password("Enter password: ").unwrap_or_else(|e| {
|
||||
error!("Failed to retrieve password - {:?}", e);
|
||||
std::process::exit(1);
|
||||
});
|
||||
client.auth_simple_password(opt.username.as_str(), password.as_str())
|
||||
client
|
||||
.auth_simple_password(opt.username.as_str(), password.as_str())
|
||||
.await
|
||||
};
|
||||
if r.is_err() {
|
||||
match r {
|
||||
|
@ -81,7 +84,10 @@ fn main() {
|
|||
std::process::exit(1);
|
||||
}
|
||||
|
||||
match client.idm_account_get_ssh_pubkeys(opt.account_id.as_str()) {
|
||||
match client
|
||||
.idm_account_get_ssh_pubkeys(opt.account_id.as_str())
|
||||
.await
|
||||
{
|
||||
Ok(pkeys) => pkeys.iter().for_each(|pkey| println!("{}", pkey)),
|
||||
Err(e) => error!("Failed to retrieve pubkeys - {:?}", e),
|
||||
}
|
||||
|
|
|
@ -71,7 +71,7 @@ r2d2_sqlite = "^0.20.0"
|
|||
reqwest = "^0.11.10"
|
||||
|
||||
users = "^0.11.0"
|
||||
async-std = { version = "^1.11.0", features = ["tokio1"] }
|
||||
#async-std = { version = "^1.11.0", features = ["tokio1"] }
|
||||
|
||||
|
||||
lru = "^0.7.5"
|
||||
|
|
|
@ -697,17 +697,16 @@ impl<'a> Drop for DbTxn<'a> {
|
|||
mod tests {
|
||||
use super::Db;
|
||||
use crate::cache::Id;
|
||||
use async_std::task;
|
||||
use kanidm_proto::v1::{UnixGroupToken, UnixUserToken};
|
||||
|
||||
const TESTACCOUNT1_PASSWORD_A: &str = "password a for account1 test";
|
||||
const TESTACCOUNT1_PASSWORD_B: &str = "password b for account1 test";
|
||||
|
||||
#[test]
|
||||
fn test_cache_db_account_basic() {
|
||||
#[tokio::test]
|
||||
async fn test_cache_db_account_basic() {
|
||||
let _ = tracing_subscriber::fmt::try_init();
|
||||
let db = Db::new("").expect("failed to create.");
|
||||
let dbtxn = task::block_on(db.write());
|
||||
let dbtxn = db.write().await;
|
||||
assert!(dbtxn.migrate().is_ok());
|
||||
|
||||
let mut ut1 = UnixUserToken {
|
||||
|
@ -787,11 +786,11 @@ mod tests {
|
|||
assert!(dbtxn.commit().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_db_group_basic() {
|
||||
#[tokio::test]
|
||||
async fn test_cache_db_group_basic() {
|
||||
let _ = tracing_subscriber::fmt::try_init();
|
||||
let db = Db::new("").expect("failed to create.");
|
||||
let dbtxn = task::block_on(db.write());
|
||||
let dbtxn = db.write().await;
|
||||
assert!(dbtxn.migrate().is_ok());
|
||||
|
||||
let mut gt1 = UnixGroupToken {
|
||||
|
@ -862,11 +861,11 @@ mod tests {
|
|||
assert!(dbtxn.commit().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_db_account_group_update() {
|
||||
#[tokio::test]
|
||||
async fn test_cache_db_account_group_update() {
|
||||
let _ = tracing_subscriber::fmt::try_init();
|
||||
let db = Db::new("").expect("failed to create.");
|
||||
let dbtxn = task::block_on(db.write());
|
||||
let dbtxn = db.write().await;
|
||||
assert!(dbtxn.migrate().is_ok());
|
||||
|
||||
let gt1 = UnixGroupToken {
|
||||
|
@ -930,11 +929,11 @@ mod tests {
|
|||
assert!(dbtxn.commit().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_db_account_password() {
|
||||
#[tokio::test]
|
||||
async fn test_cache_db_account_password() {
|
||||
let _ = tracing_subscriber::fmt::try_init();
|
||||
let db = Db::new("").expect("failed to create.");
|
||||
let dbtxn = task::block_on(db.write());
|
||||
let dbtxn = db.write().await;
|
||||
assert!(dbtxn.migrate().is_ok());
|
||||
|
||||
let uuid1 = "0302b99c-f0f6-41ab-9492-852692b0fd16";
|
||||
|
@ -979,11 +978,11 @@ mod tests {
|
|||
assert!(dbtxn.commit().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_db_group_rename_duplicate() {
|
||||
#[tokio::test]
|
||||
async fn test_cache_db_group_rename_duplicate() {
|
||||
let _ = tracing_subscriber::fmt::try_init();
|
||||
let db = Db::new("").expect("failed to create.");
|
||||
let dbtxn = task::block_on(db.write());
|
||||
let dbtxn = db.write().await;
|
||||
assert!(dbtxn.migrate().is_ok());
|
||||
|
||||
let mut gt1 = UnixGroupToken {
|
||||
|
@ -1034,11 +1033,11 @@ mod tests {
|
|||
assert!(dbtxn.commit().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_db_account_rename_duplicate() {
|
||||
#[tokio::test]
|
||||
async fn test_cache_db_account_rename_duplicate() {
|
||||
let _ = tracing_subscriber::fmt::try_init();
|
||||
let db = Db::new("").expect("failed to create.");
|
||||
let dbtxn = task::block_on(db.write());
|
||||
let dbtxn = db.write().await;
|
||||
assert!(dbtxn.migrate().is_ok());
|
||||
|
||||
let mut ut1 = UnixUserToken {
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1412,6 +1412,10 @@ pub struct AccessControlsReadTransaction<'a> {
|
|||
Cell<ARCacheReadTxn<'a, (IdentityId, Filter<FilterValid>), Filter<FilterValidResolved>>>,
|
||||
}
|
||||
|
||||
unsafe impl<'a> Sync for AccessControlsReadTransaction<'a> {}
|
||||
|
||||
unsafe impl<'a> Send for AccessControlsReadTransaction<'a> {}
|
||||
|
||||
impl<'a> AccessControlsTransaction<'a> for AccessControlsReadTransaction<'a> {
|
||||
fn get_search(&self) -> &Vec<AccessControlSearch> {
|
||||
&self.inner.acps_search
|
||||
|
|
|
@ -10,6 +10,7 @@ use crate::be::BackendTransaction;
|
|||
use crate::event::{
|
||||
AuthEvent, AuthResult, OnlineBackupEvent, SearchEvent, SearchResult, WhoamiResult,
|
||||
};
|
||||
use crate::idm::credupdatesession::CredentialUpdateSessionToken;
|
||||
use crate::idm::event::{
|
||||
CredentialStatusEvent, RadiusAuthTokenEvent, ReadBackupCodeEvent, UnixGroupTokenEvent,
|
||||
UnixUserAuthEvent, UnixUserTokenEvent,
|
||||
|
@ -27,8 +28,8 @@ use crate::ldap::{LdapBoundToken, LdapResponseState, LdapServer};
|
|||
|
||||
use kanidm_proto::v1::Entry as ProtoEntry;
|
||||
use kanidm_proto::v1::{
|
||||
AuthRequest, CredentialStatus, SearchRequest, SearchResponse, UnixGroupToken, UnixUserToken,
|
||||
WhoamiResponse,
|
||||
AuthRequest, CURequest, CUSessionToken, CUStatus, CredentialStatus, SearchRequest,
|
||||
SearchResponse, UnixGroupToken, UnixUserToken, WhoamiResponse,
|
||||
};
|
||||
|
||||
use regex::Regex;
|
||||
|
@ -967,6 +968,136 @@ impl QueryServerReadV1 {
|
|||
res
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "idmcredentialupdatestatus",
|
||||
skip(self, session_token, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
pub async fn handle_idmcredentialupdatestatus(
|
||||
&self,
|
||||
session_token: CUSessionToken,
|
||||
eventid: Uuid,
|
||||
) -> Result<CUStatus, OperationError> {
|
||||
let ct = duration_from_epoch_now();
|
||||
let idms_cred_update = self.idms.cred_update_transaction_async().await;
|
||||
let res = spanned!("actors::v1_read::handle<IdmCredentialUpdateStatus>", {
|
||||
let session_token = CredentialUpdateSessionToken {
|
||||
token_enc: session_token.session_token,
|
||||
};
|
||||
|
||||
idms_cred_update
|
||||
.credential_update_status(&session_token, ct)
|
||||
.map_err(|e| {
|
||||
admin_error!(
|
||||
err = ?e,
|
||||
"Failed to begin credential_update_status",
|
||||
);
|
||||
e
|
||||
})
|
||||
.map(|sta| sta.into())
|
||||
});
|
||||
res
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "idmcredentialupdate",
|
||||
skip(self, session_token, scr, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
pub async fn handle_idmcredentialupdate(
|
||||
&self,
|
||||
session_token: CUSessionToken,
|
||||
scr: CURequest,
|
||||
eventid: Uuid,
|
||||
) -> Result<CUStatus, OperationError> {
|
||||
let ct = duration_from_epoch_now();
|
||||
let idms_cred_update = self.idms.cred_update_transaction_async().await;
|
||||
let res = spanned!("actors::v1_read::handle<IdmCredentialUpdate>", {
|
||||
let session_token = CredentialUpdateSessionToken {
|
||||
token_enc: session_token.session_token,
|
||||
};
|
||||
|
||||
match scr {
|
||||
CURequest::PrimaryRemove => idms_cred_update
|
||||
.credential_primary_delete(&session_token, ct)
|
||||
.map_err(|e| {
|
||||
admin_error!(
|
||||
err = ?e,
|
||||
"Failed to begin credential_primary_delete",
|
||||
);
|
||||
e
|
||||
}),
|
||||
CURequest::Password(pw) => idms_cred_update
|
||||
.credential_primary_set_password(&session_token, ct, &pw)
|
||||
.map_err(|e| {
|
||||
admin_error!(
|
||||
err = ?e,
|
||||
"Failed to begin credential_primary_set_password",
|
||||
);
|
||||
e
|
||||
}),
|
||||
CURequest::TotpGenerate => idms_cred_update
|
||||
.credential_primary_init_totp(&session_token, ct)
|
||||
.map_err(|e| {
|
||||
admin_error!(
|
||||
err = ?e,
|
||||
"Failed to begin credential_primary_init_totp",
|
||||
);
|
||||
e
|
||||
}),
|
||||
CURequest::TotpVerify(totp_chal) => idms_cred_update
|
||||
.credential_primary_check_totp(&session_token, ct, totp_chal)
|
||||
.map_err(|e| {
|
||||
admin_error!(
|
||||
err = ?e,
|
||||
"Failed to begin credential_primary_check_totp",
|
||||
);
|
||||
e
|
||||
}),
|
||||
CURequest::TotpAcceptSha1 => idms_cred_update
|
||||
.credential_primary_accept_sha1_totp(&session_token, ct)
|
||||
.map_err(|e| {
|
||||
admin_error!(
|
||||
err = ?e,
|
||||
"Failed to begin credential_primary_remove_totp",
|
||||
);
|
||||
e
|
||||
}),
|
||||
CURequest::TotpRemove => idms_cred_update
|
||||
.credential_primary_remove_totp(&session_token, ct)
|
||||
.map_err(|e| {
|
||||
admin_error!(
|
||||
err = ?e,
|
||||
"Failed to begin credential_primary_remove_totp",
|
||||
);
|
||||
e
|
||||
}),
|
||||
CURequest::BackupCodeGenerate => idms_cred_update
|
||||
.credential_primary_init_backup_codes(&session_token, ct)
|
||||
.map_err(|e| {
|
||||
admin_error!(
|
||||
err = ?e,
|
||||
"Failed to begin credential_primary_init_backup_codes",
|
||||
);
|
||||
e
|
||||
}),
|
||||
CURequest::BackupCodeRemove => idms_cred_update
|
||||
.credential_primary_remove_backup_codes(&session_token, ct)
|
||||
.map_err(|e| {
|
||||
admin_error!(
|
||||
err = ?e,
|
||||
"Failed to begin credential_primary_remove_backup_codes",
|
||||
);
|
||||
e
|
||||
}),
|
||||
}
|
||||
.map(|sta| sta.into())
|
||||
});
|
||||
res
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "oauth2_authorise",
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
use std::iter;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tracing::{info, instrument, span, trace, Level};
|
||||
|
||||
use crate::idm::event::GenerateBackupCodeEvent;
|
||||
use crate::idm::event::RemoveBackupCodeEvent;
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::idm::credupdatesession::{
|
||||
CredentialUpdateIntentToken, CredentialUpdateSessionToken, InitCredentialUpdateIntentEvent,
|
||||
};
|
||||
|
||||
use crate::event::{
|
||||
CreateEvent, DeleteEvent, ModifyEvent, PurgeRecycledEvent, PurgeTombstoneEvent,
|
||||
ReviveRecycledEvent,
|
||||
|
@ -28,8 +33,8 @@ use kanidm_proto::v1::Entry as ProtoEntry;
|
|||
use kanidm_proto::v1::Modify as ProtoModify;
|
||||
use kanidm_proto::v1::ModifyList as ProtoModifyList;
|
||||
use kanidm_proto::v1::{
|
||||
AccountPersonSet, AccountUnixExtend, CreateRequest, DeleteRequest, GroupUnixExtend,
|
||||
ModifyRequest, SetCredentialRequest, SetCredentialResponse,
|
||||
AccountPersonSet, AccountUnixExtend, CUIntentToken, CUSessionToken, CreateRequest,
|
||||
DeleteRequest, GroupUnixExtend, ModifyRequest, SetCredentialRequest, SetCredentialResponse,
|
||||
};
|
||||
|
||||
use uuid::Uuid;
|
||||
|
@ -650,6 +655,125 @@ impl QueryServerWriteV1 {
|
|||
res
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "idmcredentialupdateintent",
|
||||
skip(self, uat, uuid_or_name, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
pub async fn handle_idmcredentialupdateintent(
|
||||
&self,
|
||||
uat: Option<String>,
|
||||
uuid_or_name: String,
|
||||
ttl: Option<Duration>,
|
||||
eventid: Uuid,
|
||||
) -> Result<CUIntentToken, OperationError> {
|
||||
let ct = duration_from_epoch_now();
|
||||
let mut idms_prox_write = self.idms.proxy_write_async(ct).await;
|
||||
let res = spanned!("actors::v1_write::handle<IdmCredentialUpdateIntent>", {
|
||||
let ident = idms_prox_write
|
||||
.validate_and_parse_uat(uat.as_deref(), ct)
|
||||
.and_then(|uat| idms_prox_write.process_uat_to_identity(&uat, ct))
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Invalid identity");
|
||||
e
|
||||
})?;
|
||||
|
||||
let target_uuid = idms_prox_write
|
||||
.qs_write
|
||||
.name_to_uuid(uuid_or_name.as_str())
|
||||
.map_err(|e| {
|
||||
admin_error!(err = ?e, "Error resolving id to target");
|
||||
e
|
||||
})?;
|
||||
|
||||
idms_prox_write
|
||||
.init_credential_update_intent(
|
||||
&InitCredentialUpdateIntentEvent::new(ident, target_uuid, ttl),
|
||||
ct,
|
||||
)
|
||||
.and_then(|tok| idms_prox_write.commit().map(|_| tok))
|
||||
.map_err(|e| {
|
||||
admin_error!(
|
||||
err = ?e,
|
||||
"Failed to begin init_credential_update_intent",
|
||||
);
|
||||
e
|
||||
})
|
||||
.map(|tok| CUIntentToken {
|
||||
intent_token: tok.token_enc,
|
||||
})
|
||||
});
|
||||
res
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "idmcredentialexchangeintent",
|
||||
skip(self, intent_token, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
pub async fn handle_idmcredentialexchangeintent(
|
||||
&self,
|
||||
intent_token: CUIntentToken,
|
||||
eventid: Uuid,
|
||||
) -> Result<CUSessionToken, OperationError> {
|
||||
let ct = duration_from_epoch_now();
|
||||
let mut idms_prox_write = self.idms.proxy_write_async(ct).await;
|
||||
let res = spanned!("actors::v1_write::handle<IdmCredentialExchangeIntent>", {
|
||||
let intent_token = CredentialUpdateIntentToken {
|
||||
token_enc: intent_token.intent_token,
|
||||
};
|
||||
|
||||
idms_prox_write
|
||||
.exchange_intent_credential_update(intent_token, ct)
|
||||
.and_then(|tok| idms_prox_write.commit().map(|_| tok))
|
||||
.map_err(|e| {
|
||||
admin_error!(
|
||||
err = ?e,
|
||||
"Failed to begin exchange_intent_credential_update",
|
||||
);
|
||||
e
|
||||
})
|
||||
.map(|tok| CUSessionToken {
|
||||
session_token: tok.token_enc,
|
||||
})
|
||||
});
|
||||
res
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "idmcredentialupdatecommit",
|
||||
skip(self, session_token, eventid)
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
pub async fn handle_idmcredentialupdatecommit(
|
||||
&self,
|
||||
session_token: CUSessionToken,
|
||||
eventid: Uuid,
|
||||
) -> Result<(), OperationError> {
|
||||
let ct = duration_from_epoch_now();
|
||||
let mut idms_prox_write = self.idms.proxy_write_async(ct).await;
|
||||
let res = spanned!("actors::v1_write::handle<IdmCredentialUpdateCommit>", {
|
||||
let session_token = CredentialUpdateSessionToken {
|
||||
token_enc: session_token.session_token,
|
||||
};
|
||||
|
||||
idms_prox_write
|
||||
.commit_credential_update(session_token, ct)
|
||||
.and_then(|tok| idms_prox_write.commit().map(|_| tok))
|
||||
.map_err(|e| {
|
||||
admin_error!(
|
||||
err = ?e,
|
||||
"Failed to begin commit_credential_update",
|
||||
);
|
||||
e
|
||||
})
|
||||
});
|
||||
res
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "trace",
|
||||
name = "idmaccountsetpassword",
|
||||
|
|
|
@ -117,6 +117,10 @@ pub struct BackendReadTransaction<'a> {
|
|||
idxmeta: CowCellReadTxn<IdxMeta>,
|
||||
}
|
||||
|
||||
unsafe impl<'a> Sync for BackendReadTransaction<'a> {}
|
||||
|
||||
unsafe impl<'a> Send for BackendReadTransaction<'a> {}
|
||||
|
||||
pub struct BackendWriteTransaction<'a> {
|
||||
idlayer: UnsafeCell<IdlArcSqliteWriteTransaction<'a>>,
|
||||
idxmeta: CowCellReadTxn<IdxMeta>,
|
||||
|
|
|
@ -9,17 +9,18 @@ use hashbrown::HashSet;
|
|||
|
||||
use crate::credential::totp::{Totp, TOTP_DEFAULT_STEP};
|
||||
|
||||
use kanidm_proto::v1::{CredentialDetail, PasswordFeedback, TotpSecret};
|
||||
use kanidm_proto::v1::{CURegState, CUStatus, CredentialDetail, PasswordFeedback, TotpSecret};
|
||||
|
||||
use crate::utils::{backup_code_from_random, uuid_from_duration};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use tokio::sync::Mutex;
|
||||
// use tokio::sync::Mutex;
|
||||
|
||||
use core::ops::Deref;
|
||||
|
||||
|
@ -47,7 +48,7 @@ struct CredentialUpdateIntentTokenInner {
|
|||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CredentialUpdateIntentToken {
|
||||
token_enc: String,
|
||||
pub token_enc: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
|
@ -58,7 +59,7 @@ struct CredentialUpdateSessionTokenInner {
|
|||
}
|
||||
|
||||
pub struct CredentialUpdateSessionToken {
|
||||
token_enc: String,
|
||||
pub token_enc: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -107,6 +108,24 @@ pub(crate) struct CredentialUpdateSessionStatus {
|
|||
mfaregstate: MfaRegStateStatus,
|
||||
}
|
||||
|
||||
impl Into<CUStatus> for CredentialUpdateSessionStatus {
|
||||
fn into(self) -> CUStatus {
|
||||
CUStatus {
|
||||
can_commit: self.can_commit,
|
||||
primary: self.primary,
|
||||
mfaregstate: match self.mfaregstate {
|
||||
MfaRegStateStatus::None => CURegState::None,
|
||||
MfaRegStateStatus::TotpCheck(c) => CURegState::TotpCheck(c),
|
||||
MfaRegStateStatus::TotpTryAgain => CURegState::TotpTryAgain,
|
||||
MfaRegStateStatus::TotpInvalidSha1 => CURegState::TotpInvalidSha1,
|
||||
MfaRegStateStatus::BackupCodes(s) => {
|
||||
CURegState::BackupCodes(s.into_iter().collect())
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&CredentialUpdateSession> for CredentialUpdateSessionStatus {
|
||||
fn from(session: &CredentialUpdateSession) -> Self {
|
||||
CredentialUpdateSessionStatus {
|
||||
|
@ -132,10 +151,18 @@ pub struct InitCredentialUpdateIntentEvent {
|
|||
// Who is it targetting?
|
||||
pub target: Uuid,
|
||||
// How long is it valid for?
|
||||
pub max_ttl: Duration,
|
||||
pub max_ttl: Option<Duration>,
|
||||
}
|
||||
|
||||
impl InitCredentialUpdateIntentEvent {
|
||||
pub fn new(ident: Identity, target: Uuid, max_ttl: Option<Duration>) -> Self {
|
||||
InitCredentialUpdateIntentEvent {
|
||||
ident,
|
||||
target,
|
||||
max_ttl,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn new_impersonate_entry(
|
||||
e: std::sync::Arc<Entry<EntrySealed, EntryCommitted>>,
|
||||
|
@ -146,7 +173,7 @@ impl InitCredentialUpdateIntentEvent {
|
|||
InitCredentialUpdateIntentEvent {
|
||||
ident,
|
||||
target,
|
||||
max_ttl,
|
||||
max_ttl: Some(max_ttl),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -255,6 +282,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
|
||||
// Store the update session into the map.
|
||||
self.cred_update_sessions.insert(sessionid, session);
|
||||
trace!("cred_update_sessions.insert - {}", sessionid);
|
||||
|
||||
// - issue the CredentialUpdateToken (enc)
|
||||
Ok(CredentialUpdateSessionToken { token_enc })
|
||||
|
@ -266,21 +294,13 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
ct: Duration,
|
||||
) -> Result<CredentialUpdateIntentToken, OperationError> {
|
||||
spanned!("idm::server::credupdatesession<Init>", {
|
||||
let _account = self.validate_init_credential_update(event.target, &event.ident)?;
|
||||
let account = self.validate_init_credential_update(event.target, &event.ident)?;
|
||||
|
||||
// ==== AUTHORISATION CHECKED ===
|
||||
|
||||
// Build the intent token.
|
||||
|
||||
// States For the user record
|
||||
// - Initial (Valid)
|
||||
// - Processing (Uuid of in flight req, plus the expiry time of the in flight)
|
||||
// - Canceled (Back to Valid)
|
||||
// - Complete (The credential was updatded).
|
||||
|
||||
// We need to actually submit a mod to the user.
|
||||
|
||||
let max_ttl = ct + event.max_ttl.clamp(MINIMUM_INTENT_TTL, MAXIMUM_INTENT_TTL);
|
||||
let mttl = event.max_ttl.unwrap_or_else(|| Duration::new(0, 0));
|
||||
let max_ttl = ct + mttl.clamp(MINIMUM_INTENT_TTL, MAXIMUM_INTENT_TTL);
|
||||
let sessionid = uuid_from_duration(max_ttl, self.sid);
|
||||
let uuid = Uuid::new_v4();
|
||||
|
||||
|
@ -302,6 +322,23 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
.token_enc_key
|
||||
.encrypt_at_time(&token_data, ct.as_secs());
|
||||
|
||||
// Mark that we have created an intent token on the user.
|
||||
let modlist = ModifyList::new_append(
|
||||
"credential_update_intent_token",
|
||||
Value::IntentToken(token.sessionid, IntentTokenState::Valid),
|
||||
);
|
||||
|
||||
self.qs_write
|
||||
.internal_modify(
|
||||
// Filter as executed
|
||||
&filter!(f_eq("uuid", PartialValue::new_uuidr(&account.uuid))),
|
||||
&modlist,
|
||||
)
|
||||
.map_err(|e| {
|
||||
request_error!(error = ?e);
|
||||
e
|
||||
})?;
|
||||
|
||||
Ok(CredentialUpdateIntentToken { token_enc })
|
||||
})
|
||||
}
|
||||
|
@ -389,13 +426,19 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
// Need to change this to the expiry time, so we can purge up to.
|
||||
let sessionid = uuid_from_duration(ct + MAXIMUM_CRED_UPDATE_TTL, self.sid);
|
||||
|
||||
let modlist = ModifyList::new_append(
|
||||
"credential_update_intent_token",
|
||||
let mut modlist = ModifyList::new();
|
||||
|
||||
modlist.push_mod(Modify::Removed(
|
||||
AttrString::from("credential_update_intent_token"),
|
||||
PartialValue::IntentToken(token.sessionid),
|
||||
));
|
||||
modlist.push_mod(Modify::Present(
|
||||
AttrString::from("credential_update_intent_token"),
|
||||
Value::IntentToken(
|
||||
token.sessionid,
|
||||
IntentTokenState::InProgress(sessionid, ct + MAXIMUM_CRED_UPDATE_TTL),
|
||||
),
|
||||
);
|
||||
));
|
||||
|
||||
self.qs_write
|
||||
.internal_modify(
|
||||
|
@ -464,7 +507,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
|
||||
let session_handle = self.cred_update_sessions.remove(&session_token.sessionid)
|
||||
.ok_or_else(|| {
|
||||
admin_error!("No such sessionid exists on this server - may be due to a load balancer failover or replay?");
|
||||
admin_error!("No such sessionid exists on this server - may be due to a load balancer failover or replay? {:?}", session_token.sessionid);
|
||||
OperationError::InvalidState
|
||||
})?;
|
||||
|
||||
|
@ -552,19 +595,24 @@ impl<'a> IdmServerCredUpdateTransaction<'a> {
|
|||
|
||||
self.cred_update_sessions.get(&session_token.sessionid)
|
||||
.ok_or_else(|| {
|
||||
admin_error!("No such sessionid exists on this server - may be due to a load balancer failover or token replay?");
|
||||
admin_error!("No such sessionid exists on this server - may be due to a load balancer failover or token replay? {}", session_token.sessionid);
|
||||
OperationError::InvalidState
|
||||
})
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub async fn credential_status(
|
||||
// I think I need this to be a try lock instead, and fail on error, because
|
||||
// of the nature of the async bits.
|
||||
pub fn credential_update_status(
|
||||
&self,
|
||||
cust: &CredentialUpdateSessionToken,
|
||||
ct: Duration,
|
||||
) -> Result<CredentialUpdateSessionStatus, OperationError> {
|
||||
let session_handle = self.get_current_session(cust, ct)?;
|
||||
let session = session_handle.lock().await;
|
||||
let session = session_handle.try_lock().map_err(|_| {
|
||||
admin_error!("Session already locked, unable to proceed.");
|
||||
OperationError::InvalidState
|
||||
})?;
|
||||
trace!(?session);
|
||||
|
||||
let status: CredentialUpdateSessionStatus = session.deref().into();
|
||||
|
@ -715,14 +763,17 @@ impl<'a> IdmServerCredUpdateTransaction<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn credential_primary_set_password(
|
||||
pub fn credential_primary_set_password(
|
||||
&self,
|
||||
cust: &CredentialUpdateSessionToken,
|
||||
ct: Duration,
|
||||
pw: &str,
|
||||
) -> Result<CredentialUpdateSessionStatus, OperationError> {
|
||||
let session_handle = self.get_current_session(cust, ct)?;
|
||||
let mut session = session_handle.lock().await;
|
||||
let mut session = session_handle.try_lock().map_err(|_| {
|
||||
admin_error!("Session already locked, unable to proceed.");
|
||||
OperationError::InvalidState
|
||||
})?;
|
||||
trace!(?session);
|
||||
|
||||
// Check pw quality (future - acc policy applies).
|
||||
|
@ -749,13 +800,16 @@ impl<'a> IdmServerCredUpdateTransaction<'a> {
|
|||
Ok(session.deref().into())
|
||||
}
|
||||
|
||||
pub async fn credential_primary_init_totp(
|
||||
pub fn credential_primary_init_totp(
|
||||
&self,
|
||||
cust: &CredentialUpdateSessionToken,
|
||||
ct: Duration,
|
||||
) -> Result<CredentialUpdateSessionStatus, OperationError> {
|
||||
let session_handle = self.get_current_session(cust, ct)?;
|
||||
let mut session = session_handle.lock().await;
|
||||
let mut session = session_handle.try_lock().map_err(|_| {
|
||||
admin_error!("Session already locked, unable to proceed.");
|
||||
OperationError::InvalidState
|
||||
})?;
|
||||
trace!(?session);
|
||||
|
||||
// Is there something else in progress?
|
||||
|
@ -773,14 +827,17 @@ impl<'a> IdmServerCredUpdateTransaction<'a> {
|
|||
Ok(session.deref().into())
|
||||
}
|
||||
|
||||
pub async fn credential_primary_check_totp(
|
||||
pub fn credential_primary_check_totp(
|
||||
&self,
|
||||
cust: &CredentialUpdateSessionToken,
|
||||
ct: Duration,
|
||||
totp_chal: u32,
|
||||
) -> Result<CredentialUpdateSessionStatus, OperationError> {
|
||||
let session_handle = self.get_current_session(cust, ct)?;
|
||||
let mut session = session_handle.lock().await;
|
||||
let mut session = session_handle.try_lock().map_err(|_| {
|
||||
admin_error!("Session already locked, unable to proceed.");
|
||||
OperationError::InvalidState
|
||||
})?;
|
||||
trace!(?session);
|
||||
|
||||
// Are we in a totp reg state?
|
||||
|
@ -824,13 +881,16 @@ impl<'a> IdmServerCredUpdateTransaction<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn credential_primary_accept_sha1_totp(
|
||||
pub fn credential_primary_accept_sha1_totp(
|
||||
&self,
|
||||
cust: &CredentialUpdateSessionToken,
|
||||
ct: Duration,
|
||||
) -> Result<CredentialUpdateSessionStatus, OperationError> {
|
||||
let session_handle = self.get_current_session(cust, ct)?;
|
||||
let mut session = session_handle.lock().await;
|
||||
let mut session = session_handle.try_lock().map_err(|_| {
|
||||
admin_error!("Session already locked, unable to proceed.");
|
||||
OperationError::InvalidState
|
||||
})?;
|
||||
trace!(?session);
|
||||
|
||||
// Are we in a totp reg state?
|
||||
|
@ -856,13 +916,16 @@ impl<'a> IdmServerCredUpdateTransaction<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn credential_primary_remove_totp(
|
||||
pub fn credential_primary_remove_totp(
|
||||
&self,
|
||||
cust: &CredentialUpdateSessionToken,
|
||||
ct: Duration,
|
||||
) -> Result<CredentialUpdateSessionStatus, OperationError> {
|
||||
let session_handle = self.get_current_session(cust, ct)?;
|
||||
let mut session = session_handle.lock().await;
|
||||
let mut session = session_handle.try_lock().map_err(|_| {
|
||||
admin_error!("Session already locked, unable to proceed.");
|
||||
OperationError::InvalidState
|
||||
})?;
|
||||
trace!(?session);
|
||||
|
||||
if !matches!(session.mfaregstate, MfaRegState::None) {
|
||||
|
@ -886,13 +949,16 @@ impl<'a> IdmServerCredUpdateTransaction<'a> {
|
|||
Ok(session.deref().into())
|
||||
}
|
||||
|
||||
pub async fn credential_primary_init_backup_codes(
|
||||
pub fn credential_primary_init_backup_codes(
|
||||
&self,
|
||||
cust: &CredentialUpdateSessionToken,
|
||||
ct: Duration,
|
||||
) -> Result<CredentialUpdateSessionStatus, OperationError> {
|
||||
let session_handle = self.get_current_session(cust, ct)?;
|
||||
let mut session = session_handle.lock().await;
|
||||
let mut session = session_handle.try_lock().map_err(|_| {
|
||||
admin_error!("Session already locked, unable to proceed.");
|
||||
OperationError::InvalidState
|
||||
})?;
|
||||
trace!(?session);
|
||||
|
||||
// I think we override/map the status to inject the codes as a once-off state message.
|
||||
|
@ -923,13 +989,16 @@ impl<'a> IdmServerCredUpdateTransaction<'a> {
|
|||
})
|
||||
}
|
||||
|
||||
pub async fn credential_primary_remove_backup_codes(
|
||||
pub fn credential_primary_remove_backup_codes(
|
||||
&self,
|
||||
cust: &CredentialUpdateSessionToken,
|
||||
ct: Duration,
|
||||
) -> Result<CredentialUpdateSessionStatus, OperationError> {
|
||||
let session_handle = self.get_current_session(cust, ct)?;
|
||||
let mut session = session_handle.lock().await;
|
||||
let mut session = session_handle.try_lock().map_err(|_| {
|
||||
admin_error!("Session already locked, unable to proceed.");
|
||||
OperationError::InvalidState
|
||||
})?;
|
||||
trace!(?session);
|
||||
|
||||
let ncred = session
|
||||
|
@ -953,13 +1022,16 @@ impl<'a> IdmServerCredUpdateTransaction<'a> {
|
|||
Ok(session.deref().into())
|
||||
}
|
||||
|
||||
pub async fn credential_primary_delete(
|
||||
pub fn credential_primary_delete(
|
||||
&self,
|
||||
cust: &CredentialUpdateSessionToken,
|
||||
ct: Duration,
|
||||
) -> Result<CredentialUpdateSessionStatus, OperationError> {
|
||||
let session_handle = self.get_current_session(cust, ct)?;
|
||||
let mut session = session_handle.lock().await;
|
||||
let mut session = session_handle.try_lock().map_err(|_| {
|
||||
admin_error!("Session already locked, unable to proceed.");
|
||||
OperationError::InvalidState
|
||||
})?;
|
||||
trace!(?session);
|
||||
session.primary = None;
|
||||
Ok(session.deref().into())
|
||||
|
@ -1348,7 +1420,8 @@ mod tests {
|
|||
// Get the credential status - this should tell
|
||||
// us the details of the credentials, as well as
|
||||
// if they are ready and valid to commit?
|
||||
let c_status = task::block_on(cutxn.credential_status(&cust, ct))
|
||||
let c_status = cutxn
|
||||
.credential_update_status(&cust, ct)
|
||||
.expect("Failed to get the current session status.");
|
||||
|
||||
trace!(?c_status);
|
||||
|
@ -1357,9 +1430,9 @@ mod tests {
|
|||
|
||||
// Test initially creating a credential.
|
||||
// - pw first
|
||||
let c_status =
|
||||
task::block_on(cutxn.credential_primary_set_password(&cust, ct, test_pw))
|
||||
.expect("Failed to update the primary cred password");
|
||||
let c_status = cutxn
|
||||
.credential_primary_set_password(&cust, ct, test_pw)
|
||||
.expect("Failed to update the primary cred password");
|
||||
|
||||
assert!(c_status.can_commit);
|
||||
|
||||
|
@ -1373,12 +1446,14 @@ mod tests {
|
|||
let cust = renew_test_session(idms, ct);
|
||||
let cutxn = idms.cred_update_transaction();
|
||||
|
||||
let c_status = task::block_on(cutxn.credential_status(&cust, ct))
|
||||
let c_status = cutxn
|
||||
.credential_update_status(&cust, ct)
|
||||
.expect("Failed to get the current session status.");
|
||||
trace!(?c_status);
|
||||
assert!(c_status.primary.is_some());
|
||||
|
||||
let c_status = task::block_on(cutxn.credential_primary_delete(&cust, ct))
|
||||
let c_status = cutxn
|
||||
.credential_primary_delete(&cust, ct)
|
||||
.expect("Failed to delete the primary cred");
|
||||
trace!(?c_status);
|
||||
assert!(c_status.primary.is_none());
|
||||
|
@ -1408,15 +1483,16 @@ mod tests {
|
|||
let cutxn = idms.cred_update_transaction();
|
||||
|
||||
// Setup the PW
|
||||
let c_status =
|
||||
task::block_on(cutxn.credential_primary_set_password(&cust, ct, test_pw))
|
||||
.expect("Failed to update the primary cred password");
|
||||
let c_status = cutxn
|
||||
.credential_primary_set_password(&cust, ct, test_pw)
|
||||
.expect("Failed to update the primary cred password");
|
||||
|
||||
// Since it's pw only.
|
||||
assert!(c_status.can_commit);
|
||||
|
||||
//
|
||||
let c_status = task::block_on(cutxn.credential_primary_init_totp(&cust, ct))
|
||||
let c_status = cutxn
|
||||
.credential_primary_init_totp(&cust, ct)
|
||||
.expect("Failed to update the primary cred password");
|
||||
|
||||
// Check the status has the token.
|
||||
|
@ -1433,7 +1509,8 @@ mod tests {
|
|||
.expect("Failed to perform totp step");
|
||||
|
||||
// Intentionally get it wrong.
|
||||
let c_status = task::block_on(cutxn.credential_primary_check_totp(&cust, ct, chal + 1))
|
||||
let c_status = cutxn
|
||||
.credential_primary_check_totp(&cust, ct, chal + 1)
|
||||
.expect("Failed to update the primary cred password");
|
||||
|
||||
assert!(matches!(
|
||||
|
@ -1441,7 +1518,8 @@ mod tests {
|
|||
MfaRegStateStatus::TotpTryAgain
|
||||
));
|
||||
|
||||
let c_status = task::block_on(cutxn.credential_primary_check_totp(&cust, ct, chal))
|
||||
let c_status = cutxn
|
||||
.credential_primary_check_totp(&cust, ct, chal)
|
||||
.expect("Failed to update the primary cred password");
|
||||
|
||||
assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
|
||||
|
@ -1463,7 +1541,8 @@ mod tests {
|
|||
let cust = renew_test_session(idms, ct);
|
||||
let cutxn = idms.cred_update_transaction();
|
||||
|
||||
let c_status = task::block_on(cutxn.credential_primary_remove_totp(&cust, ct))
|
||||
let c_status = cutxn
|
||||
.credential_primary_remove_totp(&cust, ct)
|
||||
.expect("Failed to update the primary cred password");
|
||||
|
||||
assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
|
||||
|
@ -1493,15 +1572,16 @@ mod tests {
|
|||
let cutxn = idms.cred_update_transaction();
|
||||
|
||||
// Setup the PW
|
||||
let c_status =
|
||||
task::block_on(cutxn.credential_primary_set_password(&cust, ct, test_pw))
|
||||
.expect("Failed to update the primary cred password");
|
||||
let c_status = cutxn
|
||||
.credential_primary_set_password(&cust, ct, test_pw)
|
||||
.expect("Failed to update the primary cred password");
|
||||
|
||||
// Since it's pw only.
|
||||
assert!(c_status.can_commit);
|
||||
|
||||
//
|
||||
let c_status = task::block_on(cutxn.credential_primary_init_totp(&cust, ct))
|
||||
let c_status = cutxn
|
||||
.credential_primary_init_totp(&cust, ct)
|
||||
.expect("Failed to update the primary cred password");
|
||||
|
||||
// Check the status has the token.
|
||||
|
@ -1520,7 +1600,8 @@ mod tests {
|
|||
.expect("Failed to perform totp step");
|
||||
|
||||
// Should getn the warn that it's sha1
|
||||
let c_status = task::block_on(cutxn.credential_primary_check_totp(&cust, ct, chal))
|
||||
let c_status = cutxn
|
||||
.credential_primary_check_totp(&cust, ct, chal)
|
||||
.expect("Failed to update the primary cred password");
|
||||
|
||||
assert!(matches!(
|
||||
|
@ -1529,7 +1610,8 @@ mod tests {
|
|||
));
|
||||
|
||||
// Accept it
|
||||
let c_status = task::block_on(cutxn.credential_primary_accept_sha1_totp(&cust, ct))
|
||||
let c_status = cutxn
|
||||
.credential_primary_accept_sha1_totp(&cust, ct)
|
||||
.expect("Failed to update the primary cred password");
|
||||
|
||||
assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
|
||||
|
@ -1561,17 +1643,18 @@ mod tests {
|
|||
let cutxn = idms.cred_update_transaction();
|
||||
|
||||
// Setup the PW
|
||||
let _c_status =
|
||||
task::block_on(cutxn.credential_primary_set_password(&cust, ct, test_pw))
|
||||
.expect("Failed to update the primary cred password");
|
||||
let _c_status = cutxn
|
||||
.credential_primary_set_password(&cust, ct, test_pw)
|
||||
.expect("Failed to update the primary cred password");
|
||||
|
||||
// Backup codes are refused to be added because we don't have mfa yet.
|
||||
assert!(matches!(
|
||||
task::block_on(cutxn.credential_primary_init_backup_codes(&cust, ct)),
|
||||
cutxn.credential_primary_init_backup_codes(&cust, ct),
|
||||
Err(OperationError::InvalidState)
|
||||
));
|
||||
|
||||
let c_status = task::block_on(cutxn.credential_primary_init_totp(&cust, ct))
|
||||
let c_status = cutxn
|
||||
.credential_primary_init_totp(&cust, ct)
|
||||
.expect("Failed to update the primary cred password");
|
||||
|
||||
let totp_token: Totp = match c_status.mfaregstate {
|
||||
|
@ -1586,7 +1669,8 @@ mod tests {
|
|||
.do_totp_duration_from_epoch(&ct)
|
||||
.expect("Failed to perform totp step");
|
||||
|
||||
let c_status = task::block_on(cutxn.credential_primary_check_totp(&cust, ct, chal))
|
||||
let c_status = cutxn
|
||||
.credential_primary_check_totp(&cust, ct, chal)
|
||||
.expect("Failed to update the primary cred password");
|
||||
|
||||
assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
|
||||
|
@ -1597,9 +1681,9 @@ mod tests {
|
|||
|
||||
// Now good to go, we need to now add our backup codes.
|
||||
// Whats the right way to get these back?
|
||||
let c_status =
|
||||
task::block_on(cutxn.credential_primary_init_backup_codes(&cust, ct))
|
||||
.expect("Failed to update the primary cred password");
|
||||
let c_status = cutxn
|
||||
.credential_primary_init_backup_codes(&cust, ct)
|
||||
.expect("Failed to update the primary cred password");
|
||||
|
||||
let codes = match c_status.mfaregstate {
|
||||
MfaRegStateStatus::BackupCodes(codes) => Some(codes),
|
||||
|
@ -1635,7 +1719,8 @@ mod tests {
|
|||
let cutxn = idms.cred_update_transaction();
|
||||
|
||||
// Only 7 codes left.
|
||||
let c_status = task::block_on(cutxn.credential_status(&cust, ct))
|
||||
let c_status = cutxn
|
||||
.credential_update_status(&cust, ct)
|
||||
.expect("Failed to get the current session status.");
|
||||
|
||||
assert!(matches!(
|
||||
|
@ -1644,9 +1729,9 @@ mod tests {
|
|||
));
|
||||
|
||||
// If we remove codes, it leaves totp.
|
||||
let c_status =
|
||||
task::block_on(cutxn.credential_primary_remove_backup_codes(&cust, ct))
|
||||
.expect("Failed to update the primary cred password");
|
||||
let c_status = cutxn
|
||||
.credential_primary_remove_backup_codes(&cust, ct)
|
||||
.expect("Failed to update the primary cred password");
|
||||
|
||||
assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
|
||||
assert!(matches!(
|
||||
|
@ -1655,9 +1740,9 @@ mod tests {
|
|||
));
|
||||
|
||||
// Re-add the codes.
|
||||
let c_status =
|
||||
task::block_on(cutxn.credential_primary_init_backup_codes(&cust, ct))
|
||||
.expect("Failed to update the primary cred password");
|
||||
let c_status = cutxn
|
||||
.credential_primary_init_backup_codes(&cust, ct)
|
||||
.expect("Failed to update the primary cred password");
|
||||
|
||||
assert!(matches!(
|
||||
c_status.mfaregstate,
|
||||
|
@ -1669,7 +1754,8 @@ mod tests {
|
|||
));
|
||||
|
||||
// If we remove totp, it removes codes.
|
||||
let c_status = task::block_on(cutxn.credential_primary_remove_totp(&cust, ct))
|
||||
let c_status = cutxn
|
||||
.credential_primary_remove_totp(&cust, ct)
|
||||
.expect("Failed to update the primary cred password");
|
||||
|
||||
assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
|
||||
|
|
|
@ -2112,6 +2112,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
self.pw_badlist_cache.commit();
|
||||
self.mfareg_sessions.commit();
|
||||
self.cred_update_sessions.commit();
|
||||
trace!("cred_update_session.commit");
|
||||
self.qs_write.commit()
|
||||
})
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
//! which is used to process authentication, store identities and enforce access controls.
|
||||
|
||||
#![recursion_limit = "512"]
|
||||
// #![deny(warnings)]
|
||||
#![deny(warnings)]
|
||||
#![warn(unused_extern_crates)]
|
||||
#![deny(clippy::todo)]
|
||||
#![deny(clippy::unimplemented)]
|
||||
|
|
|
@ -84,6 +84,10 @@ pub struct QueryServerReadTransaction<'a> {
|
|||
Cell<ARCacheReadTxn<'a, (IdentityId, Filter<FilterValid>), Filter<FilterValidResolved>>>,
|
||||
}
|
||||
|
||||
unsafe impl<'a> Sync for QueryServerReadTransaction<'a> {}
|
||||
|
||||
unsafe impl<'a> Send for QueryServerReadTransaction<'a> {}
|
||||
|
||||
pub struct QueryServerWriteTransaction<'a> {
|
||||
committed: bool,
|
||||
d_info: CowCellWriteTxn<'a, DomainInfo>,
|
||||
|
|
|
@ -23,7 +23,7 @@ tide = "^0.16.0"
|
|||
tide-openssl = "^0.1.1"
|
||||
|
||||
futures-util = "^0.3.21"
|
||||
tokio = { version = "^1.17.0", features = ["net", "sync", "io-util"] }
|
||||
tokio = { version = "^1.17.0", features = ["net", "sync", "io-util", "macros"] }
|
||||
tokio-util = { version = "^0.7.1", features = ["codec"] }
|
||||
tokio-openssl = "^0.6.3"
|
||||
openssl = "^0.10.38"
|
||||
|
|
|
@ -93,9 +93,10 @@ impl RequestExtensions for tide::Request<AppState> {
|
|||
}
|
||||
|
||||
fn get_url_param(&self, param: &str) -> Result<String, tide::Error> {
|
||||
self.param(param)
|
||||
.map(str::to_string)
|
||||
.map_err(|_| tide::Error::from_str(tide::StatusCode::ImATeapot, "teapot"))
|
||||
self.param(param).map(str::to_string).map_err(|e| {
|
||||
error!(?e);
|
||||
tide::Error::from_str(tide::StatusCode::ImATeapot, "teapot")
|
||||
})
|
||||
}
|
||||
|
||||
fn new_eventid(&self) -> (Uuid, String) {
|
||||
|
@ -544,6 +545,12 @@ pub fn create_https_server(
|
|||
.get(account_get_backup_code);
|
||||
// .post(account_post_backup_code_regenerate) // use "/:id/_credential/primary" instead
|
||||
// .delete(account_delete_backup_code); // same as above
|
||||
account_route
|
||||
.at("/:id/_credential/_update_intent")
|
||||
.get(account_get_id_credential_update_intent);
|
||||
account_route
|
||||
.at("/:id/_credential/_update_intent/:ttl")
|
||||
.get(account_get_id_credential_update_intent);
|
||||
|
||||
account_route
|
||||
.at("/:id/_ssh_pubkeys")
|
||||
|
@ -570,6 +577,17 @@ pub fn create_https_server(
|
|||
.put(account_put_id_unix_credential)
|
||||
.delete(account_delete_id_unix_credential);
|
||||
|
||||
let mut cred_route = appserver.at("/v1/credential");
|
||||
cred_route
|
||||
.at("/_exchange_intent")
|
||||
.post(credential_update_exchange_intent);
|
||||
|
||||
cred_route.at("/_status").post(credential_update_status);
|
||||
|
||||
cred_route.at("/_update").post(credential_update_update);
|
||||
|
||||
cred_route.at("/_commit").post(credential_update_commit);
|
||||
|
||||
let mut group_route = appserver.at("/v1/group");
|
||||
group_route.at("/").get(group_get).post(group_post);
|
||||
group_route
|
||||
|
|
|
@ -7,13 +7,15 @@ use kanidm::status::StatusRequestEvent;
|
|||
use kanidm_proto::v1::Entry as ProtoEntry;
|
||||
use kanidm_proto::v1::{
|
||||
AccountPersonSet, AccountUnixExtend, AuthRequest, AuthResponse, AuthState as ProtoAuthState,
|
||||
CreateRequest, DeleteRequest, GroupUnixExtend, ModifyRequest, OperationError, SearchRequest,
|
||||
SetCredentialRequest, SingleStringRequest,
|
||||
CUIntentToken, CURequest, CUSessionToken, CreateRequest, DeleteRequest, GroupUnixExtend,
|
||||
ModifyRequest, OperationError, SearchRequest, SetCredentialRequest, SingleStringRequest,
|
||||
};
|
||||
|
||||
use super::{to_tide_response, AppState, RequestExtensions};
|
||||
use async_std::task;
|
||||
use compact_jwt::Jws;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
@ -411,6 +413,79 @@ pub async fn account_put_id_credential_primary(req: tide::Request<AppState>) ->
|
|||
json_rest_event_credential_put(req).await
|
||||
}
|
||||
|
||||
pub async fn account_get_id_credential_update_intent(req: tide::Request<AppState>) -> tide::Result {
|
||||
let uat = req.get_current_uat();
|
||||
let uuid_or_name = req.get_url_param("id")?;
|
||||
let ttl = req
|
||||
.param("ttl")
|
||||
.ok()
|
||||
.and_then(|s| {
|
||||
u64::from_str(s)
|
||||
.map_err(|_e| {
|
||||
error!("Invalid TTL integer, ignoring.");
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.map(|s| Duration::from_secs(s));
|
||||
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
|
||||
let res = req
|
||||
.state()
|
||||
.qe_w_ref
|
||||
.handle_idmcredentialupdateintent(uat, uuid_or_name, ttl, eventid)
|
||||
.await;
|
||||
to_tide_response(res, hvalue)
|
||||
}
|
||||
|
||||
pub async fn credential_update_exchange_intent(mut req: tide::Request<AppState>) -> tide::Result {
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
let intent_token: CUIntentToken = req.body_json().await?;
|
||||
|
||||
let res = req
|
||||
.state()
|
||||
.qe_w_ref
|
||||
.handle_idmcredentialexchangeintent(intent_token, eventid)
|
||||
.await;
|
||||
to_tide_response(res, hvalue)
|
||||
}
|
||||
|
||||
pub async fn credential_update_status(mut req: tide::Request<AppState>) -> tide::Result {
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
let session_token: CUSessionToken = req.body_json().await?;
|
||||
|
||||
let res = req
|
||||
.state()
|
||||
.qe_r_ref
|
||||
.handle_idmcredentialupdatestatus(session_token, eventid)
|
||||
.await;
|
||||
to_tide_response(res, hvalue)
|
||||
}
|
||||
|
||||
pub async fn credential_update_update(mut req: tide::Request<AppState>) -> tide::Result {
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
let (scr, session_token): (CURequest, CUSessionToken) = req.body_json().await?;
|
||||
|
||||
let res = req
|
||||
.state()
|
||||
.qe_r_ref
|
||||
.handle_idmcredentialupdate(session_token, scr, eventid)
|
||||
.await;
|
||||
to_tide_response(res, hvalue)
|
||||
}
|
||||
|
||||
pub async fn credential_update_commit(mut req: tide::Request<AppState>) -> tide::Result {
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
let session_token: CUSessionToken = req.body_json().await?;
|
||||
|
||||
let res = req
|
||||
.state()
|
||||
.qe_w_ref
|
||||
.handle_idmcredentialupdatecommit(session_token, eventid)
|
||||
.await;
|
||||
to_tide_response(res, hvalue)
|
||||
}
|
||||
|
||||
pub async fn account_get_id_credential_status(req: tide::Request<AppState>) -> tide::Result {
|
||||
let uat = req.get_current_uat();
|
||||
let uuid_or_name = req.get_url_param("id")?;
|
||||
|
|
|
@ -1,15 +1,12 @@
|
|||
use std::net::TcpStream;
|
||||
use std::sync::atomic::{AtomicU16, Ordering};
|
||||
use std::thread;
|
||||
|
||||
use kanidm::audit::LogLevel;
|
||||
use kanidm::config::{Configuration, IntegrationTestConfig, ServerRole};
|
||||
use kanidm::tracing_tree;
|
||||
use kanidm_client::{KanidmClient, KanidmClientBuilder};
|
||||
use kanidm_client::{asynchronous::KanidmAsyncClient, KanidmClientBuilder};
|
||||
use score::create_server_core;
|
||||
|
||||
use async_std::task;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::task;
|
||||
|
||||
pub const ADMIN_TEST_USER: &str = "admin";
|
||||
pub const ADMIN_TEST_PASSWORD: &str = "integration test admin password";
|
||||
|
@ -24,13 +21,9 @@ fn is_free_port(port: u16) -> bool {
|
|||
}
|
||||
|
||||
// Test external behaviours of the service.
|
||||
|
||||
pub fn run_test(test_fn: fn(KanidmClient) -> ()) {
|
||||
pub async fn setup_async_test() -> KanidmAsyncClient {
|
||||
let _ = tracing_tree::test_init();
|
||||
|
||||
let (ready_tx, mut ready_rx) = mpsc::channel(1);
|
||||
let (finish_tx, mut finish_rx) = mpsc::channel(1);
|
||||
|
||||
let mut counter = 0;
|
||||
let port = loop {
|
||||
let possible_port = PORT_ALLOC.fetch_add(1, Ordering::SeqCst);
|
||||
|
@ -60,46 +53,18 @@ pub fn run_test(test_fn: fn(KanidmClient) -> ()) {
|
|||
// config.log_level = Some(LogLevel::FullTrace as u32);
|
||||
config.threads = 1;
|
||||
|
||||
let t_handle = thread::spawn(move || {
|
||||
// Spawn a thread for the test runner, this should have a unique
|
||||
// port....
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("failed to start tokio");
|
||||
rt.block_on(async {
|
||||
create_server_core(config, false)
|
||||
.await
|
||||
.expect("failed to start server core");
|
||||
// We have to yield now to guarantee that the tide elements are setup.
|
||||
task::yield_now().await;
|
||||
ready_tx
|
||||
.send(())
|
||||
.await
|
||||
.expect("failed in indicate readiness");
|
||||
finish_rx.recv().await;
|
||||
});
|
||||
});
|
||||
create_server_core(config, false)
|
||||
.await
|
||||
.expect("failed to start server core");
|
||||
// We have to yield now to guarantee that the tide elements are setup.
|
||||
task::yield_now().await;
|
||||
|
||||
let _ = task::block_on(ready_rx.recv()).expect("failed to start ctx");
|
||||
// Do we need any fixtures?
|
||||
// Yes probably, but they'll need to be futures as well ...
|
||||
// later we could accept fixture as it's own future for re-use
|
||||
|
||||
// Setup the client, and the address we selected.
|
||||
let addr = format!("http://127.0.0.1:{}", port);
|
||||
let rsclient = KanidmClientBuilder::new()
|
||||
.address(addr)
|
||||
.no_proxy()
|
||||
.build()
|
||||
.build_async()
|
||||
.expect("Failed to build client");
|
||||
|
||||
test_fn(rsclient);
|
||||
|
||||
// We DO NOT need teardown, as sqlite is in mem
|
||||
// let the tables hit the floor
|
||||
|
||||
// At this point, when the channel drops, it drops the thread too.
|
||||
task::block_on(finish_tx.send(())).expect("unable to send to ctx");
|
||||
t_handle.join().expect("failed to join thread");
|
||||
rsclient
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
|||
#![deny(warnings)]
|
||||
mod common;
|
||||
use crate::common::{run_test, ADMIN_TEST_PASSWORD};
|
||||
use kanidm_client::KanidmClient;
|
||||
use crate::common::{setup_async_test, ADMIN_TEST_PASSWORD};
|
||||
|
||||
use compact_jwt::{JwkKeySet, JwsValidator, OidcToken, OidcUnverified};
|
||||
use kanidm_proto::oauth2::{
|
||||
|
@ -37,322 +37,318 @@ macro_rules! assert_no_cache {
|
|||
}};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_oauth2_openid_basic_flow() {
|
||||
run_test(|rsclient: KanidmClient| {
|
||||
let res = rsclient.auth_simple_password("admin", ADMIN_TEST_PASSWORD);
|
||||
assert!(res.is_ok());
|
||||
#[tokio::test]
|
||||
async fn test_oauth2_openid_basic_flow() {
|
||||
let rsclient = setup_async_test().await;
|
||||
let res = rsclient
|
||||
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
|
||||
.await;
|
||||
assert!(res.is_ok());
|
||||
|
||||
// Create an oauth2 application integration.
|
||||
rsclient
|
||||
.idm_oauth2_rs_basic_create(
|
||||
"test_integration",
|
||||
"Test Integration",
|
||||
"https://demo.example.com",
|
||||
)
|
||||
.expect("Failed to create oauth2 config");
|
||||
// Create an oauth2 application integration.
|
||||
rsclient
|
||||
.idm_oauth2_rs_basic_create(
|
||||
"test_integration",
|
||||
"Test Integration",
|
||||
"https://demo.example.com",
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create oauth2 config");
|
||||
|
||||
// Extend the admin account with extended details for openid claims.
|
||||
rsclient
|
||||
.idm_group_add_members("idm_hp_people_extend_priv", &["admin"])
|
||||
.unwrap();
|
||||
// Extend the admin account with extended details for openid claims.
|
||||
rsclient
|
||||
.idm_group_add_members("idm_hp_people_extend_priv", &["admin"])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
rsclient
|
||||
.idm_account_person_extend(
|
||||
"admin",
|
||||
Some(&["admin@idm.example.com".to_string()]),
|
||||
Some("Admin Istrator"),
|
||||
)
|
||||
.expect("Failed to extend account details");
|
||||
rsclient
|
||||
.idm_account_person_extend(
|
||||
"admin",
|
||||
Some(&["admin@idm.example.com".to_string()]),
|
||||
Some("Admin Istrator"),
|
||||
)
|
||||
.await
|
||||
.expect("Failed to extend account details");
|
||||
|
||||
rsclient
|
||||
.idm_oauth2_rs_update(
|
||||
"test_integration",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(vec!["read", "email", "openid"]),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.expect("Failed to update oauth2 config");
|
||||
rsclient
|
||||
.idm_oauth2_rs_update(
|
||||
"test_integration",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(vec!["read", "email", "openid"]),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to update oauth2 config");
|
||||
|
||||
let oauth2_config = rsclient
|
||||
.idm_oauth2_rs_get("test_integration")
|
||||
.ok()
|
||||
.flatten()
|
||||
.expect("Failed to retrieve test_integration config");
|
||||
let oauth2_config = rsclient
|
||||
.idm_oauth2_rs_get("test_integration")
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.expect("Failed to retrieve test_integration config");
|
||||
|
||||
let client_secret = oauth2_config
|
||||
.attrs
|
||||
.get("oauth2_rs_basic_secret")
|
||||
.map(|s| s[0].to_string())
|
||||
.expect("No basic secret present");
|
||||
let client_secret = oauth2_config
|
||||
.attrs
|
||||
.get("oauth2_rs_basic_secret")
|
||||
.map(|s| s[0].to_string())
|
||||
.expect("No basic secret present");
|
||||
|
||||
// Get our admin's auth token for our new client.
|
||||
// We have to re-auth to update the mail field.
|
||||
let res = rsclient.auth_simple_password("admin", ADMIN_TEST_PASSWORD);
|
||||
assert!(res.is_ok());
|
||||
let admin_uat = rsclient.get_token().expect("No user auth token found");
|
||||
// Get our admin's auth token for our new client.
|
||||
// We have to re-auth to update the mail field.
|
||||
let res = rsclient
|
||||
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
|
||||
.await;
|
||||
assert!(res.is_ok());
|
||||
let admin_uat = rsclient
|
||||
.get_token()
|
||||
.await
|
||||
.expect("No user auth token found");
|
||||
|
||||
let url = rsclient.get_url().to_string();
|
||||
let url = rsclient.get_url().to_string();
|
||||
|
||||
// We need a new reqwest client here.
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("failed to start tokio");
|
||||
rt.block_on(async {
|
||||
// from here, we can now begin what would be a "interaction" to the oauth server.
|
||||
// Create a new reqwest client - we'll be using this manually.
|
||||
let client = reqwest::Client::builder()
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.no_proxy()
|
||||
.build()
|
||||
.expect("Failed to create client.");
|
||||
// We need a new reqwest client here.
|
||||
|
||||
// Step 0 - get the openid discovery details and the public key.
|
||||
let response = client
|
||||
.get(format!(
|
||||
"{}/oauth2/openid/test_integration/.well-known/openid-configuration",
|
||||
url
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send request.");
|
||||
// from here, we can now begin what would be a "interaction" to the oauth server.
|
||||
// Create a new reqwest client - we'll be using this manually.
|
||||
let client = reqwest::Client::builder()
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.no_proxy()
|
||||
.build()
|
||||
.expect("Failed to create client.");
|
||||
|
||||
assert!(response.status() == reqwest::StatusCode::OK);
|
||||
assert_no_cache!(response);
|
||||
// Step 0 - get the openid discovery details and the public key.
|
||||
let response = client
|
||||
.get(format!(
|
||||
"{}/oauth2/openid/test_integration/.well-known/openid-configuration",
|
||||
url
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send request.");
|
||||
|
||||
let discovery: OidcDiscoveryResponse = response
|
||||
.json()
|
||||
.await
|
||||
.expect("Failed to access response body");
|
||||
assert!(response.status() == reqwest::StatusCode::OK);
|
||||
assert_no_cache!(response);
|
||||
|
||||
// Most values are checked in idm/oauth2.rs, but we want to sanity check
|
||||
// the urls here as an extended function smoke test.
|
||||
assert!(
|
||||
discovery.issuer
|
||||
== Url::parse("https://idm.example.com/oauth2/openid/test_integration")
|
||||
.unwrap()
|
||||
);
|
||||
let discovery: OidcDiscoveryResponse = response
|
||||
.json()
|
||||
.await
|
||||
.expect("Failed to access response body");
|
||||
|
||||
assert!(
|
||||
discovery.authorization_endpoint
|
||||
== Url::parse("https://idm.example.com/ui/oauth2").unwrap()
|
||||
);
|
||||
// Most values are checked in idm/oauth2.rs, but we want to sanity check
|
||||
// the urls here as an extended function smoke test.
|
||||
assert!(
|
||||
discovery.issuer
|
||||
== Url::parse("https://idm.example.com/oauth2/openid/test_integration").unwrap()
|
||||
);
|
||||
|
||||
assert!(
|
||||
discovery.token_endpoint
|
||||
== Url::parse("https://idm.example.com/oauth2/token").unwrap()
|
||||
);
|
||||
assert!(
|
||||
discovery.authorization_endpoint
|
||||
== Url::parse("https://idm.example.com/ui/oauth2").unwrap()
|
||||
);
|
||||
|
||||
assert!(
|
||||
discovery.userinfo_endpoint
|
||||
== Some(
|
||||
Url::parse(
|
||||
"https://idm.example.com/oauth2/openid/test_integration/userinfo"
|
||||
)
|
||||
.unwrap()
|
||||
)
|
||||
);
|
||||
assert!(
|
||||
discovery.token_endpoint == Url::parse("https://idm.example.com/oauth2/token").unwrap()
|
||||
);
|
||||
|
||||
assert!(
|
||||
discovery.jwks_uri
|
||||
== Url::parse(
|
||||
"https://idm.example.com/oauth2/openid/test_integration/public_key.jwk"
|
||||
)
|
||||
assert!(
|
||||
discovery.userinfo_endpoint
|
||||
== Some(
|
||||
Url::parse("https://idm.example.com/oauth2/openid/test_integration/userinfo")
|
||||
.unwrap()
|
||||
);
|
||||
)
|
||||
);
|
||||
|
||||
// Step 0 - get the jwks public key.
|
||||
let response = client
|
||||
.get(format!(
|
||||
"{}/oauth2/openid/test_integration/public_key.jwk",
|
||||
url
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send request.");
|
||||
assert!(
|
||||
discovery.jwks_uri
|
||||
== Url::parse("https://idm.example.com/oauth2/openid/test_integration/public_key.jwk")
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
assert!(response.status() == reqwest::StatusCode::OK);
|
||||
assert_no_cache!(response);
|
||||
// Step 0 - get the jwks public key.
|
||||
let response = client
|
||||
.get(format!(
|
||||
"{}/oauth2/openid/test_integration/public_key.jwk",
|
||||
url
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send request.");
|
||||
|
||||
let mut jwk_set: JwkKeySet = response
|
||||
.json()
|
||||
.await
|
||||
.expect("Failed to access response body");
|
||||
assert!(response.status() == reqwest::StatusCode::OK);
|
||||
assert_no_cache!(response);
|
||||
|
||||
let public_jwk = jwk_set.keys.pop().expect("No public key in set!");
|
||||
let mut jwk_set: JwkKeySet = response
|
||||
.json()
|
||||
.await
|
||||
.expect("Failed to access response body");
|
||||
|
||||
let jws_validator =
|
||||
JwsValidator::try_from(&public_jwk).expect("failed to build validator");
|
||||
let public_jwk = jwk_set.keys.pop().expect("No public key in set!");
|
||||
|
||||
// Step 1 - the Oauth2 Resource Server would send a redirect to the authorisation
|
||||
// server, where the url contains a series of authorisation request parameters.
|
||||
//
|
||||
// Since we are a client, we can just "pretend" we got the redirect, and issue the
|
||||
// get call directly. This should be a 200. (?)
|
||||
let jws_validator = JwsValidator::try_from(&public_jwk).expect("failed to build validator");
|
||||
|
||||
let (pkce_code_challenge, pkce_code_verifier) = PkceCodeChallenge::new_random_sha256();
|
||||
// Step 1 - the Oauth2 Resource Server would send a redirect to the authorisation
|
||||
// server, where the url contains a series of authorisation request parameters.
|
||||
//
|
||||
// Since we are a client, we can just "pretend" we got the redirect, and issue the
|
||||
// get call directly. This should be a 200. (?)
|
||||
|
||||
let response = client
|
||||
.get(format!("{}/oauth2/authorise", url))
|
||||
.bearer_auth(admin_uat.clone())
|
||||
.query(&[
|
||||
("response_type", "code"),
|
||||
("client_id", "test_integration"),
|
||||
("state", "YWJjZGVm"),
|
||||
("code_challenge", pkce_code_challenge.as_str()),
|
||||
("code_challenge_method", "S256"),
|
||||
("redirect_uri", "https://demo.example.com/oauth2/flow"),
|
||||
("scope", "email read openid"),
|
||||
])
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send request.");
|
||||
let (pkce_code_challenge, pkce_code_verifier) = PkceCodeChallenge::new_random_sha256();
|
||||
|
||||
assert!(response.status() == reqwest::StatusCode::OK);
|
||||
assert_no_cache!(response);
|
||||
let response = client
|
||||
.get(format!("{}/oauth2/authorise", url))
|
||||
.bearer_auth(admin_uat.clone())
|
||||
.query(&[
|
||||
("response_type", "code"),
|
||||
("client_id", "test_integration"),
|
||||
("state", "YWJjZGVm"),
|
||||
("code_challenge", pkce_code_challenge.as_str()),
|
||||
("code_challenge_method", "S256"),
|
||||
("redirect_uri", "https://demo.example.com/oauth2/flow"),
|
||||
("scope", "email read openid"),
|
||||
])
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send request.");
|
||||
|
||||
let consent_req: ConsentRequest = response
|
||||
.json()
|
||||
.await
|
||||
.expect("Failed to access response body");
|
||||
assert!(response.status() == reqwest::StatusCode::OK);
|
||||
assert_no_cache!(response);
|
||||
|
||||
// Step 2 - we now send the consent get to the server which yields a redirect with a
|
||||
// state and code.
|
||||
let consent_req: ConsentRequest = response
|
||||
.json()
|
||||
.await
|
||||
.expect("Failed to access response body");
|
||||
|
||||
let response = client
|
||||
.get(format!("{}/oauth2/authorise/permit", url))
|
||||
.bearer_auth(admin_uat)
|
||||
.query(&[("token", consent_req.consent_token.as_str())])
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send request.");
|
||||
// Step 2 - we now send the consent get to the server which yields a redirect with a
|
||||
// state and code.
|
||||
|
||||
// This should yield a 302 redirect with some query params.
|
||||
assert!(response.status() == reqwest::StatusCode::FOUND);
|
||||
assert_no_cache!(response);
|
||||
let response = client
|
||||
.get(format!("{}/oauth2/authorise/permit", url))
|
||||
.bearer_auth(admin_uat)
|
||||
.query(&[("token", consent_req.consent_token.as_str())])
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send request.");
|
||||
|
||||
// And we should have a URL in the location header.
|
||||
let redir_str = response
|
||||
.headers()
|
||||
.get("Location")
|
||||
.map(|hv| hv.to_str().ok().map(str::to_string))
|
||||
.flatten()
|
||||
.expect("Invalid redirect url");
|
||||
// This should yield a 302 redirect with some query params.
|
||||
assert!(response.status() == reqwest::StatusCode::FOUND);
|
||||
assert_no_cache!(response);
|
||||
|
||||
// Now check it's content
|
||||
let redir_url = Url::parse(&redir_str).expect("Url parse failure");
|
||||
// And we should have a URL in the location header.
|
||||
let redir_str = response
|
||||
.headers()
|
||||
.get("Location")
|
||||
.map(|hv| hv.to_str().ok().map(str::to_string))
|
||||
.flatten()
|
||||
.expect("Invalid redirect url");
|
||||
|
||||
// We should have state and code.
|
||||
let pairs: HashMap<_, _> = redir_url.query_pairs().collect();
|
||||
// Now check it's content
|
||||
let redir_url = Url::parse(&redir_str).expect("Url parse failure");
|
||||
|
||||
let code = pairs.get("code").expect("code not found!");
|
||||
// We should have state and code.
|
||||
let pairs: HashMap<_, _> = redir_url.query_pairs().collect();
|
||||
|
||||
let state = pairs.get("state").expect("state not found!");
|
||||
let code = pairs.get("code").expect("code not found!");
|
||||
|
||||
assert!(state == "YWJjZGVm");
|
||||
let state = pairs.get("state").expect("state not found!");
|
||||
|
||||
// Step 3 - the "resource server" then uses this state and code to directly contact
|
||||
// the authorisation server to request a token.
|
||||
assert!(state == "YWJjZGVm");
|
||||
|
||||
let form_req = AccessTokenRequest {
|
||||
grant_type: "authorization_code".to_string(),
|
||||
code: code.to_string(),
|
||||
redirect_uri: Url::parse("https://demo.example.com/oauth2/flow")
|
||||
.expect("Invalid URL"),
|
||||
client_id: None,
|
||||
client_secret: None,
|
||||
code_verifier: Some(pkce_code_verifier.secret().clone()),
|
||||
};
|
||||
// Step 3 - the "resource server" then uses this state and code to directly contact
|
||||
// the authorisation server to request a token.
|
||||
|
||||
let response = client
|
||||
.post(format!("{}/oauth2/token", url))
|
||||
.basic_auth("test_integration", Some(client_secret.clone()))
|
||||
.form(&form_req)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send code exchange request.");
|
||||
let form_req = AccessTokenRequest {
|
||||
grant_type: "authorization_code".to_string(),
|
||||
code: code.to_string(),
|
||||
redirect_uri: Url::parse("https://demo.example.com/oauth2/flow").expect("Invalid URL"),
|
||||
client_id: None,
|
||||
client_secret: None,
|
||||
code_verifier: Some(pkce_code_verifier.secret().clone()),
|
||||
};
|
||||
|
||||
assert!(response.status() == reqwest::StatusCode::OK);
|
||||
assert_no_cache!(response);
|
||||
let response = client
|
||||
.post(format!("{}/oauth2/token", url))
|
||||
.basic_auth("test_integration", Some(client_secret.clone()))
|
||||
.form(&form_req)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send code exchange request.");
|
||||
|
||||
// The body is a json AccessTokenResponse
|
||||
assert!(response.status() == reqwest::StatusCode::OK);
|
||||
assert_no_cache!(response);
|
||||
|
||||
let atr = response
|
||||
.json::<AccessTokenResponse>()
|
||||
.await
|
||||
.expect("Unable to decode AccessTokenResponse");
|
||||
// The body is a json AccessTokenResponse
|
||||
|
||||
// Step 4 - inspect the granted token.
|
||||
let intr_request = AccessTokenIntrospectRequest {
|
||||
token: atr.access_token.clone(),
|
||||
token_type_hint: None,
|
||||
};
|
||||
let atr = response
|
||||
.json::<AccessTokenResponse>()
|
||||
.await
|
||||
.expect("Unable to decode AccessTokenResponse");
|
||||
|
||||
let response = client
|
||||
.post(format!("{}/oauth2/token/introspect", url))
|
||||
.basic_auth("test_integration", Some(client_secret))
|
||||
.form(&intr_request)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send token introspect request.");
|
||||
// Step 4 - inspect the granted token.
|
||||
let intr_request = AccessTokenIntrospectRequest {
|
||||
token: atr.access_token.clone(),
|
||||
token_type_hint: None,
|
||||
};
|
||||
|
||||
assert!(response.status() == reqwest::StatusCode::OK);
|
||||
assert_no_cache!(response);
|
||||
let response = client
|
||||
.post(format!("{}/oauth2/token/introspect", url))
|
||||
.basic_auth("test_integration", Some(client_secret))
|
||||
.form(&intr_request)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send token introspect request.");
|
||||
|
||||
let tir = response
|
||||
.json::<AccessTokenIntrospectResponse>()
|
||||
.await
|
||||
.expect("Unable to decode AccessTokenIntrospectResponse");
|
||||
assert!(response.status() == reqwest::StatusCode::OK);
|
||||
assert_no_cache!(response);
|
||||
|
||||
assert!(tir.active);
|
||||
assert!(tir.scope.is_some());
|
||||
assert!(tir.client_id.as_deref() == Some("test_integration"));
|
||||
assert!(tir.username.as_deref() == Some("admin@idm.example.com"));
|
||||
assert!(tir.token_type.as_deref() == Some("access_token"));
|
||||
assert!(tir.exp.is_some());
|
||||
assert!(tir.iat.is_some());
|
||||
assert!(tir.nbf.is_some());
|
||||
assert!(tir.sub.is_some());
|
||||
assert!(tir.aud.as_deref() == Some("test_integration"));
|
||||
assert!(tir.iss.is_none());
|
||||
assert!(tir.jti.is_none());
|
||||
let tir = response
|
||||
.json::<AccessTokenIntrospectResponse>()
|
||||
.await
|
||||
.expect("Unable to decode AccessTokenIntrospectResponse");
|
||||
|
||||
// Step 5 - check that the id_token (openid) matches the userinfo endpoint.
|
||||
let oidc_unverified = OidcUnverified::from_str(atr.id_token.as_ref().unwrap())
|
||||
.expect("Failed to parse id_token");
|
||||
assert!(tir.active);
|
||||
assert!(tir.scope.is_some());
|
||||
assert!(tir.client_id.as_deref() == Some("test_integration"));
|
||||
assert!(tir.username.as_deref() == Some("admin@idm.example.com"));
|
||||
assert!(tir.token_type.as_deref() == Some("access_token"));
|
||||
assert!(tir.exp.is_some());
|
||||
assert!(tir.iat.is_some());
|
||||
assert!(tir.nbf.is_some());
|
||||
assert!(tir.sub.is_some());
|
||||
assert!(tir.aud.as_deref() == Some("test_integration"));
|
||||
assert!(tir.iss.is_none());
|
||||
assert!(tir.jti.is_none());
|
||||
|
||||
let oidc = oidc_unverified
|
||||
.validate(&jws_validator, 0)
|
||||
.expect("Failed to verify oidc");
|
||||
// Step 5 - check that the id_token (openid) matches the userinfo endpoint.
|
||||
let oidc_unverified =
|
||||
OidcUnverified::from_str(atr.id_token.as_ref().unwrap()).expect("Failed to parse id_token");
|
||||
|
||||
// This is mostly checked inside of idm/oauth2.rs. This is more to check the oidc
|
||||
// token and the userinfo endpoints.
|
||||
assert!(
|
||||
oidc.iss
|
||||
== Url::parse("https://idm.example.com/oauth2/openid/test_integration")
|
||||
.unwrap()
|
||||
);
|
||||
assert!(oidc.s_claims.email.as_deref() == Some("admin@idm.example.com"));
|
||||
assert!(oidc.s_claims.email_verified == Some(true));
|
||||
let oidc = oidc_unverified
|
||||
.validate(&jws_validator, 0)
|
||||
.expect("Failed to verify oidc");
|
||||
|
||||
let response = client
|
||||
.get(format!("{}/oauth2/openid/test_integration/userinfo", url))
|
||||
.bearer_auth(atr.access_token.clone())
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send userinfo request.");
|
||||
// This is mostly checked inside of idm/oauth2.rs. This is more to check the oidc
|
||||
// token and the userinfo endpoints.
|
||||
assert!(
|
||||
oidc.iss == Url::parse("https://idm.example.com/oauth2/openid/test_integration").unwrap()
|
||||
);
|
||||
assert!(oidc.s_claims.email.as_deref() == Some("admin@idm.example.com"));
|
||||
assert!(oidc.s_claims.email_verified == Some(true));
|
||||
|
||||
let userinfo = response
|
||||
.json::<OidcToken>()
|
||||
.await
|
||||
.expect("Unable to decode OidcToken from userinfo");
|
||||
let response = client
|
||||
.get(format!("{}/oauth2/openid/test_integration/userinfo", url))
|
||||
.bearer_auth(atr.access_token.clone())
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send userinfo request.");
|
||||
|
||||
assert!(userinfo == oidc);
|
||||
})
|
||||
})
|
||||
let userinfo = response
|
||||
.json::<OidcToken>()
|
||||
.await
|
||||
.expect("Unable to decode OidcToken from userinfo");
|
||||
|
||||
assert!(userinfo == oidc);
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -41,7 +41,6 @@ openssl = "^0.10.38"
|
|||
ldap3_proto = "^0.2.3"
|
||||
|
||||
crossbeam = "0.8.1"
|
||||
async-std = { version = "^1.11.0", features = ["tokio1"] }
|
||||
|
||||
mathru = "^0.12.0"
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ struct CsvRow {
|
|||
count: usize,
|
||||
}
|
||||
|
||||
fn basic_arbiter(
|
||||
async fn basic_arbiter(
|
||||
mut broadcast_rx: tokio::sync::broadcast::Receiver<TestPhase>,
|
||||
raw_results_rx: &crossbeam::channel::Receiver<(Duration, Duration, usize)>,
|
||||
warmup_seconds: u32,
|
||||
|
@ -37,7 +37,7 @@ fn basic_arbiter(
|
|||
info!("Starting test arbiter ...");
|
||||
|
||||
// Wait on the message that the workers have started the warm up.
|
||||
let bcast_msg = async_std::task::block_on(broadcast_rx.recv()).unwrap();
|
||||
let bcast_msg = broadcast_rx.recv().await.unwrap();
|
||||
|
||||
if !matches!(bcast_msg, TestPhase::WarmUp) {
|
||||
error!("Invalid broadcast state to arbiter");
|
||||
|
@ -207,7 +207,9 @@ pub(crate) async fn basic(
|
|||
// This should use spawn blocking.
|
||||
let warmup_seconds = profile.search_basic_config.warmup_seconds;
|
||||
let arbiter_join_handle =
|
||||
task::spawn_blocking(move || basic_arbiter(broadcast_rx, &raw_results_rx, warmup_seconds));
|
||||
task::spawn(
|
||||
async move { basic_arbiter(broadcast_rx, &raw_results_rx, warmup_seconds).await },
|
||||
);
|
||||
|
||||
// Get out our conn details
|
||||
let mut rng = rand::thread_rng();
|
||||
|
|
Loading…
Reference in a new issue