1use async_trait::async_trait;
2use serde::{Deserialize, Serialize};
3
4use crate::{types::*, KeycloakError};
5
6mod generated_rest;
7mod url_enc;
8
9pub struct KeycloakAdmin<TS: KeycloakTokenSupplier = KeycloakAdminToken> {
10 url: String,
11 client: reqwest::Client,
12 token_supplier: TS,
13}
14
15#[async_trait]
16pub trait KeycloakTokenSupplier {
17 async fn get(&self, url: &str) -> Result<String, KeycloakError>;
18}
19
20#[derive(Clone)]
21pub struct KeycloakServiceAccountAdminTokenRetriever {
22 client_id: String,
23 client_secret: String,
24 realm: String,
25 reqwest_client: reqwest::Client,
26}
27
28#[async_trait]
29impl KeycloakTokenSupplier for KeycloakServiceAccountAdminTokenRetriever {
30 async fn get(&self, url: &str) -> Result<String, KeycloakError> {
31 let admin_token = self.acquire(url).await?;
32 Ok(admin_token.access_token)
33 }
34}
35
36impl KeycloakServiceAccountAdminTokenRetriever {
37 pub fn create(client_id: &str, client_secret: &str, client: reqwest::Client) -> Self {
45 Self {
46 client_id: client_id.into(),
47 client_secret: client_secret.into(),
48 realm: "master".into(),
49 reqwest_client: client,
50 }
51 }
52
53 pub fn create_with_custom_realm(
54 client_id: &str,
55 client_secret: &str,
56 realm: &str,
57 client: reqwest::Client,
58 ) -> Self {
59 Self {
60 client_id: client_id.into(),
61 client_secret: client_secret.into(),
62 realm: realm.into(),
63 reqwest_client: client,
64 }
65 }
66
67 pub async fn acquire(&self, url: &str) -> Result<KeycloakAdminToken, KeycloakError> {
68 let realm = &self.realm;
69 let response = self
70 .reqwest_client
71 .post(format!(
72 "{url}/realms/{realm}/protocol/openid-connect/token",
73 ))
74 .form(&[
75 ("client_id", self.client_id.as_str()),
76 ("client_secret", self.client_secret.as_str()),
77 ("grant_type", "client_credentials"),
78 ])
79 .send()
80 .await?;
81 Ok(error_check(response).await?.json().await?)
82 }
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
86pub struct KeycloakAdminToken {
87 access_token: String,
88 expires_in: usize,
89 #[serde(rename = "not-before-policy")]
90 not_before_policy: Option<usize>,
91 refresh_expires_in: Option<usize>,
92 refresh_token: Option<String>,
93 scope: String,
94 session_state: Option<String>,
95 token_type: String,
96}
97
98#[async_trait]
99impl KeycloakTokenSupplier for KeycloakAdminToken {
100 async fn get(&self, _url: &str) -> Result<String, KeycloakError> {
101 Ok(self.access_token.clone())
102 }
103}
104
105impl KeycloakAdminToken {
106 pub async fn acquire(
107 url: &str,
108 username: &str,
109 password: &str,
110 client: &reqwest::Client,
111 ) -> Result<KeycloakAdminToken, KeycloakError> {
112 Self::acquire_custom_realm(
113 url,
114 username,
115 password,
116 "master",
117 "admin-cli",
118 "password",
119 client,
120 )
121 .await
122 }
123
124 pub async fn acquire_custom_realm(
125 url: &str,
126 username: &str,
127 password: &str,
128 realm: &str,
129 client_id: &str,
130 grant_type: &str,
131 client: &reqwest::Client,
132 ) -> Result<KeycloakAdminToken, KeycloakError> {
133 let response = client
134 .post(format!(
135 "{url}/realms/{realm}/protocol/openid-connect/token",
136 ))
137 .form(&[
138 ("username", username),
139 ("password", password),
140 ("client_id", client_id),
141 ("grant_type", grant_type),
142 ])
143 .send()
144 .await?;
145 Ok(error_check(response).await?.json().await?)
146 }
147}
148
149async fn error_check(response: reqwest::Response) -> Result<reqwest::Response, KeycloakError> {
150 if !response.status().is_success() {
151 let status = response.status().into();
152 let text = response.text().await?;
153 return Err(KeycloakError::HttpFailure {
154 status,
155 body: serde_json::from_str(&text).ok(),
156 text,
157 });
158 }
159
160 Ok(response)
161}
162
163fn to_id(response: reqwest::Response) -> Option<TypeString> {
164 response
165 .headers()
166 .get(reqwest::header::LOCATION)
167 .and_then(|v| v.to_str().ok())
168 .and_then(|v| v.split('/').last())
169 .map(From::from)
170}
171
172impl<TS: KeycloakTokenSupplier> KeycloakAdmin<TS> {
173 pub fn new(url: &str, token_supplier: TS, client: reqwest::Client) -> Self {
174 Self {
175 url: url.into(),
176 client,
177 token_supplier,
178 }
179 }
180}