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 *
string_mode_type(pam_list_mode_t op_mode,boolean_t allow)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
log_illegal_combination(const char * s1,const char * s2)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
pam_sm_acct_mgmt(pam_handle_t * pamh,int flags,int argc,const char ** argv)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