From 21f3c6294aa1ec231afa223390fb89bf7cfca836 Mon Sep 17 00:00:00 2001 From: Martin Jurk Date: Wed, 18 Sep 2024 14:27:38 +0200 Subject: [PATCH 01/37] test token --- central/src/authentik.rs | 439 +++++++++++++++++++++++++++++++++++++++ central/src/config.rs | 4 +- central/src/keycloak.rs | 2 +- central/src/main.rs | 1 + dev/docker-compose.yaml | 20 +- 5 files changed, 453 insertions(+), 13 deletions(-) create mode 100644 central/src/authentik.rs diff --git a/central/src/authentik.rs b/central/src/authentik.rs new file mode 100644 index 0000000..4e27857 --- /dev/null +++ b/central/src/authentik.rs @@ -0,0 +1,439 @@ +use crate::CLIENT; +use beam_lib::reqwest::{self, StatusCode, Url}; +use clap::Parser; +use serde_json::{json, Value}; +use shared::{OIDCConfig, SecretResult}; + +#[derive(Debug, Parser, Clone)] +pub struct AuthentikConfig { + /// authentik url + #[clap(long, env)] + pub authentik_url: Url, + #[clap(long, env)] + pub authentik_id: String, + #[clap(long, env)] + pub authentik_secret: String, + #[clap(long, env, default_value = "master")] + pub authentik_realm: String, + #[clap(long, env, value_parser, value_delimiter = ',', default_values_t = [] as [String; 0])] + pub authentik_service_account_roles: Vec, + #[clap(long, env, value_parser, value_delimiter = ',', default_values_t = [] as [String; 0])] + pub authentik_groups_per_bh: Vec, +} + +async fn get_access_token(conf: &AuthentikConfig) -> reqwest::Result { + #[derive(serde::Deserialize)] + struct Token { + access_token: String, + } + CLIENT + .post(&format!( + "{}/application/o/token", + conf.authentik_url + )) + .form(&json!({ + "grant_type": "client_credentials", + "client_id": conf.authentik_id, + "username": "", + "passord": "", + "scope": "" + })) + .send() + .await? + .json::() + .await + .map(|t| t.access_token) +} + +#[cfg(test)] +async fn get_access_token_via_admin_login() -> reqwest::Result { + #[derive(serde::Deserialize)] + struct Token { + access_token: String, + } + CLIENT + .post(&format!( + "{}/application/o/token", + if cfg!(test) { "http://localhost:9000"} else { "http://keycloak:8080" } + )) + .form(&json!({ + "grant_type": "client_credentials", + "client_id": "MI4DbeyktmjbXJRmUY9tkWvhK7yOzly139EgzhPZ", + "client_secret": "YGcFnXQMI7HqeDUWClhTkZmPtYj4aB2z3khnoMNpCo8CgTOhUqqOFE56dP2WOJoPGOeqdPsVCrR4yvjnJviYK6dY8WeykDqnzAO1xCLHOsPxefcSAa21qe0ru2bwWBi7", + "scope": "openid" + })) + .send() + .await? + .json::() + .await + .map(|t| t.access_token) +} + +async fn get_client( + name: &str, + token: &str, + oidc_client_config: &OIDCConfig, + conf: &KeyCloakConfig, +) -> reqwest::Result { + let id = format!("{name}-{}", if oidc_client_config.is_public { "public" } else { "private" }); + 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( + name: &str, + oidc_client_config: &OIDCConfig, + secret: &str, + conf: &KeyCloakConfig, +) -> reqwest::Result { + let token = get_access_token(conf).await?; + compare_clients(&token, name, oidc_client_config, conf, secret).await +} + +async fn compare_clients( + token: &str, + name: &str, + oidc_client_config: &OIDCConfig, + conf: &KeyCloakConfig, + secret: &str, +) -> Result { + let client = get_client(name, token, oidc_client_config, conf).await?; + let wanted_client = generate_client(name, oidc_client_config, secret); + Ok(client.get("secret") == wanted_client.get("secret") + && client_configs_match(&client, &wanted_client)) +} + +fn client_configs_match(a: &Value, b: &Value) -> bool { + let includes_other_json_array = |key, comparator: &dyn Fn(_, _) -> bool| a + .get(key) + .and_then(Value::as_array) + .is_some_and(|a_values| b + .get(key) + .and_then(Value::as_array) + .is_some_and(|vec| vec.iter().all(|v| comparator(a_values, v))) + ); + + a.get("name") == b.get("name") + && includes_other_json_array("defaultClientScopes", &|a_v, v| a_v.contains(v)) + && includes_other_json_array("redirectUris", &|a_v, v| a_v.contains(v)) + && includes_other_json_array("protocolMappers", &|a_v, v| a_v.iter().any(|a_v| a_v.get("name") == v.get("name"))) +} + +fn generate_client(name: &str, oidc_client_config: &OIDCConfig, secret: &str) -> Value { + let secret = (!oidc_client_config.is_public).then_some(secret); + let id = format!("{name}-{}", if oidc_client_config.is_public { "public" } else { "private" }); + let mut json = json!({ + "name": id, + "id": id, + "clientId": id, + "redirectUris": oidc_client_config.redirect_urls, + "webOrigins": ["+"], // Will allow all hosts that are named in redirectUris. This is not the same as '*' + "publicClient": oidc_client_config.is_public, + "serviceAccountsEnabled": !oidc_client_config.is_public, + "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": id, + "id.token.claim": "true", + "access.token.claim": "true" + } + }] + }); + if let Some(secret) = secret { + json.as_object_mut().unwrap().insert("secret".to_owned(), secret.into()); + } + json +} + +#[cfg(test)] +async fn setup_keycloak() -> reqwest::Result<(String, KeyCloakConfig)> { + let token = get_access_token_via_admin_login().await?; + let res = CLIENT + .post("http://localhost:1337/admin/realms/master/client-scopes") + .bearer_auth(&token) + .json(&json!({ + "name": "groups", + "protocol": "openid-connect" + })) + .send() + .await?; + dbg!(&res.status()); + Ok(( + token, + KeyCloakConfig { + keycloak_url: "http://localhost:1337".parse().unwrap(), + keycloak_id: "unused in tests".into(), + keycloak_secret: "unused in tests".into(), + keycloak_realm: "master".into(), + keycloak_service_account_roles: vec!["query-users".into(), "view-users".into()], + keycloak_groups_per_bh: vec!["DKTK_CCP_#".into(), "DKTK_CCP_#_Verwalter".into()], + }, + )) +} + +#[ignore = "Requires setting up a keycloak"] +#[tokio::test] +async fn test_create_client() -> reqwest::Result<()> { + let (token, conf) = setup_keycloak().await?; + let name = "test"; + // public client + let client_config = OIDCConfig { is_public: true, redirect_urls: vec!["http://foo/bar".into()] }; + let (SecretResult::Created(pw) | SecretResult::AlreadyExisted(pw)) = dbg!(post_client(&token, name, &client_config, &conf).await?) else { + panic!("Not created or existed") + }; + let c = dbg!(get_client(name, &token, &client_config, &conf).await.unwrap()); + assert!(client_configs_match(&c, &generate_client(name, &client_config, &pw))); + assert!(dbg!(compare_clients(&token, name, &client_config, &conf, &pw).await?)); + + // private client + let client_config = OIDCConfig { is_public: false, redirect_urls: vec!["http://foo/bar".into()] }; + let (SecretResult::Created(pw) | SecretResult::AlreadyExisted(pw)) = dbg!(post_client(&token, name, &client_config, &conf).await?) else { + panic!("Not created or existed") + }; + let c = dbg!(get_client(name, &token, &client_config, &conf).await.unwrap()); + assert!(client_configs_match(&c, &generate_client(name, &client_config, &pw))); + assert!(dbg!(compare_clients(&token, name, &client_config, &conf, &pw).await?)); + + Ok(()) +} + +async fn post_client( + token: &str, + name: &str, + oidc_client_config: &OIDCConfig, + conf: &KeyCloakConfig, +) -> reqwest::Result { + let secret = if !oidc_client_config.is_public { + generate_secret() + } else { + String::with_capacity(0) + }; + let generated_client = generate_client(name, oidc_client_config, &secret); + let res = CLIENT + .post(&format!( + "{}/admin/realms/{}/clients", + conf.keycloak_url, conf.keycloak_realm + )) + .bearer_auth(token) + .json(&generated_client) + .send() + .await?; + // Create groups for this client + create_groups(name, token, conf).await?; + match res.status() { + StatusCode::CREATED => { + println!("Client for {name} created."); + if !oidc_client_config.is_public { + let client_id = generated_client + .get("clientId") + .and_then(Value::as_str) + .expect("Always present"); + add_service_account_roles(token, client_id, conf).await?; + } + Ok(SecretResult::Created(secret)) + } + StatusCode::CONFLICT => { + let conflicting_client = get_client(name, token, oidc_client_config, conf).await?; + if client_configs_match(&conflicting_client, &generated_client) { + Ok(SecretResult::AlreadyExisted(conflicting_client + .as_object() + .and_then(|o| o.get("secret")) + .and_then(Value::as_str) + .unwrap_or("") + .to_owned())) + } else { + Ok(CLIENT + .put(&format!( + "{}/admin/realms/{}/clients/{}", + conf.keycloak_url, + conf.keycloak_realm, + conflicting_client + .get("clientId") + .and_then(Value::as_str) + .expect("We have a valid client") + )) + .bearer_auth(token) + .json(&generated_client) + .send() + .await? + .status() + .is_success() + .then_some(SecretResult::AlreadyExisted(secret)) + .expect("We know the client already exists so updating should be successful")) + } + } + s => unreachable!("Unexpected statuscode {s} while creating keycloak client. {res:?}"), + } +} + +async fn create_groups(name: &str, token: &str, conf: &KeyCloakConfig) -> reqwest::Result<()> { + let capitalize = |s: &str| { + let mut chrs = s.chars(); + chrs.next().map(char::to_uppercase).map(Iterator::collect).unwrap_or(String::new()) + chrs.as_str() + }; + let name = capitalize(name); + for group in &conf.keycloak_groups_per_bh { + post_group(&group.replace('#', &name), token, conf).await?; + } + Ok(()) +} + +async fn post_group(name: &str, token: &str, conf: &KeyCloakConfig) -> reqwest::Result<()> { + let res = CLIENT + .post(&format!( + "{}/admin/realms/{}/groups", + conf.keycloak_url, conf.keycloak_realm + )) + .bearer_auth(token) + .json(&json!({ + "name": name + })) + .send() + .await?; + match res.status() { + StatusCode::CREATED => println!("Created group {name}"), + StatusCode::CONFLICT => println!("Group {name} already existed"), + s => unreachable!("Unexpected statuscode {s} while creating group {name}") + } + Ok(()) +} + +fn generate_secret() -> String { + use rand::Rng; + const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\ + abcdefghijklmnopqrstuvwxyz\ + 0123456789"; + const PASSWORD_LEN: usize = 30; + let mut rng = rand::thread_rng(); + + (0..PASSWORD_LEN) + .map(|_| { + let idx = rng.gen_range(0..CHARSET.len()); + CHARSET[idx] as char + }) + .collect() +} + +pub async fn create_client( + name: &str, + oidc_client_config: OIDCConfig, + conf: &KeyCloakConfig, +) -> reqwest::Result { + let token = get_access_token(conf).await?; + post_client(&token, name, &oidc_client_config, conf).await +} + +#[ignore = "Requires setting up a keycloak"] +#[tokio::test] +async fn service_account_test() -> reqwest::Result<()> { + let (token, conf) = setup_keycloak().await?; + create_groups("test", &token, &conf).await?; + // dbg!(get_realm_permission_roles(&token, &conf).await?); + // add_service_account_roles(&token, "test-private", &conf).await?; + Ok(()) +} + +async fn add_service_account_roles( + token: &str, + client_id: &str, + conf: &KeyCloakConfig, +) -> reqwest::Result<()> { + if conf.keycloak_service_account_roles.is_empty() { + return Ok(()); + } + #[derive(serde::Deserialize)] + struct UserIdExtractor { + id: String, + } + let service_account_id = CLIENT.get(&format!( + "{}/admin/realms/{}/clients/{}/service-account-user", + conf.keycloak_url, conf.keycloak_realm, client_id + )) + .bearer_auth(token) + .send() + .await? + .json::() + .await? + .id; + let roles: Vec<_> = get_realm_permission_roles(token, conf) + .await? + .into_iter() + .filter(|f| conf.keycloak_service_account_roles.contains(&f.name)) + .collect(); + + assert_eq!(roles.len(), conf.keycloak_service_account_roles.len(), "Failed to find all required service account roles got {roles:#?} but expected all of these: {:#?}", conf.keycloak_service_account_roles); + let realm_id = roles[0].container_id.clone(); + CLIENT.post(&format!( + "{}/admin/realms/{}/users/{}/role-mappings/clients/{}", + conf.keycloak_url, conf.keycloak_realm, service_account_id, realm_id + )) + .bearer_auth(token) + .json(&roles) + .send() + .await?; + + Ok(()) +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +struct ServiceAccountRole { + id: String, + #[serde(rename = "containerId", skip_serializing)] + container_id: String, + name: String +} + +async fn get_realm_permission_roles(token: &str, conf: &KeyCloakConfig) -> reqwest::Result> { + #[derive(Debug, serde::Deserialize)] + struct RealmId { + id: String, + #[serde(rename = "clientId")] + client_id: String + } + let permission_realm = if conf.keycloak_realm == "master" { + "master-realm" + } else { + "realm-management" + }; + let res = CLIENT.get(&format!( + "{}/admin/realms/{}/clients/?q={permission_realm}&search", + conf.keycloak_url, conf.keycloak_realm + )) + .bearer_auth(token) + .send() + .await? + .json::>() + .await?; + let role_client = res.into_iter() + .find(|v| v.client_id.starts_with(permission_realm)) + .expect(&format!("Failed to find realm id for {permission_realm}")); + CLIENT.get(&format!( + "{}/admin/realms/{}/clients/{}/roles", + conf.keycloak_url, conf.keycloak_realm, role_client.id + )) + .bearer_auth(token) + .send() + .await? + .json() + .await +} diff --git a/central/src/config.rs b/central/src/config.rs index b9cee5b..ec744ca 100644 --- a/central/src/config.rs +++ b/central/src/config.rs @@ -38,7 +38,7 @@ impl OIDCProvider { pub async fn create_client(&self, name: &str, oidc_client_config: OIDCConfig) -> Result { match self { - OIDCProvider::Keycloak(conf) => keycloak::create_client(name, oidc_client_config, conf).await, + OIDCProvider::Keycloak(conf) => authentik::create_client(name, oidc_client_config, conf).await, }.map_err(|e| { println!("Failed to create client: {e}"); "Error creating OIDC client".into() @@ -48,7 +48,7 @@ impl OIDCProvider { pub async fn validate_client(&self, name: &str, secret: &str, oidc_client_config: &OIDCConfig) -> Result { match self { OIDCProvider::Keycloak(conf) => { - keycloak::validate_client(name, oidc_client_config, secret, conf) + authentik::validate_client(name, oidc_client_config, secret, conf) .await .map_err(|e| { eprintln!("Failed to validate client {name}: {e}"); diff --git a/central/src/keycloak.rs b/central/src/keycloak.rs index d20590f..43acef2 100644 --- a/central/src/keycloak.rs +++ b/central/src/keycloak.rs @@ -439,4 +439,4 @@ async fn get_realm_permission_roles(token: &str, conf: &KeyCloakConfig) -> reqwe .await? .json() .await -} +} \ No newline at end of file diff --git a/central/src/main.rs b/central/src/main.rs index e6a2e5b..c4aff9b 100644 --- a/central/src/main.rs +++ b/central/src/main.rs @@ -7,6 +7,7 @@ use once_cell::sync::Lazy; use shared::{SecretRequest, SecretResult, SecretRequestType}; mod config; +mod authentik; mod keycloak; pub static CONFIG: Lazy = Lazy::new(Config::parse); diff --git a/dev/docker-compose.yaml b/dev/docker-compose.yaml index 118121f..cc1553a 100644 --- a/dev/docker-compose.yaml +++ b/dev/docker-compose.yaml @@ -26,26 +26,26 @@ services: dockerfile: Dockerfile.central image: samply/secret-sync-central:latest depends_on: - - keycloak + - authentik environment: - BEAM_URL=http://proxy:8082 - BEAM_ID=app2.proxy2.broker - BEAM_SECRET=App1Secret - - KEYCLOAK_URL=http://keycloak:8080 - - KEYCLOAK_ID=admin - - KEYCLOAK_SECRET=admin - - KEYCLOAK_SERVICE_ACCOUNT_ROLES=query-users + - AUTHENTIK_URL=http://authentik:9000 + - AUTHENTIK_ID=admin + - AUTHENTIK_SECRET=admin + - AUTHENTIK_SERVICE_ACCOUNT_ROLES=query-users extra_hosts: - "proxy:${PROXY_IP}" - keycloak: - image: quay.io/keycloak/keycloak:latest + authentik: + image: beryju/authentik:latest command: start-dev ports: - - "1337:8080" + - "9000:9000" environment: - - KEYCLOAK_ADMIN=admin - - KEYCLOAK_ADMIN_PASSWORD=admin + - PG_PASS=admin + - AUTHENTIK_SECRET_KEY=admin secrets: privkey.pem: From cba8b8811f3513080cb6631a401ed2cf34dbfd3f Mon Sep 17 00:00:00 2001 From: Martin Jurk Date: Wed, 18 Sep 2024 14:39:57 +0200 Subject: [PATCH 02/37] test only token set --- central/src/authentik.rs | 9 +++++---- central/src/config.rs | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/central/src/authentik.rs b/central/src/authentik.rs index 4e27857..7137b2b 100644 --- a/central/src/authentik.rs +++ b/central/src/authentik.rs @@ -33,10 +33,9 @@ async fn get_access_token(conf: &AuthentikConfig) -> reqwest::Result { )) .form(&json!({ "grant_type": "client_credentials", - "client_id": conf.authentik_id, - "username": "", - "passord": "", - "scope": "" + "client_id": "MI4DbeyktmjbXJRmUY9tkWvhK7yOzly139EgzhPZ", + "client_secret": "YGcFnXQMI7HqeDUWClhTkZmPtYj4aB2z3khnoMNpCo8CgTOhUqqOFE56dP2WOJoPGOeqdPsVCrR4yvjnJviYK6dY8WeykDqnzAO1xCLHOsPxefcSAa21qe0ru2bwWBi7", + "scope": "openid" })) .send() .await? @@ -69,6 +68,7 @@ async fn get_access_token_via_admin_login() -> reqwest::Result { .map(|t| t.access_token) } +/* async fn get_client( name: &str, token: &str, @@ -437,3 +437,4 @@ async fn get_realm_permission_roles(token: &str, conf: &KeyCloakConfig) -> reqwe .json() .await } +*/ \ No newline at end of file diff --git a/central/src/config.rs b/central/src/config.rs index ec744ca..b9cee5b 100644 --- a/central/src/config.rs +++ b/central/src/config.rs @@ -38,7 +38,7 @@ impl OIDCProvider { pub async fn create_client(&self, name: &str, oidc_client_config: OIDCConfig) -> Result { match self { - OIDCProvider::Keycloak(conf) => authentik::create_client(name, oidc_client_config, conf).await, + OIDCProvider::Keycloak(conf) => keycloak::create_client(name, oidc_client_config, conf).await, }.map_err(|e| { println!("Failed to create client: {e}"); "Error creating OIDC client".into() @@ -48,7 +48,7 @@ impl OIDCProvider { pub async fn validate_client(&self, name: &str, secret: &str, oidc_client_config: &OIDCConfig) -> Result { match self { OIDCProvider::Keycloak(conf) => { - authentik::validate_client(name, oidc_client_config, secret, conf) + keycloak::validate_client(name, oidc_client_config, secret, conf) .await .map_err(|e| { eprintln!("Failed to validate client {name}: {e}"); From f96eb23e42c1aaff28fdfe42b8909fd49f2b013e Mon Sep 17 00:00:00 2001 From: Martin Jurk Date: Wed, 18 Sep 2024 16:28:19 +0200 Subject: [PATCH 03/37] test token access --- central/src/{ => auth}/authentik.rs | 23 ----------------------- central/src/auth/authentik_test.rs | 29 +++++++++++++++++++++++++++++ central/src/{ => auth}/config.rs | 2 +- central/src/{ => auth}/keycloak.rs | 0 central/src/auth/mod.rs | 4 ++++ central/src/main.rs | 6 ++---- 6 files changed, 36 insertions(+), 28 deletions(-) rename central/src/{ => auth}/authentik.rs (94%) create mode 100644 central/src/auth/authentik_test.rs rename central/src/{ => auth}/config.rs (97%) rename central/src/{ => auth}/keycloak.rs (100%) create mode 100644 central/src/auth/mod.rs diff --git a/central/src/authentik.rs b/central/src/auth/authentik.rs similarity index 94% rename from central/src/authentik.rs rename to central/src/auth/authentik.rs index 7137b2b..92f2a82 100644 --- a/central/src/authentik.rs +++ b/central/src/auth/authentik.rs @@ -44,29 +44,6 @@ async fn get_access_token(conf: &AuthentikConfig) -> reqwest::Result { .map(|t| t.access_token) } -#[cfg(test)] -async fn get_access_token_via_admin_login() -> reqwest::Result { - #[derive(serde::Deserialize)] - struct Token { - access_token: String, - } - CLIENT - .post(&format!( - "{}/application/o/token", - if cfg!(test) { "http://localhost:9000"} else { "http://keycloak:8080" } - )) - .form(&json!({ - "grant_type": "client_credentials", - "client_id": "MI4DbeyktmjbXJRmUY9tkWvhK7yOzly139EgzhPZ", - "client_secret": "YGcFnXQMI7HqeDUWClhTkZmPtYj4aB2z3khnoMNpCo8CgTOhUqqOFE56dP2WOJoPGOeqdPsVCrR4yvjnJviYK6dY8WeykDqnzAO1xCLHOsPxefcSAa21qe0ru2bwWBi7", - "scope": "openid" - })) - .send() - .await? - .json::() - .await - .map(|t| t.access_token) -} /* async fn get_client( diff --git a/central/src/auth/authentik_test.rs b/central/src/auth/authentik_test.rs new file mode 100644 index 0000000..5f5524d --- /dev/null +++ b/central/src/auth/authentik_test.rs @@ -0,0 +1,29 @@ +use beam_lib::reqwest::{self, Error, StatusCode, Url}; +use serde_json::json; + +use crate::CLIENT; + + +#[tokio::test] +async fn get_access_token() { + let path_url = "http://localhost:9000/application/o/token"; + #[derive(serde::Deserialize, Debug)] + struct Token { + access_token: String, + } + let test = CLIENT + .post(path_url) + .form(&json!({ + "grant_type": "client_credentials", + "client_id": "MI4DbeyktmjbXJRmUY9tkWvhK7yOzly139EgzhPZ", + "client_secret": "YGcFnXQMI7HqeDUWClhTkZmPtYj4aB2z3khnoMNpCo8CgTOhUqqOFE56dP2WOJoPGOeqdPsVCrR4yvjnJviYK6dY8WeykDqnzAO1xCLHOsPxefcSAa21qe0ru2bwWBi7", + "scope": "openid" + })) + .send() + .await + .unwrap() + .json::() + .await + .unwrap(); + dbg!(test); +} diff --git a/central/src/config.rs b/central/src/auth/config.rs similarity index 97% rename from central/src/config.rs rename to central/src/auth/config.rs index b9cee5b..bb80d87 100644 --- a/central/src/config.rs +++ b/central/src/auth/config.rs @@ -4,7 +4,7 @@ use beam_lib::{AppId, reqwest::Url}; use clap::Parser; use shared::{SecretResult, OIDCConfig}; -use crate::keycloak::{KeyCloakConfig, self}; +use crate::auth::keycloak::{KeyCloakConfig, self}; /// Central secret sync #[derive(Debug, Parser)] diff --git a/central/src/keycloak.rs b/central/src/auth/keycloak.rs similarity index 100% rename from central/src/keycloak.rs rename to central/src/auth/keycloak.rs diff --git a/central/src/auth/mod.rs b/central/src/auth/mod.rs new file mode 100644 index 0000000..44b48f6 --- /dev/null +++ b/central/src/auth/mod.rs @@ -0,0 +1,4 @@ +mod keycloak; +pub(crate) mod config; +mod authentik; +mod authentik_test; \ No newline at end of file diff --git a/central/src/main.rs b/central/src/main.rs index c4aff9b..2840305 100644 --- a/central/src/main.rs +++ b/central/src/main.rs @@ -2,13 +2,11 @@ use std::{collections::HashSet, time::Duration}; use beam_lib::{reqwest::Client, BeamClient, BlockingOptions, TaskRequest, TaskResult, AppId}; use clap::Parser; -use config::{Config, OIDCProvider}; +use auth::config::{Config, OIDCProvider}; use once_cell::sync::Lazy; use shared::{SecretRequest, SecretResult, SecretRequestType}; -mod config; -mod authentik; -mod keycloak; +mod auth; pub static CONFIG: Lazy = Lazy::new(Config::parse); From d9f5ee5bbab2827db03c57d55fbc6e3bb07a9cef Mon Sep 17 00:00:00 2001 From: Martin Jurk Date: Fri, 20 Sep 2024 11:49:13 +0200 Subject: [PATCH 04/37] test form x www urlencoded --- central/src/auth/authentik_test.rs | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/central/src/auth/authentik_test.rs b/central/src/auth/authentik_test.rs index 5f5524d..378e1f0 100644 --- a/central/src/auth/authentik_test.rs +++ b/central/src/auth/authentik_test.rs @@ -1,4 +1,5 @@ use beam_lib::reqwest::{self, Error, StatusCode, Url}; +use serde::{Deserialize, Serialize}; use serde_json::json; use crate::CLIENT; @@ -7,23 +8,27 @@ use crate::CLIENT; #[tokio::test] async fn get_access_token() { let path_url = "http://localhost:9000/application/o/token"; - #[derive(serde::Deserialize, Debug)] + #[derive(Deserialize, Serialize, Debug)] struct Token { access_token: String, } - let test = CLIENT + let response = CLIENT .post(path_url) - .form(&json!({ - "grant_type": "client_credentials", - "client_id": "MI4DbeyktmjbXJRmUY9tkWvhK7yOzly139EgzhPZ", - "client_secret": "YGcFnXQMI7HqeDUWClhTkZmPtYj4aB2z3khnoMNpCo8CgTOhUqqOFE56dP2WOJoPGOeqdPsVCrR4yvjnJviYK6dY8WeykDqnzAO1xCLHOsPxefcSAa21qe0ru2bwWBi7", - "scope": "openid" - })) + .header("Content-Type", "applikation/x-www-form-urlencoded") + .form(&[ + ("grant_type", "client_credentials"), + ("client_id", "MI4DbeyktmjbXJRmUY9tkWvhK7yOzly139EgzhPZ"), + ("client_secret", "YGcFnXQMI7HqeDUWClhTkZmPtYj4aB2z3khnoMNpCo8CgTOhUqqOFE56dP2WOJoPGOeqdPsVCrR4yvjnJviYK6dY8WeykDqnzAO1xCLHOsPxefcSAa21qe0ru2bwWBi7"), + ("scope", "openid") + ]) .send() .await - .unwrap() + .expect("no response"); + + let t = response .json::() .await - .unwrap(); - dbg!(test); + .expect("Token can not be parseed"); + dbg!(&t); + assert!(!t.access_token.is_empty()); } From 19a6934f08c63146d1486b15cbb54608283c649a Mon Sep 17 00:00:00 2001 From: Martin Jurk Date: Fri, 20 Sep 2024 12:10:40 +0200 Subject: [PATCH 05/37] test token emty --- central/src/auth/authentik_test.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/central/src/auth/authentik_test.rs b/central/src/auth/authentik_test.rs index 378e1f0..03dd681 100644 --- a/central/src/auth/authentik_test.rs +++ b/central/src/auth/authentik_test.rs @@ -11,10 +11,13 @@ async fn get_access_token() { #[derive(Deserialize, Serialize, Debug)] struct Token { access_token: String, + token_type: String, + expires_in: i32, + id_token: String } let response = CLIENT .post(path_url) - .header("Content-Type", "applikation/x-www-form-urlencoded") + .header("Content-Type", "application/x-www-form-urlencoded") .form(&[ ("grant_type", "client_credentials"), ("client_id", "MI4DbeyktmjbXJRmUY9tkWvhK7yOzly139EgzhPZ"), @@ -24,11 +27,13 @@ async fn get_access_token() { .send() .await .expect("no response"); + let raw = response.text().await.expect("no resoponse"); + dbg!(&raw); - let t = response - .json::() - .await - .expect("Token can not be parseed"); - dbg!(&t); - assert!(!t.access_token.is_empty()); + // let t = response + // .json::() + // .await + // .expect("Token can not be parseed"); + // dbg!(&t); + // assert!(!t.access_token.is_empty()); } From cc1c1285a7fc325b90c3f1ed42b5f99c5e802995 Mon Sep 17 00:00:00 2001 From: Martin Jurk Date: Tue, 24 Sep 2024 09:15:27 +0200 Subject: [PATCH 06/37] test admin --- central/src/auth/authentik.rs | 417 ------------------ central/src/auth/authentik/group.rs | 124 ++++++ central/src/auth/authentik/mod.rs | 246 +++++++++++ central/src/auth/authentik/test.rs | 126 ++++++ central/src/auth/authentik_test.rs | 39 -- central/src/auth/config.rs | 7 +- .../src/auth/{keycloak.rs => keycloak/mod.rs} | 0 central/src/auth/mod.rs | 3 +- 8 files changed, 503 insertions(+), 459 deletions(-) delete mode 100644 central/src/auth/authentik.rs create mode 100644 central/src/auth/authentik/group.rs create mode 100644 central/src/auth/authentik/mod.rs create mode 100644 central/src/auth/authentik/test.rs delete mode 100644 central/src/auth/authentik_test.rs rename central/src/auth/{keycloak.rs => keycloak/mod.rs} (100%) diff --git a/central/src/auth/authentik.rs b/central/src/auth/authentik.rs deleted file mode 100644 index 92f2a82..0000000 --- a/central/src/auth/authentik.rs +++ /dev/null @@ -1,417 +0,0 @@ -use crate::CLIENT; -use beam_lib::reqwest::{self, StatusCode, Url}; -use clap::Parser; -use serde_json::{json, Value}; -use shared::{OIDCConfig, SecretResult}; - -#[derive(Debug, Parser, Clone)] -pub struct AuthentikConfig { - /// authentik url - #[clap(long, env)] - pub authentik_url: Url, - #[clap(long, env)] - pub authentik_id: String, - #[clap(long, env)] - pub authentik_secret: String, - #[clap(long, env, default_value = "master")] - pub authentik_realm: String, - #[clap(long, env, value_parser, value_delimiter = ',', default_values_t = [] as [String; 0])] - pub authentik_service_account_roles: Vec, - #[clap(long, env, value_parser, value_delimiter = ',', default_values_t = [] as [String; 0])] - pub authentik_groups_per_bh: Vec, -} - -async fn get_access_token(conf: &AuthentikConfig) -> reqwest::Result { - #[derive(serde::Deserialize)] - struct Token { - access_token: String, - } - CLIENT - .post(&format!( - "{}/application/o/token", - conf.authentik_url - )) - .form(&json!({ - "grant_type": "client_credentials", - "client_id": "MI4DbeyktmjbXJRmUY9tkWvhK7yOzly139EgzhPZ", - "client_secret": "YGcFnXQMI7HqeDUWClhTkZmPtYj4aB2z3khnoMNpCo8CgTOhUqqOFE56dP2WOJoPGOeqdPsVCrR4yvjnJviYK6dY8WeykDqnzAO1xCLHOsPxefcSAa21qe0ru2bwWBi7", - "scope": "openid" - })) - .send() - .await? - .json::() - .await - .map(|t| t.access_token) -} - - -/* -async fn get_client( - name: &str, - token: &str, - oidc_client_config: &OIDCConfig, - conf: &KeyCloakConfig, -) -> reqwest::Result { - let id = format!("{name}-{}", if oidc_client_config.is_public { "public" } else { "private" }); - 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( - name: &str, - oidc_client_config: &OIDCConfig, - secret: &str, - conf: &KeyCloakConfig, -) -> reqwest::Result { - let token = get_access_token(conf).await?; - compare_clients(&token, name, oidc_client_config, conf, secret).await -} - -async fn compare_clients( - token: &str, - name: &str, - oidc_client_config: &OIDCConfig, - conf: &KeyCloakConfig, - secret: &str, -) -> Result { - let client = get_client(name, token, oidc_client_config, conf).await?; - let wanted_client = generate_client(name, oidc_client_config, secret); - Ok(client.get("secret") == wanted_client.get("secret") - && client_configs_match(&client, &wanted_client)) -} - -fn client_configs_match(a: &Value, b: &Value) -> bool { - let includes_other_json_array = |key, comparator: &dyn Fn(_, _) -> bool| a - .get(key) - .and_then(Value::as_array) - .is_some_and(|a_values| b - .get(key) - .and_then(Value::as_array) - .is_some_and(|vec| vec.iter().all(|v| comparator(a_values, v))) - ); - - a.get("name") == b.get("name") - && includes_other_json_array("defaultClientScopes", &|a_v, v| a_v.contains(v)) - && includes_other_json_array("redirectUris", &|a_v, v| a_v.contains(v)) - && includes_other_json_array("protocolMappers", &|a_v, v| a_v.iter().any(|a_v| a_v.get("name") == v.get("name"))) -} - -fn generate_client(name: &str, oidc_client_config: &OIDCConfig, secret: &str) -> Value { - let secret = (!oidc_client_config.is_public).then_some(secret); - let id = format!("{name}-{}", if oidc_client_config.is_public { "public" } else { "private" }); - let mut json = json!({ - "name": id, - "id": id, - "clientId": id, - "redirectUris": oidc_client_config.redirect_urls, - "webOrigins": ["+"], // Will allow all hosts that are named in redirectUris. This is not the same as '*' - "publicClient": oidc_client_config.is_public, - "serviceAccountsEnabled": !oidc_client_config.is_public, - "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": id, - "id.token.claim": "true", - "access.token.claim": "true" - } - }] - }); - if let Some(secret) = secret { - json.as_object_mut().unwrap().insert("secret".to_owned(), secret.into()); - } - json -} - -#[cfg(test)] -async fn setup_keycloak() -> reqwest::Result<(String, KeyCloakConfig)> { - let token = get_access_token_via_admin_login().await?; - let res = CLIENT - .post("http://localhost:1337/admin/realms/master/client-scopes") - .bearer_auth(&token) - .json(&json!({ - "name": "groups", - "protocol": "openid-connect" - })) - .send() - .await?; - dbg!(&res.status()); - Ok(( - token, - KeyCloakConfig { - keycloak_url: "http://localhost:1337".parse().unwrap(), - keycloak_id: "unused in tests".into(), - keycloak_secret: "unused in tests".into(), - keycloak_realm: "master".into(), - keycloak_service_account_roles: vec!["query-users".into(), "view-users".into()], - keycloak_groups_per_bh: vec!["DKTK_CCP_#".into(), "DKTK_CCP_#_Verwalter".into()], - }, - )) -} - -#[ignore = "Requires setting up a keycloak"] -#[tokio::test] -async fn test_create_client() -> reqwest::Result<()> { - let (token, conf) = setup_keycloak().await?; - let name = "test"; - // public client - let client_config = OIDCConfig { is_public: true, redirect_urls: vec!["http://foo/bar".into()] }; - let (SecretResult::Created(pw) | SecretResult::AlreadyExisted(pw)) = dbg!(post_client(&token, name, &client_config, &conf).await?) else { - panic!("Not created or existed") - }; - let c = dbg!(get_client(name, &token, &client_config, &conf).await.unwrap()); - assert!(client_configs_match(&c, &generate_client(name, &client_config, &pw))); - assert!(dbg!(compare_clients(&token, name, &client_config, &conf, &pw).await?)); - - // private client - let client_config = OIDCConfig { is_public: false, redirect_urls: vec!["http://foo/bar".into()] }; - let (SecretResult::Created(pw) | SecretResult::AlreadyExisted(pw)) = dbg!(post_client(&token, name, &client_config, &conf).await?) else { - panic!("Not created or existed") - }; - let c = dbg!(get_client(name, &token, &client_config, &conf).await.unwrap()); - assert!(client_configs_match(&c, &generate_client(name, &client_config, &pw))); - assert!(dbg!(compare_clients(&token, name, &client_config, &conf, &pw).await?)); - - Ok(()) -} - -async fn post_client( - token: &str, - name: &str, - oidc_client_config: &OIDCConfig, - conf: &KeyCloakConfig, -) -> reqwest::Result { - let secret = if !oidc_client_config.is_public { - generate_secret() - } else { - String::with_capacity(0) - }; - let generated_client = generate_client(name, oidc_client_config, &secret); - let res = CLIENT - .post(&format!( - "{}/admin/realms/{}/clients", - conf.keycloak_url, conf.keycloak_realm - )) - .bearer_auth(token) - .json(&generated_client) - .send() - .await?; - // Create groups for this client - create_groups(name, token, conf).await?; - match res.status() { - StatusCode::CREATED => { - println!("Client for {name} created."); - if !oidc_client_config.is_public { - let client_id = generated_client - .get("clientId") - .and_then(Value::as_str) - .expect("Always present"); - add_service_account_roles(token, client_id, conf).await?; - } - Ok(SecretResult::Created(secret)) - } - StatusCode::CONFLICT => { - let conflicting_client = get_client(name, token, oidc_client_config, conf).await?; - if client_configs_match(&conflicting_client, &generated_client) { - Ok(SecretResult::AlreadyExisted(conflicting_client - .as_object() - .and_then(|o| o.get("secret")) - .and_then(Value::as_str) - .unwrap_or("") - .to_owned())) - } else { - Ok(CLIENT - .put(&format!( - "{}/admin/realms/{}/clients/{}", - conf.keycloak_url, - conf.keycloak_realm, - conflicting_client - .get("clientId") - .and_then(Value::as_str) - .expect("We have a valid client") - )) - .bearer_auth(token) - .json(&generated_client) - .send() - .await? - .status() - .is_success() - .then_some(SecretResult::AlreadyExisted(secret)) - .expect("We know the client already exists so updating should be successful")) - } - } - s => unreachable!("Unexpected statuscode {s} while creating keycloak client. {res:?}"), - } -} - -async fn create_groups(name: &str, token: &str, conf: &KeyCloakConfig) -> reqwest::Result<()> { - let capitalize = |s: &str| { - let mut chrs = s.chars(); - chrs.next().map(char::to_uppercase).map(Iterator::collect).unwrap_or(String::new()) + chrs.as_str() - }; - let name = capitalize(name); - for group in &conf.keycloak_groups_per_bh { - post_group(&group.replace('#', &name), token, conf).await?; - } - Ok(()) -} - -async fn post_group(name: &str, token: &str, conf: &KeyCloakConfig) -> reqwest::Result<()> { - let res = CLIENT - .post(&format!( - "{}/admin/realms/{}/groups", - conf.keycloak_url, conf.keycloak_realm - )) - .bearer_auth(token) - .json(&json!({ - "name": name - })) - .send() - .await?; - match res.status() { - StatusCode::CREATED => println!("Created group {name}"), - StatusCode::CONFLICT => println!("Group {name} already existed"), - s => unreachable!("Unexpected statuscode {s} while creating group {name}") - } - Ok(()) -} - -fn generate_secret() -> String { - use rand::Rng; - const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\ - abcdefghijklmnopqrstuvwxyz\ - 0123456789"; - const PASSWORD_LEN: usize = 30; - let mut rng = rand::thread_rng(); - - (0..PASSWORD_LEN) - .map(|_| { - let idx = rng.gen_range(0..CHARSET.len()); - CHARSET[idx] as char - }) - .collect() -} - -pub async fn create_client( - name: &str, - oidc_client_config: OIDCConfig, - conf: &KeyCloakConfig, -) -> reqwest::Result { - let token = get_access_token(conf).await?; - post_client(&token, name, &oidc_client_config, conf).await -} - -#[ignore = "Requires setting up a keycloak"] -#[tokio::test] -async fn service_account_test() -> reqwest::Result<()> { - let (token, conf) = setup_keycloak().await?; - create_groups("test", &token, &conf).await?; - // dbg!(get_realm_permission_roles(&token, &conf).await?); - // add_service_account_roles(&token, "test-private", &conf).await?; - Ok(()) -} - -async fn add_service_account_roles( - token: &str, - client_id: &str, - conf: &KeyCloakConfig, -) -> reqwest::Result<()> { - if conf.keycloak_service_account_roles.is_empty() { - return Ok(()); - } - #[derive(serde::Deserialize)] - struct UserIdExtractor { - id: String, - } - let service_account_id = CLIENT.get(&format!( - "{}/admin/realms/{}/clients/{}/service-account-user", - conf.keycloak_url, conf.keycloak_realm, client_id - )) - .bearer_auth(token) - .send() - .await? - .json::() - .await? - .id; - let roles: Vec<_> = get_realm_permission_roles(token, conf) - .await? - .into_iter() - .filter(|f| conf.keycloak_service_account_roles.contains(&f.name)) - .collect(); - - assert_eq!(roles.len(), conf.keycloak_service_account_roles.len(), "Failed to find all required service account roles got {roles:#?} but expected all of these: {:#?}", conf.keycloak_service_account_roles); - let realm_id = roles[0].container_id.clone(); - CLIENT.post(&format!( - "{}/admin/realms/{}/users/{}/role-mappings/clients/{}", - conf.keycloak_url, conf.keycloak_realm, service_account_id, realm_id - )) - .bearer_auth(token) - .json(&roles) - .send() - .await?; - - Ok(()) -} - -#[derive(Debug, serde::Deserialize, serde::Serialize)] -struct ServiceAccountRole { - id: String, - #[serde(rename = "containerId", skip_serializing)] - container_id: String, - name: String -} - -async fn get_realm_permission_roles(token: &str, conf: &KeyCloakConfig) -> reqwest::Result> { - #[derive(Debug, serde::Deserialize)] - struct RealmId { - id: String, - #[serde(rename = "clientId")] - client_id: String - } - let permission_realm = if conf.keycloak_realm == "master" { - "master-realm" - } else { - "realm-management" - }; - let res = CLIENT.get(&format!( - "{}/admin/realms/{}/clients/?q={permission_realm}&search", - conf.keycloak_url, conf.keycloak_realm - )) - .bearer_auth(token) - .send() - .await? - .json::>() - .await?; - let role_client = res.into_iter() - .find(|v| v.client_id.starts_with(permission_realm)) - .expect(&format!("Failed to find realm id for {permission_realm}")); - CLIENT.get(&format!( - "{}/admin/realms/{}/clients/{}/roles", - conf.keycloak_url, conf.keycloak_realm, role_client.id - )) - .bearer_auth(token) - .send() - .await? - .json() - .await -} -*/ \ No newline at end of file diff --git a/central/src/auth/authentik/group.rs b/central/src/auth/authentik/group.rs new file mode 100644 index 0000000..48b7a2f --- /dev/null +++ b/central/src/auth/authentik/group.rs @@ -0,0 +1,124 @@ +use crate::CLIENT; +use beam_lib::reqwest::{self, StatusCode, Url}; +use serde_json::json; + +use super::AuthentikConfig; + + +pub async fn create_groups(name: &str, token: &str, conf: &AuthentikConfig) -> reqwest::Result<()> { + let capitalize = |s: &str| { + let mut chrs = s.chars(); + chrs.next().map(char::to_uppercase).map(Iterator::collect).unwrap_or(String::new()) + chrs.as_str() + }; + let name = capitalize(name); + for group in &conf.authentik_groups_per_bh { + post_group(&group.replace('#', &name), token, conf).await?; + } + Ok(()) +} + +pub async fn post_group(name: &str, token: &str, conf: &AuthentikConfig) -> reqwest::Result<()> { + let res = CLIENT + .post(&format!( + "{}/api/v3/core/groups/", + conf.authentik_url + )) + .bearer_auth(token) + .json(&json!({ + "name": name + })) + .send() + .await?; + match res.status() { + StatusCode::CREATED => println!("Created group {name}"), + StatusCode::CONFLICT => println!("Group {name} already existed"), + s => unreachable!("Unexpected statuscode {s} while creating group {name}") + } + Ok(()) +} + +async fn add_service_account_roles( + token: &str, + client_id: &str, + conf: &KeyCloakConfig, +) -> reqwest::Result<()> { + if conf.keycloak_service_account_roles.is_empty() { + return Ok(()); + } + #[derive(serde::Deserialize)] + struct UserIdExtractor { + id: String, + } + let service_account_id = CLIENT.get(&format!( + "{}/admin/realms/{}/clients/{}/service-account-user", + conf.keycloak_url, conf.keycloak_realm, client_id + )) + .bearer_auth(token) + .send() + .await? + .json::() + .await? + .id; + let roles: Vec<_> = get_realm_permission_roles(token, conf) + .await? + .into_iter() + .filter(|f| conf.keycloak_service_account_roles.contains(&f.name)) + .collect(); + + assert_eq!(roles.len(), conf.keycloak_service_account_roles.len(), "Failed to find all required service account roles got {roles:#?} but expected all of these: {:#?}", conf.keycloak_service_account_roles); + let realm_id = roles[0].container_id.clone(); + CLIENT.post(&format!( + "{}/admin/realms/{}/users/{}/role-mappings/clients/{}", + conf.keycloak_url, conf.keycloak_realm, service_account_id, realm_id + )) + .bearer_auth(token) + .json(&roles) + .send() + .await?; + + Ok(()) +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +struct ServiceAccountRole { + id: String, + #[serde(rename = "containerId", skip_serializing)] + container_id: String, + name: String +} +/* +async fn get_realm_permission_roles(token: &str, conf: &KeyCloakConfig) -> reqwest::Result> { + #[derive(Debug, serde::Deserialize)] + struct RealmId { + id: String, + #[serde(rename = "clientId")] + client_id: String + } + let permission_realm = if conf.keycloak_realm == "master" { + "master-realm" + } else { + "realm-management" + }; + let res = CLIENT.get(&format!( + "{}/admin/realms/{}/clients/?q={permission_realm}&search", + conf.keycloak_url, conf.keycloak_realm + )) + .bearer_auth(token) + .send() + .await? + .json::>() + .await?; + let role_client = res.into_iter() + .find(|v| v.client_id.starts_with(permission_realm)) + .expect(&format!("Failed to find realm id for {permission_realm}")); + CLIENT.get(&format!( + "{}/admin/realms/{}/clients/{}/roles", + conf.keycloak_url, conf.keycloak_realm, role_client.id + )) + .bearer_auth(token) + .send() + .await? + .json() + .await +} +*/ \ No newline at end of file diff --git a/central/src/auth/authentik/mod.rs b/central/src/auth/authentik/mod.rs new file mode 100644 index 0000000..44329a1 --- /dev/null +++ b/central/src/auth/authentik/mod.rs @@ -0,0 +1,246 @@ +mod test; +mod group; + + +use crate::CLIENT; +use beam_lib::reqwest::{self, StatusCode, Url}; +use clap::Parser; +use group::create_groups; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use shared::{OIDCConfig, SecretResult}; + +#[derive(Debug, Parser, Clone)] +pub struct AuthentikConfig { + /// authentik url + #[clap(long, env)] + pub authentik_url: Url, + #[clap(long, env)] + pub authentik_id: String, + #[clap(long, env)] + pub authentik_secret: String, + // !Todo is it needed + #[clap(long, env, default_value = "master")] + pub authentik_tenant: String, + #[clap(long, env, value_parser, value_delimiter = ',', default_values_t = [] as [String; 0])] + pub authentik_service_account_roles: Vec, + #[clap(long, env, value_parser, value_delimiter = ',', default_values_t = [] as [String; 0])] + pub authentik_groups_per_bh: Vec, +} + +async fn get_access_token(conf: &AuthentikConfig) -> reqwest::Result { + #[derive(Deserialize, Serialize, Debug)] + struct Token { + access_token: String, + } + CLIENT + .post(&format!( + "{}/application/o/token/", + conf.authentik_url + )) + .form(&json!({ + "grant_type": "client_credentials", + "client_id": conf.authentik_id, + "client_secret": conf.authentik_secret, + "scope": "openid" + })) + .send() + .await? + .json::() + .await + .map(|t| t.access_token) +} + + + +async fn get_application( + name: &str, + token: &str, + oidc_client_config: &OIDCConfig, + conf: &AuthentikConfig, +) -> reqwest::Result { + let id = format!("{name}-{}", if oidc_client_config.is_public { "public" } else { "private" }); + CLIENT + .get(&format!( + "{}/api/v3/core/applications/{id}/", + conf.authentik_url + )) + .bearer_auth(token) + .send() + .await? + .json() + .await +} + +pub async fn validate_application( + name: &str, + oidc_client_config: &OIDCConfig, + secret: &str, + conf: &AuthentikConfig, +) -> reqwest::Result { + let token = get_access_token(conf).await?; + compare_applications(&token, name, oidc_client_config, conf, secret).await +} + +async fn compare_applications( + token: &str, + name: &str, + oidc_client_config: &OIDCConfig, + conf: &AuthentikConfig, + secret: &str, +) -> Result { + let client = get_application(name, token, oidc_client_config, conf).await?; + let wanted_client = generate_application(name, oidc_client_config, secret); + Ok(client.get("secret") == wanted_client.get("secret") + && client_configs_match(&client, &wanted_client)) +} + +fn client_configs_match(a: &Value, b: &Value) -> bool { + let includes_other_json_array = |key, comparator: &dyn Fn(_, _) -> bool| a + .get(key) + .and_then(Value::as_array) + .is_some_and(|a_values| b + .get(key) + .and_then(Value::as_array) + .is_some_and(|vec| vec.iter().all(|v| comparator(a_values, v))) + ); + + a.get("name") == b.get("name") + && includes_other_json_array("defaultClientScopes", &|a_v, v| a_v.contains(v)) + && includes_other_json_array("redirectUris", &|a_v, v| a_v.contains(v)) + && includes_other_json_array("protocolMappers", &|a_v, v| a_v.iter().any(|a_v| a_v.get("name") == v.get("name"))) +} + +fn generate_application(name: &str, oidc_client_config: &OIDCConfig, secret: &str) -> Value { + let secret = (!oidc_client_config.is_public).then_some(secret); + let id = format!("{name}-{}", if oidc_client_config.is_public { "public" } else { "private" }); +// Todo noch anpassen + let mut json = json!({ + "name": id, + "id": id, + "clientId": id, + "redirectUris": oidc_client_config.redirect_urls, + "webOrigins": ["+"], // Will allow all hosts that are named in redirectUris. This is not the same as '*' + "publicClient": oidc_client_config.is_public, + "serviceAccountsEnabled": !oidc_client_config.is_public, + "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": id, + "id.token.claim": "true", + "access.token.claim": "true" + } + }] + }); + if let Some(secret) = secret { + json.as_object_mut().unwrap().insert("secret".to_owned(), secret.into()); + } + json +} + +async fn post_application( + token: &str, + name: &str, + oidc_client_config: &OIDCConfig, + conf: &AuthentikConfig, +) -> reqwest::Result { + let secret = if !oidc_client_config.is_public { + generate_secret() + } else { + String::with_capacity(0) + }; + let generated_app = generate_application(name, oidc_client_config, &secret); + let res = CLIENT + .post(&format!( + "{}/api/v3/core/applications/", + conf.authentik_url + )) + .bearer_auth(token) + .json(&generated_app) + .send() + .await?; + // Create groups for this client + create_groups(name, token, conf).await?; + match res.status() { + StatusCode::CREATED => { + println!("Client for {name} created."); + if !oidc_client_config.is_public { + let client_id = generated_app + .get("clientId") + .and_then(Value::as_str) + .expect("Always present"); + add_service_account_roles(token, client_id, conf).await?; + } + Ok(SecretResult::Created(secret)) + } + StatusCode::CONFLICT => { + let conflicting_client = get_client(name, token, oidc_client_config, conf).await?; + if client_configs_match(&conflicting_client, &generated_client) { + Ok(SecretResult::AlreadyExisted(conflicting_client + .as_object() + .and_then(|o| o.get("secret")) + .and_then(Value::as_str) + .unwrap_or("") + .to_owned())) + } else { + Ok(CLIENT + .put(&format!( + "{}/admin/realms/{}/clients/{}", + conf.keycloak_url, + conf.keycloak_realm, + conflicting_client + .get("clientId") + .and_then(Value::as_str) + .expect("We have a valid client") + )) + .bearer_auth(token) + .json(&generated_client) + .send() + .await? + .status() + .is_success() + .then_some(SecretResult::AlreadyExisted(secret)) + .expect("We know the client already exists so updating should be successful")) + } + } + s => unreachable!("Unexpected statuscode {s} while creating keycloak client. {res:?}"), + } +} + + +fn generate_secret() -> String { + use rand::Rng; + const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\ + abcdefghijklmnopqrstuvwxyz\ + 0123456789"; + const PASSWORD_LEN: usize = 30; + let mut rng = rand::thread_rng(); + + (0..PASSWORD_LEN) + .map(|_| { + let idx = rng.gen_range(0..CHARSET.len()); + CHARSET[idx] as char + }) + .collect() +} + +pub async fn create_application( + name: &str, + oidc_client_config: OIDCConfig, + conf: &AuthentikConfig, +) -> reqwest::Result { + let token = get_access_token(conf).await?; + post_application(&token, name, &oidc_client_config, conf).await +} diff --git a/central/src/auth/authentik/test.rs b/central/src/auth/authentik/test.rs new file mode 100644 index 0000000..baeafb7 --- /dev/null +++ b/central/src/auth/authentik/test.rs @@ -0,0 +1,126 @@ +use beam_lib::reqwest::{self, Error, StatusCode, Url}; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use crate::auth::authentik::AuthentikConfig; +use crate::CLIENT; + + +#[tokio::test] +async fn get_access_test() { + let path_url = "http://localhost:9000/application/o/token/"; + #[derive(Deserialize, Serialize, Debug)] + struct Token { + access_token: String, + } + let response = CLIENT + .post(path_url) + .form(&json!({ + "grant_type": "client_credentials", + "client_id": "MI4DbeyktmjbXJRmUY9tkWvhK7yOzly139EgzhPZ", + "client_secret": "YGcFnXQMI7HqeDUWClhTkZmPtYj4aB2z3khnoMNpCo8CgTOhUqqOFE56dP2WOJoPGOeqdPsVCrR4yvjnJviYK6dY8WeykDqnzAO1xCLHOsPxefcSAa21qe0ru2bwWBi7", + "scope": "openid" + })) + .send() + .await + .expect("no response"); + // let raw = response.text().await.expect("no resoponse"); + // dbg!(&raw); + + let t = response + .json::() + .await + .expect("Token can not be parseed"); + dbg!(&t); + assert!(!t.access_token.is_empty()); +} + +#[cfg(test)] +// nicht möglich mit authentik +async fn get_access_token_via_admin_login() -> reqwest::Result { + #[derive(serde::Deserialize)] + struct Token { + access_token: String, + } + CLIENT + .post(&format!( + "{}/application/o/token/", + if cfg!(test) { "http://localhost:9000"} else { "http://authentik:8080" } + )) + .form(&json!({ + "client_id": "admin-cli", + "username": "Merlin@frech.com", + "password": "MErlin", + "grant_type": "password" + })) + .send() + .await? + .json::() + .await + .map(|t| t.access_token) +} +/* +#[cfg(test)] +async fn setup_keycloak() -> reqwest::Result<(String, AuthentikConfig)> { + let token = get_access_token_via_admin_login().await?; + let res = CLIENT + .post("http://localhost:1337/admin/realms/master/client-scopes") + .bearer_auth(&token) + .json(&json!({ + "name": "groups", + "protocol": "openid-connect" + })) + .send() + .await?; + dbg!(&res.status()); + Ok(( + token, + AuthentikConfig { + keycloak_url: "http://localhost:1337".parse().unwrap(), + keycloak_id: "unused in tests".into(), + keycloak_secret: "unused in tests".into(), + keycloak_realm: "master".into(), + keycloak_service_account_roles: vec!["query-users".into(), "view-users".into()], + keycloak_groups_per_bh: vec!["DKTK_CCP_#".into(), "DKTK_CCP_#_Verwalter".into()], + }, + )) +} + + +#[ignore = "Requires setting up a keycloak"] +#[tokio::test] +async fn test_create_client() -> reqwest::Result<()> { + let (token, conf) = setup_keycloak().await?; + let name = "test"; + // public client + let client_config = OIDCConfig { is_public: true, redirect_urls: vec!["http://foo/bar".into()] }; + let (SecretResult::Created(pw) | SecretResult::AlreadyExisted(pw)) = dbg!(post_client(&token, name, &client_config, &conf).await?) else { + panic!("Not created or existed") + }; + let c = dbg!(get_client(name, &token, &client_config, &conf).await.unwrap()); + assert!(client_configs_match(&c, &generate_client(name, &client_config, &pw))); + assert!(dbg!(compare_clients(&token, name, &client_config, &conf, &pw).await?)); + + // private client + let client_config = OIDCConfig { is_public: false, redirect_urls: vec!["http://foo/bar".into()] }; + let (SecretResult::Created(pw) | SecretResult::AlreadyExisted(pw)) = dbg!(post_client(&token, name, &client_config, &conf).await?) else { + panic!("Not created or existed") + }; + let c = dbg!(get_client(name, &token, &client_config, &conf).await.unwrap()); + assert!(client_configs_match(&c, &generate_client(name, &client_config, &pw))); + assert!(dbg!(compare_clients(&token, name, &client_config, &conf, &pw).await?)); + + Ok(()) +} + +#[ignore = "Requires setting up a keycloak"] +#[tokio::test] +async fn service_account_test() -> reqwest::Result<()> { + let (token, conf) = setup_keycloak().await?; + create_groups("test", &token, &conf).await?; + // dbg!(get_realm_permission_roles(&token, &conf).await?); + // add_service_account_roles(&token, "test-private", &conf).await?; + Ok(()) +} + + + */ \ No newline at end of file diff --git a/central/src/auth/authentik_test.rs b/central/src/auth/authentik_test.rs deleted file mode 100644 index 03dd681..0000000 --- a/central/src/auth/authentik_test.rs +++ /dev/null @@ -1,39 +0,0 @@ -use beam_lib::reqwest::{self, Error, StatusCode, Url}; -use serde::{Deserialize, Serialize}; -use serde_json::json; - -use crate::CLIENT; - - -#[tokio::test] -async fn get_access_token() { - let path_url = "http://localhost:9000/application/o/token"; - #[derive(Deserialize, Serialize, Debug)] - struct Token { - access_token: String, - token_type: String, - expires_in: i32, - id_token: String - } - let response = CLIENT - .post(path_url) - .header("Content-Type", "application/x-www-form-urlencoded") - .form(&[ - ("grant_type", "client_credentials"), - ("client_id", "MI4DbeyktmjbXJRmUY9tkWvhK7yOzly139EgzhPZ"), - ("client_secret", "YGcFnXQMI7HqeDUWClhTkZmPtYj4aB2z3khnoMNpCo8CgTOhUqqOFE56dP2WOJoPGOeqdPsVCrR4yvjnJviYK6dY8WeykDqnzAO1xCLHOsPxefcSAa21qe0ru2bwWBi7"), - ("scope", "openid") - ]) - .send() - .await - .expect("no response"); - let raw = response.text().await.expect("no resoponse"); - dbg!(&raw); - - // let t = response - // .json::() - // .await - // .expect("Token can not be parseed"); - // dbg!(&t); - // assert!(!t.access_token.is_empty()); -} diff --git a/central/src/auth/config.rs b/central/src/auth/config.rs index bb80d87..f4129e9 100644 --- a/central/src/auth/config.rs +++ b/central/src/auth/config.rs @@ -6,6 +6,8 @@ use shared::{SecretResult, OIDCConfig}; use crate::auth::keycloak::{KeyCloakConfig, self}; +use super::authentik::AuthentikConfig; + /// Central secret sync #[derive(Debug, Parser)] pub struct Config { @@ -28,7 +30,8 @@ pub struct Config { #[derive(Clone, Debug)] pub enum OIDCProvider { - Keycloak(KeyCloakConfig) + Keycloak(KeyCloakConfig), + Authentik(AuthentikConfig) } impl OIDCProvider { @@ -39,6 +42,7 @@ impl OIDCProvider { pub async fn create_client(&self, name: &str, oidc_client_config: OIDCConfig) -> Result { match self { OIDCProvider::Keycloak(conf) => keycloak::create_client(name, oidc_client_config, conf).await, + OIDCProvider::Authentik(conf) => todo!("create Authentik") }.map_err(|e| { println!("Failed to create client: {e}"); "Error creating OIDC client".into() @@ -55,6 +59,7 @@ impl OIDCProvider { "Failed to validate client. See upstrean logs.".into() }) }, + OIDCProvider::Authentik(conf) => todo!("create Authentik") } } } diff --git a/central/src/auth/keycloak.rs b/central/src/auth/keycloak/mod.rs similarity index 100% rename from central/src/auth/keycloak.rs rename to central/src/auth/keycloak/mod.rs diff --git a/central/src/auth/mod.rs b/central/src/auth/mod.rs index 44b48f6..cb2f2f0 100644 --- a/central/src/auth/mod.rs +++ b/central/src/auth/mod.rs @@ -1,4 +1,3 @@ mod keycloak; pub(crate) mod config; -mod authentik; -mod authentik_test; \ No newline at end of file +mod authentik; \ No newline at end of file From 871b39f4f9e48bfc3b8fba08a79f5740fa72c2d3 Mon Sep 17 00:00:00 2001 From: Martin Jurk Date: Fri, 27 Sep 2024 16:36:42 +0200 Subject: [PATCH 07/37] test app and provider create --- central/src/auth/authentik/app.rs | 141 ++++++++++++++++++++++++++++ central/src/auth/authentik/group.rs | 7 +- central/src/auth/authentik/mod.rs | 116 +---------------------- central/src/auth/authentik/test.rs | 49 +++++----- 4 files changed, 170 insertions(+), 143 deletions(-) create mode 100644 central/src/auth/authentik/app.rs diff --git a/central/src/auth/authentik/app.rs b/central/src/auth/authentik/app.rs new file mode 100644 index 0000000..85271db --- /dev/null +++ b/central/src/auth/authentik/app.rs @@ -0,0 +1,141 @@ +use crate::CLIENT; +use beam_lib::reqwest::{self, StatusCode, Url}; +use clap::Parser; +use group::create_groups; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use shared::{OIDCConfig, SecretResult}; + +use super::{generate_secret, group, AuthentikConfig}; + +async fn generate_application(provider: &str, name: &str, oidc_client_config: &OIDCConfig, secret: &str, conf: &AuthentikConfig, token: &str) +-> Result<(), String> +{ + let secret = (!oidc_client_config.is_public).then_some(secret); + let id = format!("{name}-{}", if oidc_client_config.is_public { "public" } else { "private" }); +// Todo noch anpassen + let mut json = json!({ + "name": id, + "slug": id, + "provider": provider, + "group": name + }); + if let Some(secret) = secret { + json.as_object_mut().unwrap().insert("secret".to_owned(), secret.into()); + } + + let res = CLIENT + .post(&format!( + "{}/api/v3/core/applications/", + conf.authentik_url + )) + .bearer_auth(token) + .json(&json) + .send() + .await + .expect("Authentik is not reachable"); + + match res.status() { + StatusCode::CREATED => return Ok(()), + StatusCode::BAD_REQUEST => return Err(format!("Unexpected statuscode Bad Request while creating authintik provider. {res:?}")), + s => return Err(format!("Unexpected statuscode {s} while creating authintik provider. {res:?}")) + } +} + + + +async fn generate_provider(name: &str, oidc_client_config: &OIDCConfig, secret: &str, conf: &AuthentikConfig, token: &str) +-> Result<(), String> +{ + let secret = (!oidc_client_config.is_public).then_some(secret); + let id = format!("{name}-{}", if oidc_client_config.is_public { "public" } else { "private" }); + let mut json = json!({ + "name": id, + "client_id": id, + "authorization_flow": "", // flow uuid + "redirect_uris": oidc_client_config.redirect_urls, + }); + if oidc_client_config.is_public { + json.as_object_mut().unwrap().insert("client_type".to_owned(), "public".into()); + } else { + json.as_object_mut().unwrap().insert("client_type".to_owned(), "confidential".into()); + } + if let Some(secret) = secret { + json.as_object_mut().unwrap().insert("client_secret".to_owned(), secret.into()); + } + let res = CLIENT + .post(&format!( + "{}/api/v3/providers/oauth2/", + conf.authentik_url + )) + .bearer_auth(token) + .json(&json) + .send() + .await.expect("Authentik not reachable"); + + match res.status() { + StatusCode::CREATED => return Ok(()), + StatusCode::BAD_REQUEST => return Err(format!("Unexpected statuscode Bad Request while creating authintik provider. {res:?}")), + s => return Err(format!("Unexpected statuscode {s} while creating authintik provider. {res:?}")) + } +} + +pub fn post_application( + token: &str, + name: &str, + oidc_client_config: &OIDCConfig, + conf: &AuthentikConfig, +) -> reqwest::Result { + let secret = if !oidc_client_config.is_public { + generate_secret() + } else { + String::with_capacity(0) + }; + let generated_provider = generate_provider(name, oidc_client_config, &secret, conf, token); + let generated_app = generate_application(name, name, oidc_client_config, &secret, conf); + // Create groups for this client + create_groups(name, token, conf).await?; + match res.status() { + StatusCode::CREATED => { + println!("Client for {name} created."); + if !oidc_client_config.is_public { + let client_id = generated_app + .get("clientId") + .and_then(Value::as_str) + .expect("Always present"); + } + Ok(SecretResult::Created(secret)) + } + StatusCode::CONFLICT => { + let conflicting_client = get_client(name, token, oidc_client_config, conf).await?; + if client_configs_match(&conflicting_client, &generated_client) { + Ok(SecretResult::AlreadyExisted(conflicting_client + .as_object() + .and_then(|o| o.get("secret")) + .and_then(Value::as_str) + .unwrap_or("") + .to_owned())) + } else { + Ok(CLIENT + .put(&format!( + "{}/admin/realms/{}/clients/{}", + conf.keycloak_url, + conf.keycloak_realm, + conflicting_client + .get("clientId") + .and_then(Value::as_str) + .expect("We have a valid client") + )) + .bearer_auth(token) + .json(&generated_client) + .send() + .await? + .status() + .is_success() + .then_some(SecretResult::AlreadyExisted(secret)) + .expect("We know the client already exists so updating should be successful")) + } + } + s => unreachable!("Unexpected statuscode {s} while creating keycloak client. {res:?}"), + } +} diff --git a/central/src/auth/authentik/group.rs b/central/src/auth/authentik/group.rs index 48b7a2f..92ab860 100644 --- a/central/src/auth/authentik/group.rs +++ b/central/src/auth/authentik/group.rs @@ -30,13 +30,13 @@ pub async fn post_group(name: &str, token: &str, conf: &AuthentikConfig) -> reqw .send() .await?; match res.status() { - StatusCode::CREATED => println!("Created group {name}"), + StatusCode::CREATED => println!("Created group {name}"), StatusCode::CONFLICT => println!("Group {name} already existed"), s => unreachable!("Unexpected statuscode {s} while creating group {name}") } Ok(()) } - +/* async fn add_service_account_roles( token: &str, client_id: &str, @@ -86,7 +86,8 @@ struct ServiceAccountRole { container_id: String, name: String } -/* + + async fn get_realm_permission_roles(token: &str, conf: &KeyCloakConfig) -> reqwest::Result> { #[derive(Debug, serde::Deserialize)] struct RealmId { diff --git a/central/src/auth/authentik/mod.rs b/central/src/auth/authentik/mod.rs index 44329a1..77d6474 100644 --- a/central/src/auth/authentik/mod.rs +++ b/central/src/auth/authentik/mod.rs @@ -1,6 +1,6 @@ mod test; mod group; - +mod app; use crate::CLIENT; use beam_lib::reqwest::{self, StatusCode, Url}; @@ -19,9 +19,6 @@ pub struct AuthentikConfig { pub authentik_id: String, #[clap(long, env)] pub authentik_secret: String, - // !Todo is it needed - #[clap(long, env, default_value = "master")] - pub authentik_tenant: String, #[clap(long, env, value_parser, value_delimiter = ',', default_values_t = [] as [String; 0])] pub authentik_service_account_roles: Vec, #[clap(long, env, value_parser, value_delimiter = ',', default_values_t = [] as [String; 0])] @@ -51,8 +48,6 @@ async fn get_access_token(conf: &AuthentikConfig) -> reqwest::Result { .map(|t| t.access_token) } - - async fn get_application( name: &str, token: &str, @@ -111,113 +106,6 @@ fn client_configs_match(a: &Value, b: &Value) -> bool { && includes_other_json_array("protocolMappers", &|a_v, v| a_v.iter().any(|a_v| a_v.get("name") == v.get("name"))) } -fn generate_application(name: &str, oidc_client_config: &OIDCConfig, secret: &str) -> Value { - let secret = (!oidc_client_config.is_public).then_some(secret); - let id = format!("{name}-{}", if oidc_client_config.is_public { "public" } else { "private" }); -// Todo noch anpassen - let mut json = json!({ - "name": id, - "id": id, - "clientId": id, - "redirectUris": oidc_client_config.redirect_urls, - "webOrigins": ["+"], // Will allow all hosts that are named in redirectUris. This is not the same as '*' - "publicClient": oidc_client_config.is_public, - "serviceAccountsEnabled": !oidc_client_config.is_public, - "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": id, - "id.token.claim": "true", - "access.token.claim": "true" - } - }] - }); - if let Some(secret) = secret { - json.as_object_mut().unwrap().insert("secret".to_owned(), secret.into()); - } - json -} - -async fn post_application( - token: &str, - name: &str, - oidc_client_config: &OIDCConfig, - conf: &AuthentikConfig, -) -> reqwest::Result { - let secret = if !oidc_client_config.is_public { - generate_secret() - } else { - String::with_capacity(0) - }; - let generated_app = generate_application(name, oidc_client_config, &secret); - let res = CLIENT - .post(&format!( - "{}/api/v3/core/applications/", - conf.authentik_url - )) - .bearer_auth(token) - .json(&generated_app) - .send() - .await?; - // Create groups for this client - create_groups(name, token, conf).await?; - match res.status() { - StatusCode::CREATED => { - println!("Client for {name} created."); - if !oidc_client_config.is_public { - let client_id = generated_app - .get("clientId") - .and_then(Value::as_str) - .expect("Always present"); - add_service_account_roles(token, client_id, conf).await?; - } - Ok(SecretResult::Created(secret)) - } - StatusCode::CONFLICT => { - let conflicting_client = get_client(name, token, oidc_client_config, conf).await?; - if client_configs_match(&conflicting_client, &generated_client) { - Ok(SecretResult::AlreadyExisted(conflicting_client - .as_object() - .and_then(|o| o.get("secret")) - .and_then(Value::as_str) - .unwrap_or("") - .to_owned())) - } else { - Ok(CLIENT - .put(&format!( - "{}/admin/realms/{}/clients/{}", - conf.keycloak_url, - conf.keycloak_realm, - conflicting_client - .get("clientId") - .and_then(Value::as_str) - .expect("We have a valid client") - )) - .bearer_auth(token) - .json(&generated_client) - .send() - .await? - .status() - .is_success() - .then_some(SecretResult::AlreadyExisted(secret)) - .expect("We know the client already exists so updating should be successful")) - } - } - s => unreachable!("Unexpected statuscode {s} while creating keycloak client. {res:?}"), - } -} fn generate_secret() -> String { @@ -243,4 +131,4 @@ pub async fn create_application( ) -> reqwest::Result { let token = get_access_token(conf).await?; post_application(&token, name, &oidc_client_config, conf).await -} +} \ No newline at end of file diff --git a/central/src/auth/authentik/test.rs b/central/src/auth/authentik/test.rs index baeafb7..7ee4706 100644 --- a/central/src/auth/authentik/test.rs +++ b/central/src/auth/authentik/test.rs @@ -1,17 +1,19 @@ use beam_lib::reqwest::{self, Error, StatusCode, Url}; use serde::{Deserialize, Serialize}; use serde_json::json; +use shared::{OIDCConfig, SecretResult}; +use crate::auth::authentik::group::create_groups; use crate::auth::authentik::AuthentikConfig; use crate::CLIENT; +#[derive(Deserialize, Serialize, Debug)] +struct Token { + access_token: String, +} #[tokio::test] async fn get_access_test() { let path_url = "http://localhost:9000/application/o/token/"; - #[derive(Deserialize, Serialize, Debug)] - struct Token { - access_token: String, - } let response = CLIENT .post(path_url) .form(&json!({ @@ -37,10 +39,6 @@ async fn get_access_test() { #[cfg(test)] // nicht möglich mit authentik async fn get_access_token_via_admin_login() -> reqwest::Result { - #[derive(serde::Deserialize)] - struct Token { - access_token: String, - } CLIENT .post(&format!( "{}/application/o/token/", @@ -58,13 +56,16 @@ async fn get_access_token_via_admin_login() -> reqwest::Result { .await .map(|t| t.access_token) } -/* + #[cfg(test)] -async fn setup_keycloak() -> reqwest::Result<(String, AuthentikConfig)> { - let token = get_access_token_via_admin_login().await?; +async fn setup_authentik() -> reqwest::Result<(String, AuthentikConfig)> { + //let token = get_access_token_via_admin_login().await?; + let token = Token{ + access_token: "kNaiFgKRew9nOJIy2TT4hDs2jIHR9TxVilFmqmqGtHRM2oike8yYv5pfKkq1".to_owned() + }; let res = CLIENT - .post("http://localhost:1337/admin/realms/master/client-scopes") - .bearer_auth(&token) + .post("http://localhost:9000/api/v3/core/applications/") + .bearer_auth(&token.access_token) .json(&json!({ "name": "groups", "protocol": "openid-connect" @@ -73,19 +74,18 @@ async fn setup_keycloak() -> reqwest::Result<(String, AuthentikConfig)> { .await?; dbg!(&res.status()); Ok(( - token, + token.access_token, AuthentikConfig { - keycloak_url: "http://localhost:1337".parse().unwrap(), - keycloak_id: "unused in tests".into(), - keycloak_secret: "unused in tests".into(), - keycloak_realm: "master".into(), - keycloak_service_account_roles: vec!["query-users".into(), "view-users".into()], - keycloak_groups_per_bh: vec!["DKTK_CCP_#".into(), "DKTK_CCP_#_Verwalter".into()], + authentik_url: "http://localhost:9000".parse().unwrap(), + authentik_id: "unused in tests".into(), + authentik_secret: "unused in tests".into(), + authentik_service_account_roles: vec!["query-users".into(), "view-users".into()], + authentik_groups_per_bh: vec!["DKTK_CCP_#".into(), "DKTK_CCP_#_Verwalter".into()], }, )) } - +/* #[ignore = "Requires setting up a keycloak"] #[tokio::test] async fn test_create_client() -> reqwest::Result<()> { @@ -111,16 +111,13 @@ async fn test_create_client() -> reqwest::Result<()> { Ok(()) } +*/ -#[ignore = "Requires setting up a keycloak"] #[tokio::test] async fn service_account_test() -> reqwest::Result<()> { - let (token, conf) = setup_keycloak().await?; + let (token, conf) = setup_authentik().await?; create_groups("test", &token, &conf).await?; // dbg!(get_realm_permission_roles(&token, &conf).await?); // add_service_account_roles(&token, "test-private", &conf).await?; Ok(()) } - - - */ \ No newline at end of file From d7c03d91a4e59aba2f9392a50b59b14010030dd5 Mon Sep 17 00:00:00 2001 From: Martin Jurk Date: Thu, 17 Oct 2024 14:04:26 +0200 Subject: [PATCH 08/37] app return type missing --- central/src/auth/authentik/app.rs | 71 ++++++++++--------------------- central/src/auth/authentik/mod.rs | 8 ---- 2 files changed, 22 insertions(+), 57 deletions(-) diff --git a/central/src/auth/authentik/app.rs b/central/src/auth/authentik/app.rs index 85271db..992ff93 100644 --- a/central/src/auth/authentik/app.rs +++ b/central/src/auth/authentik/app.rs @@ -6,9 +6,24 @@ use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use shared::{OIDCConfig, SecretResult}; -use super::{generate_secret, group, AuthentikConfig}; +use super::{generate_secret, get_access_token, group, AuthentikConfig}; -async fn generate_application(provider: &str, name: &str, oidc_client_config: &OIDCConfig, secret: &str, conf: &AuthentikConfig, token: &str) +pub async fn create_application( + name: &str, + oidc_client_config: OIDCConfig, + conf: &AuthentikConfig, +) -> reqwest::Result { + let token = get_access_token(conf).await?; + post_application(&token, name, &oidc_client_config, conf).await +} + +async fn generate_application( + provider: &str, + name: &str, + oidc_client_config: &OIDCConfig, + secret: &str, + conf: &AuthentikConfig, + token: &str) -> Result<(), String> { let secret = (!oidc_client_config.is_public).then_some(secret); @@ -80,7 +95,7 @@ async fn generate_provider(name: &str, oidc_client_config: &OIDCConfig, secret: } } -pub fn post_application( +pub async fn post_application( token: &str, name: &str, oidc_client_config: &OIDCConfig, @@ -91,51 +106,9 @@ pub fn post_application( } else { String::with_capacity(0) }; - let generated_provider = generate_provider(name, oidc_client_config, &secret, conf, token); - let generated_app = generate_application(name, name, oidc_client_config, &secret, conf); + //let generated_provider = generate_provider(name, oidc_client_config, &secret, conf, token).await; + let generated_app = generate_application(name, name, oidc_client_config, &secret, conf, token).await; // Create groups for this client - create_groups(name, token, conf).await?; - match res.status() { - StatusCode::CREATED => { - println!("Client for {name} created."); - if !oidc_client_config.is_public { - let client_id = generated_app - .get("clientId") - .and_then(Value::as_str) - .expect("Always present"); - } - Ok(SecretResult::Created(secret)) - } - StatusCode::CONFLICT => { - let conflicting_client = get_client(name, token, oidc_client_config, conf).await?; - if client_configs_match(&conflicting_client, &generated_client) { - Ok(SecretResult::AlreadyExisted(conflicting_client - .as_object() - .and_then(|o| o.get("secret")) - .and_then(Value::as_str) - .unwrap_or("") - .to_owned())) - } else { - Ok(CLIENT - .put(&format!( - "{}/admin/realms/{}/clients/{}", - conf.keycloak_url, - conf.keycloak_realm, - conflicting_client - .get("clientId") - .and_then(Value::as_str) - .expect("We have a valid client") - )) - .bearer_auth(token) - .json(&generated_client) - .send() - .await? - .status() - .is_success() - .then_some(SecretResult::AlreadyExisted(secret)) - .expect("We know the client already exists so updating should be successful")) - } - } - s => unreachable!("Unexpected statuscode {s} while creating keycloak client. {res:?}"), - } + let generated_group = create_groups(name, token, conf).await?; + } diff --git a/central/src/auth/authentik/mod.rs b/central/src/auth/authentik/mod.rs index 77d6474..eb5c1b6 100644 --- a/central/src/auth/authentik/mod.rs +++ b/central/src/auth/authentik/mod.rs @@ -124,11 +124,3 @@ fn generate_secret() -> String { .collect() } -pub async fn create_application( - name: &str, - oidc_client_config: OIDCConfig, - conf: &AuthentikConfig, -) -> reqwest::Result { - let token = get_access_token(conf).await?; - post_application(&token, name, &oidc_client_config, conf).await -} \ No newline at end of file From 1c1a35c02ea53fec5f030020b91e1d2b84f2265a Mon Sep 17 00:00:00 2001 From: Martin Jurk Date: Sun, 20 Oct 2024 17:55:43 +0200 Subject: [PATCH 09/37] min functions are provideded for authentik --- central/src/auth/authentik/app.rs | 85 ++++++++++++++++++++++-------- central/src/auth/authentik/mod.rs | 15 +++--- central/src/auth/authentik/test.rs | 7 ++- central/src/auth/config.rs | 25 +++++++-- 4 files changed, 96 insertions(+), 36 deletions(-) diff --git a/central/src/auth/authentik/app.rs b/central/src/auth/authentik/app.rs index 992ff93..e4b4ac7 100644 --- a/central/src/auth/authentik/app.rs +++ b/central/src/auth/authentik/app.rs @@ -1,12 +1,12 @@ use crate::CLIENT; -use beam_lib::reqwest::{self, StatusCode, Url}; +use beam_lib::reqwest::{self, Response, StatusCode, Url}; use clap::Parser; use group::create_groups; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use shared::{OIDCConfig, SecretResult}; -use super::{generate_secret, get_access_token, group, AuthentikConfig}; +use super::{app_configs_match, generate_secret, get_access_token, get_application, group, AuthentikConfig}; pub async fn create_application( name: &str, @@ -14,17 +14,15 @@ pub async fn create_application( conf: &AuthentikConfig, ) -> reqwest::Result { let token = get_access_token(conf).await?; - post_application(&token, name, &oidc_client_config, conf).await + combine_application(&token, name, &oidc_client_config, conf).await } -async fn generate_application( +pub fn generate_app_values( provider: &str, name: &str, oidc_client_config: &OIDCConfig, secret: &str, - conf: &AuthentikConfig, - token: &str) --> Result<(), String> +) -> Value { let secret = (!oidc_client_config.is_public).then_some(secret); let id = format!("{name}-{}", if oidc_client_config.is_public { "public" } else { "private" }); @@ -38,27 +36,29 @@ async fn generate_application( if let Some(secret) = secret { json.as_object_mut().unwrap().insert("secret".to_owned(), secret.into()); } + json +} - let res = CLIENT +async fn generate_application( + provider: &str, + name: &str, + oidc_client_config: &OIDCConfig, + secret: &str, + conf: &AuthentikConfig, + token: &str) + -> Result +{ + CLIENT .post(&format!( "{}/api/v3/core/applications/", conf.authentik_url )) .bearer_auth(token) - .json(&json) + .json(&generate_app_values(provider, name, oidc_client_config, secret)) .send() .await - .expect("Authentik is not reachable"); - - match res.status() { - StatusCode::CREATED => return Ok(()), - StatusCode::BAD_REQUEST => return Err(format!("Unexpected statuscode Bad Request while creating authintik provider. {res:?}")), - s => return Err(format!("Unexpected statuscode {s} while creating authintik provider. {res:?}")) - } } - - async fn generate_provider(name: &str, oidc_client_config: &OIDCConfig, secret: &str, conf: &AuthentikConfig, token: &str) -> Result<(), String> { @@ -95,7 +95,7 @@ async fn generate_provider(name: &str, oidc_client_config: &OIDCConfig, secret: } } -pub async fn post_application( +pub async fn combine_application( token: &str, name: &str, oidc_client_config: &OIDCConfig, @@ -106,9 +106,52 @@ pub async fn post_application( } else { String::with_capacity(0) }; - //let generated_provider = generate_provider(name, oidc_client_config, &secret, conf, token).await; - let generated_app = generate_application(name, name, oidc_client_config, &secret, conf, token).await; + let generated_provider = generate_provider(name, oidc_client_config, &secret, conf, token).await; + // Todo match if not posible // Create groups for this client let generated_group = create_groups(name, token, conf).await?; + let res = generate_application(name, name, oidc_client_config, &secret, conf, token).await?; + match res.status() { + StatusCode::CREATED => { + println!("Client for {name} created."); + if !oidc_client_config.is_public { + let client_id = generate_app_values(name, name, oidc_client_config, &secret) + .get("slug") + .and_then(Value::as_str) + .expect("Always present"); + } + Ok(SecretResult::Created(secret)) + } + StatusCode::CONFLICT => { + let conflicting_client = get_application(name, token, oidc_client_config, conf).await?; + if app_configs_match(&conflicting_client, &generate_app_values(name, name, oidc_client_config, &secret)) { + Ok(SecretResult::AlreadyExisted(conflicting_client + .as_object() + .and_then(|o| o.get("secret")) + .and_then(Value::as_str) + .unwrap_or("") + .to_owned())) + } else { + Ok(CLIENT + .put(&format!( + "{}/api/v3/core/applicaions/{}", + conf.authentik_url, + conflicting_client + .get("slug") + .and_then(Value::as_str) + .expect("We have a valid client") + )) + .bearer_auth(token) + .json(&generate_app_values(name, name, oidc_client_config, &secret)) + .send() + .await? + .status() + .is_success() + .then_some(SecretResult::AlreadyExisted(secret)) + .expect("We know the client already exists so updating should be successful")) + } + } + s => unreachable!("Unexpected statuscode {s} while creating keycloak client. {res:?}"), + } } diff --git a/central/src/auth/authentik/mod.rs b/central/src/auth/authentik/mod.rs index eb5c1b6..d8169ba 100644 --- a/central/src/auth/authentik/mod.rs +++ b/central/src/auth/authentik/mod.rs @@ -1,8 +1,9 @@ mod test; mod group; -mod app; +pub mod app; use crate::CLIENT; +use app::generate_app_values; use beam_lib::reqwest::{self, StatusCode, Url}; use clap::Parser; use group::create_groups; @@ -20,9 +21,8 @@ pub struct AuthentikConfig { #[clap(long, env)] pub authentik_secret: String, #[clap(long, env, value_parser, value_delimiter = ',', default_values_t = [] as [String; 0])] - pub authentik_service_account_roles: Vec, - #[clap(long, env, value_parser, value_delimiter = ',', default_values_t = [] as [String; 0])] pub authentik_groups_per_bh: Vec, + } async fn get_access_token(conf: &AuthentikConfig) -> reqwest::Result { @@ -85,12 +85,12 @@ async fn compare_applications( secret: &str, ) -> Result { let client = get_application(name, token, oidc_client_config, conf).await?; - let wanted_client = generate_application(name, oidc_client_config, secret); + let wanted_client = generate_app_values(name, name, oidc_client_config, secret); Ok(client.get("secret") == wanted_client.get("secret") - && client_configs_match(&client, &wanted_client)) + && app_configs_match(&client, &wanted_client)) } -fn client_configs_match(a: &Value, b: &Value) -> bool { +fn app_configs_match(a: &Value, b: &Value) -> bool { let includes_other_json_array = |key, comparator: &dyn Fn(_, _) -> bool| a .get(key) .and_then(Value::as_array) @@ -99,7 +99,8 @@ fn client_configs_match(a: &Value, b: &Value) -> bool { .and_then(Value::as_array) .is_some_and(|vec| vec.iter().all(|v| comparator(a_values, v))) ); - + // Todo! compare values test + todo!("compare keys must be changed"); a.get("name") == b.get("name") && includes_other_json_array("defaultClientScopes", &|a_v, v| a_v.contains(v)) && includes_other_json_array("redirectUris", &|a_v, v| a_v.contains(v)) diff --git a/central/src/auth/authentik/test.rs b/central/src/auth/authentik/test.rs index 7ee4706..77a4a98 100644 --- a/central/src/auth/authentik/test.rs +++ b/central/src/auth/authentik/test.rs @@ -18,8 +18,8 @@ async fn get_access_test() { .post(path_url) .form(&json!({ "grant_type": "client_credentials", - "client_id": "MI4DbeyktmjbXJRmUY9tkWvhK7yOzly139EgzhPZ", - "client_secret": "YGcFnXQMI7HqeDUWClhTkZmPtYj4aB2z3khnoMNpCo8CgTOhUqqOFE56dP2WOJoPGOeqdPsVCrR4yvjnJviYK6dY8WeykDqnzAO1xCLHOsPxefcSAa21qe0ru2bwWBi7", + "client_id": "UtKuQ4Yh7xsPOqI8yRH86azKhEjSmrQMo2MyrvNi", + "client_secret": "wFfVgSj1w25xpIvpZGad0nLU1NglYUSYMpPyzhbptDPEGlLlaJ0lHStEN0HHuiHMtTlqMtJoMIa2Ye4psz8EBMLdliahsqYatgcmMEYPvTL3BK0bS1YLVzhhXbxgzVgi", "scope": "openid" })) .send() @@ -61,7 +61,7 @@ async fn get_access_token_via_admin_login() -> reqwest::Result { async fn setup_authentik() -> reqwest::Result<(String, AuthentikConfig)> { //let token = get_access_token_via_admin_login().await?; let token = Token{ - access_token: "kNaiFgKRew9nOJIy2TT4hDs2jIHR9TxVilFmqmqGtHRM2oike8yYv5pfKkq1".to_owned() + access_token: "jq65BCuTfAq0gIGbNeHd3KiFwI2gMbxoI258d2P5BY2OPInCT7Fja3CVV07U".to_owned() }; let res = CLIENT .post("http://localhost:9000/api/v3/core/applications/") @@ -79,7 +79,6 @@ async fn setup_authentik() -> reqwest::Result<(String, AuthentikConfig)> { authentik_url: "http://localhost:9000".parse().unwrap(), authentik_id: "unused in tests".into(), authentik_secret: "unused in tests".into(), - authentik_service_account_roles: vec!["query-users".into(), "view-users".into()], authentik_groups_per_bh: vec!["DKTK_CCP_#".into(), "DKTK_CCP_#_Verwalter".into()], }, )) diff --git a/central/src/auth/config.rs b/central/src/auth/config.rs index f4129e9..ceab9ef 100644 --- a/central/src/auth/config.rs +++ b/central/src/auth/config.rs @@ -6,7 +6,7 @@ use shared::{SecretResult, OIDCConfig}; use crate::auth::keycloak::{KeyCloakConfig, self}; -use super::authentik::AuthentikConfig; +use super::authentik::{self, AuthentikConfig}; /// Central secret sync #[derive(Debug, Parser)] @@ -36,13 +36,23 @@ pub enum OIDCProvider { impl OIDCProvider { pub fn try_init() -> Option { - KeyCloakConfig::try_parse().map_err(|e| println!("{e}")).ok().map(Self::Keycloak) + match KeyCloakConfig::try_parse() { + Ok(res) => return Some(OIDCProvider::Keycloak(res)), + Err(e) => println!("{e}") + } + match AuthentikConfig::try_parse() { + Ok(res) => return Some(OIDCProvider::Authentik(res)), + Err(e) => println!("{e}") + } + //KeyCloakConfig::try_parse().map_err(|e| println!("{e}")).ok().map(Self::Keycloak) + //AuthentikConfig::try_parse().map_err(|e| println!("{e}")).ok().map(Self::Authentik)) + None } pub async fn create_client(&self, name: &str, oidc_client_config: OIDCConfig) -> Result { match self { OIDCProvider::Keycloak(conf) => keycloak::create_client(name, oidc_client_config, conf).await, - OIDCProvider::Authentik(conf) => todo!("create Authentik") + OIDCProvider::Authentik(conf) => authentik::app::create_application(name, oidc_client_config, conf).await }.map_err(|e| { println!("Failed to create client: {e}"); "Error creating OIDC client".into() @@ -59,7 +69,14 @@ impl OIDCProvider { "Failed to validate client. See upstrean logs.".into() }) }, - OIDCProvider::Authentik(conf) => todo!("create Authentik") + OIDCProvider::Authentik(conf) => { + authentik::validate_application(name, oidc_client_config, secret, conf) + .await + .map_err(|e| { + eprintln!("Failed to validate client {name}: {e}"); + "Failed to validate client. See upstrean logs.".into() + }) + } } } } From 6364ee141a0278b1026ff06a3110699167d2adc6 Mon Sep 17 00:00:00 2001 From: Martin Jurk Date: Tue, 22 Oct 2024 17:19:23 +0200 Subject: [PATCH 10/37] flow and property set --- central/src/auth/authentik/app.rs | 21 +++++-- central/src/auth/authentik/group.rs | 3 +- central/src/auth/authentik/mod.rs | 86 ++++++++++++++++++++++++++--- central/src/auth/authentik/test.rs | 51 ++++++++--------- central/src/auth/config.rs | 11 +++- central/src/main.rs | 2 +- 6 files changed, 132 insertions(+), 42 deletions(-) diff --git a/central/src/auth/authentik/app.rs b/central/src/auth/authentik/app.rs index e4b4ac7..33a4b08 100644 --- a/central/src/auth/authentik/app.rs +++ b/central/src/auth/authentik/app.rs @@ -1,4 +1,5 @@ -use crate::CLIENT; +use crate::auth::authentik::CLIENT; +use crate::auth::config::FlowPropertymapping; use beam_lib::reqwest::{self, Response, StatusCode, Url}; use clap::Parser; use group::create_groups; @@ -6,7 +7,7 @@ use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use shared::{OIDCConfig, SecretResult}; -use super::{app_configs_match, generate_secret, get_access_token, get_application, group, AuthentikConfig}; +use super::{app_configs_match, generate_secret, get_access_token, get_application, get_property_mappings_uuids, group, AuthentikConfig}; pub async fn create_application( name: &str, @@ -62,14 +63,26 @@ async fn generate_application( async fn generate_provider(name: &str, oidc_client_config: &OIDCConfig, secret: &str, conf: &AuthentikConfig, token: &str) -> Result<(), String> { + let mapping = FlowPropertymapping::new(conf, token).await.expect("missing flow or property"); + let secret = (!oidc_client_config.is_public).then_some(secret); let id = format!("{name}-{}", if oidc_client_config.is_public { "public" } else { "private" }); let mut json = json!({ "name": id, "client_id": id, - "authorization_flow": "", // flow uuid + "authorization_flow": mapping.authorization_flow, + "property_mappings": [ + mapping.property_mapping.get("web-origins"), + mapping.property_mapping.get("acr"), + mapping.property_mapping.get("profile"), + mapping.property_mapping.get("roles"), + mapping.property_mapping.get("email"), + mapping.property_mapping.get("microprofile-jwt"), + mapping.property_mapping.get("groups") + ], "redirect_uris": oidc_client_config.redirect_urls, }); + if oidc_client_config.is_public { json.as_object_mut().unwrap().insert("client_type".to_owned(), "public".into()); } else { @@ -113,7 +126,7 @@ pub async fn combine_application( let res = generate_application(name, name, oidc_client_config, &secret, conf, token).await?; match res.status() { - StatusCode::CREATED => { + StatusCode::OK => { println!("Client for {name} created."); if !oidc_client_config.is_public { let client_id = generate_app_values(name, name, oidc_client_config, &secret) diff --git a/central/src/auth/authentik/group.rs b/central/src/auth/authentik/group.rs index 92ab860..fd7ba43 100644 --- a/central/src/auth/authentik/group.rs +++ b/central/src/auth/authentik/group.rs @@ -30,7 +30,8 @@ pub async fn post_group(name: &str, token: &str, conf: &AuthentikConfig) -> reqw .send() .await?; match res.status() { - StatusCode::CREATED => println!("Created group {name}"), + StatusCode::CREATED => println!("Created group {name}"), + StatusCode::OK => println!("Created group {name}"), StatusCode::CONFLICT => println!("Group {name} already existed"), s => unreachable!("Unexpected statuscode {s} while creating group {name}") } diff --git a/central/src/auth/authentik/mod.rs b/central/src/auth/authentik/mod.rs index d8169ba..a456ed9 100644 --- a/central/src/auth/authentik/mod.rs +++ b/central/src/auth/authentik/mod.rs @@ -2,14 +2,17 @@ mod test; mod group; pub mod app; +use std::collections::HashMap; + use crate::CLIENT; use app::generate_app_values; -use beam_lib::reqwest::{self, StatusCode, Url}; -use clap::Parser; -use group::create_groups; +use beam_lib::reqwest::{self, Url}; +use clap::{builder::Str, Parser}; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; -use shared::{OIDCConfig, SecretResult}; +use shared::OIDCConfig; + +use super::config::FlowPropertymapping; #[derive(Debug, Parser, Clone)] pub struct AuthentikConfig { @@ -22,7 +25,30 @@ pub struct AuthentikConfig { pub authentik_secret: String, #[clap(long, env, value_parser, value_delimiter = ',', default_values_t = [] as [String; 0])] pub authentik_groups_per_bh: Vec, +} +// ctruct is in config +impl FlowPropertymapping { + async fn new(conf: &AuthentikConfig, token: &str) -> Option { + let flow_key = "authorization_flow"; + let property_keys = vec![ + "web-origins", + "acr", + "profile", + "roles", + "email", + "microprofile-jwt", + "groups" + ]; + let flow_url = "/api/v3/flows/instances/?ordering=slug&page=1&page_size=20&search="; + let property_url = "/api/v3/propertymappings/all/?managed__isnull=true&ordering=name&page=1&page_size=20&search="; + let property_mapping = get_property_mappings_uuids(property_url, conf, token, property_keys).await; + let authorization_flow = get_uuid(flow_url, conf, token, flow_key).await; // flow uuid + return Some(FlowPropertymapping{ + authorization_flow, + property_mapping + }); + } } async fn get_access_token(conf: &AuthentikConfig) -> reqwest::Result { @@ -99,15 +125,59 @@ fn app_configs_match(a: &Value, b: &Value) -> bool { .and_then(Value::as_array) .is_some_and(|vec| vec.iter().all(|v| comparator(a_values, v))) ); - // Todo! compare values test - todo!("compare keys must be changed"); a.get("name") == b.get("name") - && includes_other_json_array("defaultClientScopes", &|a_v, v| a_v.contains(v)) + && includes_other_json_array("authorization_flow", &|a_v, v| a_v.contains(v)) && includes_other_json_array("redirectUris", &|a_v, v| a_v.contains(v)) - && includes_other_json_array("protocolMappers", &|a_v, v| a_v.iter().any(|a_v| a_v.get("name") == v.get("name"))) + && includes_other_json_array("property_mappings", &|a_v, v| a_v.iter().any(|a_v| a_v.get("name") == v.get("name"))) } +async fn get_uuid(target_url: &str, conf: &AuthentikConfig, token: &str, search_key: &str) -> String { + println!("{:?}", search_key); + let target_value: reqwest::Result = CLIENT + .get(&format!( + "{}{}{}", + conf.authentik_url, + target_url, + search_key + )) + .bearer_auth(token) + .send() + .await + .expect("test faild {search_key}" ) + .json() + .await + .into(); + println!("{:?}", target_value); + // pk is the uuid for this result + target_value + .expect("flow or propertymapping type is not present") + .as_object() + .and_then(|o| { + let res= o.get("results"); + println!("{:?}", res); + res + }) + .and_then(Value::as_array) + .and_then(|a| { + let res = a.get(0); + println!("{:?}", res); + res + }) + .and_then(|o| o.as_object()) + .and_then(|o| o.get("pk")) + .and_then(Value::as_str) + .unwrap_or_else(|| "default-pk-value") + .to_string() + } + +async fn get_property_mappings_uuids(target_url: &str, conf: &AuthentikConfig, token: &str, search_key: Vec<&str>) -> HashMap { + let mut result: HashMap = HashMap::new(); + for key in search_key { + result.insert(key.to_string(), get_uuid(target_url, conf, token, key).await); + } + result +} fn generate_secret() -> String { use rand::Rng; diff --git a/central/src/auth/authentik/test.rs b/central/src/auth/authentik/test.rs index 77a4a98..9c8e767 100644 --- a/central/src/auth/authentik/test.rs +++ b/central/src/auth/authentik/test.rs @@ -2,8 +2,9 @@ use beam_lib::reqwest::{self, Error, StatusCode, Url}; use serde::{Deserialize, Serialize}; use serde_json::json; use shared::{OIDCConfig, SecretResult}; +use crate::auth::authentik::app::{combine_application, generate_app_values}; use crate::auth::authentik::group::create_groups; -use crate::auth::authentik::AuthentikConfig; +use crate::auth::authentik::{app_configs_match, compare_applications, get_application, AuthentikConfig}; use crate::CLIENT; #[derive(Deserialize, Serialize, Debug)] @@ -61,18 +62,8 @@ async fn get_access_token_via_admin_login() -> reqwest::Result { async fn setup_authentik() -> reqwest::Result<(String, AuthentikConfig)> { //let token = get_access_token_via_admin_login().await?; let token = Token{ - access_token: "jq65BCuTfAq0gIGbNeHd3KiFwI2gMbxoI258d2P5BY2OPInCT7Fja3CVV07U".to_owned() + access_token: "ztIT7PfCQKm2Y2VFEr2IVKs4ehvtBesBWRj71PhOZIGrLoKpDoRtO0mPUlGC".to_owned() }; - let res = CLIENT - .post("http://localhost:9000/api/v3/core/applications/") - .bearer_auth(&token.access_token) - .json(&json!({ - "name": "groups", - "protocol": "openid-connect" - })) - .send() - .await?; - dbg!(&res.status()); Ok(( token.access_token, AuthentikConfig { @@ -84,39 +75,45 @@ async fn setup_authentik() -> reqwest::Result<(String, AuthentikConfig)> { )) } -/* -#[ignore = "Requires setting up a keycloak"] + #[tokio::test] async fn test_create_client() -> reqwest::Result<()> { - let (token, conf) = setup_keycloak().await?; - let name = "test"; + let (token, conf) = setup_authentik().await?; + let name = "home"; // public client let client_config = OIDCConfig { is_public: true, redirect_urls: vec!["http://foo/bar".into()] }; - let (SecretResult::Created(pw) | SecretResult::AlreadyExisted(pw)) = dbg!(post_client(&token, name, &client_config, &conf).await?) else { + let (SecretResult::Created(pw) | SecretResult::AlreadyExisted(pw)) = dbg!(combine_application(&token, name, &client_config, &conf).await?) else { panic!("Not created or existed") }; - let c = dbg!(get_client(name, &token, &client_config, &conf).await.unwrap()); - assert!(client_configs_match(&c, &generate_client(name, &client_config, &pw))); - assert!(dbg!(compare_clients(&token, name, &client_config, &conf, &pw).await?)); + let c = dbg!(get_application(name, &token, &client_config, &conf).await.unwrap()); + assert!(app_configs_match(&c, &generate_app_values(name, name, &client_config, &pw))); + assert!(dbg!(compare_applications(&token, name, &client_config, &conf, &pw).await?)); // private client let client_config = OIDCConfig { is_public: false, redirect_urls: vec!["http://foo/bar".into()] }; - let (SecretResult::Created(pw) | SecretResult::AlreadyExisted(pw)) = dbg!(post_client(&token, name, &client_config, &conf).await?) else { + let (SecretResult::Created(pw) | SecretResult::AlreadyExisted(pw)) = dbg!(combine_application(&token, name, &client_config, &conf).await?) else { panic!("Not created or existed") }; - let c = dbg!(get_client(name, &token, &client_config, &conf).await.unwrap()); - assert!(client_configs_match(&c, &generate_client(name, &client_config, &pw))); - assert!(dbg!(compare_clients(&token, name, &client_config, &conf, &pw).await?)); + let c = dbg!(get_application(name, &token, &client_config, &conf).await.unwrap()); + assert!(app_configs_match(&c, &generate_app_values(name, name, &client_config, &pw))); + assert!(dbg!(compare_applications(&token, name, &client_config, &conf, &pw).await?)); Ok(()) } -*/ + #[tokio::test] -async fn service_account_test() -> reqwest::Result<()> { +async fn group_test() -> reqwest::Result<()> { let (token, conf) = setup_authentik().await?; - create_groups("test", &token, &conf).await?; + create_groups("em", &token, &conf).await?; // dbg!(get_realm_permission_roles(&token, &conf).await?); // add_service_account_roles(&token, "test-private", &conf).await?; Ok(()) } + +#[tokio::test] +async fn test_flow_property() -> reqwest::Result<()> { + let (token, conf) = setup_authentik().await?; + + Ok(()) +} \ No newline at end of file diff --git a/central/src/auth/config.rs b/central/src/auth/config.rs index ceab9ef..9382163 100644 --- a/central/src/auth/config.rs +++ b/central/src/auth/config.rs @@ -1,7 +1,8 @@ -use std::{net::SocketAddr, convert::Infallible}; +use std::{collections::HashMap, convert::Infallible, net::SocketAddr}; use beam_lib::{AppId, reqwest::Url}; use clap::Parser; +use serde::{Deserialize, Serialize}; use shared::{SecretResult, OIDCConfig}; use crate::auth::keycloak::{KeyCloakConfig, self}; @@ -26,6 +27,7 @@ pub struct Config { /// The app id of this application #[clap(long, env, value_parser=|id: &str| Ok::<_, Infallible>(AppId::new_unchecked(id)))] pub beam_id: AppId, + } #[derive(Clone, Debug)] @@ -80,3 +82,10 @@ impl OIDCProvider { } } } + +#[derive(Debug, Serialize, Deserialize)] +pub struct FlowPropertymapping { + pub authorization_flow: String, + pub property_mapping: HashMap +} + diff --git a/central/src/main.rs b/central/src/main.rs index 2840305..736bfb1 100644 --- a/central/src/main.rs +++ b/central/src/main.rs @@ -2,7 +2,7 @@ use std::{collections::HashSet, time::Duration}; use beam_lib::{reqwest::Client, BeamClient, BlockingOptions, TaskRequest, TaskResult, AppId}; use clap::Parser; -use auth::config::{Config, OIDCProvider}; +use auth::config::{Config, FlowPropertymapping, OIDCProvider}; use once_cell::sync::Lazy; use shared::{SecretRequest, SecretResult, SecretRequestType}; From acb169429b753cf6b66b2cc78c209762fb3c8d9f Mon Sep 17 00:00:00 2001 From: Martin Jurk Date: Wed, 23 Oct 2024 08:44:49 +0200 Subject: [PATCH 11/37] test for flow uuid --- central/src/auth/authentik/test.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/central/src/auth/authentik/test.rs b/central/src/auth/authentik/test.rs index 9c8e767..ae5500d 100644 --- a/central/src/auth/authentik/test.rs +++ b/central/src/auth/authentik/test.rs @@ -4,7 +4,7 @@ use serde_json::json; use shared::{OIDCConfig, SecretResult}; use crate::auth::authentik::app::{combine_application, generate_app_values}; use crate::auth::authentik::group::create_groups; -use crate::auth::authentik::{app_configs_match, compare_applications, get_application, AuthentikConfig}; +use crate::auth::authentik::{app_configs_match, compare_applications, get_application, get_uuid, AuthentikConfig}; use crate::CLIENT; #[derive(Deserialize, Serialize, Debug)] @@ -62,7 +62,7 @@ async fn get_access_token_via_admin_login() -> reqwest::Result { async fn setup_authentik() -> reqwest::Result<(String, AuthentikConfig)> { //let token = get_access_token_via_admin_login().await?; let token = Token{ - access_token: "ztIT7PfCQKm2Y2VFEr2IVKs4ehvtBesBWRj71PhOZIGrLoKpDoRtO0mPUlGC".to_owned() + access_token: "iLAxhzywZhZUHKyCuk2ZPRDFxGJYNml0oTs78qF82kTlUyp5JRGVc4UrL2V8".to_owned() }; Ok(( token.access_token, @@ -105,15 +105,21 @@ async fn test_create_client() -> reqwest::Result<()> { #[tokio::test] async fn group_test() -> reqwest::Result<()> { let (token, conf) = setup_authentik().await?; - create_groups("em", &token, &conf).await?; + create_groups("e", &token, &conf).await?; // dbg!(get_realm_permission_roles(&token, &conf).await?); // add_service_account_roles(&token, "test-private", &conf).await?; Ok(()) } #[tokio::test] -async fn test_flow_property() -> reqwest::Result<()> { - let (token, conf) = setup_authentik().await?; +async fn test_flow_property() { + let (token, conf) = setup_authentik().await.expect("Cannot setup authentik as test"); + let test_key = "groups"; + let flow_url = "/api/v3/flows/instances/?ordering=slug&page=1&page_size=20&search="; + let res = get_uuid(flow_url, &conf, &token, test_key).await; + if res.is_empty() { + } else { + dbg!(res); + } - Ok(()) } \ No newline at end of file From 7eee90a9e4a66538599890a8b930ea3923aa254b Mon Sep 17 00:00:00 2001 From: Martin Jurk Date: Wed, 23 Oct 2024 09:27:14 +0200 Subject: [PATCH 12/37] test ignored for build --- central/src/auth/authentik/test.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/central/src/auth/authentik/test.rs b/central/src/auth/authentik/test.rs index ae5500d..439b801 100644 --- a/central/src/auth/authentik/test.rs +++ b/central/src/auth/authentik/test.rs @@ -12,6 +12,8 @@ struct Token { access_token: String, } + +#[ignore = "Requires setting up a authentik"] #[tokio::test] async fn get_access_test() { let path_url = "http://localhost:9000/application/o/token/"; @@ -75,7 +77,7 @@ async fn setup_authentik() -> reqwest::Result<(String, AuthentikConfig)> { )) } - +#[ignore = "Requires setting up a authentik"] #[tokio::test] async fn test_create_client() -> reqwest::Result<()> { let (token, conf) = setup_authentik().await?; @@ -101,7 +103,7 @@ async fn test_create_client() -> reqwest::Result<()> { Ok(()) } - +#[ignore = "Requires setting up a authentik"] #[tokio::test] async fn group_test() -> reqwest::Result<()> { let (token, conf) = setup_authentik().await?; @@ -111,6 +113,7 @@ async fn group_test() -> reqwest::Result<()> { Ok(()) } +#[ignore = "Requires setting up a authentik"] #[tokio::test] async fn test_flow_property() { let (token, conf) = setup_authentik().await.expect("Cannot setup authentik as test"); From 8e1abead36f098fa43041b1e30fa84e69086c9ac Mon Sep 17 00:00:00 2001 From: janskiba Date: Thu, 24 Oct 2024 07:53:53 +0000 Subject: [PATCH 13/37] feat: add property caching --- central/src/auth/authentik/mod.rs | 14 ++++++++++---- central/src/auth/config.rs | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/central/src/auth/authentik/mod.rs b/central/src/auth/authentik/mod.rs index a456ed9..ca6f60b 100644 --- a/central/src/auth/authentik/mod.rs +++ b/central/src/auth/authentik/mod.rs @@ -2,7 +2,7 @@ mod test; mod group; pub mod app; -use std::collections::HashMap; +use std::{collections::HashMap, sync::Mutex}; use crate::CLIENT; use app::generate_app_values; @@ -29,7 +29,11 @@ pub struct AuthentikConfig { // ctruct is in config impl FlowPropertymapping { - async fn new(conf: &AuthentikConfig, token: &str) -> Option { + async fn new(conf: &AuthentikConfig, token: &str) -> reqwest::Result { + static PROPERTY_MAPPING_CACHE: Mutex> = Mutex::new(None); + if let Some(flow) = PROPERTY_MAPPING_CACHE.lock().unwrap().as_ref() { + return Ok(flow.clone()); + } let flow_key = "authorization_flow"; let property_keys = vec![ "web-origins", @@ -44,10 +48,12 @@ impl FlowPropertymapping { let property_url = "/api/v3/propertymappings/all/?managed__isnull=true&ordering=name&page=1&page_size=20&search="; let property_mapping = get_property_mappings_uuids(property_url, conf, token, property_keys).await; let authorization_flow = get_uuid(flow_url, conf, token, flow_key).await; // flow uuid - return Some(FlowPropertymapping{ + let mapping = FlowPropertymapping{ authorization_flow, property_mapping - }); + }; + *PROPERTY_MAPPING_CACHE.lock().unwrap() = Some(mapping.clone()); + Ok(mapping) } } diff --git a/central/src/auth/config.rs b/central/src/auth/config.rs index 9382163..5795946 100644 --- a/central/src/auth/config.rs +++ b/central/src/auth/config.rs @@ -83,7 +83,7 @@ impl OIDCProvider { } } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct FlowPropertymapping { pub authorization_flow: String, pub property_mapping: HashMap From c59c4958eb2cf18137b34ee6f76a82db37c14f46 Mon Sep 17 00:00:00 2001 From: janskiba Date: Thu, 24 Oct 2024 08:09:14 +0000 Subject: [PATCH 14/37] refactor: clean up oidc init logic --- central/src/auth/config.rs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/central/src/auth/config.rs b/central/src/auth/config.rs index 5795946..d451081 100644 --- a/central/src/auth/config.rs +++ b/central/src/auth/config.rs @@ -38,17 +38,14 @@ pub enum OIDCProvider { impl OIDCProvider { pub fn try_init() -> Option { - match KeyCloakConfig::try_parse() { - Ok(res) => return Some(OIDCProvider::Keycloak(res)), - Err(e) => println!("{e}") - } - match AuthentikConfig::try_parse() { - Ok(res) => return Some(OIDCProvider::Authentik(res)), - Err(e) => println!("{e}") + match (KeyCloakConfig::try_parse(), AuthentikConfig::try_parse()) { + (Ok(key), _) => Some(OIDCProvider::Keycloak(key)), + (_, Ok(auth)) => Some(OIDCProvider::Authentik(auth)), + (Err(e), _) => { + eprintln!("{e:#?}"); + None + } } - //KeyCloakConfig::try_parse().map_err(|e| println!("{e}")).ok().map(Self::Keycloak) - //AuthentikConfig::try_parse().map_err(|e| println!("{e}")).ok().map(Self::Authentik)) - None } pub async fn create_client(&self, name: &str, oidc_client_config: OIDCConfig) -> Result { From 8e98fb37f2ae035b418df6dac25372126a4c663c Mon Sep 17 00:00:00 2001 From: Martin Jurk Date: Thu, 31 Oct 2024 12:59:43 +0100 Subject: [PATCH 15/37] provider compare authentik and keyclaok bail macro instat of unteachable --- central/Cargo.toml | 1 + central/src/auth/authentik/app.rs | 231 +++++++++----------- central/src/auth/authentik/group.rs | 93 +------- central/src/auth/authentik/mod.rs | 190 +++++++++------- central/src/auth/authentik/provider.rs | 91 ++++++++ central/src/auth/authentik/test.rs | 25 +-- central/src/auth/config.rs | 4 +- central/src/auth/keycloak/client.rs | 165 ++++++++++++++ central/src/auth/keycloak/mod.rs | 291 +++---------------------- central/src/auth/keycloak/test.rs | 96 ++++++++ central/src/auth/mod.rs | 2 +- 11 files changed, 608 insertions(+), 581 deletions(-) create mode 100644 central/src/auth/authentik/provider.rs create mode 100644 central/src/auth/keycloak/client.rs create mode 100644 central/src/auth/keycloak/test.rs diff --git a/central/Cargo.toml b/central/Cargo.toml index 036e84c..2a6b0ed 100644 --- a/central/Cargo.toml +++ b/central/Cargo.toml @@ -15,3 +15,4 @@ futures = { workspace = true } serde_json = "1" rand = "0.8" reqwest = { version = "0.12", default_features = false, features = ["default-tls"] } +anyhow = "1.0.91" diff --git a/central/src/auth/authentik/app.rs b/central/src/auth/authentik/app.rs index 33a4b08..fdba476 100644 --- a/central/src/auth/authentik/app.rs +++ b/central/src/auth/authentik/app.rs @@ -1,150 +1,67 @@ use crate::auth::authentik::CLIENT; -use crate::auth::config::FlowPropertymapping; -use beam_lib::reqwest::{self, Response, StatusCode, Url}; -use clap::Parser; -use group::create_groups; -use serde::{Deserialize, Serialize}; +use beam_lib::reqwest::{self, Response, StatusCode}; use serde_json::{json, Value}; -use shared::{OIDCConfig, SecretResult}; +use shared::OIDCConfig; -use super::{app_configs_match, generate_secret, get_access_token, get_application, get_property_mappings_uuids, group, AuthentikConfig}; - -pub async fn create_application( - name: &str, - oidc_client_config: OIDCConfig, - conf: &AuthentikConfig, -) -> reqwest::Result { - let token = get_access_token(conf).await?; - combine_application(&token, name, &oidc_client_config, conf).await -} +use super::AuthentikConfig; pub fn generate_app_values( - provider: &str, - name: &str, - oidc_client_config: &OIDCConfig, - secret: &str, -) -> Value -{ - let secret = (!oidc_client_config.is_public).then_some(secret); - let id = format!("{name}-{}", if oidc_client_config.is_public { "public" } else { "private" }); -// Todo noch anpassen - let mut json = json!({ + provider: &str, + name: &str, + oidc_client_config: &OIDCConfig, +) -> Value { + let id = format!( + "{name}-{}", + if oidc_client_config.is_public { + "public" + } else { + "private" + } + ); + // Todo noch anpassen + json!({ "name": id, "slug": id, "provider": provider, "group": name - }); - if let Some(secret) = secret { - json.as_object_mut().unwrap().insert("secret".to_owned(), secret.into()); - } - json + }) } -async fn generate_application( - provider: &str, - name: &str, - oidc_client_config: &OIDCConfig, - secret: &str, - conf: &AuthentikConfig, - token: &str) - -> Result -{ +pub async fn generate_application( + provider: &str, + name: &str, + oidc_client_config: &OIDCConfig, + conf: &AuthentikConfig, + token: &str, +) -> reqwest::Result { CLIENT - .post(&format!( - "{}/api/v3/core/applications/", - conf.authentik_url - )) - .bearer_auth(token) - .json(&generate_app_values(provider, name, oidc_client_config, secret)) - .send() - .await -} - -async fn generate_provider(name: &str, oidc_client_config: &OIDCConfig, secret: &str, conf: &AuthentikConfig, token: &str) --> Result<(), String> -{ - let mapping = FlowPropertymapping::new(conf, token).await.expect("missing flow or property"); - - let secret = (!oidc_client_config.is_public).then_some(secret); - let id = format!("{name}-{}", if oidc_client_config.is_public { "public" } else { "private" }); - let mut json = json!({ - "name": id, - "client_id": id, - "authorization_flow": mapping.authorization_flow, - "property_mappings": [ - mapping.property_mapping.get("web-origins"), - mapping.property_mapping.get("acr"), - mapping.property_mapping.get("profile"), - mapping.property_mapping.get("roles"), - mapping.property_mapping.get("email"), - mapping.property_mapping.get("microprofile-jwt"), - mapping.property_mapping.get("groups") - ], - "redirect_uris": oidc_client_config.redirect_urls, - }); - - if oidc_client_config.is_public { - json.as_object_mut().unwrap().insert("client_type".to_owned(), "public".into()); - } else { - json.as_object_mut().unwrap().insert("client_type".to_owned(), "confidential".into()); - } - if let Some(secret) = secret { - json.as_object_mut().unwrap().insert("client_secret".to_owned(), secret.into()); - } - let res = CLIENT - .post(&format!( - "{}/api/v3/providers/oauth2/", - conf.authentik_url - )) - .bearer_auth(token) - .json(&json) - .send() - .await.expect("Authentik not reachable"); - - match res.status() { - StatusCode::CREATED => return Ok(()), - StatusCode::BAD_REQUEST => return Err(format!("Unexpected statuscode Bad Request while creating authintik provider. {res:?}")), - s => return Err(format!("Unexpected statuscode {s} while creating authintik provider. {res:?}")) - } + .post(&format!("{}/api/v3/core/applications/", conf.authentik_url)) + .bearer_auth(token) + .json(&generate_app_values( + provider, + name, + oidc_client_config, + )) + .send() + .await } -pub async fn combine_application( +pub async fn check_app_result( token: &str, name: &str, oidc_client_config: &OIDCConfig, conf: &AuthentikConfig, -) -> reqwest::Result { - let secret = if !oidc_client_config.is_public { - generate_secret() - } else { - String::with_capacity(0) - }; - let generated_provider = generate_provider(name, oidc_client_config, &secret, conf, token).await; - // Todo match if not posible - // Create groups for this client - let generated_group = create_groups(name, token, conf).await?; - - let res = generate_application(name, name, oidc_client_config, &secret, conf, token).await?; +) -> anyhow::Result { + let res = generate_application(name, name, oidc_client_config, conf, token).await?; match res.status() { StatusCode::OK => { - println!("Client for {name} created."); - if !oidc_client_config.is_public { - let client_id = generate_app_values(name, name, oidc_client_config, &secret) - .get("slug") - .and_then(Value::as_str) - .expect("Always present"); - } - Ok(SecretResult::Created(secret)) + println!("Application for {name} created."); + return Ok(true) } StatusCode::CONFLICT => { let conflicting_client = get_application(name, token, oidc_client_config, conf).await?; - if app_configs_match(&conflicting_client, &generate_app_values(name, name, oidc_client_config, &secret)) { - Ok(SecretResult::AlreadyExisted(conflicting_client - .as_object() - .and_then(|o| o.get("secret")) - .and_then(Value::as_str) - .unwrap_or("") - .to_owned())) + if app_configs_match(&conflicting_client, &generate_app_values(name, name, oidc_client_config)) { + Ok(true) } else { Ok(CLIENT .put(&format!( @@ -156,15 +73,71 @@ pub async fn combine_application( .expect("We have a valid client") )) .bearer_auth(token) - .json(&generate_app_values(name, name, oidc_client_config, &secret)) + .json(&generate_app_values(name, name, oidc_client_config)) .send() .await? .status() - .is_success() - .then_some(SecretResult::AlreadyExisted(secret)) - .expect("We know the client already exists so updating should be successful")) + .is_success()) } } - s => unreachable!("Unexpected statuscode {s} while creating keycloak client. {res:?}"), + s => anyhow::bail!("Unexpected statuscode {s} while creating keycloak client. {res:?}"), } + +} + + +pub async fn get_application( + name: &str, + token: &str, + oidc_client_config: &OIDCConfig, + conf: &AuthentikConfig, +) -> reqwest::Result { + let id = format!( + "{name}-{}", + if oidc_client_config.is_public { + "public" + } else { + "private" + } + ); + CLIENT + .get(&format!( + "{}/api/v3/core/applications/{id}/", + conf.authentik_url + )) + .bearer_auth(token) + .send() + .await? + .json() + .await +} + +pub async fn compare_applications( + token: &str, + name: &str, + oidc_client_config: &OIDCConfig, + conf: &AuthentikConfig, +) -> anyhow::Result { + let client = get_application(name, token, oidc_client_config, conf).await?; + let wanted_client = generate_app_values(name, name, oidc_client_config); + Ok(client.get("client_secret") == wanted_client.get("client_ secret") + && app_configs_match(&client, &wanted_client)) } + +pub fn app_configs_match(a: &Value, b: &Value) -> bool { + let includes_other_json_array = |key, comparator: &dyn Fn(_, _) -> bool| { + a.get(key) + .and_then(Value::as_array) + .is_some_and(|a_values| { + b.get(key) + .and_then(Value::as_array) + .is_some_and(|vec| vec.iter().all(|v| comparator(a_values, v))) + }) + }; + a.get("name") == b.get("name") + && includes_other_json_array("authorization_flow", &|a_v, v| a_v.contains(v)) + && includes_other_json_array("redirectUris", &|a_v, v| a_v.contains(v)) + && includes_other_json_array("property_mappings", &|a_v, v| { + a_v.iter().any(|a_v| a_v.get("name") == v.get("name")) + }) +} \ No newline at end of file diff --git a/central/src/auth/authentik/group.rs b/central/src/auth/authentik/group.rs index fd7ba43..22d6598 100644 --- a/central/src/auth/authentik/group.rs +++ b/central/src/auth/authentik/group.rs @@ -5,7 +5,7 @@ use serde_json::json; use super::AuthentikConfig; -pub async fn create_groups(name: &str, token: &str, conf: &AuthentikConfig) -> reqwest::Result<()> { +pub async fn create_groups(name: &str, token: &str, conf: &AuthentikConfig) -> anyhow::Result<()> { let capitalize = |s: &str| { let mut chrs = s.chars(); chrs.next().map(char::to_uppercase).map(Iterator::collect).unwrap_or(String::new()) + chrs.as_str() @@ -17,7 +17,7 @@ pub async fn create_groups(name: &str, token: &str, conf: &AuthentikConfig) -> r Ok(()) } -pub async fn post_group(name: &str, token: &str, conf: &AuthentikConfig) -> reqwest::Result<()> { +pub async fn post_group(name: &str, token: &str, conf: &AuthentikConfig) -> anyhow::Result<()> { let res = CLIENT .post(&format!( "{}/api/v3/core/groups/", @@ -33,94 +33,7 @@ pub async fn post_group(name: &str, token: &str, conf: &AuthentikConfig) -> reqw StatusCode::CREATED => println!("Created group {name}"), StatusCode::OK => println!("Created group {name}"), StatusCode::CONFLICT => println!("Group {name} already existed"), - s => unreachable!("Unexpected statuscode {s} while creating group {name}") + s => anyhow::bail!("Unexpected statuscode {s} while creating group {name}") } Ok(()) } -/* -async fn add_service_account_roles( - token: &str, - client_id: &str, - conf: &KeyCloakConfig, -) -> reqwest::Result<()> { - if conf.keycloak_service_account_roles.is_empty() { - return Ok(()); - } - #[derive(serde::Deserialize)] - struct UserIdExtractor { - id: String, - } - let service_account_id = CLIENT.get(&format!( - "{}/admin/realms/{}/clients/{}/service-account-user", - conf.keycloak_url, conf.keycloak_realm, client_id - )) - .bearer_auth(token) - .send() - .await? - .json::() - .await? - .id; - let roles: Vec<_> = get_realm_permission_roles(token, conf) - .await? - .into_iter() - .filter(|f| conf.keycloak_service_account_roles.contains(&f.name)) - .collect(); - - assert_eq!(roles.len(), conf.keycloak_service_account_roles.len(), "Failed to find all required service account roles got {roles:#?} but expected all of these: {:#?}", conf.keycloak_service_account_roles); - let realm_id = roles[0].container_id.clone(); - CLIENT.post(&format!( - "{}/admin/realms/{}/users/{}/role-mappings/clients/{}", - conf.keycloak_url, conf.keycloak_realm, service_account_id, realm_id - )) - .bearer_auth(token) - .json(&roles) - .send() - .await?; - - Ok(()) -} - -#[derive(Debug, serde::Deserialize, serde::Serialize)] -struct ServiceAccountRole { - id: String, - #[serde(rename = "containerId", skip_serializing)] - container_id: String, - name: String -} - - -async fn get_realm_permission_roles(token: &str, conf: &KeyCloakConfig) -> reqwest::Result> { - #[derive(Debug, serde::Deserialize)] - struct RealmId { - id: String, - #[serde(rename = "clientId")] - client_id: String - } - let permission_realm = if conf.keycloak_realm == "master" { - "master-realm" - } else { - "realm-management" - }; - let res = CLIENT.get(&format!( - "{}/admin/realms/{}/clients/?q={permission_realm}&search", - conf.keycloak_url, conf.keycloak_realm - )) - .bearer_auth(token) - .send() - .await? - .json::>() - .await?; - let role_client = res.into_iter() - .find(|v| v.client_id.starts_with(permission_realm)) - .expect(&format!("Failed to find realm id for {permission_realm}")); - CLIENT.get(&format!( - "{}/admin/realms/{}/clients/{}/roles", - conf.keycloak_url, conf.keycloak_realm, role_client.id - )) - .bearer_auth(token) - .send() - .await? - .json() - .await -} -*/ \ No newline at end of file diff --git a/central/src/auth/authentik/mod.rs b/central/src/auth/authentik/mod.rs index ca6f60b..866ea37 100644 --- a/central/src/auth/authentik/mod.rs +++ b/central/src/auth/authentik/mod.rs @@ -1,16 +1,21 @@ mod test; mod group; -pub mod app; +mod app; +mod provider; use std::{collections::HashMap, sync::Mutex}; use crate::CLIENT; -use app::generate_app_values; +use anyhow::bail; +use app::{app_configs_match, check_app_result, compare_applications, generate_app_values, generate_application, get_application}; use beam_lib::reqwest::{self, Url}; use clap::{builder::Str, Parser}; +use group::create_groups; +use provider::{generate_provider_values, get_provider, provider_configs_match}; +use reqwest::StatusCode; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; -use shared::OIDCConfig; +use shared::{OIDCConfig, SecretResult}; use super::config::FlowPropertymapping; @@ -47,7 +52,7 @@ impl FlowPropertymapping { let flow_url = "/api/v3/flows/instances/?ordering=slug&page=1&page_size=20&search="; let property_url = "/api/v3/propertymappings/all/?managed__isnull=true&ordering=name&page=1&page_size=20&search="; let property_mapping = get_property_mappings_uuids(property_url, conf, token, property_keys).await; - let authorization_flow = get_uuid(flow_url, conf, token, flow_key).await; // flow uuid + let authorization_flow = get_uuid(flow_url, conf, token, flow_key).await.expect("No default flow present"); // flow uuid let mapping = FlowPropertymapping{ authorization_flow, property_mapping @@ -57,89 +62,93 @@ impl FlowPropertymapping { } } -async fn get_access_token(conf: &AuthentikConfig) -> reqwest::Result { - #[derive(Deserialize, Serialize, Debug)] - struct Token { - access_token: String, - } - CLIENT - .post(&format!( - "{}/application/o/token/", - conf.authentik_url - )) - .form(&json!({ - "grant_type": "client_credentials", - "client_id": conf.authentik_id, - "client_secret": conf.authentik_secret, - "scope": "openid" - })) - .send() - .await? - .json::() - .await - .map(|t| t.access_token) -} -async fn get_application( +pub async fn validate_application( name: &str, - token: &str, oidc_client_config: &OIDCConfig, conf: &AuthentikConfig, -) -> reqwest::Result { - let id = format!("{name}-{}", if oidc_client_config.is_public { "public" } else { "private" }); - CLIENT - .get(&format!( - "{}/api/v3/core/applications/{id}/", - conf.authentik_url - )) - .bearer_auth(token) - .send() - .await? - .json() - .await +) -> anyhow::Result { + let token = get_access_token(conf).await?; + compare_applications(&token, name, oidc_client_config, conf).await } -pub async fn validate_application( +pub async fn create_app_provider( name: &str, - oidc_client_config: &OIDCConfig, - secret: &str, + oidc_client_config: OIDCConfig, conf: &AuthentikConfig, -) -> reqwest::Result { +) -> anyhow::Result { let token = get_access_token(conf).await?; - compare_applications(&token, name, oidc_client_config, conf, secret).await + combine_app_provider(&token, name, &oidc_client_config, conf).await } -async fn compare_applications( +pub async fn combine_app_provider( token: &str, name: &str, oidc_client_config: &OIDCConfig, conf: &AuthentikConfig, - secret: &str, -) -> Result { - let client = get_application(name, token, oidc_client_config, conf).await?; - let wanted_client = generate_app_values(name, name, oidc_client_config, secret); - Ok(client.get("secret") == wanted_client.get("secret") - && app_configs_match(&client, &wanted_client)) +) -> anyhow::Result { + let secret = if !oidc_client_config.is_public { + generate_secret() + } else { + String::with_capacity(0) + }; + let generated_provider = generate_provider_values(name, oidc_client_config, &secret, conf, token) + .await?; + let provider_res = CLIENT + .post(&format!( + "{}/api/v3/providers/oauth2/", + conf.authentik_url + )) + .bearer_auth(token) + .json(&generated_provider) + .send() + .await?; + // Create groups for this client + create_groups(name, token, conf).await?; + match provider_res.status() { + StatusCode::CREATED => { + println!("Client for {name} created."); + check_app_result(token, name, oidc_client_config, conf).await?; + Ok(SecretResult::Created(secret)) + } + StatusCode::CONFLICT => { + let conflicting_provider = get_provider(name, token, oidc_client_config, conf).await?; + if provider_configs_match(&conflicting_provider, &generated_provider) { + check_app_result(token, name, oidc_client_config, conf).await?; + Ok(SecretResult::AlreadyExisted(conflicting_provider + .as_object() + .and_then(|o| o.get("client_secret")) + .and_then(Value::as_str) + .unwrap_or("") + .to_owned())) + } else { + Ok(CLIENT + .put(&format!( + "{}/api/v3/providers/oauth2/{}", + conf.authentik_url, + conflicting_provider + .get("pk") + .and_then(Value::as_str) + .expect("We have a valid client") + )) + .bearer_auth(token) + .json(&generated_provider) + .send() + .await? + .status() + .is_success() + .then_some(SecretResult::AlreadyExisted(secret)) + .expect("We know the provider already exists so updating should be successful")) + } + } + s => bail!("Unexpected statuscode {s} while creating keycloak client. {provider_res:?}"), + } } -fn app_configs_match(a: &Value, b: &Value) -> bool { - let includes_other_json_array = |key, comparator: &dyn Fn(_, _) -> bool| a - .get(key) - .and_then(Value::as_array) - .is_some_and(|a_values| b - .get(key) - .and_then(Value::as_array) - .is_some_and(|vec| vec.iter().all(|v| comparator(a_values, v))) - ); - a.get("name") == b.get("name") - && includes_other_json_array("authorization_flow", &|a_v, v| a_v.contains(v)) - && includes_other_json_array("redirectUris", &|a_v, v| a_v.contains(v)) - && includes_other_json_array("property_mappings", &|a_v, v| a_v.iter().any(|a_v| a_v.get("name") == v.get("name"))) -} -async fn get_uuid(target_url: &str, conf: &AuthentikConfig, token: &str, search_key: &str) -> String { +async fn get_uuid(target_url: &str, conf: &AuthentikConfig, token: &str, search_key: &str) -> Option { println!("{:?}", search_key); - let target_value: reqwest::Result = CLIENT + let target_value: serde_json::Value = CLIENT .get(&format!( "{}{}{}", conf.authentik_url, @@ -149,38 +158,31 @@ async fn get_uuid(target_url: &str, conf: &AuthentikConfig, token: &str, search_ .bearer_auth(token) .send() .await - .expect("test faild {search_key}" ) + .ok()? .json() .await - .into(); - println!("{:?}", target_value); - + .ok()?; // pk is the uuid for this result target_value - .expect("flow or propertymapping type is not present") .as_object() .and_then(|o| { - let res= o.get("results"); - println!("{:?}", res); - res - }) + o.get("results") + }) .and_then(Value::as_array) .and_then(|a| { - let res = a.get(0); - println!("{:?}", res); - res + a.get(0) }) .and_then(|o| o.as_object()) .and_then(|o| o.get("pk")) .and_then(Value::as_str) - .unwrap_or_else(|| "default-pk-value") - .to_string() + .map(|s| s.to_string()) + } async fn get_property_mappings_uuids(target_url: &str, conf: &AuthentikConfig, token: &str, search_key: Vec<&str>) -> HashMap { let mut result: HashMap = HashMap::new(); for key in search_key { - result.insert(key.to_string(), get_uuid(target_url, conf, token, key).await); + result.insert(key.to_string(), get_uuid(target_url, conf, token, key).await.expect(&format!("Property: {:?}", key))); } result } @@ -201,3 +203,25 @@ fn generate_secret() -> String { .collect() } +async fn get_access_token(conf: &AuthentikConfig) -> reqwest::Result { + #[derive(Deserialize, Serialize, Debug)] + struct Token { + access_token: String, + } + CLIENT + .post(&format!( + "{}/application/o/token/", + conf.authentik_url + )) + .form(&json!({ + "grant_type": "client_credentials", + "client_id": conf.authentik_id, + "client_secret": conf.authentik_secret, + "scope": "openid" + })) + .send() + .await? + .json::() + .await + .map(|t| t.access_token) +} diff --git a/central/src/auth/authentik/provider.rs b/central/src/auth/authentik/provider.rs new file mode 100644 index 0000000..e84487c --- /dev/null +++ b/central/src/auth/authentik/provider.rs @@ -0,0 +1,91 @@ +use anyhow::Ok; +use reqwest::{Response, StatusCode}; +use serde_json::{json, Value}; +use shared::{OIDCConfig, SecretResult}; + +use crate::{auth::config::FlowPropertymapping, CLIENT}; + +use super::{get_uuid, AuthentikConfig}; + + + +pub async fn generate_provider_values(name: &str, oidc_client_config: &OIDCConfig, secret: &str, conf: &AuthentikConfig, token: &str) -> anyhow::Result { + let mapping = FlowPropertymapping::new(conf, token).await?; + + let secret = (!oidc_client_config.is_public).then_some(secret); + let id = format!("{name}-{}", if oidc_client_config.is_public { "public" } else { "private" }); + let mut json = json!({ + "name": id, + "client_id": id, + "authorization_flow": mapping.authorization_flow, + "property_mappings": [ + mapping.property_mapping.get("web-origins"), + mapping.property_mapping.get("acr"), + mapping.property_mapping.get("profile"), + mapping.property_mapping.get("roles"), + mapping.property_mapping.get("email"), + mapping.property_mapping.get("microprofile-jwt"), + mapping.property_mapping.get("groups") + ], + "redirect_uris": oidc_client_config.redirect_urls, + }); + + if oidc_client_config.is_public { + json.as_object_mut().unwrap().insert("client_type".to_owned(), "public".into()); + } else { + json.as_object_mut().unwrap().insert("client_type".to_owned(), "confidential".into()); + } + if let Some(secret) = secret { + json.as_object_mut().unwrap().insert("client_secret".to_owned(), secret.into()); + } + Ok(json) +} + +pub async fn get_provider( + name: &str, + token: &str, + oidc_client_config: &OIDCConfig, + conf: &AuthentikConfig, +) -> reqwest::Result { + let id = format!("{name}-{}", if oidc_client_config.is_public { "public" } else { "private" }); + let provider_url = "/api/v3/providers/all/?ordering=name&page=1&page_size=20&search="; + let pk = get_uuid(&provider_url, conf, token, &id).await.expect(&format!("Property: {:?}", id)); + CLIENT + .get(&format!( + "{}/api/v3/providers/oauth2/{pk}/", + conf.authentik_url + )) + .bearer_auth(token) + .send() + .await? + .json() + .await +} + +pub async fn compare_provider( + token: &str, + name: &str, + oidc_client_config: &OIDCConfig, + conf: &AuthentikConfig, + secret: &str, +) -> anyhow::Result { + let client = get_provider(name, token, oidc_client_config, conf).await?; + let wanted_client = generate_provider_values(name, oidc_client_config, secret, conf, token).await?; + Ok(client.get("client_secret") == wanted_client.get("client_secret") + && provider_configs_match(&client, &wanted_client)) +} + +pub fn provider_configs_match(a: &Value, b: &Value) -> bool { + let includes_other_json_array = |key, comparator: &dyn Fn(_, _) -> bool| a + .get(key) + .and_then(Value::as_array) + .is_some_and(|a_values| b + .get(key) + .and_then(Value::as_array) + .is_some_and(|vec| vec.iter().all(|v| comparator(a_values, v))) + ); + a.get("name") == b.get("name") + && includes_other_json_array("authorization_flow", &|a_v, v| a_v.contains(v)) + && includes_other_json_array("redirectUris", &|a_v, v| a_v.contains(v)) + && includes_other_json_array("property_mappings", &|a_v, v| a_v.iter().any(|a_v| a_v.get("name") == v.get("name"))) +} \ No newline at end of file diff --git a/central/src/auth/authentik/test.rs b/central/src/auth/authentik/test.rs index 439b801..27ef404 100644 --- a/central/src/auth/authentik/test.rs +++ b/central/src/auth/authentik/test.rs @@ -2,9 +2,9 @@ use beam_lib::reqwest::{self, Error, StatusCode, Url}; use serde::{Deserialize, Serialize}; use serde_json::json; use shared::{OIDCConfig, SecretResult}; -use crate::auth::authentik::app::{combine_application, generate_app_values}; +use crate::auth::authentik::app::generate_app_values; use crate::auth::authentik::group::create_groups; -use crate::auth::authentik::{app_configs_match, compare_applications, get_application, get_uuid, AuthentikConfig}; +use crate::auth::authentik::{app_configs_match, combine_app_provider, compare_applications, get_application, get_uuid, AuthentikConfig}; use crate::CLIENT; #[derive(Deserialize, Serialize, Debug)] @@ -79,33 +79,33 @@ async fn setup_authentik() -> reqwest::Result<(String, AuthentikConfig)> { #[ignore = "Requires setting up a authentik"] #[tokio::test] -async fn test_create_client() -> reqwest::Result<()> { +async fn test_create_client() -> anyhow::Result<()> { let (token, conf) = setup_authentik().await?; let name = "home"; // public client let client_config = OIDCConfig { is_public: true, redirect_urls: vec!["http://foo/bar".into()] }; - let (SecretResult::Created(pw) | SecretResult::AlreadyExisted(pw)) = dbg!(combine_application(&token, name, &client_config, &conf).await?) else { + let (SecretResult::Created(pw) | SecretResult::AlreadyExisted(pw)) = dbg!(combine_app_provider(&token, name, &client_config, &conf).await?) else { panic!("Not created or existed") }; let c = dbg!(get_application(name, &token, &client_config, &conf).await.unwrap()); - assert!(app_configs_match(&c, &generate_app_values(name, name, &client_config, &pw))); - assert!(dbg!(compare_applications(&token, name, &client_config, &conf, &pw).await?)); + assert!(app_configs_match(&c, &generate_app_values(name, name, &client_config))); + assert!(dbg!(compare_applications(&token, name, &client_config, &conf).await?)); // private client let client_config = OIDCConfig { is_public: false, redirect_urls: vec!["http://foo/bar".into()] }; - let (SecretResult::Created(pw) | SecretResult::AlreadyExisted(pw)) = dbg!(combine_application(&token, name, &client_config, &conf).await?) else { + let (SecretResult::Created(pw) | SecretResult::AlreadyExisted(pw)) = dbg!(combine_app_provider(&token, name, &client_config, &conf).await?) else { panic!("Not created or existed") }; let c = dbg!(get_application(name, &token, &client_config, &conf).await.unwrap()); - assert!(app_configs_match(&c, &generate_app_values(name, name, &client_config, &pw))); - assert!(dbg!(compare_applications(&token, name, &client_config, &conf, &pw).await?)); + assert!(app_configs_match(&c, &generate_app_values(name, name, &client_config))); + assert!(dbg!(compare_applications(&token, name, &client_config, &conf).await?)); Ok(()) } #[ignore = "Requires setting up a authentik"] #[tokio::test] -async fn group_test() -> reqwest::Result<()> { +async fn group_test() -> anyhow::Result<()> { let (token, conf) = setup_authentik().await?; create_groups("e", &token, &conf).await?; // dbg!(get_realm_permission_roles(&token, &conf).await?); @@ -120,9 +120,6 @@ async fn test_flow_property() { let test_key = "groups"; let flow_url = "/api/v3/flows/instances/?ordering=slug&page=1&page_size=20&search="; let res = get_uuid(flow_url, &conf, &token, test_key).await; - if res.is_empty() { - } else { - dbg!(res); - } + dbg!(res); } \ No newline at end of file diff --git a/central/src/auth/config.rs b/central/src/auth/config.rs index d451081..56a37fe 100644 --- a/central/src/auth/config.rs +++ b/central/src/auth/config.rs @@ -51,7 +51,7 @@ impl OIDCProvider { pub async fn create_client(&self, name: &str, oidc_client_config: OIDCConfig) -> Result { match self { OIDCProvider::Keycloak(conf) => keycloak::create_client(name, oidc_client_config, conf).await, - OIDCProvider::Authentik(conf) => authentik::app::create_application(name, oidc_client_config, conf).await + OIDCProvider::Authentik(conf) => authentik::create_app_provider(name, oidc_client_config, conf).await }.map_err(|e| { println!("Failed to create client: {e}"); "Error creating OIDC client".into() @@ -69,7 +69,7 @@ impl OIDCProvider { }) }, OIDCProvider::Authentik(conf) => { - authentik::validate_application(name, oidc_client_config, secret, conf) + authentik::validate_application(name, oidc_client_config, conf) .await .map_err(|e| { eprintln!("Failed to validate client {name}: {e}"); diff --git a/central/src/auth/keycloak/client.rs b/central/src/auth/keycloak/client.rs new file mode 100644 index 0000000..9d7df7e --- /dev/null +++ b/central/src/auth/keycloak/client.rs @@ -0,0 +1,165 @@ +use crate::{auth::keycloak::add_service_account_roles, CLIENT}; +use anyhow::bail; +use beam_lib::reqwest::{self, StatusCode, Url}; +use clap::Parser; +use serde_json::{json, Value}; +use shared::{OIDCConfig, SecretResult}; + +use super::{create_groups, generate_secret, KeyCloakConfig}; + +pub async fn get_client( + name: &str, + token: &str, + oidc_client_config: &OIDCConfig, + conf: &KeyCloakConfig, +) -> reqwest::Result { + let id = format!("{name}-{}", if oidc_client_config.is_public { "public" } else { "private" }); + CLIENT + .get(&format!( + "{}admin/realms/{}/clients/{id}", + conf.keycloak_url, conf.keycloak_realm + )) + .bearer_auth(token) + .send() + .await? + .json() + .await +} + + +pub async fn compare_clients( + token: &str, + name: &str, + oidc_client_config: &OIDCConfig, + conf: &KeyCloakConfig, + secret: &str, +) -> Result { + let client = get_client(name, token, oidc_client_config, conf).await?; + let wanted_client = generate_client(name, oidc_client_config, secret); + Ok(client.get("secret") == wanted_client.get("secret") + && client_configs_match(&client, &wanted_client)) +} + +pub fn client_configs_match(a: &Value, b: &Value) -> bool { + let includes_other_json_array = |key, comparator: &dyn Fn(_, _) -> bool| a + .get(key) + .and_then(Value::as_array) + .is_some_and(|a_values| b + .get(key) + .and_then(Value::as_array) + .is_some_and(|vec| vec.iter().all(|v| comparator(a_values, v))) + ); + + a.get("name") == b.get("name") + && includes_other_json_array("defaultClientScopes", &|a_v, v| a_v.contains(v)) + && includes_other_json_array("redirectUris", &|a_v, v| a_v.contains(v)) + && includes_other_json_array("protocolMappers", &|a_v, v| a_v.iter().any(|a_v| a_v.get("name") == v.get("name"))) +} + +pub fn generate_client(name: &str, oidc_client_config: &OIDCConfig, secret: &str) -> Value { + let secret = (!oidc_client_config.is_public).then_some(secret); + let id = format!("{name}-{}", if oidc_client_config.is_public { "public" } else { "private" }); + let mut json = json!({ + "name": id, + "id": id, + "clientId": id, + "redirectUris": oidc_client_config.redirect_urls, + "webOrigins": ["+"], // Will allow all hosts that are named in redirectUris. This is not the same as '*' + "publicClient": oidc_client_config.is_public, + "serviceAccountsEnabled": !oidc_client_config.is_public, + "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": id, + "id.token.claim": "true", + "access.token.claim": "true" + } + }] + }); + if let Some(secret) = secret { + json.as_object_mut().unwrap().insert("secret".to_owned(), secret.into()); + } + json +} + + +pub async fn post_client( + token: &str, + name: &str, + oidc_client_config: &OIDCConfig, + conf: &KeyCloakConfig, +) -> anyhow::Result { + let secret = if !oidc_client_config.is_public { + generate_secret() + } else { + String::with_capacity(0) + }; + let generated_client = generate_client(name, oidc_client_config, &secret); + let res = CLIENT + .post(&format!( + "{}admin/realms/{}/clients", + conf.keycloak_url, conf.keycloak_realm + )) + .bearer_auth(token) + .json(&generated_client) + .send() + .await?; + // Create groups for this client + create_groups(name, token, conf).await?; + match res.status() { + StatusCode::CREATED => { + println!("Client for {name} created."); + if !oidc_client_config.is_public { + let client_id = generated_client + .get("clientId") + .and_then(Value::as_str) + .expect("Always present"); + add_service_account_roles(token, client_id, conf).await?; + } + Ok(SecretResult::Created(secret)) + } + StatusCode::CONFLICT => { + let conflicting_client = get_client(name, token, oidc_client_config, conf).await?; + if client_configs_match(&conflicting_client, &generated_client) { + Ok(SecretResult::AlreadyExisted(conflicting_client + .as_object() + .and_then(|o| o.get("secret")) + .and_then(Value::as_str) + .unwrap_or("") + .to_owned())) + } else { + Ok(CLIENT + .put(&format!( + "{}admin/realms/{}/clients/{}", + conf.keycloak_url, + conf.keycloak_realm, + conflicting_client + .get("clientId") + .and_then(Value::as_str) + .expect("We have a valid client") + )) + .bearer_auth(token) + .json(&generated_client) + .send() + .await? + .status() + .is_success() + .then_some(SecretResult::AlreadyExisted(secret)) + .expect("We know the client already exists so updating should be successful")) + } + } + s => bail!("Unexpected statuscode {s} while creating keycloak client. {res:?}"), + } +} \ No newline at end of file diff --git a/central/src/auth/keycloak/mod.rs b/central/src/auth/keycloak/mod.rs index 104c4d5..f23bb2f 100644 --- a/central/src/auth/keycloak/mod.rs +++ b/central/src/auth/keycloak/mod.rs @@ -1,6 +1,11 @@ +mod test; +mod client; + use crate::CLIENT; +use anyhow::bail; use beam_lib::reqwest::{self, StatusCode, Url}; use clap::Parser; +use client::{compare_clients, post_client}; use serde_json::{json, Value}; use shared::{OIDCConfig, SecretResult}; @@ -26,71 +31,17 @@ pub struct KeyCloakConfig { pub keycloak_groups_per_bh: Vec, } -async fn get_access_token(conf: &KeyCloakConfig) -> reqwest::Result { - #[derive(serde::Deserialize)] - struct Token { - access_token: String, - } - CLIENT - .post(&format!( - "{}realms/{}/protocol/openid-connect/token", - conf.keycloak_url, conf.keycloak_realm - )) - .form(&json!({ - "client_id": conf.keycloak_id, - "client_secret": conf.keycloak_secret, - "grant_type": "client_credentials" - })) - .send() - .await? - .json::() - .await - .map(|t| t.access_token) -} -#[cfg(test)] -async fn get_access_token_via_admin_login() -> reqwest::Result { - #[derive(serde::Deserialize)] - struct Token { - access_token: String, - } - CLIENT - .post(&format!( - "{}/realms/master/protocol/openid-connect/token", - if cfg!(test) { "http://localhost:1337"} else { "http://keycloak:8080" } - )) - .form(&json!({ - "client_id": "admin-cli", - "username": "admin", - "password": "admin", - "grant_type": "password" - })) - .send() - .await? - .json::() - .await - .map(|t| t.access_token) -} - -async fn get_client( +pub async fn create_client( name: &str, - token: &str, - oidc_client_config: &OIDCConfig, + oidc_client_config: OIDCConfig, conf: &KeyCloakConfig, -) -> reqwest::Result { - let id = format!("{name}-{}", if oidc_client_config.is_public { "public" } else { "private" }); - CLIENT - .get(&format!( - "{}admin/realms/{}/clients/{id}", - conf.keycloak_url, conf.keycloak_realm - )) - .bearer_auth(token) - .send() - .await? - .json() - .await +) -> anyhow::Result { + let token = get_access_token(conf).await?; + post_client(&token, name, &oidc_client_config, conf).await } + pub async fn validate_client( name: &str, oidc_client_config: &OIDCConfig, @@ -101,195 +52,30 @@ pub async fn validate_client( compare_clients(&token, name, oidc_client_config, conf, secret).await } -async fn compare_clients( - token: &str, - name: &str, - oidc_client_config: &OIDCConfig, - conf: &KeyCloakConfig, - secret: &str, -) -> Result { - let client = get_client(name, token, oidc_client_config, conf).await?; - let wanted_client = generate_client(name, oidc_client_config, secret); - Ok(client.get("secret") == wanted_client.get("secret") - && client_configs_match(&client, &wanted_client)) -} - -fn client_configs_match(a: &Value, b: &Value) -> bool { - let includes_other_json_array = |key, comparator: &dyn Fn(_, _) -> bool| a - .get(key) - .and_then(Value::as_array) - .is_some_and(|a_values| b - .get(key) - .and_then(Value::as_array) - .is_some_and(|vec| vec.iter().all(|v| comparator(a_values, v))) - ); - - a.get("name") == b.get("name") - && includes_other_json_array("defaultClientScopes", &|a_v, v| a_v.contains(v)) - && includes_other_json_array("redirectUris", &|a_v, v| a_v.contains(v)) - && includes_other_json_array("protocolMappers", &|a_v, v| a_v.iter().any(|a_v| a_v.get("name") == v.get("name"))) -} -fn generate_client(name: &str, oidc_client_config: &OIDCConfig, secret: &str) -> Value { - let secret = (!oidc_client_config.is_public).then_some(secret); - let id = format!("{name}-{}", if oidc_client_config.is_public { "public" } else { "private" }); - let mut json = json!({ - "name": id, - "id": id, - "clientId": id, - "redirectUris": oidc_client_config.redirect_urls, - "webOrigins": ["+"], // Will allow all hosts that are named in redirectUris. This is not the same as '*' - "publicClient": oidc_client_config.is_public, - "serviceAccountsEnabled": !oidc_client_config.is_public, - "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": id, - "id.token.claim": "true", - "access.token.claim": "true" - } - }] - }); - if let Some(secret) = secret { - json.as_object_mut().unwrap().insert("secret".to_owned(), secret.into()); +async fn get_access_token(conf: &KeyCloakConfig) -> reqwest::Result { + #[derive(serde::Deserialize)] + struct Token { + access_token: String, } - json -} - -#[cfg(test)] -async fn setup_keycloak() -> reqwest::Result<(String, KeyCloakConfig)> { - let token = get_access_token_via_admin_login().await?; - let res = CLIENT - .post("http://localhost:1337/admin/realms/master/client-scopes") - .bearer_auth(&token) - .json(&json!({ - "name": "groups", - "protocol": "openid-connect" - })) - .send() - .await?; - dbg!(&res.status()); - Ok(( - token, - KeyCloakConfig { - keycloak_url: "http://localhost:1337".parse().unwrap(), - keycloak_id: "unused in tests".into(), - keycloak_secret: "unused in tests".into(), - keycloak_realm: "master".into(), - keycloak_service_account_roles: vec!["query-users".into(), "view-users".into()], - keycloak_groups_per_bh: vec!["DKTK_CCP_#".into(), "DKTK_CCP_#_Verwalter".into()], - }, - )) -} - -#[ignore = "Requires setting up a keycloak"] -#[tokio::test] -async fn test_create_client() -> reqwest::Result<()> { - let (token, conf) = setup_keycloak().await?; - let name = "test"; - // public client - let client_config = OIDCConfig { is_public: true, redirect_urls: vec!["http://foo/bar".into()] }; - let (SecretResult::Created(pw) | SecretResult::AlreadyExisted(pw)) = dbg!(post_client(&token, name, &client_config, &conf).await?) else { - panic!("Not created or existed") - }; - let c = dbg!(get_client(name, &token, &client_config, &conf).await.unwrap()); - assert!(client_configs_match(&c, &generate_client(name, &client_config, &pw))); - assert!(dbg!(compare_clients(&token, name, &client_config, &conf, &pw).await?)); - - // private client - let client_config = OIDCConfig { is_public: false, redirect_urls: vec!["http://foo/bar".into()] }; - let (SecretResult::Created(pw) | SecretResult::AlreadyExisted(pw)) = dbg!(post_client(&token, name, &client_config, &conf).await?) else { - panic!("Not created or existed") - }; - let c = dbg!(get_client(name, &token, &client_config, &conf).await.unwrap()); - assert!(client_configs_match(&c, &generate_client(name, &client_config, &pw))); - assert!(dbg!(compare_clients(&token, name, &client_config, &conf, &pw).await?)); - - Ok(()) -} - -async fn post_client( - token: &str, - name: &str, - oidc_client_config: &OIDCConfig, - conf: &KeyCloakConfig, -) -> reqwest::Result { - let secret = if !oidc_client_config.is_public { - generate_secret() - } else { - String::with_capacity(0) - }; - let generated_client = generate_client(name, oidc_client_config, &secret); - let res = CLIENT + CLIENT .post(&format!( - "{}admin/realms/{}/clients", + "{}realms/{}/protocol/openid-connect/token", conf.keycloak_url, conf.keycloak_realm )) - .bearer_auth(token) - .json(&generated_client) + .form(&json!({ + "client_id": conf.keycloak_id, + "client_secret": conf.keycloak_secret, + "grant_type": "client_credentials" + })) .send() - .await?; - // Create groups for this client - create_groups(name, token, conf).await?; - match res.status() { - StatusCode::CREATED => { - println!("Client for {name} created."); - if !oidc_client_config.is_public { - let client_id = generated_client - .get("clientId") - .and_then(Value::as_str) - .expect("Always present"); - add_service_account_roles(token, client_id, conf).await?; - } - Ok(SecretResult::Created(secret)) - } - StatusCode::CONFLICT => { - let conflicting_client = get_client(name, token, oidc_client_config, conf).await?; - if client_configs_match(&conflicting_client, &generated_client) { - Ok(SecretResult::AlreadyExisted(conflicting_client - .as_object() - .and_then(|o| o.get("secret")) - .and_then(Value::as_str) - .unwrap_or("") - .to_owned())) - } else { - Ok(CLIENT - .put(&format!( - "{}admin/realms/{}/clients/{}", - conf.keycloak_url, - conf.keycloak_realm, - conflicting_client - .get("clientId") - .and_then(Value::as_str) - .expect("We have a valid client") - )) - .bearer_auth(token) - .json(&generated_client) - .send() - .await? - .status() - .is_success() - .then_some(SecretResult::AlreadyExisted(secret)) - .expect("We know the client already exists so updating should be successful")) - } - } - s => unreachable!("Unexpected statuscode {s} while creating keycloak client. {res:?}"), - } + .await? + .json::() + .await + .map(|t| t.access_token) } -async fn create_groups(name: &str, token: &str, conf: &KeyCloakConfig) -> reqwest::Result<()> { +async fn create_groups(name: &str, token: &str, conf: &KeyCloakConfig) -> anyhow::Result<()> { let capitalize = |s: &str| { let mut chrs = s.chars(); chrs.next().map(char::to_uppercase).map(Iterator::collect).unwrap_or(String::new()) + chrs.as_str() @@ -301,7 +87,7 @@ async fn create_groups(name: &str, token: &str, conf: &KeyCloakConfig) -> reqwes Ok(()) } -async fn post_group(name: &str, token: &str, conf: &KeyCloakConfig) -> reqwest::Result<()> { +async fn post_group(name: &str, token: &str, conf: &KeyCloakConfig) -> anyhow::Result<()> { let res = CLIENT .post(&format!( "{}admin/realms/{}/groups", @@ -316,7 +102,7 @@ async fn post_group(name: &str, token: &str, conf: &KeyCloakConfig) -> reqwest:: match res.status() { StatusCode::CREATED => println!("Created group {name}"), StatusCode::CONFLICT => println!("Group {name} already existed"), - s => unreachable!("Unexpected statuscode {s} while creating group {name}") + s => bail!("Unexpected statuscode {s} while creating group {name}") } Ok(()) } @@ -337,25 +123,6 @@ fn generate_secret() -> String { .collect() } -pub async fn create_client( - name: &str, - oidc_client_config: OIDCConfig, - conf: &KeyCloakConfig, -) -> reqwest::Result { - let token = get_access_token(conf).await?; - post_client(&token, name, &oidc_client_config, conf).await -} - -#[ignore = "Requires setting up a keycloak"] -#[tokio::test] -async fn service_account_test() -> reqwest::Result<()> { - let (token, conf) = setup_keycloak().await?; - create_groups("test", &token, &conf).await?; - // dbg!(get_realm_permission_roles(&token, &conf).await?); - // add_service_account_roles(&token, "test-private", &conf).await?; - Ok(()) -} - async fn add_service_account_roles( token: &str, client_id: &str, diff --git a/central/src/auth/keycloak/test.rs b/central/src/auth/keycloak/test.rs new file mode 100644 index 0000000..09d556d --- /dev/null +++ b/central/src/auth/keycloak/test.rs @@ -0,0 +1,96 @@ +use crate::auth::keycloak::client::{client_configs_match, compare_clients, generate_client, get_client, post_client}; +use crate::{auth::keycloak::create_groups, CLIENT}; +use crate::auth::keycloak::KeyCloakConfig; +use beam_lib::reqwest::{self, StatusCode, Url}; +use serde_json::{json, Value}; +use shared::{OIDCConfig, SecretResult}; + + +#[cfg(test)] +async fn get_access_token_via_admin_login() -> reqwest::Result { + + #[derive(serde::Deserialize)] + struct Token { + access_token: String, + } + CLIENT + .post(&format!( + "{}/realms/master/protocol/openid-connect/token", + if cfg!(test) { "http://localhost:1337"} else { "http://keycloak:8080" } + )) + .form(&json!({ + "client_id": "admin-cli", + "username": "admin", + "password": "admin", + "grant_type": "password" + })) + .send() + .await? + .json::() + .await + .map(|t| t.access_token) +} + +#[cfg(test)] +async fn setup_keycloak() -> reqwest::Result<(String, KeyCloakConfig)> { + let token = get_access_token_via_admin_login().await?; + let res = CLIENT + .post("http://localhost:1337/admin/realms/master/client-scopes") + .bearer_auth(&token) + .json(&json!({ + "name": "groups", + "protocol": "openid-connect" + })) + .send() + .await?; + dbg!(&res.status()); + Ok(( + token, + KeyCloakConfig { + keycloak_url: "http://localhost:1337".parse().unwrap(), + keycloak_id: "unused in tests".into(), + keycloak_secret: "unused in tests".into(), + keycloak_realm: "master".into(), + keycloak_service_account_roles: vec!["query-users".into(), "view-users".into()], + keycloak_groups_per_bh: vec!["DKTK_CCP_#".into(), "DKTK_CCP_#_Verwalter".into()], + }, + )) +} + + +#[ignore = "Requires setting up a keycloak"] +#[tokio::test] +async fn service_account_test() -> anyhow::Result<()> { + let (token, conf) = setup_keycloak().await?; + create_groups("test", &token, &conf).await?; + // dbg!(get_realm_permission_roles(&token, &conf).await?); + // add_service_account_roles(&token, "test-private", &conf).await?; + Ok(()) +} + + +#[ignore = "Requires setting up a keycloak"] +#[tokio::test] +async fn test_create_client() -> anyhow::Result<()> { + let (token, conf) = setup_keycloak().await?; + let name = "test"; + // public client + let client_config = OIDCConfig { is_public: true, redirect_urls: vec!["http://foo/bar".into()] }; + let (SecretResult::Created(pw) | SecretResult::AlreadyExisted(pw)) = dbg!(post_client(&token, name, &client_config, &conf).await?) else { + panic!("Not created or existed") + }; + let c = dbg!(get_client(name, &token, &client_config, &conf).await.unwrap()); + assert!(client_configs_match(&c, &generate_client(name, &client_config, &pw))); + assert!(dbg!(compare_clients(&token, name, &client_config, &conf, &pw).await?)); + + // private client + let client_config = OIDCConfig { is_public: false, redirect_urls: vec!["http://foo/bar".into()] }; + let (SecretResult::Created(pw) | SecretResult::AlreadyExisted(pw)) = dbg!(post_client(&token, name, &client_config, &conf).await?) else { + panic!("Not created or existed") + }; + let c = dbg!(get_client(name, &token, &client_config, &conf).await.unwrap()); + assert!(client_configs_match(&c, &generate_client(name, &client_config, &pw))); + assert!(dbg!(compare_clients(&token, name, &client_config, &conf, &pw).await?)); + + Ok(()) +} diff --git a/central/src/auth/mod.rs b/central/src/auth/mod.rs index cb2f2f0..fdb44de 100644 --- a/central/src/auth/mod.rs +++ b/central/src/auth/mod.rs @@ -1,3 +1,3 @@ mod keycloak; pub(crate) mod config; -mod authentik; \ No newline at end of file +pub mod authentik; \ No newline at end of file From b4d2bc46074d6f0c6bb6b6d424a035b461ac7717 Mon Sep 17 00:00:00 2001 From: Martin Jurk Date: Tue, 12 Nov 2024 14:29:10 +0100 Subject: [PATCH 16/37] failt test --- central/src/auth/authentik/test.rs | 155 ++++++++++++++++++++--------- 1 file changed, 109 insertions(+), 46 deletions(-) diff --git a/central/src/auth/authentik/test.rs b/central/src/auth/authentik/test.rs index 27ef404..5418b81 100644 --- a/central/src/auth/authentik/test.rs +++ b/central/src/auth/authentik/test.rs @@ -1,17 +1,36 @@ +use crate::auth::authentik::app::generate_app_values; +use crate::auth::authentik::group::create_groups; +use crate::auth::authentik::{ + app_configs_match, combine_app_provider, compare_applications, get_application, get_uuid, + AuthentikConfig, +}; +use crate::CLIENT; use beam_lib::reqwest::{self, Error, StatusCode, Url}; use serde::{Deserialize, Serialize}; use serde_json::json; use shared::{OIDCConfig, SecretResult}; -use crate::auth::authentik::app::generate_app_values; -use crate::auth::authentik::group::create_groups; -use crate::auth::authentik::{app_configs_match, combine_app_provider, compare_applications, get_application, get_uuid, AuthentikConfig}; -use crate::CLIENT; #[derive(Deserialize, Serialize, Debug)] struct Token { access_token: String, } +#[cfg(test)] +async fn setup_authentik() -> reqwest::Result<(String, AuthentikConfig)> { + //let token = get_access_token_via_admin_login().await?; + let token = Token { + access_token: "NXChuwYcHBf4ggcg1VdWdwKaEgqNxwl07nWLSnBsAK27YHNTdi0z45K6Ioun".to_owned(), + }; + Ok(( + token.access_token, + AuthentikConfig { + authentik_url: "http://localhost:9000".parse().unwrap(), + authentik_id: "unused in tests".into(), + authentik_secret: "unused in tests".into(), + authentik_groups_per_bh: vec!["DKTK_CCP_#".into(), "DKTK_CCP_#_Verwalter".into()], + }, + )) +} #[ignore = "Requires setting up a authentik"] #[tokio::test] @@ -45,7 +64,11 @@ async fn get_access_token_via_admin_login() -> reqwest::Result { CLIENT .post(&format!( "{}/application/o/token/", - if cfg!(test) { "http://localhost:9000"} else { "http://authentik:8080" } + if cfg!(test) { + "http://localhost:9000" + } else { + "http://authentik:8080" + } )) .form(&json!({ "client_id": "admin-cli", @@ -60,66 +83,106 @@ async fn get_access_token_via_admin_login() -> reqwest::Result { .map(|t| t.access_token) } -#[cfg(test)] -async fn setup_authentik() -> reqwest::Result<(String, AuthentikConfig)> { - //let token = get_access_token_via_admin_login().await?; - let token = Token{ - access_token: "iLAxhzywZhZUHKyCuk2ZPRDFxGJYNml0oTs78qF82kTlUyp5JRGVc4UrL2V8".to_owned() - }; - Ok(( - token.access_token, - AuthentikConfig { - authentik_url: "http://localhost:9000".parse().unwrap(), - authentik_id: "unused in tests".into(), - authentik_secret: "unused in tests".into(), - authentik_groups_per_bh: vec!["DKTK_CCP_#".into(), "DKTK_CCP_#_Verwalter".into()], - }, - )) -} - -#[ignore = "Requires setting up a authentik"] +//#[ignore = "Requires setting up a authentik"] #[tokio::test] async fn test_create_client() -> anyhow::Result<()> { let (token, conf) = setup_authentik().await?; - let name = "home"; + let name = "office"; // public client - let client_config = OIDCConfig { is_public: true, redirect_urls: vec!["http://foo/bar".into()] }; - let (SecretResult::Created(pw) | SecretResult::AlreadyExisted(pw)) = dbg!(combine_app_provider(&token, name, &client_config, &conf).await?) else { + let client_config = OIDCConfig { + is_public: true, + redirect_urls: vec!["http://foo/bar".into()], + }; + let (SecretResult::Created(pw) | SecretResult::AlreadyExisted(pw)) = + dbg!(combine_app_provider(&token, name, &client_config, &conf).await?) + else { panic!("Not created or existed") }; - let c = dbg!(get_application(name, &token, &client_config, &conf).await.unwrap()); - assert!(app_configs_match(&c, &generate_app_values(name, name, &client_config))); - assert!(dbg!(compare_applications(&token, name, &client_config, &conf).await?)); + let c = dbg!(get_application(name, &token, &client_config, &conf) + .await + .unwrap()); + assert!(app_configs_match( + &c, + &generate_app_values(name, name, &client_config) + )); + assert!(dbg!( + compare_applications(&token, name, &client_config, &conf).await? + )); // private client - let client_config = OIDCConfig { is_public: false, redirect_urls: vec!["http://foo/bar".into()] }; - let (SecretResult::Created(pw) | SecretResult::AlreadyExisted(pw)) = dbg!(combine_app_provider(&token, name, &client_config, &conf).await?) else { + let client_config = OIDCConfig { + is_public: false, + redirect_urls: vec!["http://foo/bar".into()], + }; + let (SecretResult::Created(pw) | SecretResult::AlreadyExisted(pw)) = + dbg!(combine_app_provider(&token, name, &client_config, &conf).await?) + else { panic!("Not created or existed") }; - let c = dbg!(get_application(name, &token, &client_config, &conf).await.unwrap()); - assert!(app_configs_match(&c, &generate_app_values(name, name, &client_config))); - assert!(dbg!(compare_applications(&token, name, &client_config, &conf).await?)); + let c = dbg!(get_application(name, &token, &client_config, &conf) + .await + .unwrap()); + assert!(app_configs_match( + &c, + &generate_app_values(name, name, &client_config) + )); + assert!(dbg!( + compare_applications(&token, name, &client_config, &conf).await? + )); Ok(()) } -#[ignore = "Requires setting up a authentik"] +//#[ignore = "Requires setting up a authentik"] #[tokio::test] async fn group_test() -> anyhow::Result<()> { let (token, conf) = setup_authentik().await?; - create_groups("e", &token, &conf).await?; - // dbg!(get_realm_permission_roles(&token, &conf).await?); - // add_service_account_roles(&token, "test-private", &conf).await?; - Ok(()) + create_groups("team", &token, &conf).await } -#[ignore = "Requires setting up a authentik"] +// #[ignore = "Requires setting up a authentik"] #[tokio::test] -async fn test_flow_property() { - let (token, conf) = setup_authentik().await.expect("Cannot setup authentik as test"); - let test_key = "groups"; +async fn test_flow() { + let (token, conf) = setup_authentik() + .await + .expect("Cannot setup authentik as test"); + let test_key = "authorization_flow"; let flow_url = "/api/v3/flows/instances/?ordering=slug&page=1&page_size=20&search="; let res = get_uuid(flow_url, &conf, &token, test_key).await; - dbg!(res); - -} \ No newline at end of file + dbg!(res); +} + +// #[ignore = "Requires setting up a authentik"] +#[tokio::test] +async fn test_property() { + let (token, conf) = setup_authentik() + .await + .expect("Cannot setup authentik as test"); + let test_key = "web-origins"; + let flow_url = "/api/v3/propertymappings/all/?managed__isnull=true&ordering=name&page=1&page_size=20&search=groups"; + let res = get_uuid(flow_url, &conf, &token, test_key).await; + dbg!(res); +} + +//#[ignore = "Requires setting up a authentik"] +#[tokio::test] +async fn create_property() { + let (token, conf) = setup_authentik() + .await + .expect("Cannot setup authentik as test"); + let acr = "test".to_owned(); + let ext = "return{}".to_owned(); + let json_property = json!({ + "name": acr, + "expression": ext + }); + let propperty_url = "/api/v3/propertymappings/source/oauth/"; + let res = CLIENT + .post(&format!("{}{}", conf.authentik_url, propperty_url)) + .bearer_auth(token) + .json(&json_property) + .send() + .await + .expect("no response"); + dbg!(res); +} From f4a8e3043cb1cd64c18a4e5d44bb9e32383d9b9d Mon Sep 17 00:00:00 2001 From: Martin Jurk Date: Wed, 13 Nov 2024 08:37:27 +0100 Subject: [PATCH 17/37] bug CLIENT static as fn --- central/src/auth/authentik/app.rs | 48 ++++---- central/src/auth/authentik/group.rs | 32 ++++-- central/src/auth/authentik/mod.rs | 151 +++++++++++++------------ central/src/auth/authentik/provider.rs | 83 ++++++++++---- central/src/auth/authentik/test.rs | 53 +++++---- central/src/auth/config.rs | 43 ++++--- central/src/main.rs | 59 ++++++---- 7 files changed, 285 insertions(+), 184 deletions(-) diff --git a/central/src/auth/authentik/app.rs b/central/src/auth/authentik/app.rs index fdba476..e7686a0 100644 --- a/central/src/auth/authentik/app.rs +++ b/central/src/auth/authentik/app.rs @@ -1,15 +1,12 @@ use crate::auth::authentik::CLIENT; use beam_lib::reqwest::{self, Response, StatusCode}; +use reqwest::Client; use serde_json::{json, Value}; use shared::OIDCConfig; use super::AuthentikConfig; -pub fn generate_app_values( - provider: &str, - name: &str, - oidc_client_config: &OIDCConfig, -) -> Value { +pub fn generate_app_values(provider: &str, name: &str, oidc_client_config: &OIDCConfig) -> Value { let id = format!( "{name}-{}", if oidc_client_config.is_public { @@ -33,15 +30,12 @@ pub async fn generate_application( oidc_client_config: &OIDCConfig, conf: &AuthentikConfig, token: &str, + client: &Client, ) -> reqwest::Result { - CLIENT + client .post(&format!("{}/api/v3/core/applications/", conf.authentik_url)) .bearer_auth(token) - .json(&generate_app_values( - provider, - name, - oidc_client_config, - )) + .json(&generate_app_values(provider, name, oidc_client_config)) .send() .await } @@ -51,16 +45,21 @@ pub async fn check_app_result( name: &str, oidc_client_config: &OIDCConfig, conf: &AuthentikConfig, + client: &Client, ) -> anyhow::Result { - let res = generate_application(name, name, oidc_client_config, conf, token).await?; + let res = generate_application(name, name, oidc_client_config, conf, token, client).await?; match res.status() { StatusCode::OK => { - println!("Application for {name} created."); - return Ok(true) + println!("Application for {name} created."); + return Ok(true); } StatusCode::CONFLICT => { - let conflicting_client = get_application(name, token, oidc_client_config, conf).await?; - if app_configs_match(&conflicting_client, &generate_app_values(name, name, oidc_client_config)) { + let conflicting_client = + get_application(name, token, oidc_client_config, conf, client).await?; + if app_configs_match( + &conflicting_client, + &generate_app_values(name, name, oidc_client_config), + ) { Ok(true) } else { Ok(CLIENT @@ -82,15 +81,14 @@ pub async fn check_app_result( } s => anyhow::bail!("Unexpected statuscode {s} while creating keycloak client. {res:?}"), } - } - pub async fn get_application( name: &str, token: &str, oidc_client_config: &OIDCConfig, conf: &AuthentikConfig, + client: &Client, ) -> reqwest::Result { let id = format!( "{name}-{}", @@ -100,7 +98,7 @@ pub async fn get_application( "private" } ); - CLIENT + client .get(&format!( "{}/api/v3/core/applications/{id}/", conf.authentik_url @@ -117,11 +115,14 @@ pub async fn compare_applications( name: &str, oidc_client_config: &OIDCConfig, conf: &AuthentikConfig, + client: &Client, ) -> anyhow::Result { - let client = get_application(name, token, oidc_client_config, conf).await?; + let client = get_application(name, token, oidc_client_config, conf, client).await?; let wanted_client = generate_app_values(name, name, oidc_client_config); - Ok(client.get("client_secret") == wanted_client.get("client_ secret") - && app_configs_match(&client, &wanted_client)) + Ok( + client.get("client_secret") == wanted_client.get("client_ secret") + && app_configs_match(&client, &wanted_client), + ) } pub fn app_configs_match(a: &Value, b: &Value) -> bool { @@ -140,4 +141,5 @@ pub fn app_configs_match(a: &Value, b: &Value) -> bool { && includes_other_json_array("property_mappings", &|a_v, v| { a_v.iter().any(|a_v| a_v.get("name") == v.get("name")) }) -} \ No newline at end of file +} + diff --git a/central/src/auth/authentik/group.rs b/central/src/auth/authentik/group.rs index 22d6598..7b797f9 100644 --- a/central/src/auth/authentik/group.rs +++ b/central/src/auth/authentik/group.rs @@ -1,28 +1,38 @@ -use crate::CLIENT; use beam_lib::reqwest::{self, StatusCode, Url}; +use reqwest::Client; use serde_json::json; use super::AuthentikConfig; - -pub async fn create_groups(name: &str, token: &str, conf: &AuthentikConfig) -> anyhow::Result<()> { +pub async fn create_groups( + name: &str, + token: &str, + conf: &AuthentikConfig, + CLIENT: &Client, +) -> anyhow::Result<()> { let capitalize = |s: &str| { let mut chrs = s.chars(); - chrs.next().map(char::to_uppercase).map(Iterator::collect).unwrap_or(String::new()) + chrs.as_str() + chrs.next() + .map(char::to_uppercase) + .map(Iterator::collect) + .unwrap_or(String::new()) + + chrs.as_str() }; let name = capitalize(name); for group in &conf.authentik_groups_per_bh { - post_group(&group.replace('#', &name), token, conf).await?; + post_group(&group.replace('#', &name), token, conf, CLIENT).await?; } Ok(()) } -pub async fn post_group(name: &str, token: &str, conf: &AuthentikConfig) -> anyhow::Result<()> { +pub async fn post_group( + name: &str, + token: &str, + conf: &AuthentikConfig, + CLIENT: &Client, +) -> anyhow::Result<()> { let res = CLIENT - .post(&format!( - "{}/api/v3/core/groups/", - conf.authentik_url - )) + .post(&format!("{}/api/v3/core/groups/", conf.authentik_url)) .bearer_auth(token) .json(&json!({ "name": name @@ -33,7 +43,7 @@ pub async fn post_group(name: &str, token: &str, conf: &AuthentikConfig) -> anyh StatusCode::CREATED => println!("Created group {name}"), StatusCode::OK => println!("Created group {name}"), StatusCode::CONFLICT => println!("Group {name} already existed"), - s => anyhow::bail!("Unexpected statuscode {s} while creating group {name}") + s => anyhow::bail!("Unexpected statuscode {s} while creating group {name}"), } Ok(()) } diff --git a/central/src/auth/authentik/mod.rs b/central/src/auth/authentik/mod.rs index 866ea37..825709a 100644 --- a/central/src/auth/authentik/mod.rs +++ b/central/src/auth/authentik/mod.rs @@ -1,18 +1,21 @@ -mod test; -mod group; mod app; +mod group; mod provider; +mod test; use std::{collections::HashMap, sync::Mutex}; -use crate::CLIENT; +use crate::{get_beamclient, CLIENT}; use anyhow::bail; -use app::{app_configs_match, check_app_result, compare_applications, generate_app_values, generate_application, get_application}; +use app::{ + app_configs_match, check_app_result, compare_applications, generate_app_values, + generate_application, get_application, +}; use beam_lib::reqwest::{self, Url}; use clap::{builder::Str, Parser}; use group::create_groups; use provider::{generate_provider_values, get_provider, provider_configs_match}; -use reqwest::StatusCode; +use reqwest::{Client, StatusCode}; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use shared::{OIDCConfig, SecretResult}; @@ -47,29 +50,32 @@ impl FlowPropertymapping { "roles", "email", "microprofile-jwt", - "groups" + "groups", ]; let flow_url = "/api/v3/flows/instances/?ordering=slug&page=1&page_size=20&search="; let property_url = "/api/v3/propertymappings/all/?managed__isnull=true&ordering=name&page=1&page_size=20&search="; - let property_mapping = get_property_mappings_uuids(property_url, conf, token, property_keys).await; - let authorization_flow = get_uuid(flow_url, conf, token, flow_key).await.expect("No default flow present"); // flow uuid - let mapping = FlowPropertymapping{ + let property_mapping = + get_property_mappings_uuids(property_url, conf, token, property_keys).await; + let authorization_flow = get_uuid(flow_url, conf, token, flow_key, &get_beamclient()) + .await + .expect("No default flow present"); // flow uuid + let mapping = FlowPropertymapping { authorization_flow, - property_mapping + property_mapping, }; *PROPERTY_MAPPING_CACHE.lock().unwrap() = Some(mapping.clone()); Ok(mapping) } } - pub async fn validate_application( name: &str, oidc_client_config: &OIDCConfig, conf: &AuthentikConfig, + client: &Client, ) -> anyhow::Result { let token = get_access_token(conf).await?; - compare_applications(&token, name, oidc_client_config, conf).await + compare_applications(&token, name, oidc_client_config, conf, client).await } pub async fn create_app_provider( @@ -78,7 +84,7 @@ pub async fn create_app_provider( conf: &AuthentikConfig, ) -> anyhow::Result { let token = get_access_token(conf).await?; - combine_app_provider(&token, name, &oidc_client_config, conf).await + combine_app_provider(&token, name, &oidc_client_config, conf, &get_beamclient()).await } pub async fn combine_app_provider( @@ -86,43 +92,43 @@ pub async fn combine_app_provider( name: &str, oidc_client_config: &OIDCConfig, conf: &AuthentikConfig, + client: &Client, ) -> anyhow::Result { let secret = if !oidc_client_config.is_public { generate_secret() } else { String::with_capacity(0) }; - let generated_provider = generate_provider_values(name, oidc_client_config, &secret, conf, token) + let generated_provider = + generate_provider_values(name, oidc_client_config, &secret, conf, token).await?; + let provider_res = client + .post(&format!("{}/api/v3/providers/oauth2/", conf.authentik_url)) + .bearer_auth(token) + .json(&generated_provider) + .send() .await?; - let provider_res = CLIENT - .post(&format!( - "{}/api/v3/providers/oauth2/", - conf.authentik_url - )) - .bearer_auth(token) - .json(&generated_provider) - .send() - .await?; // Create groups for this client - create_groups(name, token, conf).await?; + create_groups(name, token, conf, client).await?; match provider_res.status() { StatusCode::CREATED => { println!("Client for {name} created."); - check_app_result(token, name, oidc_client_config, conf).await?; + check_app_result(token, name, oidc_client_config, conf, client).await?; Ok(SecretResult::Created(secret)) } StatusCode::CONFLICT => { let conflicting_provider = get_provider(name, token, oidc_client_config, conf).await?; if provider_configs_match(&conflicting_provider, &generated_provider) { - check_app_result(token, name, oidc_client_config, conf).await?; - Ok(SecretResult::AlreadyExisted(conflicting_provider - .as_object() - .and_then(|o| o.get("client_secret")) - .and_then(Value::as_str) - .unwrap_or("") - .to_owned())) + check_app_result(token, name, oidc_client_config, conf, client).await?; + Ok(SecretResult::AlreadyExisted( + conflicting_provider + .as_object() + .and_then(|o| o.get("client_secret")) + .and_then(Value::as_str) + .unwrap_or("") + .to_owned(), + )) } else { - Ok(CLIENT + Ok(client .put(&format!( "{}/api/v3/providers/oauth2/{}", conf.authentik_url, @@ -145,44 +151,52 @@ pub async fn combine_app_provider( } } - -async fn get_uuid(target_url: &str, conf: &AuthentikConfig, token: &str, search_key: &str) -> Option { +async fn get_uuid( + target_url: &str, + conf: &AuthentikConfig, + token: &str, + search_key: &str, + client: &Client, +) -> Option { println!("{:?}", search_key); - let target_value: serde_json::Value = CLIENT - .get(&format!( - "{}{}{}", - conf.authentik_url, - target_url, - search_key - )) - .bearer_auth(token) - .send() - .await - .ok()? - .json() - .await - .ok()?; + let target_value: serde_json::Value = client + .get(&format!( + "{}{}{}", + conf.authentik_url, target_url, search_key + )) + .bearer_auth(token) + .send() + .await + .ok()? + .json() + .await + .ok()?; // pk is the uuid for this result - target_value - .as_object() - .and_then(|o| { - o.get("results") - }) - .and_then(Value::as_array) - .and_then(|a| { - a.get(0) - }) - .and_then(|o| o.as_object()) - .and_then(|o| o.get("pk")) - .and_then(Value::as_str) - .map(|s| s.to_string()) - - } + target_value + .as_object() + .and_then(|o| o.get("results")) + .and_then(Value::as_array) + .and_then(|a| a.get(0)) + .and_then(|o| o.as_object()) + .and_then(|o| o.get("pk")) + .and_then(Value::as_str) + .map(|s| s.to_string()) +} -async fn get_property_mappings_uuids(target_url: &str, conf: &AuthentikConfig, token: &str, search_key: Vec<&str>) -> HashMap { +async fn get_property_mappings_uuids( + target_url: &str, + conf: &AuthentikConfig, + token: &str, + search_key: Vec<&str>, +) -> HashMap { let mut result: HashMap = HashMap::new(); for key in search_key { - result.insert(key.to_string(), get_uuid(target_url, conf, token, key).await.expect(&format!("Property: {:?}", key))); + result.insert( + key.to_string(), + get_uuid(target_url, conf, token, key, &get_beamclient()) + .await + .expect(&format!("Property: {:?}", key)), + ); } result } @@ -209,10 +223,7 @@ async fn get_access_token(conf: &AuthentikConfig) -> reqwest::Result { access_token: String, } CLIENT - .post(&format!( - "{}/application/o/token/", - conf.authentik_url - )) + .post(&format!("{}/application/o/token/", conf.authentik_url)) .form(&json!({ "grant_type": "client_credentials", "client_id": conf.authentik_id, diff --git a/central/src/auth/authentik/provider.rs b/central/src/auth/authentik/provider.rs index e84487c..b0cd86d 100644 --- a/central/src/auth/authentik/provider.rs +++ b/central/src/auth/authentik/provider.rs @@ -3,18 +3,29 @@ use reqwest::{Response, StatusCode}; use serde_json::{json, Value}; use shared::{OIDCConfig, SecretResult}; -use crate::{auth::config::FlowPropertymapping, CLIENT}; +use crate::{auth::config::FlowPropertymapping, get_beamclient, CLIENT}; use super::{get_uuid, AuthentikConfig}; - - -pub async fn generate_provider_values(name: &str, oidc_client_config: &OIDCConfig, secret: &str, conf: &AuthentikConfig, token: &str) -> anyhow::Result { +pub async fn generate_provider_values( + name: &str, + oidc_client_config: &OIDCConfig, + secret: &str, + conf: &AuthentikConfig, + token: &str, +) -> anyhow::Result { let mapping = FlowPropertymapping::new(conf, token).await?; - + let secret = (!oidc_client_config.is_public).then_some(secret); - let id = format!("{name}-{}", if oidc_client_config.is_public { "public" } else { "private" }); - let mut json = json!({ + let id = format!( + "{name}-{}", + if oidc_client_config.is_public { + "public" + } else { + "private" + } + ); + let mut json = json!({ "name": id, "client_id": id, "authorization_flow": mapping.authorization_flow, @@ -29,14 +40,20 @@ pub async fn generate_provider_values(name: &str, oidc_client_config: &OIDCConfi ], "redirect_uris": oidc_client_config.redirect_urls, }); - + if oidc_client_config.is_public { - json.as_object_mut().unwrap().insert("client_type".to_owned(), "public".into()); + json.as_object_mut() + .unwrap() + .insert("client_type".to_owned(), "public".into()); } else { - json.as_object_mut().unwrap().insert("client_type".to_owned(), "confidential".into()); + json.as_object_mut() + .unwrap() + .insert("client_type".to_owned(), "confidential".into()); } if let Some(secret) = secret { - json.as_object_mut().unwrap().insert("client_secret".to_owned(), secret.into()); + json.as_object_mut() + .unwrap() + .insert("client_secret".to_owned(), secret.into()); } Ok(json) } @@ -47,9 +64,18 @@ pub async fn get_provider( oidc_client_config: &OIDCConfig, conf: &AuthentikConfig, ) -> reqwest::Result { - let id = format!("{name}-{}", if oidc_client_config.is_public { "public" } else { "private" }); + let id = format!( + "{name}-{}", + if oidc_client_config.is_public { + "public" + } else { + "private" + } + ); let provider_url = "/api/v3/providers/all/?ordering=name&page=1&page_size=20&search="; - let pk = get_uuid(&provider_url, conf, token, &id).await.expect(&format!("Property: {:?}", id)); + let pk = get_uuid(&provider_url, conf, token, &id, &get_beamclient()) + .await + .expect(&format!("Property: {:?}", id)); CLIENT .get(&format!( "{}/api/v3/providers/oauth2/{pk}/", @@ -70,22 +96,29 @@ pub async fn compare_provider( secret: &str, ) -> anyhow::Result { let client = get_provider(name, token, oidc_client_config, conf).await?; - let wanted_client = generate_provider_values(name, oidc_client_config, secret, conf, token).await?; - Ok(client.get("client_secret") == wanted_client.get("client_secret") - && provider_configs_match(&client, &wanted_client)) + let wanted_client = + generate_provider_values(name, oidc_client_config, secret, conf, token).await?; + Ok( + client.get("client_secret") == wanted_client.get("client_secret") + && provider_configs_match(&client, &wanted_client), + ) } pub fn provider_configs_match(a: &Value, b: &Value) -> bool { - let includes_other_json_array = |key, comparator: &dyn Fn(_, _) -> bool| a - .get(key) - .and_then(Value::as_array) - .is_some_and(|a_values| b - .get(key) + let includes_other_json_array = |key, comparator: &dyn Fn(_, _) -> bool| { + a.get(key) .and_then(Value::as_array) - .is_some_and(|vec| vec.iter().all(|v| comparator(a_values, v))) - ); + .is_some_and(|a_values| { + b.get(key) + .and_then(Value::as_array) + .is_some_and(|vec| vec.iter().all(|v| comparator(a_values, v))) + }) + }; a.get("name") == b.get("name") && includes_other_json_array("authorization_flow", &|a_v, v| a_v.contains(v)) && includes_other_json_array("redirectUris", &|a_v, v| a_v.contains(v)) - && includes_other_json_array("property_mappings", &|a_v, v| a_v.iter().any(|a_v| a_v.get("name") == v.get("name"))) -} \ No newline at end of file + && includes_other_json_array("property_mappings", &|a_v, v| { + a_v.iter().any(|a_v| a_v.get("name") == v.get("name")) + }) +} + diff --git a/central/src/auth/authentik/test.rs b/central/src/auth/authentik/test.rs index 5418b81..983d77e 100644 --- a/central/src/auth/authentik/test.rs +++ b/central/src/auth/authentik/test.rs @@ -4,7 +4,7 @@ use crate::auth::authentik::{ app_configs_match, combine_app_provider, compare_applications, get_application, get_uuid, AuthentikConfig, }; -use crate::CLIENT; +use crate::{get_beamclient, CLIENT}; use beam_lib::reqwest::{self, Error, StatusCode, Url}; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -16,10 +16,10 @@ struct Token { } #[cfg(test)] -async fn setup_authentik() -> reqwest::Result<(String, AuthentikConfig)> { +pub async fn setup_authentik() -> reqwest::Result<(String, AuthentikConfig)> { //let token = get_access_token_via_admin_login().await?; let token = Token { - access_token: "NXChuwYcHBf4ggcg1VdWdwKaEgqNxwl07nWLSnBsAK27YHNTdi0z45K6Ioun".to_owned(), + access_token: "sgihMevO0egohcQWPiT3bGnvr7iSQaBVc5IDhnIriHg2s40hgOroj45MxkED".to_owned(), }; Ok(( token.access_token, @@ -83,7 +83,7 @@ async fn get_access_token_via_admin_login() -> reqwest::Result { .map(|t| t.access_token) } -//#[ignore = "Requires setting up a authentik"] +// #[ignore = "Requires setting up a authentik"] #[tokio::test] async fn test_create_client() -> anyhow::Result<()> { let (token, conf) = setup_authentik().await?; @@ -94,19 +94,21 @@ async fn test_create_client() -> anyhow::Result<()> { redirect_urls: vec!["http://foo/bar".into()], }; let (SecretResult::Created(pw) | SecretResult::AlreadyExisted(pw)) = - dbg!(combine_app_provider(&token, name, &client_config, &conf).await?) + dbg!(combine_app_provider(&token, name, &client_config, &conf, &get_beamclient()).await?) else { panic!("Not created or existed") }; - let c = dbg!(get_application(name, &token, &client_config, &conf) - .await - .unwrap()); + let c = dbg!( + get_application(name, &token, &client_config, &conf, &get_beamclient()) + .await + .unwrap() + ); assert!(app_configs_match( &c, &generate_app_values(name, name, &client_config) )); assert!(dbg!( - compare_applications(&token, name, &client_config, &conf).await? + compare_applications(&token, name, &client_config, &conf, &get_beamclient()).await? )); // private client @@ -115,19 +117,21 @@ async fn test_create_client() -> anyhow::Result<()> { redirect_urls: vec!["http://foo/bar".into()], }; let (SecretResult::Created(pw) | SecretResult::AlreadyExisted(pw)) = - dbg!(combine_app_provider(&token, name, &client_config, &conf).await?) + dbg!(combine_app_provider(&token, name, &client_config, &conf, &get_beamclient()).await?) else { panic!("Not created or existed") }; - let c = dbg!(get_application(name, &token, &client_config, &conf) - .await - .unwrap()); + let c = dbg!( + get_application(name, &token, &client_config, &conf, &get_beamclient()) + .await + .unwrap() + ); assert!(app_configs_match( &c, &generate_app_values(name, name, &client_config) )); assert!(dbg!( - compare_applications(&token, name, &client_config, &conf).await? + compare_applications(&token, name, &client_config, &conf, &get_beamclient()).await? )); Ok(()) @@ -137,7 +141,7 @@ async fn test_create_client() -> anyhow::Result<()> { #[tokio::test] async fn group_test() -> anyhow::Result<()> { let (token, conf) = setup_authentik().await?; - create_groups("team", &token, &conf).await + create_groups("uuuu", &token, &conf, &get_beamclient()).await } // #[ignore = "Requires setting up a authentik"] @@ -148,8 +152,17 @@ async fn test_flow() { .expect("Cannot setup authentik as test"); let test_key = "authorization_flow"; let flow_url = "/api/v3/flows/instances/?ordering=slug&page=1&page_size=20&search="; - let res = get_uuid(flow_url, &conf, &token, test_key).await; - dbg!(res); + let res = get_uuid(flow_url, &conf, &token, test_key, &get_beamclient()).await; + dbg!(&res); + match res { + Some(uuid) => { + println!("Found: {}", uuid); + assert!(!uuid.is_empty(), "empty"); + } + None => { + panic!("Expected {}", test_key); + } + } } // #[ignore = "Requires setting up a authentik"] @@ -160,11 +173,11 @@ async fn test_property() { .expect("Cannot setup authentik as test"); let test_key = "web-origins"; let flow_url = "/api/v3/propertymappings/all/?managed__isnull=true&ordering=name&page=1&page_size=20&search=groups"; - let res = get_uuid(flow_url, &conf, &token, test_key).await; + let res = get_uuid(flow_url, &conf, &token, test_key, &get_beamclient()).await; dbg!(res); } -//#[ignore = "Requires setting up a authentik"] +// #[ignore = "Requires setting up a authentik"] #[tokio::test] async fn create_property() { let (token, conf) = setup_authentik() @@ -177,7 +190,7 @@ async fn create_property() { "expression": ext }); let propperty_url = "/api/v3/propertymappings/source/oauth/"; - let res = CLIENT + let res = get_beamclient() .post(&format!("{}{}", conf.authentik_url, propperty_url)) .bearer_auth(token) .json(&json_property) diff --git a/central/src/auth/config.rs b/central/src/auth/config.rs index 56a37fe..5348a02 100644 --- a/central/src/auth/config.rs +++ b/central/src/auth/config.rs @@ -1,11 +1,14 @@ use std::{collections::HashMap, convert::Infallible, net::SocketAddr}; -use beam_lib::{AppId, reqwest::Url}; +use beam_lib::{reqwest::Url, AppId}; use clap::Parser; use serde::{Deserialize, Serialize}; -use shared::{SecretResult, OIDCConfig}; +use shared::{OIDCConfig, SecretResult}; -use crate::auth::keycloak::{KeyCloakConfig, self}; +use crate::{ + auth::keycloak::{self, KeyCloakConfig}, + get_beamclient, +}; use super::authentik::{self, AuthentikConfig}; @@ -27,13 +30,12 @@ pub struct Config { /// The app id of this application #[clap(long, env, value_parser=|id: &str| Ok::<_, Infallible>(AppId::new_unchecked(id)))] pub beam_id: AppId, - } #[derive(Clone, Debug)] pub enum OIDCProvider { Keycloak(KeyCloakConfig), - Authentik(AuthentikConfig) + Authentik(AuthentikConfig), } impl OIDCProvider { @@ -48,17 +50,31 @@ impl OIDCProvider { } } - pub async fn create_client(&self, name: &str, oidc_client_config: OIDCConfig) -> Result { + pub async fn create_client( + &self, + name: &str, + oidc_client_config: OIDCConfig, + ) -> Result { match self { - OIDCProvider::Keycloak(conf) => keycloak::create_client(name, oidc_client_config, conf).await, - OIDCProvider::Authentik(conf) => authentik::create_app_provider(name, oidc_client_config, conf).await - }.map_err(|e| { + OIDCProvider::Keycloak(conf) => { + keycloak::create_client(name, oidc_client_config, conf).await + } + OIDCProvider::Authentik(conf) => { + authentik::create_app_provider(name, oidc_client_config, conf).await + } + } + .map_err(|e| { println!("Failed to create client: {e}"); "Error creating OIDC client".into() }) } - pub async fn validate_client(&self, name: &str, secret: &str, oidc_client_config: &OIDCConfig) -> Result { + pub async fn validate_client( + &self, + name: &str, + secret: &str, + oidc_client_config: &OIDCConfig, + ) -> Result { match self { OIDCProvider::Keycloak(conf) => { keycloak::validate_client(name, oidc_client_config, secret, conf) @@ -67,9 +83,9 @@ impl OIDCProvider { eprintln!("Failed to validate client {name}: {e}"); "Failed to validate client. See upstrean logs.".into() }) - }, + } OIDCProvider::Authentik(conf) => { - authentik::validate_application(name, oidc_client_config, conf) + authentik::validate_application(name, oidc_client_config, conf, &get_beamclient()) .await .map_err(|e| { eprintln!("Failed to validate client {name}: {e}"); @@ -83,6 +99,5 @@ impl OIDCProvider { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct FlowPropertymapping { pub authorization_flow: String, - pub property_mapping: HashMap + pub property_mapping: HashMap, } - diff --git a/central/src/main.rs b/central/src/main.rs index 736bfb1..881e046 100644 --- a/central/src/main.rs +++ b/central/src/main.rs @@ -1,10 +1,10 @@ use std::{collections::HashSet, time::Duration}; -use beam_lib::{reqwest::Client, BeamClient, BlockingOptions, TaskRequest, TaskResult, AppId}; -use clap::Parser; use auth::config::{Config, FlowPropertymapping, OIDCProvider}; +use beam_lib::{reqwest::Client, AppId, BeamClient, BlockingOptions, TaskRequest, TaskResult}; +use clap::Parser; use once_cell::sync::Lazy; -use shared::{SecretRequest, SecretResult, SecretRequestType}; +use shared::{SecretRequest, SecretRequestType, SecretResult}; mod auth; @@ -18,6 +18,10 @@ pub static BEAM_CLIENT: Lazy = Lazy::new(|| { ) }); +pub fn get_beamclient() -> Client { + return Client::new(); +} + pub static OIDC_PROVIDER: Lazy> = Lazy::new(OIDCProvider::try_init); pub static CLIENT: Lazy = Lazy::new(Client::new); @@ -53,31 +57,42 @@ async fn main() { pub async fn handle_task(task: TaskRequest>) { let from = task.from; - let results = futures::future::join_all(task.body.into_iter().map(|t| handle_secret_task(t, &from))).await; - let result = BEAM_CLIENT.put_result( - &TaskResult { - from: CONFIG.beam_id.clone(), - to: vec![from], - task: task.id, - status: beam_lib::WorkStatus::Succeeded, - body: results, - metadata: ().try_into().unwrap(), - }, - &task.id - ).await; + let results = + futures::future::join_all(task.body.into_iter().map(|t| handle_secret_task(t, &from))) + .await; + let result = BEAM_CLIENT + .put_result( + &TaskResult { + from: CONFIG.beam_id.clone(), + to: vec![from], + task: task.id, + status: beam_lib::WorkStatus::Succeeded, + body: results, + metadata: ().try_into().unwrap(), + }, + &task.id, + ) + .await; if let Err(e) = result { eprintln!("Failed to respond to task: {e}") } } -pub async fn handle_secret_task(task: SecretRequestType, from: &AppId) -> Result { +pub async fn handle_secret_task( + task: SecretRequestType, + from: &AppId, +) -> Result { let name = from.as_ref().split('.').nth(1).unwrap(); println!("Working on secret task {task:?} from {from}"); match task { - SecretRequestType::ValidateOrCreate { current, request } if is_valid(¤t, &request, name).await? => Ok(SecretResult::AlreadyValid), - SecretRequestType::ValidateOrCreate { request, .. } | - SecretRequestType::Create(request) => create_secret(request, name).await, + SecretRequestType::ValidateOrCreate { current, request } + if is_valid(¤t, &request, name).await? => + { + Ok(SecretResult::AlreadyValid) + } + SecretRequestType::ValidateOrCreate { request, .. } + | SecretRequestType::Create(request) => create_secret(request, name).await, } } @@ -98,7 +113,9 @@ pub async fn is_valid(secret: &str, request: &SecretRequest, name: &str) -> Resu let Some(oidc_provider) = OIDC_PROVIDER.as_ref() else { return Err("No OIDC provider configured!".into()); }; - oidc_provider.validate_client(name, secret, oidc_client_config).await - }, + oidc_provider + .validate_client(name, secret, oidc_client_config) + .await + } } } From 3d40bc89263131d2f3f72b5f4c5c6f7fb8117a26 Mon Sep 17 00:00:00 2001 From: Martin Jurk Date: Wed, 13 Nov 2024 09:56:52 +0100 Subject: [PATCH 18/37] test no creation --- central/Cargo.toml | 6 ++- central/src/auth/authentik/app.rs | 3 +- central/src/auth/authentik/mod.rs | 11 +++--- central/src/auth/authentik/provider.rs | 12 +++--- central/src/auth/authentik/test.rs | 8 ++-- dev/authentik.yaml | 54 ++++++++++++++++++++++++++ 6 files changed, 76 insertions(+), 18 deletions(-) create mode 100644 dev/authentik.yaml diff --git a/central/Cargo.toml b/central/Cargo.toml index 2a6b0ed..90bad47 100644 --- a/central/Cargo.toml +++ b/central/Cargo.toml @@ -7,12 +7,14 @@ license = "Apache-2.0" [dependencies] beam-lib = { workspace = true } clap = { workspace = true } -tokio = { workspace = true, features= ["signal"] } +tokio = { workspace = true, features = ["signal"] } once_cell = { workspace = true } shared = { workspace = true } serde = { workspace = true } futures = { workspace = true } serde_json = "1" rand = "0.8" -reqwest = { version = "0.12", default_features = false, features = ["default-tls"] } +reqwest = { version = "0.12", default_features = false, features = [ + "default-tls", +] } anyhow = "1.0.91" diff --git a/central/src/auth/authentik/app.rs b/central/src/auth/authentik/app.rs index e7686a0..e748639 100644 --- a/central/src/auth/authentik/app.rs +++ b/central/src/auth/authentik/app.rs @@ -1,4 +1,3 @@ -use crate::auth::authentik::CLIENT; use beam_lib::reqwest::{self, Response, StatusCode}; use reqwest::Client; use serde_json::{json, Value}; @@ -62,7 +61,7 @@ pub async fn check_app_result( ) { Ok(true) } else { - Ok(CLIENT + Ok(client .put(&format!( "{}/api/v3/core/applicaions/{}", conf.authentik_url, diff --git a/central/src/auth/authentik/mod.rs b/central/src/auth/authentik/mod.rs index 825709a..a75d671 100644 --- a/central/src/auth/authentik/mod.rs +++ b/central/src/auth/authentik/mod.rs @@ -5,7 +5,7 @@ mod test; use std::{collections::HashMap, sync::Mutex}; -use crate::{get_beamclient, CLIENT}; +use crate::get_beamclient; use anyhow::bail; use app::{ app_configs_match, check_app_result, compare_applications, generate_app_values, @@ -110,13 +110,14 @@ pub async fn combine_app_provider( // Create groups for this client create_groups(name, token, conf, client).await?; match provider_res.status() { - StatusCode::CREATED => { + StatusCode::OK => { println!("Client for {name} created."); check_app_result(token, name, oidc_client_config, conf, client).await?; Ok(SecretResult::Created(secret)) } StatusCode::CONFLICT => { - let conflicting_provider = get_provider(name, token, oidc_client_config, conf).await?; + let conflicting_provider = + get_provider(name, token, oidc_client_config, conf, client).await?; if provider_configs_match(&conflicting_provider, &generated_provider) { check_app_result(token, name, oidc_client_config, conf, client).await?; Ok(SecretResult::AlreadyExisted( @@ -147,7 +148,7 @@ pub async fn combine_app_provider( .expect("We know the provider already exists so updating should be successful")) } } - s => bail!("Unexpected statuscode {s} while creating keycloak client. {provider_res:?}"), + s => bail!("Unexpected statuscode {s} while creating authentik client. {provider_res:?}"), } } @@ -222,7 +223,7 @@ async fn get_access_token(conf: &AuthentikConfig) -> reqwest::Result { struct Token { access_token: String, } - CLIENT + get_beamclient() .post(&format!("{}/application/o/token/", conf.authentik_url)) .form(&json!({ "grant_type": "client_credentials", diff --git a/central/src/auth/authentik/provider.rs b/central/src/auth/authentik/provider.rs index b0cd86d..8fd0aad 100644 --- a/central/src/auth/authentik/provider.rs +++ b/central/src/auth/authentik/provider.rs @@ -1,9 +1,9 @@ use anyhow::Ok; -use reqwest::{Response, StatusCode}; +use reqwest::{Client, Response, StatusCode}; use serde_json::{json, Value}; use shared::{OIDCConfig, SecretResult}; -use crate::{auth::config::FlowPropertymapping, get_beamclient, CLIENT}; +use crate::{auth::config::FlowPropertymapping, get_beamclient}; use super::{get_uuid, AuthentikConfig}; @@ -63,6 +63,7 @@ pub async fn get_provider( token: &str, oidc_client_config: &OIDCConfig, conf: &AuthentikConfig, + client: &Client, ) -> reqwest::Result { let id = format!( "{name}-{}", @@ -73,10 +74,10 @@ pub async fn get_provider( } ); let provider_url = "/api/v3/providers/all/?ordering=name&page=1&page_size=20&search="; - let pk = get_uuid(&provider_url, conf, token, &id, &get_beamclient()) + let pk = get_uuid(&provider_url, conf, token, &id, client) .await .expect(&format!("Property: {:?}", id)); - CLIENT + client .get(&format!( "{}/api/v3/providers/oauth2/{pk}/", conf.authentik_url @@ -94,8 +95,9 @@ pub async fn compare_provider( oidc_client_config: &OIDCConfig, conf: &AuthentikConfig, secret: &str, + client: &Client, ) -> anyhow::Result { - let client = get_provider(name, token, oidc_client_config, conf).await?; + let client = get_provider(name, token, oidc_client_config, conf, client).await?; let wanted_client = generate_provider_values(name, oidc_client_config, secret, conf, token).await?; Ok( diff --git a/central/src/auth/authentik/test.rs b/central/src/auth/authentik/test.rs index 983d77e..d87d35b 100644 --- a/central/src/auth/authentik/test.rs +++ b/central/src/auth/authentik/test.rs @@ -19,7 +19,7 @@ struct Token { pub async fn setup_authentik() -> reqwest::Result<(String, AuthentikConfig)> { //let token = get_access_token_via_admin_login().await?; let token = Token { - access_token: "sgihMevO0egohcQWPiT3bGnvr7iSQaBVc5IDhnIriHg2s40hgOroj45MxkED".to_owned(), + access_token: "WYKYEMRAJF0y2hBuBZfP1sDvEE7rBkoYFodsCXIuXK0SR3RukJyYfPHrnEfk".to_owned(), }; Ok(( token.access_token, @@ -87,7 +87,7 @@ async fn get_access_token_via_admin_login() -> reqwest::Result { #[tokio::test] async fn test_create_client() -> anyhow::Result<()> { let (token, conf) = setup_authentik().await?; - let name = "office"; + let name = "window"; // public client let client_config = OIDCConfig { is_public: true, @@ -141,7 +141,7 @@ async fn test_create_client() -> anyhow::Result<()> { #[tokio::test] async fn group_test() -> anyhow::Result<()> { let (token, conf) = setup_authentik().await?; - create_groups("uuuu", &token, &conf, &get_beamclient()).await + create_groups("qqqq", &token, &conf, &get_beamclient()).await } // #[ignore = "Requires setting up a authentik"] @@ -172,7 +172,7 @@ async fn test_property() { .await .expect("Cannot setup authentik as test"); let test_key = "web-origins"; - let flow_url = "/api/v3/propertymappings/all/?managed__isnull=true&ordering=name&page=1&page_size=20&search=groups"; + let flow_url = "/api/v3/propertymappings/all/?managed__isnull=true&ordering=name&page=1&page_size=20&search="; let res = get_uuid(flow_url, &conf, &token, test_key, &get_beamclient()).await; dbg!(res); } diff --git a/dev/authentik.yaml b/dev/authentik.yaml new file mode 100644 index 0000000..cc1553a --- /dev/null +++ b/dev/authentik.yaml @@ -0,0 +1,54 @@ +version: "3" + +services: + local: + build: + context: ../ + dockerfile: Dockerfile.local + image: samply/secret-sync-local:latest + environment: + - PROXY_ID=proxy1.broker + - OIDC_PROVIDER=app2.proxy2.broker + - BROKER_URL=http://broker:8080 + - SECRET_DEFINITIONS=${ARGS} + volumes: + # Path can be configuard via CACHE_PATH this container path is the default + - ${CACHE_PATH}:/usr/local/cache + extra_hosts: + - "broker:${BROKER_IP}" + secrets: + - privkey.pem + - root.crt.pem + + central: + build: + context: ../ + dockerfile: Dockerfile.central + image: samply/secret-sync-central:latest + depends_on: + - authentik + environment: + - BEAM_URL=http://proxy:8082 + - BEAM_ID=app2.proxy2.broker + - BEAM_SECRET=App1Secret + - AUTHENTIK_URL=http://authentik:9000 + - AUTHENTIK_ID=admin + - AUTHENTIK_SECRET=admin + - AUTHENTIK_SERVICE_ACCOUNT_ROLES=query-users + extra_hosts: + - "proxy:${PROXY_IP}" + + authentik: + image: beryju/authentik:latest + command: start-dev + ports: + - "9000:9000" + environment: + - PG_PASS=admin + - AUTHENTIK_SECRET_KEY=admin + +secrets: + privkey.pem: + file: ${BEAM_DIR}/dev/pki/proxy1.priv.pem + root.crt.pem: + file: ${BEAM_DIR}/dev/pki/root.crt.pem From 9186f18a4251092e0e2fdc1b6cd2ed85aecda72f Mon Sep 17 00:00:00 2001 From: Martin Jurk Date: Fri, 15 Nov 2024 09:56:38 +0100 Subject: [PATCH 19/37] test tokio multi thread --- central/src/auth/authentik/app.rs | 1 - central/src/auth/authentik/group.rs | 12 ++++-------- central/src/auth/authentik/test.rs | 25 +++++++++++++------------ dev/docker-compose.yaml | 20 ++++++++++---------- 4 files changed, 27 insertions(+), 31 deletions(-) diff --git a/central/src/auth/authentik/app.rs b/central/src/auth/authentik/app.rs index e748639..a65cb10 100644 --- a/central/src/auth/authentik/app.rs +++ b/central/src/auth/authentik/app.rs @@ -141,4 +141,3 @@ pub fn app_configs_match(a: &Value, b: &Value) -> bool { a_v.iter().any(|a_v| a_v.get("name") == v.get("name")) }) } - diff --git a/central/src/auth/authentik/group.rs b/central/src/auth/authentik/group.rs index 7b797f9..504c3f0 100644 --- a/central/src/auth/authentik/group.rs +++ b/central/src/auth/authentik/group.rs @@ -20,18 +20,14 @@ pub async fn create_groups( }; let name = capitalize(name); for group in &conf.authentik_groups_per_bh { - post_group(&group.replace('#', &name), token, conf, CLIENT).await?; + post_group(&group.replace('#', &name), token, conf).await?; } Ok(()) } -pub async fn post_group( - name: &str, - token: &str, - conf: &AuthentikConfig, - CLIENT: &Client, -) -> anyhow::Result<()> { - let res = CLIENT +pub async fn post_group(name: &str, token: &str, conf: &AuthentikConfig) -> anyhow::Result<()> { + let client = reqwest::Client::builder().build().unwrap(); + let res = client .post(&format!("{}/api/v3/core/groups/", conf.authentik_url)) .bearer_auth(token) .json(&json!({ diff --git a/central/src/auth/authentik/test.rs b/central/src/auth/authentik/test.rs index d87d35b..e4fca66 100644 --- a/central/src/auth/authentik/test.rs +++ b/central/src/auth/authentik/test.rs @@ -1,5 +1,5 @@ use crate::auth::authentik::app::generate_app_values; -use crate::auth::authentik::group::create_groups; +use crate::auth::authentik::group::{create_groups, post_group}; use crate::auth::authentik::{ app_configs_match, combine_app_provider, compare_applications, get_application, get_uuid, AuthentikConfig, @@ -19,7 +19,7 @@ struct Token { pub async fn setup_authentik() -> reqwest::Result<(String, AuthentikConfig)> { //let token = get_access_token_via_admin_login().await?; let token = Token { - access_token: "WYKYEMRAJF0y2hBuBZfP1sDvEE7rBkoYFodsCXIuXK0SR3RukJyYfPHrnEfk".to_owned(), + access_token: "1xkspjuyWAREk6tKAy4Fw7sIwnKCPfZF0zs6VdHTTIRm6yo2EjTyKAMxQMs2".to_owned(), }; Ok(( token.access_token, @@ -83,8 +83,8 @@ async fn get_access_token_via_admin_login() -> reqwest::Result { .map(|t| t.access_token) } -// #[ignore = "Requires setting up a authentik"] -#[tokio::test] +#[ignore = "Requires setting up a authentik"] +#[tokio::test(flavor = "multi_thread")] async fn test_create_client() -> anyhow::Result<()> { let (token, conf) = setup_authentik().await?; let name = "window"; @@ -141,11 +141,12 @@ async fn test_create_client() -> anyhow::Result<()> { #[tokio::test] async fn group_test() -> anyhow::Result<()> { let (token, conf) = setup_authentik().await?; - create_groups("qqqq", &token, &conf, &get_beamclient()).await + post_group("single", &token, &conf).await + //create_groups("next1", &token, &conf, &get_beamclient()).await } -// #[ignore = "Requires setting up a authentik"] -#[tokio::test] +#[ignore = "Requires setting up a authentik"] +#[tokio::test(flavor = "multi_thread")] async fn test_flow() { let (token, conf) = setup_authentik() .await @@ -165,8 +166,8 @@ async fn test_flow() { } } -// #[ignore = "Requires setting up a authentik"] -#[tokio::test] +#[ignore = "Requires setting up a authentik"] +#[tokio::test(flavor = "multi_thread")] async fn test_property() { let (token, conf) = setup_authentik() .await @@ -177,13 +178,13 @@ async fn test_property() { dbg!(res); } -// #[ignore = "Requires setting up a authentik"] -#[tokio::test] +#[ignore = "Requires setting up a authentik"] +#[tokio::test(flavor = "multi_thread")] async fn create_property() { let (token, conf) = setup_authentik() .await .expect("Cannot setup authentik as test"); - let acr = "test".to_owned(); + let acr = "web-origins".to_owned(); let ext = "return{}".to_owned(); let json_property = json!({ "name": acr, diff --git a/dev/docker-compose.yaml b/dev/docker-compose.yaml index cc1553a..118121f 100644 --- a/dev/docker-compose.yaml +++ b/dev/docker-compose.yaml @@ -26,26 +26,26 @@ services: dockerfile: Dockerfile.central image: samply/secret-sync-central:latest depends_on: - - authentik + - keycloak environment: - BEAM_URL=http://proxy:8082 - BEAM_ID=app2.proxy2.broker - BEAM_SECRET=App1Secret - - AUTHENTIK_URL=http://authentik:9000 - - AUTHENTIK_ID=admin - - AUTHENTIK_SECRET=admin - - AUTHENTIK_SERVICE_ACCOUNT_ROLES=query-users + - KEYCLOAK_URL=http://keycloak:8080 + - KEYCLOAK_ID=admin + - KEYCLOAK_SECRET=admin + - KEYCLOAK_SERVICE_ACCOUNT_ROLES=query-users extra_hosts: - "proxy:${PROXY_IP}" - authentik: - image: beryju/authentik:latest + keycloak: + image: quay.io/keycloak/keycloak:latest command: start-dev ports: - - "9000:9000" + - "1337:8080" environment: - - PG_PASS=admin - - AUTHENTIK_SECRET_KEY=admin + - KEYCLOAK_ADMIN=admin + - KEYCLOAK_ADMIN_PASSWORD=admin secrets: privkey.pem: From 0951cbb97b06d872f6a12a4849213e3cdfb2da92 Mon Sep 17 00:00:00 2001 From: Martin Jurk Date: Fri, 15 Nov 2024 11:59:52 +0100 Subject: [PATCH 20/37] authentik docker compose --- central/src/auth/authentik/test.rs | 4 +- dev/.env | 3 + dev/authentik.yaml | 89 ++++++++++++++++++++-- dev/{docker-compose.yaml => keycloak.yaml} | 0 4 files changed, 87 insertions(+), 9 deletions(-) create mode 100644 dev/.env rename dev/{docker-compose.yaml => keycloak.yaml} (100%) diff --git a/central/src/auth/authentik/test.rs b/central/src/auth/authentik/test.rs index e4fca66..3d98b1b 100644 --- a/central/src/auth/authentik/test.rs +++ b/central/src/auth/authentik/test.rs @@ -19,7 +19,7 @@ struct Token { pub async fn setup_authentik() -> reqwest::Result<(String, AuthentikConfig)> { //let token = get_access_token_via_admin_login().await?; let token = Token { - access_token: "1xkspjuyWAREk6tKAy4Fw7sIwnKCPfZF0zs6VdHTTIRm6yo2EjTyKAMxQMs2".to_owned(), + access_token: "161ycDK4e1es7pRPIAnkLH5zmRvj8dglfjXqOSyjic7sTaqqZwE8V5Z9lqEx".to_owned(), }; Ok(( token.access_token, @@ -138,7 +138,7 @@ async fn test_create_client() -> anyhow::Result<()> { } //#[ignore = "Requires setting up a authentik"] -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn group_test() -> anyhow::Result<()> { let (token, conf) = setup_authentik().await?; post_group("single", &token, &conf).await diff --git a/dev/.env b/dev/.env new file mode 100644 index 0000000..b8929b2 --- /dev/null +++ b/dev/.env @@ -0,0 +1,3 @@ +PG_PASS=NoEWSQ1aKMLh9PPk5rGHT4nIXUBYsTkdgoe4eHBDYAL/wIUL +AUTHENTIK_SECRET_KEY=zbkMDG2a4KzYBQyQv2AyBxSFOCffdzYpHvddj4+TWFRFGDSQfZSbvqBOgBaYVWyeUChjWCQqjkt+vScP +AUTHENTIK_LOG_LEVEL=trace diff --git a/dev/authentik.yaml b/dev/authentik.yaml index cc1553a..47eab59 100644 --- a/dev/authentik.yaml +++ b/dev/authentik.yaml @@ -37,18 +37,93 @@ services: - AUTHENTIK_SERVICE_ACCOUNT_ROLES=query-users extra_hosts: - "proxy:${PROXY_IP}" - - authentik: - image: beryju/authentik:latest - command: start-dev + + postgresql: + image: docker.io/library/postgres:16-alpine + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"] + start_period: 20s + interval: 30s + retries: 5 + timeout: 5s + volumes: + - database:/var/lib/postgresql/data + environment: + POSTGRES_PASSWORD: ${PG_PASS:?database password required} + POSTGRES_USER: ${PG_USER:-authentik} + POSTGRES_DB: ${PG_DB:-authentik} + env_file: + - .env + redis: + image: docker.io/library/redis:alpine + command: --save 60 1 --loglevel warning + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "redis-cli ping | grep PONG"] + start_period: 20s + interval: 30s + retries: 5 + timeout: 3s + volumes: + - redis:/data + server: + image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.10.1} + restart: unless-stopped + command: server + environment: + AUTHENTIK_REDIS__HOST: redis + AUTHENTIK_POSTGRESQL__HOST: postgresql + AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik} + AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik} + AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS} + volumes: + - ./media:/media + - ./custom-templates:/templates + env_file: + - .env ports: - - "9000:9000" + - "${COMPOSE_PORT_HTTP:-9000}:9000" + - "${COMPOSE_PORT_HTTPS:-9443}:9443" + depends_on: + - postgresql + - redis + worker: + image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.10.1} + restart: unless-stopped + command: worker environment: - - PG_PASS=admin - - AUTHENTIK_SECRET_KEY=admin + AUTHENTIK_REDIS__HOST: redis + AUTHENTIK_POSTGRESQL__HOST: postgresql + AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik} + AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik} + AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS} + # `user: root` and the docker socket volume are optional. + # See more for the docker socket integration here: + # https://goauthentik.io/docs/outposts/integrations/docker + # Removing `user: root` also prevents the worker from fixing the permissions + # on the mounted folders, so when removing this make sure the folders have the correct UID/GID + # (1000:1000 by default) + user: root + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./media:/media + - ./certs:/certs + - ./custom-templates:/templates + env_file: + - .env + depends_on: + - postgresql + - redis secrets: privkey.pem: file: ${BEAM_DIR}/dev/pki/proxy1.priv.pem root.crt.pem: file: ${BEAM_DIR}/dev/pki/root.crt.pem + +volumes: + database: + driver: local + redis: + driver: local diff --git a/dev/docker-compose.yaml b/dev/keycloak.yaml similarity index 100% rename from dev/docker-compose.yaml rename to dev/keycloak.yaml From dfbb787f4c9c67b1bceb4f20388aca19ca2e7bb2 Mon Sep 17 00:00:00 2001 From: janskiba Date: Wed, 20 Nov 2024 13:44:29 +0000 Subject: [PATCH 21/37] fix: docker setup --- dev/authentik.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dev/authentik.yaml b/dev/authentik.yaml index 47eab59..5e637fb 100644 --- a/dev/authentik.yaml +++ b/dev/authentik.yaml @@ -13,7 +13,7 @@ services: - SECRET_DEFINITIONS=${ARGS} volumes: # Path can be configuard via CACHE_PATH this container path is the default - - ${CACHE_PATH}:/usr/local/cache + - ${CACHE_PATH:-/tmp/asdf}:/usr/local/cache extra_hosts: - "broker:${BROKER_IP}" secrets: @@ -26,7 +26,8 @@ services: dockerfile: Dockerfile.central image: samply/secret-sync-central:latest depends_on: - - authentik + - worker + - server environment: - BEAM_URL=http://proxy:8082 - BEAM_ID=app2.proxy2.broker From 3fdc507e2ea1018940221db3f5fa4fa42c449ece Mon Sep 17 00:00:00 2001 From: janskiba Date: Wed, 20 Nov 2024 13:45:00 +0000 Subject: [PATCH 22/37] fix: authentik group generation url --- central/src/auth/authentik/group.rs | 9 +++++---- central/src/auth/authentik/test.rs | 13 +++++-------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/central/src/auth/authentik/group.rs b/central/src/auth/authentik/group.rs index 504c3f0..76c96b8 100644 --- a/central/src/auth/authentik/group.rs +++ b/central/src/auth/authentik/group.rs @@ -26,9 +26,10 @@ pub async fn create_groups( } pub async fn post_group(name: &str, token: &str, conf: &AuthentikConfig) -> anyhow::Result<()> { - let client = reqwest::Client::builder().build().unwrap(); + let client = reqwest::Client::new(); + let res = client - .post(&format!("{}/api/v3/core/groups/", conf.authentik_url)) + .post(conf.authentik_url.join("api/v3/core/groups/")?) .bearer_auth(token) .json(&json!({ "name": name @@ -38,8 +39,8 @@ pub async fn post_group(name: &str, token: &str, conf: &AuthentikConfig) -> anyh match res.status() { StatusCode::CREATED => println!("Created group {name}"), StatusCode::OK => println!("Created group {name}"), - StatusCode::CONFLICT => println!("Group {name} already existed"), - s => anyhow::bail!("Unexpected statuscode {s} while creating group {name}"), + StatusCode::BAD_REQUEST => println!("Group {name} already existed"), + s => anyhow::bail!("Unexpected statuscode {s} while creating group {name}: {:#?}", res.json::().await.unwrap_or_default()), } Ok(()) } diff --git a/central/src/auth/authentik/test.rs b/central/src/auth/authentik/test.rs index 3d98b1b..9e3e6dc 100644 --- a/central/src/auth/authentik/test.rs +++ b/central/src/auth/authentik/test.rs @@ -16,10 +16,10 @@ struct Token { } #[cfg(test)] -pub async fn setup_authentik() -> reqwest::Result<(String, AuthentikConfig)> { +pub fn setup_authentik() -> reqwest::Result<(String, AuthentikConfig)> { //let token = get_access_token_via_admin_login().await?; let token = Token { - access_token: "161ycDK4e1es7pRPIAnkLH5zmRvj8dglfjXqOSyjic7sTaqqZwE8V5Z9lqEx".to_owned(), + access_token: "ecSepyVwTIQzGO3tPzUuNLl5Rp5KSbs4AhJup1PUMsGD1h1dSUvs3HT3uhgK".to_owned(), }; Ok(( token.access_token, @@ -86,7 +86,7 @@ async fn get_access_token_via_admin_login() -> reqwest::Result { #[ignore = "Requires setting up a authentik"] #[tokio::test(flavor = "multi_thread")] async fn test_create_client() -> anyhow::Result<()> { - let (token, conf) = setup_authentik().await?; + let (token, conf) = setup_authentik()?; let name = "window"; // public client let client_config = OIDCConfig { @@ -138,9 +138,9 @@ async fn test_create_client() -> anyhow::Result<()> { } //#[ignore = "Requires setting up a authentik"] -#[tokio::test(flavor = "multi_thread")] +#[tokio::test] async fn group_test() -> anyhow::Result<()> { - let (token, conf) = setup_authentik().await?; + let (token, conf) = setup_authentik()?; post_group("single", &token, &conf).await //create_groups("next1", &token, &conf, &get_beamclient()).await } @@ -149,7 +149,6 @@ async fn group_test() -> anyhow::Result<()> { #[tokio::test(flavor = "multi_thread")] async fn test_flow() { let (token, conf) = setup_authentik() - .await .expect("Cannot setup authentik as test"); let test_key = "authorization_flow"; let flow_url = "/api/v3/flows/instances/?ordering=slug&page=1&page_size=20&search="; @@ -170,7 +169,6 @@ async fn test_flow() { #[tokio::test(flavor = "multi_thread")] async fn test_property() { let (token, conf) = setup_authentik() - .await .expect("Cannot setup authentik as test"); let test_key = "web-origins"; let flow_url = "/api/v3/propertymappings/all/?managed__isnull=true&ordering=name&page=1&page_size=20&search="; @@ -182,7 +180,6 @@ async fn test_property() { #[tokio::test(flavor = "multi_thread")] async fn create_property() { let (token, conf) = setup_authentik() - .await .expect("Cannot setup authentik as test"); let acr = "web-origins".to_owned(); let ext = "return{}".to_owned(); From b47a778ff0c1270550fbdf9f7adf0e2654743683 Mon Sep 17 00:00:00 2001 From: Martin Jurk Date: Tue, 26 Nov 2024 08:04:50 +0100 Subject: [PATCH 23/37] group, app, provider success created, matches failed --- .cargo/config.toml | 3 + Cargo.toml | 24 ++++-- central/Cargo.toml | 5 +- central/src/auth/authentik/app.rs | 65 ++++++++------ central/src/auth/authentik/group.rs | 27 +++--- central/src/auth/authentik/mod.rs | 95 +++++++++++++-------- central/src/auth/authentik/provider.rs | 42 ++++++---- central/src/auth/authentik/test.rs | 112 ++++++++++++++++--------- central/src/auth/keycloak/client.rs | 61 +++++++++----- central/src/auth/keycloak/mod.rs | 39 +++++---- central/src/auth/keycloak/test.rs | 60 +++++++++---- central/src/auth/mod.rs | 4 +- central/src/main.rs | 48 ++++++++++- dev/docker-compose.yaml | 86 +++++++++++++++++++ local/src/cache.rs | 13 ++- local/src/config.rs | 26 ++++-- local/src/main.rs | 23 +++-- shared/src/lib.rs | 15 ++-- 18 files changed, 536 insertions(+), 212 deletions(-) create mode 100644 .cargo/config.toml create mode 100644 dev/docker-compose.yaml diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..b5db647 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,3 @@ +[env] +AUTHENTIK_TOKEN = "JVMsRhNLQvfyQKXzxwrjdbqY6I5oxm2Db8PbDQ0iUXZS3WhIzKKqPU5Yv2xn" +RUST_LOG = "debug" diff --git a/Cargo.toml b/Cargo.toml index f5fbff1..fe8fd4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,20 +3,28 @@ members = ["local", "central", "shared"] resolver = "2" [workspace.dependencies] -beam-lib = { git = "https://github.com/samply/beam", features = ["http-util"], branch = "develop" } -clap = { version = "4.4", features = ["derive", "env"]} +beam-lib = { git = "https://github.com/samply/beam", features = [ + "http-util", +], branch = "develop" } +clap = { version = "4.4", features = ["derive", "env"] } once_cell = "1" -tokio = { version = "1", default-features = false, features = ["macros", "rt-multi-thread"] } -serde = { version = "1", features = ["derive"]} +tokio = { version = "1", default-features = false, features = [ + "macros", + "rt-multi-thread", +] } +serde = { version = "1", features = ["derive"] } futures = "0.3" shared = { path = "./shared" } +tracing = "0.1" +tracing-subscriber = "0.3.0" +anyhow = "1.0.91" [profile.release] #opt-level = "z" # Optimize for size. -lto = true # Enable Link Time Optimization -codegen-units = 1 # Reduce number of codegen units to increase optimizations. -panic = "abort" # Abort on panic -strip = true # Automatically strip symbols from the binary. +lto = true # Enable Link Time Optimization +codegen-units = 1 # Reduce number of codegen units to increase optimizations. +panic = "abort" # Abort on panic +strip = true # Automatically strip symbols from the binary. [profile.bloat] inherits = "release" diff --git a/central/Cargo.toml b/central/Cargo.toml index 90bad47..73a5c3c 100644 --- a/central/Cargo.toml +++ b/central/Cargo.toml @@ -17,4 +17,7 @@ rand = "0.8" reqwest = { version = "0.12", default_features = false, features = [ "default-tls", ] } -anyhow = "1.0.91" +anyhow = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +arboard = "3.4.1" diff --git a/central/src/auth/authentik/app.rs b/central/src/auth/authentik/app.rs index a65cb10..76a3001 100644 --- a/central/src/auth/authentik/app.rs +++ b/central/src/auth/authentik/app.rs @@ -1,11 +1,14 @@ +use std::i64; + use beam_lib::reqwest::{self, Response, StatusCode}; use reqwest::Client; use serde_json::{json, Value}; use shared::OIDCConfig; +use tracing::{debug, info}; use super::AuthentikConfig; -pub fn generate_app_values(provider: &str, name: &str, oidc_client_config: &OIDCConfig) -> Value { +pub fn generate_app_values(provider: i64, name: &str, oidc_client_config: &OIDCConfig) -> Value { let id = format!( "{name}-{}", if oidc_client_config.is_public { @@ -14,7 +17,6 @@ pub fn generate_app_values(provider: &str, name: &str, oidc_client_config: &OIDC "private" } ); - // Todo noch anpassen json!({ "name": id, "slug": id, @@ -24,17 +26,24 @@ pub fn generate_app_values(provider: &str, name: &str, oidc_client_config: &OIDC } pub async fn generate_application( - provider: &str, + provider: i64, name: &str, oidc_client_config: &OIDCConfig, conf: &AuthentikConfig, token: &str, client: &Client, ) -> reqwest::Result { + debug!(provider); + let app_value = generate_app_values(provider, name, oidc_client_config); + debug!("{:#?}", app_value); client - .post(&format!("{}/api/v3/core/applications/", conf.authentik_url)) + .post( + conf.authentik_url + .join("api/v3/core/applications/") + .expect("Error parsing app url"), + ) .bearer_auth(token) - .json(&generate_app_values(provider, name, oidc_client_config)) + .json(&app_value) .send() .await } @@ -42,43 +51,47 @@ pub async fn generate_application( pub async fn check_app_result( token: &str, name: &str, + provider_pk: i64, oidc_client_config: &OIDCConfig, conf: &AuthentikConfig, client: &Client, ) -> anyhow::Result { - let res = generate_application(name, name, oidc_client_config, conf, token, client).await?; + let res = + generate_application(provider_pk, name, oidc_client_config, conf, token, client).await?; match res.status() { - StatusCode::OK => { - println!("Application for {name} created."); - return Ok(true); + StatusCode::CREATED => { + info!("Application for {name} created."); + Ok(true) } StatusCode::CONFLICT => { let conflicting_client = get_application(name, token, oidc_client_config, conf, client).await?; if app_configs_match( &conflicting_client, - &generate_app_values(name, name, oidc_client_config), + &generate_app_values(provider_pk, name, oidc_client_config), ) { + info!("Application {name} exists."); Ok(true) } else { + info!("Application for {name} is updated."); Ok(client - .put(&format!( - "{}/api/v3/core/applicaions/{}", - conf.authentik_url, - conflicting_client - .get("slug") - .and_then(Value::as_str) - .expect("We have a valid client") - )) + .put( + conf.authentik_url.join("api/v3/core/applicaions/")?.join( + conflicting_client + .get("slug") + .and_then(Value::as_str) + .expect("No valid client"), + )?, + ) .bearer_auth(token) - .json(&generate_app_values(name, name, oidc_client_config)) + .json(&generate_app_values(provider_pk, name, oidc_client_config)) .send() .await? .status() .is_success()) } } - s => anyhow::bail!("Unexpected statuscode {s} while creating keycloak client. {res:?}"), + s => anyhow::bail!("Unexpected statuscode {s} while creating authentik client. {res:?}"), } } @@ -98,10 +111,11 @@ pub async fn get_application( } ); client - .get(&format!( - "{}/api/v3/core/applications/{id}/", + .get( conf.authentik_url - )) + .join(&format!("api/v3/core/applications/{id}/")) + .expect("Error parsing app url"), + ) .bearer_auth(token) .send() .await? @@ -117,9 +131,10 @@ pub async fn compare_applications( client: &Client, ) -> anyhow::Result { let client = get_application(name, token, oidc_client_config, conf, client).await?; - let wanted_client = generate_app_values(name, name, oidc_client_config); + let test = 27; + let wanted_client = generate_app_values(test, name, oidc_client_config); Ok( - client.get("client_secret") == wanted_client.get("client_ secret") + client.get("client_secret") == wanted_client.get("client_secret") && app_configs_match(&client, &wanted_client), ) } diff --git a/central/src/auth/authentik/group.rs b/central/src/auth/authentik/group.rs index 76c96b8..93888d1 100644 --- a/central/src/auth/authentik/group.rs +++ b/central/src/auth/authentik/group.rs @@ -1,6 +1,7 @@ -use beam_lib::reqwest::{self, StatusCode, Url}; +use beam_lib::reqwest::{self, StatusCode}; use reqwest::Client; use serde_json::json; +use tracing::info; use super::AuthentikConfig; @@ -8,7 +9,7 @@ pub async fn create_groups( name: &str, token: &str, conf: &AuthentikConfig, - CLIENT: &Client, + client: &Client, ) -> anyhow::Result<()> { let capitalize = |s: &str| { let mut chrs = s.chars(); @@ -20,14 +21,17 @@ pub async fn create_groups( }; let name = capitalize(name); for group in &conf.authentik_groups_per_bh { - post_group(&group.replace('#', &name), token, conf).await?; + post_group(&group.replace('#', &name), token, conf, client).await?; } Ok(()) } -pub async fn post_group(name: &str, token: &str, conf: &AuthentikConfig) -> anyhow::Result<()> { - let client = reqwest::Client::new(); - +pub async fn post_group( + name: &str, + token: &str, + conf: &AuthentikConfig, + client: &Client, +) -> anyhow::Result<()> { let res = client .post(conf.authentik_url.join("api/v3/core/groups/")?) .bearer_auth(token) @@ -37,10 +41,13 @@ pub async fn post_group(name: &str, token: &str, conf: &AuthentikConfig) -> anyh .send() .await?; match res.status() { - StatusCode::CREATED => println!("Created group {name}"), - StatusCode::OK => println!("Created group {name}"), - StatusCode::BAD_REQUEST => println!("Group {name} already existed"), - s => anyhow::bail!("Unexpected statuscode {s} while creating group {name}: {:#?}", res.json::().await.unwrap_or_default()), + StatusCode::CREATED => info!("Created group {name}"), + StatusCode::OK => info!("Created group {name}"), + StatusCode::BAD_REQUEST => info!("Group {name} already existed"), + s => anyhow::bail!( + "Unexpected statuscode {s} while creating group {name}: {:#?}", + res.json::().await.unwrap_or_default() + ), } Ok(()) } diff --git a/central/src/auth/authentik/mod.rs b/central/src/auth/authentik/mod.rs index a75d671..1e99e5c 100644 --- a/central/src/auth/authentik/mod.rs +++ b/central/src/auth/authentik/mod.rs @@ -14,11 +14,12 @@ use app::{ use beam_lib::reqwest::{self, Url}; use clap::{builder::Str, Parser}; use group::create_groups; -use provider::{generate_provider_values, get_provider, provider_configs_match}; +use provider::{compare_provider, generate_provider_values, get_provider, provider_configs_match}; use reqwest::{Client, StatusCode}; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use shared::{OIDCConfig, SecretResult}; +use tracing::{debug, field::debug, info}; use super::config::FlowPropertymapping; @@ -52,11 +53,32 @@ impl FlowPropertymapping { "microprofile-jwt", "groups", ]; - let flow_url = "/api/v3/flows/instances/?ordering=slug&page=1&page_size=20&search="; - let property_url = "/api/v3/propertymappings/all/?managed__isnull=true&ordering=name&page=1&page_size=20&search="; - let property_mapping = - get_property_mappings_uuids(property_url, conf, token, property_keys).await; - let authorization_flow = get_uuid(flow_url, conf, token, flow_key, &get_beamclient()) + //let flow_url = "/api/v3/flows/instances/?ordering=slug&page=1&page_size=20&search="; + let base_url = conf.authentik_url.join("api/v3/flows/instances/").unwrap(); + let flow_url = Url::parse_with_params( + base_url.as_str(), + &[("orderung", "slug"), ("page", "1"), ("page_size", "20")], + ) + .unwrap(); + + //let property_url = "/api/v3/propertymappings/all/?managed__isnull=true&ordering=name&page=1&page_size=20&search="; + let base_url = conf + .authentik_url + .join("api/v3/propertymappings/all/") + .unwrap(); + let query_url = Url::parse_with_params( + base_url.as_str(), + &[ + ("managed__isnull", "true"), + ("ordering", "name"), + ("page", "1"), + ("page_size", "20"), + ], + ) + .unwrap(); + + let property_mapping = get_property_mappings_uuids(&query_url, token, property_keys).await; + let authorization_flow = get_uuid(&flow_url, token, flow_key, &get_beamclient()) .await .expect("No default flow present"); // flow uuid let mapping = FlowPropertymapping { @@ -101,25 +123,30 @@ pub async fn combine_app_provider( }; let generated_provider = generate_provider_values(name, oidc_client_config, &secret, conf, token).await?; + debug!("{:#?}", generated_provider); let provider_res = client - .post(&format!("{}/api/v3/providers/oauth2/", conf.authentik_url)) + .post(conf.authentik_url.join("api/v3/providers/oauth2/")?) .bearer_auth(token) .json(&generated_provider) .send() .await?; // Create groups for this client create_groups(name, token, conf, client).await?; + dbg!(&provider_res); match provider_res.status() { - StatusCode::OK => { - println!("Client for {name} created."); - check_app_result(token, name, oidc_client_config, conf, client).await?; + StatusCode::CREATED => { + let pk: serde_json::Value = provider_res.json().await?; + let provider_pk = pk.get("pk").and_then(|v| v.as_i64()).unwrap(); + debug!("{:?}", provider_pk); + info!("Provider for {name} created."); + check_app_result(token, name, provider_pk, oidc_client_config, conf, client).await?; Ok(SecretResult::Created(secret)) } StatusCode::CONFLICT => { let conflicting_provider = get_provider(name, token, oidc_client_config, conf, client).await?; - if provider_configs_match(&conflicting_provider, &generated_provider) { - check_app_result(token, name, oidc_client_config, conf, client).await?; + if compare_provider(token, name, oidc_client_config, conf, &secret, client).await? { + info!("Provider {name} existed."); Ok(SecretResult::AlreadyExisted( conflicting_provider .as_object() @@ -130,14 +157,14 @@ pub async fn combine_app_provider( )) } else { Ok(client - .put(&format!( - "{}/api/v3/providers/oauth2/{}", - conf.authentik_url, - conflicting_provider - .get("pk") - .and_then(Value::as_str) - .expect("We have a valid client") - )) + .put( + conf.authentik_url.join("api/v3/providers/oauth2/")?.join( + conflicting_provider + .get("pk") + .and_then(Value::as_str) + .expect("We have a valid client"), + )?, + ) .bearer_auth(token) .json(&generated_provider) .send() @@ -148,23 +175,21 @@ pub async fn combine_app_provider( .expect("We know the provider already exists so updating should be successful")) } } - s => bail!("Unexpected statuscode {s} while creating authentik client. {provider_res:?}"), + s => bail!( + "Unexpected statuscode {s} while creating authentik app and provider. {provider_res:?}" + ), } } async fn get_uuid( - target_url: &str, - conf: &AuthentikConfig, + target_url: &Url, token: &str, search_key: &str, client: &Client, ) -> Option { - println!("{:?}", search_key); let target_value: serde_json::Value = client - .get(&format!( - "{}{}{}", - conf.authentik_url, target_url, search_key - )) + .get(target_url.to_owned()) + .query(&[("search", search_key)]) .bearer_auth(token) .send() .await @@ -172,12 +197,13 @@ async fn get_uuid( .json() .await .ok()?; + debug!("Value search key {search_key}: {:?}", &target_value); // pk is the uuid for this result target_value .as_object() .and_then(|o| o.get("results")) .and_then(Value::as_array) - .and_then(|a| a.get(0)) + .and_then(|a| a.first()) .and_then(|o| o.as_object()) .and_then(|o| o.get("pk")) .and_then(Value::as_str) @@ -185,8 +211,7 @@ async fn get_uuid( } async fn get_property_mappings_uuids( - target_url: &str, - conf: &AuthentikConfig, + target_url: &Url, token: &str, search_key: Vec<&str>, ) -> HashMap { @@ -194,7 +219,7 @@ async fn get_property_mappings_uuids( for key in search_key { result.insert( key.to_string(), - get_uuid(target_url, conf, token, key, &get_beamclient()) + get_uuid(target_url, token, key, &get_beamclient()) .await .expect(&format!("Property: {:?}", key)), ); @@ -224,7 +249,11 @@ async fn get_access_token(conf: &AuthentikConfig) -> reqwest::Result { access_token: String, } get_beamclient() - .post(&format!("{}/application/o/token/", conf.authentik_url)) + .post( + conf.authentik_url + .join("application/o/token/") + .expect("Error parsing token url"), + ) .form(&json!({ "grant_type": "client_credentials", "client_id": conf.authentik_id, diff --git a/central/src/auth/authentik/provider.rs b/central/src/auth/authentik/provider.rs index 8fd0aad..90a4e9e 100644 --- a/central/src/auth/authentik/provider.rs +++ b/central/src/auth/authentik/provider.rs @@ -1,9 +1,10 @@ -use anyhow::Ok; -use reqwest::{Client, Response, StatusCode}; +use anyhow::{Context, Ok}; +use reqwest::{Client, Response, StatusCode, Url}; use serde_json::{json, Value}; use shared::{OIDCConfig, SecretResult}; +use tracing::debug; -use crate::{auth::config::FlowPropertymapping, get_beamclient}; +use crate::auth::config::FlowPropertymapping; use super::{get_uuid, AuthentikConfig}; @@ -38,7 +39,7 @@ pub async fn generate_provider_values( mapping.property_mapping.get("microprofile-jwt"), mapping.property_mapping.get("groups") ], - "redirect_uris": oidc_client_config.redirect_urls, + "redirect_uris": oidc_client_config.redirect_urls.first().unwrap(), }); if oidc_client_config.is_public { @@ -64,7 +65,7 @@ pub async fn get_provider( oidc_client_config: &OIDCConfig, conf: &AuthentikConfig, client: &Client, -) -> reqwest::Result { +) -> anyhow::Result { let id = format!( "{name}-{}", if oidc_client_config.is_public { @@ -73,20 +74,34 @@ pub async fn get_provider( "private" } ); - let provider_url = "/api/v3/providers/all/?ordering=name&page=1&page_size=20&search="; - let pk = get_uuid(&provider_url, conf, token, &id, client) + //let provider_search = "api/v3/providers/all/?ordering=name&page=1&page_size=20&search="; + let base_url = conf.authentik_url.join("api/v3/providers/all/").unwrap(); + let query_url = Url::parse_with_params( + base_url.as_str(), + &[("ordering", "name"), ("page", "1"), ("page_size", "20")], + ) + .unwrap(); + + let pk = get_uuid(&query_url, token, &id, client) .await - .expect(&format!("Property: {:?}", id)); + .context(format!("Property: {:?}", id))?; + let mut base_url = conf + .authentik_url + .join("api/v3/providers/oauth2/") + .context("Error parsing provider")?; + { + let mut provider_url = base_url.path_segments_mut().unwrap(); + provider_url.push(&pk); + } client - .get(&format!( - "{}/api/v3/providers/oauth2/{pk}/", - conf.authentik_url - )) + .get(base_url) .bearer_auth(token) .send() - .await? + .await + .context("No Response")? .json() .await + .context("No valid json Response") } pub async fn compare_provider( @@ -123,4 +138,3 @@ pub fn provider_configs_match(a: &Value, b: &Value) -> bool { a_v.iter().any(|a_v| a_v.get("name") == v.get("name")) }) } - diff --git a/central/src/auth/authentik/test.rs b/central/src/auth/authentik/test.rs index 9e3e6dc..96ec657 100644 --- a/central/src/auth/authentik/test.rs +++ b/central/src/auth/authentik/test.rs @@ -1,5 +1,6 @@ use crate::auth::authentik::app::generate_app_values; use crate::auth::authentik::group::{create_groups, post_group}; +use crate::auth::authentik::provider::get_provider; use crate::auth::authentik::{ app_configs_match, combine_app_provider, compare_applications, get_application, get_uuid, AuthentikConfig, @@ -7,9 +8,12 @@ use crate::auth::authentik::{ use crate::{get_beamclient, CLIENT}; use beam_lib::reqwest::{self, Error, StatusCode, Url}; use serde::{Deserialize, Serialize}; -use serde_json::json; +use serde_json::{json, Value}; use shared::{OIDCConfig, SecretResult}; +use arboard::Clipboard; +use tracing::debug; +use tracing::field::debug; #[derive(Deserialize, Serialize, Debug)] struct Token { access_token: String, @@ -18,11 +22,17 @@ struct Token { #[cfg(test)] pub fn setup_authentik() -> reqwest::Result<(String, AuthentikConfig)> { //let token = get_access_token_via_admin_login().await?; - let token = Token { - access_token: "ecSepyVwTIQzGO3tPzUuNLl5Rp5KSbs4AhJup1PUMsGD1h1dSUvs3HT3uhgK".to_owned(), - }; + let _ = tracing_subscriber::fmt() + .with_max_level(tracing::Level::DEBUG) + .with_test_writer() + .try_init(); + let token = std::env::var("AUTHENTIK_TOKEN").expect("Missing ENV Authentik_Token"); + // copy from clipboard + //let mut clipboard = Clipboard::new().unwrap(); + //let t = clipboard.get_text().unwrap(); + //debug!("test: {:?}", t); Ok(( - token.access_token, + token, AuthentikConfig { authentik_url: "http://localhost:9000".parse().unwrap(), authentik_id: "unused in tests".into(), @@ -32,6 +42,7 @@ pub fn setup_authentik() -> reqwest::Result<(String, AuthentikConfig)> { )) } +// test is working #[ignore = "Requires setting up a authentik"] #[tokio::test] async fn get_access_test() { @@ -83,21 +94,26 @@ async fn get_access_token_via_admin_login() -> reqwest::Result { .map(|t| t.access_token) } -#[ignore = "Requires setting up a authentik"] -#[tokio::test(flavor = "multi_thread")] +//#[ignore = "Requires setting up a authentik"] +#[tokio::test] async fn test_create_client() -> anyhow::Result<()> { let (token, conf) = setup_authentik()?; - let name = "window"; + let name = "leaf"; // public client let client_config = OIDCConfig { is_public: true, - redirect_urls: vec!["http://foo/bar".into()], + redirect_urls: vec!["ttp://foo/bar".into()], }; let (SecretResult::Created(pw) | SecretResult::AlreadyExisted(pw)) = dbg!(combine_app_provider(&token, name, &client_config, &conf, &get_beamclient()).await?) else { panic!("Not created or existed") }; + let provider_pk = get_provider(name, &token, &client_config, &conf, &get_beamclient()) + .await? + .get("pk") + .and_then(|v| v.as_i64()) + .unwrap(); let c = dbg!( get_application(name, &token, &client_config, &conf, &get_beamclient()) .await @@ -105,7 +121,7 @@ async fn test_create_client() -> anyhow::Result<()> { ); assert!(app_configs_match( &c, - &generate_app_values(name, name, &client_config) + &generate_app_values(provider_pk, name, &client_config) )); assert!(dbg!( compare_applications(&token, name, &client_config, &conf, &get_beamclient()).await? @@ -128,7 +144,7 @@ async fn test_create_client() -> anyhow::Result<()> { ); assert!(app_configs_match( &c, - &generate_app_values(name, name, &client_config) + &generate_app_values(provider_pk, name, &client_config) )); assert!(dbg!( compare_applications(&token, name, &client_config, &conf, &get_beamclient()).await? @@ -137,63 +153,81 @@ async fn test_create_client() -> anyhow::Result<()> { Ok(()) } -//#[ignore = "Requires setting up a authentik"] +#[ignore = "Requires setting up a authentik"] #[tokio::test] async fn group_test() -> anyhow::Result<()> { let (token, conf) = setup_authentik()?; - post_group("single", &token, &conf).await - //create_groups("next1", &token, &conf, &get_beamclient()).await + create_groups("next2", &token, &conf, &get_beamclient()).await } -#[ignore = "Requires setting up a authentik"] -#[tokio::test(flavor = "multi_thread")] +//#[ignore = "Requires setting up a authentik"] +#[tokio::test] async fn test_flow() { - let (token, conf) = setup_authentik() - .expect("Cannot setup authentik as test"); - let test_key = "authorization_flow"; - let flow_url = "/api/v3/flows/instances/?ordering=slug&page=1&page_size=20&search="; - let res = get_uuid(flow_url, &conf, &token, test_key, &get_beamclient()).await; - dbg!(&res); + let (token, conf) = setup_authentik().expect("Cannot setup authentik as test"); + let test_key = "authentication_flow"; + let base_url = conf.authentik_url.join("api/v3/flows/instances/").unwrap(); + let query_url = Url::parse_with_params( + base_url.as_str(), + &[("orderung", "slug"), ("page", "1"), ("page_size", "20")], + ) + .unwrap(); + //let flow_url = "api/v3/flows/instances/?ordering=slug&page=1&page_size=20&search="; + let res = get_uuid(&query_url, &token, test_key, &get_beamclient()).await; + debug!(res); match res { Some(uuid) => { - println!("Found: {}", uuid); + debug!("Found flow id: {}", uuid); assert!(!uuid.is_empty(), "empty"); } None => { - panic!("Expected {}", test_key); + debug!("Result flow {} not found", test_key); } } } -#[ignore = "Requires setting up a authentik"] -#[tokio::test(flavor = "multi_thread")] +//#[ignore = "Requires setting up a authentik"] +#[tokio::test] async fn test_property() { - let (token, conf) = setup_authentik() - .expect("Cannot setup authentik as test"); + let (token, conf) = setup_authentik().expect("Cannot setup authentik as test"); let test_key = "web-origins"; - let flow_url = "/api/v3/propertymappings/all/?managed__isnull=true&ordering=name&page=1&page_size=20&search="; - let res = get_uuid(flow_url, &conf, &token, test_key, &get_beamclient()).await; - dbg!(res); + let base_url = conf + .authentik_url + .join("api/v3/propertymappings/all/") + .unwrap(); + let query_url = Url::parse_with_params( + base_url.as_str(), + &[ + ("managed__isnull", "true"), + ("ordering", "name"), + ("page", "1"), + ("page_size", "20"), + ], + ) + .unwrap(); + //let flow_url = "api/v3/propertymappings/all/?managed__isnull=true&ordering=name&page=1&page_size=20&search="; + let res = get_uuid(&query_url, &token, test_key, &get_beamclient()).await; + //debug!("Result Property for {test_key}: {:#?}", res); + debug!("{:?}", query_url); + debug!("{:?}", res); } -#[ignore = "Requires setting up a authentik"] -#[tokio::test(flavor = "multi_thread")] +//#[ignore = "Requires setting up a authentik"] +#[tokio::test] async fn create_property() { - let (token, conf) = setup_authentik() - .expect("Cannot setup authentik as test"); - let acr = "web-origins".to_owned(); + let (token, conf) = setup_authentik().expect("Cannot setup authentik as test"); + let acr = "not-needed2".to_owned(); let ext = "return{}".to_owned(); let json_property = json!({ "name": acr, "expression": ext }); - let propperty_url = "/api/v3/propertymappings/source/oauth/"; + let property_url = "api/v3/propertymappings/source/oauth/"; let res = get_beamclient() - .post(&format!("{}{}", conf.authentik_url, propperty_url)) + .post(conf.authentik_url.join(property_url).expect("No valid Url")) .bearer_auth(token) .json(&json_property) .send() .await .expect("no response"); - dbg!(res); + tracing::debug!("Result: {:#?}", res); } diff --git a/central/src/auth/keycloak/client.rs b/central/src/auth/keycloak/client.rs index 9d7df7e..20c00cc 100644 --- a/central/src/auth/keycloak/client.rs +++ b/central/src/auth/keycloak/client.rs @@ -13,7 +13,14 @@ pub async fn get_client( oidc_client_config: &OIDCConfig, conf: &KeyCloakConfig, ) -> reqwest::Result { - let id = format!("{name}-{}", if oidc_client_config.is_public { "public" } else { "private" }); + let id = format!( + "{name}-{}", + if oidc_client_config.is_public { + "public" + } else { + "private" + } + ); CLIENT .get(&format!( "{}admin/realms/{}/clients/{id}", @@ -26,7 +33,6 @@ pub async fn get_client( .await } - pub async fn compare_clients( token: &str, name: &str, @@ -41,24 +47,34 @@ pub async fn compare_clients( } pub fn client_configs_match(a: &Value, b: &Value) -> bool { - let includes_other_json_array = |key, comparator: &dyn Fn(_, _) -> bool| a - .get(key) - .and_then(Value::as_array) - .is_some_and(|a_values| b - .get(key) + let includes_other_json_array = |key, comparator: &dyn Fn(_, _) -> bool| { + a.get(key) .and_then(Value::as_array) - .is_some_and(|vec| vec.iter().all(|v| comparator(a_values, v))) - ); - + .is_some_and(|a_values| { + b.get(key) + .and_then(Value::as_array) + .is_some_and(|vec| vec.iter().all(|v| comparator(a_values, v))) + }) + }; + a.get("name") == b.get("name") && includes_other_json_array("defaultClientScopes", &|a_v, v| a_v.contains(v)) && includes_other_json_array("redirectUris", &|a_v, v| a_v.contains(v)) - && includes_other_json_array("protocolMappers", &|a_v, v| a_v.iter().any(|a_v| a_v.get("name") == v.get("name"))) + && includes_other_json_array("protocolMappers", &|a_v, v| { + a_v.iter().any(|a_v| a_v.get("name") == v.get("name")) + }) } pub fn generate_client(name: &str, oidc_client_config: &OIDCConfig, secret: &str) -> Value { let secret = (!oidc_client_config.is_public).then_some(secret); - let id = format!("{name}-{}", if oidc_client_config.is_public { "public" } else { "private" }); + let id = format!( + "{name}-{}", + if oidc_client_config.is_public { + "public" + } else { + "private" + } + ); let mut json = json!({ "name": id, "id": id, @@ -89,12 +105,13 @@ pub fn generate_client(name: &str, oidc_client_config: &OIDCConfig, secret: &str }] }); if let Some(secret) = secret { - json.as_object_mut().unwrap().insert("secret".to_owned(), secret.into()); + json.as_object_mut() + .unwrap() + .insert("secret".to_owned(), secret.into()); } json } - pub async fn post_client( token: &str, name: &str, @@ -133,12 +150,14 @@ pub async fn post_client( StatusCode::CONFLICT => { let conflicting_client = get_client(name, token, oidc_client_config, conf).await?; if client_configs_match(&conflicting_client, &generated_client) { - Ok(SecretResult::AlreadyExisted(conflicting_client - .as_object() - .and_then(|o| o.get("secret")) - .and_then(Value::as_str) - .unwrap_or("") - .to_owned())) + Ok(SecretResult::AlreadyExisted( + conflicting_client + .as_object() + .and_then(|o| o.get("secret")) + .and_then(Value::as_str) + .unwrap_or("") + .to_owned(), + )) } else { Ok(CLIENT .put(&format!( @@ -162,4 +181,4 @@ pub async fn post_client( } s => bail!("Unexpected statuscode {s} while creating keycloak client. {res:?}"), } -} \ No newline at end of file +} diff --git a/central/src/auth/keycloak/mod.rs b/central/src/auth/keycloak/mod.rs index f23bb2f..476a749 100644 --- a/central/src/auth/keycloak/mod.rs +++ b/central/src/auth/keycloak/mod.rs @@ -1,5 +1,5 @@ -mod test; mod client; +mod test; use crate::CLIENT; use anyhow::bail; @@ -31,7 +31,6 @@ pub struct KeyCloakConfig { pub keycloak_groups_per_bh: Vec, } - pub async fn create_client( name: &str, oidc_client_config: OIDCConfig, @@ -41,7 +40,6 @@ pub async fn create_client( post_client(&token, name, &oidc_client_config, conf).await } - pub async fn validate_client( name: &str, oidc_client_config: &OIDCConfig, @@ -52,7 +50,6 @@ pub async fn validate_client( compare_clients(&token, name, oidc_client_config, conf, secret).await } - async fn get_access_token(conf: &KeyCloakConfig) -> reqwest::Result { #[derive(serde::Deserialize)] struct Token { @@ -78,7 +75,11 @@ async fn get_access_token(conf: &KeyCloakConfig) -> reqwest::Result { async fn create_groups(name: &str, token: &str, conf: &KeyCloakConfig) -> anyhow::Result<()> { let capitalize = |s: &str| { let mut chrs = s.chars(); - chrs.next().map(char::to_uppercase).map(Iterator::collect).unwrap_or(String::new()) + chrs.as_str() + chrs.next() + .map(char::to_uppercase) + .map(Iterator::collect) + .unwrap_or(String::new()) + + chrs.as_str() }; let name = capitalize(name); for group in &conf.keycloak_groups_per_bh { @@ -102,7 +103,7 @@ async fn post_group(name: &str, token: &str, conf: &KeyCloakConfig) -> anyhow::R match res.status() { StatusCode::CREATED => println!("Created group {name}"), StatusCode::CONFLICT => println!("Group {name} already existed"), - s => bail!("Unexpected statuscode {s} while creating group {name}") + s => bail!("Unexpected statuscode {s} while creating group {name}"), } Ok(()) } @@ -135,7 +136,8 @@ async fn add_service_account_roles( struct UserIdExtractor { id: String, } - let service_account_id = CLIENT.get(&format!( + let service_account_id = CLIENT + .get(&format!( "{}admin/realms/{}/clients/{}/service-account-user", conf.keycloak_url, conf.keycloak_realm, client_id )) @@ -153,7 +155,8 @@ async fn add_service_account_roles( assert_eq!(roles.len(), conf.keycloak_service_account_roles.len(), "Failed to find all required service account roles got {roles:#?} but expected all of these: {:#?}", conf.keycloak_service_account_roles); let realm_id = roles[0].container_id.clone(); - CLIENT.post(&format!( + CLIENT + .post(&format!( "{}admin/realms/{}/users/{}/role-mappings/clients/{}", conf.keycloak_url, conf.keycloak_realm, service_account_id, realm_id )) @@ -170,22 +173,26 @@ struct ServiceAccountRole { id: String, #[serde(rename = "containerId", skip_serializing)] container_id: String, - name: String + name: String, } -async fn get_realm_permission_roles(token: &str, conf: &KeyCloakConfig) -> reqwest::Result> { +async fn get_realm_permission_roles( + token: &str, + conf: &KeyCloakConfig, +) -> reqwest::Result> { #[derive(Debug, serde::Deserialize)] struct RealmId { id: String, #[serde(rename = "clientId")] - client_id: String + client_id: String, } let permission_realm = if conf.keycloak_realm == "master" { "master-realm" } else { "realm-management" }; - let res = CLIENT.get(&format!( + let res = CLIENT + .get(&format!( "{}admin/realms/{}/clients/?q={permission_realm}&search", conf.keycloak_url, conf.keycloak_realm )) @@ -194,10 +201,12 @@ async fn get_realm_permission_roles(token: &str, conf: &KeyCloakConfig) -> reqwe .await? .json::>() .await?; - let role_client = res.into_iter() + let role_client = res + .into_iter() .find(|v| v.client_id.starts_with(permission_realm)) .expect(&format!("Failed to find realm id for {permission_realm}")); - CLIENT.get(&format!( + CLIENT + .get(&format!( "{}admin/realms/{}/clients/{}/roles", conf.keycloak_url, conf.keycloak_realm, role_client.id )) @@ -206,4 +215,4 @@ async fn get_realm_permission_roles(token: &str, conf: &KeyCloakConfig) -> reqwe .await? .json() .await -} \ No newline at end of file +} diff --git a/central/src/auth/keycloak/test.rs b/central/src/auth/keycloak/test.rs index 09d556d..1c0a7a5 100644 --- a/central/src/auth/keycloak/test.rs +++ b/central/src/auth/keycloak/test.rs @@ -1,14 +1,14 @@ -use crate::auth::keycloak::client::{client_configs_match, compare_clients, generate_client, get_client, post_client}; -use crate::{auth::keycloak::create_groups, CLIENT}; +use crate::auth::keycloak::client::{ + client_configs_match, compare_clients, generate_client, get_client, post_client, +}; use crate::auth::keycloak::KeyCloakConfig; +use crate::{auth::keycloak::create_groups, CLIENT}; use beam_lib::reqwest::{self, StatusCode, Url}; use serde_json::{json, Value}; use shared::{OIDCConfig, SecretResult}; - #[cfg(test)] async fn get_access_token_via_admin_login() -> reqwest::Result { - #[derive(serde::Deserialize)] struct Token { access_token: String, @@ -16,7 +16,11 @@ async fn get_access_token_via_admin_login() -> reqwest::Result { CLIENT .post(&format!( "{}/realms/master/protocol/openid-connect/token", - if cfg!(test) { "http://localhost:1337"} else { "http://keycloak:8080" } + if cfg!(test) { + "http://localhost:1337" + } else { + "http://keycloak:8080" + } )) .form(&json!({ "client_id": "admin-cli", @@ -57,7 +61,6 @@ async fn setup_keycloak() -> reqwest::Result<(String, KeyCloakConfig)> { )) } - #[ignore = "Requires setting up a keycloak"] #[tokio::test] async fn service_account_test() -> anyhow::Result<()> { @@ -68,29 +71,52 @@ async fn service_account_test() -> anyhow::Result<()> { Ok(()) } - #[ignore = "Requires setting up a keycloak"] #[tokio::test] async fn test_create_client() -> anyhow::Result<()> { let (token, conf) = setup_keycloak().await?; let name = "test"; // public client - let client_config = OIDCConfig { is_public: true, redirect_urls: vec!["http://foo/bar".into()] }; - let (SecretResult::Created(pw) | SecretResult::AlreadyExisted(pw)) = dbg!(post_client(&token, name, &client_config, &conf).await?) else { + let client_config = OIDCConfig { + is_public: true, + redirect_urls: vec!["http://foo/bar".into()], + }; + let (SecretResult::Created(pw) | SecretResult::AlreadyExisted(pw)) = + dbg!(post_client(&token, name, &client_config, &conf).await?) + else { panic!("Not created or existed") }; - let c = dbg!(get_client(name, &token, &client_config, &conf).await.unwrap()); - assert!(client_configs_match(&c, &generate_client(name, &client_config, &pw))); - assert!(dbg!(compare_clients(&token, name, &client_config, &conf, &pw).await?)); + let c = dbg!(get_client(name, &token, &client_config, &conf) + .await + .unwrap()); + assert!(client_configs_match( + &c, + &generate_client(name, &client_config, &pw) + )); + assert!(dbg!( + compare_clients(&token, name, &client_config, &conf, &pw).await? + )); // private client - let client_config = OIDCConfig { is_public: false, redirect_urls: vec!["http://foo/bar".into()] }; - let (SecretResult::Created(pw) | SecretResult::AlreadyExisted(pw)) = dbg!(post_client(&token, name, &client_config, &conf).await?) else { + let client_config = OIDCConfig { + is_public: false, + redirect_urls: vec!["http://foo/bar".into()], + }; + let (SecretResult::Created(pw) | SecretResult::AlreadyExisted(pw)) = + dbg!(post_client(&token, name, &client_config, &conf).await?) + else { panic!("Not created or existed") }; - let c = dbg!(get_client(name, &token, &client_config, &conf).await.unwrap()); - assert!(client_configs_match(&c, &generate_client(name, &client_config, &pw))); - assert!(dbg!(compare_clients(&token, name, &client_config, &conf, &pw).await?)); + let c = dbg!(get_client(name, &token, &client_config, &conf) + .await + .unwrap()); + assert!(client_configs_match( + &c, + &generate_client(name, &client_config, &pw) + )); + assert!(dbg!( + compare_clients(&token, name, &client_config, &conf, &pw).await? + )); Ok(()) } diff --git a/central/src/auth/mod.rs b/central/src/auth/mod.rs index fdb44de..97c5a87 100644 --- a/central/src/auth/mod.rs +++ b/central/src/auth/mod.rs @@ -1,3 +1,3 @@ -mod keycloak; +pub mod authentik; pub(crate) mod config; -pub mod authentik; \ No newline at end of file +mod keycloak; diff --git a/central/src/main.rs b/central/src/main.rs index 881e046..a2f3a01 100644 --- a/central/src/main.rs +++ b/central/src/main.rs @@ -1,10 +1,13 @@ use std::{collections::HashSet, time::Duration}; -use auth::config::{Config, FlowPropertymapping, OIDCProvider}; +use auth::config::{Config, OIDCProvider}; use beam_lib::{reqwest::Client, AppId, BeamClient, BlockingOptions, TaskRequest, TaskResult}; use clap::Parser; +use futures::FutureExt; use once_cell::sync::Lazy; use shared::{SecretRequest, SecretRequestType, SecretResult}; +use tokio::time::sleep; +use tracing::info; mod auth; @@ -19,7 +22,7 @@ pub static BEAM_CLIENT: Lazy = Lazy::new(|| { }); pub fn get_beamclient() -> Client { - return Client::new(); + Client::new() } pub static OIDC_PROVIDER: Lazy> = Lazy::new(OIDCProvider::try_init); @@ -28,9 +31,11 @@ pub static CLIENT: Lazy = Lazy::new(Client::new); #[tokio::main] async fn main() { + tracing_subscriber::fmt::init(); // TODO: Remove once beam feature/stream-tasks is merged let mut seen = HashSet::new(); let block_one = BlockingOptions::from_count(1); + let retry_timer = sleep(Duration::from_secs(0)).fuse(); // TODO: Fast shutdown loop { match BEAM_CLIENT.poll_pending_tasks(&block_one).await { @@ -53,6 +58,38 @@ async fn main() { } } } + /* + loop { + tokio::select! { + _ = shutdown_signal() => { + break; + }, + result = BEAM_CLIENT.poll_pending_tasks(&block_one) => { + match result { + Ok(tasks) => tasks.into_iter().for_each(|task| { + if !seen.contains(&task.id) { + seen.insert(task.id); + tokio::spawn(handle_task(task)); + } + }), + Err(beam_lib::BeamError::ReqwestError(e)) if e.is_connect() => { + eprintln!( + "Failed to connect to beam proxy on {}. Retrying in 30s", + CONFIG.beam_url + ); + tokio::time::sleep(Duration::from_secs(30)).await + } + Err(e) => { + eprintln!("Error during task polling {e}"); + tokio::time::sleep(Duration::from_secs(5)).await; + } + + } + + } + } + } + */ } pub async fn handle_task(task: TaskRequest>) { @@ -119,3 +156,10 @@ pub async fn is_valid(secret: &str, request: &SecretRequest, name: &str) -> Resu } } } + +async fn shutdown_signal() { + tokio::signal::ctrl_c() + .await + .expect("Expect shutdown signal handler"); + info!("Shutdown recieved"); +} diff --git a/dev/docker-compose.yaml b/dev/docker-compose.yaml new file mode 100644 index 0000000..804a762 --- /dev/null +++ b/dev/docker-compose.yaml @@ -0,0 +1,86 @@ +version: "3" + +services: + postgresql: + image: docker.io/library/postgres:16-alpine + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"] + start_period: 20s + interval: 30s + retries: 5 + timeout: 5s + volumes: + - database:/var/lib/postgresql/data + environment: + POSTGRES_PASSWORD: ${PG_PASS:?database password required} + POSTGRES_USER: ${PG_USER:-authentik} + POSTGRES_DB: ${PG_DB:-authentik} + env_file: + - .env + redis: + image: docker.io/library/redis:alpine + command: --save 60 1 --loglevel warning + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "redis-cli ping | grep PONG"] + start_period: 20s + interval: 30s + retries: 5 + timeout: 3s + volumes: + - redis:/data + server: + image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.10.1} + restart: unless-stopped + command: server + environment: + AUTHENTIK_REDIS__HOST: redis + AUTHENTIK_POSTGRESQL__HOST: postgresql + AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik} + AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik} + AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS} + volumes: + - ./media:/media + - ./custom-templates:/templates + env_file: + - .env + ports: + - "${COMPOSE_PORT_HTTP:-9000}:9000" + - "${COMPOSE_PORT_HTTPS:-9443}:9443" + depends_on: + - postgresql + - redis + worker: + image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.10.1} + restart: unless-stopped + command: worker + environment: + AUTHENTIK_REDIS__HOST: redis + AUTHENTIK_POSTGRESQL__HOST: postgresql + AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik} + AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik} + AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS} + # `user: root` and the docker socket volume are optional. + # See more for the docker socket integration here: + # https://goauthentik.io/docs/outposts/integrations/docker + # Removing `user: root` also prevents the worker from fixing the permissions + # on the mounted folders, so when removing this make sure the folders have the correct UID/GID + # (1000:1000 by default) + user: root + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./media:/media + - ./certs:/certs + - ./custom-templates:/templates + env_file: + - .env + depends_on: + - postgresql + - redis + +volumes: + database: + driver: local + redis: + driver: local diff --git a/local/src/cache.rs b/local/src/cache.rs index 0fa5ac8..ae40272 100644 --- a/local/src/cache.rs +++ b/local/src/cache.rs @@ -25,13 +25,22 @@ impl Cache { Self( file.split('\n') .flat_map(|l| l.split_once('=')) - .map(|(k, v)| (k.to_string(), v.trim_start_matches('"').trim_end_matches('"').to_string())) + .map(|(k, v)| { + ( + k.to_string(), + v.trim_start_matches('"').trim_end_matches('"').to_string(), + ) + }) .collect(), ) } pub fn write(&self, path: impl AsRef) -> io::Result<()> { - let data: Vec<_> = self.0.iter().map(|(k, v)| format!(r#"{k}="{v}""#)).collect(); + let data: Vec<_> = self + .0 + .iter() + .map(|(k, v)| format!(r#"{k}="{v}""#)) + .collect(); fs::write(path, data.join("\n")) } } diff --git a/local/src/config.rs b/local/src/config.rs index a5ddad1..62ff5a9 100644 --- a/local/src/config.rs +++ b/local/src/config.rs @@ -1,8 +1,8 @@ -use std::{path::PathBuf, convert::Infallible, str::FromStr}; +use std::{convert::Infallible, path::PathBuf, str::FromStr}; use beam_lib::AppId; use clap::Parser; -use shared::{SecretRequest, OIDCConfig}; +use shared::{OIDCConfig, SecretRequest}; /// Local secret sync #[derive(Debug, Parser)] @@ -31,14 +31,18 @@ impl FromStr for SecretDefinitions { type Err = String; fn from_str(s: &str) -> Result { - s.split('\x1E').filter(|s| !s.is_empty()).map(SecretArg::from_str).collect::>().map(Self) + s.split('\x1E') + .filter(|s| !s.is_empty()) + .map(SecretArg::from_str) + .collect::>() + .map(Self) } } #[derive(Debug, Clone)] pub struct SecretArg { pub name: String, - pub request: SecretRequest + pub request: SecretRequest, } impl FromStr for SecretArg { @@ -59,11 +63,17 @@ impl FromStr for SecretArg { _ => return Err(format!("Invalid OIDC parameters '{args}'. Syntax is ;")), }; let redirect_urls = args.split(',').map(ToString::to_string).collect(); - Ok(SecretRequest::OpenIdConnect(OIDCConfig{ redirect_urls, is_public })) - }, - _ => Err(format!("Unknown secret type {secret_type}")) + Ok(SecretRequest::OpenIdConnect(OIDCConfig { + redirect_urls, + is_public, + })) + } + _ => Err(format!("Unknown secret type {secret_type}")), }?; - Ok(SecretArg { name: name.to_string(), request }) + Ok(SecretArg { + name: name.to_string(), + request, + }) } } diff --git a/local/src/main.rs b/local/src/main.rs index 1286d18..f4ad887 100644 --- a/local/src/main.rs +++ b/local/src/main.rs @@ -71,13 +71,15 @@ async fn main() -> ExitCode { println!("{name} has been created."); secret }); - }, + } Ok(SecretResult::AlreadyExisted(secret)) => { - cache.entry(name.to_string()) + cache + .entry(name.to_string()) .and_modify(|v| { println!("{name} was cached but needed to be updated."); *v = secret.clone() - }).or_insert_with(|| { + }) + .or_insert_with(|| { println!("{name} already existed but was not cached."); secret }); @@ -116,7 +118,9 @@ async fn send_secret_request( metadata: ().try_into().unwrap(), }); } else { - return Err(beam_lib::BeamError::Other("Got OIDC connect tasks but no OIDC provider was configurad".into())); + return Err(beam_lib::BeamError::Other( + "Got OIDC connect tasks but no OIDC provider was configurad".into(), + )); } } assert_eq!( @@ -126,15 +130,20 @@ async fn send_secret_request( ); futures::future::try_join_all(tasks.into_iter().map(|t| async move { BEAM_CLIENT.post_task(&t).await?; - BEAM_CLIENT.poll_results::>>(&t.id, &BlockingOptions::from_count(1)) + BEAM_CLIENT + .poll_results::>>( + &t.id, + &BlockingOptions::from_count(1), + ) .await? .pop() .map(|res| res.body) .ok_or(BeamError::Other( "Got no result from secret provider".into(), )) - }) - ).map_ok(|v| v.into_iter().flatten().collect()).await + })) + .map_ok(|v| v.into_iter().flatten().collect()) + .await } async fn wait_for_beam_proxy() -> beam_lib::Result<()> { diff --git a/shared/src/lib.rs b/shared/src/lib.rs index 0e24ccc..696f1a6 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -1,11 +1,10 @@ use std::ops::Deref; -use serde::{Serialize, Deserialize}; - +use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize, Clone)] pub enum SecretRequest { - OpenIdConnect(OIDCConfig) + OpenIdConnect(OIDCConfig), } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -18,16 +17,16 @@ pub struct OIDCConfig { pub enum SecretResult { AlreadyValid, Created(String), - AlreadyExisted(String) + AlreadyExisted(String), } #[derive(Debug, Serialize, Deserialize, Clone)] pub enum SecretRequestType { ValidateOrCreate { current: String, - request: SecretRequest + request: SecretRequest, }, - Create(SecretRequest) + Create(SecretRequest), } impl Deref for SecretRequestType { @@ -35,8 +34,8 @@ impl Deref for SecretRequestType { fn deref(&self) -> &Self::Target { match self { - SecretRequestType::ValidateOrCreate { request, .. } | - SecretRequestType::Create(request) => request, + SecretRequestType::ValidateOrCreate { request, .. } + | SecretRequestType::Create(request) => request, } } } From 2595825d384a57106769efc0d4c2475b4ff91071 Mon Sep 17 00:00:00 2001 From: Martin Jurk Date: Tue, 26 Nov 2024 08:13:33 +0100 Subject: [PATCH 24/37] docker file for tests --- dev/{docker-compose.yaml => test-authentik.yaml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename dev/{docker-compose.yaml => test-authentik.yaml} (100%) diff --git a/dev/docker-compose.yaml b/dev/test-authentik.yaml similarity index 100% rename from dev/docker-compose.yaml rename to dev/test-authentik.yaml From d95436b927100605a5452ba1bde69563be435d27 Mon Sep 17 00:00:00 2001 From: Martin Jurk Date: Tue, 26 Nov 2024 14:53:58 +0100 Subject: [PATCH 25/37] provider, app created, matched, conflict faild --- .cargo/config.toml | 2 +- central/src/auth/authentik/app.rs | 13 ++++-- central/src/auth/authentik/mod.rs | 3 +- central/src/auth/authentik/provider.rs | 62 ++++++++++++++++++++------ central/src/auth/authentik/test.rs | 4 +- 5 files changed, 62 insertions(+), 22 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index b5db647..8a55750 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,3 @@ [env] -AUTHENTIK_TOKEN = "JVMsRhNLQvfyQKXzxwrjdbqY6I5oxm2Db8PbDQ0iUXZS3WhIzKKqPU5Yv2xn" +AUTHENTIK_TOKEN = "8GIAkaCKkVUrmVuDLBqKmljGDXVwBR8zzXugIKJs5pm0NdWiZFrZF86CeJh8" RUST_LOG = "debug" diff --git a/central/src/auth/authentik/app.rs b/central/src/auth/authentik/app.rs index 76a3001..0105b3d 100644 --- a/central/src/auth/authentik/app.rs +++ b/central/src/auth/authentik/app.rs @@ -1,12 +1,13 @@ use std::i64; +use anyhow::Context; use beam_lib::reqwest::{self, Response, StatusCode}; -use reqwest::Client; +use reqwest::{Client, Url}; use serde_json::{json, Value}; use shared::OIDCConfig; use tracing::{debug, info}; -use super::AuthentikConfig; +use super::{get_uuid, provider::get_provider, AuthentikConfig}; pub fn generate_app_values(provider: i64, name: &str, oidc_client_config: &OIDCConfig) -> Value { let id = format!( @@ -130,9 +131,13 @@ pub async fn compare_applications( conf: &AuthentikConfig, client: &Client, ) -> anyhow::Result { + let provider_pk = get_provider(name, token, oidc_client_config, conf, client) + .await? + .get("pk") + .and_then(|v| v.as_i64()) + .unwrap(); let client = get_application(name, token, oidc_client_config, conf, client).await?; - let test = 27; - let wanted_client = generate_app_values(test, name, oidc_client_config); + let wanted_client = generate_app_values(provider_pk, name, oidc_client_config); Ok( client.get("client_secret") == wanted_client.get("client_secret") && app_configs_match(&client, &wanted_client), diff --git a/central/src/auth/authentik/mod.rs b/central/src/auth/authentik/mod.rs index 1e99e5c..c4f97b2 100644 --- a/central/src/auth/authentik/mod.rs +++ b/central/src/auth/authentik/mod.rs @@ -142,9 +142,10 @@ pub async fn combine_app_provider( check_app_result(token, name, provider_pk, oidc_client_config, conf, client).await?; Ok(SecretResult::Created(secret)) } - StatusCode::CONFLICT => { + StatusCode::BAD_REQUEST => { let conflicting_provider = get_provider(name, token, oidc_client_config, conf, client).await?; + debug!("{:#?}", conflicting_provider); if compare_provider(token, name, oidc_client_config, conf, &secret, client).await? { info!("Provider {name} existed."); Ok(SecretResult::AlreadyExisted( diff --git a/central/src/auth/authentik/provider.rs b/central/src/auth/authentik/provider.rs index 90a4e9e..32eee33 100644 --- a/central/src/auth/authentik/provider.rs +++ b/central/src/auth/authentik/provider.rs @@ -1,8 +1,10 @@ +use std::i64; + use anyhow::{Context, Ok}; use reqwest::{Client, Response, StatusCode, Url}; use serde_json::{json, Value}; use shared::{OIDCConfig, SecretResult}; -use tracing::debug; +use tracing::{debug, field::debug}; use crate::auth::config::FlowPropertymapping; @@ -26,6 +28,7 @@ pub async fn generate_provider_values( "private" } ); + // only one redirect url is possible let mut json = json!({ "name": id, "client_id": id, @@ -59,6 +62,34 @@ pub async fn generate_provider_values( Ok(json) } +async fn get_provider_id( + target_url: &Url, + token: &str, + search_key: &str, + client: &Client, +) -> Option { + let target_value: serde_json::Value = client + .get(target_url.to_owned()) + .query(&[("search", search_key)]) + .bearer_auth(token) + .send() + .await + .ok()? + .json() + .await + .ok()?; + debug!("Value search key {search_key}: {:?}", &target_value); + // pk is the uuid for this result + target_value + .as_object() + .and_then(|o| o.get("results")) + .and_then(Value::as_array) + .and_then(|a| a.first()) + .and_then(|o| o.as_object()) + .and_then(|o| o.get("pk")) + .and_then(|v| v.as_i64()) +} + pub async fn get_provider( name: &str, token: &str, @@ -82,17 +113,20 @@ pub async fn get_provider( ) .unwrap(); - let pk = get_uuid(&query_url, token, &id, client) - .await - .context(format!("Property: {:?}", id))?; + let res = get_provider_id(&query_url, token, &id, client).await; + debug!(res); + let pk = res.unwrap(); let mut base_url = conf .authentik_url - .join("api/v3/providers/oauth2/") + .join(&format!("api/v3/providers/oauth2/{pk}/")) .context("Error parsing provider")?; - { - let mut provider_url = base_url.path_segments_mut().unwrap(); - provider_url.push(&pk); - } + /* + { + let mut provider_url = base_url.path_segments_mut().unwrap(); + provider_url.push(&pk); + } + */ + client .get(base_url) .bearer_auth(token) @@ -115,6 +149,8 @@ pub async fn compare_provider( let client = get_provider(name, token, oidc_client_config, conf, client).await?; let wanted_client = generate_provider_values(name, oidc_client_config, secret, conf, token).await?; + debug!("{:#?}", client); + debug!("{:#?}", wanted_client); Ok( client.get("client_secret") == wanted_client.get("client_secret") && provider_configs_match(&client, &wanted_client), @@ -132,9 +168,7 @@ pub fn provider_configs_match(a: &Value, b: &Value) -> bool { }) }; a.get("name") == b.get("name") - && includes_other_json_array("authorization_flow", &|a_v, v| a_v.contains(v)) - && includes_other_json_array("redirectUris", &|a_v, v| a_v.contains(v)) - && includes_other_json_array("property_mappings", &|a_v, v| { - a_v.iter().any(|a_v| a_v.get("name") == v.get("name")) - }) + && a.get("authorization_flow") == b.get("authorization_flow") + && includes_other_json_array("property_mappings", &|a_v, v| a_v.contains(v)) + && a.get("redirect_uris") == b.get("redirect_uris") } diff --git a/central/src/auth/authentik/test.rs b/central/src/auth/authentik/test.rs index 96ec657..a45619d 100644 --- a/central/src/auth/authentik/test.rs +++ b/central/src/auth/authentik/test.rs @@ -98,11 +98,11 @@ async fn get_access_token_via_admin_login() -> reqwest::Result { #[tokio::test] async fn test_create_client() -> anyhow::Result<()> { let (token, conf) = setup_authentik()?; - let name = "leaf"; + let name = "pipe"; // public client let client_config = OIDCConfig { is_public: true, - redirect_urls: vec!["ttp://foo/bar".into()], + redirect_urls: vec!["http://foo/bar".into()], }; let (SecretResult::Created(pw) | SecretResult::AlreadyExisted(pw)) = dbg!(combine_app_provider(&token, name, &client_config, &conf, &get_beamclient()).await?) From 04eaaa09bf1b7cb05a350acf98abc44a5b635a12 Mon Sep 17 00:00:00 2001 From: Martin Jurk Date: Wed, 27 Nov 2024 16:15:07 +0100 Subject: [PATCH 26/37] matches completed, validation todo and secret todo --- .cargo/config.toml | 2 +- central/src/auth/authentik/app.rs | 74 ++++++++++++++--- central/src/auth/authentik/mod.rs | 109 ++++++++++++++++++------- central/src/auth/authentik/provider.rs | 53 ++++++------ central/src/auth/authentik/test.rs | 31 +------ central/src/auth/config.rs | 20 +++-- 6 files changed, 182 insertions(+), 107 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 8a55750..138538a 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,3 @@ [env] -AUTHENTIK_TOKEN = "8GIAkaCKkVUrmVuDLBqKmljGDXVwBR8zzXugIKJs5pm0NdWiZFrZF86CeJh8" +AUTHENTIK_TOKEN = "DW1CVJavj3IYFN3ivrsXj1zsNvFSob6MSCTk04FpH2WaqqJm7FSy5N5beAi6" RUST_LOG = "debug" diff --git a/central/src/auth/authentik/app.rs b/central/src/auth/authentik/app.rs index 0105b3d..08340a5 100644 --- a/central/src/auth/authentik/app.rs +++ b/central/src/auth/authentik/app.rs @@ -7,7 +7,13 @@ use serde_json::{json, Value}; use shared::OIDCConfig; use tracing::{debug, info}; -use super::{get_uuid, provider::get_provider, AuthentikConfig}; +use crate::auth::config::FlowPropertymapping; + +use super::{ + get_uuid, + provider::{get_provider, get_provider_id}, + AuthentikConfig, +}; pub fn generate_app_values(provider: i64, name: &str, oidc_client_config: &OIDCConfig) -> Value { let id = format!( @@ -124,24 +130,66 @@ pub async fn get_application( .await } -pub async fn compare_applications( +// used only from validate in config +pub async fn compare_app_provider( token: &str, name: &str, oidc_client_config: &OIDCConfig, + secret: &str, conf: &AuthentikConfig, client: &Client, ) -> anyhow::Result { - let provider_pk = get_provider(name, token, oidc_client_config, conf, client) - .await? - .get("pk") - .and_then(|v| v.as_i64()) - .unwrap(); - let client = get_application(name, token, oidc_client_config, conf, client).await?; - let wanted_client = generate_app_values(provider_pk, name, oidc_client_config); - Ok( - client.get("client_secret") == wanted_client.get("client_secret") - && app_configs_match(&client, &wanted_client), - ) + let provider_pk = get_provider_id(name, token, oidc_client_config, conf, client).await; + match provider_pk { + Some(res) => { + let app_res = get_application(name, token, oidc_client_config, conf, client).await?; + let wanted_client = + generate_all_validation(name, token, conf, oidc_client_config).await?; + Ok(app_res.get("client_secret").unwrap() == secret + && app_configs_match(&app_res, &wanted_client)) + } + None => Ok(false), + } +} + +pub async fn generate_all_validation( + name: &str, + token: &str, + conf: &AuthentikConfig, + oidc_client_config: &OIDCConfig, +) -> anyhow::Result { + let mapping = FlowPropertymapping::new(conf, token).await?; + let id = format!( + "{name}-{}", + if oidc_client_config.is_public { + "public" + } else { + "private" + } + ); + let app_json = json!({ + "name": id, + "slug": id, + "provider_obj": { + "name": id, + "authorization_flow": mapping.authorization_flow, + "property_mappings": [ + mapping.property_mapping.get("web-origins"), + mapping.property_mapping.get("acr"), + mapping.property_mapping.get("profile"), + mapping.property_mapping.get("roles"), + mapping.property_mapping.get("email"), + mapping.property_mapping.get("microprofile-jwt"), + mapping.property_mapping.get("groups") + ], + "assigned_application_slug": id, + "assigned_application_name": id, + }, + "launch_url": oidc_client_config.redirect_urls.first().unwrap(), + "group": name + }); + + Ok(app_json) } pub fn app_configs_match(a: &Value, b: &Value) -> bool { diff --git a/central/src/auth/authentik/mod.rs b/central/src/auth/authentik/mod.rs index c4f97b2..114d1bc 100644 --- a/central/src/auth/authentik/mod.rs +++ b/central/src/auth/authentik/mod.rs @@ -7,14 +7,14 @@ use std::{collections::HashMap, sync::Mutex}; use crate::get_beamclient; use anyhow::bail; -use app::{ - app_configs_match, check_app_result, compare_applications, generate_app_values, - generate_application, get_application, -}; +use app::{app_configs_match, check_app_result, compare_app_provider, get_application}; use beam_lib::reqwest::{self, Url}; use clap::{builder::Str, Parser}; use group::create_groups; -use provider::{compare_provider, generate_provider_values, get_provider, provider_configs_match}; +use provider::{ + compare_provider, generate_provider_values, get_provider, get_provider_id, + provider_configs_match, +}; use reqwest::{Client, StatusCode}; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; @@ -93,11 +93,12 @@ impl FlowPropertymapping { pub async fn validate_application( name: &str, oidc_client_config: &OIDCConfig, + secret: &str, conf: &AuthentikConfig, client: &Client, ) -> anyhow::Result { let token = get_access_token(conf).await?; - compare_applications(&token, name, oidc_client_config, conf, client).await + compare_app_provider(&token, name, oidc_client_config, secret, conf, client).await } pub async fn create_app_provider( @@ -135,37 +136,66 @@ pub async fn combine_app_provider( dbg!(&provider_res); match provider_res.status() { StatusCode::CREATED => { - let pk: serde_json::Value = provider_res.json().await?; - let provider_pk = pk.get("pk").and_then(|v| v.as_i64()).unwrap(); + let res_provider: serde_json::Value = provider_res.json().await?; + let provider_pk = res_provider.get("pk").and_then(|v| v.as_i64()).unwrap(); + let provider_name = res_provider.get("name").and_then(|v| v.as_str()).unwrap(); debug!("{:?}", provider_pk); - info!("Provider for {name} created."); - check_app_result(token, name, provider_pk, oidc_client_config, conf, client).await?; - Ok(SecretResult::Created(secret)) + info!("Provider for {provider_name} created."); + if check_app_result(token, name, provider_pk, oidc_client_config, conf, client).await? { + Ok(SecretResult::Created(secret)) + } else { + bail!( + "Unexpected Conflict {name} while overwriting authentik app. {:?}", + get_application(name, token, oidc_client_config, conf, client).await? + ); + } } StatusCode::BAD_REQUEST => { let conflicting_provider = get_provider(name, token, oidc_client_config, conf, client).await?; debug!("{:#?}", conflicting_provider); + + let app = conflicting_provider + .get("name") + .and_then(|v| v.as_str()) + .unwrap(); if compare_provider(token, name, oidc_client_config, conf, &secret, client).await? { - info!("Provider {name} existed."); - Ok(SecretResult::AlreadyExisted( + info!("Provider {app} existed."); + if check_app_result( + token, + name, conflicting_provider - .as_object() - .and_then(|o| o.get("client_secret")) - .and_then(Value::as_str) - .unwrap_or("") - .to_owned(), - )) + .get("pk") + .and_then(|v| v.as_i64()) + .unwrap(), + oidc_client_config, + conf, + client, + ) + .await? + { + Ok(SecretResult::AlreadyExisted( + conflicting_provider + .as_object() + .and_then(|o| o.get("client_secret")) + .and_then(Value::as_str) + .unwrap_or("") + .to_owned(), + )) + } else { + bail!( + "Unexpected Conflict {name} while overwriting authentik app. {:?}", + get_application(name, token, oidc_client_config, conf, client).await? + ); + } } else { - Ok(client - .put( - conf.authentik_url.join("api/v3/providers/oauth2/")?.join( - conflicting_provider - .get("pk") - .and_then(Value::as_str) - .expect("We have a valid client"), - )?, - ) + let res = client + .patch(conf.authentik_url.join(&format!( + "api/v3/providers/oauth2/{}/", + get_provider_id(name, token, oidc_client_config, conf, client) + .await + .unwrap() + ))?) .bearer_auth(token) .json(&generated_provider) .send() @@ -173,7 +203,28 @@ pub async fn combine_app_provider( .status() .is_success() .then_some(SecretResult::AlreadyExisted(secret)) - .expect("We know the provider already exists so updating should be successful")) + .expect("We know the provider already exists so updating should be successful"); + info!("Provider {app} updated"); + if check_app_result( + token, + name, + conflicting_provider + .get("pk") + .and_then(|v| v.as_i64()) + .unwrap(), + oidc_client_config, + conf, + client, + ) + .await? + { + Ok(res) + } else { + bail!( + "Unexpected Conflict {name} while overwriting authentik app. {:?}", + get_application(name, token, oidc_client_config, conf, client).await? + ); + } } } s => bail!( diff --git a/central/src/auth/authentik/provider.rs b/central/src/auth/authentik/provider.rs index 32eee33..d7c998a 100644 --- a/central/src/auth/authentik/provider.rs +++ b/central/src/auth/authentik/provider.rs @@ -62,15 +62,33 @@ pub async fn generate_provider_values( Ok(json) } -async fn get_provider_id( - target_url: &Url, +pub async fn get_provider_id( + name: &str, token: &str, - search_key: &str, + oidc_client_config: &OIDCConfig, + conf: &AuthentikConfig, client: &Client, ) -> Option { + let id = format!( + "{name}-{}", + if oidc_client_config.is_public { + "public" + } else { + "private" + } + ); + + //let provider_search = "api/v3/providers/all/?ordering=name&page=1&page_size=20&search="; + let base_url = conf.authentik_url.join("api/v3/providers/all/").unwrap(); + let query_url = Url::parse_with_params( + base_url.as_str(), + &[("ordering", "name"), ("page", "1"), ("page_size", "20")], + ) + .unwrap(); + let target_value: serde_json::Value = client - .get(target_url.to_owned()) - .query(&[("search", search_key)]) + .get(query_url.to_owned()) + .query(&[("search", &id)]) .bearer_auth(token) .send() .await @@ -78,7 +96,7 @@ async fn get_provider_id( .json() .await .ok()?; - debug!("Value search key {search_key}: {:?}", &target_value); + debug!("Value search key {id}: {:?}", &target_value); // pk is the uuid for this result target_value .as_object() @@ -97,23 +115,7 @@ pub async fn get_provider( conf: &AuthentikConfig, client: &Client, ) -> anyhow::Result { - let id = format!( - "{name}-{}", - if oidc_client_config.is_public { - "public" - } else { - "private" - } - ); - //let provider_search = "api/v3/providers/all/?ordering=name&page=1&page_size=20&search="; - let base_url = conf.authentik_url.join("api/v3/providers/all/").unwrap(); - let query_url = Url::parse_with_params( - base_url.as_str(), - &[("ordering", "name"), ("page", "1"), ("page_size", "20")], - ) - .unwrap(); - - let res = get_provider_id(&query_url, token, &id, client).await; + let res = get_provider_id(name, token, oidc_client_config, conf, client).await; debug!(res); let pk = res.unwrap(); let mut base_url = conf @@ -151,10 +153,7 @@ pub async fn compare_provider( generate_provider_values(name, oidc_client_config, secret, conf, token).await?; debug!("{:#?}", client); debug!("{:#?}", wanted_client); - Ok( - client.get("client_secret") == wanted_client.get("client_secret") - && provider_configs_match(&client, &wanted_client), - ) + Ok(provider_configs_match(&client, &wanted_client)) } pub fn provider_configs_match(a: &Value, b: &Value) -> bool { diff --git a/central/src/auth/authentik/test.rs b/central/src/auth/authentik/test.rs index a45619d..bdf22bc 100644 --- a/central/src/auth/authentik/test.rs +++ b/central/src/auth/authentik/test.rs @@ -1,10 +1,7 @@ use crate::auth::authentik::app::generate_app_values; use crate::auth::authentik::group::{create_groups, post_group}; use crate::auth::authentik::provider::get_provider; -use crate::auth::authentik::{ - app_configs_match, combine_app_provider, compare_applications, get_application, get_uuid, - AuthentikConfig, -}; +use crate::auth::authentik::{combine_app_provider, get_application, get_uuid, AuthentikConfig}; use crate::{get_beamclient, CLIENT}; use beam_lib::reqwest::{self, Error, StatusCode, Url}; use serde::{Deserialize, Serialize}; @@ -98,7 +95,7 @@ async fn get_access_token_via_admin_login() -> reqwest::Result { #[tokio::test] async fn test_create_client() -> anyhow::Result<()> { let (token, conf) = setup_authentik()?; - let name = "pipe"; + let name = "runner"; // public client let client_config = OIDCConfig { is_public: true, @@ -114,18 +111,6 @@ async fn test_create_client() -> anyhow::Result<()> { .get("pk") .and_then(|v| v.as_i64()) .unwrap(); - let c = dbg!( - get_application(name, &token, &client_config, &conf, &get_beamclient()) - .await - .unwrap() - ); - assert!(app_configs_match( - &c, - &generate_app_values(provider_pk, name, &client_config) - )); - assert!(dbg!( - compare_applications(&token, name, &client_config, &conf, &get_beamclient()).await? - )); // private client let client_config = OIDCConfig { @@ -137,18 +122,6 @@ async fn test_create_client() -> anyhow::Result<()> { else { panic!("Not created or existed") }; - let c = dbg!( - get_application(name, &token, &client_config, &conf, &get_beamclient()) - .await - .unwrap() - ); - assert!(app_configs_match( - &c, - &generate_app_values(provider_pk, name, &client_config) - )); - assert!(dbg!( - compare_applications(&token, name, &client_config, &conf, &get_beamclient()).await? - )); Ok(()) } diff --git a/central/src/auth/config.rs b/central/src/auth/config.rs index 5348a02..1c95ffc 100644 --- a/central/src/auth/config.rs +++ b/central/src/auth/config.rs @@ -84,14 +84,18 @@ impl OIDCProvider { "Failed to validate client. See upstrean logs.".into() }) } - OIDCProvider::Authentik(conf) => { - authentik::validate_application(name, oidc_client_config, conf, &get_beamclient()) - .await - .map_err(|e| { - eprintln!("Failed to validate client {name}: {e}"); - "Failed to validate client. See upstrean logs.".into() - }) - } + OIDCProvider::Authentik(conf) => authentik::validate_application( + name, + oidc_client_config, + secret, + conf, + &get_beamclient(), + ) + .await + .map_err(|e| { + eprintln!("Failed to validate client {name}: {e}"); + "Failed to validate client. See upstrean logs.".into() + }), } } } From 2046bf7fec9fe57f877c1cb6c576fa552a1bd222 Mon Sep 17 00:00:00 2001 From: Martin Jurk Date: Mon, 2 Dec 2024 15:52:42 +0100 Subject: [PATCH 27/37] successful test bug redirect_is --- .cargo/config.toml | 2 +- central/src/auth/authentik/mod.rs | 10 +++++-- central/src/auth/authentik/provider.rs | 36 +++++++++++++++++++++----- central/src/auth/authentik/test.rs | 2 +- central/src/auth/config.rs | 1 + 5 files changed, 40 insertions(+), 11 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 138538a..d5a9d2f 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,3 @@ [env] -AUTHENTIK_TOKEN = "DW1CVJavj3IYFN3ivrsXj1zsNvFSob6MSCTk04FpH2WaqqJm7FSy5N5beAi6" +AUTHENTIK_TOKEN = "fkURHQo04Pi9SVpyBUN6ZjsahTjWAPX6jyva3oonQO94bSlmKQG37SGUmW2u" RUST_LOG = "debug" diff --git a/central/src/auth/authentik/mod.rs b/central/src/auth/authentik/mod.rs index 114d1bc..c4c64e0 100644 --- a/central/src/auth/authentik/mod.rs +++ b/central/src/auth/authentik/mod.rs @@ -43,7 +43,8 @@ impl FlowPropertymapping { if let Some(flow) = PROPERTY_MAPPING_CACHE.lock().unwrap().as_ref() { return Ok(flow.clone()); } - let flow_key = "authorization_flow"; + let flow_auth = "authorization_flow"; + let flow_invalidation = "default-provider-invalidation-flow"; let property_keys = vec![ "web-origins", "acr", @@ -78,11 +79,16 @@ impl FlowPropertymapping { .unwrap(); let property_mapping = get_property_mappings_uuids(&query_url, token, property_keys).await; - let authorization_flow = get_uuid(&flow_url, token, flow_key, &get_beamclient()) + let authorization_flow = get_uuid(&flow_url, token, flow_auth, &get_beamclient()) .await .expect("No default flow present"); // flow uuid + let invalidation_flow = get_uuid(&flow_url, token, flow_invalidation, &get_beamclient()) + .await + .expect("No default flow present"); // flow uuid + let mapping = FlowPropertymapping { authorization_flow, + invalidation_flow, property_mapping, }; *PROPERTY_MAPPING_CACHE.lock().unwrap() = Some(mapping.clone()); diff --git a/central/src/auth/authentik/provider.rs b/central/src/auth/authentik/provider.rs index d7c998a..fdbac8b 100644 --- a/central/src/auth/authentik/provider.rs +++ b/central/src/auth/authentik/provider.rs @@ -2,14 +2,21 @@ use std::i64; use anyhow::{Context, Ok}; use reqwest::{Client, Response, StatusCode, Url}; +use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use shared::{OIDCConfig, SecretResult}; -use tracing::{debug, field::debug}; +use tracing::debug; use crate::auth::config::FlowPropertymapping; use super::{get_uuid, AuthentikConfig}; +#[derive(Debug, Serialize, Deserialize)] +struct RedirectURIS { + matching_mode: String, + url: String, +} + pub async fn generate_provider_values( name: &str, oidc_client_config: &OIDCConfig, @@ -33,6 +40,7 @@ pub async fn generate_provider_values( "name": id, "client_id": id, "authorization_flow": mapping.authorization_flow, + "invalidation_flow": mapping.invalidation_flow, "property_mappings": [ mapping.property_mapping.get("web-origins"), mapping.property_mapping.get("acr"), @@ -41,10 +49,23 @@ pub async fn generate_provider_values( mapping.property_mapping.get("email"), mapping.property_mapping.get("microprofile-jwt"), mapping.property_mapping.get("groups") - ], - "redirect_uris": oidc_client_config.redirect_urls.first().unwrap(), + ] }); + if !oidc_client_config.redirect_urls.is_empty() { + let mut res_urls: Vec = vec![]; + for url in &oidc_client_config.redirect_urls { + res_urls.push(RedirectURIS { + matching_mode: "strict".to_owned(), + url: url.to_owned(), + }); + } + + json.as_object_mut() + .unwrap() + .insert("redirect_uris".to_owned(), json!(res_urls)); + } + if oidc_client_config.is_public { json.as_object_mut() .unwrap() @@ -116,7 +137,7 @@ pub async fn get_provider( client: &Client, ) -> anyhow::Result { let res = get_provider_id(name, token, oidc_client_config, conf, client).await; - debug!(res); + debug!("id {:?}", res); let pk = res.unwrap(); let mut base_url = conf .authentik_url @@ -129,9 +150,10 @@ pub async fn get_provider( } */ - client - .get(base_url) - .bearer_auth(token) + let cli = client.get(base_url); + debug!("cli {:?}", cli); + + cli.bearer_auth(token) .send() .await .context("No Response")? diff --git a/central/src/auth/authentik/test.rs b/central/src/auth/authentik/test.rs index bdf22bc..5b2e8c4 100644 --- a/central/src/auth/authentik/test.rs +++ b/central/src/auth/authentik/test.rs @@ -95,7 +95,7 @@ async fn get_access_token_via_admin_login() -> reqwest::Result { #[tokio::test] async fn test_create_client() -> anyhow::Result<()> { let (token, conf) = setup_authentik()?; - let name = "runner"; + let name = "tourtle"; // public client let client_config = OIDCConfig { is_public: true, diff --git a/central/src/auth/config.rs b/central/src/auth/config.rs index 1c95ffc..2397981 100644 --- a/central/src/auth/config.rs +++ b/central/src/auth/config.rs @@ -103,5 +103,6 @@ impl OIDCProvider { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct FlowPropertymapping { pub authorization_flow: String, + pub invalidation_flow: String, pub property_mapping: HashMap, } From 08b660c47abbdccfc53301aa2ba79c9ca74dbe72 Mon Sep 17 00:00:00 2001 From: Martin Jurk Date: Wed, 4 Dec 2024 17:34:58 +0100 Subject: [PATCH 28/37] provider and app match fn --- .cargo/config.toml | 2 +- central/src/auth/authentik/app.rs | 75 +++++--------------------- central/src/auth/authentik/provider.rs | 42 ++++++++------- central/src/auth/authentik/test.rs | 36 ++++++++++--- 4 files changed, 69 insertions(+), 86 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index d5a9d2f..db1f9e3 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,3 @@ [env] -AUTHENTIK_TOKEN = "fkURHQo04Pi9SVpyBUN6ZjsahTjWAPX6jyva3oonQO94bSlmKQG37SGUmW2u" +AUTHENTIK_TOKEN = "UJvoHJED630QAkhzqZyQKkKVjokI5w4XaT0MBPkCpTiHPlg9DZ9FwC4qmHRb" RUST_LOG = "debug" diff --git a/central/src/auth/authentik/app.rs b/central/src/auth/authentik/app.rs index 08340a5..4a4ffdb 100644 --- a/central/src/auth/authentik/app.rs +++ b/central/src/auth/authentik/app.rs @@ -11,7 +11,7 @@ use crate::auth::config::FlowPropertymapping; use super::{ get_uuid, - provider::{get_provider, get_provider_id}, + provider::{compare_provider, get_provider, get_provider_id, RedirectURIS}, AuthentikConfig, }; @@ -70,7 +70,7 @@ pub async fn check_app_result( info!("Application for {name} created."); Ok(true) } - StatusCode::CONFLICT => { + StatusCode::BAD_REQUEST => { let conflicting_client = get_application(name, token, oidc_client_config, conf, client).await?; if app_configs_match( @@ -141,71 +141,24 @@ pub async fn compare_app_provider( ) -> anyhow::Result { let provider_pk = get_provider_id(name, token, oidc_client_config, conf, client).await; match provider_pk { - Some(res) => { + Some(pr_id) => { let app_res = get_application(name, token, oidc_client_config, conf, client).await?; - let wanted_client = - generate_all_validation(name, token, conf, oidc_client_config).await?; - Ok(app_res.get("client_secret").unwrap() == secret - && app_configs_match(&app_res, &wanted_client)) + if app_configs_match( + &app_res, + &generate_app_values(pr_id, name, oidc_client_config), + ) { + return compare_provider(token, name, oidc_client_config, conf, secret, client) + .await; + } else { + return Ok(false); + } } None => Ok(false), } } -pub async fn generate_all_validation( - name: &str, - token: &str, - conf: &AuthentikConfig, - oidc_client_config: &OIDCConfig, -) -> anyhow::Result { - let mapping = FlowPropertymapping::new(conf, token).await?; - let id = format!( - "{name}-{}", - if oidc_client_config.is_public { - "public" - } else { - "private" - } - ); - let app_json = json!({ - "name": id, - "slug": id, - "provider_obj": { - "name": id, - "authorization_flow": mapping.authorization_flow, - "property_mappings": [ - mapping.property_mapping.get("web-origins"), - mapping.property_mapping.get("acr"), - mapping.property_mapping.get("profile"), - mapping.property_mapping.get("roles"), - mapping.property_mapping.get("email"), - mapping.property_mapping.get("microprofile-jwt"), - mapping.property_mapping.get("groups") - ], - "assigned_application_slug": id, - "assigned_application_name": id, - }, - "launch_url": oidc_client_config.redirect_urls.first().unwrap(), - "group": name - }); - - Ok(app_json) -} - pub fn app_configs_match(a: &Value, b: &Value) -> bool { - let includes_other_json_array = |key, comparator: &dyn Fn(_, _) -> bool| { - a.get(key) - .and_then(Value::as_array) - .is_some_and(|a_values| { - b.get(key) - .and_then(Value::as_array) - .is_some_and(|vec| vec.iter().all(|v| comparator(a_values, v))) - }) - }; a.get("name") == b.get("name") - && includes_other_json_array("authorization_flow", &|a_v, v| a_v.contains(v)) - && includes_other_json_array("redirectUris", &|a_v, v| a_v.contains(v)) - && includes_other_json_array("property_mappings", &|a_v, v| { - a_v.iter().any(|a_v| a_v.get("name") == v.get("name")) - }) + && a.get("group") == b.get("group") + && a.get("provider") == b.get("provider") } diff --git a/central/src/auth/authentik/provider.rs b/central/src/auth/authentik/provider.rs index fdbac8b..949bb18 100644 --- a/central/src/auth/authentik/provider.rs +++ b/central/src/auth/authentik/provider.rs @@ -1,20 +1,18 @@ -use std::i64; - use anyhow::{Context, Ok}; -use reqwest::{Client, Response, StatusCode, Url}; +use reqwest::{Client, Url}; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; -use shared::{OIDCConfig, SecretResult}; +use shared::OIDCConfig; use tracing::debug; use crate::auth::config::FlowPropertymapping; -use super::{get_uuid, AuthentikConfig}; +use super::AuthentikConfig; #[derive(Debug, Serialize, Deserialize)] -struct RedirectURIS { - matching_mode: String, - url: String, +pub struct RedirectURIS { + pub matching_mode: String, + pub url: String, } pub async fn generate_provider_values( @@ -137,18 +135,11 @@ pub async fn get_provider( client: &Client, ) -> anyhow::Result { let res = get_provider_id(name, token, oidc_client_config, conf, client).await; - debug!("id {:?}", res); - let pk = res.unwrap(); - let mut base_url = conf + let pk = res.ok_or_else(|| anyhow::anyhow!("Failed to get a provider id"))?; + let base_url = conf .authentik_url .join(&format!("api/v3/providers/oauth2/{pk}/")) .context("Error parsing provider")?; - /* - { - let mut provider_url = base_url.path_segments_mut().unwrap(); - provider_url.push(&pk); - } - */ let cli = client.get(base_url); debug!("cli {:?}", cli); @@ -188,8 +179,23 @@ pub fn provider_configs_match(a: &Value, b: &Value) -> bool { .is_some_and(|vec| vec.iter().all(|v| comparator(a_values, v))) }) }; + + let redirct_url_match = || { + let a_uris = a.get("redirect_uris").and_then(Value::as_array); + let b_uris = b.get("redirect_uris").and_then(Value::as_array); + match (a_uris, b_uris) { + (Some(a_uris), Some(b_uris)) => { + a_uris.iter().all(|auri| b_uris.contains(auri)) + && b_uris.iter().all(|buri| a_uris.contains(buri)) + } + (None, None) => true, + _ => false, + } + }; a.get("name") == b.get("name") + && a.get("client_secret") == b.get("client_secret") && a.get("authorization_flow") == b.get("authorization_flow") + && a.get("invalidation_flow") == b.get("invalidation_flow") && includes_other_json_array("property_mappings", &|a_v, v| a_v.contains(v)) - && a.get("redirect_uris") == b.get("redirect_uris") + && redirct_url_match() } diff --git a/central/src/auth/authentik/test.rs b/central/src/auth/authentik/test.rs index 5b2e8c4..c649ee7 100644 --- a/central/src/auth/authentik/test.rs +++ b/central/src/auth/authentik/test.rs @@ -1,7 +1,9 @@ use crate::auth::authentik::app::generate_app_values; use crate::auth::authentik::group::{create_groups, post_group}; use crate::auth::authentik::provider::get_provider; -use crate::auth::authentik::{combine_app_provider, get_application, get_uuid, AuthentikConfig}; +use crate::auth::authentik::{ + combine_app_provider, get_application, get_uuid, validate_application, AuthentikConfig, +}; use crate::{get_beamclient, CLIENT}; use beam_lib::reqwest::{self, Error, StatusCode, Url}; use serde::{Deserialize, Serialize}; @@ -95,11 +97,15 @@ async fn get_access_token_via_admin_login() -> reqwest::Result { #[tokio::test] async fn test_create_client() -> anyhow::Result<()> { let (token, conf) = setup_authentik()?; - let name = "tourtle"; + let name = "dark"; // public client let client_config = OIDCConfig { is_public: true, - redirect_urls: vec!["http://foo/bar".into()], + redirect_urls: vec![ + "http://foo/bar".into(), + "http://verbis/test".into(), + "http://dkfz/verbis/test".into(), + ], }; let (SecretResult::Created(pw) | SecretResult::AlreadyExisted(pw)) = dbg!(combine_app_provider(&token, name, &client_config, &conf, &get_beamclient()).await?) @@ -133,7 +139,7 @@ async fn group_test() -> anyhow::Result<()> { create_groups("next2", &token, &conf, &get_beamclient()).await } -//#[ignore = "Requires setting up a authentik"] +#[ignore = "Requires setting up a authentik"] #[tokio::test] async fn test_flow() { let (token, conf) = setup_authentik().expect("Cannot setup authentik as test"); @@ -158,7 +164,7 @@ async fn test_flow() { } } -//#[ignore = "Requires setting up a authentik"] +#[ignore = "Requires setting up a authentik"] #[tokio::test] async fn test_property() { let (token, conf) = setup_authentik().expect("Cannot setup authentik as test"); @@ -184,7 +190,7 @@ async fn test_property() { debug!("{:?}", res); } -//#[ignore = "Requires setting up a authentik"] +#[ignore = "Requires setting up a authentik"] #[tokio::test] async fn create_property() { let (token, conf) = setup_authentik().expect("Cannot setup authentik as test"); @@ -204,3 +210,21 @@ async fn create_property() { .expect("no response"); tracing::debug!("Result: {:#?}", res); } + +#[ignore = "Requires setting up a authentik"] +#[tokio::test] +async fn test_validate_client() -> anyhow::Result<()> { + let (token, conf) = setup_authentik()?; + let name = "dark"; + // public client + let client_config = OIDCConfig { + is_public: true, + redirect_urls: vec![ + "http://foo/bar".into(), + "http://verbis/test".into(), + "http://dkfz/verbis/test".into(), + ], + }; + validate_application(name, &client_config, "", &conf, &get_beamclient()); + Ok(()) +} From e59513cafc3c716a4f8b7ebb774d6c27210da02c Mon Sep 17 00:00:00 2001 From: Martin Jurk Date: Tue, 10 Dec 2024 08:54:37 +0100 Subject: [PATCH 29/37] test validate and update provider --- .cargo/config.toml | 2 +- central/src/auth/authentik/test.rs | 55 +++++++++++++++++++++++++++--- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index db1f9e3..c7caa18 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,3 @@ [env] -AUTHENTIK_TOKEN = "UJvoHJED630QAkhzqZyQKkKVjokI5w4XaT0MBPkCpTiHPlg9DZ9FwC4qmHRb" +AUTHENTIK_TOKEN = "54s6MbwtLexO1HKKBZsPL4cq8IQCoByDE012xWFlt1jAopiS0mgB2XE8lHOw" RUST_LOG = "debug" diff --git a/central/src/auth/authentik/test.rs b/central/src/auth/authentik/test.rs index c649ee7..9136ba5 100644 --- a/central/src/auth/authentik/test.rs +++ b/central/src/auth/authentik/test.rs @@ -1,6 +1,6 @@ -use crate::auth::authentik::app::generate_app_values; +use crate::auth::authentik::app::{check_app_result, compare_app_provider, generate_app_values}; use crate::auth::authentik::group::{create_groups, post_group}; -use crate::auth::authentik::provider::get_provider; +use crate::auth::authentik::provider::{generate_provider_values, get_provider, get_provider_id}; use crate::auth::authentik::{ combine_app_provider, get_application, get_uuid, validate_application, AuthentikConfig, }; @@ -97,7 +97,7 @@ async fn get_access_token_via_admin_login() -> reqwest::Result { #[tokio::test] async fn test_create_client() -> anyhow::Result<()> { let (token, conf) = setup_authentik()?; - let name = "dark"; + let name = "walker"; // public client let client_config = OIDCConfig { is_public: true, @@ -215,7 +215,7 @@ async fn create_property() { #[tokio::test] async fn test_validate_client() -> anyhow::Result<()> { let (token, conf) = setup_authentik()?; - let name = "dark"; + let name = "air"; // public client let client_config = OIDCConfig { is_public: true, @@ -225,6 +225,51 @@ async fn test_validate_client() -> anyhow::Result<()> { "http://dkfz/verbis/test".into(), ], }; - validate_application(name, &client_config, "", &conf, &get_beamclient()); + let res = + compare_app_provider(&token, name, &client_config, "", &conf, &get_beamclient()).await?; + debug!("Validate: {res}"); + Ok(()) +} + +#[ignore = "Requires setting up a authentik"] +#[tokio::test] +async fn test_patch_provider() -> anyhow::Result<()> { + let (token, conf) = setup_authentik()?; + let name = "dark"; + // public client + let client_config = OIDCConfig { + is_public: false, + redirect_urls: vec![ + "http://foo/bar".into(), + "http://verbis/test".into(), + "http://dkfz/verbis/test".into(), + ], + }; + let pk_id = get_provider_id(name, &token, &client_config, &conf, &get_beamclient()) + .await + .unwrap(); + let generated_provider = + generate_provider_values(name, &client_config, "", &conf, &token).await?; + debug!("{:#?}", generated_provider); + + let res = get_beamclient() + .patch( + conf.authentik_url + .join(&format!("api/v3/providers/oauth2/{}/", pk_id))?, + ) + .bearer_auth(&token) + .json(&generated_provider) + .send() + .await? + .status() + .is_success() + .then_some(SecretResult::AlreadyExisted("test".to_owned())) + .expect("We know the provider already exists so updating should be successful"); + debug!("Updated: {:#?}", res); + debug!("Provider {name} updated"); + debug!( + "App now: {:#?}", + get_application(name, &token, &client_config, &conf, &get_beamclient()).await? + ); Ok(()) } From beed17de205691cc0fe54e15a1ecedd9a436535e Mon Sep 17 00:00:00 2001 From: Martin Jurk Date: Tue, 10 Dec 2024 09:31:26 +0100 Subject: [PATCH 30/37] private uris fix and debug output --- central/src/auth/authentik/mod.rs | 4 ++-- central/src/auth/authentik/test.rs | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/central/src/auth/authentik/mod.rs b/central/src/auth/authentik/mod.rs index c4c64e0..e0e41f4 100644 --- a/central/src/auth/authentik/mod.rs +++ b/central/src/auth/authentik/mod.rs @@ -130,7 +130,7 @@ pub async fn combine_app_provider( }; let generated_provider = generate_provider_values(name, oidc_client_config, &secret, conf, token).await?; - debug!("{:#?}", generated_provider); + debug!("Provider Values: {:#?}", generated_provider); let provider_res = client .post(conf.authentik_url.join("api/v3/providers/oauth2/")?) .bearer_auth(token) @@ -139,7 +139,7 @@ pub async fn combine_app_provider( .await?; // Create groups for this client create_groups(name, token, conf, client).await?; - dbg!(&provider_res); + debug!("Result Provider: {:#?}", provider_res); match provider_res.status() { StatusCode::CREATED => { let res_provider: serde_json::Value = provider_res.json().await?; diff --git a/central/src/auth/authentik/test.rs b/central/src/auth/authentik/test.rs index 9136ba5..eba9f3f 100644 --- a/central/src/auth/authentik/test.rs +++ b/central/src/auth/authentik/test.rs @@ -97,7 +97,7 @@ async fn get_access_token_via_admin_login() -> reqwest::Result { #[tokio::test] async fn test_create_client() -> anyhow::Result<()> { let (token, conf) = setup_authentik()?; - let name = "walker"; + let name = "green"; // public client let client_config = OIDCConfig { is_public: true, @@ -121,7 +121,11 @@ async fn test_create_client() -> anyhow::Result<()> { // private client let client_config = OIDCConfig { is_public: false, - redirect_urls: vec!["http://foo/bar".into()], + redirect_urls: vec![ + "http://foo/bar".into(), + "http://verbis/test".into(), + "http://dkfz/verbis/test".into(), + ], }; let (SecretResult::Created(pw) | SecretResult::AlreadyExisted(pw)) = dbg!(combine_app_provider(&token, name, &client_config, &conf, &get_beamclient()).await?) From 10e0cd01b7fd2031b3fad3fa19f28accbc51e960 Mon Sep 17 00:00:00 2001 From: martinjurk Date: Tue, 10 Dec 2024 09:15:15 +0000 Subject: [PATCH 31/37] docker version authentik --- dev/authentik.yaml | 4 ++-- dev/test-authentik.yaml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dev/authentik.yaml b/dev/authentik.yaml index 5e637fb..ff04577 100644 --- a/dev/authentik.yaml +++ b/dev/authentik.yaml @@ -69,7 +69,7 @@ services: volumes: - redis:/data server: - image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.10.1} + image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.10.4} restart: unless-stopped command: server environment: @@ -90,7 +90,7 @@ services: - postgresql - redis worker: - image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.10.1} + image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.10.4} restart: unless-stopped command: worker environment: diff --git a/dev/test-authentik.yaml b/dev/test-authentik.yaml index 804a762..e0b4324 100644 --- a/dev/test-authentik.yaml +++ b/dev/test-authentik.yaml @@ -1,4 +1,4 @@ -version: "3" +--- services: postgresql: @@ -31,7 +31,7 @@ services: volumes: - redis:/data server: - image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.10.1} + image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.10.4} restart: unless-stopped command: server environment: @@ -52,7 +52,7 @@ services: - postgresql - redis worker: - image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.10.1} + image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.10.4} restart: unless-stopped command: worker environment: From 88cf53922b5eb8a3f7ecf771ebd89c9bdb16e343 Mon Sep 17 00:00:00 2001 From: Martin Jurk Date: Wed, 8 Jan 2025 10:21:59 +0100 Subject: [PATCH 32/37] removed test clipborard token copy --- central/Cargo.toml | 1 - central/src/auth/authentik/test.rs | 5 ----- 2 files changed, 6 deletions(-) diff --git a/central/Cargo.toml b/central/Cargo.toml index 73a5c3c..5e9dc89 100644 --- a/central/Cargo.toml +++ b/central/Cargo.toml @@ -20,4 +20,3 @@ reqwest = { version = "0.12", default_features = false, features = [ anyhow = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } -arboard = "3.4.1" diff --git a/central/src/auth/authentik/test.rs b/central/src/auth/authentik/test.rs index eba9f3f..6837c4b 100644 --- a/central/src/auth/authentik/test.rs +++ b/central/src/auth/authentik/test.rs @@ -10,7 +10,6 @@ use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use shared::{OIDCConfig, SecretResult}; -use arboard::Clipboard; use tracing::debug; use tracing::field::debug; #[derive(Deserialize, Serialize, Debug)] @@ -26,10 +25,6 @@ pub fn setup_authentik() -> reqwest::Result<(String, AuthentikConfig)> { .with_test_writer() .try_init(); let token = std::env::var("AUTHENTIK_TOKEN").expect("Missing ENV Authentik_Token"); - // copy from clipboard - //let mut clipboard = Clipboard::new().unwrap(); - //let t = clipboard.get_text().unwrap(); - //debug!("test: {:?}", t); Ok(( token, AuthentikConfig { From 1ae98d47317383696e3db4c1ff4285b61b6847b2 Mon Sep 17 00:00:00 2001 From: Martin Jurk Date: Wed, 8 Jan 2025 10:33:36 +0100 Subject: [PATCH 33/37] test fast shutdown not successfull --- central/src/main.rs | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/central/src/main.rs b/central/src/main.rs index a2f3a01..431ba13 100644 --- a/central/src/main.rs +++ b/central/src/main.rs @@ -58,38 +58,6 @@ async fn main() { } } } - /* - loop { - tokio::select! { - _ = shutdown_signal() => { - break; - }, - result = BEAM_CLIENT.poll_pending_tasks(&block_one) => { - match result { - Ok(tasks) => tasks.into_iter().for_each(|task| { - if !seen.contains(&task.id) { - seen.insert(task.id); - tokio::spawn(handle_task(task)); - } - }), - Err(beam_lib::BeamError::ReqwestError(e)) if e.is_connect() => { - eprintln!( - "Failed to connect to beam proxy on {}. Retrying in 30s", - CONFIG.beam_url - ); - tokio::time::sleep(Duration::from_secs(30)).await - } - Err(e) => { - eprintln!("Error during task polling {e}"); - tokio::time::sleep(Duration::from_secs(5)).await; - } - - } - - } - } - } - */ } pub async fn handle_task(task: TaskRequest>) { From 1253d9da6ca0f71ae12b26e84b5feeb5f6b2cf35 Mon Sep 17 00:00:00 2001 From: Martin Jurk Date: Wed, 8 Jan 2025 11:52:44 +0100 Subject: [PATCH 34/37] config is now in mod auth --- central/src/config.rs | 56 ------------------------------------------- central/src/main.rs | 32 ++++++++++++++++--------- 2 files changed, 21 insertions(+), 67 deletions(-) delete mode 100644 central/src/config.rs diff --git a/central/src/config.rs b/central/src/config.rs deleted file mode 100644 index 1a4f3f2..0000000 --- a/central/src/config.rs +++ /dev/null @@ -1,56 +0,0 @@ -use std::convert::Infallible; - -use beam_lib::{AppId, reqwest::Url}; -use clap::Parser; -use shared::{SecretResult, OIDCConfig}; - -use crate::keycloak::{KeyCloakConfig, self}; - -/// Central secret sync -#[derive(Debug, Parser)] -pub struct Config { - /// Url of the local beam proxy which is required to have sockets enabled - #[clap(env, long, default_value = "http://beam-proxy:8081")] - pub beam_url: Url, - - /// Beam api key - #[clap(env, long)] - pub beam_secret: String, - - /// The app id of this application - #[clap(long, env, value_parser=|id: &str| Ok::<_, Infallible>(AppId::new_unchecked(id)))] - pub beam_id: AppId, -} - -#[derive(Clone, Debug)] -pub enum OIDCProvider { - Keycloak(KeyCloakConfig) -} - -impl OIDCProvider { - pub fn try_init() -> Option { - KeyCloakConfig::try_parse().map_err(|e| println!("{e}")).ok().map(Self::Keycloak) - } - - pub async fn create_client(&self, name: &str, oidc_client_config: OIDCConfig) -> Result { - match self { - OIDCProvider::Keycloak(conf) => keycloak::create_client(name, oidc_client_config, conf).await, - }.map_err(|e| { - println!("Failed to create client: {e}"); - "Error creating OIDC client".into() - }) - } - - pub async fn validate_client(&self, name: &str, secret: &str, oidc_client_config: &OIDCConfig) -> Result { - match self { - OIDCProvider::Keycloak(conf) => { - keycloak::validate_client(name, oidc_client_config, secret, conf) - .await - .map_err(|e| { - eprintln!("Failed to validate client {name}: {e}"); - "Failed to validate client. See upstrean logs.".into() - }) - }, - } - } -} diff --git a/central/src/main.rs b/central/src/main.rs index 9673c5b..0b000d5 100644 --- a/central/src/main.rs +++ b/central/src/main.rs @@ -3,16 +3,13 @@ use std::{collections::HashSet, time::Duration}; use auth::config::{Config, OIDCProvider}; use beam_lib::{reqwest::Client, AppId, BeamClient, BlockingOptions, TaskRequest, TaskResult}; use clap::Parser; -use config::{Config, OIDCProvider}; use gitlab::GitLabProjectAccessTokenProvider; use once_cell::sync::Lazy; use shared::{SecretRequest, SecretRequestType, SecretResult}; -use tokio::time::sleep; use tracing::info; -mod config; +mod auth; mod gitlab; -mod keycloak; pub static CONFIG: Lazy = Lazy::new(Config::parse); @@ -40,7 +37,6 @@ async fn main() { // TODO: Remove once beam feature/stream-tasks is merged let mut seen = HashSet::new(); let block_one = BlockingOptions::from_count(1); - let retry_timer = sleep(Duration::from_secs(0)).fuse(); // TODO: Fast shutdown loop { match BEAM_CLIENT.poll_pending_tasks(&block_one).await { @@ -89,16 +85,26 @@ pub async fn handle_task(task: TaskRequest>) { } } -pub async fn handle_secret_task(task: SecretRequestType, from: &AppId) -> Result { +pub async fn handle_secret_task( + task: SecretRequestType, + from: &AppId, +) -> Result { println!("Working on secret task {task:?} from {from}"); match task { - SecretRequestType::ValidateOrCreate { current, request } if is_valid(¤t, &request, from).await? => Ok(SecretResult::AlreadyValid), - SecretRequestType::ValidateOrCreate { request, .. } | - SecretRequestType::Create(request) => create_secret(request, from).await, + SecretRequestType::ValidateOrCreate { current, request } + if is_valid(¤t, &request, from).await? => + { + Ok(SecretResult::AlreadyValid) + } + SecretRequestType::ValidateOrCreate { request, .. } + | SecretRequestType::Create(request) => create_secret(request, from).await, } } -pub async fn create_secret(request: SecretRequest, requester: &AppId) -> Result { +pub async fn create_secret( + request: SecretRequest, + requester: &AppId, +) -> Result { match request { SecretRequest::OpenIdConnect(oidc_client_config) => { let Some(oidc_provider) = OIDC_PROVIDER.as_ref() else { @@ -120,7 +126,11 @@ pub async fn create_secret(request: SecretRequest, requester: &AppId) -> Result< } } -pub async fn is_valid(secret: &str, request: &SecretRequest, requester: &AppId) -> Result { +pub async fn is_valid( + secret: &str, + request: &SecretRequest, + requester: &AppId, +) -> Result { match request { SecretRequest::OpenIdConnect(oidc_client_config) => { let Some(oidc_provider) = OIDC_PROVIDER.as_ref() else { From ad521ad3e51b810fefd2d280efea2cae5125f5e7 Mon Sep 17 00:00:00 2001 From: Martin Jurk Date: Wed, 8 Jan 2025 14:12:26 +0100 Subject: [PATCH 35/37] new version 2024.12 --- dev-keycloak/authentik.yaml | 4 ++-- dev-keycloak/test-authentik.yaml | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/dev-keycloak/authentik.yaml b/dev-keycloak/authentik.yaml index ff04577..ef867ca 100644 --- a/dev-keycloak/authentik.yaml +++ b/dev-keycloak/authentik.yaml @@ -69,7 +69,7 @@ services: volumes: - redis:/data server: - image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.10.4} + image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.12} restart: unless-stopped command: server environment: @@ -90,7 +90,7 @@ services: - postgresql - redis worker: - image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.10.4} + image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.12} restart: unless-stopped command: worker environment: diff --git a/dev-keycloak/test-authentik.yaml b/dev-keycloak/test-authentik.yaml index e0b4324..08e57f4 100644 --- a/dev-keycloak/test-authentik.yaml +++ b/dev-keycloak/test-authentik.yaml @@ -1,5 +1,4 @@ --- - services: postgresql: image: docker.io/library/postgres:16-alpine @@ -31,7 +30,7 @@ services: volumes: - redis:/data server: - image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.10.4} + image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.12} restart: unless-stopped command: server environment: @@ -52,7 +51,7 @@ services: - postgresql - redis worker: - image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.10.4} + image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.12} restart: unless-stopped command: worker environment: From dd9b4209b7b24d6527bd977538bd987f9ef55d9a Mon Sep 17 00:00:00 2001 From: Martin Jurk Date: Thu, 9 Jan 2025 16:08:31 +0100 Subject: [PATCH 36/37] code review fix --- .cargo/config.toml | 1 - Cargo.toml | 2 +- central/Cargo.toml | 6 +- central/src/auth/authentik/app.rs | 91 ++++++--------- central/src/auth/authentik/group.rs | 23 ++-- central/src/auth/authentik/mod.rs | 106 ++++++++---------- central/src/auth/authentik/provider.rs | 86 +++++--------- central/src/auth/authentik/test.rs | 81 +++++++------ central/src/auth/keycloak/client.rs | 10 +- central/src/auth/keycloak/mod.rs | 18 +-- central/src/auth/mod.rs | 19 +++- central/src/{auth => }/config.rs | 38 ++----- central/src/main.rs | 14 +-- {dev-keycloak => dev-auth}/authentik.yaml | 13 ++- .../docker-compose.yaml | 0 {dev-keycloak => dev-auth}/keycloak.yaml | 0 {dev-keycloak => dev-auth}/start.sh | 0 .../test-authentik.yaml | 0 dev-keycloak/.env | 3 - 19 files changed, 214 insertions(+), 297 deletions(-) rename central/src/{auth => }/config.rs (73%) rename {dev-keycloak => dev-auth}/authentik.yaml (93%) rename {dev-keycloak => dev-auth}/docker-compose.yaml (100%) rename {dev-keycloak => dev-auth}/keycloak.yaml (100%) rename {dev-keycloak => dev-auth}/start.sh (100%) rename {dev-keycloak => dev-auth}/test-authentik.yaml (100%) delete mode 100644 dev-keycloak/.env diff --git a/.cargo/config.toml b/.cargo/config.toml index c7caa18..a7ffaa7 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,2 @@ [env] -AUTHENTIK_TOKEN = "54s6MbwtLexO1HKKBZsPL4cq8IQCoByDE012xWFlt1jAopiS0mgB2XE8lHOw" RUST_LOG = "debug" diff --git a/Cargo.toml b/Cargo.toml index fe8fd4a..99d6f59 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ futures = "0.3" shared = { path = "./shared" } tracing = "0.1" tracing-subscriber = "0.3.0" -anyhow = "1.0.91" +anyhow = "1" [profile.release] #opt-level = "z" # Optimize for size. diff --git a/central/Cargo.toml b/central/Cargo.toml index 953f215..d4ecdd5 100644 --- a/central/Cargo.toml +++ b/central/Cargo.toml @@ -17,6 +17,8 @@ rand = "0.8" anyhow = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } -reqwest = { version = "0.12", default_features = false, features = ["default-tls"] } +reqwest = { version = "0.12", default_features = false, features = [ + "default-tls", +] } urlencoding = "2.1.3" -chrono = "0.4.39" +chrono = "0.4" diff --git a/central/src/auth/authentik/app.rs b/central/src/auth/authentik/app.rs index 4a4ffdb..256e22c 100644 --- a/central/src/auth/authentik/app.rs +++ b/central/src/auth/authentik/app.rs @@ -1,13 +1,11 @@ -use std::i64; - -use anyhow::Context; use beam_lib::reqwest::{self, Response, StatusCode}; use reqwest::{Client, Url}; use serde_json::{json, Value}; use shared::OIDCConfig; +use std::i64; use tracing::{debug, info}; -use crate::auth::config::FlowPropertymapping; +use crate::CLIENT; use super::{ get_uuid, @@ -15,35 +13,24 @@ use super::{ AuthentikConfig, }; -pub fn generate_app_values(provider: i64, name: &str, oidc_client_config: &OIDCConfig) -> Value { - let id = format!( - "{name}-{}", - if oidc_client_config.is_public { - "public" - } else { - "private" - } - ); +pub fn generate_app_values(provider: i64, client_id: &str) -> Value { json!({ - "name": id, - "slug": id, + "name": client_id, + "slug": client_id, "provider": provider, - "group": name + "group": client_id.split('-').next().expect("group name does not contain - ") }) } pub async fn generate_application( provider: i64, - name: &str, - oidc_client_config: &OIDCConfig, + client_id: &str, conf: &AuthentikConfig, token: &str, - client: &Client, ) -> reqwest::Result { - debug!(provider); - let app_value = generate_app_values(provider, name, oidc_client_config); + let app_value = generate_app_values(provider, client_id); debug!("{:#?}", app_value); - client + CLIENT .post( conf.authentik_url .join("api/v3/core/applications/") @@ -57,31 +44,27 @@ pub async fn generate_application( pub async fn check_app_result( token: &str, - name: &str, + client_id: &str, provider_pk: i64, - oidc_client_config: &OIDCConfig, conf: &AuthentikConfig, - client: &Client, ) -> anyhow::Result { - let res = - generate_application(provider_pk, name, oidc_client_config, conf, token, client).await?; + let res = generate_application(provider_pk, client_id, conf, token).await?; match res.status() { StatusCode::CREATED => { - info!("Application for {name} created."); + info!("Application for {client_id} created."); Ok(true) } StatusCode::BAD_REQUEST => { - let conflicting_client = - get_application(name, token, oidc_client_config, conf, client).await?; + let conflicting_client = get_application(client_id, token, conf).await?; if app_configs_match( &conflicting_client, - &generate_app_values(provider_pk, name, oidc_client_config), + &generate_app_values(provider_pk, client_id), ) { - info!("Application {name} exists."); + info!("Application {client_id} exists."); Ok(true) } else { - info!("Application for {name} is updated."); - Ok(client + info!("Application for {client_id} is updated."); + Ok(CLIENT .put( conf.authentik_url.join("api/v3/core/applicaions/")?.join( conflicting_client @@ -91,7 +74,7 @@ pub async fn check_app_result( )?, ) .bearer_auth(token) - .json(&generate_app_values(provider_pk, name, oidc_client_config)) + .json(&generate_app_values(provider_pk, client_id)) .send() .await? .status() @@ -103,24 +86,14 @@ pub async fn check_app_result( } pub async fn get_application( - name: &str, + client_id: &str, token: &str, - oidc_client_config: &OIDCConfig, conf: &AuthentikConfig, - client: &Client, ) -> reqwest::Result { - let id = format!( - "{name}-{}", - if oidc_client_config.is_public { - "public" - } else { - "private" - } - ); - client + CLIENT .get( conf.authentik_url - .join(&format!("api/v3/core/applications/{id}/")) + .join(&format!("api/v3/core/applications/{client_id}/")) .expect("Error parsing app url"), ) .bearer_auth(token) @@ -137,18 +110,22 @@ pub async fn compare_app_provider( oidc_client_config: &OIDCConfig, secret: &str, conf: &AuthentikConfig, - client: &Client, ) -> anyhow::Result { - let provider_pk = get_provider_id(name, token, oidc_client_config, conf, client).await; + let client_id = format!( + "{name}-{}", + if oidc_client_config.is_public { + "public" + } else { + "private" + } + ); + + let provider_pk = get_provider_id(&client_id, token, oidc_client_config, conf).await; match provider_pk { Some(pr_id) => { - let app_res = get_application(name, token, oidc_client_config, conf, client).await?; - if app_configs_match( - &app_res, - &generate_app_values(pr_id, name, oidc_client_config), - ) { - return compare_provider(token, name, oidc_client_config, conf, secret, client) - .await; + let app_res = get_application(&client_id, token, conf).await?; + if app_configs_match(&app_res, &generate_app_values(pr_id, &client_id)) { + return compare_provider(token, &client_id, oidc_client_config, conf, secret).await; } else { return Ok(false); } diff --git a/central/src/auth/authentik/group.rs b/central/src/auth/authentik/group.rs index 93888d1..f707dcc 100644 --- a/central/src/auth/authentik/group.rs +++ b/central/src/auth/authentik/group.rs @@ -1,16 +1,12 @@ -use beam_lib::reqwest::{self, StatusCode}; -use reqwest::Client; +use beam_lib::reqwest::StatusCode; use serde_json::json; use tracing::info; +use crate::CLIENT; + use super::AuthentikConfig; -pub async fn create_groups( - name: &str, - token: &str, - conf: &AuthentikConfig, - client: &Client, -) -> anyhow::Result<()> { +pub async fn create_groups(name: &str, token: &str, conf: &AuthentikConfig) -> anyhow::Result<()> { let capitalize = |s: &str| { let mut chrs = s.chars(); chrs.next() @@ -21,18 +17,13 @@ pub async fn create_groups( }; let name = capitalize(name); for group in &conf.authentik_groups_per_bh { - post_group(&group.replace('#', &name), token, conf, client).await?; + post_group(&group.replace('#', &name), token, conf).await?; } Ok(()) } -pub async fn post_group( - name: &str, - token: &str, - conf: &AuthentikConfig, - client: &Client, -) -> anyhow::Result<()> { - let res = client +pub async fn post_group(name: &str, token: &str, conf: &AuthentikConfig) -> anyhow::Result<()> { + let res = CLIENT .post(conf.authentik_url.join("api/v3/core/groups/")?) .bearer_auth(token) .json(&json!({ diff --git a/central/src/auth/authentik/mod.rs b/central/src/auth/authentik/mod.rs index e0e41f4..c4554ea 100644 --- a/central/src/auth/authentik/mod.rs +++ b/central/src/auth/authentik/mod.rs @@ -3,25 +3,21 @@ mod group; mod provider; mod test; +use crate::auth::generate_secret; use std::{collections::HashMap, sync::Mutex}; -use crate::get_beamclient; +use crate::CLIENT; use anyhow::bail; -use app::{app_configs_match, check_app_result, compare_app_provider, get_application}; +use app::{check_app_result, compare_app_provider, get_application}; use beam_lib::reqwest::{self, Url}; -use clap::{builder::Str, Parser}; +use clap::Parser; use group::create_groups; -use provider::{ - compare_provider, generate_provider_values, get_provider, get_provider_id, - provider_configs_match, -}; -use reqwest::{Client, StatusCode}; +use provider::{compare_provider, generate_provider_values, get_provider, get_provider_id}; +use reqwest::StatusCode; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use shared::{OIDCConfig, SecretResult}; -use tracing::{debug, field::debug, info}; - -use super::config::FlowPropertymapping; +use tracing::{debug, info}; #[derive(Debug, Parser, Clone)] pub struct AuthentikConfig { @@ -36,7 +32,12 @@ pub struct AuthentikConfig { pub authentik_groups_per_bh: Vec, } -// ctruct is in config +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct FlowPropertymapping { + pub authorization_flow: String, + pub invalidation_flow: String, + pub property_mapping: HashMap, +} impl FlowPropertymapping { async fn new(conf: &AuthentikConfig, token: &str) -> reqwest::Result { static PROPERTY_MAPPING_CACHE: Mutex> = Mutex::new(None); @@ -79,10 +80,10 @@ impl FlowPropertymapping { .unwrap(); let property_mapping = get_property_mappings_uuids(&query_url, token, property_keys).await; - let authorization_flow = get_uuid(&flow_url, token, flow_auth, &get_beamclient()) + let authorization_flow = get_uuid(&flow_url, token, flow_auth) .await .expect("No default flow present"); // flow uuid - let invalidation_flow = get_uuid(&flow_url, token, flow_invalidation, &get_beamclient()) + let invalidation_flow = get_uuid(&flow_url, token, flow_invalidation) .await .expect("No default flow present"); // flow uuid @@ -101,10 +102,9 @@ pub async fn validate_application( oidc_client_config: &OIDCConfig, secret: &str, conf: &AuthentikConfig, - client: &Client, ) -> anyhow::Result { let token = get_access_token(conf).await?; - compare_app_provider(&token, name, oidc_client_config, secret, conf, client).await + compare_app_provider(&token, name, oidc_client_config, secret, conf).await } pub async fn create_app_provider( @@ -113,7 +113,7 @@ pub async fn create_app_provider( conf: &AuthentikConfig, ) -> anyhow::Result { let token = get_access_token(conf).await?; - combine_app_provider(&token, name, &oidc_client_config, conf, &get_beamclient()).await + combine_app_provider(&token, name, &oidc_client_config, conf).await } pub async fn combine_app_provider( @@ -121,24 +121,32 @@ pub async fn combine_app_provider( name: &str, oidc_client_config: &OIDCConfig, conf: &AuthentikConfig, - client: &Client, ) -> anyhow::Result { + let client_id = format!( + "{name}-{}", + if oidc_client_config.is_public { + "public" + } else { + "private" + } + ); + let secret = if !oidc_client_config.is_public { generate_secret() } else { String::with_capacity(0) }; let generated_provider = - generate_provider_values(name, oidc_client_config, &secret, conf, token).await?; + generate_provider_values(&client_id, oidc_client_config, &secret, conf, token).await?; debug!("Provider Values: {:#?}", generated_provider); - let provider_res = client + let provider_res = CLIENT .post(conf.authentik_url.join("api/v3/providers/oauth2/")?) .bearer_auth(token) .json(&generated_provider) .send() .await?; // Create groups for this client - create_groups(name, token, conf, client).await?; + create_groups(name, token, conf).await?; debug!("Result Provider: {:#?}", provider_res); match provider_res.status() { StatusCode::CREATED => { @@ -147,36 +155,34 @@ pub async fn combine_app_provider( let provider_name = res_provider.get("name").and_then(|v| v.as_str()).unwrap(); debug!("{:?}", provider_pk); info!("Provider for {provider_name} created."); - if check_app_result(token, name, provider_pk, oidc_client_config, conf, client).await? { + if check_app_result(token, &client_id, provider_pk, conf).await? { Ok(SecretResult::Created(secret)) } else { bail!( "Unexpected Conflict {name} while overwriting authentik app. {:?}", - get_application(name, token, oidc_client_config, conf, client).await? + get_application(&client_id, token, conf).await? ); } } StatusCode::BAD_REQUEST => { let conflicting_provider = - get_provider(name, token, oidc_client_config, conf, client).await?; + get_provider(&client_id, token, oidc_client_config, conf).await?; debug!("{:#?}", conflicting_provider); let app = conflicting_provider .get("name") .and_then(|v| v.as_str()) .unwrap(); - if compare_provider(token, name, oidc_client_config, conf, &secret, client).await? { + if compare_provider(token, &client_id, oidc_client_config, conf, &secret).await? { info!("Provider {app} existed."); if check_app_result( token, - name, + &client_id, conflicting_provider .get("pk") .and_then(|v| v.as_i64()) - .unwrap(), - oidc_client_config, + .expect("pk id not found"), conf, - client, ) .await? { @@ -191,14 +197,14 @@ pub async fn combine_app_provider( } else { bail!( "Unexpected Conflict {name} while overwriting authentik app. {:?}", - get_application(name, token, oidc_client_config, conf, client).await? + get_application(&client_id, token, conf).await? ); } } else { - let res = client + let res = CLIENT .patch(conf.authentik_url.join(&format!( "api/v3/providers/oauth2/{}/", - get_provider_id(name, token, oidc_client_config, conf, client) + get_provider_id(&client_id, token, oidc_client_config, conf) .await .unwrap() ))?) @@ -213,14 +219,12 @@ pub async fn combine_app_provider( info!("Provider {app} updated"); if check_app_result( token, - name, + &client_id, conflicting_provider .get("pk") .and_then(|v| v.as_i64()) .unwrap(), - oidc_client_config, conf, - client, ) .await? { @@ -228,7 +232,7 @@ pub async fn combine_app_provider( } else { bail!( "Unexpected Conflict {name} while overwriting authentik app. {:?}", - get_application(name, token, oidc_client_config, conf, client).await? + get_application(&client_id, token, conf).await? ); } } @@ -239,13 +243,8 @@ pub async fn combine_app_provider( } } -async fn get_uuid( - target_url: &Url, - token: &str, - search_key: &str, - client: &Client, -) -> Option { - let target_value: serde_json::Value = client +async fn get_uuid(target_url: &Url, token: &str, search_key: &str) -> Option { + let target_value: serde_json::Value = CLIENT .get(target_url.to_owned()) .query(&[("search", search_key)]) .bearer_auth(token) @@ -273,11 +272,12 @@ async fn get_property_mappings_uuids( token: &str, search_key: Vec<&str>, ) -> HashMap { + // TODO: async iter to collect let mut result: HashMap = HashMap::new(); for key in search_key { result.insert( key.to_string(), - get_uuid(target_url, token, key, &get_beamclient()) + get_uuid(target_url, token, key) .await .expect(&format!("Property: {:?}", key)), ); @@ -285,28 +285,12 @@ async fn get_property_mappings_uuids( result } -fn generate_secret() -> String { - use rand::Rng; - const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\ - abcdefghijklmnopqrstuvwxyz\ - 0123456789"; - const PASSWORD_LEN: usize = 30; - let mut rng = rand::thread_rng(); - - (0..PASSWORD_LEN) - .map(|_| { - let idx = rng.gen_range(0..CHARSET.len()); - CHARSET[idx] as char - }) - .collect() -} - async fn get_access_token(conf: &AuthentikConfig) -> reqwest::Result { #[derive(Deserialize, Serialize, Debug)] struct Token { access_token: String, } - get_beamclient() + CLIENT .post( conf.authentik_url .join("application/o/token/") diff --git a/central/src/auth/authentik/provider.rs b/central/src/auth/authentik/provider.rs index 949bb18..5b07e61 100644 --- a/central/src/auth/authentik/provider.rs +++ b/central/src/auth/authentik/provider.rs @@ -1,13 +1,13 @@ use anyhow::{Context, Ok}; -use reqwest::{Client, Url}; +use reqwest::Url; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use shared::OIDCConfig; use tracing::debug; -use crate::auth::config::FlowPropertymapping; +use crate::CLIENT; -use super::AuthentikConfig; +use super::{AuthentikConfig, FlowPropertymapping}; #[derive(Debug, Serialize, Deserialize)] pub struct RedirectURIS { @@ -16,7 +16,7 @@ pub struct RedirectURIS { } pub async fn generate_provider_values( - name: &str, + client_id: &str, oidc_client_config: &OIDCConfig, secret: &str, conf: &AuthentikConfig, @@ -25,18 +25,10 @@ pub async fn generate_provider_values( let mapping = FlowPropertymapping::new(conf, token).await?; let secret = (!oidc_client_config.is_public).then_some(secret); - let id = format!( - "{name}-{}", - if oidc_client_config.is_public { - "public" - } else { - "private" - } - ); // only one redirect url is possible let mut json = json!({ - "name": id, - "client_id": id, + "name": client_id, + "client_id": client_id, "authorization_flow": mapping.authorization_flow, "invalidation_flow": mapping.invalidation_flow, "property_mappings": [ @@ -51,52 +43,34 @@ pub async fn generate_provider_values( }); if !oidc_client_config.redirect_urls.is_empty() { - let mut res_urls: Vec = vec![]; - for url in &oidc_client_config.redirect_urls { - res_urls.push(RedirectURIS { + let res_urls: Vec = oidc_client_config + .redirect_urls + .iter() + .map(|url| RedirectURIS { matching_mode: "strict".to_owned(), url: url.to_owned(), - }); - } - - json.as_object_mut() - .unwrap() - .insert("redirect_uris".to_owned(), json!(res_urls)); + }) + .collect(); + json["redirect_uris"] = json!(res_urls); } - if oidc_client_config.is_public { - json.as_object_mut() - .unwrap() - .insert("client_type".to_owned(), "public".into()); + json["client_type"] = if oidc_client_config.is_public { + json!("public") } else { - json.as_object_mut() - .unwrap() - .insert("client_type".to_owned(), "confidential".into()); - } + json!("confidential") + }; if let Some(secret) = secret { - json.as_object_mut() - .unwrap() - .insert("client_secret".to_owned(), secret.into()); + json["client_secret"] = json!(secret); } Ok(json) } pub async fn get_provider_id( - name: &str, + client_id: &str, token: &str, oidc_client_config: &OIDCConfig, conf: &AuthentikConfig, - client: &Client, ) -> Option { - let id = format!( - "{name}-{}", - if oidc_client_config.is_public { - "public" - } else { - "private" - } - ); - //let provider_search = "api/v3/providers/all/?ordering=name&page=1&page_size=20&search="; let base_url = conf.authentik_url.join("api/v3/providers/all/").unwrap(); let query_url = Url::parse_with_params( @@ -105,9 +79,9 @@ pub async fn get_provider_id( ) .unwrap(); - let target_value: serde_json::Value = client + let target_value: serde_json::Value = CLIENT .get(query_url.to_owned()) - .query(&[("search", &id)]) + .query(&[("search", &client_id)]) .bearer_auth(token) .send() .await @@ -115,7 +89,7 @@ pub async fn get_provider_id( .json() .await .ok()?; - debug!("Value search key {id}: {:?}", &target_value); + debug!("Value search key {client_id}: {:?}", &target_value); // pk is the uuid for this result target_value .as_object() @@ -128,20 +102,19 @@ pub async fn get_provider_id( } pub async fn get_provider( - name: &str, + client_id: &str, token: &str, oidc_client_config: &OIDCConfig, conf: &AuthentikConfig, - client: &Client, ) -> anyhow::Result { - let res = get_provider_id(name, token, oidc_client_config, conf, client).await; + let res = get_provider_id(client_id, token, oidc_client_config, conf).await; let pk = res.ok_or_else(|| anyhow::anyhow!("Failed to get a provider id"))?; let base_url = conf .authentik_url .join(&format!("api/v3/providers/oauth2/{pk}/")) .context("Error parsing provider")?; - - let cli = client.get(base_url); + // TODO: remove debug + let cli = CLIENT.get(base_url); debug!("cli {:?}", cli); cli.bearer_auth(token) @@ -155,15 +128,14 @@ pub async fn get_provider( pub async fn compare_provider( token: &str, - name: &str, + client_id: &str, oidc_client_config: &OIDCConfig, conf: &AuthentikConfig, secret: &str, - client: &Client, ) -> anyhow::Result { - let client = get_provider(name, token, oidc_client_config, conf, client).await?; + let client = get_provider(client_id, token, oidc_client_config, conf).await?; let wanted_client = - generate_provider_values(name, oidc_client_config, secret, conf, token).await?; + generate_provider_values(client_id, oidc_client_config, secret, conf, token).await?; debug!("{:#?}", client); debug!("{:#?}", wanted_client); Ok(provider_configs_match(&client, &wanted_client)) diff --git a/central/src/auth/authentik/test.rs b/central/src/auth/authentik/test.rs index 6837c4b..2d3ddf2 100644 --- a/central/src/auth/authentik/test.rs +++ b/central/src/auth/authentik/test.rs @@ -4,7 +4,7 @@ use crate::auth::authentik::provider::{generate_provider_values, get_provider, g use crate::auth::authentik::{ combine_app_provider, get_application, get_uuid, validate_application, AuthentikConfig, }; -use crate::{get_beamclient, CLIENT}; +use crate::CLIENT; use beam_lib::reqwest::{self, Error, StatusCode, Url}; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; @@ -24,6 +24,8 @@ pub fn setup_authentik() -> reqwest::Result<(String, AuthentikConfig)> { .with_max_level(tracing::Level::DEBUG) .with_test_writer() .try_init(); + //let token = ""; + // export AUTHENTIK_TOKEN= let token = std::env::var("AUTHENTIK_TOKEN").expect("Missing ENV Authentik_Token"); Ok(( token, @@ -45,8 +47,8 @@ async fn get_access_test() { .post(path_url) .form(&json!({ "grant_type": "client_credentials", - "client_id": "UtKuQ4Yh7xsPOqI8yRH86azKhEjSmrQMo2MyrvNi", - "client_secret": "wFfVgSj1w25xpIvpZGad0nLU1NglYUSYMpPyzhbptDPEGlLlaJ0lHStEN0HHuiHMtTlqMtJoMIa2Ye4psz8EBMLdliahsqYatgcmMEYPvTL3BK0bS1YLVzhhXbxgzVgi", + "client_id": "", + "client_secret": "", "scope": "openid" })) .send() @@ -76,10 +78,10 @@ async fn get_access_token_via_admin_login() -> reqwest::Result { } )) .form(&json!({ - "client_id": "admin-cli", - "username": "Merlin@frech.com", - "password": "MErlin", - "grant_type": "password" + "client_id": "", + "username": "", + "password": "", + "grant_type": "" })) .send() .await? @@ -92,7 +94,7 @@ async fn get_access_token_via_admin_login() -> reqwest::Result { #[tokio::test] async fn test_create_client() -> anyhow::Result<()> { let (token, conf) = setup_authentik()?; - let name = "green"; + let name = "tree"; // public client let client_config = OIDCConfig { is_public: true, @@ -103,11 +105,11 @@ async fn test_create_client() -> anyhow::Result<()> { ], }; let (SecretResult::Created(pw) | SecretResult::AlreadyExisted(pw)) = - dbg!(combine_app_provider(&token, name, &client_config, &conf, &get_beamclient()).await?) + dbg!(combine_app_provider(&token, name, &client_config, &conf).await?) else { panic!("Not created or existed") }; - let provider_pk = get_provider(name, &token, &client_config, &conf, &get_beamclient()) + let provider_pk = get_provider(name, &token, &client_config, &conf) .await? .get("pk") .and_then(|v| v.as_i64()) @@ -123,7 +125,7 @@ async fn test_create_client() -> anyhow::Result<()> { ], }; let (SecretResult::Created(pw) | SecretResult::AlreadyExisted(pw)) = - dbg!(combine_app_provider(&token, name, &client_config, &conf, &get_beamclient()).await?) + dbg!(combine_app_provider(&token, name, &client_config, &conf).await?) else { panic!("Not created or existed") }; @@ -135,7 +137,7 @@ async fn test_create_client() -> anyhow::Result<()> { #[tokio::test] async fn group_test() -> anyhow::Result<()> { let (token, conf) = setup_authentik()?; - create_groups("next2", &token, &conf, &get_beamclient()).await + create_groups("next2", &token, &conf).await } #[ignore = "Requires setting up a authentik"] @@ -150,7 +152,7 @@ async fn test_flow() { ) .unwrap(); //let flow_url = "api/v3/flows/instances/?ordering=slug&page=1&page_size=20&search="; - let res = get_uuid(&query_url, &token, test_key, &get_beamclient()).await; + let res = get_uuid(&query_url, &token, test_key).await; debug!(res); match res { Some(uuid) => { @@ -183,7 +185,7 @@ async fn test_property() { ) .unwrap(); //let flow_url = "api/v3/propertymappings/all/?managed__isnull=true&ordering=name&page=1&page_size=20&search="; - let res = get_uuid(&query_url, &token, test_key, &get_beamclient()).await; + let res = get_uuid(&query_url, &token, test_key).await; //debug!("Result Property for {test_key}: {:#?}", res); debug!("{:?}", query_url); debug!("{:?}", res); @@ -193,21 +195,33 @@ async fn test_property() { #[tokio::test] async fn create_property() { let (token, conf) = setup_authentik().expect("Cannot setup authentik as test"); - let acr = "not-needed2".to_owned(); - let ext = "return{}".to_owned(); - let json_property = json!({ - "name": acr, - "expression": ext - }); - let property_url = "api/v3/propertymappings/source/oauth/"; - let res = get_beamclient() - .post(conf.authentik_url.join(property_url).expect("No valid Url")) - .bearer_auth(token) - .json(&json_property) - .send() - .await - .expect("no response"); - tracing::debug!("Result: {:#?}", res); + // let flow_auth = "authorization_flow"; + // let flow_invalidation = "default-provider-invalidation-flow"; + let property_keys = vec![ + "web-origins", + "acr", + "profile", + "roles", + "email", + "microprofile-jwt", + "groups", + ]; + for key in property_keys { + let ext = "return{}".to_owned(); + let json_property = json!({ + "name": key, + "expression": ext + }); + let property_url = "api/v3/propertymappings/source/oauth/"; + let res = CLIENT + .post(conf.authentik_url.join(property_url).expect("No valid Url")) + .bearer_auth(&token) + .json(&json_property) + .send() + .await + .expect("no response"); + tracing::debug!("Result: {:#?}", res); + } } #[ignore = "Requires setting up a authentik"] @@ -224,8 +238,7 @@ async fn test_validate_client() -> anyhow::Result<()> { "http://dkfz/verbis/test".into(), ], }; - let res = - compare_app_provider(&token, name, &client_config, "", &conf, &get_beamclient()).await?; + let res = compare_app_provider(&token, name, &client_config, "", &conf).await?; debug!("Validate: {res}"); Ok(()) } @@ -244,14 +257,14 @@ async fn test_patch_provider() -> anyhow::Result<()> { "http://dkfz/verbis/test".into(), ], }; - let pk_id = get_provider_id(name, &token, &client_config, &conf, &get_beamclient()) + let pk_id = get_provider_id(name, &token, &client_config, &conf) .await .unwrap(); let generated_provider = generate_provider_values(name, &client_config, "", &conf, &token).await?; debug!("{:#?}", generated_provider); - let res = get_beamclient() + let res = CLIENT .patch( conf.authentik_url .join(&format!("api/v3/providers/oauth2/{}/", pk_id))?, @@ -268,7 +281,7 @@ async fn test_patch_provider() -> anyhow::Result<()> { debug!("Provider {name} updated"); debug!( "App now: {:#?}", - get_application(name, &token, &client_config, &conf, &get_beamclient()).await? + get_application(name, &token, &conf).await? ); Ok(()) } diff --git a/central/src/auth/keycloak/client.rs b/central/src/auth/keycloak/client.rs index 20c00cc..8ec53e3 100644 --- a/central/src/auth/keycloak/client.rs +++ b/central/src/auth/keycloak/client.rs @@ -1,11 +1,13 @@ -use crate::{auth::keycloak::add_service_account_roles, CLIENT}; +use crate::{ + auth::{generate_secret, keycloak::add_service_account_roles}, + CLIENT, +}; use anyhow::bail; -use beam_lib::reqwest::{self, StatusCode, Url}; -use clap::Parser; +use beam_lib::reqwest::{self, StatusCode}; use serde_json::{json, Value}; use shared::{OIDCConfig, SecretResult}; -use super::{create_groups, generate_secret, KeyCloakConfig}; +use super::{create_groups, KeyCloakConfig}; pub async fn get_client( name: &str, diff --git a/central/src/auth/keycloak/mod.rs b/central/src/auth/keycloak/mod.rs index 476a749..9325ff2 100644 --- a/central/src/auth/keycloak/mod.rs +++ b/central/src/auth/keycloak/mod.rs @@ -6,7 +6,7 @@ use anyhow::bail; use beam_lib::reqwest::{self, StatusCode, Url}; use clap::Parser; use client::{compare_clients, post_client}; -use serde_json::{json, Value}; +use serde_json::json; use shared::{OIDCConfig, SecretResult}; #[derive(Debug, Parser, Clone)] @@ -108,22 +108,6 @@ async fn post_group(name: &str, token: &str, conf: &KeyCloakConfig) -> anyhow::R Ok(()) } -fn generate_secret() -> String { - use rand::Rng; - const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\ - abcdefghijklmnopqrstuvwxyz\ - 0123456789"; - const PASSWORD_LEN: usize = 30; - let mut rng = rand::thread_rng(); - - (0..PASSWORD_LEN) - .map(|_| { - let idx = rng.gen_range(0..CHARSET.len()); - CHARSET[idx] as char - }) - .collect() -} - async fn add_service_account_roles( token: &str, client_id: &str, diff --git a/central/src/auth/mod.rs b/central/src/auth/mod.rs index 97c5a87..634f570 100644 --- a/central/src/auth/mod.rs +++ b/central/src/auth/mod.rs @@ -1,3 +1,18 @@ pub mod authentik; -pub(crate) mod config; -mod keycloak; +pub mod keycloak; + +pub fn generate_secret() -> String { + use rand::Rng; + const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\ + abcdefghijklmnopqrstuvwxyz\ + 0123456789"; + const PASSWORD_LEN: usize = 30; + let mut rng = rand::thread_rng(); + + (0..PASSWORD_LEN) + .map(|_| { + let idx = rng.gen_range(0..CHARSET.len()); + CHARSET[idx] as char + }) + .collect() +} diff --git a/central/src/auth/config.rs b/central/src/config.rs similarity index 73% rename from central/src/auth/config.rs rename to central/src/config.rs index 2397981..bf8d323 100644 --- a/central/src/auth/config.rs +++ b/central/src/config.rs @@ -1,17 +1,14 @@ -use std::{collections::HashMap, convert::Infallible, net::SocketAddr}; +use std::{convert::Infallible, net::SocketAddr}; use beam_lib::{reqwest::Url, AppId}; use clap::Parser; -use serde::{Deserialize, Serialize}; use shared::{OIDCConfig, SecretResult}; -use crate::{ - auth::keycloak::{self, KeyCloakConfig}, - get_beamclient, +use crate::auth::{ + authentik::{self, AuthentikConfig}, + keycloak::{self, KeyCloakConfig}, }; -use super::authentik::{self, AuthentikConfig}; - /// Central secret sync #[derive(Debug, Parser)] pub struct Config { @@ -84,25 +81,14 @@ impl OIDCProvider { "Failed to validate client. See upstrean logs.".into() }) } - OIDCProvider::Authentik(conf) => authentik::validate_application( - name, - oidc_client_config, - secret, - conf, - &get_beamclient(), - ) - .await - .map_err(|e| { - eprintln!("Failed to validate client {name}: {e}"); - "Failed to validate client. See upstrean logs.".into() - }), + OIDCProvider::Authentik(conf) => { + authentik::validate_application(name, oidc_client_config, secret, conf) + .await + .map_err(|e| { + eprintln!("Failed to validate client {name}: {e}"); + "Failed to validate client. See upstrean logs.".into() + }) + } } } } - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct FlowPropertymapping { - pub authorization_flow: String, - pub invalidation_flow: String, - pub property_mapping: HashMap, -} diff --git a/central/src/main.rs b/central/src/main.rs index 0b000d5..44f1183 100644 --- a/central/src/main.rs +++ b/central/src/main.rs @@ -1,8 +1,8 @@ use std::{collections::HashSet, time::Duration}; -use auth::config::{Config, OIDCProvider}; use beam_lib::{reqwest::Client, AppId, BeamClient, BlockingOptions, TaskRequest, TaskResult}; use clap::Parser; +use config::{Config, OIDCProvider}; use gitlab::GitLabProjectAccessTokenProvider; use once_cell::sync::Lazy; use shared::{SecretRequest, SecretRequestType, SecretResult}; @@ -11,6 +11,7 @@ use tracing::info; mod auth; mod gitlab; +pub(crate) mod config; pub static CONFIG: Lazy = Lazy::new(Config::parse); pub static BEAM_CLIENT: Lazy = Lazy::new(|| { @@ -21,10 +22,6 @@ pub static BEAM_CLIENT: Lazy = Lazy::new(|| { ) }); -pub fn get_beamclient() -> Client { - Client::new() -} - pub static OIDC_PROVIDER: Lazy> = Lazy::new(OIDCProvider::try_init); pub static GITLAB_PROJECT_ACCESS_TOKEN_PROVIDER: Lazy> = Lazy::new(GitLabProjectAccessTokenProvider::try_init); @@ -153,10 +150,3 @@ pub async fn is_valid( } } } - -async fn shutdown_signal() { - tokio::signal::ctrl_c() - .await - .expect("Expect shutdown signal handler"); - info!("Shutdown recieved"); -} diff --git a/dev-keycloak/authentik.yaml b/dev-auth/authentik.yaml similarity index 93% rename from dev-keycloak/authentik.yaml rename to dev-auth/authentik.yaml index ef867ca..2eb65aa 100644 --- a/dev-keycloak/authentik.yaml +++ b/dev-auth/authentik.yaml @@ -54,8 +54,9 @@ services: POSTGRES_PASSWORD: ${PG_PASS:?database password required} POSTGRES_USER: ${PG_USER:-authentik} POSTGRES_DB: ${PG_DB:-authentik} - env_file: - - .env + PG_PASS: admin + AUTHENTIK_SECRET_KEY: admin + AUTHENTIK_LOG_LEVEL: trace redis: image: docker.io/library/redis:alpine command: --save 60 1 --loglevel warning @@ -78,11 +79,12 @@ services: AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik} AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik} AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS} + PG_PASS: admin + AUTHENTIK_SECRET_KEY: admin + AUTHENTIK_LOG_LEVEL: trace volumes: - ./media:/media - ./custom-templates:/templates - env_file: - - .env ports: - "${COMPOSE_PORT_HTTP:-9000}:9000" - "${COMPOSE_PORT_HTTPS:-9443}:9443" @@ -99,6 +101,9 @@ services: AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik} AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik} AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS} + PG_PASS: admin + AUTHENTIK_SECRET_KEY: admin + AUTHENTIK_LOG_LEVEL: trace # `user: root` and the docker socket volume are optional. # See more for the docker socket integration here: # https://goauthentik.io/docs/outposts/integrations/docker diff --git a/dev-keycloak/docker-compose.yaml b/dev-auth/docker-compose.yaml similarity index 100% rename from dev-keycloak/docker-compose.yaml rename to dev-auth/docker-compose.yaml diff --git a/dev-keycloak/keycloak.yaml b/dev-auth/keycloak.yaml similarity index 100% rename from dev-keycloak/keycloak.yaml rename to dev-auth/keycloak.yaml diff --git a/dev-keycloak/start.sh b/dev-auth/start.sh similarity index 100% rename from dev-keycloak/start.sh rename to dev-auth/start.sh diff --git a/dev-keycloak/test-authentik.yaml b/dev-auth/test-authentik.yaml similarity index 100% rename from dev-keycloak/test-authentik.yaml rename to dev-auth/test-authentik.yaml diff --git a/dev-keycloak/.env b/dev-keycloak/.env deleted file mode 100644 index b8929b2..0000000 --- a/dev-keycloak/.env +++ /dev/null @@ -1,3 +0,0 @@ -PG_PASS=NoEWSQ1aKMLh9PPk5rGHT4nIXUBYsTkdgoe4eHBDYAL/wIUL -AUTHENTIK_SECRET_KEY=zbkMDG2a4KzYBQyQv2AyBxSFOCffdzYpHvddj4+TWFRFGDSQfZSbvqBOgBaYVWyeUChjWCQqjkt+vScP -AUTHENTIK_LOG_LEVEL=trace From 3b64520cba19a2db74b7a330566efd27e6cddd63 Mon Sep 17 00:00:00 2001 From: Martin Jurk Date: Thu, 9 Jan 2025 16:25:22 +0100 Subject: [PATCH 37/37] param deleted --- central/src/auth/authentik/app.rs | 6 +++--- central/src/auth/authentik/mod.rs | 8 +++----- central/src/auth/authentik/provider.rs | 17 +++++------------ central/src/auth/authentik/test.rs | 4 +--- 4 files changed, 12 insertions(+), 23 deletions(-) diff --git a/central/src/auth/authentik/app.rs b/central/src/auth/authentik/app.rs index 256e22c..06ae270 100644 --- a/central/src/auth/authentik/app.rs +++ b/central/src/auth/authentik/app.rs @@ -120,14 +120,14 @@ pub async fn compare_app_provider( } ); - let provider_pk = get_provider_id(&client_id, token, oidc_client_config, conf).await; + let provider_pk = get_provider_id(&client_id, token, conf).await; match provider_pk { Some(pr_id) => { let app_res = get_application(&client_id, token, conf).await?; if app_configs_match(&app_res, &generate_app_values(pr_id, &client_id)) { - return compare_provider(token, &client_id, oidc_client_config, conf, secret).await; + compare_provider(token, &client_id, oidc_client_config, conf, secret).await } else { - return Ok(false); + Ok(false) } } None => Ok(false), diff --git a/central/src/auth/authentik/mod.rs b/central/src/auth/authentik/mod.rs index c4554ea..c7e2f6c 100644 --- a/central/src/auth/authentik/mod.rs +++ b/central/src/auth/authentik/mod.rs @@ -203,11 +203,9 @@ pub async fn combine_app_provider( } else { let res = CLIENT .patch(conf.authentik_url.join(&format!( - "api/v3/providers/oauth2/{}/", - get_provider_id(&client_id, token, oidc_client_config, conf) - .await - .unwrap() - ))?) + "api/v3/providers/oauth2/{}/", + get_provider_id(&client_id, token, conf).await.unwrap() + ))?) .bearer_auth(token) .json(&generated_provider) .send() diff --git a/central/src/auth/authentik/provider.rs b/central/src/auth/authentik/provider.rs index 5b07e61..cd2bd0d 100644 --- a/central/src/auth/authentik/provider.rs +++ b/central/src/auth/authentik/provider.rs @@ -65,12 +65,7 @@ pub async fn generate_provider_values( Ok(json) } -pub async fn get_provider_id( - client_id: &str, - token: &str, - oidc_client_config: &OIDCConfig, - conf: &AuthentikConfig, -) -> Option { +pub async fn get_provider_id(client_id: &str, token: &str, conf: &AuthentikConfig) -> Option { //let provider_search = "api/v3/providers/all/?ordering=name&page=1&page_size=20&search="; let base_url = conf.authentik_url.join("api/v3/providers/all/").unwrap(); let query_url = Url::parse_with_params( @@ -107,17 +102,15 @@ pub async fn get_provider( oidc_client_config: &OIDCConfig, conf: &AuthentikConfig, ) -> anyhow::Result { - let res = get_provider_id(client_id, token, oidc_client_config, conf).await; + let res = get_provider_id(client_id, token, conf).await; let pk = res.ok_or_else(|| anyhow::anyhow!("Failed to get a provider id"))?; let base_url = conf .authentik_url .join(&format!("api/v3/providers/oauth2/{pk}/")) .context("Error parsing provider")?; - // TODO: remove debug - let cli = CLIENT.get(base_url); - debug!("cli {:?}", cli); - - cli.bearer_auth(token) + CLIENT + .get(base_url) + .bearer_auth(token) .send() .await .context("No Response")? diff --git a/central/src/auth/authentik/test.rs b/central/src/auth/authentik/test.rs index 2d3ddf2..ae3323e 100644 --- a/central/src/auth/authentik/test.rs +++ b/central/src/auth/authentik/test.rs @@ -257,9 +257,7 @@ async fn test_patch_provider() -> anyhow::Result<()> { "http://dkfz/verbis/test".into(), ], }; - let pk_id = get_provider_id(name, &token, &client_config, &conf) - .await - .unwrap(); + let pk_id = get_provider_id(name, &token, &conf).await.unwrap(); let generated_provider = generate_provider_values(name, &client_config, "", &conf, &token).await?; debug!("{:#?}", generated_provider);