1use std::{fmt, future::Future, pin::Pin, time::Duration};
4
5use krabka_client_admin::{AdminClient, AdminError};
6use krabka_client_core::security::{ClientSecurity, SaslCredentials};
7use krabka_security::SaslMechanism;
8use krabka_units::prelude::*;
9use serde::{Deserialize, Serialize};
10
11use crate::{
12 admin::AdminFacade,
13 config::AdminUiConfig,
14 error::UiError,
15 permissions::{Capabilities, derive_capabilities},
16 session::{SessionCredentials, SessionStore, SessionUser},
17};
18
19const BROKER_PROBE_TIMEOUT: Time = secs(2);
22
23#[derive(Clone, PartialEq, Eq, Deserialize)]
24pub struct LoginRequest {
25 pub username: String,
26 pub password: String,
27}
28
29#[derive(Clone, PartialEq, Eq, Serialize)]
30pub struct LoginSuccess {
31 pub username: String,
32 pub principal: String,
33 pub session_id: String,
34}
35
36impl fmt::Debug for LoginSuccess {
37 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
38 formatter
39 .debug_struct("LoginSuccess")
40 .field("username", &self.username)
41 .field("principal", &self.principal)
42 .field("session_id", &"<redacted>")
43 .finish()
44 }
45}
46
47#[must_use]
48pub fn build_scram_sha512_security(
49 cfg: &AdminUiConfig,
50 username: &str,
51 password: &str,
52) -> ClientSecurity {
53 ClientSecurity {
54 protocol: cfg.security.listener_protocol(),
55 tls: cfg.security.tls(),
56 sasl: Some(SaslCredentials::Scram {
57 mechanism: SaslMechanism::ScramSha512,
58 username: username.to_string(),
59 password: password.to_string(),
60 delegation_token: false,
61 }),
62 sasl_host: None,
63 }
64}
65
66#[derive(Debug, Clone, Copy, Default)]
67pub struct AdminClientLoginBroker;
68
69pub trait LoginBroker {
70 fn authenticate<'a>(
76 &'a self,
77 cfg: &'a AdminUiConfig,
78 username: &'a str,
79 password: &'a str,
80 ) -> Pin<Box<dyn Future<Output = Result<Capabilities, UiError>> + Send + 'a>>;
81}
82
83impl LoginBroker for AdminClientLoginBroker {
84 fn authenticate<'a>(
85 &'a self,
86 cfg: &'a AdminUiConfig,
87 username: &'a str,
88 password: &'a str,
89 ) -> Pin<Box<dyn Future<Output = Result<Capabilities, UiError>> + Send + 'a>> {
90 Box::pin(async move {
91 let security = build_scram_sha512_security(cfg, username, password);
92 let client =
93 match AdminClient::connect_secured(&cfg.bootstrap_addrs, Some(security)).await {
94 Ok(client) => client,
95 Err(error) => return Err(classify_connect_failure(cfg, error).await),
96 };
97
98 Ok(capabilities_from_broker(client, username).await)
99 })
100 }
101}
102
103impl<T: LoginBroker + ?Sized> LoginBroker for &T {
104 fn authenticate<'a>(
105 &'a self,
106 cfg: &'a AdminUiConfig,
107 username: &'a str,
108 password: &'a str,
109 ) -> Pin<Box<dyn Future<Output = Result<Capabilities, UiError>> + Send + 'a>> {
110 (*self).authenticate(cfg, username, password)
111 }
112}
113
114async fn capabilities_from_broker(client: AdminClient, username: &str) -> Capabilities {
122 let mut facade = AdminFacade::new(client);
123 let principal = format!("User:{username}");
124
125 let Ok(entries) = facade.acl_entries().await else {
126 return Capabilities::all();
127 };
128 let derived = derive_capabilities(&principal, &entries);
129
130 if derived == Capabilities::none() {
131 return Capabilities::all();
132 }
133
134 derived
135}
136
137async fn classify_connect_failure(cfg: &AdminUiConfig, error: AdminError) -> UiError {
145 if !matches!(error, AdminError::Connect { .. }) {
146 return UiError::from(error);
147 }
148
149 if any_address_accepts_tcp(&cfg.bootstrap_addrs, BROKER_PROBE_TIMEOUT).await {
150 return UiError::NotAuthenticated;
151 }
152
153 UiError::from(error)
154}
155
156pub async fn any_address_accepts_tcp(addrs: &[String], timeout: Time) -> bool {
159 let deadline = Duration::try_from_secs_f64(timeout.secs_f64()).unwrap_or(Duration::ZERO);
160
161 for addr in addrs {
162 if let Ok(Ok(stream)) =
163 tokio::time::timeout(deadline, tokio::net::TcpStream::connect(addr.as_str())).await
164 {
165 drop(stream);
166 return true;
167 }
168 }
169
170 false
171}
172
173pub struct AuthService<'a, B = AdminClientLoginBroker> {
174 cfg: &'a AdminUiConfig,
175 sessions: &'a SessionStore,
176 broker: B,
177}
178
179impl<'a> AuthService<'a, AdminClientLoginBroker> {
180 #[must_use]
181 pub const fn new(cfg: &'a AdminUiConfig, sessions: &'a SessionStore) -> Self {
182 Self {
183 cfg,
184 sessions,
185 broker: AdminClientLoginBroker,
186 }
187 }
188}
189
190impl<'a, B: LoginBroker> AuthService<'a, B> {
191 #[must_use]
192 pub const fn new_with_broker(
193 cfg: &'a AdminUiConfig,
194 sessions: &'a SessionStore,
195 broker: B,
196 ) -> Self {
197 Self {
198 cfg,
199 sessions,
200 broker,
201 }
202 }
203
204 pub async fn login(&self, request: LoginRequest) -> Result<LoginSuccess, UiError> {
207 let capabilities = self
208 .broker
209 .authenticate(self.cfg, &request.username, &request.password)
210 .await?;
211
212 let principal = format!("User:{}", request.username);
213 let session_id = self.sessions.create_authenticated(
214 SessionUser {
215 username: request.username.clone(),
216 principal: principal.clone(),
217 },
218 SessionCredentials::scram_sha512(request.password),
219 capabilities,
220 );
221
222 Ok(LoginSuccess {
223 username: request.username,
224 principal,
225 session_id: session_id.expose_for_cookie().to_string(),
226 })
227 }
228}