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