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
19static 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*/
70int 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)) {
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}
286