From 06bdc5c1cd257e7e85d8d29833ca54dd55b3a4f2 Mon Sep 17 00:00:00 2001 From: Greg Hudson Date: Thu, 12 Aug 2010 17:41:41 +0000 Subject: [PATCH] Add GIC option for password/account expiration callback Add a new GIC option to specify a callback to receive password and account expiration times found in an AS reply. See also: http://k5wiki.kerberos.org/wiki/Projects/Password_expiration_API ticket: 6755 git-svn-id: svn://anonsvn.mit.edu/krb5/trunk@24241 dc483132-0cff-0310-8789-dd5450dbe970 --- src/include/k5-int.h | 2 + src/include/krb5/krb5.hin | 44 +++++++++++++++ src/lib/krb5/krb/Makefile.in | 6 +++ src/lib/krb5/krb/gic_opt.c | 19 +++++++ src/lib/krb5/krb/gic_pwd.c | 73 ++++++++++++++++--------- src/lib/krb5/krb/t_expire_warn.c | 90 +++++++++++++++++++++++++++++++ src/lib/krb5/krb/t_expire_warn.py | 62 +++++++++++++++++++++ src/lib/krb5/libkrb5.exports | 1 + 8 files changed, 273 insertions(+), 24 deletions(-) create mode 100644 src/lib/krb5/krb/t_expire_warn.c create mode 100644 src/lib/krb5/krb/t_expire_warn.py diff --git a/src/include/k5-int.h b/src/include/k5-int.h index 19bf26b38..bb078c070 100644 --- a/src/include/k5-int.h +++ b/src/include/k5-int.h @@ -1142,6 +1142,8 @@ typedef struct _krb5_gic_opt_private { char * fast_ccache_name; krb5_ccache out_ccache; krb5_flags fast_flags; + krb5_expire_callback_func *expire_cb; + void *expire_data; } krb5_gic_opt_private; /* diff --git a/src/include/krb5/krb5.hin b/src/include/krb5/krb5.hin index 7d7e425a3..f49ef95e2 100644 --- a/src/include/krb5/krb5.hin +++ b/src/include/krb5/krb5.hin @@ -1003,6 +1003,8 @@ krb5_verify_checksum(krb5_context context, krb5_cksumtype ctype, #define KRB5_LRQ_ONE_LAST_REQ (-5) #define KRB5_LRQ_ALL_PW_EXPTIME 6 #define KRB5_LRQ_ONE_PW_EXPTIME (-6) +#define KRB5_LRQ_ALL_ACCT_EXPTIME 7 +#define KRB5_LRQ_ONE_ACCT_EXPTIME (-7) /* PADATA types */ #define KRB5_PADATA_NONE 0 @@ -2352,6 +2354,48 @@ krb5_get_init_creds_opt_get_fast_flags(krb5_context context, /* Fast flags*/ #define KRB5_FAST_REQUIRED 1l<<0 /*!< Require KDC to support FAST*/ +typedef void +krb5_expire_callback_func(krb5_context context, void *data, + krb5_timestamp password_expiration, + krb5_timestamp account_expiration, + krb5_boolean is_last_req); + +/** + * Set a callback to receive password and account expiration times. + * + * This option only applies to krb5_get_init_creds_password(). @a cb will be + * invoked if and only if credentials are successfully acquired. The callback + * will receive the @a context from the krb5_get_init_creds_password() call and + * the @a data argument supplied with this API. The remaining arguments should + * be interpreted as follows: + * + * If @a is_last_req is true, then the KDC reply contained last-req entries + * which unambiguously indicated the password expiration, account expiration, + * or both. (If either value was not present, the corresponding argument will + * be 0.) Furthermore, a non-zero @a password_expiration should be taken as a + * suggestion from the KDC that a warning be displayed. + * + * If @a is_last_req is false, then @a account_expiration will be 0 and @a + * password_expiration will contain the expiration time of either the password + * or account, or 0 if no expiration time was indicated in the KDC reply. The + * callback should independently decide whether to display a password + * expiration warning. + * + * Note that @a cb may be invoked even if credentials are being acquired for + * the kadmin/changepw service in order to change the password. It is the + * caller's responsibility to avoid displaying a password expiry warning in + * this case. + * + * Setting an expire callback with this API will cause + * krb5_get_init_creds_password() not to send password expiry warnings to the + * prompter, as it ordinarily may. + */ +krb5_error_code KRB5_CALLCONV +krb5_get_init_creds_opt_set_expire_callback(krb5_context context, + krb5_get_init_creds_opt *opt, + krb5_expire_callback_func cb, + void *data); + krb5_error_code KRB5_CALLCONV krb5_get_init_creds_password(krb5_context context, krb5_creds *creds, krb5_principal client, char *password, diff --git a/src/lib/krb5/krb/Makefile.in b/src/lib/krb5/krb/Makefile.in index e52200d4d..895d44478 100644 --- a/src/lib/krb5/krb/Makefile.in +++ b/src/lib/krb5/krb/Makefile.in @@ -383,6 +383,9 @@ t_princ: $(T_PRINC_OBJS) $(KRB5_BASE_DEPLIBS) t_etypes: $(T_ETYPES_OBJS) $(KRB5_BASE_DEPLIBS) $(CC_LINK) -o t_etypes $(T_ETYPES_OBJS) $(KRB5_BASE_LIBS) +t_expire_warn: t_expire_warn.o $(KRB5_BASE_DEPLIBS) + $(CC_LINK) -o $@ t_expire_warn.o $(KRB5_BASE_LIBS) + TEST_PROGS= t_walk_rtree t_kerb t_ser t_deltat t_expand t_authdata t_pac \ t_princ t_etypes @@ -423,6 +426,9 @@ check-unix:: $(TEST_PROGS) $(RUN_SETUP) $(VALGRIND) ./t_princ $(RUN_SETUP) $(VALGRIND) ./t_etypes +check-pytests:: t_expire_warn + $(RUNPYTEST) $(srcdir)/t_expire_warn.py $(PYTESTFLAGS) + clean:: $(RM) $(OUTPRE)t_walk_rtree$(EXEEXT) $(OUTPRE)t_walk_rtree.$(OBJEXT) \ $(OUTPRE)t_kerb$(EXEEXT) $(OUTPRE)t_kerb.$(OBJEXT) \ diff --git a/src/lib/krb5/krb/gic_opt.c b/src/lib/krb5/krb/gic_opt.c index ab29740bb..36f4f00a1 100644 --- a/src/lib/krb5/krb/gic_opt.c +++ b/src/lib/krb5/krb/gic_opt.c @@ -480,3 +480,22 @@ krb5_get_init_creds_opt_get_fast_flags(krb5_context context, *out_flags = opte->opt_private->fast_flags; return retval; } + +krb5_error_code KRB5_CALLCONV +krb5_get_init_creds_opt_set_expire_callback(krb5_context context, + krb5_get_init_creds_opt *opt, + krb5_expire_callback_func cb, + void *data) +{ + krb5_error_code retval = 0; + krb5_gic_opt_ext *opte; + + retval = krb5int_gic_opt_to_opte(context, opt, &opte, 0, + "krb5_get_init_creds_opt_set_" + "expire_callback"); + if (retval) + return retval; + opte->opt_private->expire_cb = cb; + opte->opt_private->expire_data = data; + return retval; +} diff --git a/src/lib/krb5/krb/gic_pwd.c b/src/lib/krb5/krb/gic_pwd.c index af873be75..1e0b741e3 100644 --- a/src/lib/krb5/krb/gic_pwd.c +++ b/src/lib/krb5/krb/gic_pwd.c @@ -106,48 +106,69 @@ krb5_init_creds_set_password(krb5_context context, /* Return the password expiry time indicated by enc_part2. Set *is_last_req * if the information came from a last_req value. */ -static krb5_timestamp -get_expiry_time(krb5_enc_kdc_rep_part *enc_part2, krb5_boolean *is_last_req) +static void +get_expiry_times(krb5_enc_kdc_rep_part *enc_part2, krb5_timestamp *pw_exp, + krb5_timestamp *acct_exp, krb5_boolean *is_last_req) { krb5_last_req_entry **last_req; + krb5_int32 lr_type; + *pw_exp = 0; + *acct_exp = 0; *is_last_req = FALSE; + + /* Look for last-req entries for password or account expiration. */ if (enc_part2->last_req) { for (last_req = enc_part2->last_req; *last_req; last_req++) { - if ((*last_req)->lr_type == KRB5_LRQ_ALL_PW_EXPTIME || - (*last_req)->lr_type == KRB5_LRQ_ONE_PW_EXPTIME) { + lr_type = (*last_req)->lr_type; + if (lr_type == KRB5_LRQ_ALL_PW_EXPTIME || + lr_type == KRB5_LRQ_ONE_PW_EXPTIME) { + *is_last_req = TRUE; + *pw_exp = (*last_req)->value; + } else if (lr_type == KRB5_LRQ_ALL_ACCT_EXPTIME || + lr_type == KRB5_LRQ_ONE_ACCT_EXPTIME) { *is_last_req = TRUE; - return (*last_req)->value; + *acct_exp = (*last_req)->value; } } } - return enc_part2->key_exp; + + /* If we didn't find any, use the ambiguous key_exp field. */ + if (*is_last_req == FALSE) + *pw_exp = enc_part2->key_exp; } -/* Send an appropriate warning to prompter if as_reply indicates that the - * password is going to expiry soon. */ +/* + * Send an appropriate warning prompter if as_reply indicates that the password + * is going to expire soon. If an expire callback was provided, use that + * instead. + */ static void -warn_pw_expiry(krb5_context context, krb5_prompter_fct prompter, void *data, +warn_pw_expiry(krb5_context context, krb5_get_init_creds_opt *options, + krb5_prompter_fct prompter, void *data, const char *in_tkt_service, krb5_kdc_rep *as_reply) { krb5_error_code ret; - krb5_timestamp exp_time, now; + krb5_timestamp pw_exp, acct_exp, now; krb5_boolean is_last_req; krb5_deltat delta; + krb5_gic_opt_ext *opte; char ts[256], banner[1024]; - /* Don't warn if the password is being changed. */ - if (in_tkt_service && strcmp(in_tkt_service, "kadmin/changepw") == 0) - return; + get_expiry_times(as_reply->enc_part2, &pw_exp, &acct_exp, &is_last_req); - /* Get the current time and password expiry time. */ - if (as_reply->enc_part2 == NULL) - return; - ret = krb5_timeofday(context, &now); - if (ret != 0) + ret = krb5int_gic_opt_to_opte(context, options, &opte, 0, ""); + if (ret == 0 && opte->opt_private->expire_cb != NULL) { + krb5_expire_callback_func *cb = opte->opt_private->expire_cb; + void *cb_data = opte->opt_private->expire_data; + + /* Invoke the expire callback and don't send prompter warnings. */ + (*cb)(context, cb_data, pw_exp, acct_exp, is_last_req); return; - exp_time = get_expiry_time(as_reply->enc_part2, &is_last_req); - if (exp_time == 0) + } + + /* Don't warn if the password is being changed. */ + if (in_tkt_service && strcmp(in_tkt_service, "kadmin/changepw") == 0) return; /* @@ -155,18 +176,21 @@ warn_pw_expiry(krb5_context context, krb5_prompter_fct prompter, void *data, * to warn. Otherwise, warn only if the expiry time is less than a week * from now. */ + ret = krb5_timeofday(context, &now); + if (ret != 0) + return; if (!is_last_req && - (exp_time < now || (exp_time - now) > 7 * 24 * 60 * 60)) + (pw_exp < now || (pw_exp - now) > 7 * 24 * 60 * 60)) return; if (!prompter) return; - ret = krb5_timestamp_to_string(exp_time, ts, sizeof(ts)); + ret = krb5_timestamp_to_string(pw_exp, ts, sizeof(ts)); if (ret != 0) return; - delta = exp_time - now; + delta = pw_exp - now; if (delta < 3600) { snprintf(banner, sizeof(banner), "Warning: Your password will expire in less than one hour " @@ -418,7 +442,8 @@ krb5_get_init_creds_password(krb5_context context, cleanup: if (ret == 0) - warn_pw_expiry(context, prompter, data, in_tkt_service, as_reply); + warn_pw_expiry(context, options, prompter, data, in_tkt_service, + as_reply); if (chpw_opts) krb5_get_init_creds_opt_free(context, chpw_opts); diff --git a/src/lib/krb5/krb/t_expire_warn.c b/src/lib/krb5/krb/t_expire_warn.c new file mode 100644 index 000000000..6e8d87ce1 --- /dev/null +++ b/src/lib/krb5/krb/t_expire_warn.c @@ -0,0 +1,90 @@ +/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */ +/* + * lib/krb5/krb/t_expire_warn.c + * + * Copyright (C) 2010 by the Massachusetts Institute of Technology. + * All rights reserved. + * + * Export of this software from the United States of America may + * require a specific license from the United States Government. + * It is the responsibility of any person or organization contemplating + * export to obtain such a license before exporting. + * + * WITHIN THAT CONSTRAINT, permission to use, copy, modify, and + * distribute this software and its documentation for any purpose and + * without fee is hereby granted, provided that the above copyright + * notice appear in all copies and that both that copyright notice and + * this permission notice appear in supporting documentation, and that + * the name of M.I.T. not be used in advertising or publicity pertaining + * to distribution of the software without specific, written prior + * permission. Furthermore if you modify this software you must label + * your software as modified software and not distribute it in such a + * fashion that it might be confused with the original M.I.T. software. + * M.I.T. makes no representations about the suitability of + * this software for any purpose. It is provided "as is" without express + * or implied warranty. + * + * + * Test harness for password expiration warnings. + */ + +#include "k5-int.h" + +static int exp_dummy, prompt_dummy; + +static krb5_error_code +prompter_cb(krb5_context ctx, void *data, const char *name, + const char *banner, int num_prompts, krb5_prompt prompts[]) +{ + /* Not expecting any actual prompts, only banners. */ + assert(num_prompts == 0); + assert(banner != NULL); + printf("Prompter: %s\n", banner); + return 0; +} + +static void +expire_cb(krb5_context ctx, void *data, krb5_timestamp password_expiration, + krb5_timestamp account_expiration, krb5_boolean is_last_req) +{ + printf("password_expiration = %ld\n", (long)password_expiration); + printf("account_expiration = %ld\n", (long)account_expiration); + printf("is_last_req = %d\n", (int)is_last_req); +} + +int +main(int argc, char **argv) +{ + krb5_context ctx; + krb5_get_init_creds_opt *opt; + char *user, *password, *service = NULL; + krb5_boolean use_cb; + krb5_principal client; + krb5_creds creds; + + if (argc < 4) { + fprintf(stderr, "Usage: %s username password {1|0} [service]\n", + argv[0]); + return 1; + } + user = argv[1]; + password = argv[2]; + use_cb = atoi(argv[3]); + if (argc >= 5) + service = argv[4]; + + assert(krb5_init_context(&ctx) == 0); + assert(krb5_get_init_creds_opt_alloc(ctx, &opt) == 0); + if (use_cb) { + assert(krb5_get_init_creds_opt_set_expire_callback(ctx, opt, expire_cb, + &exp_dummy) == 0); + } + assert(krb5_parse_name(ctx, user, &client) == 0); + assert(krb5_get_init_creds_password(ctx, &creds, client, password, + prompter_cb, &prompt_dummy, 0, service, + opt) == 0); + krb5_get_init_creds_opt_free(ctx, opt); + krb5_free_principal(ctx, client); + krb5_free_cred_contents(ctx, &creds); + return 0; +} diff --git a/src/lib/krb5/krb/t_expire_warn.py b/src/lib/krb5/krb/t_expire_warn.py new file mode 100644 index 000000000..dc49a4c03 --- /dev/null +++ b/src/lib/krb5/krb/t_expire_warn.py @@ -0,0 +1,62 @@ +# Copyright (C) 2010 by the Massachusetts Institute of Technology. +# All rights reserved. +# +# Export of this software from the United States of America may +# require a specific license from the United States Government. +# It is the responsibility of any person or organization contemplating +# export to obtain such a license before exporting. +# +# WITHIN THAT CONSTRAINT, permission to use, copy, modify, and +# distribute this software and its documentation for any purpose and +# without fee is hereby granted, provided that the above copyright +# notice appear in all copies and that both that copyright notice and +# this permission notice appear in supporting documentation, and that +# the name of M.I.T. not be used in advertising or publicity pertaining +# to distribution of the software without specific, written prior +# permission. Furthermore if you modify this software you must label +# your software as modified software and not distribute it in such a +# fashion that it might be confused with the original M.I.T. software. +# M.I.T. makes no representations about the suitability of +# this software for any purpose. It is provided "as is" without express +# or implied warranty. + +#!/usr/bin/python +from k5test import * + +# Create a bare-bones KDC. +realm = K5Realm(create_user=False, create_host=False, start_kadmind=False) + +# Create principals with various password expirations. +realm.run_kadminl('addprinc -pw pass noexpire') +realm.run_kadminl('addprinc -pw pass -pwexpire "30 minutes" minutes') +realm.run_kadminl('addprinc -pw pass -pwexpire "12 hours" hours') +realm.run_kadminl('addprinc -pw pass -pwexpire "3 days" days') + +# Check for expected prompter warnings when no expire callback is used. +output = realm.run_as_client(['./t_expire_warn', 'noexpire', 'pass', '0']) +if output: + fail('Unexpected output for noexpire') +output = realm.run_as_client(['./t_expire_warn', 'minutes', 'pass', '0']) +if ' less than one hour on ' not in output: + fail('Expected warning not seen for minutes') +output = realm.run_as_client(['./t_expire_warn', 'hours', 'pass', '0']) +if ' hours on ' not in output: + fail('Expected warning not seen for hours') +output = realm.run_as_client(['./t_expire_warn', 'days', 'pass', '0']) +if ' days on ' not in output: + fail('Expected warning not seen for days') + +# Check for expected expire callback behavior. These tests are +# carefully agnostic about whether the KDC supports last_req fields, +# and could be made more specific if last_req support is added. +output = realm.run_as_client(['./t_expire_warn', 'noexpire', 'pass', '1']) +if 'password_expiration = 0\n' not in output or \ + 'account_expiration = 0\n' not in output or \ + 'is_last_req = ' not in output: + fail('Expected callback output not seen for noexpire') +output = realm.run_as_client(['./t_expire_warn', 'days', 'pass', '1']) +if 'password_expiration = ' not in output or \ + 'password_expiration = 0\n' in output: + fail('Expected non-zero password expiration not seen for days') + +success('Password expiration warning tests.') diff --git a/src/lib/krb5/libkrb5.exports b/src/lib/krb5/libkrb5.exports index 2bd597204..af661edcc 100644 --- a/src/lib/krb5/libkrb5.exports +++ b/src/lib/krb5/libkrb5.exports @@ -346,6 +346,7 @@ krb5_get_init_creds_opt_set_anonymous krb5_get_init_creds_opt_set_canonicalize krb5_get_init_creds_opt_set_change_password_prompt krb5_get_init_creds_opt_set_etype_list +krb5_get_init_creds_opt_set_expire_callback krb5_get_init_creds_opt_set_fast_ccache_name krb5_get_init_creds_opt_set_fast_flags krb5_get_init_creds_opt_set_forwardable -- 2.26.2