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