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