Skip to main content

opentalk_keycloak_admin/
lib.rs

1// SPDX-FileCopyrightText: OpenTalk GmbH <mail@opentalk.eu>
2//
3// SPDX-License-Identifier: EUPL-1.2
4
5use ::reqwest::{Client, RequestBuilder, Response};
6use snafu::Snafu;
7use url::Url;
8
9mod authorized_client;
10pub mod reqwest;
11
12pub mod users;
13
14pub type Result<T, E = Error> = std::result::Result<T, E>;
15
16pub use authorized_client::AuthorizedClient;
17
18#[derive(Debug, Snafu)]
19pub enum Error {
20    #[snafu(display("Reqwest error: {}", source), context(false))]
21    Reqwest { source: ::reqwest::Error },
22    #[snafu(display("Failed to serialize body: {source}\n {body}"))]
23    InvalidBody {
24        body: String,
25        source: serde_json::Error,
26    },
27    #[snafu(display("Keycloak error response: {status} - {response}"))]
28    KeyCloak { status: u16, response: String },
29    #[snafu(display("Invalid credentials"))]
30    InvalidCredentials,
31    #[snafu(display("Given base URL is not a base: {}", url))]
32    NotBaseUrl { url: Url },
33    #[snafu(display("The OIDC metadata couldn't be fetched from this URL: {}", url))]
34    OidcMetadataError { url: Url },
35}
36
37impl Error {
38    /// Generate an [Error] from a keycloak response.
39    async fn from_keycloak_response(response: Response, message: &str) -> Self {
40        let status = response.status();
41        let error_response = response
42            .text()
43            .await
44            .unwrap_or_else(|_| "<failed to receive body>".to_string());
45        log::error!(
46            "{} (status: {}, response: '{}'",
47            message,
48            status.as_u16(),
49            error_response,
50        );
51        Self::KeyCloak {
52            status: status.as_u16(),
53            response: error_response,
54        }
55    }
56}
57
58/// HTTP client to access some Keycloak admin APIs
59pub struct KeycloakAdminClient {
60    api_base_url: Url,
61
62    authorized_client: AuthorizedClient,
63
64    dump_failed_responses: bool,
65}
66
67impl KeycloakAdminClient {
68    /// Create a new client from all required configurations
69    pub fn new(api_base_url: Url, authorized_client: AuthorizedClient) -> Result<Self, Error> {
70        Self::with_dump_flag(api_base_url, authorized_client, false)
71    }
72
73    pub fn with_dump_flag(
74        api_base_url: Url,
75        authorized_client: AuthorizedClient,
76        dump_failed_responses: bool,
77    ) -> Result<Self, Error> {
78        if api_base_url.cannot_be_a_base() {
79            return Err(Error::NotBaseUrl { url: api_base_url });
80        }
81
82        Ok(Self {
83            api_base_url,
84            authorized_client,
85            dump_failed_responses,
86        })
87    }
88
89    fn api_url<I>(&self, path_segments: I) -> Result<Url>
90    where
91        I: IntoIterator,
92        I::Item: AsRef<str>,
93    {
94        build_url(self.api_base_url.clone(), path_segments)
95    }
96
97    async fn send_authorized(&self, f: impl Fn(&Client) -> RequestBuilder) -> Result<Response> {
98        self.authorized_client.send_authorized(f).await
99    }
100}
101
102/// internal url builder
103fn build_url<I>(base_url: Url, path_segments: I) -> Result<Url>
104where
105    I: IntoIterator,
106    I::Item: AsRef<str>,
107{
108    let err_url = base_url.clone();
109    let mut url = base_url;
110    url.path_segments_mut()
111        .map_err(|_| Error::NotBaseUrl { url: err_url })?
112        .extend(path_segments);
113    Ok(url)
114}