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