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, &current_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