kanidmd_core/https/extractors/
mod.rs

1use crate::https::ServerState;
2use axum::{
3    async_trait,
4    extract::{connect_info::Connected, FromRequestParts},
5    http::{header::AUTHORIZATION as AUTHORISATION, request::Parts, StatusCode},
6};
7use axum_extra::extract::cookie::CookieJar;
8use compact_jwt::JwsCompact;
9use kanidm_proto::internal::COOKIE_BEARER_TOKEN;
10use kanidmd_lib::prelude::{ClientAuthInfo, ClientCertInfo, Source};
11use std::net::{IpAddr, SocketAddr};
12use std::str::FromStr;
13
14// Re-export
15pub use kanidmd_lib::idm::server::DomainInfoRead;
16
17pub struct VerifiedClientInformation(pub ClientAuthInfo);
18
19#[async_trait]
20impl FromRequestParts<ServerState> for VerifiedClientInformation {
21    type Rejection = (StatusCode, &'static str);
22
23    // Need to skip all to prevent leaking tokens to logs.
24    #[instrument(level = "debug", skip_all)]
25    async fn from_request_parts(
26        parts: &mut Parts,
27        state: &ServerState,
28    ) -> Result<Self, Self::Rejection> {
29        let ClientConnInfo {
30            connection_addr: _,
31            client_ip_addr,
32            client_cert,
33        } = parts.extensions.remove::<ClientConnInfo>().ok_or((
34            StatusCode::INTERNAL_SERVER_ERROR,
35            "request info contains invalid data",
36        ))?;
37
38        let (basic_authz, bearer_token) = if let Some(header) = parts.headers.get(AUTHORISATION) {
39            if let Some((authz_type, authz_data)) = header
40                .to_str()
41                .map_err(|err| {
42                    warn!(?err, "Invalid authz header, ignoring");
43                })
44                .ok()
45                .and_then(|s| s.split_once(' '))
46            {
47                let authz_type = authz_type.to_lowercase();
48
49                if authz_type == "basic" {
50                    (Some(authz_data.to_string()), None)
51                } else if authz_type == "bearer" {
52                    if let Ok(jwsc) = JwsCompact::from_str(authz_data) {
53                        (None, Some(jwsc))
54                    } else {
55                        warn!("bearer jws invalid");
56                        (None, None)
57                    }
58                } else {
59                    warn!("authorisation header invalid, ignoring");
60                    (None, None)
61                }
62            } else {
63                (None, None)
64            }
65        } else {
66            // Only if there are no credentials in bearer, do we examine cookies.
67            let jar = CookieJar::from_headers(&parts.headers);
68
69            let value: Option<&str> = jar.get(COOKIE_BEARER_TOKEN).map(|c| c.value());
70
71            let maybe_bearer = value.and_then(|authz_data| JwsCompact::from_str(authz_data).ok());
72
73            (None, maybe_bearer)
74        };
75
76        let mut client_auth_info = ClientAuthInfo::new(
77            Source::Https(client_ip_addr),
78            client_cert,
79            bearer_token,
80            basic_authz,
81        );
82
83        // now, we want to update the client auth info with the sessions user-auth-token
84        // if any. We ignore errors here as the auth info MAY NOT be a valid token
85        // and so in that case no prevalidation will occur.
86        let _ = state
87            .qe_r_ref
88            .pre_validate_client_auth_info(&mut client_auth_info)
89            .await;
90
91        Ok(VerifiedClientInformation(client_auth_info))
92    }
93}
94
95pub struct DomainInfo(pub DomainInfoRead);
96
97#[async_trait]
98impl FromRequestParts<ServerState> for DomainInfo {
99    type Rejection = (StatusCode, &'static str);
100
101    // Need to skip all to prevent leaking tokens to logs.
102    #[instrument(level = "debug", skip_all)]
103    async fn from_request_parts(
104        _parts: &mut Parts,
105        state: &ServerState,
106    ) -> Result<Self, Self::Rejection> {
107        Ok(DomainInfo(state.qe_r_ref.domain_info_read()))
108    }
109}
110
111#[derive(Debug, Clone)]
112pub struct ClientConnInfo {
113    /// This is the address that is *connected* to Kanidm right now
114    /// for this operation.
115    #[allow(dead_code)]
116    pub connection_addr: SocketAddr,
117    /// This is the client address as reported by a remote IP source
118    /// such as x-forward-for or the PROXY protocol header
119    pub client_ip_addr: IpAddr,
120    // Only set if the certificate is VALID
121    pub client_cert: Option<ClientCertInfo>,
122}
123
124// This is the normal way that our extractors get the ip info
125impl Connected<ClientConnInfo> for ClientConnInfo {
126    fn connect_info(target: ClientConnInfo) -> Self {
127        target
128    }
129}
130
131// This is only used for plaintext http - in other words, integration tests only.
132impl Connected<SocketAddr> for ClientConnInfo {
133    fn connect_info(connection_addr: SocketAddr) -> Self {
134        ClientConnInfo {
135            client_ip_addr: connection_addr.ip(),
136            connection_addr,
137            client_cert: None,
138        }
139    }
140}