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