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