ba773fcb27fd2cc81ce4a478b66e3dcae20bc2ae
[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                 if (shadow)
115                         ret = 0; /* missing shadow is not an error */
116                 goto free_mem;
117         }
118         old_fd = fileno(old_fp);
119
120         selinux_preserve_fcontext(old_fd);
121
122         /* Try to create "/etc/passwd+". Wait if it exists. */
123         i = 30;
124         do {
125                 // FIXME: on last iteration try w/o O_EXCL but with O_TRUNC?
126                 new_fd = open(fnamesfx, O_WRONLY|O_CREAT|O_EXCL, 0600);
127                 if (new_fd >= 0) goto created;
128                 if (errno != EEXIST) break;
129                 usleep(100000); /* 0.1 sec */
130         } while (--i);
131         bb_perror_msg("can't create '%s'", fnamesfx);
132         goto close_old_fp;
133
134  created:
135         if (!fstat(old_fd, &sb)) {
136                 fchmod(new_fd, sb.st_mode & 0777); /* ignore errors */
137                 fchown(new_fd, sb.st_uid, sb.st_gid);
138         }
139         errno = 0;
140         new_fp = fdopen(new_fd, "w");
141         if (!new_fp) {
142                 bb_perror_nomsg();
143                 close(new_fd);
144                 goto unlink_new;
145         }
146
147         /* Backup file is "/etc/passwd-" */
148         *sfx_char = '-';
149         /* Delete old backup */
150         i = (unlink(fnamesfx) && errno != ENOENT);
151         /* Create backup as a hardlink to current */
152         if (i || link(filename, fnamesfx))
153                 bb_perror_msg("warning: can't create backup copy '%s'",
154                                 fnamesfx);
155         *sfx_char = '+';
156
157         /* Lock the password file before updating */
158         lock.l_type = F_WRLCK;
159         lock.l_whence = SEEK_SET;
160         lock.l_start = 0;
161         lock.l_len = 0;
162         if (fcntl(old_fd, F_SETLK, &lock) < 0)
163                 bb_perror_msg("warning: can't lock '%s'", filename);
164         lock.l_type = F_UNLCK;
165
166         /* Read current password file, write updated /etc/passwd+ */
167         changed_lines = 0;
168         while (1) {
169                 char *cp, *line;
170
171                 line = xmalloc_fgetline(old_fp);
172                 if (!line) /* EOF/error */
173                         break;
174                 if (strncmp(name, line, user_len) != 0) {
175                         fprintf(new_fp, "%s\n", line);
176                         goto next;
177                 }
178
179                 /* We have a match with "name:"... */
180                 cp = line + user_len; /* move past name: */
181
182 #if ENABLE_FEATURE_ADDUSER_TO_GROUP || ENABLE_FEATURE_DEL_USER_FROM_GROUP
183                 if (member) {
184                         /* It's actually /etc/group+, not /etc/passwd+ */
185                         if (ENABLE_FEATURE_ADDUSER_TO_GROUP
186                          && applet_name[0] == 'a'
187                         ) {
188                                 /* Add user to group */
189                                 fprintf(new_fp, "%s%s%s\n", line,
190                                         last_char_is(line, ':') ? "" : ",",
191                                         member);
192                                 changed_lines++;
193                         } else if (ENABLE_FEATURE_DEL_USER_FROM_GROUP
194                         /* && applet_name[0] == 'd' */
195                         ) {
196                                 /* Delete user from group */
197                                 char *tmp;
198                                 const char *fmt = "%s";
199
200                                 /* find the start of the member list: last ':' */
201                                 cp = strrchr(line, ':');
202                                 /* cut it */
203                                 *cp++ = '\0';
204                                 /* write the cut line name:passwd:gid:
205                                  * or name:!:: */
206                                 fprintf(new_fp, "%s:", line);
207                                 /* parse the tokens of the member list */
208                                 tmp = cp;
209                                 while ((cp = strsep(&tmp, ",")) != NULL) {
210                                         if (strcmp(member, cp) != 0) {
211                                                 fprintf(new_fp, fmt, cp);
212                                                 fmt = ",%s";
213                                         } else {
214                                                 /* found member, skip it */
215                                                 changed_lines++;
216                                         }
217                                 }
218                                 fprintf(new_fp, "\n");
219                         }
220                 } else
221 #endif
222                 if ((ENABLE_PASSWD && applet_name[0] == 'p')
223                  || (ENABLE_CHPASSWD && applet_name[0] == 'c')
224                 ) {
225                         /* Change passwd */
226                         cp = strchrnul(cp, ':'); /* move past old passwd */
227
228                         if (shadow && *cp == ':') {
229                                 /* /etc/shadow's field 3 (passwd change date) needs updating */
230                                 /* move past old change date */
231                                 cp = strchrnul(cp + 1, ':');
232                                 /* "name:" + "new_passwd" + ":" + "change date" + ":rest of line" */
233                                 fprintf(new_fp, "%s%s:%u%s\n", name, new_passwd,
234                                         (unsigned)(time(NULL)) / (24*60*60), cp);
235                         } else {
236                                 /* "name:" + "new_passwd" + ":rest of line" */
237                                 fprintf(new_fp, "%s%s%s\n", name, new_passwd, cp);
238                         }
239                         changed_lines++;
240                 } /* else delete user or group: skip the line */
241  next:
242                 free(line);
243         }
244
245         if (changed_lines == 0) {
246 #if ENABLE_FEATURE_DEL_USER_FROM_GROUP
247                 if (member)
248                         bb_error_msg("can't find %s in %s", member, filename);
249 #endif
250                 if ((ENABLE_ADDUSER || ENABLE_ADDGROUP)
251                  && applet_name[0] == 'a' && !member
252                 ) {
253                         /* add user or group */
254                         fprintf(new_fp, "%s%s\n", name, new_passwd);
255                         changed_lines++;
256                 }
257         }
258
259         fcntl(old_fd, F_SETLK, &lock);
260
261         /* We do want all of them to execute, thus | instead of || */
262         errno = 0;
263         if ((ferror(old_fp) | fflush(new_fp) | fsync(new_fd) | fclose(new_fp))
264          || rename(fnamesfx, filename)
265         ) {
266                 /* At least one of those failed */
267                 bb_perror_nomsg();
268                 goto unlink_new;
269         }
270         /* Success: ret >= 0 */
271         ret = changed_lines;
272
273  unlink_new:
274         if (ret < 0)
275                 unlink(fnamesfx);
276
277  close_old_fp:
278         fclose(old_fp);
279
280  free_mem:
281         free(fnamesfx);
282         free((char *)filename);
283         free((char *)name);
284         return ret;
285 }