ash: deal with some TODOs (mostly trivial)
[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 tarball for details.
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_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                 if (checkPasswdAccess(PASSWD__PASSWD) != 0)
34                         bb_error_msg_and_die("SELinux: access denied");
35         }
36         if (ENABLE_FEATURE_CLEAN_UP)
37                 freecon(context);
38 }
39 #else
40 # define check_selinux_update_passwd(username) ((void)0)
41 #endif
42
43 /*
44  1) add a user: update_passwd(FILE, USER, REMAINING_PWLINE, NULL)
45     only if CONFIG_ADDUSER=y and applet_name[0] == 'a' like in adduser
46
47  2) add a group: update_passwd(FILE, GROUP, REMAINING_GRLINE, NULL)
48     only if CONFIG_ADDGROUP=y and applet_name[0] == 'a' like in addgroup
49
50  3) add a user to a group: update_passwd(FILE, GROUP, NULL, MEMBER)
51     only if CONFIG_FEATURE_ADDUSER_TO_GROUP=y, applet_name[0] == 'a'
52     like in addgroup and member != NULL
53
54  4) delete a user: update_passwd(FILE, USER, NULL, NULL)
55
56  5) delete a group: update_passwd(FILE, GROUP, NULL, NULL)
57
58  6) delete a user from a group: update_passwd(FILE, GROUP, NULL, MEMBER)
59     only if CONFIG_FEATURE_DEL_USER_FROM_GROUP=y and member != NULL
60
61  7) change user's passord: update_passwd(FILE, USER, NEW_PASSWD, NULL)
62     only if CONFIG_PASSWD=y and applet_name[0] == 'p' like in passwd
63     or if CONFIG_CHPASSWD=y and applet_name[0] == 'c' like in chpasswd
64
65  This function does not validate the arguments fed to it
66  so the calling program should take care of that.
67
68  Returns number of lines changed, or -1 on error.
69 */
70 int FAST_FUNC update_passwd(const char *filename,
71                 const char *name,
72                 const char *new_passwd,
73                 const char *member)
74 {
75 #if !(ENABLE_FEATURE_ADDUSER_TO_GROUP || ENABLE_FEATURE_DEL_USER_FROM_GROUP)
76 #define member NULL
77 #endif
78         struct stat sb;
79         struct flock lock;
80         FILE *old_fp;
81         FILE *new_fp;
82         char *fnamesfx;
83         char *sfx_char;
84         unsigned user_len;
85         int old_fd;
86         int new_fd;
87         int i;
88         int changed_lines;
89         int ret = -1; /* failure */
90         /* used as a bool: "are we modifying /etc/shadow?" */
91 #if ENABLE_FEATURE_SHADOWPASSWDS
92         const char *shadow = strstr(filename, "shadow");
93 #else
94 # define shadow NULL
95 #endif
96
97         filename = xmalloc_follow_symlinks(filename);
98         if (filename == NULL)
99                 return ret;
100
101         check_selinux_update_passwd(name);
102
103         /* New passwd file, "/etc/passwd+" for now */
104         fnamesfx = xasprintf("%s+", filename);
105         sfx_char = &fnamesfx[strlen(fnamesfx)-1];
106         name = xasprintf("%s:", name);
107         user_len = strlen(name);
108
109         if (shadow)
110                 old_fp = fopen(filename, "r+");
111         else
112                 old_fp = fopen_or_warn(filename, "r+");
113         if (!old_fp)
114                 goto free_mem;
115         old_fd = fileno(old_fp);
116
117         selinux_preserve_fcontext(old_fd);
118
119         /* Try to create "/etc/passwd+". Wait if it exists. */
120         i = 30;
121         do {
122                 // FIXME: on last iteration try w/o O_EXCL but with O_TRUNC?
123                 new_fd = open(fnamesfx, O_WRONLY|O_CREAT|O_EXCL, 0600);
124                 if (new_fd >= 0) goto created;
125                 if (errno != EEXIST) break;
126                 usleep(100000); /* 0.1 sec */
127         } while (--i);
128         bb_perror_msg("can't create '%s'", fnamesfx);
129         goto close_old_fp;
130
131  created:
132         if (!fstat(old_fd, &sb)) {
133                 fchmod(new_fd, sb.st_mode & 0777); /* ignore errors */
134                 fchown(new_fd, sb.st_uid, sb.st_gid);
135         }
136         errno = 0;
137         new_fp = fdopen(new_fd, "w");
138         if (!new_fp) {
139                 bb_perror_nomsg();
140                 close(new_fd);
141                 goto unlink_new;
142         }
143
144         /* Backup file is "/etc/passwd-" */
145         *sfx_char = '-';
146         /* Delete old backup */
147         i = (unlink(fnamesfx) && errno != ENOENT);
148         /* Create backup as a hardlink to current */
149         if (i || link(filename, fnamesfx))
150                 bb_perror_msg("warning: can't create backup copy '%s'",
151                                 fnamesfx);
152         *sfx_char = '+';
153
154         /* Lock the password file before updating */
155         lock.l_type = F_WRLCK;
156         lock.l_whence = SEEK_SET;
157         lock.l_start = 0;
158         lock.l_len = 0;
159         if (fcntl(old_fd, F_SETLK, &lock) < 0)
160                 bb_perror_msg("warning: can't lock '%s'", filename);
161         lock.l_type = F_UNLCK;
162
163         /* Read current password file, write updated /etc/passwd+ */
164         changed_lines = 0;
165         while (1) {
166                 char *cp, *line;
167
168                 line = xmalloc_fgetline(old_fp);
169                 if (!line) /* EOF/error */
170                         break;
171                 if (strncmp(name, line, user_len) != 0) {
172                         fprintf(new_fp, "%s\n", line);
173                         goto next;
174                 }
175
176                 /* We have a match with "name:"... */
177                 cp = line + user_len; /* move past name: */
178
179 #if ENABLE_FEATURE_ADDUSER_TO_GROUP || ENABLE_FEATURE_DEL_USER_FROM_GROUP
180                 if (member) {
181                         /* It's actually /etc/group+, not /etc/passwd+ */
182                         if (ENABLE_FEATURE_ADDUSER_TO_GROUP
183                          && applet_name[0] == 'a'
184                         ) {
185                                 /* Add user to group */
186                                 fprintf(new_fp, "%s%s%s\n", line,
187                                         last_char_is(line, ':') ? "" : ",",
188                                         member);
189                                 changed_lines++;
190                         } else if (ENABLE_FEATURE_DEL_USER_FROM_GROUP
191                         /* && applet_name[0] == 'd' */
192                         ) {
193                                 /* Delete user from group */
194                                 char *tmp;
195                                 const char *fmt = "%s";
196
197                                 /* find the start of the member list: last ':' */
198                                 cp = strrchr(line, ':');
199                                 /* cut it */
200                                 *cp++ = '\0';
201                                 /* write the cut line name:passwd:gid:
202                                  * or name:!:: */
203                                 fprintf(new_fp, "%s:", line);
204                                 /* parse the tokens of the member list */
205                                 tmp = cp;
206                                 while ((cp = strsep(&tmp, ",")) != NULL) {
207                                         if (strcmp(member, cp) != 0) {
208                                                 fprintf(new_fp, fmt, cp);
209                                                 fmt = ",%s";
210                                         } else {
211                                                 /* found member, skip it */
212                                                 changed_lines++;
213                                         }
214                                 }
215                                 fprintf(new_fp, "\n");
216                         }
217                 } else
218 #endif
219                 if ((ENABLE_PASSWD && applet_name[0] == 'p')
220                  || (ENABLE_CHPASSWD && applet_name[0] == 'c')
221                 ) {
222                         /* Change passwd */
223                         cp = strchrnul(cp, ':'); /* move past old passwd */
224
225                         if (shadow && *cp == ':') {
226                                 /* /etc/shadow's field 3 (passwd change date) needs updating */
227                                 /* move past old change date */
228                                 cp = strchrnul(cp + 1, ':');
229                                 /* "name:" + "new_passwd" + ":" + "change date" + ":rest of line" */
230                                 fprintf(new_fp, "%s%s:%u%s\n", name, new_passwd,
231                                         (unsigned)(time(NULL)) / (24*60*60), cp);
232                         } else {
233                                 /* "name:" + "new_passwd" + ":rest of line" */
234                                 fprintf(new_fp, "%s%s%s\n", name, new_passwd, cp);
235                         }
236                         changed_lines++;
237                 } /* else delete user or group: skip the line */
238  next:
239                 free(line);
240         }
241
242         if (changed_lines == 0) {
243 #if ENABLE_FEATURE_DEL_USER_FROM_GROUP
244                 if (member)
245                         bb_error_msg("can't find %s in %s", member, filename);
246 #endif
247                 if ((ENABLE_ADDUSER || ENABLE_ADDGROUP)
248                  && applet_name[0] == 'a' && !member
249                 ) {
250                         /* add user or group */
251                         fprintf(new_fp, "%s%s\n", name, new_passwd);
252                         changed_lines++;
253                 }
254         }
255
256         fcntl(old_fd, F_SETLK, &lock);
257
258         /* We do want all of them to execute, thus | instead of || */
259         errno = 0;
260         if ((ferror(old_fp) | fflush(new_fp) | fsync(new_fd) | fclose(new_fp))
261          || rename(fnamesfx, filename)
262         ) {
263                 /* At least one of those failed */
264                 bb_perror_nomsg();
265                 goto unlink_new;
266         }
267         /* Success: ret >= 0 */
268         ret = changed_lines;
269
270  unlink_new:
271         if (ret < 0)
272                 unlink(fnamesfx);
273
274  close_old_fp:
275         fclose(old_fp);
276
277  free_mem:
278         free(fnamesfx);
279         free((char *)filename);
280         free((char *)name);
281         return ret;
282 }