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