xref: /illumos-gate/usr/src/lib/pam_modules/krb5/krb5_acct_mgmt.c (revision cb6207858a9fcc2feaee22e626912fba281ac969)
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  * Copyright 2007 Sun Microsystems, Inc.  All rights reserved.
23  * Use is subject to license terms.
24  */
25 
26 #pragma ident	"%Z%%M%	%I%	%E% SMI"
27 
28 
29 #include <kadm5/admin.h>
30 #include <krb5.h>
31 
32 #include <security/pam_appl.h>
33 #include <security/pam_modules.h>
34 #include <security/pam_impl.h>
35 #include <syslog.h>
36 #include <string.h>
37 #include <stdio.h>
38 #include <stdlib.h>
39 #include <sys/types.h>
40 #include <pwd.h>
41 #include <libintl.h>
42 #include <netdb.h>
43 #include "utils.h"
44 #include <shadow.h>
45 
46 #include "krb5_repository.h"
47 
48 #define	KRB5_AUTOMIGRATE_DATA	"SUNW-KRB5-AUTOMIGRATE-DATA"
49 
50 #define	min(a, b) ((a) < (b) ? (a) : (b))
51 
52 /*
53  * pam_sm_acct_mgmt	  main account managment routine.
54  */
55 
56 static int
57 fetch_princ_entry(
58 	krb5_module_data_t *kmd,
59 	char *princ_str,
60 	kadm5_principal_ent_rec *prent,	/* out */
61 	int debug)
62 
63 {
64 	kadm5_ret_t		code;
65 	krb5_principal 		princ = 0;
66 	char 			admin_realm[1024];
67 	char			kprinc[2*MAXHOSTNAMELEN];
68 	char			*cpw_service, *password;
69 	void 			*server_handle;
70 	krb5_context		context;
71 	kadm5_config_params	params;
72 
73 	password = kmd->password;
74 	context = kmd->kcontext;
75 
76 	if ((code = get_kmd_kuser(context, (const char *)princ_str,
77 	    kprinc, 2*MAXHOSTNAMELEN)) != 0) {
78 		return (code);
79 	}
80 
81 	code = krb5_parse_name(context, kprinc, &princ);
82 	if (code != 0) {
83 		krb5_free_context(context);
84 		return (PAM_SYSTEM_ERR);
85 	}
86 
87 	if (strlen(password) == 0) {
88 		krb5_free_principal(context, princ);
89 		krb5_free_context(context);
90 		if (debug)
91 			__pam_log(LOG_AUTH | LOG_DEBUG,
92 			    "PAM-KRB5 (acct): fetch_princ_entry: pwlen=0");
93 		return (PAM_AUTH_ERR);
94 	}
95 
96 	(void) strlcpy(admin_realm,
97 		    krb5_princ_realm(context, princ)->data,
98 		    sizeof (admin_realm));
99 
100 	(void) memset((char *)&params, 0, sizeof (params));
101 	params.mask |= KADM5_CONFIG_REALM;
102 	params.realm = admin_realm;
103 
104 	if (kadm5_get_cpw_host_srv_name(context, admin_realm, &cpw_service)) {
105 		__pam_log(LOG_AUTH | LOG_ERR,
106 			"PAM-KRB5 (acct):  unable to get host based "
107 			"service name for realm '%s'",
108 			admin_realm);
109 		krb5_free_principal(context, princ);
110 		krb5_free_context(context);
111 		return (PAM_SYSTEM_ERR);
112 	}
113 
114 	code = kadm5_init_with_password(kprinc, password, cpw_service,
115 					&params, KADM5_STRUCT_VERSION,
116 					KADM5_API_VERSION_2, &server_handle);
117 	if (code != 0) {
118 		if (debug)
119 			__pam_log(LOG_AUTH | LOG_DEBUG,
120 			    "PAM-KRB5 (acct): fetch_princ_entry: "
121 			    "init_with_pw failed: code = %d", code);
122 		krb5_free_principal(context, princ);
123 		krb5_free_context(context);
124 		return ((code == KADM5_BAD_PASSWORD) ?
125 			PAM_AUTH_ERR : PAM_SYSTEM_ERR);
126 	}
127 
128 	if (_kadm5_get_kpasswd_protocol(server_handle) != KRB5_CHGPWD_RPCSEC) {
129 		if (debug)
130 			__pam_log(LOG_AUTH | LOG_DEBUG,
131 			    "PAM-KRB5 (acct): fetch_princ_entry: "
132 			    "non-RPCSEC_GSS chpw server, can't get "
133 			    "princ entry");
134 		(void) kadm5_destroy(server_handle);
135 		krb5_free_principal(context, princ);
136 		krb5_free_context(context);
137 		return (PAM_SYSTEM_ERR);
138 	}
139 
140 	code = kadm5_get_principal(server_handle, princ, prent,
141 				KADM5_PRINCIPAL_NORMAL_MASK);
142 
143 	if (code != 0) {
144 		(void) kadm5_destroy(server_handle);
145 		krb5_free_principal(context, princ);
146 		krb5_free_context(context);
147 		return ((code == KADM5_UNK_PRINC) ?
148 			PAM_USER_UNKNOWN : PAM_SYSTEM_ERR);
149 	}
150 
151 	(void) kadm5_destroy(server_handle);
152 	krb5_free_principal(context, princ);
153 	krb5_free_context(context);
154 
155 	return (PAM_SUCCESS);
156 }
157 
158 /*
159  * exp_warn
160  *
161  * Warn the user if their pw is set to expire.
162  *
163  * We first check to see if the KDC had set any account or password
164  * expiration information in the key expiration field.  If this was
165  * not set then we must assume that the KDC could be broken and revert
166  * to fetching pw/account expiration information from kadm.  We can not
167  * determine the difference between broken KDCs that do not send key-exp
168  * vs. principals that do not have an expiration policy.  The up-shot
169  * is that pam_krb5 will probably not be stacked for acct mgmt if the
170  * environment does not have an exp policy, avoiding the second exchange
171  * using the kadm protocol.
172  */
173 static int
174 exp_warn(
175 	pam_handle_t *pamh,
176 	char *user,
177 	krb5_module_data_t *kmd,
178 	int debug)
179 
180 {
181 	int err;
182 	kadm5_principal_ent_rec prent;
183 	krb5_timestamp  now, days, expiration;
184 	char    messages[PAM_MAX_NUM_MSG][PAM_MAX_MSG_SIZE], *password;
185 	krb5_error_code code;
186 
187 	if (debug)
188 		__pam_log(LOG_AUTH | LOG_DEBUG,
189 		    "PAM-KRB5 (acct): exp_warn start: user = '%s'",
190 		    user ? user : "<null>");
191 
192 	password = kmd->password;
193 
194 	if (!pamh || !user || !password) {
195 		err = PAM_SERVICE_ERR;
196 		goto out;
197 	}
198 
199 	if (code = krb5_init_context(&kmd->kcontext)) {
200 		err = PAM_SYSTEM_ERR;
201 		if (debug)
202 			__pam_log(LOG_AUTH | LOG_ERR, "PAM-KRB5 (acct): "
203 			    "krb5_init_context failed: code=%d",
204 			    code);
205 		goto out;
206 	}
207 	if (code = krb5_timeofday(kmd->kcontext, &now)) {
208 		err = PAM_SYSTEM_ERR;
209 		if (debug)
210 			__pam_log(LOG_AUTH | LOG_ERR,
211 			    "PAM-KRB5 (acct): krb5_timeofday failed: code=%d",
212 			    code);
213 		goto out;
214 	}
215 
216 	if (kmd->expiration != 0) {
217 		expiration = kmd->expiration;
218 	} else {
219 		(void) memset(&prent, 0, sizeof (prent));
220 		if ((err = fetch_princ_entry(kmd, user, &prent, debug))
221 		    != PAM_SUCCESS) {
222 			if (debug)
223 				__pam_log(LOG_AUTH | LOG_DEBUG,
224 				"PAM-KRB5 (acct): exp_warn: fetch_pr failed %d",
225 				err);
226 			goto out;
227 		}
228 		if (prent.princ_expire_time != 0 && prent.pw_expiration != 0)
229 			expiration = min(prent.princ_expire_time,
230 				prent.pw_expiration);
231 		else
232 			expiration = prent.princ_expire_time ?
233 				prent.princ_expire_time : prent.pw_expiration;
234 	}
235 
236 	if (debug)
237 		__pam_log(LOG_AUTH | LOG_DEBUG,
238 		    "PAM-KRB5 (acct): exp_warn: "
239 		    "princ/pw_exp exp=%ld, now =%ld, days=%ld",
240 		    expiration,
241 		    now,
242 		    expiration > 0
243 		    ? ((expiration - now) / DAY)
244 		    : 0);
245 
246 	/* warn user if principal's pw is set to expire */
247 	if (expiration > 0) {
248 		days = (expiration - now) / DAY;
249 		if (days <= 0)
250 			(void) snprintf(messages[0],
251 				sizeof (messages[0]),
252 				dgettext(TEXT_DOMAIN,
253 				"Your Kerberos account/password will expire "
254 				"within 24 hours.\n"));
255 		else if (days == 1)
256 			(void) snprintf(messages[0],
257 				sizeof (messages[0]),
258 				dgettext(TEXT_DOMAIN,
259 				"Your Kerberos account/password will expire "
260 				"in 1 day.\n"));
261 		else
262 			(void) snprintf(messages[0],
263 				sizeof (messages[0]),
264 				dgettext(TEXT_DOMAIN,
265 				"Your Kerberos account/password will expire in "
266 				"%d days.\n"),
267 				(int)days);
268 
269 		(void) __pam_display_msg(pamh, PAM_TEXT_INFO, 1,
270 					messages, NULL);
271 	}
272 
273 	/* things went smooth */
274 	err = PAM_SUCCESS;
275 
276 out:
277 	if (debug)
278 		__pam_log(LOG_AUTH | LOG_DEBUG,
279 		    "PAM-KRB5 (acct): exp_warn end: err = %d", err);
280 
281 	return (err);
282 }
283 
284 /*
285  * pam_krb5 acct_mgmt
286  *
287  * we do
288  *    - check if pw expired (flag set in auth)
289  *    - warn user if pw is set to expire
290  *
291  * notes
292  *    - we require the auth module to have already run (sets module data)
293  *    - we don't worry about an expired princ cuz if that's the case,
294  *      auth would have failed
295  */
296 int
297 pam_sm_acct_mgmt(
298 	pam_handle_t *pamh,
299 	int	flags,
300 	int	argc,
301 	const char **argv)
302 
303 {
304 	char *user = NULL;
305 	char *userdata = NULL;
306 	int err;
307 	int i;
308 	krb5_module_data_t *kmd = NULL;
309 	char    messages[PAM_MAX_NUM_MSG][PAM_MAX_MSG_SIZE];
310 	int debug = 0;  /* pam.conf entry option */
311 	int nowarn = 0; /* pam.conf entry option, no expire warnings */
312 	pam_repository_t	*rep_data = NULL;
313 
314 	for (i = 0; i < argc; i++) {
315 		if (strcasecmp(argv[i], "debug") == 0)
316 			debug = 1;
317 		else if (strcasecmp(argv[i], "nowarn") == 0) {
318 			nowarn = 1;
319 			flags = flags | PAM_SILENT;
320 		} else {
321 			__pam_log(LOG_AUTH | LOG_ERR,
322 			    "PAM-KRB5 (acct): illegal option %s",
323 			    argv[i]);
324 		}
325 	}
326 
327 	if (debug)
328 		__pam_log(LOG_AUTH | LOG_DEBUG,
329 		    "PAM-KRB5 (acct): debug=%d, nowarn=%d",
330 		    debug, nowarn);
331 
332 	(void) pam_get_item(pamh, PAM_REPOSITORY, (void **)&rep_data);
333 
334 	if (rep_data != NULL) {
335 		/*
336 		 * If the repository is not ours,
337 		 * return PAM_IGNORE.
338 		 */
339 		if (strcmp(rep_data->type, KRB5_REPOSITORY_NAME) != 0) {
340 			if (debug)
341 				__pam_log(LOG_AUTH | LOG_DEBUG,
342 					"PAM-KRB5 (acct): wrong"
343 					"repository found (%s), returning "
344 					"PAM_IGNORE", rep_data->type);
345 			return (PAM_IGNORE);
346 		}
347 	}
348 
349 
350 	/* get user name */
351 	(void) pam_get_item(pamh, PAM_USER, (void **) &user);
352 
353 	if (user == NULL || *user == '\0') {
354 		err = PAM_USER_UNKNOWN;
355 		goto out;
356 	}
357 
358 	/* get pam_krb5_migrate specific data */
359 	err = pam_get_data(pamh, KRB5_AUTOMIGRATE_DATA,
360 					(const void **)&userdata);
361 	if (err != PAM_SUCCESS) {
362 		if (debug)
363 			__pam_log(LOG_AUTH | LOG_DEBUG, "PAM-KRB5 (acct): "
364 				"no module data for KRB5_AUTOMIGRATE_DATA");
365 	} else {
366 		/*
367 		 * We try and reauthenticate, since this user has a
368 		 * newly created krb5 principal via the pam_krb5_migrate
369 		 * auth module. That way, this new user will have fresh
370 		 * creds (assuming pam_sm_authenticate() succeeds).
371 		 */
372 		if (strcmp(user, userdata) == 0)
373 			(void) pam_sm_authenticate(pamh, flags, argc,
374 					(const char **)argv);
375 		else
376 			if (debug)
377 				__pam_log(LOG_AUTH | LOG_DEBUG,
378 				"PAM-KRB5 (acct): PAM_USER %s"
379 				"does not match user %s from pam_get_data()",
380 				user, (char *)userdata);
381 	}
382 
383 	/* get krb5 module data  */
384 	if ((err = pam_get_data(pamh, KRB5_DATA, (const void **)&kmd))
385 	    != PAM_SUCCESS) {
386 		if (err == PAM_NO_MODULE_DATA) {
387 			/*
388 			 * pam_auth never called (possible config
389 			 * error; no pam_krb5 auth entry in pam.conf),
390 			 */
391 			if (debug) {
392 				__pam_log(LOG_AUTH | LOG_DEBUG,
393 				    "PAM-KRB5 (acct): no module data");
394 			}
395 			err = PAM_IGNORE;
396 			goto out;
397 		} else {
398 			__pam_log(LOG_AUTH | LOG_ERR,
399 				    "PAM-KRB5 (acct): get module"
400 				    " data failed: err=%d",
401 			    err);
402 		}
403 		goto out;
404 	}
405 
406 	debug = debug || kmd->debug;
407 
408 	/*
409 	 * auth mod set status to ignore, most likely cuz root key is
410 	 * in keytab, so skip other checks and return ignore
411 	 */
412 	if (kmd->auth_status == PAM_IGNORE) {
413 		if (debug)
414 			__pam_log(LOG_AUTH | LOG_DEBUG,
415 			    "PAM-KRB5 (acct): kmd auth_status is IGNORE");
416 		err = PAM_IGNORE;
417 		goto out;
418 	}
419 
420 	/*
421 	 * If there is no Kerberos related user and there is authentication
422 	 * data, this means that while the user has successfully passed
423 	 * authentication, Kerberos is not the account authority because there
424 	 * is no valid Kerberos principal.  PAM_IGNORE is returned since
425 	 * Kerberos is not authoritative for this user.  Other modules in the
426 	 * account stack will need to determine the success or failure for this
427 	 * user.
428 	 */
429 	if (kmd->auth_status == PAM_USER_UNKNOWN) {
430 		if (debug)
431 			syslog(LOG_DEBUG,
432 			    "PAM-KRB5 (acct): kmd auth_status is USER UNKNOWN");
433 		err = PAM_IGNORE;
434 		goto out;
435 	}
436 
437 	/*
438 	 * age_status will be set to PAM_NEW_AUTHTOK_REQD in pam_krb5's
439 	 * 'auth' if the user's key/pw has expired and needs to be changed
440 	 */
441 	if (kmd->age_status == PAM_NEW_AUTHTOK_REQD) {
442 		if (!nowarn) {
443 			(void) snprintf(messages[0], sizeof (messages[0]),
444 				dgettext(TEXT_DOMAIN,
445 				"Your Kerberos password has expired.\n"));
446 			(void) __pam_display_msg(pamh, PAM_TEXT_INFO,
447 					1, messages, NULL);
448 		}
449 		err = PAM_NEW_AUTHTOK_REQD;
450 		goto out;
451 	}
452 
453 	if (kmd->auth_status == PAM_SUCCESS && !(flags & PAM_SILENT) &&
454 	    !nowarn && kmd->password) {
455 		/* if we fail, let it slide, it's only a warning brah */
456 		(void) exp_warn(pamh, user, kmd, debug);
457 	}
458 
459 	/*
460 	 * If Kerberos is treated as optional in the PAM stack, it is possible
461 	 * that there is a KRB5_DATA item and a non-Kerberos account authority.
462 	 * In that case, PAM_IGNORE is returned.
463 	 */
464 	err = kmd->auth_status != PAM_SUCCESS ? PAM_IGNORE : kmd->auth_status;
465 
466 out:
467 	if (debug)
468 		__pam_log(LOG_AUTH | LOG_DEBUG,
469 		    "PAM-KRB5 (acct): end: %s", pam_strerror(pamh, err));
470 
471 	return (err);
472 }
473