kanidm_unix_resolver/idprovider/
system.rs
1use super::interface::{AuthCredHandler, AuthRequest, Id, IdpError};
2use hashbrown::HashMap;
3use kanidm_unix_common::unix_passwd::{CryptPw, EtcGroup, EtcShadow, EtcUser};
4use kanidm_unix_common::unix_proto::PamAuthRequest;
5use kanidm_unix_common::unix_proto::{NssGroup, NssUser};
6use std::sync::Arc;
7use time::OffsetDateTime;
8use tokio::sync::Mutex;
9
10const SYSTEM_GID_BOUNDARY: u32 = 1000;
12
13pub struct SystemProviderInternal {
14 users: HashMap<Id, Arc<EtcUser>>,
15 user_list: Vec<Arc<EtcUser>>,
16 groups: HashMap<Id, Arc<EtcGroup>>,
17 group_list: Vec<Arc<EtcGroup>>,
18
19 shadow_enabled: bool,
20 shadow: HashMap<String, Arc<Shadow>>,
21}
22
23pub enum SystemProviderAuthInit {
24 Begin {
25 next_request: AuthRequest,
26 cred_handler: AuthCredHandler,
27 shadow: Arc<Shadow>,
28 },
29 ShadowMissing,
30 CredentialsUnavailable,
31 Expired,
32 Ignore,
33}
34
35pub enum SystemProviderSession {
36 Start,
37 Ignore,
40}
41
42pub enum SystemAuthResult {
43 Denied,
44 Success,
45 Next(AuthRequest),
46}
47
48#[allow(dead_code)]
49struct AgingPolicy {
50 last_change: time::OffsetDateTime,
51 min_password_change: time::OffsetDateTime,
52 max_password_change: Option<time::OffsetDateTime>,
53 warning_period_start: Option<time::OffsetDateTime>,
54 inactivity_period_deadline: Option<time::OffsetDateTime>,
55}
56
57impl AgingPolicy {
58 fn new(
59 last_change: time::OffsetDateTime,
60 days_min_password_age: i64,
61 days_max_password_age: Option<i64>,
62 days_warning_period: i64,
63 days_inactivity_period: Option<i64>,
64 ) -> Self {
65 let min_password_change = last_change + time::Duration::days(days_min_password_age);
66
67 let max_password_change =
68 days_max_password_age.map(|max| last_change + time::Duration::days(max));
69
70 let (warning_period_start, inactivity_period_deadline) =
71 if let Some(expiry) = max_password_change.as_ref() {
72 let warning = if days_warning_period != 0 {
77 Some(*expiry - time::Duration::days(days_warning_period))
79 } else {
80 None
81 };
82
83 let inactive =
84 days_inactivity_period.map(|inactive| *expiry + time::Duration::days(inactive));
85
86 (warning, inactive)
87 } else {
88 (None, None)
89 };
90
91 AgingPolicy {
92 last_change,
93 min_password_change,
94 max_password_change,
95 warning_period_start,
96 inactivity_period_deadline,
97 }
98 }
99}
100
101pub struct Shadow {
102 crypt_pw: CryptPw,
103 #[allow(dead_code)]
104 aging_policy: Option<AgingPolicy>,
105 expiration_date: Option<time::OffsetDateTime>,
106}
107
108impl Shadow {
109 pub fn auth_step(
110 &self,
111 cred_handler: &mut AuthCredHandler,
112 pam_next_req: PamAuthRequest,
113 ) -> SystemAuthResult {
114 match (cred_handler, pam_next_req) {
115 (AuthCredHandler::Password, PamAuthRequest::Password { cred }) => {
116 if self.crypt_pw.check_pw(&cred) {
117 SystemAuthResult::Success
118 } else {
119 SystemAuthResult::Denied
120 }
121 }
122 _ => SystemAuthResult::Denied,
123 }
124 }
125}
126
127pub struct SystemProvider {
128 inner: Mutex<SystemProviderInternal>,
129}
130
131impl SystemProvider {
132 pub fn new() -> Result<Self, IdpError> {
133 Ok(SystemProvider {
134 inner: Mutex::new(SystemProviderInternal {
135 users: Default::default(),
136 user_list: Default::default(),
137 groups: Default::default(),
138 group_list: Default::default(),
139 shadow_enabled: Default::default(),
140 shadow: Default::default(),
141 }),
142 })
143 }
144
145 pub async fn reload(&self, users: Vec<EtcUser>, shadow: Vec<EtcShadow>, groups: Vec<EtcGroup>) {
146 let mut system_ids_txn = self.inner.lock().await;
147 system_ids_txn.users.clear();
148 system_ids_txn.user_list.clear();
149 system_ids_txn.groups.clear();
150 system_ids_txn.group_list.clear();
151 system_ids_txn.shadow.clear();
152
153 system_ids_txn.shadow_enabled = !shadow.is_empty();
154
155 let s_iter = shadow.into_iter().filter_map(|shadow_entry| {
156 let EtcShadow {
157 name,
158 password,
159 epoch_change_seconds,
160 days_min_password_age,
161 days_max_password_age,
162 days_warning_period,
163 days_inactivity_period,
164 epoch_expire_seconds,
165 flag_reserved: _,
166 } = shadow_entry;
167
168 if password.is_valid() {
169 let aging_policy = epoch_change_seconds.map(|change_seconds| {
170 AgingPolicy::new(
171 change_seconds,
172 days_min_password_age,
173 days_max_password_age,
174 days_warning_period,
175 days_inactivity_period,
176 )
177 });
178
179 let expiration_date = epoch_expire_seconds;
180
181 Some((
182 name,
183 Arc::new(Shadow {
184 crypt_pw: password,
185 aging_policy,
186 expiration_date,
187 }),
188 ))
189 } else {
190 debug!(?name, "account password is invalid.");
192 None
193 }
194 });
195
196 system_ids_txn.shadow.extend(s_iter);
197
198 for group in groups {
199 let name = Id::Name(group.name.clone());
200 let gid = Id::Gid(group.gid);
201 let group = Arc::new(group);
202
203 if system_ids_txn.groups.insert(name, group.clone()).is_some() {
204 error!(name = %group.name, gid = %group.gid, "group name conflict");
205 };
206 if system_ids_txn.groups.insert(gid, group.clone()).is_some() {
207 error!(name = %group.name, gid = %group.gid, "group id conflict");
208 }
209 system_ids_txn.group_list.push(group);
210 }
211
212 for user in users {
213 let name = Id::Name(user.name.clone());
214 let uid = Id::Gid(user.uid);
215 let gid = Id::Gid(user.gid);
216
217 if user.uid != user.gid {
219 warn!(name = %user.name, uid = %user.uid, gid = %user.gid, "user uid and gid are not the same, this may be a security risk!");
220 } else if let Some(group) = system_ids_txn.groups.get(&gid) {
221 if group.name != user.name {
222 warn!(name = %user.name, uid = %user.uid, gid = %user.gid, "user private group does not appear to have the same name as the user, this may be a security risk!");
223 }
224 if !(group.members.is_empty()
225 || (group.members.len() == 1 && group.members.first() == Some(&user.name)))
226 {
227 warn!(name = %user.name, uid = %user.uid, gid = %user.gid, members = ?group.members, "user private group must not have members, THIS IS A SECURITY RISK!");
228 }
229 } else if user.uid < SYSTEM_GID_BOUNDARY {
230 warn!(name = %user.name, uid = %user.uid, gid = %user.gid, "user private group is not present on system, ignoring as this is a system account.");
231 } else {
232 info!(name = %user.name, uid = %user.uid, gid = %user.gid, "user private group is not present on system, synthesising it.");
233 let group = Arc::new(EtcGroup {
234 name: user.name.clone(),
235 password: String::new(),
236 gid: user.gid,
237 members: vec![user.name.clone()],
238 });
239
240 system_ids_txn.groups.insert(name.clone(), group.clone());
241 system_ids_txn.groups.insert(gid.clone(), group.clone());
242 system_ids_txn.group_list.push(group);
243 }
244
245 let user = Arc::new(user);
246 if system_ids_txn.users.insert(name, user.clone()).is_some() {
247 error!(name = %user.name, uid = %user.uid, "user name conflict");
248 }
249 if system_ids_txn.users.insert(uid, user.clone()).is_some() {
250 error!(name = %user.name, uid = %user.uid, "user id conflict");
251 }
252 system_ids_txn.user_list.push(user);
253 }
254 }
255
256 pub async fn auth_init(
257 &self,
258 account_id: &Id,
259 current_time: OffsetDateTime,
260 ) -> SystemProviderAuthInit {
261 let inner = self.inner.lock().await;
262
263 let Some(user) = inner.users.get(account_id) else {
264 return SystemProviderAuthInit::Ignore;
266 };
267
268 if !inner.shadow_enabled {
269 return SystemProviderAuthInit::ShadowMissing;
272 }
273
274 let Some(shadow) = inner.shadow.get(user.name.as_str()) else {
276 return SystemProviderAuthInit::CredentialsUnavailable;
277 };
278
279 if let Some(expire) = shadow.expiration_date.as_ref() {
281 if current_time >= *expire {
282 return SystemProviderAuthInit::Expired;
283 }
284 }
285
286 let cred_handler = AuthCredHandler::Password;
289
290 let next_request = AuthRequest::Password;
291
292 SystemProviderAuthInit::Begin {
293 next_request,
294 cred_handler,
295 shadow: shadow.clone(),
296 }
297 }
298
299 pub async fn authorise(&self, account_id: &Id) -> Option<bool> {
300 let inner = self.inner.lock().await;
301 if inner.users.contains_key(account_id) {
302 Some(true)
303 } else {
304 None
305 }
306 }
307
308 pub async fn begin_session(&self, account_id: &Id) -> SystemProviderSession {
309 let inner = self.inner.lock().await;
310 if inner.users.contains_key(account_id) {
311 SystemProviderSession::Start
312 } else {
313 SystemProviderSession::Ignore
314 }
315 }
316
317 pub async fn contains_group(&self, account_id: &Id) -> bool {
318 let inner = self.inner.lock().await;
319 inner.groups.contains_key(account_id)
320 }
321
322 pub async fn get_nssaccount(&self, account_id: &Id) -> Option<NssUser> {
323 let inner = self.inner.lock().await;
324 inner.users.get(account_id).map(NssUser::from)
325 }
326
327 pub async fn get_nssaccounts(&self) -> Vec<NssUser> {
328 let inner = self.inner.lock().await;
329 inner.user_list.iter().map(NssUser::from).collect()
330 }
331
332 pub async fn get_nssgroup(&self, grp_id: &Id) -> Option<NssGroup> {
333 let inner = self.inner.lock().await;
334 inner.groups.get(grp_id).map(NssGroup::from)
335 }
336
337 pub async fn get_nssgroups(&self) -> Vec<NssGroup> {
338 let inner = self.inner.lock().await;
339 inner.group_list.iter().map(NssGroup::from).collect()
340 }
341}