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