mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
Add the ability to configure and provide Oauth2 authentication for Kanidm. (#485)
This commit is contained in:
parent
8aa0056df6
commit
1de1b2db3b
34
Cargo.lock
generated
34
Cargo.lock
generated
|
@ -1469,9 +1469,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.1.18"
|
||||
version = "0.1.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c"
|
||||
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
@ -1793,11 +1793,13 @@ name = "kanidm_client"
|
|||
version = "1.1.0-alpha.4"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"base64 0.13.0",
|
||||
"env_logger",
|
||||
"futures",
|
||||
"kanidm",
|
||||
"kanidm_proto",
|
||||
"log",
|
||||
"oauth2",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
|
@ -1819,6 +1821,7 @@ dependencies = [
|
|||
"serde_derive",
|
||||
"serde_json",
|
||||
"time 0.2.27",
|
||||
"url",
|
||||
"uuid",
|
||||
"webauthn-rs",
|
||||
]
|
||||
|
@ -2184,6 +2187,24 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oauth2"
|
||||
version = "4.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10ad4d0136960353683efa6160b9c867088b4b8f567b762cd37420a10ce32703"
|
||||
dependencies = [
|
||||
"base64 0.12.3",
|
||||
"chrono",
|
||||
"http",
|
||||
"rand 0.7.3",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"sha2 0.9.5",
|
||||
"thiserror",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.8.0"
|
||||
|
@ -3000,6 +3021,15 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_path_to_error"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f6109f0506e20f7e0f910e51a0079acf41da8e0694e6442527c4ddf5a2b158"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_qs"
|
||||
version = "0.7.2"
|
||||
|
|
|
@ -93,21 +93,21 @@ made based on other factors like group membership.
|
|||
|
||||
::
|
||||
|
||||
class: oauth_resource_server
|
||||
class: oauth_resource_server_basic
|
||||
oauth_rs_name: String,
|
||||
oauth_rs_basic_secret: String,
|
||||
class: oauth2_resource_server
|
||||
class: oauth2_resource_server_basic
|
||||
oauth2_rs_name: String,
|
||||
oauth2_rs_basic_secret: String,
|
||||
# To validate the redirect root
|
||||
oauth_rs_origin: String/URI
|
||||
oauth2_rs_origin: String/URI
|
||||
# Scopes that apply to all users
|
||||
oauth_rs_scope_implicit: String
|
||||
oauth2_rs_scope_implicit: String
|
||||
# Scopes that map to groups which will be enforced.
|
||||
oauth_rs_scope_map: (String, reference)
|
||||
oauth2_rs_scope_map: (String, reference)
|
||||
# Filter of accounts that may authorise through this.
|
||||
oauth_rs_account_filter: Filter
|
||||
oauth2_rs_account_filter: Filter
|
||||
# A per-resource server fernet key for token/codes.
|
||||
# Allows reset per/application in case of suspect compromise.
|
||||
oauth_rs_token_key: String
|
||||
oauth2_rs_token_key: String
|
||||
|
||||
The returned authorisation code should be fernet encrypted and contains the unsigned UAT content of the authorised
|
||||
user.
|
||||
|
|
|
@ -19,7 +19,7 @@ serde_json = "1.0"
|
|||
serde_derive = "1.0"
|
||||
toml = "0.5"
|
||||
uuid = { version = "0.8", features = ["serde", "v4"] }
|
||||
url = "2.1.1"
|
||||
url = { version = "2", features = ["serde"] }
|
||||
webauthn-rs = "0.3.0-alpha.7"
|
||||
tokio = { version = "1", features = ["rt", "net", "time", "macros", "sync", "signal"] }
|
||||
|
||||
|
@ -30,3 +30,5 @@ futures = "0.3"
|
|||
async-std = "1.6"
|
||||
|
||||
webauthn-authenticator-rs = "0.3.0-alpha.9"
|
||||
oauth2_ext = { package = "oauth2", version = "4.0", default-features = false }
|
||||
base64 = "0.13"
|
||||
|
|
|
@ -29,6 +29,10 @@ impl KanidmAsyncClient {
|
|||
self.origin.as_str()
|
||||
}
|
||||
|
||||
pub fn get_url(&self) -> &str {
|
||||
self.addr.as_str()
|
||||
}
|
||||
|
||||
pub async fn set_token(&self, new_token: String) {
|
||||
let mut tguard = self.bearer_token.write().await;
|
||||
*tguard = Some(new_token);
|
||||
|
@ -55,10 +59,7 @@ impl KanidmAsyncClient {
|
|||
dest: &str,
|
||||
request: R,
|
||||
) -> Result<T, ClientError> {
|
||||
let dest = [self.addr.as_str(), dest].concat();
|
||||
debug!("{:?}", dest);
|
||||
// format doesn't work in async ?!
|
||||
// let dest = format!("{}{}", self.addr, dest);
|
||||
let dest = format!("{}{}", self.get_url(), dest);
|
||||
|
||||
let req_string = serde_json::to_string(&request).map_err(ClientError::JsonEncode)?;
|
||||
|
||||
|
@ -102,8 +103,9 @@ impl KanidmAsyncClient {
|
|||
|
||||
let opid = headers
|
||||
.get(KOPID)
|
||||
.and_then(|hv| hv.to_str().ok().map(str::to_string))
|
||||
.unwrap_or_else(|| "missing_kopid".to_string());
|
||||
.and_then(|hv| hv.to_str().ok())
|
||||
.unwrap_or("missing_kopid")
|
||||
.to_string();
|
||||
debug!("opid -> {:?}", opid);
|
||||
|
||||
match response.status() {
|
||||
|
@ -128,10 +130,7 @@ impl KanidmAsyncClient {
|
|||
dest: &str,
|
||||
request: R,
|
||||
) -> Result<T, ClientError> {
|
||||
let dest = [self.addr.as_str(), dest].concat();
|
||||
debug!("{:?}", dest);
|
||||
// format doesn't work in async ?!
|
||||
// let dest = format!("{}{}", self.addr, dest);
|
||||
let dest = format!("{}{}", self.get_url(), dest);
|
||||
|
||||
let req_string = serde_json::to_string(&request).map_err(ClientError::JsonEncode)?;
|
||||
let response = self
|
||||
|
@ -154,8 +153,9 @@ impl KanidmAsyncClient {
|
|||
let opid = response
|
||||
.headers()
|
||||
.get(KOPID)
|
||||
.and_then(|hv| hv.to_str().ok().map(str::to_string))
|
||||
.unwrap_or_else(|| "missing_kopid".to_string());
|
||||
.and_then(|hv| hv.to_str().ok())
|
||||
.unwrap_or("missing_kopid")
|
||||
.to_string();
|
||||
debug!("opid -> {:?}", opid);
|
||||
|
||||
match response.status() {
|
||||
|
@ -180,10 +180,7 @@ impl KanidmAsyncClient {
|
|||
dest: &str,
|
||||
request: R,
|
||||
) -> Result<T, ClientError> {
|
||||
let dest = [self.addr.as_str(), dest].concat();
|
||||
debug!("{:?}", dest);
|
||||
// format doesn't work in async ?!
|
||||
// let dest = format!("{}{}", self.addr, dest);
|
||||
let dest = format!("{}{}", self.get_url(), dest);
|
||||
|
||||
let req_string = serde_json::to_string(&request).map_err(ClientError::JsonEncode)?;
|
||||
|
||||
|
@ -209,8 +206,9 @@ impl KanidmAsyncClient {
|
|||
let opid = response
|
||||
.headers()
|
||||
.get(KOPID)
|
||||
.and_then(|hv| hv.to_str().ok().map(str::to_string))
|
||||
.unwrap_or_else(|| "missing_kopid".to_string());
|
||||
.and_then(|hv| hv.to_str().ok())
|
||||
.unwrap_or("missing_kopid")
|
||||
.to_string();
|
||||
|
||||
debug!("opid -> {:?}", opid);
|
||||
|
||||
|
@ -231,10 +229,58 @@ impl KanidmAsyncClient {
|
|||
.map_err(|e| ClientError::JsonDecode(e, opid))
|
||||
}
|
||||
|
||||
async fn perform_patch_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
|
||||
.patch(dest.as_str())
|
||||
.body(req_string)
|
||||
.header(CONTENT_TYPE, APPLICATION_JSON);
|
||||
|
||||
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(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_get_request<T: DeserializeOwned>(&self, dest: &str) -> Result<T, ClientError> {
|
||||
let dest = [self.addr.as_str(), dest].concat();
|
||||
debug!("{:?}", dest);
|
||||
// let dest = format!("{}{}", self.addr, dest);
|
||||
let dest = format!("{}{}", self.get_url(), dest);
|
||||
let response = self.client.get(dest.as_str());
|
||||
|
||||
let response = {
|
||||
|
@ -251,8 +297,9 @@ impl KanidmAsyncClient {
|
|||
let opid = response
|
||||
.headers()
|
||||
.get(KOPID)
|
||||
.and_then(|hv| hv.to_str().ok().map(str::to_string))
|
||||
.unwrap_or_else(|| "missing_kopid".to_string());
|
||||
.and_then(|hv| hv.to_str().ok())
|
||||
.unwrap_or("missing_kopid")
|
||||
.to_string();
|
||||
|
||||
debug!("opid -> {:?}", opid);
|
||||
|
||||
|
@ -274,7 +321,7 @@ impl KanidmAsyncClient {
|
|||
}
|
||||
|
||||
async fn perform_delete_request(&self, dest: &str) -> Result<(), ClientError> {
|
||||
let dest = format!("{}{}", self.addr, dest);
|
||||
let dest = format!("{}{}", self.get_url(), dest);
|
||||
|
||||
let response = self
|
||||
.client
|
||||
|
@ -295,8 +342,9 @@ impl KanidmAsyncClient {
|
|||
let opid = response
|
||||
.headers()
|
||||
.get(KOPID)
|
||||
.and_then(|hv| hv.to_str().ok().map(str::to_string))
|
||||
.unwrap_or_else(|| "missing_kopid".to_string());
|
||||
.and_then(|hv| hv.to_str().ok())
|
||||
.unwrap_or("missing_kopid")
|
||||
.to_string();
|
||||
debug!("opid -> {:?}", opid);
|
||||
|
||||
match response.status() {
|
||||
|
@ -321,7 +369,7 @@ impl KanidmAsyncClient {
|
|||
dest: &str,
|
||||
request: R,
|
||||
) -> Result<(), ClientError> {
|
||||
let dest = format!("{}{}", self.addr, dest);
|
||||
let dest = format!("{}{}", self.get_url(), dest);
|
||||
|
||||
let req_string = serde_json::to_string(&request).map_err(ClientError::JsonEncode)?;
|
||||
let response = self
|
||||
|
@ -344,8 +392,9 @@ impl KanidmAsyncClient {
|
|||
let opid = response
|
||||
.headers()
|
||||
.get(KOPID)
|
||||
.and_then(|hv| hv.to_str().ok().map(str::to_string))
|
||||
.unwrap_or_else(|| "missing_kopid".to_string());
|
||||
.and_then(|hv| hv.to_str().ok())
|
||||
.unwrap_or("missing_kopid")
|
||||
.to_string();
|
||||
debug!("opid -> {:?}", opid);
|
||||
|
||||
match response.status() {
|
||||
|
@ -625,8 +674,9 @@ impl KanidmAsyncClient {
|
|||
let opid = response
|
||||
.headers()
|
||||
.get(KOPID)
|
||||
.and_then(|hv| hv.to_str().ok().map(str::to_string))
|
||||
.unwrap_or_else(|| "missing_kopid".to_string());
|
||||
.and_then(|hv| hv.to_str().ok())
|
||||
.unwrap_or("missing_kopid")
|
||||
.to_string();
|
||||
debug!("opid -> {:?}", opid);
|
||||
|
||||
match response.status() {
|
||||
|
@ -1196,6 +1246,74 @@ impl KanidmAsyncClient {
|
|||
.await
|
||||
}
|
||||
|
||||
// ==== Oauth2 resource server configuration
|
||||
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,
|
||||
origin: &str,
|
||||
) -> Result<(), ClientError> {
|
||||
let mut new_oauth2_rs = Entry::default();
|
||||
new_oauth2_rs
|
||||
.attrs
|
||||
.insert("oauth2_rs_name".to_string(), vec![name.to_string()]);
|
||||
new_oauth2_rs
|
||||
.attrs
|
||||
.insert("oauth2_rs_origin".to_string(), vec![origin.to_string()]);
|
||||
self.perform_post_request("/v1/oauth2/_basic", new_oauth2_rs)
|
||||
.await
|
||||
}
|
||||
|
||||
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_update(
|
||||
&self,
|
||||
id: &str,
|
||||
name: Option<&str>,
|
||||
origin: Option<&str>,
|
||||
reset_secret: bool,
|
||||
reset_token_key: bool,
|
||||
) -> Result<(), ClientError> {
|
||||
let mut update_oauth2_rs = Entry {
|
||||
attrs: BTreeMap::new(),
|
||||
};
|
||||
|
||||
if let Some(newname) = name {
|
||||
update_oauth2_rs
|
||||
.attrs
|
||||
.insert("oauth2_rs_name".to_string(), vec![newname.to_string()]);
|
||||
}
|
||||
if let Some(neworigin) = origin {
|
||||
update_oauth2_rs
|
||||
.attrs
|
||||
.insert("oauth2_rs_origin".to_string(), vec![neworigin.to_string()]);
|
||||
}
|
||||
if reset_secret {
|
||||
update_oauth2_rs
|
||||
.attrs
|
||||
.insert("oauth2_rs_basic_secret".to_string(), Vec::new());
|
||||
}
|
||||
if reset_token_key {
|
||||
update_oauth2_rs
|
||||
.attrs
|
||||
.insert("oauth2_rs_basic_token_key".to_string(), Vec::new());
|
||||
}
|
||||
|
||||
self.perform_patch_request(format!("/v1/oauth2/{}", id).as_str(), update_oauth2_rs)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn idm_oauth2_rs_delete(&self, id: &str) -> Result<(), ClientError> {
|
||||
self.perform_delete_request(["/v1/oauth2/", id].concat().as_str())
|
||||
.await
|
||||
}
|
||||
|
||||
// ==== recycle bin
|
||||
pub async fn recycle_bin_list(&self) -> Result<Vec<Entry>, ClientError> {
|
||||
self.perform_get_request("/v1/recycle_bin").await
|
||||
|
|
|
@ -368,6 +368,10 @@ impl KanidmClient {
|
|||
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
|
||||
|
@ -808,6 +812,41 @@ impl KanidmClient {
|
|||
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, origin: &str) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_oauth2_rs_basic_create(name, origin))
|
||||
}
|
||||
|
||||
pub fn idm_oauth2_rs_get(&self, id: &str) -> Result<Option<Entry>, ClientError> {
|
||||
tokio_block_on(self.asclient.idm_oauth2_rs_get(id))
|
||||
}
|
||||
|
||||
pub fn idm_oauth2_rs_update(
|
||||
&self,
|
||||
id: &str,
|
||||
name: Option<&str>,
|
||||
origin: Option<&str>,
|
||||
reset_secret: bool,
|
||||
reset_token_key: bool,
|
||||
) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_oauth2_rs_update(
|
||||
id,
|
||||
name,
|
||||
origin,
|
||||
reset_secret,
|
||||
reset_token_key,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn idm_oauth2_rs_delete(&self, id: &str) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_oauth2_rs_delete(id))
|
||||
}
|
||||
|
||||
// ==== recycle bin
|
||||
pub fn recycle_bin_list(&self) -> Result<Vec<Entry>, ClientError> {
|
||||
tokio_block_on(self.asclient.recycle_bin_list())
|
||||
|
|
|
@ -319,7 +319,7 @@ fn test_default_entries_rbac_group_managers() {
|
|||
.map(|entry| entry.attrs.get("name").unwrap().first().unwrap())
|
||||
.cloned()
|
||||
.collect();
|
||||
assert_eq!(default_group_names, group_names);
|
||||
assert!(default_group_names.is_subset(&group_names));
|
||||
|
||||
test_modify_group(&rsclient, &DEFAULT_HP_GROUP_NAMES, false);
|
||||
test_modify_group(&rsclient, &DEFAULT_NOT_HP_GROUP_NAMES, true);
|
||||
|
@ -423,7 +423,7 @@ fn test_default_entries_rbac_admins_schema_entries() {
|
|||
.collect();
|
||||
println!("{:?}", classnames);
|
||||
|
||||
assert_eq!(default_classnames, classnames);
|
||||
assert!(default_classnames.is_subset(&classnames));
|
||||
|
||||
let default_attributenames: HashSet<String> = [
|
||||
"acp_create_attr",
|
||||
|
@ -486,7 +486,6 @@ fn test_default_entries_rbac_admins_schema_entries() {
|
|||
.cloned()
|
||||
.collect();
|
||||
|
||||
// I wonder if this should be a subset op?
|
||||
assert!(default_attributenames.is_subset(&attributenames));
|
||||
});
|
||||
}
|
||||
|
|
148
kanidm_client/tests/oauth2_test.rs
Normal file
148
kanidm_client/tests/oauth2_test.rs
Normal file
|
@ -0,0 +1,148 @@
|
|||
mod common;
|
||||
use crate::common::{run_test, ADMIN_TEST_PASSWORD};
|
||||
use kanidm_client::KanidmClient;
|
||||
|
||||
use kanidm_proto::oauth2::{AccessTokenRequest, AccessTokenResponse, ConsentRequest};
|
||||
use oauth2_ext::PkceCodeChallenge;
|
||||
use std::collections::HashMap;
|
||||
use url::Url;
|
||||
|
||||
#[test]
|
||||
fn test_oauth2_basic_flow() {
|
||||
run_test(|rsclient: KanidmClient| {
|
||||
let res = rsclient.auth_simple_password("admin", ADMIN_TEST_PASSWORD);
|
||||
assert!(res.is_ok());
|
||||
|
||||
// Create an oauth2 application integration.
|
||||
rsclient
|
||||
.idm_oauth2_rs_basic_create("test_integration", "https://demo.example.com")
|
||||
.expect("Failed to create oauth2 config");
|
||||
|
||||
let oauth2_config = rsclient
|
||||
.idm_oauth2_rs_get("test_integration")
|
||||
.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");
|
||||
|
||||
// Get our admin's auth token for our new client.
|
||||
let admin_uat = rsclient.get_token().expect("No user auth token found");
|
||||
|
||||
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.");
|
||||
// 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 (pkce_code_challenge, pkce_code_verifier) = PkceCodeChallenge::new_random_sha256();
|
||||
|
||||
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", "mail+name+test"),
|
||||
])
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send request.");
|
||||
|
||||
assert!(response.status() == reqwest::StatusCode::OK);
|
||||
|
||||
let consent_req: ConsentRequest = response
|
||||
.json()
|
||||
.await
|
||||
.expect("Failed to access response body");
|
||||
|
||||
// Step 2 - we now send the consent get to the server which yields a redirect with a
|
||||
// state and code.
|
||||
|
||||
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.");
|
||||
|
||||
// This should yield a 302 redirect with some query params.
|
||||
assert!(response.status() == reqwest::StatusCode::FOUND);
|
||||
|
||||
// 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");
|
||||
|
||||
// Now check it's content
|
||||
let redir_url = Url::parse(&redir_str).expect("Url parse failure");
|
||||
|
||||
// We should have state and code.
|
||||
let pairs: HashMap<_, _> = redir_url.query_pairs().collect();
|
||||
|
||||
let code = pairs.get("code").expect("code not found!");
|
||||
|
||||
let state = pairs.get("state").expect("state not found!");
|
||||
|
||||
assert!(state == "YWJjZGVm");
|
||||
|
||||
// Step 3 - the "resource server" then uses this state and code to directly contact
|
||||
// the authorisation server to request a token.
|
||||
|
||||
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,
|
||||
code_verifier: pkce_code_verifier.secret().clone(),
|
||||
};
|
||||
|
||||
let response = client
|
||||
.post(format!("{}/oauth2/token", url))
|
||||
.basic_auth("test_integration", Some(client_secret))
|
||||
.form(&form_req)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send code exchange request.");
|
||||
|
||||
assert!(response.status() == reqwest::StatusCode::OK);
|
||||
|
||||
// The body is a json AccessTokenResponse
|
||||
|
||||
let _atr = response
|
||||
.json::<AccessTokenResponse>()
|
||||
.await
|
||||
.expect("Unable to decode AccessTokenResponse");
|
||||
|
||||
// Step 4 - inspect the granted token.
|
||||
})
|
||||
})
|
||||
}
|
|
@ -83,7 +83,7 @@ fn test_server_whoami_anonymous() {
|
|||
None => panic!(),
|
||||
};
|
||||
debug!("{}", uat);
|
||||
assert!(uat.name == "anonymous");
|
||||
assert!(uat.spn == "anonymous@example.com");
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -104,7 +104,7 @@ fn test_server_whoami_admin_simple_password() {
|
|||
None => panic!(),
|
||||
};
|
||||
debug!("{}", uat);
|
||||
assert!(uat.name == "admin");
|
||||
assert!(uat.spn == "admin@example.com");
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -987,6 +987,80 @@ fn test_server_rest_webauthn_mfa_auth_lifecycle() {
|
|||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_server_rest_oauth2_basic_lifecycle() {
|
||||
run_test(|rsclient: KanidmClient| {
|
||||
let res = rsclient.auth_simple_password("admin", ADMIN_TEST_PASSWORD);
|
||||
assert!(res.is_ok());
|
||||
|
||||
// List, there are non.
|
||||
let initial_configs = rsclient
|
||||
.idm_oauth2_rs_list()
|
||||
.expect("Failed to retrieve oauth2 configs");
|
||||
|
||||
assert!(initial_configs.is_empty());
|
||||
|
||||
// Create a new oauth2 config
|
||||
rsclient
|
||||
.idm_oauth2_rs_basic_create("test_integration", "https://demo.example.com")
|
||||
.expect("Failed to create oauth2 config");
|
||||
|
||||
// List, there is what we created.
|
||||
let initial_configs = rsclient
|
||||
.idm_oauth2_rs_list()
|
||||
.expect("Failed to retrieve oauth2 configs");
|
||||
|
||||
assert!(initial_configs.len() == 1);
|
||||
|
||||
// Get the value. Assert we have oauth2_rs_basic_secret,
|
||||
// but can NOT see the token_secret.
|
||||
let oauth2_config = rsclient
|
||||
.idm_oauth2_rs_get("test_integration")
|
||||
.ok()
|
||||
.flatten()
|
||||
.expect("Failed to retrieve test_integration config");
|
||||
|
||||
// What can we see?
|
||||
assert!(oauth2_config.attrs.contains_key("oauth2_rs_basic_secret"));
|
||||
// This is present, but redacted.
|
||||
assert!(oauth2_config
|
||||
.attrs
|
||||
.contains_key("oauth2_rs_basic_token_key"));
|
||||
|
||||
// Mod delete the secret/key and check them again.
|
||||
// Check we can patch the oauth2_rs_name / oauth2_rs_origin
|
||||
rsclient
|
||||
.idm_oauth2_rs_update(
|
||||
"test_integration",
|
||||
None,
|
||||
Some("https://new_demo.example.com"),
|
||||
true,
|
||||
true,
|
||||
)
|
||||
.expect("Failed to update config");
|
||||
|
||||
let oauth2_config_updated = rsclient
|
||||
.idm_oauth2_rs_get("test_integration")
|
||||
.ok()
|
||||
.flatten()
|
||||
.expect("Failed to retrieve test_integration config");
|
||||
|
||||
assert!(oauth2_config_updated != oauth2_config);
|
||||
|
||||
// Delete the config
|
||||
rsclient
|
||||
.idm_oauth2_rs_delete("test_integration")
|
||||
.expect("Failed to delete test_integration");
|
||||
|
||||
// List, there are none.
|
||||
let final_configs = rsclient
|
||||
.idm_oauth2_rs_list()
|
||||
.expect("Failed to retrieve oauth2 configs");
|
||||
|
||||
assert!(final_configs.is_empty());
|
||||
});
|
||||
}
|
||||
|
||||
// Test setting account expiry
|
||||
|
||||
// Test the self version of the radius path.
|
||||
|
|
|
@ -17,6 +17,7 @@ uuid = { version = "0.8", features = ["serde", "wasm-bindgen"] }
|
|||
base32 = "0.4"
|
||||
webauthn-rs = { version = "0.3.0-alpha.7", default-features = false, features = ["wasm"] }
|
||||
time = { version = "0.2", features = ["serde", "std"] }
|
||||
url = { version = "2", features = ["serde"] }
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = "1.0"
|
||||
|
|
|
@ -11,4 +11,5 @@
|
|||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
|
||||
pub mod oauth2;
|
||||
pub mod v1;
|
||||
|
|
87
kanidm_proto/src/oauth2.rs
Normal file
87
kanidm_proto/src/oauth2.rs
Normal file
|
@ -0,0 +1,87 @@
|
|||
use url::Url;
|
||||
use webauthn_rs::base64_data::Base64UrlSafeData;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
pub enum CodeChallengeMethod {
|
||||
// default to plain if not requested as S256. Reject the auth?
|
||||
// plain
|
||||
// BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
|
||||
S256,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct AuthorisationRequest {
|
||||
// Must be "code". (or token, see 4.2.1)
|
||||
pub response_type: String,
|
||||
pub client_id: String,
|
||||
pub state: Base64UrlSafeData,
|
||||
// base64?
|
||||
pub code_challenge: Base64UrlSafeData,
|
||||
// Probably also should be an enum.
|
||||
pub code_challenge_method: CodeChallengeMethod,
|
||||
// Uri?
|
||||
pub redirect_uri: Url,
|
||||
// appears to be + seperated?
|
||||
pub scope: String,
|
||||
}
|
||||
|
||||
/// We ask our user to consent to this Authorisation Request with the
|
||||
/// following data.
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct ConsentRequest {
|
||||
// A pretty-name of the client
|
||||
pub client_name: String,
|
||||
pub scopes: Vec<String>,
|
||||
// The users displayname (?)
|
||||
// pub display_name: String,
|
||||
// The token we need to be given back to allow this to proceed
|
||||
pub consent_token: String,
|
||||
}
|
||||
|
||||
// The resource server then contacts the token endpoint with
|
||||
//
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct AccessTokenRequest {
|
||||
// must be authorization_code
|
||||
pub grant_type: String,
|
||||
// As sent by the authorisationCode
|
||||
pub code: String,
|
||||
// Must be the same as the original redirect uri.
|
||||
pub redirect_uri: Url,
|
||||
// REQUIRED, if the client is not authenticating with the
|
||||
// authorization server as described in Section 3.2.1.
|
||||
pub client_id: Option<String>,
|
||||
//
|
||||
pub code_verifier: String,
|
||||
}
|
||||
|
||||
// We now check code_verifier is the same via the formula.
|
||||
|
||||
// If and only if it checks out, we proceed.
|
||||
|
||||
// Returned as a json body
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct AccessTokenResponse {
|
||||
// Could be Base64UrlSafeData
|
||||
pub access_token: String,
|
||||
// Enum?
|
||||
pub token_type: String,
|
||||
// seconds.
|
||||
pub expires_in: u32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub refresh_token: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
/// Space seperated list of scopes that were approved, if this differs from the
|
||||
/// original request.
|
||||
pub scope: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct ErrorResponse {
|
||||
pub error: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error_description: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error_uri: Option<Url>,
|
||||
}
|
|
@ -32,6 +32,7 @@ pub enum PluginError {
|
|||
Base(String),
|
||||
ReferentialIntegrity(String),
|
||||
PasswordImport(String),
|
||||
Oauth2Secrets,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
|
@ -179,19 +180,17 @@ pub enum AuthType {
|
|||
#[serde(rename_all = "lowercase")]
|
||||
pub struct UserAuthToken {
|
||||
pub session_id: Uuid,
|
||||
// When this data should be considered invalid. Interpretation
|
||||
pub auth_type: AuthType,
|
||||
// When this token should be considered expired. Interpretation
|
||||
// may depend on the client application.
|
||||
pub expiry: time::OffsetDateTime,
|
||||
pub name: String,
|
||||
pub spn: String,
|
||||
pub displayname: String,
|
||||
pub uuid: Uuid,
|
||||
// #[serde(skip_serializing_if = "Option::is_none")]
|
||||
// pub application: Option<Application>,
|
||||
pub groups: Vec<Group>,
|
||||
pub claims: Vec<Claim>,
|
||||
pub auth_type: AuthType,
|
||||
// Should we allow supplemental ava's to be added on request?
|
||||
// pub name: String,
|
||||
pub spn: String,
|
||||
// pub groups: Vec<Group>,
|
||||
// pub claims: Vec<Claim>,
|
||||
// Should we just retrieve these inside the server instead of in the uat?
|
||||
// or do we want per-session limit capabilities?
|
||||
pub lim_uidx: bool,
|
||||
pub lim_rmax: usize,
|
||||
pub lim_pmax: usize,
|
||||
|
@ -200,17 +199,19 @@ pub struct UserAuthToken {
|
|||
|
||||
impl fmt::Display for UserAuthToken {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
writeln!(f, "name: {}", self.name)?;
|
||||
// writeln!(f, "name: {}", self.name)?;
|
||||
writeln!(f, "spn: {}", self.spn)?;
|
||||
writeln!(f, "display: {}", self.displayname)?;
|
||||
writeln!(f, "uuid: {}", self.uuid)?;
|
||||
/*
|
||||
writeln!(f, "display: {}", self.displayname)?;
|
||||
for group in &self.groups {
|
||||
writeln!(f, "group: {:?}", group.name)?;
|
||||
}
|
||||
for claim in &self.claims {
|
||||
writeln!(f, "claim: {:?}", claim)?;
|
||||
}
|
||||
writeln!(f, "expiry: {}", self.expiry)
|
||||
*/
|
||||
writeln!(f, "token expiry: {}", self.expiry)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -395,7 +396,7 @@ pub struct BackupCodesView {
|
|||
// the in memory server core entry type, without affecting the protoEntry type
|
||||
//
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
|
||||
pub struct Entry {
|
||||
pub attrs: BTreeMap<String, Vec<String>>,
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ include!("../opt/kanidm.rs");
|
|||
pub mod account;
|
||||
pub mod common;
|
||||
pub mod group;
|
||||
pub mod oauth2;
|
||||
pub mod raw;
|
||||
pub mod recycle;
|
||||
pub mod session;
|
||||
|
@ -71,6 +72,20 @@ impl SelfOpt {
|
|||
}
|
||||
}
|
||||
|
||||
impl SystemOpt {
|
||||
pub fn debug(&self) -> bool {
|
||||
match self {
|
||||
SystemOpt::Oauth2(oopt) => oopt.debug(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn exec(&self) {
|
||||
match self {
|
||||
SystemOpt::Oauth2(oopt) => oopt.exec(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl KanidmClientOpt {
|
||||
pub fn debug(&self) -> bool {
|
||||
match self {
|
||||
|
@ -81,6 +96,7 @@ impl KanidmClientOpt {
|
|||
KanidmClientOpt::CSelf(csopt) => csopt.debug(),
|
||||
KanidmClientOpt::Account(aopt) => aopt.debug(),
|
||||
KanidmClientOpt::Group(gopt) => gopt.debug(),
|
||||
KanidmClientOpt::System(sopt) => sopt.debug(),
|
||||
KanidmClientOpt::Recycle(ropt) => ropt.debug(),
|
||||
}
|
||||
}
|
||||
|
@ -94,6 +110,7 @@ impl KanidmClientOpt {
|
|||
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(),
|
||||
}
|
||||
}
|
||||
|
|
48
kanidm_tools/src/cli/oauth2.rs
Normal file
48
kanidm_tools/src/cli/oauth2.rs
Normal file
|
@ -0,0 +1,48 @@
|
|||
use crate::Oauth2Opt;
|
||||
|
||||
impl Oauth2Opt {
|
||||
pub fn debug(&self) -> bool {
|
||||
match self {
|
||||
Oauth2Opt::List(copt) => copt.debug,
|
||||
Oauth2Opt::Get(nopt) => nopt.copt.debug,
|
||||
Oauth2Opt::CreateBasic(cbopt) => cbopt.nopt.copt.debug,
|
||||
Oauth2Opt::Delete(nopt) => nopt.copt.debug,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn exec(&self) {
|
||||
match self {
|
||||
Oauth2Opt::List(copt) => {
|
||||
let client = copt.to_client();
|
||||
match client.idm_oauth2_rs_list() {
|
||||
Ok(r) => r.iter().for_each(|ent| println!("{}", ent)),
|
||||
Err(e) => eprintln!("Error -> {:?}", e),
|
||||
}
|
||||
}
|
||||
Oauth2Opt::Get(nopt) => {
|
||||
let client = nopt.copt.to_client();
|
||||
match client.idm_oauth2_rs_get(nopt.name.as_str()) {
|
||||
Ok(Some(e)) => println!("{}", e),
|
||||
Ok(None) => println!("No matching entries"),
|
||||
Err(e) => eprintln!("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.origin.as_str())
|
||||
{
|
||||
Ok(_) => println!("Success"),
|
||||
Err(e) => eprintln!("Error -> {:?}", e),
|
||||
}
|
||||
}
|
||||
Oauth2Opt::Delete(nopt) => {
|
||||
let client = nopt.copt.to_client();
|
||||
match client.idm_oauth2_rs_delete(nopt.name.as_str()) {
|
||||
Ok(_) => println!("Success"),
|
||||
Err(e) => eprintln!("Error -> {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -301,7 +301,7 @@ impl LoginOpt {
|
|||
};
|
||||
|
||||
// write them out.
|
||||
if let Err(_) = write_tokens(&tokens) {
|
||||
if write_tokens(&tokens).is_err() {
|
||||
error!("Error persisting authentication token store");
|
||||
std::process::exit(1);
|
||||
};
|
||||
|
|
|
@ -342,6 +342,40 @@ pub enum SelfOpt {
|
|||
SetPassword(CommonOpt),
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub struct Oauth2BasicCreateOpt {
|
||||
#[structopt(name = "origin")]
|
||||
origin: String,
|
||||
#[structopt(flatten)]
|
||||
nopt: Named,
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub enum Oauth2Opt {
|
||||
#[structopt(name = "list")]
|
||||
/// List all configured oauth2 resource servers
|
||||
List(CommonOpt),
|
||||
#[structopt(name = "get")]
|
||||
/// Display a selected oauth2 resource server
|
||||
Get(Named),
|
||||
// #[structopt(name = "set")]
|
||||
// /// Set options for a selected oauth2 resource server
|
||||
// Set(),
|
||||
#[structopt(name = "create")]
|
||||
/// Create a new oauth2 resource server
|
||||
CreateBasic(Oauth2BasicCreateOpt),
|
||||
#[structopt(name = "delete")]
|
||||
/// Delete a oauth2 resource server
|
||||
Delete(Named),
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub enum SystemOpt {
|
||||
#[structopt(name = "oauth2")]
|
||||
/// Configure and display oauth2/oidc resource server configuration
|
||||
Oauth2(Oauth2Opt),
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
#[structopt(about = "Kanidm Client Utility")]
|
||||
pub enum KanidmClientOpt {
|
||||
|
@ -363,6 +397,9 @@ pub enum KanidmClientOpt {
|
|||
#[structopt(name = "group")]
|
||||
/// Group operations
|
||||
Group(GroupOpt),
|
||||
#[structopt(name = "system")]
|
||||
/// System configuration operations
|
||||
System(SystemOpt),
|
||||
#[structopt(name = "recycle_bin")]
|
||||
/// Recycle Bin operations
|
||||
Recycle(RecycleOpt),
|
||||
|
|
|
@ -22,12 +22,12 @@ kanidm_proto = { path = "../kanidm_proto", version = "1.1.0-alpha" }
|
|||
|
||||
jemallocator = { version = "0.3.0", optional = true }
|
||||
|
||||
url = "2.1"
|
||||
url = { version = "2", features = ["serde"] }
|
||||
tide = "0.16"
|
||||
tide-rustls = "0.3"
|
||||
async-trait = "0.1"
|
||||
async-h1 = "2.0"
|
||||
fernet = "^0.1.4"
|
||||
fernet = { version = "^0.1.4", features = ["fernet_danger_timestamps"] }
|
||||
bundy = "^0.1.1"
|
||||
|
||||
async-std = "1.6"
|
||||
|
|
|
@ -6,5 +6,5 @@ tls_chain = "../insecure/chain.pem"
|
|||
tls_key = "../insecure/key.pem"
|
||||
# log_level = "perfbasic"
|
||||
# log_level = "quiet"
|
||||
log_level = "verbose"
|
||||
# log_level = "verbose"
|
||||
origin = "https://idm.example.com:8443"
|
||||
|
|
|
@ -13,6 +13,10 @@ use crate::value::PartialValue;
|
|||
use kanidm_proto::v1::{BackupCodesView, OperationError, RadiusAuthToken};
|
||||
|
||||
use crate::filter::{Filter, FilterInvalid};
|
||||
use crate::idm::oauth2::{
|
||||
AccessTokenRequest, AccessTokenResponse, AuthorisationRequest, AuthorisePermitSuccess,
|
||||
ConsentRequest, Oauth2Error,
|
||||
};
|
||||
use crate::idm::server::{IdmServer, IdmServerTransaction};
|
||||
use crate::ldap::{LdapBoundToken, LdapResponseState, LdapServer};
|
||||
|
||||
|
@ -392,9 +396,10 @@ impl QueryServerReadV1 {
|
|||
let r = entries
|
||||
.pop()
|
||||
// From the entry, turn it into the value
|
||||
.and_then(|e| {
|
||||
e.get_ava_single("radius_secret")
|
||||
.and_then(|v| v.get_radius_secret().map(|s| s.to_string()))
|
||||
.and_then(|entry| {
|
||||
entry
|
||||
.get_ava_single("radius_secret")
|
||||
.and_then(|v| v.get_secret_str().map(str::to_string))
|
||||
});
|
||||
Ok(r)
|
||||
}
|
||||
|
@ -898,6 +903,112 @@ impl QueryServerReadV1 {
|
|||
res
|
||||
}
|
||||
|
||||
pub async fn handle_oauth2_authorise(
|
||||
&self,
|
||||
uat: Option<String>,
|
||||
auth_req: AuthorisationRequest,
|
||||
eventid: Uuid,
|
||||
) -> Result<ConsentRequest, Oauth2Error> {
|
||||
let mut audit = AuditScope::new("oauth2_authorise", eventid, self.log_level);
|
||||
let ct = duration_from_epoch_now();
|
||||
let idms_prox_read = self.idms.proxy_read_async().await;
|
||||
let res = lperf_op_segment!(
|
||||
&mut audit,
|
||||
"actors::v1_read::handle<Oauth2Authorise>",
|
||||
|| {
|
||||
let (ident, uat) = idms_prox_read
|
||||
.validate_and_parse_uat(&mut audit, uat.as_deref(), ct)
|
||||
.and_then(|uat| {
|
||||
idms_prox_read
|
||||
.process_uat_to_identity(&mut audit, &uat, ct)
|
||||
.map(|ident| (ident, uat))
|
||||
})
|
||||
.map_err(|e| {
|
||||
ladmin_error!(audit, "Invalid identity: {:?}", e);
|
||||
Oauth2Error::AuthenticationRequired
|
||||
})?;
|
||||
|
||||
// Now we can send to the idm server for authorisation checking.
|
||||
idms_prox_read.check_oauth2_authorisation(&mut audit, &ident, &uat, &auth_req, ct)
|
||||
}
|
||||
);
|
||||
self.log.send(audit).map_err(|_| {
|
||||
error!("CRITICAL: UNABLE TO COMMIT LOGS");
|
||||
Oauth2Error::ServerError(OperationError::InvalidState)
|
||||
})?;
|
||||
res
|
||||
}
|
||||
|
||||
pub async fn handle_oauth2_authorise_permit(
|
||||
&self,
|
||||
uat: Option<String>,
|
||||
consent_req: String,
|
||||
eventid: Uuid,
|
||||
) -> Result<AuthorisePermitSuccess, OperationError> {
|
||||
let mut audit = AuditScope::new("oauth2_authorise_permit", eventid, self.log_level);
|
||||
let ct = duration_from_epoch_now();
|
||||
let idms_prox_read = self.idms.proxy_read_async().await;
|
||||
let res = lperf_op_segment!(
|
||||
&mut audit,
|
||||
"actors::v1_read::handle<Oauth2AuthorisePermit>",
|
||||
|| {
|
||||
let (ident, uat) = idms_prox_read
|
||||
.validate_and_parse_uat(&mut audit, uat.as_deref(), ct)
|
||||
.and_then(|uat| {
|
||||
idms_prox_read
|
||||
.process_uat_to_identity(&mut audit, &uat, ct)
|
||||
.map(|ident| (ident, uat))
|
||||
})
|
||||
.map_err(|e| {
|
||||
ladmin_error!(audit, "Invalid identity: {:?}", e);
|
||||
e
|
||||
})?;
|
||||
|
||||
idms_prox_read.check_oauth2_authorise_permit(
|
||||
&mut audit,
|
||||
&ident,
|
||||
&uat,
|
||||
&consent_req,
|
||||
ct,
|
||||
)
|
||||
}
|
||||
);
|
||||
self.log.send(audit).map_err(|_| {
|
||||
error!("CRITICAL: UNABLE TO COMMIT LOGS");
|
||||
OperationError::InvalidState
|
||||
})?;
|
||||
res
|
||||
}
|
||||
|
||||
pub async fn handle_oauth2_token_exchange(
|
||||
&self,
|
||||
client_authz: String,
|
||||
token_req: AccessTokenRequest,
|
||||
eventid: Uuid,
|
||||
) -> Result<AccessTokenResponse, Oauth2Error> {
|
||||
let mut audit = AuditScope::new("oauth2_token_exchange", eventid, self.log_level);
|
||||
let ct = duration_from_epoch_now();
|
||||
let idms_prox_read = self.idms.proxy_read_async().await;
|
||||
let res = lperf_op_segment!(
|
||||
&mut audit,
|
||||
"actors::v1_read::handle<Oauth2TokenExchange>",
|
||||
|| {
|
||||
// Now we can send to the idm server for authorisation checking.
|
||||
idms_prox_read.check_oauth2_token_exchange(
|
||||
&mut audit,
|
||||
&client_authz,
|
||||
&token_req,
|
||||
ct,
|
||||
)
|
||||
}
|
||||
);
|
||||
self.log.send(audit).map_err(|_| {
|
||||
error!("CRITICAL: UNABLE TO COMMIT LOGS");
|
||||
Oauth2Error::ServerError(OperationError::InvalidState)
|
||||
})?;
|
||||
res
|
||||
}
|
||||
|
||||
pub async fn handle_ldaprequest(
|
||||
&self,
|
||||
eventid: Uuid,
|
||||
|
|
|
@ -24,6 +24,7 @@ use crate::idm::delayed::DelayedAction;
|
|||
use crate::idm::server::{IdmServer, IdmServerTransaction};
|
||||
use crate::utils::duration_from_epoch_now;
|
||||
|
||||
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::{
|
||||
|
@ -142,12 +143,15 @@ impl QueryServerWriteV1 {
|
|||
e
|
||||
})?;
|
||||
|
||||
let f_uuid = filter_all!(f_eq("uuid", PartialValue::new_uuid(target_uuid)));
|
||||
// Add any supplemental conditions we have.
|
||||
let joined_filter = Filter::join_parts_and(f_uuid, filter);
|
||||
|
||||
let mdf = match ModifyEvent::from_internal_parts(
|
||||
audit,
|
||||
ident,
|
||||
target_uuid,
|
||||
ml,
|
||||
filter,
|
||||
&joined_filter,
|
||||
&idms_prox_write.qs_write,
|
||||
) {
|
||||
Ok(m) => m,
|
||||
|
@ -314,6 +318,64 @@ impl QueryServerWriteV1 {
|
|||
res
|
||||
}
|
||||
|
||||
pub async fn handle_internalpatch(
|
||||
&self,
|
||||
uat: Option<String>,
|
||||
filter: Filter<FilterInvalid>,
|
||||
update: ProtoEntry,
|
||||
eventid: Uuid,
|
||||
) -> Result<(), OperationError> {
|
||||
// Given a protoEntry, turn this into a modification set.
|
||||
let mut audit = AuditScope::new("internal_patch", eventid, self.log_level);
|
||||
let idms_prox_write = self.idms.proxy_write_async(duration_from_epoch_now()).await;
|
||||
let res = lperf_segment!(
|
||||
&mut audit,
|
||||
"actors::v1_write::handle<InternalPatch>",
|
||||
|| {
|
||||
let ct = duration_from_epoch_now();
|
||||
let ident = idms_prox_write
|
||||
.validate_and_parse_uat(&mut audit, uat.as_deref(), ct)
|
||||
.and_then(|uat| idms_prox_write.process_uat_to_identity(&mut audit, &uat, ct))
|
||||
.map_err(|e| {
|
||||
ladmin_error!(&mut audit, "Invalid identity: {:?}", e);
|
||||
e
|
||||
})?;
|
||||
|
||||
// Transform the ProtoEntry to a Modlist
|
||||
let modlist =
|
||||
ModifyList::from_patch(&mut audit, &update, &idms_prox_write.qs_write)
|
||||
.map_err(|e| {
|
||||
ladmin_error!(&mut audit, "Invalid Patch Request: {:?}", e);
|
||||
e
|
||||
})?;
|
||||
|
||||
let mdf = ModifyEvent::from_internal_parts(
|
||||
&mut audit,
|
||||
ident,
|
||||
&modlist,
|
||||
&filter,
|
||||
&idms_prox_write.qs_write,
|
||||
)
|
||||
.map_err(|e| {
|
||||
ladmin_error!(audit, "Failed to begin modify: {:?}", e);
|
||||
e
|
||||
})?;
|
||||
|
||||
ltrace!(audit, "Begin modify event {:?}", mdf);
|
||||
|
||||
idms_prox_write
|
||||
.qs_write
|
||||
.modify(&mut audit, &mdf)
|
||||
.and_then(|_| idms_prox_write.commit(&mut audit))
|
||||
}
|
||||
);
|
||||
self.log.send(audit).map_err(|_| {
|
||||
error!("CRITICAL: UNABLE TO COMMIT LOGS");
|
||||
OperationError::InvalidState
|
||||
})?;
|
||||
res.map(|_| ())
|
||||
}
|
||||
|
||||
pub async fn handle_internaldelete(
|
||||
&self,
|
||||
uat: Option<String>,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use std::{collections::HashSet, time::Duration};
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
use webauthn_rs::proto::COSEKey;
|
||||
|
||||
|
@ -146,6 +147,8 @@ pub enum DbValueV1 {
|
|||
DateTime(String),
|
||||
#[serde(rename = "EM")]
|
||||
EmailAddress(DbValueEmailAddressV1),
|
||||
#[serde(rename = "UR")]
|
||||
Url(Url),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -988,3 +988,58 @@ pub const JSON_IDM_HP_ACP_GROUP_UNIX_EXTEND_PRIV_V1: &str = r#"{
|
|||
"acp_modify_class": ["posixgroup"]
|
||||
}
|
||||
}"#;
|
||||
|
||||
// 35 oauth2 manage
|
||||
pub const JSON_IDM_HP_ACP_OAUTH2_MANAGE_PRIV_V1: &str = r#"{
|
||||
"attrs": {
|
||||
"class": [
|
||||
"object",
|
||||
"access_control_profile",
|
||||
"access_control_search",
|
||||
"access_control_modify",
|
||||
"access_control_delete",
|
||||
"access_control_create"
|
||||
],
|
||||
"name": ["idm_acp_hp_oauth2_manage_priv"],
|
||||
"uuid": ["00000000-0000-0000-0000-ffffff000035"],
|
||||
"description": ["Builtin IDM Control for managing oauth2 resource server integrations."],
|
||||
"acp_receiver": [
|
||||
"{\"eq\":[\"memberof\",\"00000000-0000-0000-0000-000000000027\"]}"
|
||||
],
|
||||
"acp_targetscope": [
|
||||
"{\"and\": [{\"eq\": [\"class\",\"oauth2_resource_server\"]},{\"andnot\": {\"or\": [{\"eq\": [\"class\", \"tombstone\"]}, {\"eq\": [\"class\", \"recycled\"]}]}}]}"
|
||||
],
|
||||
"acp_search_attr": [
|
||||
"class",
|
||||
"description",
|
||||
"oauth2_rs_name",
|
||||
"oauth2_rs_origin",
|
||||
"oauth2_rs_account_filter",
|
||||
"oauth2_rs_basic_secret",
|
||||
"oauth2_rs_basic_token_key"
|
||||
],
|
||||
"acp_modify_removedattr": [
|
||||
"description",
|
||||
"oauth2_rs_name",
|
||||
"oauth2_rs_origin",
|
||||
"oauth2_rs_account_filter",
|
||||
"oauth2_rs_basic_secret",
|
||||
"oauth2_rs_basic_token_key"
|
||||
],
|
||||
"acp_modify_presentattr": [
|
||||
"description",
|
||||
"oauth2_rs_name",
|
||||
"oauth2_rs_origin",
|
||||
"oauth2_rs_account_filter"
|
||||
],
|
||||
"acp_modify_class": [],
|
||||
"acp_create_attr": [
|
||||
"class",
|
||||
"description",
|
||||
"oauth2_rs_name",
|
||||
"oauth2_rs_origin",
|
||||
"oauth2_rs_account_filter"
|
||||
],
|
||||
"acp_create_class": ["oauth2_resource_server", "oauth2_resource_server_basic", "object"]
|
||||
}
|
||||
}"#;
|
||||
|
|
|
@ -292,6 +292,17 @@ pub const JSON_DOMAIN_ADMINS: &str = r#"{
|
|||
]
|
||||
}
|
||||
}"#;
|
||||
pub const JSON_IDM_HP_OAUTH2_MANAGE_PRIV_V1: &str = r#"{
|
||||
"attrs": {
|
||||
"class": ["group", "object"],
|
||||
"name": ["idm_hp_oauth2_manage_priv"],
|
||||
"uuid": ["00000000-0000-0000-0000-000000000027"],
|
||||
"description": ["Builtin IDM Group for managing oauth2 resource server integrations to this authentication domain."],
|
||||
"member": [
|
||||
"00000000-0000-0000-0000-000000000019"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
||||
// This must be the last group to init to include the UUID of the other high priv groups.
|
||||
pub const JSON_IDM_HIGH_PRIVILEGE_V1: &str = r#"{
|
||||
|
@ -324,6 +335,7 @@ pub const JSON_IDM_HIGH_PRIVILEGE_V1: &str = r#"{
|
|||
"00000000-0000-0000-0000-000000000024",
|
||||
"00000000-0000-0000-0000-000000000025",
|
||||
"00000000-0000-0000-0000-000000000026",
|
||||
"00000000-0000-0000-0000-000000000027",
|
||||
"00000000-0000-0000-0000-000000001000"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ pub use crate::constants::system_config::*;
|
|||
pub use crate::constants::uuids::*;
|
||||
|
||||
// Increment this as we add new schema types and values!!!
|
||||
pub const SYSTEM_INDEX_VERSION: i64 = 17;
|
||||
pub const SYSTEM_INDEX_VERSION: i64 = 18;
|
||||
// On test builds, define to 60 seconds
|
||||
#[cfg(test)]
|
||||
pub const PURGE_FREQUENCY: u64 = 60;
|
||||
|
|
|
@ -175,7 +175,7 @@ pub const JSON_SCHEMA_ATTR_RADIUS_SECRET: &str = r#"{
|
|||
"radius_secret"
|
||||
],
|
||||
"syntax": [
|
||||
"RADIUS_UTF8STRING"
|
||||
"SECRET_UTF8STRING"
|
||||
],
|
||||
"uuid": [
|
||||
"00000000-0000-0000-0000-ffff00000051"
|
||||
|
@ -484,6 +484,153 @@ pub const JSON_SCHEMA_ATTR_ACCOUNT_VALID_FROM: &str = r#"{
|
|||
}
|
||||
}"#;
|
||||
|
||||
pub const JSON_SCHEMA_ATTR_OAUTH2_RS_NAME: &str = r#"{
|
||||
"attrs": {
|
||||
"class": [
|
||||
"object",
|
||||
"system",
|
||||
"attributetype"
|
||||
],
|
||||
"description": [
|
||||
"The unique name of an external Oauth2 resource"
|
||||
],
|
||||
"index": [
|
||||
"EQUALITY"
|
||||
],
|
||||
"unique": [
|
||||
"true"
|
||||
],
|
||||
"multivalue": [
|
||||
"false"
|
||||
],
|
||||
"attributename": [
|
||||
"oauth2_rs_name"
|
||||
],
|
||||
"syntax": [
|
||||
"UTF8STRING_INAME"
|
||||
],
|
||||
"uuid": [
|
||||
"00000000-0000-0000-0000-ffff00000080"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
||||
pub const JSON_SCHEMA_ATTR_OAUTH2_RS_ORIGIN: &str = r#"{
|
||||
"attrs": {
|
||||
"class": [
|
||||
"object",
|
||||
"system",
|
||||
"attributetype"
|
||||
],
|
||||
"description": [
|
||||
"The origin domain of an oauth2 resource server"
|
||||
],
|
||||
"index": [],
|
||||
"unique": [
|
||||
"false"
|
||||
],
|
||||
"multivalue": [
|
||||
"false"
|
||||
],
|
||||
"attributename": [
|
||||
"oauth2_rs_origin"
|
||||
],
|
||||
"syntax": [
|
||||
"URL"
|
||||
],
|
||||
"uuid": [
|
||||
"00000000-0000-0000-0000-ffff00000081"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
||||
pub const JSON_SCHEMA_ATTR_OAUTH2_RS_ACCOUNT_FILTER: &str = r#"{
|
||||
"attrs": {
|
||||
"class": [
|
||||
"object",
|
||||
"system",
|
||||
"attributetype"
|
||||
],
|
||||
"description": [
|
||||
"A filter describing who may access the associated oauth2 resource server"
|
||||
],
|
||||
"index": [],
|
||||
"unique": [
|
||||
"false"
|
||||
],
|
||||
"multivalue": [
|
||||
"false"
|
||||
],
|
||||
"attributename": [
|
||||
"oauth2_rs_account_filter"
|
||||
],
|
||||
"syntax": [
|
||||
"JSON_FILTER"
|
||||
],
|
||||
"uuid": [
|
||||
"00000000-0000-0000-0000-ffff00000082"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
||||
pub const JSON_SCHEMA_ATTR_OAUTH2_RS_BASIC_SECRET: &str = r#"{
|
||||
"attrs": {
|
||||
"class": [
|
||||
"object",
|
||||
"system",
|
||||
"attributetype"
|
||||
],
|
||||
"description": [
|
||||
"When using oauth2 basic authentication, the secret string of the resource server"
|
||||
],
|
||||
"index": [],
|
||||
"unique": [
|
||||
"false"
|
||||
],
|
||||
"multivalue": [
|
||||
"false"
|
||||
],
|
||||
"attributename": [
|
||||
"oauth2_rs_basic_secret"
|
||||
],
|
||||
"syntax": [
|
||||
"UTF8STRING"
|
||||
],
|
||||
"uuid": [
|
||||
"00000000-0000-0000-0000-ffff00000083"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
||||
pub const JSON_SCHEMA_ATTR_OAUTH2_RS_BASIC_TOKEN_KEY: &str = r#"{
|
||||
"attrs": {
|
||||
"class": [
|
||||
"object",
|
||||
"system",
|
||||
"attributetype"
|
||||
],
|
||||
"description": [
|
||||
"An oauth2 basic resource servers unique token signing key"
|
||||
],
|
||||
"index": [],
|
||||
"unique": [
|
||||
"false"
|
||||
],
|
||||
"multivalue": [
|
||||
"false"
|
||||
],
|
||||
"attributename": [
|
||||
"oauth2_rs_basic_token_key"
|
||||
],
|
||||
"syntax": [
|
||||
"SECRET_UTF8STRING"
|
||||
],
|
||||
"uuid": [
|
||||
"00000000-0000-0000-0000-ffff00000084"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
||||
// === classes ===
|
||||
|
||||
pub const JSON_SCHEMA_CLASS_PERSON: &str = r#"
|
||||
|
@ -686,3 +833,58 @@ pub const JSON_SCHEMA_CLASS_SYSTEM_CONFIG: &str = r#"
|
|||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
pub const JSON_SCHEMA_CLASS_OAUTH2_RS: &str = r#"
|
||||
{
|
||||
"attrs": {
|
||||
"class": [
|
||||
"object",
|
||||
"system",
|
||||
"classtype"
|
||||
],
|
||||
"description": [
|
||||
"The class representing a configured Oauth2 Resource Server"
|
||||
],
|
||||
"classname": [
|
||||
"oauth2_resource_server"
|
||||
],
|
||||
"systemmay": [
|
||||
"description",
|
||||
"oauth2_rs_account_filter"
|
||||
],
|
||||
"systemmust": [
|
||||
"oauth2_rs_name",
|
||||
"oauth2_rs_origin"
|
||||
],
|
||||
"uuid": [
|
||||
"00000000-0000-0000-0000-ffff00000085"
|
||||
]
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
pub const JSON_SCHEMA_CLASS_OAUTH2_RS_BASIC: &str = r#"
|
||||
{
|
||||
"attrs": {
|
||||
"class": [
|
||||
"object",
|
||||
"system",
|
||||
"classtype"
|
||||
],
|
||||
"description": [
|
||||
"The class representing a configured Oauth2 Resource Server"
|
||||
],
|
||||
"classname": [
|
||||
"oauth2_resource_server_basic"
|
||||
],
|
||||
"systemmay": [],
|
||||
"systemmust": [
|
||||
"oauth2_rs_basic_secret",
|
||||
"oauth2_rs_basic_token_key"
|
||||
],
|
||||
"uuid": [
|
||||
"00000000-0000-0000-0000-ffff00000086"
|
||||
]
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
|
|
@ -21,7 +21,7 @@ pub const _STR_UUID_IDM_ACCOUNT_MANAGE_PRIV: &str = "00000000-0000-0000-0000-000
|
|||
pub const _STR_UUID_IDM_GROUP_MANAGE_PRIV: &str = "00000000-0000-0000-0000-000000000015";
|
||||
pub const _STR_UUID_IDM_HP_ACCOUNT_MANAGE_PRIV: &str = "00000000-0000-0000-0000-000000000016";
|
||||
pub const _STR_UUID_IDM_HP_GROUP_MANAGE_PRIV: &str = "00000000-0000-0000-0000-000000000017";
|
||||
pub const _STR_UUID_IDM_ADMIN_V1: &str = "00000000-0000-0000-0000-000000000018";
|
||||
pub const STR_UUID_IDM_ADMIN_V1: &str = "00000000-0000-0000-0000-000000000018";
|
||||
pub const _STR_UUID_SYSTEM_ADMINS: &str = "00000000-0000-0000-0000-000000000019";
|
||||
pub const STR_UUID_DOMAIN_ADMINS: &str = "00000000-0000-0000-0000-000000000020";
|
||||
pub const _STR_UUID_IDM_ACCOUNT_UNIX_EXTEND_PRIV: &str = "00000000-0000-0000-0000-000000000021";
|
||||
|
@ -33,6 +33,8 @@ pub const _STR_UUID_IDM_PEOPLE_EXTEND_PRIV: &str = "00000000-0000-0000-0000-0000
|
|||
pub const _STR_UUID_IDM_HP_ACCOUNT_UNIX_EXTEND_PRIV: &str = "00000000-0000-0000-0000-000000000025";
|
||||
pub const _STR_UUID_IDM_HP_GROUP_UNIX_EXTEND_PRIV: &str = "00000000-0000-0000-0000-000000000026";
|
||||
|
||||
pub const _STR_UUID_IDM_HP_OAUTH2_MANAGE_PRIV: &str = "00000000-0000-0000-0000-000000000027";
|
||||
|
||||
//
|
||||
pub const _STR_UUID_IDM_HIGH_PRIVILEGE: &str = "00000000-0000-0000-0000-000000001000";
|
||||
|
||||
|
@ -130,6 +132,18 @@ pub const STR_UUID_SCHEMA_ATTR_KEYS: &str = "00000000-0000-0000-0000-ffff0000007
|
|||
pub const STR_UUID_SCHEMA_ATTR_SSHPUBLICKEY: &str = "00000000-0000-0000-0000-ffff00000078";
|
||||
pub const STR_UUID_SCHEMA_ATTR_CN: &str = "00000000-0000-0000-0000-ffff00000078";
|
||||
pub const STR_UUID_SCHEMA_ATTR_UIDNUMBER: &str = "00000000-0000-0000-0000-ffff00000079";
|
||||
|
||||
pub const _STR_UUID_SCHEMA_ATTR_OAUTH2_RS_NAME: &str = "00000000-0000-0000-0000-ffff00000080";
|
||||
pub const _STR_UUID_SCHEMA_ATTR_OAUTH2_RS_ORIGIN: &str = "00000000-0000-0000-0000-ffff00000081";
|
||||
pub const _STR_UUID_SCHEMA_ATTR_OAUTH2_RS_ACCOUNT_FILTER: &str =
|
||||
"00000000-0000-0000-0000-ffff00000082";
|
||||
pub const _STR_UUID_SCHEMA_ATTR_OAUTH2_RS_BASIC_SECRET: &str =
|
||||
"00000000-0000-0000-0000-ffff00000083";
|
||||
pub const _STR_UUID_SCHEMA_ATTR_OAUTH2_RS_BASIC_TOKEN_KEY: &str =
|
||||
"00000000-0000-0000-0000-ffff00000084";
|
||||
pub const STR_UUID_SCHEMA_CLASS_OAUTH2_RS: &str = "00000000-0000-0000-0000-ffff00000085";
|
||||
pub const STR_UUID_SCHEMA_CLASS_OAUTH2_RS_BASIC: &str = "00000000-0000-0000-0000-ffff00000086";
|
||||
|
||||
// System and domain infos
|
||||
// I'd like to strongly criticise william of the past for making poor choices about these allocations.
|
||||
pub const STR_UUID_SYSTEM_INFO: &str = "00000000-0000-0000-0000-ffffff000001";
|
||||
|
@ -178,6 +192,7 @@ pub const _STR_UUID_IDM_HP_ACP_ACCOUNT_UNIX_EXTEND_PRIV_V1: &str =
|
|||
"00000000-0000-0000-0000-ffffff000033";
|
||||
pub const _STR_UUID_IDM_HP_ACP_GROUP_UNIX_EXTEND_PRIV_V1: &str =
|
||||
"00000000-0000-0000-0000-ffffff000034";
|
||||
pub const _STR_UUID_IDM_HP_ACP_OAUTH2_MANAGE_PRIV_V1: &str = "00000000-0000-0000-0000-ffffff000035";
|
||||
|
||||
// End of system ranges
|
||||
pub const STR_UUID_DOES_NOT_EXIST: &str = "00000000-0000-0000-0000-fffffffffffe";
|
||||
|
@ -185,6 +200,7 @@ pub const STR_UUID_ANONYMOUS: &str = "00000000-0000-0000-0000-ffffffffffff";
|
|||
|
||||
lazy_static! {
|
||||
pub static ref UUID_ADMIN: Uuid = Uuid::parse_str(STR_UUID_ADMIN).unwrap();
|
||||
pub static ref UUID_IDM_ADMIN: Uuid = Uuid::parse_str(STR_UUID_IDM_ADMIN_V1).unwrap();
|
||||
pub static ref UUID_DOES_NOT_EXIST: Uuid = Uuid::parse_str(STR_UUID_DOES_NOT_EXIST).unwrap();
|
||||
pub static ref UUID_ANONYMOUS: Uuid = Uuid::parse_str(STR_UUID_ANONYMOUS).unwrap();
|
||||
pub static ref UUID_SYSTEM_CONFIG: Uuid = Uuid::parse_str(STR_UUID_SYSTEM_CONFIG).unwrap();
|
||||
|
@ -300,4 +316,8 @@ lazy_static! {
|
|||
pub static ref UUID_SCHEMA_ATTR_CN: Uuid = Uuid::parse_str(STR_UUID_SCHEMA_ATTR_CN).unwrap();
|
||||
pub static ref UUID_SCHEMA_ATTR_UIDNUMBER: Uuid =
|
||||
Uuid::parse_str(STR_UUID_SCHEMA_ATTR_UIDNUMBER).unwrap();
|
||||
pub static ref UUID_SCHEMA_CLASS_OAUTH2_RS: Uuid =
|
||||
Uuid::parse_str(STR_UUID_SCHEMA_CLASS_OAUTH2_RS).unwrap();
|
||||
pub static ref UUID_SCHEMA_CLASS_OAUTH2_RS_BASIC: Uuid =
|
||||
Uuid::parse_str(STR_UUID_SCHEMA_CLASS_OAUTH2_RS_BASIC).unwrap();
|
||||
}
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
mod oauth2;
|
||||
|
||||
use self::oauth2::*;
|
||||
|
||||
use crate::actors::v1_read::QueryServerReadV1;
|
||||
use crate::actors::v1_write::QueryServerWriteV1;
|
||||
use crate::config::{ServerRole, TlsConfiguration};
|
||||
use crate::event::AuthResult;
|
||||
use crate::filter::{Filter, FilterInvalid};
|
||||
use crate::idm::AuthState;
|
||||
use crate::prelude::*;
|
||||
use crate::status::{StatusActor, StatusRequestEvent};
|
||||
use crate::value::PartialValue;
|
||||
|
||||
use kanidm_proto::v1::Entry as ProtoEntry;
|
||||
use kanidm_proto::v1::{
|
||||
|
@ -42,6 +46,8 @@ pub trait RequestExtensions {
|
|||
fn get_current_auth_session_id(&self) -> Option<Uuid>;
|
||||
|
||||
fn get_url_param(&self, param: &str) -> Result<String, tide::Error>;
|
||||
|
||||
fn new_eventid(&self) -> (Uuid, String);
|
||||
}
|
||||
|
||||
impl RequestExtensions for tide::Request<AppState> {
|
||||
|
@ -59,6 +65,7 @@ impl RequestExtensions for tide::Request<AppState> {
|
|||
h.as_str().strip_prefix("Bearer ")
|
||||
})
|
||||
.map(|s| s.to_string())
|
||||
.or_else(|| self.session().get::<String>("bearer"))
|
||||
/*
|
||||
.and_then(|ts| {
|
||||
// Take the token str and attempt to decrypt
|
||||
|
@ -91,9 +98,15 @@ impl RequestExtensions for tide::Request<AppState> {
|
|||
|
||||
fn get_url_param(&self, param: &str) -> Result<String, tide::Error> {
|
||||
self.param(param)
|
||||
.map(|s| s.to_string())
|
||||
.map(str::to_string)
|
||||
.map_err(|_| tide::Error::from_str(tide::StatusCode::ImATeapot, "teapot"))
|
||||
}
|
||||
|
||||
fn new_eventid(&self) -> (Uuid, String) {
|
||||
let eventid = Uuid::new_v4();
|
||||
let hv = eventid.to_hyphenated().to_string();
|
||||
(eventid, hv)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_tide_response<T: Serialize>(
|
||||
|
@ -135,14 +148,6 @@ pub fn to_tide_response<T: Serialize>(
|
|||
})
|
||||
}
|
||||
|
||||
macro_rules! new_eventid {
|
||||
() => {{
|
||||
let eventid = Uuid::new_v4();
|
||||
let hv = eventid.to_hyphenated().to_string();
|
||||
(eventid, hv)
|
||||
}};
|
||||
}
|
||||
|
||||
// Handle the various end points we need to expose
|
||||
async fn index_view(_req: tide::Request<AppState>) -> tide::Result {
|
||||
let mut res = tide::Response::new(200);
|
||||
|
@ -177,7 +182,7 @@ pub async fn create(mut req: tide::Request<AppState>) -> tide::Result {
|
|||
// parse the req to a CreateRequest
|
||||
let msg: CreateRequest = req.body_json().await?;
|
||||
|
||||
let (eventid, hvalue) = new_eventid!();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
|
||||
let res = req.state().qe_w_ref.handle_create(uat, msg, eventid).await;
|
||||
to_tide_response(res, hvalue)
|
||||
|
@ -186,7 +191,7 @@ pub async fn create(mut req: tide::Request<AppState>) -> tide::Result {
|
|||
pub async fn modify(mut req: tide::Request<AppState>) -> tide::Result {
|
||||
let uat = req.get_current_uat();
|
||||
let msg: ModifyRequest = req.body_json().await?;
|
||||
let (eventid, hvalue) = new_eventid!();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
let res = req.state().qe_w_ref.handle_modify(uat, msg, eventid).await;
|
||||
to_tide_response(res, hvalue)
|
||||
}
|
||||
|
@ -194,7 +199,7 @@ pub async fn modify(mut req: tide::Request<AppState>) -> tide::Result {
|
|||
pub async fn delete(mut req: tide::Request<AppState>) -> tide::Result {
|
||||
let uat = req.get_current_uat();
|
||||
let msg: DeleteRequest = req.body_json().await?;
|
||||
let (eventid, hvalue) = new_eventid!();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
let res = req.state().qe_w_ref.handle_delete(uat, msg, eventid).await;
|
||||
to_tide_response(res, hvalue)
|
||||
}
|
||||
|
@ -202,14 +207,14 @@ pub async fn delete(mut req: tide::Request<AppState>) -> tide::Result {
|
|||
pub async fn search(mut req: tide::Request<AppState>) -> tide::Result {
|
||||
let uat = req.get_current_uat();
|
||||
let msg: SearchRequest = req.body_json().await?;
|
||||
let (eventid, hvalue) = new_eventid!();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
let res = req.state().qe_r_ref.handle_search(uat, msg, eventid).await;
|
||||
to_tide_response(res, hvalue)
|
||||
}
|
||||
|
||||
pub async fn whoami(req: tide::Request<AppState>) -> tide::Result {
|
||||
let uat = req.get_current_uat();
|
||||
let (eventid, hvalue) = new_eventid!();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
// New event, feed current auth data from the token to it.
|
||||
let res = req.state().qe_r_ref.handle_whoami(uat, eventid).await;
|
||||
to_tide_response(res, hvalue)
|
||||
|
@ -224,7 +229,7 @@ pub async fn json_rest_event_get(
|
|||
) -> tide::Result {
|
||||
let uat = req.get_current_uat();
|
||||
|
||||
let (eventid, hvalue) = new_eventid!();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
|
||||
let res = req
|
||||
.state()
|
||||
|
@ -244,7 +249,7 @@ async fn json_rest_event_get_id(
|
|||
|
||||
let filter = Filter::join_parts_and(filter, filter_all!(f_id(id.as_str())));
|
||||
|
||||
let (eventid, hvalue) = new_eventid!();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
|
||||
let res = req
|
||||
.state()
|
||||
|
@ -263,7 +268,7 @@ async fn json_rest_event_delete_id(
|
|||
let id = req.get_url_param("id")?;
|
||||
|
||||
let filter = Filter::join_parts_and(filter, filter_all!(f_id(id.as_str())));
|
||||
let (eventid, hvalue) = new_eventid!();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
|
||||
let res = req
|
||||
.state()
|
||||
|
@ -282,7 +287,7 @@ async fn json_rest_event_get_id_attr(
|
|||
let uat = req.get_current_uat();
|
||||
|
||||
let filter = Filter::join_parts_and(filter, filter_all!(f_id(id.as_str())));
|
||||
let (eventid, hvalue) = new_eventid!();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
|
||||
let attrs = Some(vec![attr.clone()]);
|
||||
|
||||
|
@ -300,7 +305,7 @@ async fn json_rest_event_post(
|
|||
classes: Vec<String>,
|
||||
) -> tide::Result {
|
||||
debug_assert!(!classes.is_empty());
|
||||
let (eventid, hvalue) = new_eventid!();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
// Read the json from the wire.
|
||||
let uat = req.get_current_uat();
|
||||
let mut obj: ProtoEntry = req.body_json().await?;
|
||||
|
@ -319,7 +324,7 @@ async fn json_rest_event_post_id_attr(
|
|||
let uuid_or_name = req.get_url_param("id")?;
|
||||
let attr = req.get_url_param("attr")?;
|
||||
let values: Vec<String> = req.body_json().await?;
|
||||
let (eventid, hvalue) = new_eventid!();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
let res = req
|
||||
.state()
|
||||
.qe_w_ref
|
||||
|
@ -337,7 +342,7 @@ async fn json_rest_event_put_id_attr(
|
|||
let attr = req.get_url_param("attr")?;
|
||||
let values: Vec<String> = req.body_json().await?;
|
||||
|
||||
let (eventid, hvalue) = new_eventid!();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
let res = req
|
||||
.state()
|
||||
.qe_w_ref
|
||||
|
@ -354,7 +359,7 @@ async fn json_rest_event_delete_id_attr(
|
|||
) -> tide::Result {
|
||||
let uat = req.get_current_uat();
|
||||
let uuid_or_name = req.get_url_param("id")?;
|
||||
let (eventid, hvalue) = new_eventid!();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
|
||||
// TODO #211: Attempt to get an option Vec<String> here?
|
||||
// It's probably better to focus on SCIM instead, it seems richer than this.
|
||||
|
@ -388,7 +393,7 @@ async fn json_rest_event_credential_put(mut req: tide::Request<AppState>) -> tid
|
|||
let uuid_or_name = req.get_url_param("id")?;
|
||||
let sac: SetCredentialRequest = req.body_json().await?;
|
||||
|
||||
let (eventid, hvalue) = new_eventid!();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
let res = req
|
||||
.state()
|
||||
.qe_w_ref
|
||||
|
@ -435,7 +440,7 @@ pub async fn schema_attributetype_get_id(req: tide::Request<AppState>) -> tide::
|
|||
f_eq("attributename", PartialValue::new_iutf8(id.as_str()))
|
||||
]));
|
||||
|
||||
let (eventid, hvalue) = new_eventid!();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
|
||||
let res = req
|
||||
.state()
|
||||
|
@ -461,7 +466,7 @@ pub async fn schema_classtype_get_id(req: tide::Request<AppState>) -> tide::Resu
|
|||
f_eq("classname", PartialValue::new_iutf8(id.as_str()))
|
||||
]));
|
||||
|
||||
let (eventid, hvalue) = new_eventid!();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
|
||||
let res = req
|
||||
.state()
|
||||
|
@ -540,7 +545,7 @@ pub async fn account_get_id_credential_status(req: tide::Request<AppState>) -> t
|
|||
let uat = req.get_current_uat();
|
||||
let uuid_or_name = req.get_url_param("id")?;
|
||||
|
||||
let (eventid, hvalue) = new_eventid!();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
|
||||
let res = req
|
||||
.state()
|
||||
|
@ -554,7 +559,7 @@ pub async fn account_get_backup_code(req: tide::Request<AppState>) -> tide::Resu
|
|||
let uat = req.get_current_uat();
|
||||
let uuid_or_name = req.get_url_param("id")?;
|
||||
|
||||
let (eventid, hvalue) = new_eventid!();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
|
||||
let res = req
|
||||
.state()
|
||||
|
@ -569,7 +574,7 @@ pub async fn account_get_id_ssh_pubkeys(req: tide::Request<AppState>) -> tide::R
|
|||
let uat = req.get_current_uat();
|
||||
let uuid_or_name = req.get_url_param("id")?;
|
||||
|
||||
let (eventid, hvalue) = new_eventid!();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
|
||||
let res = req
|
||||
.state()
|
||||
|
@ -585,7 +590,7 @@ pub async fn account_post_id_ssh_pubkey(mut req: tide::Request<AppState>) -> tid
|
|||
let (tag, key): (String, String) = req.body_json().await?;
|
||||
let filter = filter_all!(f_eq("class", PartialValue::new_class("account")));
|
||||
|
||||
let (eventid, hvalue) = new_eventid!();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
// Add a msg here
|
||||
let res = req
|
||||
.state()
|
||||
|
@ -600,7 +605,7 @@ pub async fn account_get_id_ssh_pubkey_tag(req: tide::Request<AppState>) -> tide
|
|||
let uuid_or_name = req.get_url_param("id")?;
|
||||
let tag = req.get_url_param("tag")?;
|
||||
|
||||
let (eventid, hvalue) = new_eventid!();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
|
||||
let res = req
|
||||
.state()
|
||||
|
@ -618,7 +623,7 @@ pub async fn account_delete_id_ssh_pubkey_tag(req: tide::Request<AppState>) -> t
|
|||
let values = vec![tag];
|
||||
let filter = filter_all!(f_eq("class", PartialValue::new_class("account")));
|
||||
|
||||
let (eventid, hvalue) = new_eventid!();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
|
||||
let res = req
|
||||
.state()
|
||||
|
@ -633,7 +638,7 @@ pub async fn account_get_id_radius(req: tide::Request<AppState>) -> tide::Result
|
|||
let uat = req.get_current_uat();
|
||||
let uuid_or_name = req.get_url_param("id")?;
|
||||
|
||||
let (eventid, hvalue) = new_eventid!();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
|
||||
let res = req
|
||||
.state()
|
||||
|
@ -648,7 +653,7 @@ pub async fn account_post_id_radius_regenerate(req: tide::Request<AppState>) ->
|
|||
let uat = req.get_current_uat();
|
||||
let uuid_or_name = req.get_url_param("id")?;
|
||||
|
||||
let (eventid, hvalue) = new_eventid!();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
|
||||
let res = req
|
||||
.state()
|
||||
|
@ -668,7 +673,7 @@ pub async fn account_get_id_radius_token(req: tide::Request<AppState>) -> tide::
|
|||
let uat = req.get_current_uat();
|
||||
let uuid_or_name = req.get_url_param("id")?;
|
||||
|
||||
let (eventid, hvalue) = new_eventid!();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
|
||||
let res = req
|
||||
.state()
|
||||
|
@ -681,7 +686,7 @@ pub async fn account_get_id_radius_token(req: tide::Request<AppState>) -> tide::
|
|||
pub async fn account_post_id_person_extend(req: tide::Request<AppState>) -> tide::Result {
|
||||
let uat = req.get_current_uat();
|
||||
let uuid_or_name = req.get_url_param("id")?;
|
||||
let (eventid, hvalue) = new_eventid!();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
let res = req
|
||||
.state()
|
||||
.qe_w_ref
|
||||
|
@ -694,7 +699,7 @@ pub async fn account_post_id_unix(mut req: tide::Request<AppState>) -> tide::Res
|
|||
let uat = req.get_current_uat();
|
||||
let uuid_or_name = req.get_url_param("id")?;
|
||||
let obj: AccountUnixExtend = req.body_json().await?;
|
||||
let (eventid, hvalue) = new_eventid!();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
let res = req
|
||||
.state()
|
||||
.qe_w_ref
|
||||
|
@ -707,7 +712,7 @@ pub async fn account_get_id_unix_token(req: tide::Request<AppState>) -> tide::Re
|
|||
let uat = req.get_current_uat();
|
||||
let uuid_or_name = req.get_url_param("id")?;
|
||||
|
||||
let (eventid, hvalue) = new_eventid!();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
|
||||
let res = req
|
||||
.state()
|
||||
|
@ -722,7 +727,7 @@ pub async fn account_post_id_unix_auth(mut req: tide::Request<AppState>) -> tide
|
|||
let uuid_or_name = req.get_url_param("id")?;
|
||||
let obj: SingleStringRequest = req.body_json().await?;
|
||||
let cred = obj.value;
|
||||
let (eventid, hvalue) = new_eventid!();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
let res = req
|
||||
.state()
|
||||
.qe_r_ref
|
||||
|
@ -736,7 +741,7 @@ pub async fn account_put_id_unix_credential(mut req: tide::Request<AppState>) ->
|
|||
let uuid_or_name = req.get_url_param("id")?;
|
||||
let obj: SingleStringRequest = req.body_json().await?;
|
||||
let cred = obj.value;
|
||||
let (eventid, hvalue) = new_eventid!();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
let res = req
|
||||
.state()
|
||||
.qe_w_ref
|
||||
|
@ -751,7 +756,7 @@ pub async fn account_delete_id_unix_credential(req: tide::Request<AppState>) ->
|
|||
let attr = "unix_password".to_string();
|
||||
let filter = filter_all!(f_eq("class", PartialValue::new_class("posixaccount")));
|
||||
|
||||
let (eventid, hvalue) = new_eventid!();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
|
||||
let res = req
|
||||
.state()
|
||||
|
@ -806,7 +811,7 @@ pub async fn group_post_id_unix(mut req: tide::Request<AppState>) -> tide::Resul
|
|||
let uat = req.get_current_uat();
|
||||
let uuid_or_name = req.get_url_param("id")?;
|
||||
let obj: GroupUnixExtend = req.body_json().await?;
|
||||
let (eventid, hvalue) = new_eventid!();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
let res = req
|
||||
.state()
|
||||
.qe_w_ref
|
||||
|
@ -819,7 +824,7 @@ pub async fn group_get_id_unix_token(req: tide::Request<AppState>) -> tide::Resu
|
|||
let uat = req.get_current_uat();
|
||||
let uuid_or_name = req.get_url_param("id")?;
|
||||
|
||||
let (eventid, hvalue) = new_eventid!();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
|
||||
let res = req
|
||||
.state()
|
||||
|
@ -857,7 +862,7 @@ pub async fn recycle_bin_get(req: tide::Request<AppState>) -> tide::Result {
|
|||
let uat = req.get_current_uat();
|
||||
let attrs = None;
|
||||
|
||||
let (eventid, hvalue) = new_eventid!();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
|
||||
let res = req
|
||||
.state()
|
||||
|
@ -873,7 +878,7 @@ pub async fn recycle_bin_id_get(req: tide::Request<AppState>) -> tide::Result {
|
|||
let filter = filter_all!(f_id(id.as_str()));
|
||||
let attrs = None;
|
||||
|
||||
let (eventid, hvalue) = new_eventid!();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
|
||||
let res = req
|
||||
.state()
|
||||
|
@ -889,7 +894,7 @@ pub async fn recycle_bin_revive_id_post(req: tide::Request<AppState>) -> tide::R
|
|||
let id = req.get_url_param("id")?;
|
||||
let filter = filter_all!(f_id(id.as_str()));
|
||||
|
||||
let (eventid, hvalue) = new_eventid!();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
let res = req
|
||||
.state()
|
||||
.qe_w_ref
|
||||
|
@ -908,7 +913,7 @@ pub async fn auth(mut req: tide::Request<AppState>) -> tide::Result {
|
|||
// First, deal with some state management.
|
||||
// Do anything here first that's needed like getting the session details
|
||||
// out of the req cookie.
|
||||
let (eventid, hvalue) = new_eventid!();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
|
||||
let maybe_sessionid = req.get_current_auth_session_id();
|
||||
debug!("🍿 {:?}", maybe_sessionid);
|
||||
|
@ -987,7 +992,12 @@ pub async fn auth(mut req: tide::Request<AppState>) -> tide::Result {
|
|||
// Remove the auth-session-id
|
||||
let msession = req.session_mut();
|
||||
msession.remove("auth-session-id");
|
||||
Ok(ProtoAuthState::Success(token))
|
||||
// Create a session cookie?
|
||||
msession.remove("bearer");
|
||||
msession
|
||||
.insert("bearer", token.clone())
|
||||
.map_err(|_| OperationError::InvalidSessionState)
|
||||
.map(|_| ProtoAuthState::Success(token))
|
||||
}
|
||||
AuthState::Denied(reason) => {
|
||||
debug!("🧩 -> AuthState::Denied");
|
||||
|
@ -1016,7 +1026,7 @@ pub async fn idm_account_set_password(mut req: tide::Request<AppState>) -> tide:
|
|||
let uat = req.get_current_uat();
|
||||
let obj: SingleStringRequest = req.body_json().await?;
|
||||
let cleartext = obj.value;
|
||||
let (eventid, hvalue) = new_eventid!();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
let res = req
|
||||
.state()
|
||||
.qe_w_ref
|
||||
|
@ -1029,7 +1039,7 @@ pub async fn idm_account_set_password(mut req: tide::Request<AppState>) -> tide:
|
|||
|
||||
pub async fn status(req: tide::Request<AppState>) -> tide::Result {
|
||||
// We ignore the body in this req
|
||||
let (eventid, hvalue) = new_eventid!();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
let r = req
|
||||
.state()
|
||||
.status_ref
|
||||
|
@ -1209,6 +1219,20 @@ pub fn create_https_server(
|
|||
.at("/openid-configuration")
|
||||
.get(get_openid_configuration);
|
||||
|
||||
// == oauth endpoints.
|
||||
|
||||
let mut oauth2_process = tserver.at("/oauth2");
|
||||
oauth2_process.at("/authorise").get(oauth2_authorise_get);
|
||||
oauth2_process
|
||||
.at("/authorise/permit")
|
||||
.get(oauth2_authorise_permit_get);
|
||||
oauth2_process.at("/token").post(oauth2_token_post);
|
||||
/*
|
||||
oauth2_process
|
||||
.at("/token/introspect")
|
||||
.get(oauth2_token_introspect_get);
|
||||
*/
|
||||
|
||||
let mut raw_route = tserver.at("/v1/raw");
|
||||
raw_route.at("/create").post(create);
|
||||
raw_route.at("/modify").post(modify);
|
||||
|
@ -1239,6 +1263,19 @@ pub fn create_https_server(
|
|||
.put(do_nothing)
|
||||
.patch(do_nothing);
|
||||
|
||||
let mut oauth2_route = tserver.at("/v1/oauth2");
|
||||
oauth2_route.at("/").get(oauth2_get);
|
||||
|
||||
oauth2_route.at("/_basic").post(oauth2_basic_post);
|
||||
|
||||
oauth2_route
|
||||
.at("/:id")
|
||||
.get(oauth2_id_get)
|
||||
// It's not really possible to replace this wholesale.
|
||||
// .put(oauth2_id_put)
|
||||
.patch(oauth2_id_patch)
|
||||
.delete(oauth2_id_delete);
|
||||
|
||||
let mut self_route = tserver.at("/v1/self");
|
||||
self_route.at("/").get(whoami);
|
||||
|
340
kanidmd/src/lib/core/https/oauth2.rs
Normal file
340
kanidmd/src/lib/core/https/oauth2.rs
Normal file
|
@ -0,0 +1,340 @@
|
|||
use super::{
|
||||
json_rest_event_get, json_rest_event_post, to_tide_response, AppState, RequestExtensions,
|
||||
};
|
||||
use crate::idm::oauth2::{
|
||||
AccessTokenRequest, AuthorisationRequest, AuthorisePermitSuccess, ErrorResponse, Oauth2Error,
|
||||
};
|
||||
use crate::prelude::*;
|
||||
use kanidm_proto::v1::Entry as ProtoEntry;
|
||||
|
||||
// == Oauth2 Configuration Endpoints ==
|
||||
|
||||
pub async fn oauth2_get(req: tide::Request<AppState>) -> tide::Result {
|
||||
let filter = filter_all!(f_eq(
|
||||
"class",
|
||||
PartialValue::new_class("oauth2_resource_server")
|
||||
));
|
||||
json_rest_event_get(req, filter, None).await
|
||||
}
|
||||
|
||||
pub async fn oauth2_basic_post(req: tide::Request<AppState>) -> tide::Result {
|
||||
let classes = vec![
|
||||
"oauth2_resource_server".to_string(),
|
||||
"oauth2_resource_server_basic".to_string(),
|
||||
"object".to_string(),
|
||||
];
|
||||
json_rest_event_post(req, classes).await
|
||||
}
|
||||
|
||||
fn oauth2_id(id: &str) -> Filter<FilterInvalid> {
|
||||
filter_all!(f_and!([
|
||||
f_eq("class", PartialValue::new_class("oauth2_resource_server")),
|
||||
f_eq("oauth2_rs_name", PartialValue::new_iname(id))
|
||||
]))
|
||||
}
|
||||
|
||||
pub async fn oauth2_id_get(req: tide::Request<AppState>) -> tide::Result {
|
||||
// Get a specific config
|
||||
let uat = req.get_current_uat();
|
||||
let id = req.get_url_param("id")?;
|
||||
|
||||
let filter = oauth2_id(&id);
|
||||
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
|
||||
let res = req
|
||||
.state()
|
||||
.qe_r_ref
|
||||
.handle_internalsearch(uat, filter, None, eventid)
|
||||
.await
|
||||
.map(|mut r| r.pop());
|
||||
to_tide_response(res, hvalue)
|
||||
}
|
||||
|
||||
pub async fn oauth2_id_patch(mut req: tide::Request<AppState>) -> tide::Result {
|
||||
// Update a value / attrs
|
||||
let uat = req.get_current_uat();
|
||||
let id = req.get_url_param("id")?;
|
||||
|
||||
let obj: ProtoEntry = req.body_json().await?;
|
||||
|
||||
let filter = oauth2_id(&id);
|
||||
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
|
||||
let res = req
|
||||
.state()
|
||||
.qe_w_ref
|
||||
.handle_internalpatch(uat, filter, obj, eventid)
|
||||
.await;
|
||||
to_tide_response(res, hvalue)
|
||||
}
|
||||
|
||||
pub async fn oauth2_id_delete(req: tide::Request<AppState>) -> tide::Result {
|
||||
// Delete this
|
||||
let uat = req.get_current_uat();
|
||||
let id = req.get_url_param("id")?;
|
||||
|
||||
let filter = oauth2_id(&id);
|
||||
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
|
||||
let res = req
|
||||
.state()
|
||||
.qe_w_ref
|
||||
.handle_internaldelete(uat, filter, eventid)
|
||||
.await;
|
||||
to_tide_response(res, hvalue)
|
||||
}
|
||||
|
||||
// == OAUTH2 PROTOCOL FLOW HANDLERS ==
|
||||
|
||||
// oauth2 (partial)
|
||||
// https://tools.ietf.org/html/rfc6749
|
||||
// oauth2 pkce
|
||||
// https://tools.ietf.org/html/rfc7636
|
||||
|
||||
// TODO
|
||||
// oauth2 token introspection
|
||||
// https://tools.ietf.org/html/rfc7662
|
||||
// oauth2 bearer token
|
||||
// https://tools.ietf.org/html/rfc6750
|
||||
|
||||
// From https://datatracker.ietf.org/doc/html/rfc6749#section-4.1
|
||||
//
|
||||
// +----------+
|
||||
// | Resource |
|
||||
// | Owner |
|
||||
// | |
|
||||
// +----------+
|
||||
// ^
|
||||
// |
|
||||
// (B)
|
||||
// +----|-----+ Client Identifier +---------------+
|
||||
// | -+----(A)-- & Redirection URI ---->| |
|
||||
// | User- | | Authorization |
|
||||
// | Agent -+----(B)-- User authenticates --->| Server |
|
||||
// | | | |
|
||||
// | -+----(C)-- Authorization Code ---<| |
|
||||
// +-|----|---+ +---------------+
|
||||
// | | ^ v
|
||||
// (A) (C) | |
|
||||
// | | | |
|
||||
// ^ v | |
|
||||
// +---------+ | |
|
||||
// | |>---(D)-- Authorization Code ---------' |
|
||||
// | Client | & Redirection URI |
|
||||
// | | |
|
||||
// | |<---(E)----- Access Token -------------------'
|
||||
// +---------+ (w/ Optional Refresh Token)
|
||||
//
|
||||
// Note: The lines illustrating steps (A), (B), and (C) are broken into
|
||||
// two parts as they pass through the user-agent.
|
||||
//
|
||||
// In this diagram, kanidm is the authorisation server. Each step is handled by:
|
||||
//
|
||||
// * Client Identifier A) oauth2_authorise_get
|
||||
// * User authenticates B) normal kanidm auth flow
|
||||
// * Authorization Code C) oauth2_authorise_permit_get
|
||||
// * Authorization Code/
|
||||
// Access Token D/E) oauth2_token_post
|
||||
//
|
||||
// These functions appear stateless, but the state is managed through encrypted
|
||||
// tokens transmitted in the responses of this flow. This is because in a HA setup
|
||||
// we can not guarantee that the User-Agent or the Resource Server (client) will
|
||||
// access the same Kanidm instance, and we can not rely on replication in these
|
||||
// cases. As a result, we must have our state in localised tokens so that any
|
||||
// valid Kanidm instance in the topology can handle these request.
|
||||
//
|
||||
|
||||
pub async fn oauth2_authorise_get(req: tide::Request<AppState>) -> tide::Result {
|
||||
// Start the oauth2 authorisation flow to present to the user.
|
||||
debug!("Request Query - {:?}", req.url().query());
|
||||
// Get the authorisation request.
|
||||
let auth_req: AuthorisationRequest = req.query().map_err(|e| {
|
||||
error!("{:?}", e);
|
||||
tide::Error::from_str(
|
||||
tide::StatusCode::BadRequest,
|
||||
"Invalid Oauth2 AuthorisationRequest",
|
||||
)
|
||||
})?;
|
||||
|
||||
let uat = req.get_current_uat();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
|
||||
let mut redir_url = auth_req.redirect_uri.clone();
|
||||
|
||||
let res = req
|
||||
.state()
|
||||
.qe_r_ref
|
||||
.handle_oauth2_authorise(uat, auth_req, eventid)
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(consent_req) => {
|
||||
// Render a redirect to the consent page for the user to interact with
|
||||
// to authorise this session-id
|
||||
let mut res = tide::Response::new(200);
|
||||
// This is json so later we can expand it with better detail.
|
||||
tide::Body::from_json(&consent_req).map(|b| {
|
||||
res.set_body(b);
|
||||
res
|
||||
})
|
||||
}
|
||||
Err(Oauth2Error::AuthenticationRequired) => {
|
||||
// This will trigger our ui to auth and retry.
|
||||
Ok(tide::Response::new(tide::StatusCode::Unauthorized))
|
||||
}
|
||||
Err(e) => {
|
||||
redir_url
|
||||
.query_pairs_mut()
|
||||
.clear()
|
||||
.append_pair(
|
||||
"error_description",
|
||||
&format!("Unable to authorise - Error ID: {}", hvalue),
|
||||
)
|
||||
.append_pair("error", &e.to_string());
|
||||
// https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1
|
||||
// Return an error, explaining why it was denied.
|
||||
let mut res = tide::Response::new(302);
|
||||
res.insert_header("Location", redir_url.as_str());
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
.map(|mut res| {
|
||||
res.insert_header("Cache-Control", "no-store");
|
||||
res.insert_header("Pragma", "no-cache");
|
||||
res.insert_header("X-KANIDM-OPID", hvalue);
|
||||
res
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
struct ConsentRequestData {
|
||||
token: String,
|
||||
}
|
||||
|
||||
pub async fn oauth2_authorise_permit_get(req: tide::Request<AppState>) -> tide::Result {
|
||||
// When this is called, this indicates consent to proceed from the user.
|
||||
debug!("Request Query - {:?}", req.url().query());
|
||||
|
||||
let consent_req: ConsentRequestData = req.query().map_err(|e| {
|
||||
error!("{:?}", e);
|
||||
tide::Error::from_str(
|
||||
tide::StatusCode::BadRequest,
|
||||
"Invalid Oauth2 Consent Permit",
|
||||
)
|
||||
})?;
|
||||
|
||||
let uat = req.get_current_uat();
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
|
||||
let res = req
|
||||
.state()
|
||||
.qe_r_ref
|
||||
.handle_oauth2_authorise_permit(uat, consent_req.token, eventid)
|
||||
.await;
|
||||
|
||||
let mut res = match res {
|
||||
Ok(AuthorisePermitSuccess {
|
||||
mut redirect_uri,
|
||||
state,
|
||||
code,
|
||||
}) => {
|
||||
let mut res = tide::Response::new(302);
|
||||
redirect_uri
|
||||
.query_pairs_mut()
|
||||
.clear()
|
||||
.append_pair("state", &state.to_string())
|
||||
.append_pair("code", &code);
|
||||
res.insert_header("Location", redirect_uri.as_str());
|
||||
res
|
||||
}
|
||||
Err(_e) => {
|
||||
// If an error happens in our consent flow, I think
|
||||
// that we should NOT redirect to the calling application
|
||||
// and we need to handle that locally somehow.
|
||||
// This needs to be better!
|
||||
tide::Response::new(500)
|
||||
}
|
||||
};
|
||||
res.insert_header("Cache-Control", "no-store");
|
||||
res.insert_header("Pragma", "no-cache");
|
||||
res.insert_header("X-KANIDM-OPID", hvalue);
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn oauth2_token_post(mut req: tide::Request<AppState>) -> tide::Result {
|
||||
// This is called directly by the resource server, where we then issue
|
||||
// the token to the caller.
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
|
||||
// Get the authz header (if present). In the future depending on the
|
||||
// type of exchanges we support, this could become an Option type.
|
||||
let client_authz = req
|
||||
.header("authorization")
|
||||
.and_then(|hv| hv.get(0))
|
||||
.and_then(|h| h.as_str().strip_prefix("Basic "))
|
||||
.map(str::to_string)
|
||||
.ok_or_else(|| {
|
||||
error!("Basic Authentication Not Provided");
|
||||
tide::Error::from_str(
|
||||
tide::StatusCode::Unauthorized,
|
||||
"Invalid Basic Authorisation",
|
||||
)
|
||||
})?;
|
||||
|
||||
// Get the accessToken Request
|
||||
let tok_req: AccessTokenRequest = req.body_form().await.map_err(|e| {
|
||||
error!("atr parse error - {:?}", e);
|
||||
tide::Error::from_str(
|
||||
tide::StatusCode::BadRequest,
|
||||
"Invalid Oauth2 AccessTokenRequest",
|
||||
)
|
||||
})?;
|
||||
|
||||
let res = req
|
||||
.state()
|
||||
.qe_r_ref
|
||||
.handle_oauth2_token_exchange(client_authz, tok_req, eventid)
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(atr) => {
|
||||
let mut res = tide::Response::new(200);
|
||||
tide::Body::from_json(&atr).map(|b| {
|
||||
res.set_body(b);
|
||||
res
|
||||
})
|
||||
}
|
||||
Err(Oauth2Error::AuthenticationRequired) => {
|
||||
// This will trigger our ui to auth and retry.
|
||||
Ok(tide::Response::new(tide::StatusCode::Unauthorized))
|
||||
}
|
||||
Err(e) => {
|
||||
// https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
|
||||
let err = ErrorResponse {
|
||||
error: e.to_string(),
|
||||
error_description: None,
|
||||
error_uri: None,
|
||||
};
|
||||
|
||||
let mut res = tide::Response::new(400);
|
||||
tide::Body::from_json(&err).map(|b| {
|
||||
res.set_body(b);
|
||||
res
|
||||
})
|
||||
}
|
||||
}
|
||||
.map(|mut res| {
|
||||
res.insert_header("Cache-Control", "no-store");
|
||||
res.insert_header("Pragma", "no-cache");
|
||||
res.insert_header("X-KANIDM-OPID", hvalue);
|
||||
res
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
pub async fn oauth2_token_introspect_get(req: tide::Request<AppState>) -> tide::Result {
|
||||
}
|
||||
*/
|
|
@ -91,7 +91,7 @@ fn setup_qs_idms(
|
|||
config: &Configuration,
|
||||
) -> Result<(QueryServer, IdmServer, IdmServerDelayed), OperationError> {
|
||||
// Create a query_server implementation
|
||||
let query_server = QueryServer::new(be, schema);
|
||||
let query_server = QueryServer::new(audit, be, schema);
|
||||
|
||||
// TODO #62: Should the IDM parts be broken out to the IdmServer?
|
||||
// What's important about this initial setup here is that it also triggers
|
||||
|
@ -380,7 +380,7 @@ pub fn verify_server_core(config: &Configuration) {
|
|||
return;
|
||||
}
|
||||
};
|
||||
let server = QueryServer::new(be, schema_mem);
|
||||
let server = QueryServer::new(&mut audit, be, schema_mem);
|
||||
|
||||
// Run verifications.
|
||||
let r = server.verify(&mut audit);
|
||||
|
|
|
@ -1694,10 +1694,9 @@ impl<VALID, STATE> Entry<VALID, STATE> {
|
|||
}
|
||||
|
||||
#[inline(always)]
|
||||
/// Return a single radius credential, if valid to transform this value.
|
||||
pub fn get_ava_single_radiuscred(&self, attr: &str) -> Option<&str> {
|
||||
self.get_ava_single(attr)
|
||||
.and_then(|a| a.get_radius_secret())
|
||||
/// Return a single secret value, if valid to transform this value.
|
||||
pub fn get_ava_single_secret(&self, attr: &str) -> Option<&str> {
|
||||
self.get_ava_single(attr).and_then(|a| a.get_secret_str())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
|
@ -1712,6 +1711,12 @@ impl<VALID, STATE> Entry<VALID, STATE> {
|
|||
self.get_ava_single(attr).and_then(|v| v.to_str())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
/// Return a single `&Url`, if valid to transform this value.
|
||||
pub fn get_ava_single_url(&self, attr: &str) -> Option<&Url> {
|
||||
self.get_ava_single(attr).and_then(|v| v.to_url())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
/// Return a single protocol filter, if valid to transform this value.
|
||||
pub fn get_ava_single_protofilter(&self, attr: &str) -> Option<&ProtoFilter> {
|
||||
|
|
|
@ -558,16 +558,11 @@ impl ModifyEvent {
|
|||
pub fn from_internal_parts(
|
||||
_audit: &mut AuditScope,
|
||||
ident: Identity,
|
||||
target_uuid: Uuid,
|
||||
ml: &ModifyList<ModifyInvalid>,
|
||||
filter: Filter<FilterInvalid>,
|
||||
filter: &Filter<FilterInvalid>,
|
||||
qs: &QueryServerWriteTransaction,
|
||||
) -> Result<Self, OperationError> {
|
||||
let f_uuid = filter_all!(f_eq("uuid", PartialValue::new_uuid(target_uuid)));
|
||||
// Add any supplemental conditions we have.
|
||||
let f = Filter::join_parts_and(f_uuid, filter);
|
||||
|
||||
let filter_orig = f
|
||||
let filter_orig = filter
|
||||
.validate(qs.get_schema())
|
||||
.map_err(OperationError::SchemaViolation)?;
|
||||
let filter = filter_orig.clone().into_ignore_hidden();
|
||||
|
|
|
@ -53,7 +53,7 @@ pub enum IdentType {
|
|||
Internal,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Hash, Ord, PartialOrd, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Hash, Ord, PartialOrd, Eq, Serialize, Deserialize)]
|
||||
/// A unique identifier of this Identity, that can be associated to various
|
||||
/// caching components.
|
||||
pub enum IdentityId {
|
||||
|
|
|
@ -67,8 +67,8 @@ macro_rules! try_from_entry {
|
|||
let expire = $value.get_ava_single_datetime("account_expire");
|
||||
|
||||
let radius_secret = $value
|
||||
.get_ava_single_radiuscred("radius_secret")
|
||||
.map(|s| s.to_string());
|
||||
.get_ava_single_secret("radius_secret")
|
||||
.map(str::to_string);
|
||||
|
||||
// Resolved by the caller
|
||||
let groups = $groups;
|
||||
|
@ -176,14 +176,14 @@ impl Account {
|
|||
Some(UserAuthToken {
|
||||
session_id,
|
||||
expiry,
|
||||
name: self.name.clone(),
|
||||
// name: self.name.clone(),
|
||||
spn: self.spn.clone(),
|
||||
displayname: self.displayname.clone(),
|
||||
// displayname: self.displayname.clone(),
|
||||
uuid: self.uuid,
|
||||
// application: None,
|
||||
groups: self.groups.iter().map(|g| g.to_proto()).collect(),
|
||||
// groups: self.groups.iter().map(|g| g.to_proto()).collect(),
|
||||
// claims: claims.iter().map(|c| c.to_proto()).collect(),
|
||||
claims: Vec::new(),
|
||||
// claims: Vec::new(),
|
||||
auth_type,
|
||||
// What's the best way to get access to these limits with regard to claims/other?
|
||||
lim_uidx: false,
|
||||
|
@ -437,7 +437,7 @@ impl Account {
|
|||
&self,
|
||||
cleartext: &str,
|
||||
) -> Result<ModifyList<ModifyInvalid>, OperationError> {
|
||||
let vcred = Value::new_radius_str(cleartext);
|
||||
let vcred = Value::new_secret_str(cleartext);
|
||||
Ok(ModifyList::new_purge_and_set("radius_secret", vcred))
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ pub(crate) mod delayed;
|
|||
pub(crate) mod event;
|
||||
pub(crate) mod group;
|
||||
pub(crate) mod mfareg;
|
||||
pub(crate) mod oauth2;
|
||||
pub(crate) mod radius;
|
||||
pub mod server;
|
||||
pub(crate) mod unix;
|
||||
|
|
1021
kanidmd/src/lib/idm/oauth2.rs
Normal file
1021
kanidmd/src/lib/idm/oauth2.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -38,7 +38,7 @@ impl RadiusAccount {
|
|||
}
|
||||
|
||||
let radius_secret = value
|
||||
.get_ava_single_radiuscred("radius_secret")
|
||||
.get_ava_single_secret("radius_secret")
|
||||
.ok_or_else(|| {
|
||||
OperationError::InvalidAccountState("Missing attribute: radius_secret".to_string())
|
||||
})?
|
||||
|
|
|
@ -13,6 +13,11 @@ use crate::idm::event::{
|
|||
UnixUserTokenEvent, VerifyTotpEvent, WebauthnDoRegisterEvent, WebauthnInitRegisterEvent,
|
||||
};
|
||||
use crate::idm::mfareg::{MfaRegCred, MfaRegNext, MfaRegSession};
|
||||
use crate::idm::oauth2::{
|
||||
AccessTokenRequest, AccessTokenResponse, AuthorisationRequest, AuthorisePermitSuccess,
|
||||
ConsentRequest, Oauth2Error, Oauth2ResourceServers, Oauth2ResourceServersReadTransaction,
|
||||
Oauth2ResourceServersWriteTransaction,
|
||||
};
|
||||
use crate::idm::radius::RadiusAccount;
|
||||
use crate::idm::unix::{UnixGroup, UnixUserAccount};
|
||||
use crate::idm::AuthState;
|
||||
|
@ -56,6 +61,7 @@ use concread::{
|
|||
hashmap::HashMap,
|
||||
};
|
||||
use rand::prelude::*;
|
||||
use std::convert::TryFrom;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use url::Url;
|
||||
|
||||
|
@ -85,6 +91,7 @@ pub struct IdmServer {
|
|||
// Our webauthn verifier/config
|
||||
webauthn: Webauthn<WebauthnDomainConfig>,
|
||||
pw_badlist_cache: Arc<CowCell<HashSet<String>>>,
|
||||
oauth2rs: Arc<Oauth2ResourceServers>,
|
||||
uat_bundy_hmac: Arc<CowCell<HS512>>,
|
||||
}
|
||||
|
||||
|
@ -112,6 +119,7 @@ pub struct IdmServerProxyReadTransaction<'a> {
|
|||
// and other structured content.
|
||||
pub qs_read: QueryServerReadTransaction<'a>,
|
||||
uat_bundy_hmac: CowCellReadTxn<HS512>,
|
||||
oauth2rs: Oauth2ResourceServersReadTransaction,
|
||||
}
|
||||
|
||||
pub struct IdmServerProxyWriteTransaction<'a> {
|
||||
|
@ -125,6 +133,7 @@ pub struct IdmServerProxyWriteTransaction<'a> {
|
|||
webauthn: &'a Webauthn<WebauthnDomainConfig>,
|
||||
pw_badlist_cache: CowCellWriteTxn<'a, HashSet<String>>,
|
||||
uat_bundy_hmac: CowCellWriteTxn<'a, HS512>,
|
||||
oauth2rs: Oauth2ResourceServersWriteTransaction<'a>,
|
||||
}
|
||||
|
||||
pub struct IdmServerDelayed {
|
||||
|
@ -150,22 +159,16 @@ impl IdmServer {
|
|||
let (async_tx, async_rx) = unbounded();
|
||||
|
||||
// Get the domain name, as the relying party id.
|
||||
let (rp_id, pw_badlist_set) = {
|
||||
let (rp_id, pw_badlist_set, oauth2rs_set) = {
|
||||
let qs_read = task::block_on(qs.read_async());
|
||||
(
|
||||
qs_read.get_domain_name(au)?,
|
||||
qs_read.get_password_badlist(au)?,
|
||||
// Add a read/reload of all oauth2 configurations.
|
||||
qs_read.get_oauth2rs_set(au)?,
|
||||
)
|
||||
};
|
||||
|
||||
let bundy_handle = HS512::generate_key()
|
||||
.and_then(|bundy_key| HS512::from_str(&bundy_key))
|
||||
.map_err(|e| {
|
||||
ladmin_error!(au, "Failed to generate uat_bundy_hmac - {:?}", e);
|
||||
OperationError::InvalidState
|
||||
})?;
|
||||
let uat_bundy_hmac = Arc::new(CowCell::new(bundy_handle));
|
||||
|
||||
// Check that it gels with our origin.
|
||||
Url::parse(origin.as_str())
|
||||
.map_err(|_e| {
|
||||
|
@ -195,6 +198,17 @@ impl IdmServer {
|
|||
rp_id,
|
||||
});
|
||||
|
||||
// Setup our auth token signing key.
|
||||
let bundy_handle = HS512::generate_key()
|
||||
.and_then(|bundy_key| HS512::from_str(&bundy_key))
|
||||
.map_err(|e| {
|
||||
ladmin_error!(au, "Failed to generate uat_bundy_hmac - {:?}", e);
|
||||
OperationError::InvalidState
|
||||
})?;
|
||||
let uat_bundy_hmac = Arc::new(CowCell::new(bundy_handle));
|
||||
|
||||
let oauth2rs = Oauth2ResourceServers::try_from(oauth2rs_set)?;
|
||||
|
||||
Ok((
|
||||
IdmServer {
|
||||
session_ticket: Semaphore::new(1),
|
||||
|
@ -208,6 +222,7 @@ impl IdmServer {
|
|||
webauthn,
|
||||
pw_badlist_cache: Arc::new(CowCell::new(pw_badlist_set)),
|
||||
uat_bundy_hmac,
|
||||
oauth2rs: Arc::new(oauth2rs),
|
||||
},
|
||||
IdmServerDelayed { async_rx },
|
||||
))
|
||||
|
@ -251,6 +266,7 @@ impl IdmServer {
|
|||
IdmServerProxyReadTransaction {
|
||||
qs_read: self.qs.read_async().await,
|
||||
uat_bundy_hmac: self.uat_bundy_hmac.read(),
|
||||
oauth2rs: self.oauth2rs.read(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -273,6 +289,7 @@ impl IdmServer {
|
|||
webauthn: &self.webauthn,
|
||||
pw_badlist_cache: self.pw_badlist_cache.write(),
|
||||
uat_bundy_hmac: self.uat_bundy_hmac.write(),
|
||||
oauth2rs: self.oauth2rs.write(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1024,6 +1041,41 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
|
|||
|
||||
account.to_backupcodesview()
|
||||
}
|
||||
|
||||
pub fn check_oauth2_authorisation(
|
||||
&self,
|
||||
audit: &mut AuditScope,
|
||||
ident: &Identity,
|
||||
uat: &UserAuthToken,
|
||||
auth_req: &AuthorisationRequest,
|
||||
ct: Duration,
|
||||
) -> Result<ConsentRequest, Oauth2Error> {
|
||||
self.oauth2rs
|
||||
.check_oauth2_authorisation(audit, ident, uat, auth_req, ct)
|
||||
}
|
||||
|
||||
pub fn check_oauth2_authorise_permit(
|
||||
&self,
|
||||
audit: &mut AuditScope,
|
||||
ident: &Identity,
|
||||
uat: &UserAuthToken,
|
||||
consent_req: &str,
|
||||
ct: Duration,
|
||||
) -> Result<AuthorisePermitSuccess, OperationError> {
|
||||
self.oauth2rs
|
||||
.check_oauth2_authorise_permit(audit, ident, uat, consent_req, ct)
|
||||
}
|
||||
|
||||
pub fn check_oauth2_token_exchange(
|
||||
&self,
|
||||
audit: &mut AuditScope,
|
||||
client_authz: &str,
|
||||
token_req: &AccessTokenRequest,
|
||||
ct: Duration,
|
||||
) -> Result<AccessTokenResponse, Oauth2Error> {
|
||||
self.oauth2rs
|
||||
.check_oauth2_token_exchange(audit, client_authz, token_req, ct)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IdmServerTransaction<'a> for IdmServerProxyWriteTransaction<'a> {
|
||||
|
@ -1102,7 +1154,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
fn target_to_account(
|
||||
pub(crate) fn target_to_account(
|
||||
&mut self,
|
||||
au: &mut AuditScope,
|
||||
target: &Uuid,
|
||||
|
@ -1859,8 +1911,15 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
.contains(&UUID_SYSTEM_CONFIG)
|
||||
{
|
||||
self.reload_password_badlist(au)?;
|
||||
self.pw_badlist_cache.commit();
|
||||
};
|
||||
if self.qs_write.get_changed_ouath2() {
|
||||
self.qs_write
|
||||
.get_oauth2rs_set(au)
|
||||
.and_then(|oauth2rs_set| self.oauth2rs.reload(oauth2rs_set))?;
|
||||
}
|
||||
// Commit everything.
|
||||
self.oauth2rs.commit();
|
||||
self.pw_badlist_cache.commit();
|
||||
self.mfareg_sessions.commit();
|
||||
self.qs_write.commit(au)
|
||||
}
|
||||
|
|
|
@ -94,8 +94,8 @@ macro_rules! try_from_entry {
|
|||
.map(|v| v.clone());
|
||||
|
||||
let radius_secret = $value
|
||||
.get_ava_single_radiuscred("radius_secret")
|
||||
.map(|s| s.to_string());
|
||||
.get_ava_single_secret("radius_secret")
|
||||
.map(str::to_string);
|
||||
|
||||
let valid_from = $value.get_ava_single_datetime("account_valid_from");
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
//! The Kanidmd server library. This implements all of the internal components of the server
|
||||
//! which is used to process authentication, store identities and enforce access controls.
|
||||
|
||||
#![recursion_limit = "512"]
|
||||
#![deny(warnings)]
|
||||
#![warn(unused_extern_crates)]
|
||||
#![deny(clippy::unwrap_used)]
|
||||
|
@ -65,10 +66,13 @@ pub mod prelude {
|
|||
pub use crate::utils::duration_from_epoch_now;
|
||||
pub use kanidm_proto::v1::OperationError;
|
||||
pub use smartstring::alias::String as AttrString;
|
||||
pub use url::Url;
|
||||
pub use uuid::Uuid;
|
||||
|
||||
pub use crate::audit::AuditScope;
|
||||
pub use crate::constants::*;
|
||||
pub use crate::filter::{Filter, FilterInvalid};
|
||||
|
||||
pub use crate::entry::{
|
||||
Entry, EntryCommitted, EntryInit, EntryInvalid, EntryInvalidCommitted, EntryNew,
|
||||
EntryReduced, EntrySealed, EntrySealedCommitted, EntryTuple, EntryValid,
|
||||
|
|
|
@ -21,7 +21,7 @@ macro_rules! setup_test {
|
|||
let be = Backend::new($au, BackendConfig::new_test(), idxmeta, false)
|
||||
.expect("Failed to init BE");
|
||||
|
||||
let qs = QueryServer::new(be, schema_outer);
|
||||
let qs = QueryServer::new($au, be, schema_outer);
|
||||
qs.initialise_helper($au, duration_from_epoch_now())
|
||||
.expect("init failed!");
|
||||
qs
|
||||
|
@ -50,7 +50,7 @@ macro_rules! setup_test {
|
|||
let be = Backend::new($au, BackendConfig::new_test(), idxmeta, false)
|
||||
.expect("Failed to init BE");
|
||||
|
||||
let qs = QueryServer::new(be, schema_outer);
|
||||
let qs = QueryServer::new($au, be, schema_outer);
|
||||
qs.initialise_helper($au, duration_from_epoch_now())
|
||||
.expect("init failed!");
|
||||
|
||||
|
@ -96,7 +96,7 @@ macro_rules! run_test_no_init {
|
|||
panic!()
|
||||
}
|
||||
};
|
||||
let test_server = QueryServer::new(be, schema_outer);
|
||||
let test_server = QueryServer::new(&mut audit, be, schema_outer);
|
||||
|
||||
$test_fn(&test_server, &mut audit);
|
||||
// Any needed teardown?
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
//! as "states" on what attribute-values should appear as within the `Entry`
|
||||
|
||||
use crate::prelude::*;
|
||||
use kanidm_proto::v1::Entry as ProtoEntry;
|
||||
use kanidm_proto::v1::Modify as ProtoModify;
|
||||
use kanidm_proto::v1::ModifyList as ProtoModifyList;
|
||||
|
||||
|
@ -133,6 +134,30 @@ impl ModifyList<ModifyInvalid> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn from_patch(
|
||||
audit: &mut AuditScope,
|
||||
pe: &ProtoEntry,
|
||||
qs: &QueryServerWriteTransaction,
|
||||
) -> Result<Self, OperationError> {
|
||||
let mut mods = Vec::new();
|
||||
|
||||
pe.attrs.iter().try_for_each(|(attr, vals)| {
|
||||
// Issue a purge to the attr.
|
||||
mods.push(m_purge(&attr));
|
||||
// Now if there are vals, push those too.
|
||||
// For each value we want to now be present.
|
||||
vals.iter().try_for_each(|val| {
|
||||
qs.clone_value(audit, &attr, &val).map(|resolved_v| {
|
||||
mods.push(Modify::Present(attr.as_str().into(), resolved_v));
|
||||
})
|
||||
})
|
||||
})?;
|
||||
Ok(ModifyList {
|
||||
valid: ModifyInvalid,
|
||||
mods,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn validate(
|
||||
&self,
|
||||
schema: &dyn SchemaTransaction,
|
||||
|
|
|
@ -14,6 +14,7 @@ mod domain;
|
|||
mod failure;
|
||||
mod gidnumber;
|
||||
mod memberof;
|
||||
mod oauth2;
|
||||
mod password_import;
|
||||
mod protected;
|
||||
mod recycle;
|
||||
|
@ -273,6 +274,9 @@ impl Plugins {
|
|||
password_import::PasswordImport
|
||||
)
|
||||
})
|
||||
.and_then(|_| {
|
||||
run_pre_create_transform_plugin!(au, qs, cand, ce, oauth2::Oauth2Secrets)
|
||||
})
|
||||
.and_then(|_| {
|
||||
run_pre_create_transform_plugin!(au, qs, cand, ce, gidnumber::GidNumber)
|
||||
})
|
||||
|
@ -334,6 +338,7 @@ impl Plugins {
|
|||
.and_then(|_| {
|
||||
run_pre_modify_plugin!(au, qs, cand, me, password_import::PasswordImport)
|
||||
})
|
||||
.and_then(|_| run_pre_modify_plugin!(au, qs, cand, me, oauth2::Oauth2Secrets))
|
||||
.and_then(|_| run_pre_modify_plugin!(au, qs, cand, me, gidnumber::GidNumber))
|
||||
.and_then(|_| run_pre_modify_plugin!(au, qs, cand, me, spn::Spn))
|
||||
// attr unique should always be last
|
||||
|
|
139
kanidmd/src/lib/plugins/oauth2.rs
Normal file
139
kanidmd/src/lib/plugins/oauth2.rs
Normal file
|
@ -0,0 +1,139 @@
|
|||
use crate::event::{CreateEvent, ModifyEvent};
|
||||
use crate::plugins::Plugin;
|
||||
use crate::prelude::*;
|
||||
use crate::utils::password_from_random;
|
||||
|
||||
lazy_static! {
|
||||
static ref CLASS_OAUTH2_BASIC: PartialValue =
|
||||
PartialValue::new_class("oauth2_resource_server_basic");
|
||||
}
|
||||
|
||||
pub struct Oauth2Secrets {}
|
||||
|
||||
macro_rules! oauth2_transform {
|
||||
(
|
||||
$au:expr,
|
||||
$e:expr
|
||||
) => {{
|
||||
if $e.attribute_equality("class", &CLASS_OAUTH2_BASIC) {
|
||||
if !$e.attribute_pres("oauth2_rs_basic_secret") {
|
||||
lsecurity!($au, "regenerating oauth2 basic secret");
|
||||
let v = Value::new_utf8(password_from_random());
|
||||
$e.add_ava("oauth2_rs_basic_secret", v);
|
||||
}
|
||||
if !$e.attribute_pres("oauth2_rs_basic_token_key") {
|
||||
lsecurity!($au, "regenerating oauth2 token key");
|
||||
let k = fernet::Fernet::generate_key();
|
||||
let v = Value::new_secret_str(&k);
|
||||
$e.add_ava("oauth2_rs_basic_token_key", v);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}};
|
||||
}
|
||||
|
||||
impl Plugin for Oauth2Secrets {
|
||||
fn id() -> &'static str {
|
||||
"plugin_oauth2_secrets"
|
||||
}
|
||||
|
||||
fn pre_create_transform(
|
||||
au: &mut AuditScope,
|
||||
_qs: &QueryServerWriteTransaction,
|
||||
cand: &mut Vec<Entry<EntryInvalid, EntryNew>>,
|
||||
_ce: &CreateEvent,
|
||||
) -> Result<(), OperationError> {
|
||||
cand.iter_mut().try_for_each(|e| oauth2_transform!(au, e))
|
||||
}
|
||||
|
||||
fn pre_modify(
|
||||
au: &mut AuditScope,
|
||||
_qs: &QueryServerWriteTransaction,
|
||||
cand: &mut Vec<Entry<EntryInvalid, EntryCommitted>>,
|
||||
_me: &ModifyEvent,
|
||||
) -> Result<(), OperationError> {
|
||||
cand.iter_mut().try_for_each(|e| oauth2_transform!(au, e))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::modify::{Modify, ModifyList};
|
||||
use crate::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn test_pre_create_oauth2_secrets() {
|
||||
let preload: Vec<Entry<EntryInit, EntryNew>> = Vec::new();
|
||||
|
||||
let uuid = Uuid::new_v4();
|
||||
let e: Entry<EntryInit, EntryNew> = entry_init!(
|
||||
("class", Value::new_class("object")),
|
||||
("class", Value::new_class("oauth2_resource_server")),
|
||||
("class", Value::new_class("oauth2_resource_server_basic")),
|
||||
("uuid", Value::new_uuid(uuid)),
|
||||
("oauth2_rs_name", Value::new_iname("test_resource_server")),
|
||||
(
|
||||
"oauth2_rs_origin",
|
||||
Value::new_url_s("https://demo.example.com").unwrap()
|
||||
)
|
||||
);
|
||||
|
||||
let create = vec![e];
|
||||
|
||||
run_create_test!(
|
||||
Ok(()),
|
||||
preload,
|
||||
create,
|
||||
None,
|
||||
|au: &mut AuditScope, qs: &QueryServerWriteTransaction| {
|
||||
let e = qs
|
||||
.internal_search_uuid(au, &uuid)
|
||||
.expect("failed to get oauth2 config");
|
||||
assert!(e.attribute_pres("oauth2_rs_basic_secret"));
|
||||
assert!(e.attribute_pres("oauth2_rs_basic_token_key"));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_modify_oauth2_secrets_regenerate() {
|
||||
let uuid = Uuid::new_v4();
|
||||
|
||||
let e: Entry<EntryInit, EntryNew> = entry_init!(
|
||||
("class", Value::new_class("object")),
|
||||
("class", Value::new_class("oauth2_resource_server")),
|
||||
("class", Value::new_class("oauth2_resource_server_basic")),
|
||||
("uuid", Value::new_uuid(uuid)),
|
||||
("oauth2_rs_name", Value::new_iname("test_resource_server")),
|
||||
(
|
||||
"oauth2_rs_origin",
|
||||
Value::new_url_s("https://demo.example.com").unwrap()
|
||||
),
|
||||
("oauth2_rs_basic_secret", Value::new_utf8s("12345")),
|
||||
("oauth2_rs_basic_token_key", Value::new_secret_str("12345"))
|
||||
);
|
||||
|
||||
let preload = vec![e];
|
||||
|
||||
run_modify_test!(
|
||||
Ok(()),
|
||||
preload,
|
||||
filter!(f_eq("uuid", PartialValue::new_uuid(uuid))),
|
||||
ModifyList::new_list(vec![
|
||||
Modify::Purged(AttrString::from("oauth2_rs_basic_secret"),),
|
||||
Modify::Purged(AttrString::from("oauth2_rs_basic_token_key"),)
|
||||
]),
|
||||
None,
|
||||
|au: &mut AuditScope, qs: &QueryServerWriteTransaction| {
|
||||
let e = qs
|
||||
.internal_search_uuid(au, &uuid)
|
||||
.expect("failed to get oauth2 config");
|
||||
assert!(e.attribute_pres("oauth2_rs_basic_secret"));
|
||||
assert!(e.attribute_pres("oauth2_rs_basic_token_key"));
|
||||
// Check the values are different.
|
||||
assert!(e.get_ava_single_str("oauth2_rs_basic_secret") != Some("12345"));
|
||||
assert!(e.get_ava_single_secret("oauth2_rs_basic_token_key") != Some("12345"));
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -193,7 +193,7 @@ impl SchemaAttribute {
|
|||
SyntaxType::UTF8STRING => v.is_utf8(),
|
||||
SyntaxType::JSON_FILTER => v.is_json_filter(),
|
||||
SyntaxType::Credential => v.is_credential(),
|
||||
SyntaxType::RadiusUtf8String => v.is_radius_string(),
|
||||
SyntaxType::SecretUtf8String => v.is_secret_string(),
|
||||
SyntaxType::SshKey => v.is_sshkey(),
|
||||
SyntaxType::SecurityPrincipalName => v.is_spn(),
|
||||
SyntaxType::UINT32 => v.is_uint32(),
|
||||
|
@ -201,6 +201,7 @@ impl SchemaAttribute {
|
|||
SyntaxType::NsUniqueId => v.is_nsuniqueid(),
|
||||
SyntaxType::DateTime => v.is_datetime(),
|
||||
SyntaxType::EmailAddress => v.is_email_address(),
|
||||
SyntaxType::Url => v.is_url(),
|
||||
};
|
||||
if r {
|
||||
Ok(())
|
||||
|
@ -241,7 +242,7 @@ impl SchemaAttribute {
|
|||
SyntaxType::UTF8STRING => ava.iter().all(Value::is_utf8),
|
||||
SyntaxType::JSON_FILTER => ava.iter().all(Value::is_json_filter),
|
||||
SyntaxType::Credential => ava.iter().all(Value::is_credential),
|
||||
SyntaxType::RadiusUtf8String => ava.iter().all(Value::is_radius_string),
|
||||
SyntaxType::SecretUtf8String => ava.iter().all(Value::is_secret_string),
|
||||
SyntaxType::SshKey => ava.iter().all(Value::is_sshkey),
|
||||
SyntaxType::SecurityPrincipalName => ava.iter().all(Value::is_spn),
|
||||
SyntaxType::UINT32 => ava.iter().all(Value::is_uint32),
|
||||
|
@ -249,6 +250,7 @@ impl SchemaAttribute {
|
|||
SyntaxType::NsUniqueId => ava.iter().all(Value::is_nsuniqueid),
|
||||
SyntaxType::DateTime => ava.iter().all(Value::is_datetime),
|
||||
SyntaxType::EmailAddress => ava.iter().all(Value::is_email_address),
|
||||
SyntaxType::Url => ava.iter().all(Value::is_url),
|
||||
};
|
||||
if valid {
|
||||
Ok(())
|
||||
|
|
|
@ -47,6 +47,7 @@ lazy_static! {
|
|||
static ref PVCLASS_ACM: PartialValue = PartialValue::new_class("access_control_modify");
|
||||
static ref PVCLASS_ACC: PartialValue = PartialValue::new_class("access_control_create");
|
||||
static ref PVCLASS_ACP: PartialValue = PartialValue::new_class("access_control_profile");
|
||||
static ref PVCLASS_OAUTH2_RS: PartialValue = PartialValue::new_class("oauth2_resource_server");
|
||||
static ref PVACP_ENABLE_FALSE: PartialValue = PartialValue::new_bool(false);
|
||||
}
|
||||
|
||||
|
@ -86,6 +87,7 @@ pub struct QueryServerWriteTransaction<'a> {
|
|||
// changing content.
|
||||
changed_schema: Cell<bool>,
|
||||
changed_acp: Cell<bool>,
|
||||
changed_oauth2: Cell<bool>,
|
||||
// Store the list of changed uuids for other invalidation needs?
|
||||
changed_uuid: Cell<HashSet<Uuid>>,
|
||||
_db_ticket: SemaphorePermit<'a>,
|
||||
|
@ -528,7 +530,7 @@ pub trait QueryServerTransaction<'a> {
|
|||
SyntaxType::JSON_FILTER => Value::new_json_filter(value)
|
||||
.ok_or_else(|| OperationError::InvalidAttribute("Invalid Filter syntax".to_string())),
|
||||
SyntaxType::Credential => Err(OperationError::InvalidAttribute("Credentials can not be supplied through modification - please use the IDM api".to_string())),
|
||||
SyntaxType::RadiusUtf8String => Err(OperationError::InvalidAttribute("Radius secrets can not be supplied through modification - please use the IDM api".to_string())),
|
||||
SyntaxType::SecretUtf8String => Err(OperationError::InvalidAttribute("Radius secrets can not be supplied through modification - please use the IDM api".to_string())),
|
||||
SyntaxType::SshKey => Err(OperationError::InvalidAttribute("SSH public keys can not be supplied through modification - please use the IDM api".to_string())),
|
||||
SyntaxType::SecurityPrincipalName => Err(OperationError::InvalidAttribute("SPNs are generated and not able to be set.".to_string())),
|
||||
SyntaxType::UINT32 => Value::new_uint32_str(value)
|
||||
|
@ -538,6 +540,8 @@ pub trait QueryServerTransaction<'a> {
|
|||
SyntaxType::DateTime => Value::new_datetime_s(value)
|
||||
.ok_or_else(|| OperationError::InvalidAttribute("Invalid DateTime (rfc3339) syntax".to_string())),
|
||||
SyntaxType::EmailAddress => Ok(Value::new_email_address_s(value)),
|
||||
SyntaxType::Url => Value::new_url_s(value)
|
||||
.ok_or_else(|| OperationError::InvalidAttribute("Invalid Url (whatwg/url) syntax".to_string())),
|
||||
}
|
||||
}
|
||||
None => {
|
||||
|
@ -611,7 +615,7 @@ pub trait QueryServerTransaction<'a> {
|
|||
})
|
||||
}
|
||||
SyntaxType::Credential => Ok(PartialValue::new_credential_tag(value)),
|
||||
SyntaxType::RadiusUtf8String => Ok(PartialValue::new_radius_string()),
|
||||
SyntaxType::SecretUtf8String => Ok(PartialValue::new_secret_str()),
|
||||
SyntaxType::SshKey => Ok(PartialValue::new_sshkey_tag_s(value)),
|
||||
SyntaxType::SecurityPrincipalName => {
|
||||
PartialValue::new_spn_s(value).ok_or_else(|| {
|
||||
|
@ -631,6 +635,11 @@ pub trait QueryServerTransaction<'a> {
|
|||
)
|
||||
}),
|
||||
SyntaxType::EmailAddress => Ok(PartialValue::new_email_address_s(value)),
|
||||
SyntaxType::Url => PartialValue::new_url_s(value).ok_or_else(|| {
|
||||
OperationError::InvalidAttribute(
|
||||
"Invalid Url (whatwg/url) syntax".to_string(),
|
||||
)
|
||||
}),
|
||||
}
|
||||
}
|
||||
None => {
|
||||
|
@ -720,6 +729,19 @@ pub trait QueryServerTransaction<'a> {
|
|||
e
|
||||
})
|
||||
}
|
||||
|
||||
fn get_oauth2rs_set(
|
||||
&self,
|
||||
audit: &mut AuditScope,
|
||||
) -> Result<Vec<EntrySealedCommitted>, OperationError> {
|
||||
self.internal_search(
|
||||
audit,
|
||||
filter!(f_eq(
|
||||
"class",
|
||||
PartialValue::new_class("oauth2_resource_server")
|
||||
)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Actually conduct a search request
|
||||
|
@ -839,10 +861,14 @@ struct QueryServerMeta {
|
|||
}
|
||||
|
||||
impl QueryServer {
|
||||
pub fn new(be: Backend, schema: Schema) -> Self {
|
||||
pub fn new(audit: &mut AuditScope, be: Backend, schema: Schema) -> Self {
|
||||
let (s_uuid, d_uuid) = {
|
||||
let wr = be.write();
|
||||
(wr.get_db_s_uuid(), wr.get_db_d_uuid())
|
||||
let res = (wr.get_db_s_uuid(), wr.get_db_d_uuid());
|
||||
#[allow(clippy::expect_used)]
|
||||
wr.commit(audit)
|
||||
.expect("Critical - unable to commit db_s_uuid or db_d_uuid");
|
||||
res
|
||||
};
|
||||
|
||||
let pool_size = be.get_pool_size();
|
||||
|
@ -933,6 +959,7 @@ impl QueryServer {
|
|||
accesscontrols: self.accesscontrols.write(),
|
||||
changed_schema: Cell::new(false),
|
||||
changed_acp: Cell::new(false),
|
||||
changed_oauth2: Cell::new(false),
|
||||
changed_uuid: Cell::new(HashSet::new()),
|
||||
_db_ticket: db_ticket,
|
||||
_write_ticket: write_ticket,
|
||||
|
@ -1136,6 +1163,13 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
.any(|e| e.attribute_equality("class", &PVCLASS_ACP)),
|
||||
)
|
||||
}
|
||||
if !self.changed_oauth2.get() {
|
||||
self.changed_oauth2.set(
|
||||
commit_cand
|
||||
.iter()
|
||||
.any(|e| e.attribute_equality("class", &PVCLASS_OAUTH2_RS)),
|
||||
)
|
||||
}
|
||||
|
||||
let cu = self.changed_uuid.as_ptr();
|
||||
unsafe {
|
||||
|
@ -1143,9 +1177,10 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
}
|
||||
ltrace!(
|
||||
audit,
|
||||
"Schema reload: {:?}, ACP reload: {:?}",
|
||||
"Schema reload: {:?}, ACP reload: {:?}, Oauth2 reload: {:?}",
|
||||
self.changed_schema,
|
||||
self.changed_acp
|
||||
self.changed_acp,
|
||||
self.changed_oauth2
|
||||
);
|
||||
|
||||
// We are complete, finalise logging and return
|
||||
|
@ -1270,6 +1305,13 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
.any(|e| e.attribute_equality("class", &PVCLASS_ACP)),
|
||||
)
|
||||
}
|
||||
if !self.changed_oauth2.get() {
|
||||
self.changed_oauth2.set(
|
||||
del_cand
|
||||
.iter()
|
||||
.any(|e| e.attribute_equality("class", &PVCLASS_OAUTH2_RS)),
|
||||
)
|
||||
}
|
||||
|
||||
let cu = self.changed_uuid.as_ptr();
|
||||
unsafe {
|
||||
|
@ -1278,9 +1320,10 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
|
||||
ltrace!(
|
||||
audit,
|
||||
"Schema reload: {:?}, ACP reload: {:?}",
|
||||
"Schema reload: {:?}, ACP reload: {:?}, Oauth2 reload: {:?}",
|
||||
self.changed_schema,
|
||||
self.changed_acp
|
||||
self.changed_acp,
|
||||
self.changed_oauth2,
|
||||
);
|
||||
|
||||
// Send result
|
||||
|
@ -1640,6 +1683,15 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
.any(|e| e.attribute_equality("class", &PVCLASS_ACP)),
|
||||
)
|
||||
}
|
||||
if !self.changed_oauth2.get() {
|
||||
self.changed_oauth2.set(
|
||||
norm_cand
|
||||
.iter()
|
||||
.chain(pre_candidates.iter())
|
||||
.any(|e| e.attribute_equality("class", &PVCLASS_OAUTH2_RS)),
|
||||
)
|
||||
}
|
||||
|
||||
let cu = self.changed_uuid.as_ptr();
|
||||
unsafe {
|
||||
(*cu).extend(
|
||||
|
@ -1652,9 +1704,10 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
|
||||
ltrace!(
|
||||
audit,
|
||||
"Schema reload: {:?}, ACP reload: {:?}",
|
||||
"Schema reload: {:?}, ACP reload: {:?}, Oauth2 reload: {:?}",
|
||||
self.changed_schema,
|
||||
self.changed_acp
|
||||
self.changed_acp,
|
||||
self.changed_oauth2,
|
||||
);
|
||||
|
||||
// return
|
||||
|
@ -1778,6 +1831,13 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
.any(|e| e.attribute_equality("class", &PVCLASS_ACP)),
|
||||
)
|
||||
}
|
||||
if !self.changed_oauth2.get() {
|
||||
self.changed_oauth2.set(
|
||||
norm_cand
|
||||
.iter()
|
||||
.any(|e| e.attribute_equality("class", &PVCLASS_OAUTH2_RS)),
|
||||
)
|
||||
}
|
||||
let cu = self.changed_uuid.as_ptr();
|
||||
unsafe {
|
||||
(*cu).extend(
|
||||
|
@ -1789,9 +1849,10 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
}
|
||||
ltrace!(
|
||||
audit,
|
||||
"Schema reload: {:?}, ACP reload: {:?}",
|
||||
"Schema reload: {:?}, ACP reload: {:?}, Oauth2 reload: {:?}",
|
||||
self.changed_schema,
|
||||
self.changed_acp
|
||||
self.changed_acp,
|
||||
self.changed_oauth2,
|
||||
);
|
||||
|
||||
ltrace!(audit, "Modify operation success");
|
||||
|
@ -2177,6 +2238,11 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
JSON_SCHEMA_ATTR_UNIX_PASSWORD,
|
||||
JSON_SCHEMA_ATTR_ACCOUNT_EXPIRE,
|
||||
JSON_SCHEMA_ATTR_ACCOUNT_VALID_FROM,
|
||||
JSON_SCHEMA_ATTR_OAUTH2_RS_NAME,
|
||||
JSON_SCHEMA_ATTR_OAUTH2_RS_ORIGIN,
|
||||
JSON_SCHEMA_ATTR_OAUTH2_RS_ACCOUNT_FILTER,
|
||||
JSON_SCHEMA_ATTR_OAUTH2_RS_BASIC_SECRET,
|
||||
JSON_SCHEMA_ATTR_OAUTH2_RS_BASIC_TOKEN_KEY,
|
||||
JSON_SCHEMA_CLASS_PERSON,
|
||||
JSON_SCHEMA_CLASS_GROUP,
|
||||
JSON_SCHEMA_CLASS_ACCOUNT,
|
||||
|
@ -2184,6 +2250,8 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
JSON_SCHEMA_CLASS_POSIXACCOUNT,
|
||||
JSON_SCHEMA_CLASS_POSIXGROUP,
|
||||
JSON_SCHEMA_CLASS_SYSTEM_CONFIG,
|
||||
JSON_SCHEMA_CLASS_OAUTH2_RS,
|
||||
JSON_SCHEMA_CLASS_OAUTH2_RS_BASIC,
|
||||
JSON_SCHEMA_ATTR_NSUNIQUEID,
|
||||
];
|
||||
|
||||
|
@ -2272,6 +2340,8 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
JSON_IDM_HP_GROUP_UNIX_EXTEND_PRIV_V1,
|
||||
JSON_IDM_ACP_MANAGE_PRIV_V1,
|
||||
JSON_DOMAIN_ADMINS,
|
||||
JSON_IDM_HP_OAUTH2_MANAGE_PRIV_V1,
|
||||
// All members must exist before we write HP
|
||||
JSON_IDM_HIGH_PRIVILEGE_V1,
|
||||
// Built in access controls.
|
||||
JSON_IDM_ADMINS_ACP_RECYCLE_SEARCH_V1,
|
||||
|
@ -2305,6 +2375,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
JSON_IDM_ACP_PEOPLE_EXTEND_PRIV_V1,
|
||||
JSON_IDM_HP_ACP_ACCOUNT_UNIX_EXTEND_PRIV_V1,
|
||||
JSON_IDM_HP_ACP_GROUP_UNIX_EXTEND_PRIV_V1,
|
||||
JSON_IDM_HP_ACP_OAUTH2_MANAGE_PRIV_V1,
|
||||
];
|
||||
|
||||
let res: Result<(), _> = idm_entries
|
||||
|
@ -2320,6 +2391,9 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
return res;
|
||||
}
|
||||
|
||||
self.changed_schema.set(true);
|
||||
self.changed_acp.set(true);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -2557,6 +2631,10 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
unsafe { &(*self.changed_uuid.as_ptr()) }
|
||||
}
|
||||
|
||||
pub fn get_changed_ouath2(&self) -> bool {
|
||||
self.changed_oauth2.get()
|
||||
}
|
||||
|
||||
pub fn commit(mut self, audit: &mut AuditScope) -> Result<(), OperationError> {
|
||||
// This could be faster if we cache the set of classes changed
|
||||
// in an operation so we can check if we need to do the reload or not
|
||||
|
|
|
@ -16,6 +16,7 @@ use std::fmt;
|
|||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
use sshkeys::PublicKey as SshPublicKey;
|
||||
|
@ -30,11 +31,11 @@ lazy_static! {
|
|||
};
|
||||
static ref INAME_RE: Regex = {
|
||||
#[allow(clippy::expect_used)]
|
||||
Regex::new("^((\\.|_).*|.*(\\s|@|,|/|\\\\|=).*|\\d+|root|nobody|nogroup|wheel|sshd|shadow|systemd.*)$").expect("Invalid Iname regex found")
|
||||
Regex::new("^((\\.|_).*|.*(\\s|:|;|@|,|/|\\\\|=).*|\\d+|root|nobody|nogroup|wheel|sshd|shadow|systemd.*)$").expect("Invalid Iname regex found")
|
||||
// ^ ^ ^ ^
|
||||
// | | | \- must not be a reserved name.
|
||||
// | | \- must not be only integers
|
||||
// | \- must not contain whitespace, @, ',', /, \, =
|
||||
// | \- must not contain whitespace, @, :, ;, ',', /, \, =
|
||||
// \- must not start with _ or .
|
||||
// Them's be the rules.
|
||||
};
|
||||
|
@ -116,8 +117,6 @@ impl fmt::Display for IndexType {
|
|||
#[allow(non_camel_case_types)]
|
||||
#[derive(Hash, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
|
||||
pub enum SyntaxType {
|
||||
// We need an insensitive string type too ...
|
||||
// We also need to "self host" a syntax type, and index type
|
||||
UTF8STRING,
|
||||
Utf8StringInsensitive,
|
||||
Utf8StringIname,
|
||||
|
@ -128,7 +127,7 @@ pub enum SyntaxType {
|
|||
REFERENCE_UUID,
|
||||
JSON_FILTER,
|
||||
Credential,
|
||||
RadiusUtf8String,
|
||||
SecretUtf8String,
|
||||
SshKey,
|
||||
SecurityPrincipalName,
|
||||
UINT32,
|
||||
|
@ -136,6 +135,7 @@ pub enum SyntaxType {
|
|||
NsUniqueId,
|
||||
DateTime,
|
||||
EmailAddress,
|
||||
Url,
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for SyntaxType {
|
||||
|
@ -154,7 +154,8 @@ impl TryFrom<&str> for SyntaxType {
|
|||
"REFERENCE_UUID" => Ok(SyntaxType::REFERENCE_UUID),
|
||||
"JSON_FILTER" => Ok(SyntaxType::JSON_FILTER),
|
||||
"CREDENTIAL" => Ok(SyntaxType::Credential),
|
||||
"RADIUS_UTF8STRING" => Ok(SyntaxType::RadiusUtf8String),
|
||||
// Compatability for older syntax name.
|
||||
"RADIUS_UTF8STRING" | "SECRET_UTF8STRING" => Ok(SyntaxType::SecretUtf8String),
|
||||
"SSHKEY" => Ok(SyntaxType::SshKey),
|
||||
"SECURITY_PRINCIPAL_NAME" => Ok(SyntaxType::SecurityPrincipalName),
|
||||
"UINT32" => Ok(SyntaxType::UINT32),
|
||||
|
@ -162,6 +163,7 @@ impl TryFrom<&str> for SyntaxType {
|
|||
"NSUNIQUEID" => Ok(SyntaxType::NsUniqueId),
|
||||
"DATETIME" => Ok(SyntaxType::DateTime),
|
||||
"EMAIL_ADDRESS" => Ok(SyntaxType::EmailAddress),
|
||||
"URL" => Ok(SyntaxType::Url),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
|
@ -181,7 +183,7 @@ impl TryFrom<usize> for SyntaxType {
|
|||
6 => Ok(SyntaxType::REFERENCE_UUID),
|
||||
7 => Ok(SyntaxType::JSON_FILTER),
|
||||
8 => Ok(SyntaxType::Credential),
|
||||
9 => Ok(SyntaxType::RadiusUtf8String),
|
||||
9 => Ok(SyntaxType::SecretUtf8String),
|
||||
10 => Ok(SyntaxType::SshKey),
|
||||
11 => Ok(SyntaxType::SecurityPrincipalName),
|
||||
12 => Ok(SyntaxType::UINT32),
|
||||
|
@ -190,6 +192,7 @@ impl TryFrom<usize> for SyntaxType {
|
|||
15 => Ok(SyntaxType::NsUniqueId),
|
||||
16 => Ok(SyntaxType::DateTime),
|
||||
17 => Ok(SyntaxType::EmailAddress),
|
||||
18 => Ok(SyntaxType::Url),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
|
@ -207,7 +210,7 @@ impl SyntaxType {
|
|||
SyntaxType::REFERENCE_UUID => 6,
|
||||
SyntaxType::JSON_FILTER => 7,
|
||||
SyntaxType::Credential => 8,
|
||||
SyntaxType::RadiusUtf8String => 9,
|
||||
SyntaxType::SecretUtf8String => 9,
|
||||
SyntaxType::SshKey => 10,
|
||||
SyntaxType::SecurityPrincipalName => 11,
|
||||
SyntaxType::UINT32 => 12,
|
||||
|
@ -216,6 +219,7 @@ impl SyntaxType {
|
|||
SyntaxType::NsUniqueId => 15,
|
||||
SyntaxType::DateTime => 16,
|
||||
SyntaxType::EmailAddress => 17,
|
||||
SyntaxType::Url => 18,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -233,7 +237,7 @@ impl fmt::Display for SyntaxType {
|
|||
SyntaxType::REFERENCE_UUID => "REFERENCE_UUID",
|
||||
SyntaxType::JSON_FILTER => "JSON_FILTER",
|
||||
SyntaxType::Credential => "CREDENTIAL",
|
||||
SyntaxType::RadiusUtf8String => "RADIUS_UTF8STRING",
|
||||
SyntaxType::SecretUtf8String => "SECRET_UTF8STRING",
|
||||
SyntaxType::SshKey => "SSHKEY",
|
||||
SyntaxType::SecurityPrincipalName => "SECURITY_PRINCIPAL_NAME",
|
||||
SyntaxType::UINT32 => "UINT32",
|
||||
|
@ -241,6 +245,7 @@ impl fmt::Display for SyntaxType {
|
|||
SyntaxType::NsUniqueId => "NSUNIQUEID",
|
||||
SyntaxType::DateTime => "DATETIME",
|
||||
SyntaxType::EmailAddress => "EMAIL_ADDRESS",
|
||||
SyntaxType::Url => "URL",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -249,7 +254,7 @@ impl fmt::Display for SyntaxType {
|
|||
pub enum DataValue {
|
||||
Cred(Credential),
|
||||
SshKey(String),
|
||||
RadiusCred(String),
|
||||
SecretValue(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for DataValue {
|
||||
|
@ -257,7 +262,7 @@ impl std::fmt::Debug for DataValue {
|
|||
match self {
|
||||
DataValue::Cred(_) => write!(f, "DataValue::Cred(_)"),
|
||||
DataValue::SshKey(_) => write!(f, "DataValue::SshKey(_)"),
|
||||
DataValue::RadiusCred(_) => write!(f, "DataValue::RadiusCred(_)"),
|
||||
DataValue::SecretValue(_) => write!(f, "DataValue::SecretValue(_)"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -284,13 +289,14 @@ pub enum PartialValue {
|
|||
// Tag, matches to a DataValue.
|
||||
Cred(String),
|
||||
SshKey(String),
|
||||
RadiusCred,
|
||||
SecretValue,
|
||||
Spn(String, String),
|
||||
Uint32(u32),
|
||||
Cid(Cid),
|
||||
Nsuniqueid(String),
|
||||
DateTime(OffsetDateTime),
|
||||
EmailAddress(String),
|
||||
Url(Url),
|
||||
}
|
||||
|
||||
impl PartialValue {
|
||||
|
@ -431,12 +437,12 @@ impl PartialValue {
|
|||
matches!(self, PartialValue::Cred(_))
|
||||
}
|
||||
|
||||
pub fn new_radius_string() -> Self {
|
||||
PartialValue::RadiusCred
|
||||
pub fn new_secret_str() -> Self {
|
||||
PartialValue::SecretValue
|
||||
}
|
||||
|
||||
pub fn is_radius_string(&self) -> bool {
|
||||
matches!(self, PartialValue::RadiusCred)
|
||||
pub fn is_secret_string(&self) -> bool {
|
||||
matches!(self, PartialValue::SecretValue)
|
||||
}
|
||||
|
||||
pub fn new_sshkey_tag(s: String) -> Self {
|
||||
|
@ -528,6 +534,14 @@ impl PartialValue {
|
|||
matches!(self, PartialValue::EmailAddress(_))
|
||||
}
|
||||
|
||||
pub fn new_url_s(s: &str) -> Option<Self> {
|
||||
Url::parse(s).ok().map(PartialValue::Url)
|
||||
}
|
||||
|
||||
pub fn is_url(&self) -> bool {
|
||||
matches!(self, PartialValue::Url(_))
|
||||
}
|
||||
|
||||
pub fn to_str(&self) -> Option<&str> {
|
||||
match self {
|
||||
PartialValue::Utf8(s) => Some(s.as_str()),
|
||||
|
@ -537,6 +551,13 @@ impl PartialValue {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn to_url(&self) -> Option<&Url> {
|
||||
match self {
|
||||
PartialValue::Url(u) => Some(&u),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contains(&self, s: &PartialValue) -> bool {
|
||||
match (self, s) {
|
||||
(PartialValue::Utf8(s1), PartialValue::Utf8(s2)) => s1.contains(s2),
|
||||
|
@ -572,7 +593,7 @@ impl PartialValue {
|
|||
}
|
||||
PartialValue::Cred(tag) => tag.to_string(),
|
||||
// This will never match as we never index radius creds! See generate_idx_eq_keys
|
||||
PartialValue::RadiusCred => "_".to_string(),
|
||||
PartialValue::SecretValue => "_".to_string(),
|
||||
PartialValue::SshKey(tag) => tag.to_string(),
|
||||
PartialValue::Spn(name, realm) => format!("{}@{}", name, realm),
|
||||
PartialValue::Uint32(u) => u.to_string(),
|
||||
|
@ -582,6 +603,7 @@ impl PartialValue {
|
|||
debug_assert!(odt.offset() == time::UtcOffset::UTC);
|
||||
odt.format(time::Format::Rfc3339)
|
||||
}
|
||||
PartialValue::Url(u) => u.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -896,22 +918,22 @@ impl Value {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn new_radius_str(cleartext: &str) -> Self {
|
||||
pub fn new_secret_str(cleartext: &str) -> Self {
|
||||
Value {
|
||||
pv: PartialValue::new_radius_string(),
|
||||
data: Some(Box::new(DataValue::RadiusCred(cleartext.to_string()))),
|
||||
pv: PartialValue::new_secret_str(),
|
||||
data: Some(Box::new(DataValue::SecretValue(cleartext.to_string()))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_radius_string(&self) -> bool {
|
||||
matches!(&self.pv, PartialValue::RadiusCred)
|
||||
pub fn is_secret_string(&self) -> bool {
|
||||
matches!(&self.pv, PartialValue::SecretValue)
|
||||
}
|
||||
|
||||
pub fn get_radius_secret(&self) -> Option<&str> {
|
||||
pub fn get_secret_str(&self) -> Option<&str> {
|
||||
match &self.pv {
|
||||
PartialValue::RadiusCred => match &self.data {
|
||||
PartialValue::SecretValue => match &self.data {
|
||||
Some(dv) => match dv.as_ref() {
|
||||
DataValue::RadiusCred(c) => Some(c.as_str()),
|
||||
DataValue::SecretValue(c) => Some(c.as_str()),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
|
@ -1042,6 +1064,14 @@ impl Value {
|
|||
self.pv.is_email_address()
|
||||
}
|
||||
|
||||
pub fn new_url_s(s: &str) -> Option<Self> {
|
||||
PartialValue::new_url_s(s).map(|pv| Value { pv, data: None })
|
||||
}
|
||||
|
||||
pub fn is_url(&self) -> bool {
|
||||
self.pv.is_url()
|
||||
}
|
||||
|
||||
pub fn contains(&self, s: &PartialValue) -> bool {
|
||||
self.pv.contains(s)
|
||||
}
|
||||
|
@ -1107,8 +1137,8 @@ impl Value {
|
|||
})
|
||||
}
|
||||
DbValueV1::SecretValue(d) => Ok(Value {
|
||||
pv: PartialValue::RadiusCred,
|
||||
data: Some(Box::new(DataValue::RadiusCred(d))),
|
||||
pv: PartialValue::SecretValue,
|
||||
data: Some(Box::new(DataValue::SecretValue(d))),
|
||||
}),
|
||||
DbValueV1::SshKey(ts) => Ok(Value {
|
||||
pv: PartialValue::SshKey(ts.tag),
|
||||
|
@ -1141,6 +1171,10 @@ impl Value {
|
|||
pv: PartialValue::EmailAddress(email_addr),
|
||||
data: None,
|
||||
}),
|
||||
DbValueV1::Url(u) => Ok(Value {
|
||||
pv: PartialValue::Url(u),
|
||||
data: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1177,10 +1211,10 @@ impl Value {
|
|||
data: c.to_db_valuev1(),
|
||||
})
|
||||
}
|
||||
PartialValue::RadiusCred => {
|
||||
PartialValue::SecretValue => {
|
||||
let ru = match &self.data {
|
||||
Some(v) => match v.as_ref() {
|
||||
DataValue::RadiusCred(rc) => rc.clone(),
|
||||
DataValue::SecretValue(rc) => rc.clone(),
|
||||
_ => unreachable!(),
|
||||
},
|
||||
None => unreachable!(),
|
||||
|
@ -1215,6 +1249,7 @@ impl Value {
|
|||
PartialValue::EmailAddress(mail) => {
|
||||
DbValueV1::EmailAddress(DbValueEmailAddressV1 { d: mail.clone() })
|
||||
}
|
||||
PartialValue::Url(u) => DbValueV1::Url(u.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1227,6 +1262,13 @@ impl Value {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn to_url(&self) -> Option<&Url> {
|
||||
match &self.pv {
|
||||
PartialValue::Url(u) => Some(&u),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_string(&self) -> Option<&String> {
|
||||
match &self.pv {
|
||||
PartialValue::Utf8(s) => Some(s),
|
||||
|
@ -1338,9 +1380,9 @@ impl Value {
|
|||
},
|
||||
None => format!("{}: corrupted value", tag),
|
||||
},
|
||||
// We don't disclose the radius credential unless by special
|
||||
// We don't disclose the secret value unless by special
|
||||
// interfaces.
|
||||
PartialValue::RadiusCred => "radius".to_string(),
|
||||
PartialValue::SecretValue => "secret".to_string(),
|
||||
PartialValue::Spn(n, r) => format!("{}@{}", n, r),
|
||||
PartialValue::Uint32(u) => u.to_string(),
|
||||
PartialValue::Cid(c) => format!("{:?}_{}_{}", c.ts, c.d_uuid, c.s_uuid),
|
||||
|
@ -1348,6 +1390,7 @@ impl Value {
|
|||
debug_assert!(odt.offset() == time::UtcOffset::UTC);
|
||||
odt.format(time::Format::Rfc3339)
|
||||
}
|
||||
PartialValue::Url(u) => u.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1377,13 +1420,14 @@ impl Value {
|
|||
},
|
||||
None => false,
|
||||
},
|
||||
PartialValue::RadiusCred => match &self.data {
|
||||
Some(v) => matches!(v.as_ref(), DataValue::RadiusCred(_)),
|
||||
PartialValue::SecretValue => match &self.data {
|
||||
Some(v) => matches!(v.as_ref(), DataValue::SecretValue(_)),
|
||||
None => false,
|
||||
},
|
||||
PartialValue::Nsuniqueid(s) => NSUNIQUEID_RE.is_match(s),
|
||||
PartialValue::DateTime(odt) => odt.offset() == time::UtcOffset::UTC,
|
||||
PartialValue::EmailAddress(mail) => validator::validate_email(mail.as_str()),
|
||||
// PartialValue::Url validated through parsing.
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
@ -1409,7 +1453,7 @@ impl Value {
|
|||
// Should this also extract the key data?
|
||||
vec![tag.to_string()]
|
||||
}
|
||||
PartialValue::RadiusCred => vec![],
|
||||
PartialValue::SecretValue => vec![],
|
||||
PartialValue::Spn(n, r) => vec![format!("{}@{}", n, r)],
|
||||
PartialValue::Uint32(u) => vec![u.to_string()],
|
||||
PartialValue::Cid(_) => vec![],
|
||||
|
@ -1417,6 +1461,7 @@ impl Value {
|
|||
debug_assert!(odt.offset() == time::UtcOffset::UTC);
|
||||
vec![odt.format(time::Format::Rfc3339)]
|
||||
}
|
||||
PartialValue::Url(u) => vec![u.to_string()],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1652,6 +1697,22 @@ mod tests {
|
|||
assert!(val3.validate());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_value_url() {
|
||||
// https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address
|
||||
let val1 = Value::new_url_s("https://localhost:8000/search?q=text#hello");
|
||||
let val2 = Value::new_url_s("https://github.com/kanidm/kanidm");
|
||||
let val3 = Value::new_url_s("ldap://foo.com");
|
||||
let inv1 = Value::new_url_s("127.0.");
|
||||
let inv2 = Value::new_url_s("🤔");
|
||||
|
||||
assert!(inv1.is_none());
|
||||
assert!(inv2.is_none());
|
||||
assert!(val1.is_some());
|
||||
assert!(val2.is_some());
|
||||
assert!(val3.is_some());
|
||||
}
|
||||
|
||||
/*
|
||||
#[test]
|
||||
fn test_schema_syntax_json_filter() {
|
||||
|
|
Loading…
Reference in a new issue