1 /*
2 Copyright 2020 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 return (atoi(key) == entry->gr_gid);
995 }
996
EqualGroupName(const char * key,const struct group * entry)997 static bool EqualGroupName(const char *key, const struct group *entry)
998 {
999 assert(entry != NULL);
1000 return (strcmp(key, entry->gr_name) == 0);
1001 }
1002
1003 #ifdef __FreeBSD__
fgetgrent(FILE * stream)1004 struct group *fgetgrent(FILE *stream)
1005 {
1006 if (stream == NULL)
1007 {
1008 return NULL;
1009 }
1010
1011 struct group *gr = NULL;
1012 char *line = NULL;
1013 size_t linecap = 0;
1014 ssize_t linelen;
1015
1016 while ((linelen = getline(&line, &linecap, stream)) > 0)
1017 {
1018 /* Skip comments and empty lines */
1019 if (*line == '\n' || *line == '#')
1020 {
1021 continue;
1022 }
1023 /* trim latest \n */
1024 if (line[linelen - 1] == '\n')
1025 {
1026 line[linelen - 1] = '\0';
1027 }
1028 gr = gr_scan(line);
1029 if (gr != NULL)
1030 {
1031 break;
1032 }
1033 }
1034 free(line);
1035
1036 return gr;
1037 }
1038 #endif
1039
1040 // Uses fgetgrent() instead of getgrnam(), to guarantee that the returned group
1041 // is a local group, and not for example from LDAP.
GetGrEntry(const char * key,bool (* equal_fn)(const char * key,const struct group * entry))1042 static struct group *GetGrEntry(const char *key,
1043 bool (*equal_fn)(const char *key, const struct group *entry))
1044 {
1045 FILE *fptr = safe_fopen("/etc/group", "r");
1046 if (!fptr)
1047 {
1048 Log(LOG_LEVEL_ERR, "Could not open '/etc/group': %s", GetErrorStr());
1049 return NULL;
1050 }
1051
1052 struct group *group_info;
1053 bool found = false;
1054 while ((group_info = fgetgrent(fptr)))
1055 {
1056 if (equal_fn(key, group_info))
1057 {
1058 found = true;
1059 break;
1060 }
1061 #ifdef __FreeBSD__
1062 free(group_info);
1063 #endif
1064 }
1065
1066 fclose(fptr);
1067
1068 if (found)
1069 {
1070 return group_info;
1071 }
1072 else
1073 {
1074 // Failure to find the user means we just set errno to zero.
1075 // Perhaps not optimal, but we cannot pass ENOENT, because the fopen might
1076 // fail for this reason, and that should not be treated the same.
1077 errno = 0;
1078 return NULL;
1079 }
1080 }
1081
TransformGidsToGroups(StringSet ** list)1082 static void TransformGidsToGroups(StringSet **list)
1083 {
1084 StringSet *new_list = StringSetNew();
1085 StringSetIterator i = StringSetIteratorInit(*list);
1086 const char *data;
1087 for (data = StringSetIteratorNext(&i); data; data = StringSetIteratorNext(&i))
1088 {
1089 if (strlen(data) != strspn(data, "0123456789"))
1090 {
1091 // Cannot possibly be a gid.
1092 StringSetAdd(new_list, xstrdup(data));
1093 continue;
1094 }
1095 // In groups vs gids, groups take precedence. So check if it exists.
1096 struct group *group_info = GetGrEntry(data, &EqualGroupName);
1097 if (!group_info)
1098 {
1099 if (errno == 0)
1100 {
1101 group_info = GetGrEntry(data, &EqualGid);
1102 if (!group_info)
1103 {
1104 if (errno != 0)
1105 {
1106 Log(LOG_LEVEL_ERR, "Error while checking group name '%s': %s", data, GetErrorStr());
1107 StringSetDestroy(new_list);
1108 return;
1109 }
1110 // Neither group nor gid is found. This will lead to an error later, but we don't
1111 // handle that here.
1112 }
1113 else
1114 {
1115 // Replace gid with group name.
1116 StringSetAdd(new_list, xstrdup(group_info->gr_name));
1117 }
1118 }
1119 else
1120 {
1121 Log(LOG_LEVEL_ERR, "Error while checking group name '%s': '%s'", data, GetErrorStr());
1122 StringSetDestroy(new_list);
1123 return;
1124 }
1125 }
1126 else
1127 {
1128 StringSetAdd(new_list, xstrdup(data));
1129 }
1130 #ifdef __FreeBSD__
1131 free(group_info);
1132 #endif
1133 }
1134 StringSet *old_list = *list;
1135 *list = new_list;
1136 StringSetDestroy(old_list);
1137 }
1138
VerifyIfUserNeedsModifs(const char * puser,const User * u,const struct passwd * passwd_info,uint32_t * changemap,StringSet * groups_to_set,StringSet * current_secondary_groups)1139 static bool VerifyIfUserNeedsModifs (const char *puser, const User *u, const struct passwd *passwd_info,
1140 uint32_t *changemap, StringSet *groups_to_set, StringSet *current_secondary_groups)
1141 {
1142 assert(u != NULL);
1143 if (u->description != NULL && strcmp (u->description, passwd_info->pw_gecos))
1144 {
1145 CFUSR_SETBIT (*changemap, i_comment);
1146 }
1147 if (u->uid != NULL && (atoi (u->uid) != passwd_info->pw_uid))
1148 {
1149 CFUSR_SETBIT (*changemap, i_uid);
1150 }
1151 if (u->home_dir != NULL && strcmp (u->home_dir, passwd_info->pw_dir))
1152 {
1153 CFUSR_SETBIT (*changemap, i_home);
1154 }
1155 if (u->shell != NULL && strcmp (u->shell, passwd_info->pw_shell))
1156 {
1157 CFUSR_SETBIT (*changemap, i_shell);
1158 }
1159 bool account_is_locked = IsAccountLocked(puser, passwd_info);
1160 if ((!account_is_locked && u->policy == USER_STATE_LOCKED)
1161 || (account_is_locked && u->policy != USER_STATE_LOCKED))
1162 {
1163 CFUSR_SETBIT(*changemap, i_locked);
1164 }
1165 // Don't bother with passwords if the account is going to be locked anyway.
1166 if (u->password != NULL && strcmp (u->password, "")
1167 && u->policy != USER_STATE_LOCKED)
1168 {
1169 if (!IsPasswordCorrect(puser, u->password, u->password_format, passwd_info))
1170 {
1171 CFUSR_SETBIT (*changemap, i_password);
1172 }
1173 }
1174 if (u->groups_secondary_given && !StringSetIsEqual(groups_to_set, current_secondary_groups))
1175 {
1176 CFUSR_SETBIT (*changemap, i_groups);
1177 }
1178 if (SafeStringLength(u->group_primary))
1179 {
1180 bool group_could_be_gid = (strlen(u->group_primary) == strspn(u->group_primary, "0123456789"));
1181 int gid;
1182
1183 // We try name first, even if it looks like a gid. Only fall back to gid.
1184 struct group *group_info;
1185 errno = 0;
1186 group_info = GetGrEntry(u->group_primary, &EqualGroupName);
1187 if (!group_info && errno != 0)
1188 {
1189 Log(LOG_LEVEL_ERR, "Could not obtain information about group '%s': %s", u->group_primary, GetErrorStr());
1190 gid = -1;
1191 }
1192 else if (!group_info)
1193 {
1194 if (group_could_be_gid)
1195 {
1196 gid = atoi(u->group_primary);
1197 }
1198 else
1199 {
1200 Log(LOG_LEVEL_ERR, "No such group '%s'.", u->group_primary);
1201 gid = -1;
1202 }
1203 }
1204 else
1205 {
1206 gid = group_info->gr_gid;
1207 }
1208
1209 if (gid != passwd_info->pw_gid)
1210 {
1211 CFUSR_SETBIT (*changemap, i_group);
1212 }
1213
1214 #ifdef __FreeBSD__
1215 free(group_info);
1216 #endif
1217 }
1218
1219 ////////////////////////////////////////////
1220 if (*changemap == 0)
1221 {
1222 return false;
1223 }
1224 else
1225 {
1226 return true;
1227 }
1228 }
1229
SupportsOption(const char * cmd,const char * option)1230 static bool SupportsOption(const char *cmd, const char *option)
1231 {
1232 bool supports_option = false;
1233 char help_argument[] = " --help";
1234 char help_command[strlen(cmd) + sizeof(help_argument)];
1235 xsnprintf(help_command, sizeof(help_command), "%s%s", cmd, help_argument);
1236
1237 FILE *fptr = cf_popen(help_command, "r", true);
1238 char *buf = NULL;
1239 size_t bufsize = 0;
1240 size_t optlen = strlen(option);
1241 while (CfReadLine(&buf, &bufsize, fptr) >= 0)
1242 {
1243 char *m_pos = buf;
1244 while ((m_pos = strstr(m_pos, option)))
1245 {
1246 // Check against false alarms, e.g. hyphenated words in normal text or an
1247 // option (say, "-M") that is part of "--M".
1248 if ((m_pos == buf
1249 || (m_pos[-1] != '-' && (isspace(m_pos[-1]) || ispunct(m_pos[-1]))))
1250 && (m_pos[optlen] == '\0'
1251 || (isspace(m_pos[optlen]) || ispunct(m_pos[optlen]))))
1252 {
1253 supports_option = true;
1254 // Break out of strstr loop, but read till the end to avoid broken pipes.
1255 break;
1256 }
1257 m_pos++;
1258 }
1259 }
1260 cf_pclose(fptr);
1261 free(buf);
1262
1263 return supports_option;
1264 }
1265
1266 #ifdef HAVE_USERADD
DoCreateUserUsingUseradd(const char * puser,const User * u,enum cfopaction action,EvalContext * ctx,const Attributes * a,const Promise * pp)1267 static bool DoCreateUserUsingUseradd(const char *puser, const User *u, enum cfopaction action,
1268 EvalContext *ctx, const Attributes *a, const Promise *pp)
1269 {
1270 assert(u != NULL);
1271 char cmd[CF_BUFSIZE];
1272 char sec_group_args[CF_BUFSIZE];
1273 if (puser == NULL || !strcmp (puser, ""))
1274 {
1275 return false;
1276 }
1277 strcpy (cmd, USERADD);
1278
1279 if (u->uid != NULL && strcmp (u->uid, ""))
1280 {
1281 StringAppend(cmd, " -u \"", sizeof(cmd));
1282 StringAppend(cmd, u->uid, sizeof(cmd));
1283 StringAppend(cmd, "\"", sizeof(cmd));
1284 }
1285
1286 if (u->description != NULL)
1287 {
1288 StringAppend(cmd, " -c \"", sizeof(cmd));
1289 StringAppend(cmd, u->description, sizeof(cmd));
1290 StringAppend(cmd, "\"", sizeof(cmd));
1291 }
1292
1293 if (u->group_primary != NULL && strcmp (u->group_primary, ""))
1294 {
1295 // TODO: Should check that group exists
1296 StringAppend(cmd, " -g \"", sizeof(cmd));
1297 StringAppend(cmd, u->group_primary, sizeof(cmd));
1298 StringAppend(cmd, "\"", sizeof(cmd));
1299 }
1300
1301 if (u->groups_secondary_given)
1302 {
1303 // TODO: Should check that groups exist
1304 strlcpy(sec_group_args, " -G \"", sizeof(sec_group_args));
1305 char sep[2] = { '\0', '\0' };
1306 for (Rlist *i = u->groups_secondary; i; i = i->next)
1307 {
1308 StringAppend(sec_group_args, sep, sizeof(sec_group_args));
1309 StringAppend(sec_group_args, RvalScalarValue(i->val), sizeof(sec_group_args));
1310 sep[0] = ',';
1311 }
1312 StringAppend(sec_group_args, "\"", sizeof(sec_group_args));
1313 StringAppend(cmd, sec_group_args, sizeof(cmd));
1314 }
1315
1316 if (u->home_dir != NULL && strcmp (u->home_dir, ""))
1317 {
1318 StringAppend(cmd, " -d \"", sizeof(cmd));
1319 StringAppend(cmd, u->home_dir, sizeof(cmd));
1320 StringAppend(cmd, "\"", sizeof(cmd));
1321 }
1322 if (u->shell != NULL && strcmp (u->shell, ""))
1323 {
1324 StringAppend(cmd, " -s \"", sizeof(cmd));
1325 StringAppend(cmd, u->shell, sizeof(cmd));
1326 StringAppend(cmd, "\"", sizeof(cmd));
1327 }
1328
1329 #ifndef __hpux
1330 // HP-UX has two variants of useradd, the normal one which does
1331 // not support -M and one variant to modify default values which
1332 // does take -M and yes or no
1333 // Since both are output with -h SupportOption incorrectly reports
1334 // -M as supported
1335 if (SupportsOption(USERADD, "-M"))
1336 {
1337 // Prevents creation of home_dir.
1338 // We want home_bundle to do that.
1339 StringAppend(cmd, " -M", sizeof(cmd));
1340 }
1341 #endif
1342 StringAppend(cmd, " ", sizeof(cmd));
1343 StringAppend(cmd, puser, sizeof(cmd));
1344
1345 if (action == cfa_warn || DONTDO)
1346 {
1347 Log(LOG_LEVEL_WARNING, "Need to create user '%s'.", puser);
1348 return false;
1349 }
1350 else
1351 {
1352 if (!ExecuteUserCommand(puser, cmd, sizeof(cmd), "creating", "Creating"))
1353 {
1354 return false;
1355 }
1356
1357 if (u->groups_secondary_given)
1358 {
1359 // Work around issue on AIX. Always set secondary groups a second time, because AIX
1360 // likes to assign the primary group as the secondary group as well, even if we didn't
1361 // ask for it.
1362 strlcpy(cmd, USERMOD, sizeof(cmd));
1363 StringAppend(cmd, sec_group_args, sizeof(cmd));
1364 StringAppend(cmd, " ", sizeof(cmd));
1365 StringAppend(cmd, puser, sizeof(cmd));
1366 if (!ExecuteUserCommand(puser, cmd, sizeof(cmd), "modifying", "Modifying"))
1367 {
1368 return false;
1369 }
1370 }
1371
1372 // Initially, "useradd" may set the password to '!', which confuses our detection for
1373 // locked accounts. So reset it to 'x' hash instead, which will never match anything.
1374 if (!ChangePassword(puser, "x", PASSWORD_FORMAT_HASH))
1375 {
1376 return false;
1377 }
1378
1379 if (u->policy == USER_STATE_LOCKED)
1380 {
1381 if (!SetAccountLocked(puser, "x", true))
1382 {
1383 return false;
1384 }
1385 }
1386
1387 if (a->havebundle)
1388 {
1389 const Constraint *method_attrib = PromiseGetConstraint(pp, "home_bundle");
1390 if (method_attrib == NULL)
1391 {
1392 Log(LOG_LEVEL_ERR, "Cannot create user (home_bundle not found)");
1393 return false;
1394 }
1395 VerifyMethod(ctx, method_attrib->rval, a, pp);
1396 }
1397
1398 if (u->policy != USER_STATE_LOCKED && u->password != NULL && strcmp (u->password, ""))
1399 {
1400 if (!ChangePassword(puser, u->password, u->password_format))
1401 {
1402 return false;
1403 }
1404 }
1405 }
1406
1407 return true;
1408 }
1409 #endif
1410
1411 #ifdef HAVE_PW
DoCreateUserUsingPw(const char * puser,const User * u,enum cfopaction action,EvalContext * ctx,const Attributes * a,const Promise * pp)1412 static bool DoCreateUserUsingPw(const char *puser, const User *u, enum cfopaction action,
1413 EvalContext *ctx, const Attributes *a, const Promise *pp)
1414 {
1415 assert(u != NULL);
1416 char cmd[CF_BUFSIZE];
1417 char sec_group_args[CF_BUFSIZE];
1418 if (NULL_OR_EMPTY(puser))
1419 {
1420 return false;
1421 }
1422 strcpy (cmd, PW);
1423
1424 StringAppend(cmd, " useradd ", sizeof(cmd));
1425 StringAppend(cmd, puser, sizeof(cmd));
1426
1427 if (!NULL_OR_EMPTY(u->uid))
1428 {
1429 StringAppend(cmd, " -u \"", sizeof(cmd));
1430 StringAppend(cmd, u->uid, sizeof(cmd));
1431 StringAppend(cmd, "\"", sizeof(cmd));
1432 }
1433
1434 if (u->description != NULL)
1435 {
1436 StringAppend(cmd, " -c \"", sizeof(cmd));
1437 StringAppend(cmd, u->description, sizeof(cmd));
1438 StringAppend(cmd, "\"", sizeof(cmd));
1439 }
1440
1441 if (u->group_primary != NULL && strcmp (u->group_primary, ""))
1442 {
1443 // TODO: Should check that group exists
1444 StringAppend(cmd, " -g \"", sizeof(cmd));
1445 StringAppend(cmd, u->group_primary, sizeof(cmd));
1446 StringAppend(cmd, "\"", sizeof(cmd));
1447 }
1448
1449 if (u->groups_secondary_given)
1450 {
1451 // TODO: Should check that groups exist
1452 strlcpy(sec_group_args, " -G \"", sizeof(sec_group_args));
1453 char sep[2] = { '\0', '\0' };
1454 for (Rlist *i = u->groups_secondary; i != NULL; i = i->next)
1455 {
1456 StringAppend(sec_group_args, sep, sizeof(sec_group_args));
1457 StringAppend(sec_group_args, RvalScalarValue(i->val), sizeof(sec_group_args));
1458 sep[0] = ',';
1459 }
1460 StringAppend(sec_group_args, "\"", sizeof(sec_group_args));
1461 StringAppend(cmd, sec_group_args, sizeof(cmd));
1462 }
1463
1464 if (u->home_dir != NULL && strcmp(u->home_dir, ""))
1465 {
1466 StringAppend(cmd, " -d \"", sizeof(cmd));
1467 StringAppend(cmd, u->home_dir, sizeof(cmd));
1468 StringAppend(cmd, "\"", sizeof(cmd));
1469 }
1470
1471 if (u->shell != NULL && strcmp (u->shell, ""))
1472 {
1473 StringAppend(cmd, " -s \"", sizeof(cmd));
1474 StringAppend(cmd, u->shell, sizeof(cmd));
1475 StringAppend(cmd, "\"", sizeof(cmd));
1476 }
1477
1478 if (action == cfa_warn || DONTDO)
1479 {
1480 Log(LOG_LEVEL_WARNING, "Need to create user '%s'", puser);
1481 return false;
1482 }
1483 else
1484 {
1485 if (!ExecuteUserCommand(puser, cmd, sizeof(cmd), "creating", "Creating"))
1486 {
1487 return false;
1488 }
1489
1490 if (!ChangePassword(puser, "x", PASSWORD_FORMAT_HASH))
1491 {
1492 return false;
1493 }
1494
1495 if (u->policy == USER_STATE_LOCKED)
1496 {
1497 if (!SetAccountLocked(puser, "x", true))
1498 {
1499 return false;
1500 }
1501 }
1502
1503 if (a->havebundle)
1504 {
1505 const Constraint *method_attrib = PromiseGetConstraint(pp, "home_bundle");
1506 if (method_attrib == NULL)
1507 {
1508 Log(LOG_LEVEL_ERR, "Cannot create user (home_bundle not found)");
1509 return false;
1510 }
1511 VerifyMethod(ctx, method_attrib->rval, a, pp);
1512 }
1513
1514 if (u->policy != USER_STATE_LOCKED && u->password != NULL && strcmp (u->password, ""))
1515 {
1516 if (!ChangePassword(puser, u->password, u->password_format))
1517 {
1518 return false;
1519 }
1520 }
1521 }
1522
1523 return true;
1524 }
1525 #endif
1526
DoCreateUser(const char * puser,const User * u,enum cfopaction action,EvalContext * ctx,const Attributes * a,const Promise * pp)1527 static bool DoCreateUser(const char *puser, const User *u, enum cfopaction action,
1528 EvalContext *ctx, const Attributes *a, const Promise *pp)
1529 {
1530 #if defined(HAVE_USERADD)
1531 return DoCreateUserUsingUseradd(puser, u, action, ctx, a, pp);
1532 #elif defined(HAVE_PW)
1533 return DoCreateUserUsingPw(puser, u, action, ctx, a, pp);
1534 #else
1535 Log(LOG_LEVEL_WARNING, "Cannot create user, not supported on this platform.");
1536 return false;
1537 #endif
1538
1539 }
1540
1541 #ifdef HAVE_PW
DoRemoveUserUsingPw(const char * puser,enum cfopaction action)1542 static bool DoRemoveUserUsingPw (const char *puser, enum cfopaction action)
1543 {
1544 char cmd[CF_BUFSIZE];
1545
1546 strcpy(cmd, PW);
1547
1548 StringAppend(cmd, " userdel ", sizeof(cmd));
1549 StringAppend(cmd, puser, sizeof(cmd));
1550
1551 if (action == cfa_warn || DONTDO)
1552 {
1553 Log(LOG_LEVEL_WARNING, "Need to remove user '%s'.", puser);
1554 return false;
1555 }
1556
1557 return ExecuteUserCommand(puser, cmd, sizeof(cmd), "removing", "Removing");
1558 }
1559 #endif
1560
1561 #ifdef HAVE_USERDEL
DoRemoveUserUsingUserdel(const char * puser,enum cfopaction action)1562 static bool DoRemoveUserUsingUserdel (const char *puser, enum cfopaction action)
1563 {
1564 char cmd[CF_BUFSIZE];
1565
1566 strcpy (cmd, USERDEL);
1567
1568 StringAppend(cmd, " ", sizeof(cmd));
1569 StringAppend(cmd, puser, sizeof(cmd));
1570
1571 if (action == cfa_warn || DONTDO)
1572 {
1573 Log(LOG_LEVEL_WARNING, "Need to remove user '%s'.", puser);
1574 return false;
1575 }
1576
1577 return ExecuteUserCommand(puser, cmd, sizeof(cmd), "removing", "Removing");
1578 }
1579 #endif
1580
DoRemoveUser(const char * puser,enum cfopaction action)1581 static bool DoRemoveUser (const char *puser, enum cfopaction action)
1582 {
1583 #if defined(HAVE_PW)
1584 return DoRemoveUserUsingPw(puser, action);
1585 #elif defined(HAVE_USERDEL)
1586 return DoRemoveUserUsingUserdel(puser, action);
1587 #else
1588 Log(LOG_LEVEL_WARNING, "Removing user '%s' not supported on this platform.", puser);
1589 return false;
1590 #endif
1591 }
1592
DoModifyUser(const char * puser,const User * u,const struct passwd * passwd_info,uint32_t changemap,enum cfopaction action,StringSet * groups_to_set)1593 static bool DoModifyUser (const char *puser, const User *u, const struct passwd *passwd_info, uint32_t changemap, enum cfopaction action, StringSet *groups_to_set)
1594 {
1595 assert(u != NULL);
1596 char cmd[CF_BUFSIZE];
1597
1598 #ifdef HAVE_PW
1599 strcpy (cmd, PW);
1600 StringAppend(cmd, " usermod -n \"", sizeof(cmd));
1601 StringAppend(cmd, puser, sizeof(cmd));
1602 StringAppend(cmd, "\" ", sizeof(cmd));
1603 #else
1604 strcpy (cmd, USERMOD);
1605 #endif
1606
1607 if (CFUSR_CHECKBIT (changemap, i_uid) != 0)
1608 {
1609 StringAppend(cmd, " -u \"", sizeof(cmd));
1610 StringAppend(cmd, u->uid, sizeof(cmd));
1611 StringAppend(cmd, "\"", sizeof(cmd));
1612 }
1613
1614 if (CFUSR_CHECKBIT (changemap, i_comment) != 0)
1615 {
1616 StringAppend(cmd, " -c \"", sizeof(cmd));
1617 StringAppend(cmd, u->description, sizeof(cmd));
1618 StringAppend(cmd, "\"", sizeof(cmd));
1619 }
1620
1621 if (CFUSR_CHECKBIT (changemap, i_group) != 0)
1622 {
1623 StringAppend(cmd, " -g \"", sizeof(cmd));
1624 StringAppend(cmd, u->group_primary, sizeof(cmd));
1625 StringAppend(cmd, "\"", sizeof(cmd));
1626 }
1627
1628 if (CFUSR_CHECKBIT (changemap, i_home) != 0)
1629 {
1630 StringAppend(cmd, " -d \"", sizeof(cmd));
1631 StringAppend(cmd, u->home_dir, sizeof(cmd));
1632 StringAppend(cmd, "\"", sizeof(cmd));
1633 }
1634
1635 if (CFUSR_CHECKBIT (changemap, i_shell) != 0)
1636 {
1637 StringAppend(cmd, " -s \"", sizeof(cmd));
1638 StringAppend(cmd, u->shell, sizeof(cmd));
1639 StringAppend(cmd, "\"", sizeof(cmd));
1640 }
1641
1642 if (CFUSR_CHECKBIT (changemap, i_password) != 0)
1643 {
1644 if (action == cfa_warn || DONTDO)
1645 {
1646 Log(LOG_LEVEL_WARNING, "Need to change password for user '%s'.", puser);
1647 return false;
1648 }
1649 else
1650 {
1651 if (!ChangePassword(puser, u->password, u->password_format))
1652 {
1653 return false;
1654 }
1655 }
1656 }
1657
1658 if (CFUSR_CHECKBIT (changemap, i_locked) != 0)
1659 {
1660 if (action == cfa_warn || DONTDO)
1661 {
1662 Log(LOG_LEVEL_WARNING, "Need to %s account for user '%s'.",
1663 (u->policy == USER_STATE_LOCKED) ? "lock" : "unlock", puser);
1664 return false;
1665 }
1666 else
1667 {
1668 const char *hash;
1669 if (CFUSR_CHECKBIT(changemap, i_password) == 0)
1670 {
1671 if (!GetPasswordHash(puser, passwd_info, &hash))
1672 {
1673 return false;
1674 }
1675 }
1676 else
1677 {
1678 // Don't unlock the hash if we already set the password. Our
1679 // cached value in passwd_info->pw_passwd will be wrong, and the
1680 // account will already have been unlocked anyway.
1681 hash = NULL;
1682 }
1683 if (!SetAccountLocked(puser, hash, (u->policy == USER_STATE_LOCKED)))
1684 {
1685 return false;
1686 }
1687 }
1688 }
1689
1690 if (CFUSR_CHECKBIT (changemap, i_groups) != 0)
1691 {
1692 StringAppend(cmd, " -G \"", sizeof(cmd));
1693 Buffer *buf = BufferNew();
1694 buf = StringSetToBuffer(groups_to_set, ',');
1695 StringAppend(cmd, buf->buffer, sizeof(cmd));
1696 BufferDestroy(buf);
1697 StringAppend(cmd, "\" ", sizeof(cmd));
1698 }
1699 #ifndef HAVE_PW
1700 StringAppend(cmd, " ", sizeof(cmd));
1701 StringAppend(cmd, puser, sizeof(cmd));
1702 #endif
1703 // If password and locking were the only things changed, don't run the command.
1704 CFUSR_CLEARBIT(changemap, i_password);
1705 CFUSR_CLEARBIT(changemap, i_locked);
1706 if (action == cfa_warn || DONTDO)
1707 {
1708 Log(LOG_LEVEL_WARNING, "Need to update user attributes (command '%s').", cmd);
1709 return false;
1710 }
1711 else if (changemap != 0)
1712 {
1713
1714 if (!ExecuteUserCommand(puser, cmd, sizeof(cmd), "modifying", "Modifying"))
1715 {
1716 return false;
1717 }
1718 }
1719 return true;
1720 }
1721
1722 #ifdef __FreeBSD__
fgetpwent(FILE * stream)1723 struct passwd *fgetpwent(FILE *stream)
1724 {
1725 if (stream == NULL)
1726 {
1727 return NULL;
1728 }
1729
1730 struct passwd *pw = NULL;
1731 char *line = NULL;
1732 size_t linecap = 0;
1733 ssize_t linelen;
1734 int pwd_scanflag = 0;
1735
1736 while ((linelen = getline(&line, &linecap, stream)) > 0)
1737 {
1738 /* Skip comments and empty lines */
1739 if (*line == '\n' || *line == '#')
1740 {
1741 continue;
1742 }
1743 /* trim latest \n */
1744 if (line[linelen - 1 ] == '\n')
1745 {
1746 line[linelen - 1] = '\0';
1747 }
1748 pw = pw_scan(line, pwd_scanflag);
1749 if (pw != NULL)
1750 {
1751 break;
1752 }
1753 }
1754 free(line);
1755
1756 return pw;
1757 }
1758 #endif
1759
1760 // Uses fgetpwent() instead of getpwnam(), to guarantee that the returned user
1761 // is a local user, and not for example from LDAP.
GetPwEntry(const char * puser)1762 static struct passwd *GetPwEntry(const char *puser)
1763 {
1764 FILE *fptr = safe_fopen("/etc/passwd", "r");
1765 if (!fptr)
1766 {
1767 Log(LOG_LEVEL_ERR, "Could not open '/etc/passwd': %s", GetErrorStr());
1768 return NULL;
1769 }
1770
1771 struct passwd *passwd_info;
1772 bool found = false;
1773 while ((passwd_info = fgetpwent(fptr)))
1774 {
1775 if (strcmp(puser, passwd_info->pw_name) == 0)
1776 {
1777 found = true;
1778 break;
1779 }
1780 #ifdef __FreeBSD__
1781 free(passwd_info);
1782 #endif
1783 }
1784
1785 fclose(fptr);
1786
1787 if (found)
1788 {
1789 return passwd_info;
1790 }
1791 else
1792 {
1793 // Failure to find the user means we just set errno to zero.
1794 // Perhaps not optimal, but we cannot pass ENOENT, because the fopen might
1795 // fail for this reason, and that should not be treated the same.
1796 errno = 0;
1797 return NULL;
1798 }
1799 }
1800
VerifyOneUsersPromise(const char * puser,const User * u,PromiseResult * result,enum cfopaction action,EvalContext * ctx,const Attributes * a,const Promise * pp)1801 void VerifyOneUsersPromise (const char *puser, const User *u, PromiseResult *result, enum cfopaction action,
1802 EvalContext *ctx, const Attributes *a, const Promise *pp)
1803 {
1804 assert(u != NULL);
1805
1806 struct passwd *passwd_info = GetPwEntry(puser);
1807 if (!passwd_info && errno != 0)
1808 {
1809 Log(LOG_LEVEL_ERR, "Could not get information from user database.");
1810 return;
1811 }
1812
1813 bool res;
1814 if (u->policy == USER_STATE_PRESENT || u->policy == USER_STATE_LOCKED)
1815 {
1816 if (passwd_info)
1817 {
1818 StringSet *groups_to_set = StringSetNew();
1819 StringSet *current_secondary_groups = StringSetNew();
1820 StringSet *groups_missing = StringSetNew();
1821 res = GetGroupInfo(puser, u, &groups_to_set, &groups_missing, ¤t_secondary_groups);
1822 if (res)
1823 {
1824 uint32_t cmap = 0;
1825 if (VerifyIfUserNeedsModifs (puser, u, passwd_info, &cmap, groups_to_set, current_secondary_groups))
1826 {
1827 res = DoModifyUser (puser, u, passwd_info, cmap, action, groups_to_set);
1828 if (res)
1829 {
1830 *result = PROMISE_RESULT_CHANGE;
1831 }
1832 else
1833 {
1834 *result = PROMISE_RESULT_FAIL;
1835 }
1836 }
1837 else
1838 {
1839 *result = PROMISE_RESULT_NOOP;
1840 }
1841 }
1842 else
1843 {
1844 *result = PROMISE_RESULT_FAIL;
1845 }
1846 StringSetDestroy(groups_to_set);
1847 StringSetDestroy(current_secondary_groups);
1848 StringSetDestroy(groups_missing);
1849 }
1850 else
1851 {
1852 res = DoCreateUser (puser, u, action, ctx, a, pp);
1853 if (res)
1854 {
1855 *result = PROMISE_RESULT_CHANGE;
1856 }
1857 else
1858 {
1859 *result = PROMISE_RESULT_FAIL;
1860 }
1861 }
1862 }
1863 else if (u->policy == USER_STATE_ABSENT)
1864 {
1865 if (passwd_info)
1866 {
1867 res = DoRemoveUser (puser, action);
1868 if (res)
1869 {
1870 *result = PROMISE_RESULT_CHANGE;
1871 }
1872 else
1873 {
1874 *result = PROMISE_RESULT_FAIL;
1875 }
1876 }
1877 else
1878 {
1879 *result = PROMISE_RESULT_NOOP;
1880 }
1881 }
1882 #ifdef __FreeBSD__
1883 free(passwd_info);
1884 #endif
1885 }
1886