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