383 170 164 authentication updates 3 (#723)

This commit is contained in:
Firstyear 2022-04-29 13:03:21 +10:00 committed by GitHub
parent 5eb9fa604e
commit 8dc0199380
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 3524 additions and 3283 deletions

5
Cargo.lock generated
View file

@ -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",

View file

@ -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,

View file

@ -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))
}
}

View file

@ -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.

View file

@ -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 }

View file

@ -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"),
}

View file

@ -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
}

View file

@ -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),
}

View file

@ -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 {}",

View file

@ -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,
}
}
}

View file

@ -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
}

View file

@ -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),
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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();

View file

@ -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),
}

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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",

View file

@ -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",

View file

@ -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>,

View file

@ -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));

View file

@ -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()
})
}

View file

@ -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)]

View file

@ -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>,

View file

@ -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"

View file

@ -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

View file

@ -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")?;

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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();