Add the ability to configure and provide Oauth2 authentication for Kanidm. (#485)

This commit is contained in:
Firstyear 2021-06-29 14:23:39 +10:00 committed by GitHub
parent 8aa0056df6
commit 1de1b2db3b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 3049 additions and 210 deletions

34
Cargo.lock generated
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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.
})
})
}

View file

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

View file

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

View file

@ -11,4 +11,5 @@
#[macro_use]
extern crate serde_derive;
pub mod oauth2;
pub mod v1;

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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"]
}
}"#;

View file

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

View file

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

View file

@ -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"
]
}
}
"#;

View file

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

View file

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

View 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 {
}
*/

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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"));
}
);
}
}

View file

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

View file

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

View file

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