xref: /illumos-gate/usr/src/lib/pam_modules/list/list.c (revision 45ede40b2394db7967e59f19288fae9b62efd4aa)
1 /*
2  * CDDL HEADER START
3  *
4  * The contents of this file are subject to the terms of the
5  * Common Development and Distribution License (the "License").
6  * You may not use this file except in compliance with the License.
7  *
8  * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
9  * or http://www.opensolaris.org/os/licensing.
10  * See the License for the specific language governing permissions
11  * and limitations under the License.
12  *
13  * When distributing Covered Code, include this CDDL HEADER in each
14  * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
15  * If applicable, add the following below this CDDL HEADER, with the
16  * fields enclosed by brackets "[]" replaced with your own identifying
17  * information: Portions Copyright [yyyy] [name of copyright owner]
18  *
19  * CDDL HEADER END
20  */
21 
22 /*
23  * Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
24  * Use is subject to license terms.
25  */
26 
27 #include <stdio.h>
28 #include <string.h>
29 #include <sys/types.h>
30 #include <sys/stat.h>
31 #include <syslog.h>
32 #include <netdb.h>
33 #include <malloc.h>
34 #include <unistd.h>
35 #include <errno.h>
36 #include <grp.h>
37 #include <security/pam_appl.h>
38 #include <security/pam_modules.h>
39 #include <security/pam_impl.h>
40 
41 #define	ILLEGAL_COMBINATION "pam_list: illegal combination of options"
42 
43 typedef enum {
44 	LIST_EXTERNAL_FILE,
45 	LIST_PLUS_CHECK,
46 	LIST_COMPAT_MODE
47 } pam_list_mode_t;
48 
49 static const char *
50 string_mode_type(pam_list_mode_t op_mode, boolean_t allow)
51 {
52 	return ((op_mode == LIST_COMPAT_MODE) ? "compat" :
53 	    (allow ? "allow" : "deny"));
54 }
55 
56 static void
57 log_illegal_combination(const char *s1, const char *s2)
58 {
59 	__pam_log(LOG_AUTH | LOG_ERR, ILLEGAL_COMBINATION
60 	    " %s and %s", s1, s2);
61 }
62 
63 /*ARGSUSED*/
64 int
65 pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc, const char **argv)
66 {
67 	FILE	*fd;
68 	const char	*allowdeny_filename = PF_PATH;
69 	char	buf[BUFSIZ];
70 	char	hostname[MAXHOSTNAMELEN];
71 	char	*username = NULL;
72 	char	*grbuf = NULL;
73 	char	*bufp;
74 	char	*rhost;
75 	char	*limit;
76 	int	userok = 0;
77 	int	hostok = 0;
78 	int	i;
79 	int	allow_deny_test = 0;
80 	long	grbuflen = 0;
81 	boolean_t	debug = B_FALSE;
82 	boolean_t	allow = B_FALSE;
83 	boolean_t	matched = B_FALSE;
84 	boolean_t	check_user = B_TRUE;
85 	boolean_t	check_group = B_FALSE;
86 	boolean_t	check_host = B_FALSE;
87 	boolean_t	check_exact = B_FALSE;
88 	pam_list_mode_t	op_mode = LIST_PLUS_CHECK;
89 
90 	// group reentrant interfaces limits
91 	if ((grbuflen = sysconf(_SC_GETGR_R_SIZE_MAX)) <= 0)
92 		return (PAM_BUF_ERR);
93 
94 	for (i = 0; i < argc; ++i) {
95 		if (strncasecmp(argv[i], "debug", sizeof ("debug")) == 0) {
96 			debug = B_TRUE;
97 		} else if (strncasecmp(argv[i], "group",
98 		    sizeof ("group")) == 0) {
99 			check_group = B_TRUE;
100 		} else if (strncasecmp(argv[i], "user", sizeof ("user")) == 0) {
101 			check_user = B_TRUE;
102 		} else if (strncasecmp(argv[i], "nouser",
103 		    sizeof ("nouser")) == 0) {
104 			check_user = B_FALSE;
105 		} else if (strncasecmp(argv[i], "host", sizeof ("host")) == 0) {
106 			check_host = B_TRUE;
107 		} else if (strncasecmp(argv[i], "nohost",
108 		    sizeof ("nohost")) == 0) {
109 			check_host = B_FALSE;
110 		} else if (strncasecmp(argv[i], "user_host_exact",
111 		    sizeof ("user_host_exact")) == 0) {
112 			check_exact = B_TRUE;
113 		} else if (strcasecmp(argv[i], "compat") == 0) {
114 			if (op_mode == LIST_PLUS_CHECK) {
115 				op_mode = LIST_COMPAT_MODE;
116 			} else {
117 				log_illegal_combination("compat",
118 				    string_mode_type(op_mode, allow));
119 				return (PAM_SERVICE_ERR);
120 			}
121 		} else if (strncasecmp(argv[i], "allow=",
122 		    sizeof ("allow=") - 1) == 0) {
123 			if (op_mode == LIST_PLUS_CHECK) {
124 				allowdeny_filename = argv[i] +
125 				    sizeof ("allow=") - 1;
126 				allow = B_TRUE;
127 				op_mode = LIST_EXTERNAL_FILE;
128 				allow_deny_test++;
129 			} else {
130 				log_illegal_combination("allow",
131 				    string_mode_type(op_mode, allow));
132 				return (PAM_SERVICE_ERR);
133 			}
134 		} else if (strncasecmp(argv[i], "deny=",
135 		    sizeof ("deny=") - 1) == 0) {
136 			if (op_mode == LIST_PLUS_CHECK) {
137 				allowdeny_filename = argv[i] +
138 				    sizeof ("deny=") - 1;
139 				allow = B_FALSE;
140 				op_mode = LIST_EXTERNAL_FILE;
141 				allow_deny_test++;
142 			} else {
143 				log_illegal_combination("deny",
144 				    string_mode_type(op_mode, allow));
145 				return (PAM_SERVICE_ERR);
146 			}
147 		} else {
148 			__pam_log(LOG_AUTH | LOG_ERR,
149 			    "pam_list: illegal option %s", argv[i]);
150 			return (PAM_SERVICE_ERR);
151 		}
152 	}
153 
154 	if (((check_user || check_group || check_host ||
155 	    check_exact) == B_FALSE) || (allow_deny_test > 1)) {
156 		__pam_log(LOG_AUTH | LOG_ERR, ILLEGAL_COMBINATION);
157 		return (PAM_SERVICE_ERR);
158 	}
159 
160 	if ((op_mode == LIST_COMPAT_MODE) && (check_user == B_FALSE)) {
161 		log_illegal_combination("compat", "nouser");
162 		return (PAM_SERVICE_ERR);
163 	}
164 
165 	if ((op_mode == LIST_COMPAT_MODE) && (check_group == B_TRUE)) {
166 		log_illegal_combination("compat", "group");
167 		return (PAM_SERVICE_ERR);
168 	}
169 
170 	if (debug) {
171 		__pam_log(LOG_AUTH | LOG_DEBUG,
172 		    "pam_list: check_user = %d, check_host = %d,"
173 		    "check_exact = %d\n",
174 		    check_user, check_host, check_exact);
175 
176 		__pam_log(LOG_AUTH | LOG_DEBUG,
177 		    "pam_list: auth_file: %s, %s\n", allowdeny_filename,
178 		    (op_mode == LIST_COMPAT_MODE) ? "compat mode" :
179 		    (allow ? "allow file" : "deny file"));
180 	}
181 
182 	(void) pam_get_item(pamh, PAM_USER, (void**)&username);
183 
184 	if ((check_user || check_group || check_exact) && ((username == NULL) ||
185 	    (*username == '\0'))) {
186 		__pam_log(LOG_AUTH | LOG_ERR,
187 		    "pam_list: username not supplied, critical error");
188 		return (PAM_USER_UNKNOWN);
189 	}
190 
191 	(void) pam_get_item(pamh, PAM_RHOST, (void**)&rhost);
192 
193 	if ((check_host || check_exact) && ((rhost == NULL) ||
194 	    (*rhost == '\0'))) {
195 		if (gethostname(hostname, MAXHOSTNAMELEN) == 0) {
196 			rhost = hostname;
197 		} else {
198 			__pam_log(LOG_AUTH | LOG_ERR,
199 			    "pam_list: error by gethostname - %m");
200 			return (PAM_SERVICE_ERR);
201 		}
202 	}
203 
204 	if (debug) {
205 		__pam_log(LOG_AUTH | LOG_DEBUG,
206 		    "pam_list: pam_sm_acct_mgmt for (%s,%s,)",
207 		    (rhost != NULL) ? rhost : "", username);
208 	}
209 
210 	if (strlen(allowdeny_filename) == 0) {
211 		__pam_log(LOG_AUTH | LOG_ERR,
212 		    "pam_list: file name not specified");
213 		return (PAM_SERVICE_ERR);
214 	}
215 
216 	if ((fd = fopen(allowdeny_filename, "rF")) == NULL) {
217 		__pam_log(LOG_AUTH | LOG_ERR, "pam_list: fopen of %s: %s",
218 		    allowdeny_filename, strerror(errno));
219 		return (PAM_SERVICE_ERR);
220 	}
221 
222 	if (check_group && ((grbuf = calloc(1, grbuflen)) == NULL)) {
223 		__pam_log(LOG_AUTH | LOG_ERR,
224 		    "pam_list: could not allocate memory for group");
225 		return (PAM_BUF_ERR);
226 	}
227 
228 	while (fgets(buf, BUFSIZ, fd) != NULL) {
229 		/* lines longer than BUFSIZ-1 */
230 		if ((strlen(buf) == (BUFSIZ - 1)) &&
231 		    (buf[BUFSIZ - 2] != '\n')) {
232 			while ((fgetc(fd) != '\n') && (!feof(fd))) {
233 				continue;
234 			}
235 			__pam_log(LOG_AUTH | LOG_DEBUG,
236 			    "pam_list: long line in file,"
237 			    "more than %d chars, the rest ignored", BUFSIZ - 1);
238 		}
239 
240 		/* remove unneeded colons if necessary */
241 		if ((limit = strpbrk(buf, ":\n")) != NULL) {
242 			*limit = '\0';
243 		}
244 
245 		/* ignore free values */
246 		if (buf[0] == '\0') {
247 			continue;
248 		}
249 
250 		bufp = buf;
251 
252 		/* test for interesting lines = +/- in /etc/passwd */
253 		if (op_mode == LIST_COMPAT_MODE) {
254 			/* simple + matches all */
255 			if ((buf[0] == '+') && (buf[1] == '\0')) {
256 				matched = B_TRUE;
257 				allow = B_TRUE;
258 				break;
259 			}
260 
261 			/* simple - is not defined */
262 			if ((buf[0] == '-') && (buf[1] == '\0')) {
263 				__pam_log(LOG_AUTH | LOG_ERR,
264 				    "pam_list: simple minus unknown, "
265 				    "illegal line in " PF_PATH);
266 				(void) fclose(fd);
267 				free(grbuf);
268 				return (PAM_SERVICE_ERR);
269 			}
270 
271 			/* @ is not allowed on the first position */
272 			if (buf[0] == '@') {
273 				__pam_log(LOG_AUTH | LOG_ERR,
274 				    "pam_list: @ is not allowed on the first "
275 				    "position in " PF_PATH);
276 				(void) fclose(fd);
277 				free(grbuf);
278 				return (PAM_SERVICE_ERR);
279 			}
280 
281 			/* -user or -@netgroup */
282 			if (buf[0] == '-') {
283 				allow = B_FALSE;
284 				bufp++;
285 			/* +user or +@netgroup */
286 			} else if (buf[0] == '+') {
287 				allow = B_TRUE;
288 				bufp++;
289 			/* user */
290 			} else {
291 				allow = B_TRUE;
292 			}
293 		} else if (op_mode == LIST_PLUS_CHECK) {
294 			if (((buf[0] != '+') && (buf[0] != '-')) ||
295 			    (buf[1] == '\0')) {
296 				continue;
297 			}
298 
299 			if (buf[0] == '+') {
300 				allow = B_TRUE;
301 			} else {
302 				allow = B_FALSE;
303 			}
304 			bufp++;
305 		}
306 
307 		/*
308 		 * if -> netgroup line
309 		 * else if -> group line
310 		 * else -> user line
311 		 */
312 		if ((bufp[0] == '@') && (bufp[1] != '\0')) {
313 			bufp++;
314 
315 			if (check_exact) {
316 				if (innetgr(bufp, rhost, username,
317 				    NULL) == 1) {
318 					matched = B_TRUE;
319 					break;
320 				}
321 			} else {
322 				if (check_user) {
323 					userok = innetgr(bufp, NULL, username,
324 					    NULL);
325 				} else {
326 					userok = 1;
327 				}
328 				if (check_host) {
329 					hostok = innetgr(bufp, rhost, NULL,
330 					    NULL);
331 				} else {
332 					hostok = 1;
333 				}
334 				if (userok && hostok) {
335 					matched = B_TRUE;
336 					break;
337 				}
338 			}
339 		} else if ((bufp[0] == '%') && (bufp[1] != '\0')) {
340 			char	**member;
341 			struct	group grp;
342 
343 			if (check_group == B_FALSE)
344 				continue;
345 
346 			bufp++;
347 
348 			if (getgrnam_r(bufp, &grp, grbuf, grbuflen) != NULL) {
349 				for (member = grp.gr_mem; *member != NULL;
350 				    member++) {
351 					if (strcmp(*member, username) == 0) {
352 						matched = B_TRUE;
353 						break;
354 					}
355 				}
356 			} else {
357 				__pam_log(LOG_AUTH | LOG_ERR,
358 				    "pam_list: %s is not a known group",
359 				    bufp);
360 			}
361 		} else {
362 			if (check_user) {
363 				if (strcmp(bufp, username) == 0) {
364 					matched = B_TRUE;
365 					break;
366 				}
367 			}
368 		}
369 
370 		/*
371 		 * No match found in /etc/passwd yet.  For compat mode
372 		 * a failure to match should result in a return of
373 		 * PAM_PERM_DENIED which is achieved below if 'matched'
374 		 * is false and 'allow' is true.
375 		 */
376 		if (op_mode == LIST_COMPAT_MODE) {
377 			allow = B_TRUE;
378 		}
379 	}
380 	(void) fclose(fd);
381 	free(grbuf);
382 
383 	if (debug) {
384 		__pam_log(LOG_AUTH | LOG_DEBUG,
385 		    "pam_list: %s for %s", matched ? "matched" : "no match",
386 		    allow ? "allow" : "deny");
387 	}
388 
389 	if (matched) {
390 		return (allow ? PAM_SUCCESS : PAM_PERM_DENIED);
391 	}
392 	/*
393 	 * For compatibility with passwd_compat mode to prevent root access
394 	 * denied.
395 	 */
396 	if (op_mode == LIST_PLUS_CHECK) {
397 		return (PAM_IGNORE);
398 	}
399 	return (allow ? PAM_PERM_DENIED : PAM_SUCCESS);
400 }
401