Add GIC option for password/account expiration callback
authorGreg Hudson <ghudson@mit.edu>
Thu, 12 Aug 2010 17:41:41 +0000 (17:41 +0000)
committerGreg Hudson <ghudson@mit.edu>
Thu, 12 Aug 2010 17:41:41 +0000 (17:41 +0000)
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
src/include/krb5/krb5.hin
src/lib/krb5/krb/Makefile.in
src/lib/krb5/krb/gic_opt.c
src/lib/krb5/krb/gic_pwd.c
src/lib/krb5/krb/t_expire_warn.c [new file with mode: 0644]
src/lib/krb5/krb/t_expire_warn.py [new file with mode: 0644]
src/lib/krb5/libkrb5.exports

index 19bf26b380b21660b12b76b420213167c29adbb5..bb078c0705ca18238f5f03e8c95102177557a490 100644 (file)
@@ -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;
 
 /*
index 7d7e425a3e2e4f7409c3f6b861d6413cf3ad4da0..f49ef95e208bd81e4f46db5b052ab3074dcfb523 100644 (file)
@@ -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,
index e52200d4d95aed6743404c9769ff4c05e714055d..895d44478aa9e0032e5a8eb0914971ff51117def 100644 (file)
@@ -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)      \
index ab29740bb6352e06bd63bada8ac3e56d41bb6043..36f4f00a1f3ff9ea4cd900c573be200685323619 100644 (file)
@@ -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;
+}
index af873be75fde618612f92c71951340e6d27e4625..1e0b741e38ac141395996a1832b492ad89284db7 100644 (file)
@@ -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 (file)
index 0000000..6e8d87c
--- /dev/null
@@ -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 (file)
index 0000000..dc49a4c
--- /dev/null
@@ -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.')
index 2bd597204b81c01e80f39f670507a5b827f41315..af661edcc586a511a9f8a6d7e975e6468c32dfb8 100644 (file)
@@ -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