0ba406
From ff6cf2a0545d12a020572dd137fd22d1edc726e4 Mon Sep 17 00:00:00 2001
9c4c6b
From: Sumit Bose <sbose@redhat.com>
9c4c6b
Date: Fri, 28 Feb 2020 10:11:49 +0100
9c4c6b
Subject: [PATCH] Do expiration warnings for all init_creds APIs
9c4c6b
9c4c6b
Move the password expiration warning code from gic_pwd.c to
9c4c6b
get_in_tkt.c.  Call it from init_creds_step_reply() on successful
9c4c6b
completion.
9c4c6b
9c4c6b
[ghudson@mit.edu: added test case; simplified doc comment; moved call
9c4c6b
site to init_creds_step_reply(); rewrote commit message]
9c4c6b
9c4c6b
ticket: 8893 (new)
9c4c6b
(cherry picked from commit e1efb890f7ac31b32c68ab816ef118dbfb5a8c7e)
9c4c6b
(cherry picked from commit c136cfe050d203c910624573a33247fde2889b09)
9c4c6b
---
9c4c6b
 src/include/krb5/krb5.hin         |   9 ++-
9c4c6b
 src/lib/krb5/krb/get_in_tkt.c     | 112 ++++++++++++++++++++++++++++++
9c4c6b
 src/lib/krb5/krb/gic_pwd.c        | 110 -----------------------------
9c4c6b
 src/lib/krb5/krb/t_expire_warn.c  |  47 +++++++++----
9c4c6b
 src/lib/krb5/krb/t_expire_warn.py |  22 ++++--
9c4c6b
 5 files changed, 165 insertions(+), 135 deletions(-)
9c4c6b
9c4c6b
diff --git a/src/include/krb5/krb5.hin b/src/include/krb5/krb5.hin
9c4c6b
index 6355e6540..f8269fb17 100644
9c4c6b
--- a/src/include/krb5/krb5.hin
9c4c6b
+++ b/src/include/krb5/krb5.hin
9c4c6b
@@ -7174,11 +7174,10 @@ typedef void
9c4c6b
  *
9c4c6b
  * Set a callback to receive password and account expiration times.
9c4c6b
  *
9c4c6b
- * This option only applies to krb5_get_init_creds_password().  @a cb will be
9c4c6b
- * invoked if and only if credentials are successfully acquired.  The callback
9c4c6b
- * will receive the @a context from the krb5_get_init_creds_password() call and
9c4c6b
- * the @a data argument supplied with this API.  The remaining arguments should
9c4c6b
- * be interpreted as follows:
9c4c6b
+ * @a cb will be invoked if and only if credentials are successfully acquired.
9c4c6b
+ * The callback will receive the @a context from the calling function and the
9c4c6b
+ * @a data argument supplied with this API.  The remaining arguments should be
9c4c6b
+ * interpreted as follows:
9c4c6b
  *
9c4c6b
  * If @a is_last_req is true, then the KDC reply contained last-req entries
9c4c6b
  * which unambiguously indicated the password expiration, account expiration,
9c4c6b
diff --git a/src/lib/krb5/krb/get_in_tkt.c b/src/lib/krb5/krb/get_in_tkt.c
9c4c6b
index 870df62a1..cc0f70e83 100644
9c4c6b
--- a/src/lib/krb5/krb/get_in_tkt.c
9c4c6b
+++ b/src/lib/krb5/krb/get_in_tkt.c
9c4c6b
@@ -1482,6 +1482,116 @@ accept_method_data(krb5_context context, krb5_init_creds_context ctx)
9c4c6b
                                      ctx->method_padata);
9c4c6b
 }
9c4c6b
 
9c4c6b
+/* Return the password expiry time indicated by enc_part2.  Set *is_last_req
9c4c6b
+ * if the information came from a last_req value. */
9c4c6b
+static void
9c4c6b
+get_expiry_times(krb5_enc_kdc_rep_part *enc_part2, krb5_timestamp *pw_exp,
9c4c6b
+                 krb5_timestamp *acct_exp, krb5_boolean *is_last_req)
9c4c6b
+{
9c4c6b
+    krb5_last_req_entry **last_req;
9c4c6b
+    krb5_int32 lr_type;
9c4c6b
+
9c4c6b
+    *pw_exp = 0;
9c4c6b
+    *acct_exp = 0;
9c4c6b
+    *is_last_req = FALSE;
9c4c6b
+
9c4c6b
+    /* Look for last-req entries for password or account expiration. */
9c4c6b
+    if (enc_part2->last_req) {
9c4c6b
+        for (last_req = enc_part2->last_req; *last_req; last_req++) {
9c4c6b
+            lr_type = (*last_req)->lr_type;
9c4c6b
+            if (lr_type == KRB5_LRQ_ALL_PW_EXPTIME ||
9c4c6b
+                lr_type == KRB5_LRQ_ONE_PW_EXPTIME) {
9c4c6b
+                *is_last_req = TRUE;
9c4c6b
+                *pw_exp = (*last_req)->value;
9c4c6b
+            } else if (lr_type == KRB5_LRQ_ALL_ACCT_EXPTIME ||
9c4c6b
+                       lr_type == KRB5_LRQ_ONE_ACCT_EXPTIME) {
9c4c6b
+                *is_last_req = TRUE;
9c4c6b
+                *acct_exp = (*last_req)->value;
9c4c6b
+            }
9c4c6b
+        }
9c4c6b
+    }
9c4c6b
+
9c4c6b
+    /* If we didn't find any, use the ambiguous key_exp field. */
9c4c6b
+    if (*is_last_req == FALSE)
9c4c6b
+        *pw_exp = enc_part2->key_exp;
9c4c6b
+}
9c4c6b
+
9c4c6b
+/*
9c4c6b
+ * Send an appropriate warning prompter if as_reply indicates that the password
9c4c6b
+ * is going to expire soon.  If an expire callback was provided, use that
9c4c6b
+ * instead.
9c4c6b
+ */
9c4c6b
+static void
9c4c6b
+warn_pw_expiry(krb5_context context, krb5_get_init_creds_opt *options,
9c4c6b
+               krb5_prompter_fct prompter, void *data,
9c4c6b
+               const char *in_tkt_service, krb5_kdc_rep *as_reply)
9c4c6b
+{
9c4c6b
+    krb5_error_code ret;
9c4c6b
+    krb5_expire_callback_func expire_cb;
9c4c6b
+    void *expire_data;
9c4c6b
+    krb5_timestamp pw_exp, acct_exp, now;
9c4c6b
+    krb5_boolean is_last_req;
9c4c6b
+    krb5_deltat delta;
9c4c6b
+    char ts[256], banner[1024];
9c4c6b
+
9c4c6b
+    if (as_reply == NULL || as_reply->enc_part2 == NULL)
9c4c6b
+        return;
9c4c6b
+
9c4c6b
+    get_expiry_times(as_reply->enc_part2, &pw_exp, &acct_exp, &is_last_req);
9c4c6b
+
9c4c6b
+    k5_gic_opt_get_expire_cb(options, &expire_cb, &expire_data);
9c4c6b
+    if (expire_cb != NULL) {
9c4c6b
+        /* Invoke the expire callback and don't send prompter warnings. */
9c4c6b
+        (*expire_cb)(context, expire_data, pw_exp, acct_exp, is_last_req);
9c4c6b
+        return;
9c4c6b
+    }
9c4c6b
+
9c4c6b
+    /* Don't warn if no password expiry value was sent. */
9c4c6b
+    if (pw_exp == 0)
9c4c6b
+        return;
9c4c6b
+
9c4c6b
+    /* Don't warn if the password is being changed. */
9c4c6b
+    if (in_tkt_service && strcmp(in_tkt_service, "kadmin/changepw") == 0)
9c4c6b
+        return;
9c4c6b
+
9c4c6b
+    /*
9c4c6b
+     * If the expiry time came from a last_req field, assume the KDC wants us
9c4c6b
+     * to warn.  Otherwise, warn only if the expiry time is less than a week
9c4c6b
+     * from now.
9c4c6b
+     */
9c4c6b
+    ret = krb5_timeofday(context, &now;;
9c4c6b
+    if (ret != 0)
9c4c6b
+        return;
9c4c6b
+    if (!is_last_req &&
9c4c6b
+        (ts_after(now, pw_exp) || ts_delta(pw_exp, now) > 7 * 24 * 60 * 60))
9c4c6b
+        return;
9c4c6b
+
9c4c6b
+    if (!prompter)
9c4c6b
+        return;
9c4c6b
+
9c4c6b
+    ret = krb5_timestamp_to_string(pw_exp, ts, sizeof(ts));
9c4c6b
+    if (ret != 0)
9c4c6b
+        return;
9c4c6b
+
9c4c6b
+    delta = ts_delta(pw_exp, now);
9c4c6b
+    if (delta < 3600) {
9c4c6b
+        snprintf(banner, sizeof(banner),
9c4c6b
+                 _("Warning: Your password will expire in less than one hour "
9c4c6b
+                   "on %s"), ts);
9c4c6b
+    } else if (delta < 86400 * 2) {
9c4c6b
+        snprintf(banner, sizeof(banner),
9c4c6b
+                 _("Warning: Your password will expire in %d hour%s on %s"),
9c4c6b
+                 delta / 3600, delta < 7200 ? "" : "s", ts);
9c4c6b
+    } else {
9c4c6b
+        snprintf(banner, sizeof(banner),
9c4c6b
+                 _("Warning: Your password will expire in %d days on %s"),
9c4c6b
+                 delta / 86400, ts);
9c4c6b
+    }
9c4c6b
+
9c4c6b
+    /* PROMPTER_INVOCATION */
9c4c6b
+    (*prompter)(context, data, 0, banner, 0, 0);
9c4c6b
+}
9c4c6b
+
9c4c6b
 static krb5_error_code
9c4c6b
 init_creds_step_reply(krb5_context context,
9c4c6b
                       krb5_init_creds_context ctx,
9c4c6b
@@ -1693,6 +1803,8 @@ init_creds_step_reply(krb5_context context,
9c4c6b
 
9c4c6b
     /* success */
9c4c6b
     ctx->complete = TRUE;
9c4c6b
+    warn_pw_expiry(context, ctx->opt, ctx->prompter, ctx->prompter_data,
9c4c6b
+                   ctx->in_tkt_service, ctx->reply);
9c4c6b
 
9c4c6b
 cleanup:
9c4c6b
     krb5_free_pa_data(context, kdc_padata);
9c4c6b
diff --git a/src/lib/krb5/krb/gic_pwd.c b/src/lib/krb5/krb/gic_pwd.c
9c4c6b
index 14ce23ba4..54e0a8ebe 100644
9c4c6b
--- a/src/lib/krb5/krb/gic_pwd.c
9c4c6b
+++ b/src/lib/krb5/krb/gic_pwd.c
9c4c6b
@@ -133,113 +133,6 @@ krb5_init_creds_set_password(krb5_context context,
9c4c6b
     return 0;
9c4c6b
 }
9c4c6b
 
9c4c6b
-/* Return the password expiry time indicated by enc_part2.  Set *is_last_req
9c4c6b
- * if the information came from a last_req value. */
9c4c6b
-static void
9c4c6b
-get_expiry_times(krb5_enc_kdc_rep_part *enc_part2, krb5_timestamp *pw_exp,
9c4c6b
-                 krb5_timestamp *acct_exp, krb5_boolean *is_last_req)
9c4c6b
-{
9c4c6b
-    krb5_last_req_entry **last_req;
9c4c6b
-    krb5_int32 lr_type;
9c4c6b
-
9c4c6b
-    *pw_exp = 0;
9c4c6b
-    *acct_exp = 0;
9c4c6b
-    *is_last_req = FALSE;
9c4c6b
-
9c4c6b
-    /* Look for last-req entries for password or account expiration. */
9c4c6b
-    if (enc_part2->last_req) {
9c4c6b
-        for (last_req = enc_part2->last_req; *last_req; last_req++) {
9c4c6b
-            lr_type = (*last_req)->lr_type;
9c4c6b
-            if (lr_type == KRB5_LRQ_ALL_PW_EXPTIME ||
9c4c6b
-                lr_type == KRB5_LRQ_ONE_PW_EXPTIME) {
9c4c6b
-                *is_last_req = TRUE;
9c4c6b
-                *pw_exp = (*last_req)->value;
9c4c6b
-            } else if (lr_type == KRB5_LRQ_ALL_ACCT_EXPTIME ||
9c4c6b
-                       lr_type == KRB5_LRQ_ONE_ACCT_EXPTIME) {
9c4c6b
-                *is_last_req = TRUE;
9c4c6b
-                *acct_exp = (*last_req)->value;
9c4c6b
-            }
9c4c6b
-        }
9c4c6b
-    }
9c4c6b
-
9c4c6b
-    /* If we didn't find any, use the ambiguous key_exp field. */
9c4c6b
-    if (*is_last_req == FALSE)
9c4c6b
-        *pw_exp = enc_part2->key_exp;
9c4c6b
-}
9c4c6b
-
9c4c6b
-/*
9c4c6b
- * Send an appropriate warning prompter if as_reply indicates that the password
9c4c6b
- * is going to expire soon.  If an expire callback was provided, use that
9c4c6b
- * instead.
9c4c6b
- */
9c4c6b
-static void
9c4c6b
-warn_pw_expiry(krb5_context context, krb5_get_init_creds_opt *options,
9c4c6b
-               krb5_prompter_fct prompter, void *data,
9c4c6b
-               const char *in_tkt_service, krb5_kdc_rep *as_reply)
9c4c6b
-{
9c4c6b
-    krb5_error_code ret;
9c4c6b
-    krb5_expire_callback_func expire_cb;
9c4c6b
-    void *expire_data;
9c4c6b
-    krb5_timestamp pw_exp, acct_exp, now;
9c4c6b
-    krb5_boolean is_last_req;
9c4c6b
-    krb5_deltat delta;
9c4c6b
-    char ts[256], banner[1024];
9c4c6b
-
9c4c6b
-    get_expiry_times(as_reply->enc_part2, &pw_exp, &acct_exp, &is_last_req);
9c4c6b
-
9c4c6b
-    k5_gic_opt_get_expire_cb(options, &expire_cb, &expire_data);
9c4c6b
-    if (expire_cb != NULL) {
9c4c6b
-        /* Invoke the expire callback and don't send prompter warnings. */
9c4c6b
-        (*expire_cb)(context, expire_data, pw_exp, acct_exp, is_last_req);
9c4c6b
-        return;
9c4c6b
-    }
9c4c6b
-
9c4c6b
-    /* Don't warn if no password expiry value was sent. */
9c4c6b
-    if (pw_exp == 0)
9c4c6b
-        return;
9c4c6b
-
9c4c6b
-    /* Don't warn if the password is being changed. */
9c4c6b
-    if (in_tkt_service && strcmp(in_tkt_service, "kadmin/changepw") == 0)
9c4c6b
-        return;
9c4c6b
-
9c4c6b
-    /*
9c4c6b
-     * If the expiry time came from a last_req field, assume the KDC wants us
9c4c6b
-     * to warn.  Otherwise, warn only if the expiry time is less than a week
9c4c6b
-     * from now.
9c4c6b
-     */
9c4c6b
-    ret = krb5_timeofday(context, &now;;
9c4c6b
-    if (ret != 0)
9c4c6b
-        return;
9c4c6b
-    if (!is_last_req &&
9c4c6b
-        (ts_after(now, pw_exp) || ts_delta(pw_exp, now) > 7 * 24 * 60 * 60))
9c4c6b
-        return;
9c4c6b
-
9c4c6b
-    if (!prompter)
9c4c6b
-        return;
9c4c6b
-
9c4c6b
-    ret = krb5_timestamp_to_string(pw_exp, ts, sizeof(ts));
9c4c6b
-    if (ret != 0)
9c4c6b
-        return;
9c4c6b
-
9c4c6b
-    delta = ts_delta(pw_exp, now);
9c4c6b
-    if (delta < 3600) {
9c4c6b
-        snprintf(banner, sizeof(banner),
9c4c6b
-                 _("Warning: Your password will expire in less than one hour "
9c4c6b
-                   "on %s"), ts);
9c4c6b
-    } else if (delta < 86400*2) {
9c4c6b
-        snprintf(banner, sizeof(banner),
9c4c6b
-                 _("Warning: Your password will expire in %d hour%s on %s"),
9c4c6b
-                 delta / 3600, delta < 7200 ? "" : "s", ts);
9c4c6b
-    } else {
9c4c6b
-        snprintf(banner, sizeof(banner),
9c4c6b
-                 _("Warning: Your password will expire in %d days on %s"),
9c4c6b
-                 delta / 86400, ts);
9c4c6b
-    }
9c4c6b
-
9c4c6b
-    /* PROMPTER_INVOCATION */
9c4c6b
-    (*prompter)(context, data, 0, banner, 0, 0);
9c4c6b
-}
9c4c6b
-
9c4c6b
 /*
9c4c6b
  * Create a temporary options structure for getting a kadmin/changepw ticket,
9c4c6b
  * based on the appplication-specified options.  Propagate all application
9c4c6b
@@ -496,9 +389,6 @@ krb5_get_init_creds_password(krb5_context context,
9c4c6b
         goto cleanup;
9c4c6b
 
9c4c6b
 cleanup:
9c4c6b
-    if (ret == 0)
9c4c6b
-        warn_pw_expiry(context, options, prompter, data, in_tkt_service,
9c4c6b
-                       as_reply);
9c4c6b
     free(chpw_opts);
9c4c6b
     zapfree(gakpw.storage.data, gakpw.storage.length);
9c4c6b
     memset(pw0array, 0, sizeof(pw0array));
9c4c6b
diff --git a/src/lib/krb5/krb/t_expire_warn.c b/src/lib/krb5/krb/t_expire_warn.c
9c4c6b
index 1e59acba1..dc8dc8fb3 100644
9c4c6b
--- a/src/lib/krb5/krb/t_expire_warn.c
9c4c6b
+++ b/src/lib/krb5/krb/t_expire_warn.c
9c4c6b
@@ -28,6 +28,13 @@
9c4c6b
 
9c4c6b
 static int exp_dummy, prompt_dummy;
9c4c6b
 
9c4c6b
+static void
9c4c6b
+check(krb5_error_code code)
9c4c6b
+{
9c4c6b
+    if (code != 0)
9c4c6b
+        abort();
9c4c6b
+}
9c4c6b
+
9c4c6b
 static krb5_error_code
9c4c6b
 prompter_cb(krb5_context ctx, void *data, const char *name,
9c4c6b
             const char *banner, int num_prompts, krb5_prompt prompts[])
9c4c6b
@@ -52,36 +59,48 @@ int
9c4c6b
 main(int argc, char **argv)
9c4c6b
 {
9c4c6b
     krb5_context ctx;
9c4c6b
+    krb5_init_creds_context icctx;
9c4c6b
     krb5_get_init_creds_opt *opt;
9c4c6b
     char *user, *password, *service = NULL;
9c4c6b
-    krb5_boolean use_cb;
9c4c6b
+    krb5_boolean use_cb, stepwise;
9c4c6b
     krb5_principal client;
9c4c6b
     krb5_creds creds;
9c4c6b
 
9c4c6b
-    if (argc < 4) {
9c4c6b
-        fprintf(stderr, "Usage: %s username password {1|0} [service]\n",
9c4c6b
+    if (argc < 5) {
9c4c6b
+        fprintf(stderr, "Usage: %s username password {1|0} {1|0} [service]\n",
9c4c6b
                 argv[0]);
9c4c6b
         return 1;
9c4c6b
     }
9c4c6b
     user = argv[1];
9c4c6b
     password = argv[2];
9c4c6b
     use_cb = atoi(argv[3]);
9c4c6b
-    if (argc >= 5)
9c4c6b
-        service = argv[4];
9c4c6b
+    stepwise = atoi(argv[4]);
9c4c6b
+    if (argc >= 6)
9c4c6b
+        service = argv[5];
9c4c6b
 
9c4c6b
-    assert(krb5_init_context(&ctx) == 0);
9c4c6b
-    assert(krb5_get_init_creds_opt_alloc(ctx, &opt) == 0);
9c4c6b
+    check(krb5_init_context(&ctx));
9c4c6b
+    check(krb5_get_init_creds_opt_alloc(ctx, &opt));
9c4c6b
     if (use_cb) {
9c4c6b
-        assert(krb5_get_init_creds_opt_set_expire_callback(ctx, opt, expire_cb,
9c4c6b
-                                                           &exp_dummy) == 0);
9c4c6b
+        check(krb5_get_init_creds_opt_set_expire_callback(ctx, opt, expire_cb,
9c4c6b
+                                                          &exp_dummy));
9c4c6b
+    }
9c4c6b
+    check(krb5_parse_name(ctx, user, &client));
9c4c6b
+    if (stepwise) {
9c4c6b
+        check(krb5_init_creds_init(ctx, client, prompter_cb, &prompt_dummy, 0,
9c4c6b
+                                   opt, &icctx));
9c4c6b
+        krb5_init_creds_set_password(ctx, icctx, password);
9c4c6b
+        if (service != NULL)
9c4c6b
+            check(krb5_init_creds_set_service(ctx, icctx, service));
9c4c6b
+        check(krb5_init_creds_get(ctx, icctx));
9c4c6b
+        krb5_init_creds_free(ctx, icctx);
9c4c6b
+    } else {
9c4c6b
+        check(krb5_get_init_creds_password(ctx, &creds, client, password,
9c4c6b
+                                           prompter_cb, &prompt_dummy, 0,
9c4c6b
+                                           service, opt));
9c4c6b
+        krb5_free_cred_contents(ctx, &creds);
9c4c6b
     }
9c4c6b
-    assert(krb5_parse_name(ctx, user, &client) == 0);
9c4c6b
-    assert(krb5_get_init_creds_password(ctx, &creds, client, password,
9c4c6b
-                                        prompter_cb, &prompt_dummy, 0, service,
9c4c6b
-                                        opt) == 0);
9c4c6b
     krb5_get_init_creds_opt_free(ctx, opt);
9c4c6b
     krb5_free_principal(ctx, client);
9c4c6b
-    krb5_free_cred_contents(ctx, &creds);
9c4c6b
     krb5_free_context(ctx);
9c4c6b
     return 0;
9c4c6b
 }
9c4c6b
diff --git a/src/lib/krb5/krb/t_expire_warn.py b/src/lib/krb5/krb/t_expire_warn.py
9c4c6b
index 781f2728a..e163cc7e4 100755
9c4c6b
--- a/src/lib/krb5/krb/t_expire_warn.py
9c4c6b
+++ b/src/lib/krb5/krb/t_expire_warn.py
9c4c6b
@@ -34,23 +34,33 @@ realm.run([kadminl, 'addprinc', '-pw', 'pass', '-pwexpire', '12 hours',
9c4c6b
 realm.run([kadminl, 'addprinc', '-pw', 'pass', '-pwexpire', '3 days', 'days'])
9c4c6b
 
9c4c6b
 # Check for expected prompter warnings when no expire callback is used.
9c4c6b
-output = realm.run(['./t_expire_warn', 'noexpire', 'pass', '0'])
9c4c6b
+output = realm.run(['./t_expire_warn', 'noexpire', 'pass', '0', '0'])
9c4c6b
 if output:
9c4c6b
     fail('Unexpected output for noexpire')
9c4c6b
-realm.run(['./t_expire_warn', 'minutes', 'pass', '0'],
9c4c6b
+realm.run(['./t_expire_warn', 'minutes', 'pass', '0', '0'],
9c4c6b
           expected_msg=' less than one hour on ')
9c4c6b
-realm.run(['./t_expire_warn', 'hours', 'pass', '0'], expected_msg=' hours on ')
9c4c6b
-realm.run(['./t_expire_warn', 'days', 'pass', '0'], expected_msg=' days on ')
9c4c6b
+realm.run(['./t_expire_warn', 'hours', 'pass', '0', '0'],
9c4c6b
+          expected_msg=' hours on ')
9c4c6b
+realm.run(['./t_expire_warn', 'days', 'pass', '0', '0'],
9c4c6b
+          expected_msg=' days on ')
9c4c6b
+# Try one case with the stepwise interface.
9c4c6b
+realm.run(['./t_expire_warn', 'days', 'pass', '0', '1'],
9c4c6b
+          expected_msg=' days on ')
9c4c6b
 
9c4c6b
 # Check for expected expire callback behavior.  These tests are
9c4c6b
 # carefully agnostic about whether the KDC supports last_req fields,
9c4c6b
 # and could be made more specific if last_req support is added.
9c4c6b
-output = realm.run(['./t_expire_warn', 'noexpire', 'pass', '1'])
9c4c6b
+output = realm.run(['./t_expire_warn', 'noexpire', 'pass', '1', '0'])
9c4c6b
 if 'password_expiration = 0\n' not in output or \
9c4c6b
         'account_expiration = 0\n' not in output or \
9c4c6b
         'is_last_req = ' not in output:
9c4c6b
     fail('Expected callback output not seen for noexpire')
9c4c6b
-output = realm.run(['./t_expire_warn', 'days', 'pass', '1'])
9c4c6b
+output = realm.run(['./t_expire_warn', 'days', 'pass', '1', '0'])
9c4c6b
+if 'password_expiration = ' not in output or \
9c4c6b
+        'password_expiration = 0\n' in output:
9c4c6b
+    fail('Expected non-zero password expiration not seen for days')
9c4c6b
+# Try one case with the stepwise interface.
9c4c6b
+output = realm.run(['./t_expire_warn', 'days', 'pass', '1', '1'])
9c4c6b
 if 'password_expiration = ' not in output or \
9c4c6b
         'password_expiration = 0\n' in output:
9c4c6b
     fail('Expected non-zero password expiration not seen for days')