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