kanidm_unix_resolver/idprovider/
interface.rs

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