Skip to content

Commit

Permalink
Add client validation
Browse files Browse the repository at this point in the history
  • Loading branch information
Threated committed Nov 8, 2023
1 parent 3d86477 commit 1582268
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 60 deletions.
4 changes: 2 additions & 2 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ services:
volumes:
# Path can be configuard via CACHE_PATH this container path is the default
- ${CACHE_PATH}:/usr/local/cache
# Used for the embeded beam proxy
# Used for the embedded beam proxy
secrets:
- privkey.pem
- root.crt.pem
Expand Down Expand Up @@ -65,7 +65,7 @@ services:
Register an Open ID Connect client at the central half of this component.

Secret type: `OIDC`
Arguments: A comma seperated list of urls permitted for redirection
Arguments: A comma separated list of urls permitted for redirection

Example:
`OIDC:MY_OIDC_CLIENT_SECRET:https://foo.com,https://bar.com`
1 change: 1 addition & 0 deletions central/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ serde = { workspace = true }
futures = { workspace = true }
serde_json = "1"
rand = "0.8"
assert-json-diff = "2.0"
13 changes: 13 additions & 0 deletions central/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,17 @@ impl OIDCProvider {
"Error creating OIDC client".into()
})
}

pub async fn validate_client(&self, name: &str, secret: &str, redirect_urls: &[String]) -> Result<bool, String> {
match self {
OIDCProvider::Keycloak(conf) => {
keycloak::validate_client(name, redirect_urls, secret, conf)
.await
.map_err(|e| {
eprintln!("Failed to validate client {name}: {e}");
"Failed to validate client. See upstrean logs.".into()
})
},
}
}
}
150 changes: 103 additions & 47 deletions central/src/keycloak.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::CLIENT;
use beam_lib::reqwest::{Result, Url, StatusCode};
use beam_lib::reqwest::{self, StatusCode, Url};
use clap::Parser;
use serde_json::json;
use serde_json::{json, Value};
use shared::SecretResult;

#[derive(Debug, Parser, Clone)]
Expand All @@ -20,7 +20,7 @@ pub struct KeyCloakConfig {
pub keycloak_realm: String,
}

async fn get_access_token(conf: &KeyCloakConfig) -> Result<String> {
async fn get_access_token(conf: &KeyCloakConfig) -> reqwest::Result<String> {
#[derive(serde::Deserialize)]
struct Token {
access_token: String,
Expand All @@ -43,7 +43,7 @@ async fn get_access_token(conf: &KeyCloakConfig) -> Result<String> {
}

#[cfg(test)]
async fn get_access_token_via_admin_login(conf: &KeyCloakConfig) -> Result<String> {
async fn get_access_token_via_admin_login(conf: &KeyCloakConfig) -> reqwest::Result<String> {
#[derive(serde::Deserialize)]
struct Token {
access_token: String,
Expand All @@ -66,22 +66,78 @@ async fn get_access_token_via_admin_login(conf: &KeyCloakConfig) -> Result<Strin
.map(|t| t.access_token)
}

#[cfg(test)]
async fn get_client(id: &str, token: &str, conf: &KeyCloakConfig) -> Result<serde_json::Value> {
dbg!(CLIENT
.get(&format!(
"{}/admin/realms/{}/clients/{id}",
conf.keycloak_url, conf.keycloak_realm
))
.bearer_auth(token)
.send()
.await?)
async fn get_client(
id: &str,
token: &str,
conf: &KeyCloakConfig,
) -> reqwest::Result<serde_json::Value> {
CLIENT
.get(&format!(
"{}/admin/realms/{}/clients/{id}",
conf.keycloak_url, conf.keycloak_realm
))
.bearer_auth(token)
.send()
.await?
.json()
.await
}

pub async fn validate_client(
id: &str,
redirect_urls: &[String],
secret: &str,
conf: &KeyCloakConfig,
) -> reqwest::Result<bool> {
let token = get_access_token(conf).await?;
let client = get_client(id, &token, conf).await?;
let wanted_client = generate_client(id, redirect_urls, secret);
Ok(client_configs_match(&client, &wanted_client))
}

fn client_configs_match(a: &Value, b: &Value) -> bool {
assert_json_diff::assert_json_matches_no_panic(
&a,
&b,
assert_json_diff::Config::new(assert_json_diff::CompareMode::Inclusive)
)
.map_err(|e| eprintln!("Clients did not match: {e}"))
.is_ok()
}

fn generate_client(name: &str, redirect_urls: &[String], secret: &str) -> Value {
json!({
"name": name,
"id": name,
"clientId": name,
"redirectUris": redirect_urls,
"secret": secret,
"publicClient": false,
"defaultClientScopes": [
"web-origins",
"acr",
"profile",
"roles",
"email",
"microprofile-jwt",
"groups"
],
"protocolMappers": [{
"name": format!("aud-mapper-{name}"),
"protocol": "openid-connect",
"protocolMapper": "oidc-audience-mapper",
"consentRequired": false,
"config": {
"included.client.audience": name,
"id.token.claim": "true",
"access.token.claim": "true"
}
}]
})
}

#[tokio::test]
async fn test_create_client() -> Result<()> {
async fn test_create_client() -> reqwest::Result<()> {
let conf = KeyCloakConfig {
keycloak_url: "http://localhost:1337".parse().unwrap(),
keycloak_id: "".to_owned(),
Expand All @@ -99,47 +155,46 @@ async fn post_client(
name: &str,
redirect_urls: Vec<String>,
conf: &KeyCloakConfig,
) -> Result<SecretResult> {
) -> reqwest::Result<SecretResult> {
let secret = generate_secret();
let generated_client = generate_client(name, &redirect_urls, &secret);
let res = CLIENT
.post(&format!(
"{}/admin/realms/{}/clients",
conf.keycloak_url, conf.keycloak_realm
))
.bearer_auth(token)
.json(&json!({
"name": name,
"id": name,
"clientId": name,
"redirectUris": redirect_urls,
"secret": secret,
"publicClient": false,
"defaultClientScopes": [
"web-origins",
"acr",
"profile",
"roles",
"email",
"microprofile-jwt",
"groups"
],
"protocolMappers": [{
"name": format!("aud-mapper-{name}"),
"protocol": "openid-connect",
"protocolMapper": "oidc-audience-mapper",
"consentRequired": false,
"config": {
"included.client.audience": name,
"id.token.claim": "true",
"access.token.claim": "true"
}
}]
}))
.json(&generated_client)
.send()
.await?;
match res.status() {
StatusCode::CREATED => Ok(SecretResult::Created(secret)),
StatusCode::CONFLICT => Ok(SecretResult::AlreadyValid),
StatusCode::CONFLICT => {
let conflicting_client = get_client(name, token, conf).await?;
if client_configs_match(&conflicting_client, &generated_client) {
Ok(conflicting_client
.as_object()
.and_then(|o| o.get("secret"))
.and_then(|v| v.as_str())
.map(|v| SecretResult::AlreadyExisted(v.into()))
.expect("These values should have a secret"))
} else {
Ok(CLIENT
.put(&format!(
"{}/admin/realms/{}/clients",
conf.keycloak_url, conf.keycloak_realm
))
.bearer_auth(token)
.json(&generated_client)
.send()
.await?
.status()
.is_success()
.then_some(secret)
.map(SecretResult::Created)
.expect("Put should be successfull"))
}
}
s => unreachable!("Unexpected statuscode {s} while creating keycloak client"),
}
}
Expand All @@ -164,8 +219,9 @@ pub async fn create_client(
name: &str,
redirect_urls: Vec<String>,
conf: &KeyCloakConfig,
) -> Result<SecretResult> {
post_client(&get_access_token(conf).await?, name, redirect_urls, conf).await
) -> reqwest::Result<SecretResult> {
let token = get_access_token(conf).await?;
post_client(&token, name, redirect_urls, conf).await
}

///
Expand Down
22 changes: 12 additions & 10 deletions central/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ pub async fn handle_secret_task(task: SecretRequestType, from: &AppId) -> Result
let name = from.as_ref().splitn(3, '.').nth(1).unwrap();
println!("Working on secret task {task:?} from {from}");
match task {
// SecretRequestType::ValidateOrCreate { current, request } if is_valid_secret(&current, &request, name).await? => Ok(SecretResult::AlreadyValid),
SecretRequestType::ValidateOrCreate { current, request } if is_valid(&current, &request, name).await? => Ok(SecretResult::AlreadyValid),
SecretRequestType::ValidateOrCreate { request, .. } |
SecretRequestType::Create(request) => create_secret(request, name).await,
}
Expand All @@ -86,18 +86,20 @@ pub async fn create_secret(request: SecretRequest, name: &str) -> Result<SecretR
match request {
SecretRequest::OpenIdConnect { redirect_urls } => {
let Some(oidc_provider) = OIDC_PROVIDER.as_ref() else {
return Err("No OIDC provider configuard!".into());
return Err("No OIDC provider configured!".into());
};
oidc_provider.create_client(name, redirect_urls).await
}
}
}

// pub async fn is_valid_secret(current: &str, request: &SecretRequest, name: &str) -> Result<bool, String> {
// match request {
// SecretRequest::OpenIdConnect { redirect_urls } => {
// // todo!("Validate if current was already created")
// Ok(false)
// },
// }
// }
pub async fn is_valid(secret: &str, request: &SecretRequest, name: &str) -> Result<bool, String> {
match request {
SecretRequest::OpenIdConnect { redirect_urls } => {
let Some(oidc_provider) = OIDC_PROVIDER.as_ref() else {
return Err("No OIDC provider configured!".into());
};
oidc_provider.validate_client(name, secret, redirect_urls).await
},
}
}
6 changes: 5 additions & 1 deletion local/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ async fn main() -> ExitCode {
Ok(SecretResult::Created(new_value)) => {
println!("{name} has been created");
cache.entry(name.clone()).or_insert(new_value);
},
Ok(SecretResult::AlreadyExisted(secret)) => {
println!("{name} already existed but was not cached");
cache.entry(name.clone()).or_insert(secret);
}
Err(e) => {
exit_code = ExitCode::FAILURE;
Expand All @@ -83,7 +87,7 @@ async fn send_secret_request(
) -> beam_lib::Result<Vec<Result<SecretResult, String>>> {
wait_for_beam_proxy().await?;
let mut tasks = Vec::with_capacity(secret_tasks.len());
// Partion tasks based on task type to send them to the correct app to fulfill the task
// Partition tasks based on task type to send them to the correct app to fulfill the task
let (oidc, rest) = secret_tasks
.into_iter()
.partition(|v| matches!(v.deref(), SecretRequest::OpenIdConnect { .. }));
Expand Down
1 change: 1 addition & 0 deletions shared/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub enum SecretRequest {
pub enum SecretResult {
AlreadyValid,
Created(String),
AlreadyExisted(String)
}

#[derive(Debug, Serialize, Deserialize, Clone)]
Expand Down

0 comments on commit 1582268

Please sign in to comment.