Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion crates/stackable-operator/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Fixed

- Fixed URL handling related to OIDC and `rootPath` with and without trailing slashes. Also added a bunch of tests ([#910]).

### Changed

- BREAKING: Made `DEFAULT_OIDC_WELLKNOWN_PATH` private. Use `AuthenticationProvider::well_known_url` instead ([#910]).

[#910]: https://github.com/stackabletech/operator-rs/pull/910

## [0.81.0] - 2024-11-05

### Added
Expand All @@ -12,7 +22,7 @@ All notable changes to this project will be documented in this file.

### Changed

- BREAKING: Split `ListenerClass.spec.preferred_address_type` into a new `PreferredAddressType` type. Use `resolve_preferred_address_type()` to access the `AddressType` as before. ([#903])
- BREAKING: Split `ListenerClass.spec.preferred_address_type` into a new `PreferredAddressType` type. Use `resolve_preferred_address_type()` to access the `AddressType` as before ([#903]).

[#903]: https://github.com/stackabletech/operator-rs/pull/903

Expand Down
114 changes: 107 additions & 7 deletions crates/stackable-operator/src/commons/authentication/oidc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,27 @@ use crate::commons::{

pub type Result<T, E = Error> = std::result::Result<T, E>;

pub const DEFAULT_OIDC_WELLKNOWN_PATH: &str = ".well-known/openid-configuration";
pub const CLIENT_ID_SECRET_KEY: &str = "clientId";
pub const CLIENT_SECRET_SECRET_KEY: &str = "clientSecret";

const DEFAULT_OIDC_WELLKNOWN_PATH: &str = "./.well-known/openid-configuration";

#[derive(Debug, PartialEq, Snafu)]
pub enum Error {
#[snafu(display("failed to parse OIDC endpoint url"))]
ParseOidcEndpointUrl { source: ParseError },

#[snafu(display(
"failed to set OIDC endpoint scheme '{scheme}' for endpoint url '{endpoint}'"
"failed to set OIDC endpoint scheme '{scheme}' for endpoint url \"{endpoint}\""
))]
SetOidcEndpointScheme { endpoint: Url, scheme: String },

#[snafu(display("failed to join the path {path:?} to URL \"{url}\""))]
JoinPath {
source: ParseError,
url: Url,
path: String,
},
}

/// This struct contains configuration values to configure an OpenID Connect
Expand Down Expand Up @@ -111,10 +119,8 @@ impl AuthenticationProvider {
}
}

/// Returns the OIDC endpoint [`Url`]. To append the default OIDC well-known
/// configuration path, use `url.join()`. This module provides the default
/// path at [`DEFAULT_OIDC_WELLKNOWN_PATH`].
pub fn endpoint_url(&self) -> Result<Url> {
/// Base [`Url`] without any path set (so only protocol, host, port and such).
fn base_url(&self) -> Result<Url> {
let mut url = Url::parse(&format!(
"http://{host}:{port}",
host = self.hostname.as_url_host(),
Expand All @@ -132,10 +138,38 @@ impl AuthenticationProvider {
})?;
}

url.set_path(&self.root_path);
Ok(url)
}

/// Returns the OIDC endpoint [`Url`] without a trailing slash.
///
/// To get the well-known URL, please use [`Self::well_known_url`].
pub fn endpoint_url(&self) -> Result<Url> {
let mut url = self.base_url()?;
// Some tools can not cope with a trailing slash, so let's remove that
url.set_path(self.root_path.trim_end_matches('/'));
Ok(url)
}

/// Returns the well-known [`Url`] without a trailing slash.
///
/// It is basically the [`Self::endpoint_url`] joined with
/// "./.well-known/openid-configuration", while watching out for URL joining madness.
pub fn well_known_url(&self) -> Result<Url> {
let mut url = self.base_url()?;

// Url::join cuts of the part after the last slash :/
// So we need to make sure we have a trailing slash, so that nothing get's cut of.
let mut root_path_with_trailing_slash = self.root_path.trim_end_matches('/').to_string();
root_path_with_trailing_slash.push('/');
url.set_path(&root_path_with_trailing_slash);
url.join(DEFAULT_OIDC_WELLKNOWN_PATH)
.with_context(|_| JoinPathSnafu {
url: url.clone(),
path: DEFAULT_OIDC_WELLKNOWN_PATH.to_owned(),
})
}

/// Returns the port to be used, which is either user configured or defaulted based upon TLS usage
pub fn port(&self) -> u16 {
self.port
Expand Down Expand Up @@ -246,6 +280,8 @@ pub struct ClientAuthenticationOptions<T = ()> {

#[cfg(test)]
mod test {
use rstest::rstest;

use super::*;

#[test]
Expand Down Expand Up @@ -338,6 +374,70 @@ mod test {
);
}

#[rstest]
#[case("/", "http://my.keycloak.server:1234/")]
#[case("/realms/sdp", "http://my.keycloak.server:1234/realms/sdp")]
#[case("/realms/sdp/", "http://my.keycloak.server:1234/realms/sdp")]
#[case("/realms/sdp//////", "http://my.keycloak.server:1234/realms/sdp")]
#[case(
"/realms/my/realm/with/slashes//////",
"http://my.keycloak.server:1234/realms/my/realm/with/slashes"
)]
fn root_path_endpoint_url(#[case] root_path: String, #[case] expected_endpoint_url: &str) {
let oidc = serde_yaml::from_str::<AuthenticationProvider>(&format!(
"
hostname: my.keycloak.server
port: 1234
rootPath: {root_path}
scopes: [openid]
principalClaim: preferred_username
"
))
.unwrap();

assert_eq!(oidc.endpoint_url().unwrap().as_str(), expected_endpoint_url);
}

#[rstest]
#[case("/", "https://my.keycloak.server/.well-known/openid-configuration")]
#[case(
"/realms/sdp",
"https://my.keycloak.server/realms/sdp/.well-known/openid-configuration"
)]
#[case(
"/realms/sdp/",
"https://my.keycloak.server/realms/sdp/.well-known/openid-configuration"
)]
#[case(
"/realms/sdp//////",
"https://my.keycloak.server/realms/sdp/.well-known/openid-configuration"
)]
#[case(
"/realms/my/realm/with/slashes//////",
"https://my.keycloak.server/realms/my/realm/with/slashes/.well-known/openid-configuration"
)]
fn root_path_well_known_url(#[case] root_path: String, #[case] expected_well_known_url: &str) {
let oidc = serde_yaml::from_str::<AuthenticationProvider>(&format!(
"
hostname: my.keycloak.server
rootPath: {root_path}
scopes: [openid]
principalClaim: preferred_username
tls:
verification:
server:
caCert:
webPki: {{}}
"
))
.unwrap();

assert_eq!(
oidc.well_known_url().unwrap().as_str(),
expected_well_known_url
);
}

#[test]
fn client_env_vars() {
let secret_name = "my-keycloak-client";
Expand Down
Loading