1 /*
2 Copyright 2021 Northern.tech AS
3
4 This file is part of CFEngine 3 - written and maintained by Northern.tech AS.
5
6 This program is free software; you can redistribute it and/or modify it
7 under the terms of the GNU General Public License as published by the
8 Free Software Foundation; version 3.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
18
19 To the extent this program is licensed as part of the Enterprise
20 versions of CFEngine, the applicable Commercial Open Source License
21 (COSL) may apply to this file if you as a licensee so wish it. See
22 included file COSL.txt.
23 */
24
25 #include <verify_users.h>
26
27 #include <string_lib.h>
28 #include <exec_tools.h>
29 #include <policy.h>
30 #include <misc_lib.h>
31 #include <rlist.h>
32 #include <pipes.h>
33 #include <files_copy.h>
34 #include <files_interfaces.h>
35 #include <files_lib.h>
36 #include <eval_context.h>
37 #include <regex.h> // CompileRegex()
38
39 #include <cf3.defs.h>
40 #include <verify_methods.h>
41
42 #include <stdio.h>
43 #include <string.h>
44
45 #include <security/pam_appl.h>
46
47 #include <sys/types.h>
48 #include <grp.h>
49 #include <pwd.h>
50
51 #ifdef HAVE_SHADOW_H
52 # include <shadow.h>
53 #endif
54
55 #ifdef __FreeBSD__
56 /* Use pw_scan() and gr_scan() to implement fgetpwent() and
57 * fgetgrent() on FreeBSD. */
58 #include <libutil.h>
59 #endif
60
61 #define CFUSR_CHECKBIT(v,p) ((v) & (1UL << (p)))
62 #define CFUSR_SETBIT(v,p) ((v) |= ((1UL) << (p)))
63 #define CFUSR_CLEARBIT(v,p) ((v) &= ~((1UL) << (p)))
64
65 typedef enum
66 {
67 i_uid,
68 i_password,
69 i_comment,
70 i_group,
71 i_groups,
72 i_home,
73 i_shell,
74 i_locked
75 } which;
76
77 static bool SupportsOption(const char *cmd, const char *option);
78
GetPlatformSpecificExpirationDate()79 static const char *GetPlatformSpecificExpirationDate()
80 {
81 // 2nd January 1970.
82
83 #if defined(_AIX)
84 return "0102000070";
85 #elif defined(__hpux) || defined(__SVR4)
86 return "02/01/70";
87 #elif defined(__NetBSD__)
88 return "January 02 1970";
89 #elif defined(__linux__)
90 return "1970-01-02";
91 #elif defined(__FreeBSD__)
92 return "02-Jan-1970";
93 #else
94 # error Your operating system lacks the proper string for the "usermod -e" utility.
95 #endif
96 }
97
PasswordSupplier(int num_msg,const struct pam_message ** msg,struct pam_response ** resp,void * appdata_ptr)98 static int PasswordSupplier(int num_msg, const struct pam_message **msg,
99 struct pam_response **resp, void *appdata_ptr)
100 {
101 // All allocations here will be freed by the pam framework.
102 *resp = xmalloc(num_msg * sizeof(struct pam_response));
103 for (int i = 0; i < num_msg; i++)
104 {
105 if ((*msg)[i].msg_style == PAM_PROMPT_ECHO_OFF)
106 {
107 (*resp)[i].resp = xstrdup((const char *)appdata_ptr);
108 }
109 else
110 {
111 (*resp)[i].resp = xstrdup("");
112 }
113 (*resp)[i].resp_retcode = 0;
114 }
115
116 return PAM_SUCCESS;
117 }
118
119 #ifdef _AIX
120 /*
121 * Format of passwd file on AIX is:
122 *
123 * user1:
124 * password = hash
125 * lastupdate = 12783612
126 * user2:
127 * password = hash
128 * lastupdate = 12783612
129 * <...>
130 */
GetAIXShadowHash(const char * puser,const char ** result)131 static bool GetAIXShadowHash(const char *puser, const char **result)
132 {
133 FILE *fptr = safe_fopen("/etc/security/passwd", "r");
134 if (fptr == NULL)
135 {
136 return false;
137 }
138
139 // Not super pretty with a static variable, but it is how POSIX functions
140 // getspnam() and friends do it.
141 static char hash_buf[CF_BUFSIZE];
142
143 bool ret = false;
144 char *buf = NULL;
145 size_t bufsize = 0;
146 size_t puser_len = strlen(puser);
147 char name_regex_str[strlen(puser) + 3];
148
149 pcre *name_regex = CompileRegex("^(\\S+):");
150 pcre *hash_regex = CompileRegex("^\\s+password\\s*=\\s*(\\S+)");
151 bool in_user_section = false;
152
153 while (true)
154 {
155 ssize_t read_result = CfReadLine(&buf, &bufsize, fptr);
156 if (read_result < 0)
157 {
158 if (feof(fptr))
159 {
160 errno = 0;
161 }
162 goto end;
163 }
164
165 int submatch_vec[6];
166
167 int pcre_result = pcre_exec(name_regex, NULL, buf, strlen(buf), 0, 0, submatch_vec, 6);
168 if (pcre_result >= 0)
169 {
170 if (submatch_vec[3] - submatch_vec[2] == puser_len
171 && strncmp(buf + submatch_vec[2], puser, puser_len) == 0)
172 {
173 in_user_section = true;
174 }
175 else
176 {
177 in_user_section = false;
178 }
179 continue;
180 }
181 else if (pcre_result != PCRE_ERROR_NOMATCH)
182 {
183 errno = EINVAL;
184 goto end;
185 }
186
187 if (!in_user_section)
188 {
189 continue;
190 }
191
192 pcre_result = pcre_exec(hash_regex, NULL, buf, strlen(buf), 0, 0, submatch_vec, 6);
193 if (pcre_result >= 0)
194 {
195 memcpy(hash_buf, buf + submatch_vec[2], submatch_vec[3] - submatch_vec[2]);
196 *result = hash_buf;
197 ret = true;
198 goto end;
199 }
200 else if (pcre_result != PCRE_ERROR_NOMATCH)
201 {
202 errno = EINVAL;
203 goto end;
204 }
205 }
206
207 end:
208 pcre_free(name_regex);
209 pcre_free(hash_regex);
210 free(buf);
211 fclose(fptr);
212 return ret;
213 }
214 #endif // _AIX
215
216 #if HAVE_FGETSPENT
217 // Uses fgetspent() instead of getspnam(), to guarantee that the returned user
218 // is a local user, and not for example from LDAP.
GetSpEntry(const char * puser)219 static struct spwd *GetSpEntry(const char *puser)
220 {
221 FILE *fptr = safe_fopen("/etc/shadow", "r");
222 if (!fptr)
223 {
224 Log(LOG_LEVEL_ERR, "Could not open '/etc/shadow': %s", GetErrorStr());
225 return NULL;
226 }
227
228 struct spwd *spwd_info;
229 bool found = false;
230 while ((spwd_info = fgetspent(fptr)))
231 {
232 if (strcmp(puser, spwd_info->sp_namp) == 0)
233 {
234 found = true;
235 break;
236 }
237 }
238
239 fclose(fptr);
240
241 if (found)
242 {
243 return spwd_info;
244 }
245 else
246 {
247 // Failure to find the user means we just set errno to zero.
248 // Perhaps not optimal, but we cannot pass ENOENT, because the fopen might
249 // fail for this reason, and that should not be treated the same.
250 errno = 0;
251 return NULL;
252 }
253 }
254 #endif // HAVE_FGETSPENT
255
GetPasswordHash(const char * puser,const struct passwd * passwd_info,const char ** result)256 static bool GetPasswordHash(const char *puser, const struct passwd *passwd_info, const char **result)
257 {
258 // Silence warning.
259 (void)puser;
260
261 // If the hash is very short, it's probably a stub. Try getting the shadow password instead.
262 if (strlen(passwd_info->pw_passwd) <= 4)
263 {
264 #ifdef HAVE_FGETSPENT
265 struct stat statbuf;
266 if (stat("/etc/shadow", &statbuf) == 0)
267 {
268 Log(LOG_LEVEL_VERBOSE, "Getting user '%s' password hash from shadow database.", puser);
269
270 struct spwd *spwd_info;
271 errno = 0;
272 spwd_info = GetSpEntry(puser);
273 if (!spwd_info)
274 {
275 if (errno)
276 {
277 Log(LOG_LEVEL_ERR, "Could not get information from user shadow database: %s", GetErrorStr());
278 return false;
279 }
280 else
281 {
282 Log(LOG_LEVEL_ERR, "Could not find user when checking password.");
283 return false;
284 }
285 }
286 else if (spwd_info)
287 {
288 *result = spwd_info->sp_pwdp;
289 return true;
290 }
291 }
292
293 #elif defined(_AIX)
294 if (!GetAIXShadowHash(puser, result))
295 {
296 Log(LOG_LEVEL_ERR, "Could not get information from user shadow database: %s", GetErrorStr());
297 return false;
298 }
299 return true;
300
301 #endif
302 }
303
304 Log(LOG_LEVEL_VERBOSE, "Getting user '%s' password hash from passwd database.", puser);
305 *result = passwd_info->pw_passwd;
306 return true;
307 }
308
IsPasswordCorrect(const char * puser,const char * password,PasswordFormat format,const struct passwd * passwd_info)309 static bool IsPasswordCorrect(const char *puser, const char* password, PasswordFormat format, const struct passwd *passwd_info)
310 {
311 /*
312 * Check if password is already correct. If format is 'hash' we just do a simple
313 * comparison with the supplied hash value, otherwise we try a pam login using
314 * the real password.
315 */
316
317 if (format == PASSWORD_FORMAT_HASH)
318 {
319 const char *system_hash;
320 if (!GetPasswordHash(puser, passwd_info, &system_hash))
321 {
322 return false;
323 }
324 bool result = (strcmp(password, system_hash) == 0);
325 Log(LOG_LEVEL_VERBOSE, "Verifying password hash for user '%s': %s.", puser, result ? "correct" : "incorrect");
326 return result;
327 }
328 else if (format != PASSWORD_FORMAT_PLAINTEXT)
329 {
330 ProgrammingError("Unknown PasswordFormat value");
331 }
332
333 int status;
334 pam_handle_t *handle;
335 struct pam_conv conv;
336 conv.conv = PasswordSupplier;
337 conv.appdata_ptr = (void*)password;
338
339 status = pam_start("login", puser, &conv, &handle);
340 if (status != PAM_SUCCESS)
341 {
342 Log(LOG_LEVEL_ERR, "Could not initialize pam session. (pam_start: '%s')", pam_strerror(NULL, status));
343 return false;
344 }
345 status = pam_authenticate(handle, PAM_SILENT);
346 pam_end(handle, status);
347 if (status == PAM_SUCCESS)
348 {
349 Log(LOG_LEVEL_VERBOSE, "Verifying plaintext password for user '%s': correct.", puser);
350 return true;
351 }
352 else if (status != PAM_AUTH_ERR)
353 {
354 Log(LOG_LEVEL_ERR, "Could not check password for user '%s' against stored password. (pam_authenticate: '%s')",
355 puser, pam_strerror(NULL, status));
356 return false;
357 }
358
359 Log(LOG_LEVEL_VERBOSE, "Verifying plaintext password for user '%s': incorrect.", puser);
360 return false;
361 }
362
ChangePlaintextPasswordUsingLibPam(const char * puser,const char * password)363 static bool ChangePlaintextPasswordUsingLibPam(const char *puser, const char *password)
364 {
365 int status;
366 pam_handle_t *handle;
367 struct pam_conv conv;
368 conv.conv = PasswordSupplier;
369 conv.appdata_ptr = (void*)password;
370
371 status = pam_start("passwd", puser, &conv, &handle);
372 if (status != PAM_SUCCESS)
373 {
374 Log(LOG_LEVEL_ERR, "Could not initialize pam session. (pam_start: '%s')", pam_strerror(NULL, status));
375 return false;
376 }
377 Log(LOG_LEVEL_VERBOSE, "Changing password for user '%s'.", puser);
378 status = pam_chauthtok(handle, PAM_SILENT);
379 pam_end(handle, status);
380 if (status == PAM_SUCCESS)
381 {
382 return true;
383 }
384 else
385 {
386 Log(LOG_LEVEL_ERR, "Could not change password for user '%s'. (pam_chauthtok: '%s')",
387 puser, pam_strerror(handle, status));
388 return false;
389 }
390 }
391
ClearPasswordAdministrationFlags(const char * puser)392 static bool ClearPasswordAdministrationFlags(const char *puser)
393 {
394 (void)puser; // Avoid warning.
395
396 #ifdef HAVE_PWDADM
397 const char *cmd_str = PWDADM " -c ";
398 char final_cmd[strlen(cmd_str) + strlen(puser) + 1];
399
400 xsnprintf(final_cmd, sizeof(final_cmd), "%s%s", cmd_str, puser);
401
402 Log(LOG_LEVEL_VERBOSE, "Clearing password administration flags for user '%s'. (command: '%s')", puser, final_cmd);
403
404 int status;
405 status = system(final_cmd);
406 if (!WIFEXITED(status) || WEXITSTATUS(status) != 0)
407 {
408 Log(LOG_LEVEL_ERR, "Command failed while trying to clear password flags for user '%s'. (Command: '%s')",
409 puser, final_cmd);
410 return false;
411 }
412 #endif // HAVE_PWDADM
413
414 return true;
415 }
416
417 #ifdef HAVE_CHPASSWD
ChangePasswordHashUsingChpasswd(const char * puser,const char * password)418 static bool ChangePasswordHashUsingChpasswd(const char *puser, const char *password)
419 {
420 int status;
421 const char *cmd_str = CHPASSWD " -e";
422 Log(LOG_LEVEL_VERBOSE, "Changing password hash for user '%s'. (command: '%s')", puser, cmd_str);
423 FILE *cmd = cf_popen_sh(cmd_str, "w");
424 if (!cmd)
425 {
426 Log(LOG_LEVEL_ERR, "Could not launch password changing command '%s': %s.", cmd_str, GetErrorStr());
427 return false;
428 }
429
430 // String lengths plus a ':' and a '\n', but not including '\0'.
431 size_t total_len = strlen(puser) + strlen(password) + 2;
432 char change_string[total_len + 1];
433 xsnprintf(change_string, total_len + 1, "%s:%s\n", puser, password);
434 clearerr(cmd);
435 if (fwrite(change_string, total_len, 1, cmd) != 1)
436 {
437 const char *error_str;
438 if (ferror(cmd))
439 {
440 error_str = GetErrorStr();
441 }
442 else
443 {
444 error_str = "Unknown error";
445 }
446 Log(LOG_LEVEL_ERR, "Could not write password to password changing command '%s': %s.", cmd_str, error_str);
447 cf_pclose(cmd);
448 return false;
449 }
450 status = cf_pclose(cmd);
451 if (status)
452 {
453 Log(LOG_LEVEL_ERR, "'%s' returned non-zero status: %i\n", cmd_str, status);
454 return false;
455 }
456
457 return true;
458 }
459 #endif // HAVE_CHPASSWD
460
461 #if defined(HAVE_LCKPWDF) && defined(HAVE_ULCKPWDF)
ChangePasswordHashUsingLckpwdf(const char * puser,const char * password)462 static bool ChangePasswordHashUsingLckpwdf(const char *puser, const char *password)
463 {
464 bool result = false;
465
466 struct stat statbuf;
467 const char *passwd_file = "/etc/shadow";
468 if (stat(passwd_file, &statbuf) == -1)
469 {
470 passwd_file = "/etc/passwd";
471 }
472
473 Log(LOG_LEVEL_VERBOSE, "Changing password hash for user '%s' by editing '%s'.", puser, passwd_file);
474
475 if (lckpwdf() != 0)
476 {
477 Log(LOG_LEVEL_ERR, "Not able to obtain lock on password database.");
478 return false;
479 }
480
481 char backup_file[strlen(passwd_file) + strlen(".cf-backup") + 1];
482 xsnprintf(backup_file, sizeof(backup_file), "%s.cf-backup", passwd_file);
483 unlink(backup_file);
484
485 char edit_file[strlen(passwd_file) + strlen(".cf-edit") + 1];
486 xsnprintf(edit_file, sizeof(edit_file), "%s.cf-edit", passwd_file);
487 unlink(edit_file);
488
489 if (!CopyRegularFileDisk(passwd_file, backup_file))
490 {
491 Log(LOG_LEVEL_ERR, "Could not back up existing password database '%s' to '%s'.", passwd_file, backup_file);
492 goto unlock_passwd;
493 }
494
495 FILE *passwd_fd = safe_fopen(passwd_file, "r");
496 if (!passwd_fd)
497 {
498 Log(LOG_LEVEL_ERR, "Could not open password database '%s'. (fopen: '%s')", passwd_file, GetErrorStr());
499 goto unlock_passwd;
500 }
501 int edit_fd_int = open(edit_file, O_WRONLY | O_CREAT | O_EXCL, S_IWUSR);
502 if (edit_fd_int < 0)
503 {
504 if (errno == EEXIST)
505 {
506 Log(LOG_LEVEL_CRIT, "Temporary file already existed when trying to open '%s'. (open: '%s') "
507 "This should NEVER happen and could mean that someone is trying to break into your system!!",
508 edit_file, GetErrorStr());
509 }
510 else
511 {
512 Log(LOG_LEVEL_ERR, "Could not open password database temporary file '%s'. (open: '%s')", edit_file, GetErrorStr());
513 }
514 goto close_passwd_fd;
515 }
516 FILE *edit_fd = fdopen(edit_fd_int, "w");
517 if (!edit_fd)
518 {
519 Log(LOG_LEVEL_ERR, "Could not open password database temporary file '%s'. (fopen: '%s')", edit_file, GetErrorStr());
520 close(edit_fd_int);
521 goto close_passwd_fd;
522 }
523
524 while (true)
525 {
526 size_t line_size = 0;
527 char *line = NULL;
528
529 int read_result = CfReadLine(&line, &line_size, passwd_fd);
530 if (read_result < 0)
531 {
532 if (!feof(passwd_fd))
533 {
534 Log(LOG_LEVEL_ERR, "Error while reading password database: %s", GetErrorStr());
535 free(line);
536 goto close_both;
537 }
538 else
539 {
540 break;
541 }
542 }
543
544 // Editing the password database is risky business, so do as little parsing as possible.
545 // Just enough to get the hash in there.
546 char *field_start = NULL;
547 char *field_end = NULL;
548 field_start = strchr(line, ':');
549 if (field_start)
550 {
551 field_end = strchr(field_start + 1, ':');
552 }
553 if (!field_start || !field_end)
554 {
555 Log(LOG_LEVEL_ERR, "Unexpected format found in password database while editing user '%s'. Not updating.",
556 puser);
557 free(line);
558 goto close_both;
559 }
560
561 // Worst case length: Existing password is empty plus one '\n' and one '\0'.
562 char new_line[strlen(line) + strlen(password) + 2];
563 *field_start = '\0';
564 *field_end = '\0';
565 if (strcmp(line, puser) == 0)
566 {
567 xsnprintf(new_line, sizeof(new_line), "%s:%s:%s\n",
568 line, password, field_end + 1);
569 }
570 else
571 {
572 xsnprintf(new_line, sizeof(new_line), "%s:%s:%s\n",
573 line, field_start + 1, field_end + 1);
574 }
575
576 free(line);
577
578 size_t new_line_size = strlen(new_line);
579 size_t written_so_far = 0;
580 while (written_so_far < new_line_size)
581 {
582 clearerr(edit_fd);
583 size_t written = fwrite(new_line, 1, new_line_size, edit_fd);
584 if (written == 0)
585 {
586 const char *err_str;
587 if (ferror(edit_fd))
588 {
589 err_str = GetErrorStr();
590 }
591 else
592 {
593 err_str = "Unknown error";
594 }
595 Log(LOG_LEVEL_ERR, "Error while writing to file '%s'. (fwrite: '%s')", edit_file, err_str);
596 goto close_both;
597 }
598 written_so_far += written;
599 }
600 }
601
602 fclose(edit_fd);
603 fclose(passwd_fd);
604
605 if (!CopyFilePermissionsDisk(passwd_file, edit_file))
606 {
607 Log(LOG_LEVEL_ERR, "Could not copy permissions from '%s' to '%s'", passwd_file, edit_file);
608 goto unlock_passwd;
609 }
610
611 if (rename(edit_file, passwd_file) < 0)
612 {
613 Log(LOG_LEVEL_ERR, "Could not replace '%s' with edited password database '%s'. (rename: '%s')",
614 passwd_file, edit_file, GetErrorStr());
615 goto unlock_passwd;
616 }
617
618 result = true;
619
620 goto unlock_passwd;
621
622 close_both:
623 fclose(edit_fd);
624 unlink(edit_file);
625 close_passwd_fd:
626 fclose(passwd_fd);
627 unlock_passwd:
628 ulckpwdf();
629
630 return result;
631 }
632 #endif // defined(HAVE_LCKPWDF) && defined(HAVE_ULCKPWDF)
633
ExecuteUserCommand(const char * puser,const char * cmd,size_t sizeof_cmd,const char * action_msg,const char * cap_action_msg)634 static bool ExecuteUserCommand(const char *puser, const char *cmd, size_t sizeof_cmd,
635 const char *action_msg, const char *cap_action_msg)
636 {
637 if (strlen(cmd) >= sizeof_cmd - 1)
638 {
639 // Instead of checking every StringAppend call, assume that a maxed out
640 // string length overflowed the string.
641 Log(LOG_LEVEL_ERR, "Command line too long while %s user '%s'", action_msg, puser);
642 return false;
643 }
644
645 Log(LOG_LEVEL_VERBOSE, "%s user '%s'. (command: '%s')", cap_action_msg, puser, cmd);
646
647 int status = system(cmd);
648 if (!WIFEXITED(status) || WEXITSTATUS(status) != 0)
649 {
650 Log(LOG_LEVEL_ERR, "Command returned error while %s user '%s'. (Command line: '%s')", action_msg, puser, cmd);
651 return false;
652 }
653 return true;
654 }
655
656 #ifdef HAVE_CHPASS
ChangePasswordHashUsingChpass(const char * puser,const char * password)657 static bool ChangePasswordHashUsingChpass(const char *puser, const char *password)
658 {
659 char cmd[CF_BUFSIZE];
660
661 strcpy(cmd, CHPASS);
662 StringAppend(cmd, " -p \'", sizeof(cmd));
663 StringAppend(cmd, password, sizeof(cmd));
664 StringAppend(cmd, "\' ", sizeof(cmd));
665 StringAppend(cmd, puser, sizeof(cmd));
666
667 Log(LOG_LEVEL_VERBOSE, "Changing password hash for user '%s'. (command: '%s')", puser, cmd);
668
669 return ExecuteUserCommand(puser, cmd, sizeof(cmd), "changing", "Changing");
670 }
671 #endif // HAVE_CHPASS
672
ChangePassword(const char * puser,const char * password,PasswordFormat format)673 static bool ChangePassword(const char *puser, const char *password, PasswordFormat format)
674 {
675 assert(format == PASSWORD_FORMAT_PLAINTEXT || format == PASSWORD_FORMAT_HASH);
676
677 bool successful = false;
678
679 if (format == PASSWORD_FORMAT_PLAINTEXT)
680 {
681 successful = ChangePlaintextPasswordUsingLibPam(puser, password);
682 }
683 else
684 {
685 #ifdef HAVE_CHPASSWD
686 struct stat statbuf;
687 if (stat(CHPASSWD, &statbuf) != -1 && SupportsOption(CHPASSWD, "-e"))
688 {
689 successful = ChangePasswordHashUsingChpasswd(puser, password);
690 }
691 else
692 #endif
693 #if defined(HAVE_LCKPWDF) && defined(HAVE_ULCKPWDF)
694 {
695 successful = ChangePasswordHashUsingLckpwdf(puser, password);
696 }
697 #elif defined(HAVE_CHPASS)
698 {
699 successful = ChangePasswordHashUsingChpass(puser, password);
700 }
701 #elif defined(HAVE_CHPASSWD)
702 {
703 Log(LOG_LEVEL_ERR, "No means to set password for user '%s' was found. Tried using the '%s' tool with no luck.",
704 puser, CHPASSWD);
705 successful = false;
706 }
707 #else
708 {
709 Log(LOG_LEVEL_WARNING, "Setting hashed password or locking user '%s' not supported on this platform.", puser);
710 successful = false;
711 }
712 #endif
713 }
714
715 if (successful)
716 {
717 successful = ClearPasswordAdministrationFlags(puser);
718 }
719
720 return successful;
721 }
722
IsHashLocked(const char * hash)723 static bool IsHashLocked(const char *hash)
724 {
725 #ifdef __FreeBSD__
726 /* Accounts are locked by prepending "*LOCKED*" to the password
727 * hash on FreeBSD and possibly other systems using pw. */
728 return (strstr(hash, "*LOCKED*") != NULL);
729 #else
730 /* Accounts are locked by prepending "!" to the password hash on
731 * some systems. */
732 return (hash[0] == '!');
733 #endif
734 }
735
IsAccountLocked(const char * puser,const struct passwd * passwd_info)736 static bool IsAccountLocked(const char *puser, const struct passwd *passwd_info)
737 {
738 /* Note that when we lock an account, we do two things, we make the password hash invalid
739 * by adding a '!', and we set the expiry date far in the past. However, we only have the
740 * possibility of checking the password hash, because the expire field is not exposed by
741 * POSIX functions. This is not a problem as long as you stick to CFEngine, but if the user
742 * unlocks the account manually, but forgets to reset the expiry time, CFEngine could think
743 * that the account is unlocked when it really isn't.
744 */
745
746 const char *system_hash;
747 if (!GetPasswordHash(puser, passwd_info, &system_hash))
748 {
749 return false;
750 }
751
752 return IsHashLocked(system_hash);
753 }
754
PlatformSupportsExpirationLock(void)755 static bool PlatformSupportsExpirationLock(void)
756 {
757 #ifdef __sun
758 // Solaris has the concept of account expiration, but it is only possible
759 // to set a date in the future. We need to set it to a past date, so we
760 // have to skip it on that platform.
761 return false;
762
763 #elif __hpux
764 struct stat statbuf;
765 // "/etc/shadow" signals the so called "trusted model" on HPUX.
766 if (stat("/etc/shadow", &statbuf) == 0)
767 {
768 return true;
769 }
770 else
771 {
772 return false;
773 }
774
775 #else
776 return true;
777 #endif
778 }
779
780 #ifdef HAVE_USERMOD
SetAccountLockExpirationUsingUsermod(const char * puser,bool lock)781 static bool SetAccountLockExpirationUsingUsermod(const char *puser, bool lock)
782 {
783 if (!PlatformSupportsExpirationLock())
784 {
785 return true;
786 }
787
788 char cmd[CF_BUFSIZE + strlen(puser)];
789
790 strcpy (cmd, USERMOD);
791 StringAppend(cmd, " -e \"", sizeof(cmd));
792 if (lock)
793 {
794 StringAppend(cmd, GetPlatformSpecificExpirationDate(), sizeof(cmd));
795 }
796 StringAppend(cmd, "\" ", sizeof(cmd));
797 StringAppend(cmd, puser, sizeof(cmd));
798
799 Log(LOG_LEVEL_VERBOSE, "%s user '%s' by setting expiry date. (command: '%s')",
800 lock ? "Locking" : "Unlocking", puser, cmd);
801
802 int status;
803 status = system(cmd);
804 if (!WIFEXITED(status) || WEXITSTATUS(status) != 0)
805 {
806 Log(LOG_LEVEL_ERR, "Command returned error while %s user '%s'. (Command line: '%s')",
807 lock ? "locking" : "unlocking", puser, cmd);
808 return false;
809 }
810
811 return true;
812 }
813 #endif
814
815 #ifdef HAVE_PW
SetAccountLockExpirationUsingPw(const char * puser,bool lock)816 static bool SetAccountLockExpirationUsingPw(const char *puser, bool lock)
817 {
818 if (!PlatformSupportsExpirationLock())
819 {
820 return true;
821 }
822
823 char cmd[CF_BUFSIZE];
824
825 strcpy(cmd, PW);
826 StringAppend(cmd, " usermod ", sizeof(cmd));
827 StringAppend(cmd, puser, sizeof(cmd));
828 StringAppend(cmd, " -e \"", sizeof(cmd));
829 if (lock)
830 {
831 StringAppend(cmd, GetPlatformSpecificExpirationDate(), sizeof(cmd));
832 }
833 StringAppend(cmd, "\" ", sizeof(cmd));
834
835 Log(LOG_LEVEL_VERBOSE, "%s user '%s' by setting expiry date. (command: '%s')",
836 lock ? "Locking" : "Unlocking", puser, cmd);
837
838 const int status = system(cmd);
839 if (!WIFEXITED(status) || WEXITSTATUS(status) != 0)
840 {
841 Log(LOG_LEVEL_ERR, "Command returned error while %s user '%s'. (Command line: '%s')",
842 lock ? "locking" : "unlocking", puser, cmd);
843 return false;
844 }
845
846 return true;
847 }
848 #endif
849
SetAccountLockExpiration(const char * puser,bool lock)850 static bool SetAccountLockExpiration(const char *puser, bool lock)
851 {
852 #if defined(HAVE_USERMOD)
853 return SetAccountLockExpirationUsingUsermod(puser, lock);
854 #elif defined(HAVE_PW)
855 return SetAccountLockExpirationUsingPw(puser, lock);
856 #else
857 Log(LOG_LEVEL_WARNING, "Cannot set account lock exporation, not supported on this platform");
858 return false;
859 #endif
860 }
861
SetAccountLocked(const char * puser,const char * hash,bool lock)862 static bool SetAccountLocked(const char *puser, const char *hash, bool lock)
863 {
864 if (hash)
865 {
866 if (lock)
867 {
868 if (!IsHashLocked(hash))
869 {
870 #ifdef HAVE_PW
871 char new_hash[strlen(hash) + 9];
872 xsnprintf(new_hash, sizeof(new_hash), "*LOCKED*%s", hash);
873 #else
874 char new_hash[strlen(hash) + 2];
875 xsnprintf(new_hash, sizeof(new_hash), "!%s", hash);
876 #endif
877 if (!ChangePassword(puser, new_hash, PASSWORD_FORMAT_HASH))
878 {
879 return false;
880 }
881 }
882 }
883 else
884 {
885 if (IsHashLocked(hash))
886 {
887 #ifdef HAVE_PW
888 /* Accounts are locked by prepending "*LOCKED*" to the
889 * password hash on FreeBSD. Skip these 8 characters
890 * to obtain only the password hash. */
891 if (!ChangePassword(puser, &hash[8], PASSWORD_FORMAT_HASH))
892 #else
893 /* Accounts are locked by prepending "!" to the
894 * password hash on some systems. Skip this 1
895 * character to obtain only the password hash. */
896 if (!ChangePassword(puser, &hash[1], PASSWORD_FORMAT_HASH))
897 #endif
898 {
899 return false;
900 }
901 }
902 }
903 }
904
905 return SetAccountLockExpiration(puser, lock);
906 }
907 static void TransformGidsToGroups(StringSet **list);
908
GetGroupInfo(const char * user,const User * u,StringSet ** groups_to_set,StringSet ** groups_missing,StringSet ** current_secondary_groups)909 static bool GetGroupInfo (const char *user, const User *u, StringSet **groups_to_set, StringSet **groups_missing, StringSet **current_secondary_groups)
910 {
911 assert(u != NULL);
912 bool ret = true;
913 struct group *group_info;
914
915 FILE *fptr = safe_fopen("/etc/group", "r");
916 if (!fptr)
917 {
918 Log(LOG_LEVEL_ERR, "Could not open '/etc/group': %s", GetErrorStr());
919 return false;
920 }
921
922 StringSet *wanted_groups = StringSetNew();
923 if (u->groups_secondary_given)
924 {
925 for (Rlist *ptr = u->groups_secondary; ptr != NULL; ptr = ptr->next)
926 {
927 StringSetAdd(*groups_missing, xstrdup(RvalScalarValue(ptr->val)));
928 StringSetAdd(wanted_groups, xstrdup(RvalScalarValue(ptr->val)));
929 }
930 TransformGidsToGroups(groups_missing);
931 TransformGidsToGroups(&wanted_groups);
932 }
933
934 while (true)
935 {
936 errno = 0;
937 // Use fgetgrent() instead of getgrent(), to guarantee that the
938 // returned group is a local group, and not for example from LDAP.
939 group_info = fgetgrent(fptr);
940 if (!group_info)
941 {
942 // Documentation among Unices is conflicting on return codes. When there
943 // are no more entries, this happens:
944 // Linux = ENOENT
945 // AIX = ESRCH
946 if (errno && errno != ENOENT && errno != ESRCH)
947 {
948 Log(LOG_LEVEL_ERR, "Error while getting group list. (fgetgrent: '%s')", GetErrorStr());
949 ret = false;
950 }
951 break;
952 }
953
954 if (StringSetContains(wanted_groups, group_info->gr_name))
955 {
956 StringSetRemove(*groups_missing, group_info->gr_name);
957 }
958
959 // At least on FreeBSD, gr_mem can be NULL:
960 if (group_info->gr_mem != NULL)
961 {
962 bool found = false;
963 for (int i = 0; !found && group_info->gr_mem[i] != NULL; i++)
964 {
965 if (strcmp(user, group_info->gr_mem[i]) == 0)
966 {
967 found = true;
968 StringSetAdd(*current_secondary_groups, xstrdup(group_info->gr_name));
969 if (StringSetContains(wanted_groups, group_info->gr_name))
970 {
971 StringSetAdd(*groups_to_set, xstrdup(group_info->gr_name));
972 }
973 }
974 }
975 if (!found && StringSetContains(wanted_groups, group_info->gr_name))
976 {
977 StringSetAdd(*groups_to_set, xstrdup(group_info->gr_name));
978 }
979 }
980 #ifdef __FreeBSD__
981 free(group_info);
982 #endif
983 }
984
985 StringSetDestroy(wanted_groups);
986 fclose(fptr);
987
988 return ret;
989 }
990
EqualGid(const char * key,const struct group * entry)991 static bool EqualGid(const char *key, const struct group *entry)
992 {
993 assert(entry != NULL);
994
995 unsigned long gid;
996 int ret = StringToUlong(key, &gid);
997 if (ret != 0)
998 {
999 LogStringToLongError(key, "EqualGid", ret);
1000 return false;
1001 }
1002
1003 return (gid == entry->gr_gid);
1004 }
1005
EqualGroupName(const char * key,const struct group * entry)1006 static bool EqualGroupName(const char *key, const struct group *entry)
1007 {
1008 assert(entry != NULL);
1009 return (strcmp(key, entry->gr_name) == 0);
1010 }
1011
1012 #ifdef __FreeBSD__
fgetgrent(FILE * stream)1013 struct group *fgetgrent(FILE *stream)
1014 {
1015 if (stream == NULL)
1016 {
1017 return NULL;
1018 }
1019
1020 struct group *gr = NULL;
1021 char *line = NULL;
1022 size_t linecap = 0;
1023 ssize_t linelen;
1024
1025 while ((linelen = getline(&line, &linecap, stream)) > 0)
1026 {
1027 /* Skip comments and empty lines */
1028 if (*line == '\n' || *line == '#')
1029 {
1030 continue;
1031 }
1032 /* trim latest \n */
1033 if (line[linelen - 1] == '\n')
1034 {
1035 line[linelen - 1] = '\0';
1036 }
1037 gr = gr_scan(line);
1038 if (gr != NULL)
1039 {
1040 break;
1041 }
1042 }
1043 free(line);
1044
1045 return gr;
1046 }
1047 #endif
1048
1049 // Uses fgetgrent() instead of getgrnam(), to guarantee that the returned group
1050 // is a local group, and not for example from LDAP.
GetGrEntry(const char * key,bool (* equal_fn)(const char * key,const struct group * entry))1051 static struct group *GetGrEntry(const char *key,
1052 bool (*equal_fn)(const char *key, const struct group *entry))
1053 {
1054 FILE *fptr = safe_fopen("/etc/group", "r");
1055 if (!fptr)
1056 {
1057 Log(LOG_LEVEL_ERR, "Could not open '/etc/group': %s", GetErrorStr());
1058 return NULL;
1059 }
1060
1061 struct group *group_info;
1062 bool found = false;
1063 while ((group_info = fgetgrent(fptr)))
1064 {
1065 if (equal_fn(key, group_info))
1066 {
1067 found = true;
1068 break;
1069 }
1070 #ifdef __FreeBSD__
1071 free(group_info);
1072 #endif
1073 }
1074
1075 fclose(fptr);
1076
1077 if (found)
1078 {
1079 return group_info;
1080 }
1081 else
1082 {
1083 // Failure to find the user means we just set errno to zero.
1084 // Perhaps not optimal, but we cannot pass ENOENT, because the fopen might
1085 // fail for this reason, and that should not be treated the same.
1086 errno = 0;
1087 return NULL;
1088 }
1089 }
1090
TransformGidsToGroups(StringSet ** list)1091 static void TransformGidsToGroups(StringSet **list)
1092 {
1093 StringSet *new_list = StringSetNew();
1094 StringSetIterator i = StringSetIteratorInit(*list);
1095 const char *data;
1096 for (data = StringSetIteratorNext(&i); data; data = StringSetIteratorNext(&i))
1097 {
1098 if (strlen(data) != strspn(data, "0123456789"))
1099 {
1100 // Cannot possibly be a gid.
1101 StringSetAdd(new_list, xstrdup(data));
1102 continue;
1103 }
1104 // In groups vs gids, groups take precedence. So check if it exists.
1105 struct group *group_info = GetGrEntry(data, &EqualGroupName);
1106 if (!group_info)
1107 {
1108 if (errno == 0)
1109 {
1110 group_info = GetGrEntry(data, &EqualGid);
1111 if (!group_info)
1112 {
1113 if (errno != 0)
1114 {
1115 Log(LOG_LEVEL_ERR, "Error while checking group name '%s': %s", data, GetErrorStr());
1116 StringSetDestroy(new_list);
1117 return;
1118 }
1119 // Neither group nor gid is found. This will lead to an error later, but we don't
1120 // handle that here.
1121 }
1122 else
1123 {
1124 // Replace gid with group name.
1125 StringSetAdd(new_list, xstrdup(group_info->gr_name));
1126 }
1127 }
1128 else
1129 {
1130 Log(LOG_LEVEL_ERR, "Error while checking group name '%s': '%s'", data, GetErrorStr());
1131 StringSetDestroy(new_list);
1132 return;
1133 }
1134 }
1135 else
1136 {
1137 StringSetAdd(new_list, xstrdup(data));
1138 }
1139 #ifdef __FreeBSD__
1140 free(group_info);
1141 #endif
1142 }
1143 StringSet *old_list = *list;
1144 *list = new_list;
1145 StringSetDestroy(old_list);
1146 }
1147
VerifyIfUserNeedsModifs(const char * puser,const User * u,const struct passwd * passwd_info,uint32_t * changemap,StringSet * groups_to_set,StringSet * current_secondary_groups)1148 static bool VerifyIfUserNeedsModifs(const char *puser, const User *u,
1149 const struct passwd *passwd_info,
1150 uint32_t *changemap,
1151 StringSet *groups_to_set,
1152 StringSet *current_secondary_groups)
1153 {
1154 assert(u != NULL);
1155 assert(passwd_info != NULL);
1156
1157 if (u->description != NULL &&
1158 !StringEqual(u->description, passwd_info->pw_gecos))
1159 {
1160 CFUSR_SETBIT(*changemap, i_comment);
1161 }
1162
1163 if (u->uid != NULL)
1164 {
1165 unsigned long uid;
1166 int ret = StringToUlong(u->uid, &uid);
1167 if (ret != 0 || uid != passwd_info->pw_uid)
1168 {
1169 CFUSR_SETBIT(*changemap, i_uid);
1170 }
1171 }
1172
1173 if (u->home_dir != NULL && !StringEqual(u->home_dir, passwd_info->pw_dir))
1174 {
1175 CFUSR_SETBIT(*changemap, i_home);
1176 }
1177
1178 if (u->shell != NULL && !StringEqual(u->shell, passwd_info->pw_shell))
1179 {
1180 CFUSR_SETBIT(*changemap, i_shell);
1181 }
1182
1183 bool account_is_locked = IsAccountLocked(puser, passwd_info);
1184 if ((!account_is_locked && u->policy == USER_STATE_LOCKED)
1185 || (account_is_locked && u->policy != USER_STATE_LOCKED))
1186 {
1187 CFUSR_SETBIT(*changemap, i_locked);
1188 }
1189
1190 // Don't bother with passwords if the account is going to be locked anyway.
1191 if (u->password != NULL && !StringEqual(u->password, "")
1192 && u->policy != USER_STATE_LOCKED)
1193 {
1194 if (!IsPasswordCorrect(puser, u->password, u->password_format, passwd_info))
1195 {
1196 CFUSR_SETBIT(*changemap, i_password);
1197 }
1198 }
1199
1200 if (u->groups_secondary_given && !StringSetIsEqual(groups_to_set, current_secondary_groups))
1201 {
1202 CFUSR_SETBIT(*changemap, i_groups);
1203 }
1204
1205 if (SafeStringLength(u->group_primary) != 0)
1206 {
1207 bool group_could_be_gid = (strlen(u->group_primary) ==
1208 strspn(u->group_primary, "0123456789"));
1209
1210 // We try name first, even if it looks like a gid. Only fall back to gid.
1211 errno = 0;
1212 struct group *group_info = GetGrEntry(u->group_primary,
1213 &EqualGroupName);
1214 if (group_info == NULL && errno != 0)
1215 {
1216 Log(LOG_LEVEL_ERR,
1217 "Could not obtain information about group '%s': %s",
1218 u->group_primary, GetErrorStr());
1219 CFUSR_SETBIT(*changemap, i_group);
1220 }
1221 else if (group_info == NULL)
1222 {
1223 if (group_could_be_gid)
1224 {
1225 unsigned long gid;
1226 int ret = StringToUlong(u->group_primary, &gid);
1227 if (ret != 0)
1228 {
1229 LogStringToLongError(u->group_primary,
1230 "VerifyIfUserNeedsModifs", ret);
1231 CFUSR_SETBIT(*changemap, i_group);
1232 }
1233 if (gid != passwd_info->pw_gid)
1234 {
1235 CFUSR_SETBIT(*changemap, i_group);
1236 }
1237 }
1238 else
1239 {
1240 Log(LOG_LEVEL_ERR, "No such group '%s'.", u->group_primary);
1241 CFUSR_SETBIT(*changemap, i_group);
1242 }
1243 }
1244 else
1245 {
1246 if (group_info->gr_gid != passwd_info->pw_gid)
1247 {
1248 CFUSR_SETBIT(*changemap, i_group);
1249 }
1250 }
1251
1252 #ifdef __FreeBSD__
1253 free(group_info);
1254 #endif
1255 }
1256
1257 ////////////////////////////////////////////
1258 if (*changemap == 0)
1259 {
1260 return false;
1261 }
1262 else
1263 {
1264 return true;
1265 }
1266 }
1267
SupportsOption(const char * cmd,const char * option)1268 static bool SupportsOption(const char *cmd, const char *option)
1269 {
1270 bool supports_option = false;
1271 char help_argument[] = " --help";
1272 char help_command[strlen(cmd) + sizeof(help_argument)];
1273 xsnprintf(help_command, sizeof(help_command), "%s%s", cmd, help_argument);
1274
1275 FILE *fptr = cf_popen(help_command, "r", true);
1276 char *buf = NULL;
1277 size_t bufsize = 0;
1278 size_t optlen = strlen(option);
1279 while (CfReadLine(&buf, &bufsize, fptr) >= 0)
1280 {
1281 char *m_pos = buf;
1282 while ((m_pos = strstr(m_pos, option)))
1283 {
1284 // Check against false alarms, e.g. hyphenated words in normal text or an
1285 // option (say, "-M") that is part of "--M".
1286 if ((m_pos == buf
1287 || (m_pos[-1] != '-' && (isspace(m_pos[-1]) || ispunct(m_pos[-1]))))
1288 && (m_pos[optlen] == '\0'
1289 || (isspace(m_pos[optlen]) || ispunct(m_pos[optlen]))))
1290 {
1291 supports_option = true;
1292 // Break out of strstr loop, but read till the end to avoid broken pipes.
1293 break;
1294 }
1295 m_pos++;
1296 }
1297 }
1298 cf_pclose(fptr);
1299 free(buf);
1300
1301 return supports_option;
1302 }
1303
1304 #ifdef HAVE_USERADD
DoCreateUserUsingUseradd(const char * puser,const User * u,enum cfopaction action,EvalContext * ctx,const Attributes * a,const Promise * pp)1305 static bool DoCreateUserUsingUseradd(const char *puser, const User *u, enum cfopaction action,
1306 EvalContext *ctx, const Attributes *a, const Promise *pp)
1307 {
1308 assert(u != NULL);
1309 char cmd[CF_BUFSIZE];
1310 char sec_group_args[CF_BUFSIZE];
1311 if (puser == NULL || !strcmp (puser, ""))
1312 {
1313 return false;
1314 }
1315 strcpy (cmd, USERADD);
1316
1317 if (u->uid != NULL && strcmp (u->uid, ""))
1318 {
1319 StringAppend(cmd, " -u \"", sizeof(cmd));
1320 StringAppend(cmd, u->uid, sizeof(cmd));
1321 StringAppend(cmd, "\"", sizeof(cmd));
1322 }
1323
1324 if (u->description != NULL)
1325 {
1326 StringAppend(cmd, " -c \"", sizeof(cmd));
1327 StringAppend(cmd, u->description, sizeof(cmd));
1328 StringAppend(cmd, "\"", sizeof(cmd));
1329 }
1330
1331 if (u->group_primary != NULL && strcmp (u->group_primary, ""))
1332 {
1333 // TODO: Should check that group exists
1334 StringAppend(cmd, " -g \"", sizeof(cmd));
1335 StringAppend(cmd, u->group_primary, sizeof(cmd));
1336 StringAppend(cmd, "\"", sizeof(cmd));
1337 }
1338
1339 if (u->groups_secondary_given)
1340 {
1341 // TODO: Should check that groups exist
1342 strlcpy(sec_group_args, " -G \"", sizeof(sec_group_args));
1343 char sep[2] = { '\0', '\0' };
1344 for (Rlist *i = u->groups_secondary; i; i = i->next)
1345 {
1346 StringAppend(sec_group_args, sep, sizeof(sec_group_args));
1347 StringAppend(sec_group_args, RvalScalarValue(i->val), sizeof(sec_group_args));
1348 sep[0] = ',';
1349 }
1350 StringAppend(sec_group_args, "\"", sizeof(sec_group_args));
1351 StringAppend(cmd, sec_group_args, sizeof(cmd));
1352 }
1353
1354 if (u->home_dir != NULL && strcmp (u->home_dir, ""))
1355 {
1356 StringAppend(cmd, " -d \"", sizeof(cmd));
1357 StringAppend(cmd, u->home_dir, sizeof(cmd));
1358 StringAppend(cmd, "\"", sizeof(cmd));
1359 }
1360 if (u->shell != NULL && strcmp (u->shell, ""))
1361 {
1362 StringAppend(cmd, " -s \"", sizeof(cmd));
1363 StringAppend(cmd, u->shell, sizeof(cmd));
1364 StringAppend(cmd, "\"", sizeof(cmd));
1365 }
1366
1367 #ifndef __hpux
1368 // HP-UX has two variants of useradd, the normal one which does
1369 // not support -M and one variant to modify default values which
1370 // does take -M and yes or no
1371 // Since both are output with -h SupportOption incorrectly reports
1372 // -M as supported
1373 if (SupportsOption(USERADD, "-M"))
1374 {
1375 // Prevents creation of home_dir.
1376 // We want home_bundle to do that.
1377 StringAppend(cmd, " -M", sizeof(cmd));
1378 }
1379 #endif
1380 StringAppend(cmd, " ", sizeof(cmd));
1381 StringAppend(cmd, puser, sizeof(cmd));
1382
1383 if (action == cfa_warn || DONTDO)
1384 {
1385 Log(LOG_LEVEL_WARNING, "Need to create user '%s'.", puser);
1386 return false;
1387 }
1388 else
1389 {
1390 if (!ExecuteUserCommand(puser, cmd, sizeof(cmd), "creating", "Creating"))
1391 {
1392 return false;
1393 }
1394
1395 if (u->groups_secondary_given)
1396 {
1397 // Work around issue on AIX. Always set secondary groups a second time, because AIX
1398 // likes to assign the primary group as the secondary group as well, even if we didn't
1399 // ask for it.
1400 strlcpy(cmd, USERMOD, sizeof(cmd));
1401 StringAppend(cmd, sec_group_args, sizeof(cmd));
1402 StringAppend(cmd, " ", sizeof(cmd));
1403 StringAppend(cmd, puser, sizeof(cmd));
1404 if (!ExecuteUserCommand(puser, cmd, sizeof(cmd), "modifying", "Modifying"))
1405 {
1406 return false;
1407 }
1408 }
1409
1410 // Initially, "useradd" may set the password to '!', which confuses our detection for
1411 // locked accounts. So reset it to 'x' hash instead, which will never match anything.
1412 if (!ChangePassword(puser, "x", PASSWORD_FORMAT_HASH))
1413 {
1414 return false;
1415 }
1416
1417 if (u->policy == USER_STATE_LOCKED)
1418 {
1419 if (!SetAccountLocked(puser, "x", true))
1420 {
1421 return false;
1422 }
1423 }
1424
1425 if (a->havebundle)
1426 {
1427 const Constraint *method_attrib = PromiseGetConstraint(pp, "home_bundle");
1428 if (method_attrib == NULL)
1429 {
1430 Log(LOG_LEVEL_ERR, "Cannot create user (home_bundle not found)");
1431 return false;
1432 }
1433 VerifyMethod(ctx, method_attrib->rval, a, pp);
1434 }
1435
1436 if (u->policy != USER_STATE_LOCKED && u->password != NULL && strcmp (u->password, ""))
1437 {
1438 if (!ChangePassword(puser, u->password, u->password_format))
1439 {
1440 return false;
1441 }
1442 }
1443 }
1444
1445 return true;
1446 }
1447 #endif
1448
1449 #ifdef HAVE_PW
DoCreateUserUsingPw(const char * puser,const User * u,enum cfopaction action,EvalContext * ctx,const Attributes * a,const Promise * pp)1450 static bool DoCreateUserUsingPw(const char *puser, const User *u, enum cfopaction action,
1451 EvalContext *ctx, const Attributes *a, const Promise *pp)
1452 {
1453 assert(u != NULL);
1454 char cmd[CF_BUFSIZE];
1455 char sec_group_args[CF_BUFSIZE];
1456 if (NULL_OR_EMPTY(puser))
1457 {
1458 return false;
1459 }
1460 strcpy (cmd, PW);
1461
1462 StringAppend(cmd, " useradd ", sizeof(cmd));
1463 StringAppend(cmd, puser, sizeof(cmd));
1464
1465 if (!NULL_OR_EMPTY(u->uid))
1466 {
1467 StringAppend(cmd, " -u \"", sizeof(cmd));
1468 StringAppend(cmd, u->uid, sizeof(cmd));
1469 StringAppend(cmd, "\"", sizeof(cmd));
1470 }
1471
1472 if (u->description != NULL)
1473 {
1474 StringAppend(cmd, " -c \"", sizeof(cmd));
1475 StringAppend(cmd, u->description, sizeof(cmd));
1476 StringAppend(cmd, "\"", sizeof(cmd));
1477 }
1478
1479 if (u->group_primary != NULL && strcmp (u->group_primary, ""))
1480 {
1481 // TODO: Should check that group exists
1482 StringAppend(cmd, " -g \"", sizeof(cmd));
1483 StringAppend(cmd, u->group_primary, sizeof(cmd));
1484 StringAppend(cmd, "\"", sizeof(cmd));
1485 }
1486
1487 if (u->groups_secondary_given)
1488 {
1489 // TODO: Should check that groups exist
1490 strlcpy(sec_group_args, " -G \"", sizeof(sec_group_args));
1491 char sep[2] = { '\0', '\0' };
1492 for (Rlist *i = u->groups_secondary; i != NULL; i = i->next)
1493 {
1494 StringAppend(sec_group_args, sep, sizeof(sec_group_args));
1495 StringAppend(sec_group_args, RvalScalarValue(i->val), sizeof(sec_group_args));
1496 sep[0] = ',';
1497 }
1498 StringAppend(sec_group_args, "\"", sizeof(sec_group_args));
1499 StringAppend(cmd, sec_group_args, sizeof(cmd));
1500 }
1501
1502 if (u->home_dir != NULL && strcmp(u->home_dir, ""))
1503 {
1504 StringAppend(cmd, " -d \"", sizeof(cmd));
1505 StringAppend(cmd, u->home_dir, sizeof(cmd));
1506 StringAppend(cmd, "\"", sizeof(cmd));
1507 }
1508
1509 if (u->shell != NULL && strcmp (u->shell, ""))
1510 {
1511 StringAppend(cmd, " -s \"", sizeof(cmd));
1512 StringAppend(cmd, u->shell, sizeof(cmd));
1513 StringAppend(cmd, "\"", sizeof(cmd));
1514 }
1515
1516 if (action == cfa_warn || DONTDO)
1517 {
1518 Log(LOG_LEVEL_WARNING, "Need to create user '%s'", puser);
1519 return false;
1520 }
1521 else
1522 {
1523 if (!ExecuteUserCommand(puser, cmd, sizeof(cmd), "creating", "Creating"))
1524 {
1525 return false;
1526 }
1527
1528 if (!ChangePassword(puser, "x", PASSWORD_FORMAT_HASH))
1529 {
1530 return false;
1531 }
1532
1533 if (u->policy == USER_STATE_LOCKED)
1534 {
1535 if (!SetAccountLocked(puser, "x", true))
1536 {
1537 return false;
1538 }
1539 }
1540
1541 if (a->havebundle)
1542 {
1543 const Constraint *method_attrib = PromiseGetConstraint(pp, "home_bundle");
1544 if (method_attrib == NULL)
1545 {
1546 Log(LOG_LEVEL_ERR, "Cannot create user (home_bundle not found)");
1547 return false;
1548 }
1549 VerifyMethod(ctx, method_attrib->rval, a, pp);
1550 }
1551
1552 if (u->policy != USER_STATE_LOCKED && u->password != NULL && strcmp (u->password, ""))
1553 {
1554 if (!ChangePassword(puser, u->password, u->password_format))
1555 {
1556 return false;
1557 }
1558 }
1559 }
1560
1561 return true;
1562 }
1563 #endif
1564
DoCreateUser(const char * puser,const User * u,enum cfopaction action,EvalContext * ctx,const Attributes * a,const Promise * pp)1565 static bool DoCreateUser(const char *puser, const User *u, enum cfopaction action,
1566 EvalContext *ctx, const Attributes *a, const Promise *pp)
1567 {
1568 #if defined(HAVE_USERADD)
1569 return DoCreateUserUsingUseradd(puser, u, action, ctx, a, pp);
1570 #elif defined(HAVE_PW)
1571 return DoCreateUserUsingPw(puser, u, action, ctx, a, pp);
1572 #else
1573 Log(LOG_LEVEL_WARNING, "Cannot create user, not supported on this platform.");
1574 return false;
1575 #endif
1576
1577 }
1578
1579 #ifdef HAVE_PW
DoRemoveUserUsingPw(const char * puser,enum cfopaction action)1580 static bool DoRemoveUserUsingPw (const char *puser, enum cfopaction action)
1581 {
1582 char cmd[CF_BUFSIZE];
1583
1584 strcpy(cmd, PW);
1585
1586 StringAppend(cmd, " userdel ", sizeof(cmd));
1587 StringAppend(cmd, puser, sizeof(cmd));
1588
1589 if (action == cfa_warn || DONTDO)
1590 {
1591 Log(LOG_LEVEL_WARNING, "Need to remove user '%s'.", puser);
1592 return false;
1593 }
1594
1595 return ExecuteUserCommand(puser, cmd, sizeof(cmd), "removing", "Removing");
1596 }
1597 #endif
1598
1599 #ifdef HAVE_USERDEL
DoRemoveUserUsingUserdel(const char * puser,enum cfopaction action)1600 static bool DoRemoveUserUsingUserdel (const char *puser, enum cfopaction action)
1601 {
1602 char cmd[CF_BUFSIZE];
1603
1604 strcpy (cmd, USERDEL);
1605
1606 StringAppend(cmd, " ", sizeof(cmd));
1607 StringAppend(cmd, puser, sizeof(cmd));
1608
1609 if (action == cfa_warn || DONTDO)
1610 {
1611 Log(LOG_LEVEL_WARNING, "Need to remove user '%s'.", puser);
1612 return false;
1613 }
1614
1615 return ExecuteUserCommand(puser, cmd, sizeof(cmd), "removing", "Removing");
1616 }
1617 #endif
1618
DoRemoveUser(const char * puser,enum cfopaction action)1619 static bool DoRemoveUser (const char *puser, enum cfopaction action)
1620 {
1621 #if defined(HAVE_PW)
1622 return DoRemoveUserUsingPw(puser, action);
1623 #elif defined(HAVE_USERDEL)
1624 return DoRemoveUserUsingUserdel(puser, action);
1625 #else
1626 Log(LOG_LEVEL_WARNING, "Removing user '%s' not supported on this platform.", puser);
1627 return false;
1628 #endif
1629 }
1630
DoModifyUser(const char * puser,const User * u,const struct passwd * passwd_info,uint32_t changemap,enum cfopaction action,StringSet * groups_to_set)1631 static bool DoModifyUser (const char *puser, const User *u, const struct passwd *passwd_info, uint32_t changemap, enum cfopaction action, StringSet *groups_to_set)
1632 {
1633 assert(u != NULL);
1634 char cmd[CF_BUFSIZE];
1635
1636 #ifdef HAVE_PW
1637 strcpy (cmd, PW);
1638 StringAppend(cmd, " usermod -n \"", sizeof(cmd));
1639 StringAppend(cmd, puser, sizeof(cmd));
1640 StringAppend(cmd, "\" ", sizeof(cmd));
1641 #else
1642 strcpy (cmd, USERMOD);
1643 #endif
1644
1645 if (CFUSR_CHECKBIT (changemap, i_uid) != 0)
1646 {
1647 StringAppend(cmd, " -u \"", sizeof(cmd));
1648 StringAppend(cmd, u->uid, sizeof(cmd));
1649 StringAppend(cmd, "\"", sizeof(cmd));
1650 }
1651
1652 if (CFUSR_CHECKBIT (changemap, i_comment) != 0)
1653 {
1654 StringAppend(cmd, " -c \"", sizeof(cmd));
1655 StringAppend(cmd, u->description, sizeof(cmd));
1656 StringAppend(cmd, "\"", sizeof(cmd));
1657 }
1658
1659 if (CFUSR_CHECKBIT (changemap, i_group) != 0)
1660 {
1661 StringAppend(cmd, " -g \"", sizeof(cmd));
1662 StringAppend(cmd, u->group_primary, sizeof(cmd));
1663 StringAppend(cmd, "\"", sizeof(cmd));
1664 }
1665
1666 if (CFUSR_CHECKBIT (changemap, i_home) != 0)
1667 {
1668 StringAppend(cmd, " -d \"", sizeof(cmd));
1669 StringAppend(cmd, u->home_dir, sizeof(cmd));
1670 StringAppend(cmd, "\"", sizeof(cmd));
1671 }
1672
1673 if (CFUSR_CHECKBIT (changemap, i_shell) != 0)
1674 {
1675 StringAppend(cmd, " -s \"", sizeof(cmd));
1676 StringAppend(cmd, u->shell, sizeof(cmd));
1677 StringAppend(cmd, "\"", sizeof(cmd));
1678 }
1679
1680 if (CFUSR_CHECKBIT (changemap, i_password) != 0)
1681 {
1682 if (action == cfa_warn || DONTDO)
1683 {
1684 Log(LOG_LEVEL_WARNING, "Need to change password for user '%s'.", puser);
1685 return false;
1686 }
1687 else
1688 {
1689 if (!ChangePassword(puser, u->password, u->password_format))
1690 {
1691 return false;
1692 }
1693 }
1694 }
1695
1696 if (CFUSR_CHECKBIT (changemap, i_locked) != 0)
1697 {
1698 if (action == cfa_warn || DONTDO)
1699 {
1700 Log(LOG_LEVEL_WARNING, "Need to %s account for user '%s'.",
1701 (u->policy == USER_STATE_LOCKED) ? "lock" : "unlock", puser);
1702 return false;
1703 }
1704 else
1705 {
1706 const char *hash;
1707 if (CFUSR_CHECKBIT(changemap, i_password) == 0)
1708 {
1709 if (!GetPasswordHash(puser, passwd_info, &hash))
1710 {
1711 return false;
1712 }
1713 }
1714 else
1715 {
1716 // Don't unlock the hash if we already set the password. Our
1717 // cached value in passwd_info->pw_passwd will be wrong, and the
1718 // account will already have been unlocked anyway.
1719 hash = NULL;
1720 }
1721 if (!SetAccountLocked(puser, hash, (u->policy == USER_STATE_LOCKED)))
1722 {
1723 return false;
1724 }
1725 }
1726 }
1727
1728 if (CFUSR_CHECKBIT (changemap, i_groups) != 0)
1729 {
1730 StringAppend(cmd, " -G \"", sizeof(cmd));
1731 Buffer *buf = BufferNew();
1732 buf = StringSetToBuffer(groups_to_set, ',');
1733 StringAppend(cmd, buf->buffer, sizeof(cmd));
1734 BufferDestroy(buf);
1735 StringAppend(cmd, "\" ", sizeof(cmd));
1736 }
1737 #ifndef HAVE_PW
1738 StringAppend(cmd, " ", sizeof(cmd));
1739 StringAppend(cmd, puser, sizeof(cmd));
1740 #endif
1741 // If password and locking were the only things changed, don't run the command.
1742 CFUSR_CLEARBIT(changemap, i_password);
1743 CFUSR_CLEARBIT(changemap, i_locked);
1744 if (action == cfa_warn || DONTDO)
1745 {
1746 Log(LOG_LEVEL_WARNING, "Need to update user attributes (command '%s').", cmd);
1747 return false;
1748 }
1749 else if (changemap != 0)
1750 {
1751
1752 if (!ExecuteUserCommand(puser, cmd, sizeof(cmd), "modifying", "Modifying"))
1753 {
1754 return false;
1755 }
1756 }
1757 return true;
1758 }
1759
1760 #ifdef __FreeBSD__
fgetpwent(FILE * stream)1761 struct passwd *fgetpwent(FILE *stream)
1762 {
1763 if (stream == NULL)
1764 {
1765 return NULL;
1766 }
1767
1768 struct passwd *pw = NULL;
1769 char *line = NULL;
1770 size_t linecap = 0;
1771 ssize_t linelen;
1772 int pwd_scanflag = 0;
1773
1774 while ((linelen = getline(&line, &linecap, stream)) > 0)
1775 {
1776 /* Skip comments and empty lines */
1777 if (*line == '\n' || *line == '#')
1778 {
1779 continue;
1780 }
1781 /* trim latest \n */
1782 if (line[linelen - 1 ] == '\n')
1783 {
1784 line[linelen - 1] = '\0';
1785 }
1786 pw = pw_scan(line, pwd_scanflag);
1787 if (pw != NULL)
1788 {
1789 break;
1790 }
1791 }
1792 free(line);
1793
1794 return pw;
1795 }
1796 #endif
1797
1798 // Uses fgetpwent() instead of getpwnam(), to guarantee that the returned user
1799 // is a local user, and not for example from LDAP.
GetPwEntry(const char * puser)1800 static struct passwd *GetPwEntry(const char *puser)
1801 {
1802 FILE *fptr = safe_fopen("/etc/passwd", "r");
1803 if (!fptr)
1804 {
1805 Log(LOG_LEVEL_ERR, "Could not open '/etc/passwd': %s", GetErrorStr());
1806 return NULL;
1807 }
1808
1809 struct passwd *passwd_info;
1810 bool found = false;
1811 while ((passwd_info = fgetpwent(fptr)))
1812 {
1813 if (strcmp(puser, passwd_info->pw_name) == 0)
1814 {
1815 found = true;
1816 break;
1817 }
1818 #ifdef __FreeBSD__
1819 free(passwd_info);
1820 #endif
1821 }
1822
1823 fclose(fptr);
1824
1825 if (found)
1826 {
1827 return passwd_info;
1828 }
1829 else
1830 {
1831 // Failure to find the user means we just set errno to zero.
1832 // Perhaps not optimal, but we cannot pass ENOENT, because the fopen might
1833 // fail for this reason, and that should not be treated the same.
1834 errno = 0;
1835 return NULL;
1836 }
1837 }
1838
VerifyOneUsersPromise(const char * puser,const User * u,PromiseResult * result,enum cfopaction action,EvalContext * ctx,const Attributes * a,const Promise * pp)1839 void VerifyOneUsersPromise (const char *puser, const User *u, PromiseResult *result, enum cfopaction action,
1840 EvalContext *ctx, const Attributes *a, const Promise *pp)
1841 {
1842 assert(u != NULL);
1843
1844 struct passwd *passwd_info = GetPwEntry(puser);
1845 if (!passwd_info && errno != 0)
1846 {
1847 Log(LOG_LEVEL_ERR, "Could not get information from user database.");
1848 return;
1849 }
1850
1851 bool res;
1852 if (u->policy == USER_STATE_PRESENT || u->policy == USER_STATE_LOCKED)
1853 {
1854 if (passwd_info)
1855 {
1856 StringSet *groups_to_set = StringSetNew();
1857 StringSet *current_secondary_groups = StringSetNew();
1858 StringSet *groups_missing = StringSetNew();
1859 res = GetGroupInfo(puser, u, &groups_to_set, &groups_missing, ¤t_secondary_groups);
1860 if (res)
1861 {
1862 uint32_t cmap = 0;
1863 if (VerifyIfUserNeedsModifs (puser, u, passwd_info, &cmap, groups_to_set, current_secondary_groups))
1864 {
1865 res = DoModifyUser (puser, u, passwd_info, cmap, action, groups_to_set);
1866 if (res)
1867 {
1868 *result = PROMISE_RESULT_CHANGE;
1869 }
1870 else
1871 {
1872 *result = PROMISE_RESULT_FAIL;
1873 }
1874 }
1875 else
1876 {
1877 *result = PROMISE_RESULT_NOOP;
1878 }
1879 }
1880 else
1881 {
1882 *result = PROMISE_RESULT_FAIL;
1883 }
1884 StringSetDestroy(groups_to_set);
1885 StringSetDestroy(current_secondary_groups);
1886 StringSetDestroy(groups_missing);
1887 }
1888 else
1889 {
1890 res = DoCreateUser (puser, u, action, ctx, a, pp);
1891 if (res)
1892 {
1893 *result = PROMISE_RESULT_CHANGE;
1894 }
1895 else
1896 {
1897 *result = PROMISE_RESULT_FAIL;
1898 }
1899 }
1900 }
1901 else if (u->policy == USER_STATE_ABSENT)
1902 {
1903 if (passwd_info)
1904 {
1905 res = DoRemoveUser (puser, action);
1906 if (res)
1907 {
1908 *result = PROMISE_RESULT_CHANGE;
1909 }
1910 else
1911 {
1912 *result = PROMISE_RESULT_FAIL;
1913 }
1914 }
1915 else
1916 {
1917 *result = PROMISE_RESULT_NOOP;
1918 }
1919 }
1920 #ifdef __FreeBSD__
1921 free(passwd_info);
1922 #endif
1923 }
1924