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