Start 1.33.0 development cycle
[oweals/busybox.git] / libbb / update_passwd.c
1 /* vi: set sw=4 ts=4: */
2 /*
3  * update_passwd
4  *
5  * update_passwd is a common function for passwd and chpasswd applets;
6  * it is responsible for updating password file (i.e. /etc/passwd or
7  * /etc/shadow) for a given user and password.
8  *
9  * Moved from loginutils/passwd.c by Alexander Shishkin <virtuoso@slind.org>
10  *
11  * Modified to be able to add or delete users, groups and users to/from groups
12  * by Tito Ragusa <farmatito@tiscali.it>
13  *
14  * Licensed under GPLv2, see file LICENSE in this source tree.
15  */
16 #include "libbb.h"
17
18 #if ENABLE_SELINUX
19 static void check_selinux_update_passwd(const char *username)
20 {
21         security_context_t context;
22         char *seuser;
23
24         if (getuid() != (uid_t)0 || is_selinux_enabled() == 0)
25                 return;  /* No need to check */
26
27         if (getprevcon_raw(&context) < 0)
28                 bb_simple_perror_msg_and_die("getprevcon failed");
29         seuser = strtok(context, ":");
30         if (!seuser)
31                 bb_error_msg_and_die("invalid context '%s'", context);
32         if (strcmp(seuser, username) != 0) {
33                 security_class_t tclass;
34                 access_vector_t av;
35
36                 tclass = string_to_security_class("passwd");
37                 if (tclass == 0)
38                         goto die;
39                 av = string_to_av_perm(tclass, "passwd");
40                 if (av == 0)
41                         goto die;
42
43                 if (selinux_check_passwd_access(av) != 0)
44  die:
45                         bb_simple_error_msg_and_die("SELinux: access denied");
46         }
47         if (ENABLE_FEATURE_CLEAN_UP)
48                 freecon(context);
49 }
50 #else
51 # define check_selinux_update_passwd(username) ((void)0)
52 #endif
53
54 /*
55  1) add a user: update_passwd(FILE, USER, REMAINING_PWLINE, NULL)
56     only if CONFIG_ADDUSER=y and applet_name[0] == 'a' like in adduser
57
58  2) add a group: update_passwd(FILE, GROUP, REMAINING_GRLINE, NULL)
59     only if CONFIG_ADDGROUP=y and applet_name[0] == 'a' like in addgroup
60
61  3) add a user to a group: update_passwd(FILE, GROUP, NULL, MEMBER)
62     only if CONFIG_FEATURE_ADDUSER_TO_GROUP=y, applet_name[0] == 'a'
63     like in addgroup and member != NULL
64
65  4) delete a user: update_passwd(FILE, USER, NULL, NULL)
66
67  5) delete a group: update_passwd(FILE, GROUP, NULL, NULL)
68
69  6) delete a user from a group: update_passwd(FILE, GROUP, NULL, MEMBER)
70     only if CONFIG_FEATURE_DEL_USER_FROM_GROUP=y and member != NULL
71
72  7) change user's password: update_passwd(FILE, USER, NEW_PASSWD, NULL)
73     only if CONFIG_PASSWD=y and applet_name[0] == 'p' like in passwd
74     or if CONFIG_CHPASSWD=y and applet_name[0] == 'c' like in chpasswd
75
76  8) delete a user from all groups: update_passwd(FILE, NULL, NULL, MEMBER)
77
78  This function does not validate the arguments fed to it
79  so the calling program should take care of that.
80
81  Returns number of lines changed, or -1 on error.
82 */
83 int FAST_FUNC update_passwd(const char *filename,
84                 const char *name,
85                 const char *new_passwd,
86                 const char *member)
87 {
88 #if !(ENABLE_FEATURE_ADDUSER_TO_GROUP || ENABLE_FEATURE_DEL_USER_FROM_GROUP)
89 #define member NULL
90 #endif
91         struct stat sb;
92         struct flock lock;
93         FILE *old_fp;
94         FILE *new_fp;
95         char *fnamesfx;
96         char *sfx_char;
97         char *name_colon;
98         int old_fd;
99         int new_fd;
100         int i;
101         int changed_lines;
102         int ret = -1; /* failure */
103         /* used as a bool: "are we modifying /etc/shadow?" */
104 #if ENABLE_FEATURE_SHADOWPASSWDS
105         const char *shadow = strstr(filename, "shadow");
106 #else
107 # define shadow NULL
108 #endif
109
110         filename = xmalloc_follow_symlinks(filename);
111         if (filename == NULL)
112                 return ret;
113
114         if (name)
115                 check_selinux_update_passwd(name);
116
117         /* New passwd file, "/etc/passwd+" for now */
118         fnamesfx = xasprintf("%s+", filename);
119         sfx_char = &fnamesfx[strlen(fnamesfx)-1];
120         name_colon = xasprintf("%s:", name ? name : "");
121
122         if (shadow)
123                 old_fp = fopen(filename, "r+");
124         else
125                 old_fp = fopen_or_warn(filename, "r+");
126         if (!old_fp) {
127                 if (shadow)
128                         ret = 0; /* missing shadow is not an error */
129                 goto free_mem;
130         }
131         old_fd = fileno(old_fp);
132
133         selinux_preserve_fcontext(old_fd);
134
135         /* Try to create "/etc/passwd+". Wait if it exists. */
136         i = 30;
137         do {
138                 // FIXME: on last iteration try w/o O_EXCL but with O_TRUNC?
139                 new_fd = open(fnamesfx, O_WRONLY|O_CREAT|O_EXCL, 0600);
140                 if (new_fd >= 0) goto created;
141                 if (errno != EEXIST) break;
142                 usleep(100000); /* 0.1 sec */
143         } while (--i);
144         bb_perror_msg("can't create '%s'", fnamesfx);
145         goto close_old_fp;
146
147  created:
148         if (fstat(old_fd, &sb) == 0) {
149                 fchmod(new_fd, sb.st_mode & 0777); /* ignore errors */
150                 fchown(new_fd, sb.st_uid, sb.st_gid);
151         }
152         errno = 0;
153         new_fp = xfdopen_for_write(new_fd);
154
155         /* Backup file is "/etc/passwd-" */
156         *sfx_char = '-';
157         /* Delete old backup */
158         i = (unlink(fnamesfx) && errno != ENOENT);
159         /* Create backup as a hardlink to current */
160         if (i || link(filename, fnamesfx))
161                 bb_perror_msg("warning: can't create backup copy '%s'",
162                                 fnamesfx);
163         *sfx_char = '+';
164
165         /* Lock the password file before updating */
166         lock.l_type = F_WRLCK;
167         lock.l_whence = SEEK_SET;
168         lock.l_start = 0;
169         lock.l_len = 0;
170         if (fcntl(old_fd, F_SETLK, &lock) < 0)
171                 bb_perror_msg("warning: can't lock '%s'", filename);
172         lock.l_type = F_UNLCK;
173
174         /* Read current password file, write updated /etc/passwd+ */
175         changed_lines = 0;
176         while (1) {
177                 char *cp, *line;
178
179                 line = xmalloc_fgetline(old_fp);
180                 if (!line) /* EOF/error */
181                         break;
182
183 #if ENABLE_FEATURE_ADDUSER_TO_GROUP || ENABLE_FEATURE_DEL_USER_FROM_GROUP
184                 if (!name && member) {
185                         /* Delete member from all groups */
186                         /* line is "GROUP:PASSWD:[member1[,member2]...]" */
187                         unsigned member_len = strlen(member);
188                         char *list = strrchr(line, ':');
189                         while (list) {
190                                 list++;
191  next_list_element:
192                                 if (is_prefixed_with(list, member)) {
193                                         char c;
194                                         changed_lines++;
195                                         c = list[member_len];
196                                         if (c == '\0') {
197                                                 if (list[-1] == ',')
198                                                         list--;
199                                                 *list = '\0';
200                                                 break;
201                                         }
202                                         if (c == ',') {
203                                                 overlapping_strcpy(list, list + member_len + 1);
204                                                 goto next_list_element;
205                                         }
206                                         changed_lines--;
207                                 }
208                                 list = strchr(list, ',');
209                         }
210                         fprintf(new_fp, "%s\n", line);
211                         goto next;
212                 }
213 #endif
214
215                 cp = is_prefixed_with(line, name_colon);
216                 if (!cp) {
217                         fprintf(new_fp, "%s\n", line);
218                         goto next;
219                 }
220
221                 /* We have a match with "name:"... */
222                 /* cp points past "name:" */
223
224 #if ENABLE_FEATURE_ADDUSER_TO_GROUP || ENABLE_FEATURE_DEL_USER_FROM_GROUP
225                 if (member) {
226                         /* It's actually /etc/group+, not /etc/passwd+ */
227                         if (ENABLE_FEATURE_ADDUSER_TO_GROUP
228                          && applet_name[0] == 'a'
229                         ) {
230                                 /* Add user to group */
231                                 fprintf(new_fp, "%s%s%s\n", line,
232                                         last_char_is(line, ':') ? "" : ",",
233                                         member);
234                                 changed_lines++;
235                         } else if (ENABLE_FEATURE_DEL_USER_FROM_GROUP
236                         /* && applet_name[0] == 'd' */
237                         ) {
238                                 /* Delete user from group */
239                                 char *tmp;
240                                 const char *fmt = "%s";
241
242                                 /* find the start of the member list: last ':' */
243                                 cp = strrchr(line, ':');
244                                 /* cut it */
245                                 *cp++ = '\0';
246                                 /* write the cut line name:passwd:gid:
247                                  * or name:!:: */
248                                 fprintf(new_fp, "%s:", line);
249                                 /* parse the tokens of the member list */
250                                 tmp = cp;
251                                 while ((cp = strsep(&tmp, ",")) != NULL) {
252                                         if (strcmp(member, cp) != 0) {
253                                                 fprintf(new_fp, fmt, cp);
254                                                 fmt = ",%s";
255                                         } else {
256                                                 /* found member, skip it */
257                                                 changed_lines++;
258                                         }
259                                 }
260                                 fprintf(new_fp, "\n");
261                         }
262                 } else
263 #endif
264                 if ((ENABLE_PASSWD && applet_name[0] == 'p')
265                  || (ENABLE_CHPASSWD && applet_name[0] == 'c')
266                 ) {
267                         /* Change passwd */
268                         cp = strchrnul(cp, ':'); /* move past old passwd */
269
270                         if (shadow && *cp == ':') {
271                                 /* /etc/shadow's field 3 (passwd change date) needs updating */
272                                 /* move past old change date */
273                                 unsigned time_days = (unsigned long)(time(NULL)) / (24*60*60);
274
275                                 if (time_days == 0) {
276                                         /* 0 as change date has special meaning, avoid it */
277                                         time_days = 1;
278                                 }
279                                 cp = strchrnul(cp + 1, ':');
280                                 /* "name:" + "new_passwd" + ":" + "change date" + ":rest of line" */
281                                 fprintf(new_fp, "%s%s:%u%s\n", name_colon, new_passwd,
282                                         time_days, cp);
283                         } else {
284                                 /* "name:" + "new_passwd" + ":rest of line" */
285                                 fprintf(new_fp, "%s%s%s\n", name_colon, new_passwd, cp);
286                         }
287                         changed_lines++;
288                 } /* else delete user or group: skip the line */
289  next:
290                 free(line);
291         }
292
293         if (changed_lines == 0) {
294 #if ENABLE_FEATURE_ADDUSER_TO_GROUP || ENABLE_FEATURE_DEL_USER_FROM_GROUP
295                 if (member) {
296                         if (ENABLE_ADDGROUP && applet_name[0] == 'a')
297                                 bb_error_msg("can't find %s in %s", name, filename);
298                         if (ENABLE_DELGROUP && applet_name[0] == 'd')
299                                 bb_error_msg("can't find %s in %s", member, filename);
300                 }
301 #endif
302                 if ((ENABLE_ADDUSER || ENABLE_ADDGROUP)
303                  && applet_name[0] == 'a' && !member
304                 ) {
305                         /* add user or group */
306                         fprintf(new_fp, "%s%s\n", name_colon, new_passwd);
307                         changed_lines++;
308                 }
309         }
310
311         fcntl(old_fd, F_SETLK, &lock);
312
313         /* We do want all of them to execute, thus | instead of || */
314         errno = 0;
315         if ((ferror(old_fp) | fflush(new_fp) | fsync(new_fd) | fclose(new_fp))
316          || rename(fnamesfx, filename)
317         ) {
318                 /* At least one of those failed */
319                 bb_perror_nomsg();
320                 goto unlink_new;
321         }
322         /* Success: ret >= 0 */
323         ret = changed_lines;
324
325  unlink_new:
326         if (ret < 0)
327                 unlink(fnamesfx);
328
329  close_old_fp:
330         fclose(old_fp);
331
332  free_mem:
333         free(fnamesfx);
334         free((char *)filename);
335         free(name_colon);
336         return ret;
337 }