use crate::{ClientError, KanidmClient};
use kanidm_proto::attribute::Attribute;
use kanidm_proto::constants::{
ATTR_DISPLAYNAME, ATTR_KEY_ACTION_REVOKE, ATTR_KEY_ACTION_ROTATE, ATTR_NAME,
ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE, ATTR_OAUTH2_ALLOW_LOCALHOST_REDIRECT,
ATTR_OAUTH2_CONSENT_PROMPT_ENABLE, ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE,
ATTR_OAUTH2_PREFER_SHORT_USERNAME, ATTR_OAUTH2_RS_BASIC_SECRET, ATTR_OAUTH2_RS_ORIGIN,
ATTR_OAUTH2_RS_ORIGIN_LANDING, ATTR_OAUTH2_STRICT_REDIRECT_URI,
};
use kanidm_proto::internal::{ImageValue, Oauth2ClaimMapJoin};
use kanidm_proto::v1::Entry;
use reqwest::multipart;
use std::collections::BTreeMap;
use time::format_description::well_known::Rfc3339;
use time::OffsetDateTime;
use url::Url;
impl KanidmClient {
#[instrument(level = "debug")]
pub async fn idm_oauth2_rs_list(&self) -> Result<Vec<Entry>, ClientError> {
self.perform_get_request("/v1/oauth2").await
}
pub async fn idm_oauth2_rs_basic_create(
&self,
name: &str,
displayname: &str,
origin: &str,
) -> Result<(), ClientError> {
let mut new_oauth2_rs = Entry::default();
new_oauth2_rs
.attrs
.insert(ATTR_NAME.to_string(), vec![name.to_string()]);
new_oauth2_rs
.attrs
.insert(ATTR_DISPLAYNAME.to_string(), vec![displayname.to_string()]);
new_oauth2_rs.attrs.insert(
ATTR_OAUTH2_RS_ORIGIN_LANDING.to_string(),
vec![origin.to_string()],
);
new_oauth2_rs.attrs.insert(
ATTR_OAUTH2_STRICT_REDIRECT_URI.to_string(),
vec!["true".to_string()],
);
self.perform_post_request("/v1/oauth2/_basic", new_oauth2_rs)
.await
}
pub async fn idm_oauth2_rs_public_create(
&self,
name: &str,
displayname: &str,
origin: &str,
) -> Result<(), ClientError> {
let mut new_oauth2_rs = Entry::default();
new_oauth2_rs
.attrs
.insert(ATTR_NAME.to_string(), vec![name.to_string()]);
new_oauth2_rs
.attrs
.insert(ATTR_DISPLAYNAME.to_string(), vec![displayname.to_string()]);
new_oauth2_rs.attrs.insert(
ATTR_OAUTH2_RS_ORIGIN_LANDING.to_string(),
vec![origin.to_string()],
);
new_oauth2_rs.attrs.insert(
ATTR_OAUTH2_STRICT_REDIRECT_URI.to_string(),
vec!["true".to_string()],
);
self.perform_post_request("/v1/oauth2/_public", new_oauth2_rs)
.await
}
pub async fn idm_oauth2_rs_get(&self, client_name: &str) -> Result<Option<Entry>, ClientError> {
self.perform_get_request(format!("/v1/oauth2/{client_name}").as_str())
.await
}
pub async fn idm_oauth2_rs_get_basic_secret(
&self,
client_name: &str,
) -> Result<Option<String>, ClientError> {
self.perform_get_request(format!("/v1/oauth2/{client_name}/_basic_secret").as_str())
.await
}
pub async fn idm_oauth2_rs_revoke_key(
&self,
client_name: &str,
key_id: &str,
) -> Result<(), ClientError> {
self.perform_post_request(
&format!("/v1/oauth2/{client_name}/_attr/{ATTR_KEY_ACTION_REVOKE}"),
vec![key_id.to_string()],
)
.await
}
pub async fn idm_oauth2_rs_rotate_keys(
&self,
client_name: &str,
rotate_at_time: OffsetDateTime,
) -> Result<(), ClientError> {
let rfc_3339_str = rotate_at_time.format(&Rfc3339).map_err(|_| {
ClientError::InvalidRequest("Unable to format rfc 3339 datetime".into())
})?;
self.perform_post_request(
&format!("/v1/oauth2/{client_name}/_attr/{ATTR_KEY_ACTION_ROTATE}"),
vec![rfc_3339_str],
)
.await
}
pub async fn idm_oauth2_rs_update(
&self,
id: &str,
name: Option<&str>,
displayname: Option<&str>,
landing: Option<&str>,
reset_secret: bool,
) -> Result<(), ClientError> {
let mut update_oauth2_rs = Entry {
attrs: BTreeMap::new(),
};
if let Some(newname) = name {
update_oauth2_rs
.attrs
.insert(ATTR_NAME.to_string(), vec![newname.to_string()]);
}
if let Some(newdisplayname) = displayname {
update_oauth2_rs.attrs.insert(
ATTR_DISPLAYNAME.to_string(),
vec![newdisplayname.to_string()],
);
}
if let Some(newlanding) = landing {
update_oauth2_rs.attrs.insert(
ATTR_OAUTH2_RS_ORIGIN_LANDING.to_string(),
vec![newlanding.to_string()],
);
}
if reset_secret {
update_oauth2_rs
.attrs
.insert(ATTR_OAUTH2_RS_BASIC_SECRET.to_string(), Vec::new());
}
self.perform_patch_request(format!("/v1/oauth2/{id}").as_str(), update_oauth2_rs)
.await
}
pub async fn idm_oauth2_rs_update_scope_map(
&self,
id: &str,
group: &str,
scopes: Vec<&str>,
) -> Result<(), ClientError> {
let scopes: Vec<String> = scopes.into_iter().map(str::to_string).collect();
self.perform_post_request(
format!("/v1/oauth2/{id}/_scopemap/{group}").as_str(),
scopes,
)
.await
}
pub async fn idm_oauth2_rs_delete_scope_map(
&self,
id: &str,
group: &str,
) -> Result<(), ClientError> {
self.perform_delete_request(format!("/v1/oauth2/{id}/_scopemap/{group}").as_str())
.await
}
pub async fn idm_oauth2_rs_update_sup_scope_map(
&self,
id: &str,
group: &str,
scopes: Vec<&str>,
) -> Result<(), ClientError> {
let scopes: Vec<String> = scopes.into_iter().map(str::to_string).collect();
self.perform_post_request(
format!("/v1/oauth2/{id}/_sup_scopemap/{group}").as_str(),
scopes,
)
.await
}
pub async fn idm_oauth2_rs_delete_sup_scope_map(
&self,
id: &str,
group: &str,
) -> Result<(), ClientError> {
self.perform_delete_request(format!("/v1/oauth2/{id}/_sup_scopemap/{group}").as_str())
.await
}
pub async fn idm_oauth2_rs_delete(&self, id: &str) -> Result<(), ClientError> {
self.perform_delete_request(["/v1/oauth2/", id].concat().as_str())
.await
}
pub async fn idm_oauth2_rs_delete_image(&self, id: &str) -> Result<(), ClientError> {
self.perform_delete_request(format!("/v1/oauth2/{id}/_image").as_str())
.await
}
pub async fn idm_oauth2_rs_update_image(
&self,
id: &str,
image: ImageValue,
) -> Result<(), ClientError> {
let file_content_type = image.filetype.as_content_type_str();
let file_data = match multipart::Part::bytes(image.contents.clone())
.file_name(image.filename)
.mime_str(file_content_type)
{
Ok(part) => part,
Err(err) => {
error!(
"Failed to generate multipart body from image data: {:}",
err
);
return Err(ClientError::SystemError);
}
};
let form = multipart::Form::new().part("image", file_data);
let response = self
.client
.post(self.make_url(&format!("/v1/oauth2/{id}/_image")))
.multipart(form);
let response = {
let tguard = self.bearer_token.read().await;
if let Some(token) = &(*tguard) {
response.bearer_auth(token)
} else {
response
}
};
let response = response
.send()
.await
.map_err(|err| self.handle_response_error(err))?;
self.expect_version(&response).await;
let opid = self.get_kopid_from_response(&response);
self.ok_or_clienterror(&opid, response)
.await?
.json()
.await
.map_err(|e| ClientError::JsonDecode(e, opid))
}
pub async fn idm_oauth2_rs_enable_pkce(&self, id: &str) -> Result<(), ClientError> {
let mut update_oauth2_rs = Entry {
attrs: BTreeMap::new(),
};
update_oauth2_rs.attrs.insert(
ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE.to_string(),
Vec::new(),
);
self.perform_patch_request(format!("/v1/oauth2/{id}").as_str(), update_oauth2_rs)
.await
}
pub async fn idm_oauth2_rs_disable_pkce(&self, id: &str) -> Result<(), ClientError> {
let mut update_oauth2_rs = Entry {
attrs: BTreeMap::new(),
};
update_oauth2_rs.attrs.insert(
ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE.to_string(),
vec!["true".to_string()],
);
self.perform_patch_request(format!("/v1/oauth2/{id}").as_str(), update_oauth2_rs)
.await
}
pub async fn idm_oauth2_rs_enable_legacy_crypto(&self, id: &str) -> Result<(), ClientError> {
let mut update_oauth2_rs = Entry {
attrs: BTreeMap::new(),
};
update_oauth2_rs.attrs.insert(
ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE.to_string(),
vec!["true".to_string()],
);
self.perform_patch_request(format!("/v1/oauth2/{id}").as_str(), update_oauth2_rs)
.await
}
pub async fn idm_oauth2_rs_disable_legacy_crypto(&self, id: &str) -> Result<(), ClientError> {
let mut update_oauth2_rs = Entry {
attrs: BTreeMap::new(),
};
update_oauth2_rs.attrs.insert(
ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE.to_string(),
vec!["false".to_string()],
);
self.perform_patch_request(format!("/v1/oauth2/{id}").as_str(), update_oauth2_rs)
.await
}
pub async fn idm_oauth2_rs_prefer_short_username(&self, id: &str) -> Result<(), ClientError> {
let mut update_oauth2_rs = Entry {
attrs: BTreeMap::new(),
};
update_oauth2_rs.attrs.insert(
ATTR_OAUTH2_PREFER_SHORT_USERNAME.to_string(),
vec!["true".to_string()],
);
self.perform_patch_request(format!("/v1/oauth2/{id}").as_str(), update_oauth2_rs)
.await
}
pub async fn idm_oauth2_rs_prefer_spn_username(&self, id: &str) -> Result<(), ClientError> {
let mut update_oauth2_rs = Entry {
attrs: BTreeMap::new(),
};
update_oauth2_rs.attrs.insert(
ATTR_OAUTH2_PREFER_SHORT_USERNAME.to_string(),
vec!["false".to_string()],
);
self.perform_patch_request(format!("/v1/oauth2/{id}").as_str(), update_oauth2_rs)
.await
}
pub async fn idm_oauth2_rs_enable_public_localhost_redirect(
&self,
id: &str,
) -> Result<(), ClientError> {
let mut update_oauth2_rs = Entry {
attrs: BTreeMap::new(),
};
update_oauth2_rs.attrs.insert(
ATTR_OAUTH2_ALLOW_LOCALHOST_REDIRECT.to_string(),
vec!["true".to_string()],
);
self.perform_patch_request(format!("/v1/oauth2/{id}").as_str(), update_oauth2_rs)
.await
}
pub async fn idm_oauth2_rs_disable_public_localhost_redirect(
&self,
id: &str,
) -> Result<(), ClientError> {
let mut update_oauth2_rs = Entry {
attrs: BTreeMap::new(),
};
update_oauth2_rs.attrs.insert(
ATTR_OAUTH2_ALLOW_LOCALHOST_REDIRECT.to_string(),
vec!["false".to_string()],
);
self.perform_patch_request(format!("/v1/oauth2/{id}").as_str(), update_oauth2_rs)
.await
}
pub async fn idm_oauth2_rs_enable_strict_redirect_uri(
&self,
id: &str,
) -> Result<(), ClientError> {
let mut update_oauth2_rs = Entry {
attrs: BTreeMap::new(),
};
update_oauth2_rs.attrs.insert(
ATTR_OAUTH2_STRICT_REDIRECT_URI.to_string(),
vec!["true".to_string()],
);
self.perform_patch_request(format!("/v1/oauth2/{id}").as_str(), update_oauth2_rs)
.await
}
pub async fn idm_oauth2_rs_disable_strict_redirect_uri(
&self,
id: &str,
) -> Result<(), ClientError> {
let mut update_oauth2_rs = Entry {
attrs: BTreeMap::new(),
};
update_oauth2_rs.attrs.insert(
ATTR_OAUTH2_STRICT_REDIRECT_URI.to_string(),
vec!["false".to_string()],
);
self.perform_patch_request(format!("/v1/oauth2/{id}").as_str(), update_oauth2_rs)
.await
}
pub async fn idm_oauth2_rs_update_claim_map(
&self,
id: &str,
claim_name: &str,
group_id: &str,
values: &[String],
) -> Result<(), ClientError> {
let values: Vec<String> = values.to_vec();
self.perform_post_request(
format!("/v1/oauth2/{id}/_claimmap/{claim_name}/{group_id}").as_str(),
values,
)
.await
}
pub async fn idm_oauth2_rs_update_claim_map_join(
&self,
id: &str,
claim_name: &str,
join: Oauth2ClaimMapJoin,
) -> Result<(), ClientError> {
self.perform_post_request(
format!("/v1/oauth2/{id}/_claimmap/{claim_name}").as_str(),
join,
)
.await
}
pub async fn idm_oauth2_rs_delete_claim_map(
&self,
id: &str,
claim_name: &str,
group_id: &str,
) -> Result<(), ClientError> {
self.perform_delete_request(
format!("/v1/oauth2/{id}/_claimmap/{claim_name}/{group_id}").as_str(),
)
.await
}
pub async fn idm_oauth2_client_add_origin(
&self,
id: &str,
origin: &Url,
) -> Result<(), ClientError> {
let url_to_add = &[origin.as_str()];
self.perform_post_request(
format!("/v1/oauth2/{id}/_attr/{ATTR_OAUTH2_RS_ORIGIN}").as_str(),
url_to_add,
)
.await
}
pub async fn idm_oauth2_client_remove_origin(
&self,
id: &str,
origin: &Url,
) -> Result<(), ClientError> {
let url_to_remove = &[origin.as_str()];
self.perform_delete_request_with_body(
format!("/v1/oauth2/{id}/_attr/{ATTR_OAUTH2_RS_ORIGIN}").as_str(),
url_to_remove,
)
.await
}
pub async fn idm_oauth2_client_device_flow_update(
&self,
id: &str,
value: bool,
) -> Result<(), ClientError> {
match value {
true => {
let mut update_oauth2_rs = Entry {
attrs: BTreeMap::new(),
};
update_oauth2_rs.attrs.insert(
Attribute::OAuth2DeviceFlowEnable.into(),
vec![value.to_string()],
);
self.perform_patch_request(format!("/v1/oauth2/{id}").as_str(), update_oauth2_rs)
.await
}
false => {
self.perform_delete_request(&format!(
"/v1/oauth2/{}/_attr/{}",
id,
Attribute::OAuth2DeviceFlowEnable.as_str()
))
.await
}
}
}
pub async fn idm_oauth2_rs_set_refresh_token_expiry(
&self,
id: &str,
expiry: Option<u32>,
) -> Result<(), ClientError> {
match expiry {
Some(exp) => {
let mut update_oauth2_rs = Entry {
attrs: BTreeMap::new(),
};
update_oauth2_rs.attrs.insert(
Attribute::OAuth2RefreshTokenExpiry.into(),
vec![exp.to_string()],
);
self.perform_patch_request(format!("/v1/oauth2/{id}").as_str(), update_oauth2_rs)
.await
}
None => {
self.perform_delete_request(&format!(
"/v1/oauth2/{}/_attr/{}",
id,
Attribute::OAuth2RefreshTokenExpiry.as_str()
))
.await
}
}
}
pub async fn idm_oauth2_rs_enable_consent_prompt(&self, id: &str) -> Result<(), ClientError> {
let mut update_oauth2_rs = Entry {
attrs: BTreeMap::new(),
};
update_oauth2_rs.attrs.insert(
ATTR_OAUTH2_CONSENT_PROMPT_ENABLE.to_string(),
vec!["true".to_string()],
);
self.perform_patch_request(format!("/v1/oauth2/{}", id).as_str(), update_oauth2_rs)
.await
}
pub async fn idm_oauth2_rs_disable_consent_prompt(&self, id: &str) -> Result<(), ClientError> {
let mut update_oauth2_rs = Entry {
attrs: BTreeMap::new(),
};
update_oauth2_rs.attrs.insert(
ATTR_OAUTH2_CONSENT_PROMPT_ENABLE.to_string(),
vec!["false".to_string()],
);
self.perform_patch_request(format!("/v1/oauth2/{}", id).as_str(), update_oauth2_rs)
.await
}
}