kanidm_unix_resolver/idprovider/
interface.rs

1use async_trait::async_trait;
2use kanidm_hsm_crypto::provider::BoxedDynTpm;
3use kanidm_unix_common::unix_proto::{
4    DeviceAuthorizationResponse, PamAuthRequest, PamAuthResponse,
5};
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use std::collections::BTreeMap;
9use std::fmt;
10use std::time::SystemTime;
11use tokio::sync::broadcast;
12use uuid::Uuid;
13
14pub type XKeyId = String;
15
16pub use kanidm_hsm_crypto as tpm;
17
18/// Errors that the IdProvider may return. These drive the resolver state machine
19/// and should be carefully selected to match your expected errors.
20#[derive(Debug)]
21pub enum IdpError {
22    /// An error occurred in the underlying communication to the Idp. A timeout or
23    /// or other communication issue exists. The resolver will take this provider
24    /// offline.
25    Transport,
26    /// The provider is online but the provider module is not current authorised with
27    /// the idp. After returning this error the operation will be retried after a
28    /// successful authentication.
29    ProviderUnauthorised,
30    /// The provider made an invalid or illogical request to the idp, and a result
31    /// is not able to be provided to the resolver.
32    BadRequest,
33    /// The idp has indicated that the requested resource does not exist and should
34    /// be considered deleted, removed, or not present.
35    NotFound,
36    /// The idp was unable to perform an operation on the underlying hsm keystorage
37    KeyStore,
38    /// The idp failed to interact with the configured TPM
39    Tpm,
40    /// The cached usertoken has no offline credentials available
41    NoOfflineCredentials,
42}
43
44pub enum UserTokenState {
45    /// Indicate to the resolver that the cached UserToken should be used, if present.
46    UseCached,
47    /// The requested entity is not found, or has been removed.
48    NotFound,
49
50    /// Update the cache state with the data found in this UserToken.
51    Update(UserToken),
52}
53
54pub enum GroupTokenState {
55    /// Indicate to the resolver that the cached GroupToken should be used, if present.
56    UseCached,
57    /// The requested entity is not found, or has been removed.
58    NotFound,
59
60    /// Update the cache state with the data found in this GroupToken.
61    Update(GroupToken),
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Hash)]
65pub enum Id {
66    Name(String),
67    Gid(u32),
68}
69
70#[derive(Debug, Serialize, Deserialize, Clone, Default, Eq, PartialEq, Hash)]
71pub enum ProviderOrigin {
72    // To allow transition, we have an ignored type that effectively
73    // causes these items to be nixed.
74    #[default]
75    Ignore,
76    /// Provided by local files, commonly /etc/passwd, /etc/group and /etc/shadow
77    System,
78    Kanidm,
79}
80
81impl fmt::Display for ProviderOrigin {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        match self {
84            ProviderOrigin::Ignore => {
85                write!(f, "Ignored")
86            }
87            ProviderOrigin::System => {
88                write!(f, "System")
89            }
90            ProviderOrigin::Kanidm => {
91                write!(f, "Kanidm")
92            }
93        }
94    }
95}
96
97#[derive(Debug, Serialize, Deserialize, Clone)]
98pub struct GroupToken {
99    #[serde(default)]
100    pub provider: ProviderOrigin,
101    pub name: String,
102    pub spn: String,
103    pub uuid: Uuid,
104    pub gidnumber: u32,
105
106    #[serde(flatten)]
107    pub extra_keys: BTreeMap<XKeyId, Value>,
108}
109
110#[derive(Debug, Serialize, Deserialize, Clone)]
111pub struct UserToken {
112    #[serde(default)]
113    pub provider: ProviderOrigin,
114
115    pub name: String,
116    pub spn: String,
117    pub uuid: Uuid,
118    pub gidnumber: u32,
119    pub displayname: String,
120    pub shell: Option<String>,
121    pub groups: Vec<GroupToken>,
122
123    // Could there be a better type here?
124    pub sshkeys: Vec<String>,
125    // Defaults to false.
126    pub valid: bool,
127
128    // These are opaque extra keys that the provider can interpret for internal
129    // functions.
130    #[serde(flatten)]
131    pub extra_keys: BTreeMap<XKeyId, Value>,
132}
133
134#[derive(Debug)]
135pub enum AuthCredHandler {
136    Password,
137    DeviceAuthorizationGrant,
138    /// Additional data required by the provider to complete the
139    /// authentication, but not required by PAM
140    ///
141    /// Sadly due to how this is passed around we can't make this a
142    /// generic associated type, else it would have to leak up to the
143    /// daemon.
144    ///
145    /// ⚠️  TODO: Optimally this should actually be a tokio oneshot receiver
146    /// with the decision from a task that is spawned.
147    MFA {
148        data: Vec<String>,
149    },
150    SetupPin,
151    Pin,
152}
153
154pub enum AuthRequest {
155    Password,
156    DeviceAuthorizationGrant {
157        data: DeviceAuthorizationResponse,
158    },
159    MFACode {
160        msg: String,
161    },
162    MFAPoll {
163        /// Message to display to the user.
164        msg: String,
165        /// Interval in seconds between poll attempts.
166        polling_interval: u32,
167    },
168    MFAPollWait,
169    SetupPin {
170        /// Message to display to the user.
171        msg: String,
172    },
173    Pin,
174}
175
176#[allow(clippy::from_over_into)]
177impl Into<PamAuthResponse> for AuthRequest {
178    fn into(self) -> PamAuthResponse {
179        match self {
180            AuthRequest::Password => PamAuthResponse::Password,
181            AuthRequest::DeviceAuthorizationGrant { data } => {
182                PamAuthResponse::DeviceAuthorizationGrant { data }
183            }
184            AuthRequest::MFACode { msg } => PamAuthResponse::MFACode { msg },
185            AuthRequest::MFAPoll {
186                msg,
187                polling_interval,
188            } => PamAuthResponse::MFAPoll {
189                msg,
190                polling_interval,
191            },
192            AuthRequest::MFAPollWait => PamAuthResponse::MFAPollWait,
193            AuthRequest::SetupPin { msg } => PamAuthResponse::SetupPin { msg },
194            AuthRequest::Pin => PamAuthResponse::Pin,
195        }
196    }
197}
198
199pub enum AuthResult {
200    Success,
201    SuccessUpdate { new_token: UserToken },
202    Denied,
203    Next(AuthRequest),
204}
205
206#[async_trait]
207#[allow(clippy::too_many_arguments)]
208pub trait IdProvider {
209    /// Retrieve this providers origin
210    fn origin(&self) -> ProviderOrigin;
211
212    /// Attempt to go online *immediately*
213    async fn attempt_online(&self, _tpm: &mut BoxedDynTpm, _now: SystemTime) -> bool;
214
215    /// Indicate if this provider is online or offline at this point in time. This will
216    /// not attempt to go online.
217    async fn is_online(&self) -> bool;
218
219    /// Mark that this provider should attempt to go online next time it
220    /// receives a request
221    async fn mark_next_check(&self, _now: SystemTime);
222
223    /// Force this provider offline immediately.
224    async fn mark_offline(&self);
225
226    /// Determine if this provider has a configured extension of a local system group
227    /// with remote members.
228    fn has_map_group(&self, local: &str) -> Option<&Id>;
229
230    // This is similar to a "domain join" process. What do we actually need to pass here
231    // for this to work for kanidm or himmelblau? Should we make it take a generic?
232    /*
233    async fn configure_machine_identity(
234        &self,
235        _keystore: &mut KeyStoreTxn,
236        _tpm: &mut BoxedDynTpm,
237        _machine_key: &tpm::MachineKey,
238    ) -> Result<(), IdpError> {
239        Ok(())
240    }
241    */
242
243    async fn unix_user_get(
244        &self,
245        _id: &Id,
246        _token: Option<&UserToken>,
247        _tpm: &mut BoxedDynTpm,
248        _now: SystemTime,
249    ) -> Result<UserTokenState, IdpError>;
250
251    async fn unix_user_online_auth_init(
252        &self,
253        _account_id: &str,
254        _token: &UserToken,
255        _tpm: &mut BoxedDynTpm,
256        _shutdown_rx: &broadcast::Receiver<()>,
257    ) -> Result<(AuthRequest, AuthCredHandler), IdpError>;
258
259    async fn unix_user_online_auth_step(
260        &self,
261        _account_id: &str,
262        _current_token: Option<&UserToken>,
263        _cred_handler: &mut AuthCredHandler,
264        _pam_next_req: PamAuthRequest,
265        _tpm: &mut BoxedDynTpm,
266        _shutdown_rx: &broadcast::Receiver<()>,
267    ) -> Result<AuthResult, IdpError>;
268
269    async fn unix_unknown_user_online_auth_init(
270        &self,
271        _account_id: &str,
272        _tpm: &mut BoxedDynTpm,
273        _shutdown_rx: &broadcast::Receiver<()>,
274    ) -> Result<Option<(AuthRequest, AuthCredHandler)>, IdpError>;
275
276    /// Determine if this cached user *could* continue with offline authentication. This
277    /// MUST NOT perform network checks or requests.
278    async fn unix_user_can_offline_auth(&self, _token: &UserToken) -> bool;
279
280    async fn unix_user_offline_auth_init(
281        &self,
282        _token: &UserToken,
283    ) -> Result<(AuthRequest, AuthCredHandler), IdpError>;
284
285    // I thought about this part of the interface a lot. we could have the
286    // provider actually need to check the password or credentials, but then
287    // we need to rework the tpm/crypto engine to be an argument to pass here
288    // as well the cached credentials.
289    //
290    // As well, since this is "offline auth" the provider isn't really "doing"
291    // anything special here - when you say you want offline password auth, the
292    // resolver can just do it for you for all the possible implementations.
293    // This is similar for offline ctap2 as well, or even offline totp.
294    //
295    // I think in the future we could reconsider this and let the provider be
296    // involved if there is some "custom logic" or similar that is needed but
297    // for now I think making it generic is a good first step and we can change
298    // it later.
299    //
300    // EDIT 04042024: When we're performing an offline PIN auth, the PIN can
301    // unlock the associated TPM key. While we can't perform a full request
302    // for an auth token, we can verify that the PIN successfully unlocks the
303    // TPM key.
304    async fn unix_user_offline_auth_step(
305        &self,
306        _current_token: Option<&UserToken>,
307        _session_token: &UserToken,
308        _cred_handler: &mut AuthCredHandler,
309        _pam_next_req: PamAuthRequest,
310        _tpm: &mut BoxedDynTpm,
311    ) -> Result<AuthResult, IdpError>;
312
313    async fn unix_user_authorise(&self, _token: &UserToken) -> Result<Option<bool>, IdpError>;
314
315    async fn unix_group_get(
316        &self,
317        id: &Id,
318        _tpm: &mut BoxedDynTpm,
319        _now: SystemTime,
320    ) -> Result<GroupTokenState, IdpError>;
321}