mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +01:00
Add strict OAuth2 URL enforcement per the RFC. This includes a transition process for the next release so that Admins can come into compliance.
449 lines
14 KiB
Rust
449 lines
14 KiB
Rust
use crate::{ClientError, KanidmClient};
|
|
use kanidm_proto::constants::{
|
|
ATTR_DISPLAYNAME, ATTR_ES256_PRIVATE_KEY_DER, ATTR_NAME,
|
|
ATTR_OAUTH2_ALLOW_INSECURE_CLIENT_DISABLE_PKCE, ATTR_OAUTH2_ALLOW_LOCALHOST_REDIRECT,
|
|
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_RS_TOKEN_KEY, ATTR_OAUTH2_STRICT_REDIRECT_URI, ATTR_RS256_PRIVATE_KEY_DER,
|
|
};
|
|
use kanidm_proto::internal::{ImageValue, Oauth2ClaimMapJoin};
|
|
use kanidm_proto::v1::Entry;
|
|
use reqwest::multipart;
|
|
use std::collections::BTreeMap;
|
|
use url::Url;
|
|
|
|
impl KanidmClient {
|
|
// ==== Oauth2 resource server configuration
|
|
#[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()],
|
|
);
|
|
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()],
|
|
);
|
|
self.perform_post_request("/v1/oauth2/_public", new_oauth2_rs)
|
|
.await
|
|
}
|
|
|
|
// TODO: the "id" here is actually the *name* not the uuid of the entry...
|
|
pub async fn idm_oauth2_rs_get(&self, id: &str) -> Result<Option<Entry>, ClientError> {
|
|
self.perform_get_request(format!("/v1/oauth2/{}", id).as_str())
|
|
.await
|
|
}
|
|
|
|
pub async fn idm_oauth2_rs_get_basic_secret(
|
|
&self,
|
|
id: &str,
|
|
) -> Result<Option<String>, ClientError> {
|
|
self.perform_get_request(format!("/v1/oauth2/{}/_basic_secret", id).as_str())
|
|
.await
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub async fn idm_oauth2_rs_update(
|
|
&self,
|
|
id: &str,
|
|
name: Option<&str>,
|
|
displayname: Option<&str>,
|
|
landing: Option<&str>,
|
|
reset_secret: bool,
|
|
reset_token_key: bool,
|
|
reset_sign_key: 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());
|
|
}
|
|
if reset_token_key {
|
|
update_oauth2_rs
|
|
.attrs
|
|
.insert(ATTR_OAUTH2_RS_TOKEN_KEY.to_string(), Vec::new());
|
|
}
|
|
if reset_sign_key {
|
|
update_oauth2_rs
|
|
.attrs
|
|
.insert(ATTR_ES256_PRIVATE_KEY_DER.to_string(), Vec::new());
|
|
update_oauth2_rs
|
|
.attrs
|
|
.insert(ATTR_RS256_PRIVATE_KEY_DER.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/{}/_scopemap/{}", id, 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/{}/_scopemap/{}", id, 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/{}/_sup_scopemap/{}", id, 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/{}/_sup_scopemap/{}", id, 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
|
|
}
|
|
|
|
/// Want to delete the image associated with a resource server? Here's your thing!
|
|
pub async fn idm_oauth2_rs_delete_image(&self, id: &str) -> Result<(), ClientError> {
|
|
self.perform_delete_request(format!("/v1/oauth2/{}/_image", id).as_str())
|
|
.await
|
|
}
|
|
|
|
/// Want to add/update the image associated with a resource server? Here's your thing!
|
|
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);
|
|
|
|
// send it
|
|
let response = self
|
|
.client
|
|
.post(self.make_url(&format!("/v1/oauth2/{}/_image", id)))
|
|
.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);
|
|
|
|
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))
|
|
}
|
|
|
|
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/{}/_claimmap/{}/{}", id, 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/{}/_claimmap/{}", id, 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/{}/_claimmap/{}/{}", id, 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/{}/_attr/{}", id, 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/{}/_attr/{}", id, ATTR_OAUTH2_RS_ORIGIN).as_str(),
|
|
url_to_remove,
|
|
)
|
|
.await
|
|
}
|
|
}
|