This plugin prevents the deletion or deactivation of the last valid
token for a user. This prevents the user from migrating back to single
factor authentication once OTP has been enabled.

Thanks to Mark Reynolds for helping me with this patch.
>From 366d508bcd7ba0431e2f2d4119ad6f85f81d9963 Mon Sep 17 00:00:00 2001
From: Nathaniel McCallum <npmccal...@redhat.com>
Date: Mon, 16 Dec 2013 16:19:08 -0500
Subject: [PATCH] Add OTP last token plugin

This plugin prevents the deletion or deactivation of the last
valid token for a user. This prevents the user from migrating
back to single factor authentication once OTP has been enabled.

Thanks to Mark Reynolds for helping me with this patch.
---
 daemons/configure.ac                               |   1 +
 daemons/ipa-slapi-plugins/Makefile.am              |   1 +
 .../ipa-otp-lasttoken/Makefile.am                  |  32 ++
 .../ipa-otp-lasttoken/ipa-otp-lasttoken.sym        |   1 +
 .../ipa-otp-lasttoken/ipa_otp_lasttoken.c          | 300 ++++++++++++++
 .../ipa-otp-lasttoken/otp-lasttoken-conf.ldif      |  15 +
 .../ipa-slapi-plugins/ipa-otp-lasttoken/t_time.c   | 313 ++++++++++++++
 daemons/ipa-slapi-plugins/ipa-otp-lasttoken/time.c | 450 +++++++++++++++++++++
 freeipa.spec.in                                    |   2 +
 ipaserver/install/dsinstance.py                    |   4 +
 10 files changed, 1119 insertions(+)
 create mode 100644 daemons/ipa-slapi-plugins/ipa-otp-lasttoken/Makefile.am
 create mode 100644 daemons/ipa-slapi-plugins/ipa-otp-lasttoken/ipa-otp-lasttoken.sym
 create mode 100644 daemons/ipa-slapi-plugins/ipa-otp-lasttoken/ipa_otp_lasttoken.c
 create mode 100644 daemons/ipa-slapi-plugins/ipa-otp-lasttoken/otp-lasttoken-conf.ldif
 create mode 100644 daemons/ipa-slapi-plugins/ipa-otp-lasttoken/t_time.c
 create mode 100644 daemons/ipa-slapi-plugins/ipa-otp-lasttoken/time.c

diff --git a/daemons/configure.ac b/daemons/configure.ac
index 7086d8e1054ad9941232c75fc0a82e050a682247..f2dfc201cb861a494a478c1e2fe483f8cc49a1cd 100644
--- a/daemons/configure.ac
+++ b/daemons/configure.ac
@@ -313,6 +313,7 @@ AC_CONFIG_FILES([
     ipa-slapi-plugins/ipa-dns/Makefile
     ipa-slapi-plugins/ipa-enrollment/Makefile
     ipa-slapi-plugins/ipa-lockout/Makefile
+    ipa-slapi-plugins/ipa-otp-lasttoken/Makefile
     ipa-slapi-plugins/ipa-pwd-extop/Makefile
     ipa-slapi-plugins/ipa-extdom-extop/Makefile
     ipa-slapi-plugins/ipa-winsync/Makefile
diff --git a/daemons/ipa-slapi-plugins/Makefile.am b/daemons/ipa-slapi-plugins/Makefile.am
index 08c7558c812effc00ae940661e448779077fb409..36fe1372f5f2adbb6cc5b403d1fa17f0855e1a1a 100644
--- a/daemons/ipa-slapi-plugins/Makefile.am
+++ b/daemons/ipa-slapi-plugins/Makefile.am
@@ -6,6 +6,7 @@ SUBDIRS =			\
 	ipa-enrollment		\
 	ipa-lockout		\
 	ipa-modrdn		\
+	ipa-otp-lasttoken	\
 	ipa-pwd-extop		\
 	ipa-extdom-extop	\
 	ipa-uuid		\
diff --git a/daemons/ipa-slapi-plugins/ipa-otp-lasttoken/Makefile.am b/daemons/ipa-slapi-plugins/ipa-otp-lasttoken/Makefile.am
new file mode 100644
index 0000000000000000000000000000000000000000..83c1e55d279f8932eabd14ca5975ff6877c0f3a2
--- /dev/null
+++ b/daemons/ipa-slapi-plugins/ipa-otp-lasttoken/Makefile.am
@@ -0,0 +1,32 @@
+MAINTAINERCLEANFILES = *~ Makefile.in
+PLUGIN_COMMON_DIR = ../common
+AM_CPPFLAGS =							\
+	-I.							\
+	-I$(srcdir)						\
+	-I$(PLUGIN_COMMON_DIR)					\
+	-I/usr/include/dirsrv					\
+	-DPREFIX=\""$(prefix)"\" 				\
+	-DBINDIR=\""$(bindir)"\"				\
+	-DLIBDIR=\""$(libdir)"\" 				\
+	-DLIBEXECDIR=\""$(libexecdir)"\"			\
+	-DDATADIR=\""$(datadir)"\"				\
+	$(AM_CFLAGS)						\
+	$(LDAP_CFLAGS)						\
+	$(WARN_CFLAGS)
+
+# Convenience Library and Tests
+noinst_LTLIBRARIES = libtime.la
+libtime_la_SOURCES = time.c
+check_PROGRAMS = t_time
+t_time_LDADD = libtime.la
+TESTS = $(check_PROGRAMS)
+
+plugindir = $(libdir)/dirsrv/plugins
+plugin_LTLIBRARIES = libipa_otp_lasttoken.la
+libipa_otp_lasttoken_la_SOURCES = ipa_otp_lasttoken.c
+libipa_otp_lasttoken_la_LDFLAGS = -avoid-version -export-symbols ipa-otp-lasttoken.sym
+libipa_otp_lasttoken_la_LIBADD = $(LDAP_LIBS) $(UUID_LIBS) libtime.la
+
+appdir = $(IPA_DATA_DIR)
+app_DATA = otp-lasttoken-conf.ldif
+EXTRA_DIST = $(app_DATA)
diff --git a/daemons/ipa-slapi-plugins/ipa-otp-lasttoken/ipa-otp-lasttoken.sym b/daemons/ipa-slapi-plugins/ipa-otp-lasttoken/ipa-otp-lasttoken.sym
new file mode 100644
index 0000000000000000000000000000000000000000..e32dc32f5693547bf604480490f42511368fdb81
--- /dev/null
+++ b/daemons/ipa-slapi-plugins/ipa-otp-lasttoken/ipa-otp-lasttoken.sym
@@ -0,0 +1 @@
+ipa_otp_lasttoken_init
diff --git a/daemons/ipa-slapi-plugins/ipa-otp-lasttoken/ipa_otp_lasttoken.c b/daemons/ipa-slapi-plugins/ipa-otp-lasttoken/ipa_otp_lasttoken.c
new file mode 100644
index 0000000000000000000000000000000000000000..7b784a3e11025bf94738b82034787d08308d27d3
--- /dev/null
+++ b/daemons/ipa-slapi-plugins/ipa-otp-lasttoken/ipa_otp_lasttoken.c
@@ -0,0 +1,300 @@
+/** BEGIN COPYRIGHT BLOCK
+ * This Program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software
+ * Foundation; version 2 of the License.
+ *
+ * This Program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this Program; if not, write to the Free Software Foundation, Inc., 59 Temple
+ * Place, Suite 330, Boston, MA 02111-1307 USA.
+ *
+ * In addition, as a special exception, Red Hat, Inc. gives You the additional
+ * right to link the code of this Program with code not covered under the GNU
+ * General Public License ("Non-GPL Code") and to distribute linked combinations
+ * including the two, subject to the limitations in this paragraph. Non-GPL Code
+ * permitted under this exception must only link to the code of this Program
+ * through those well defined interfaces identified in the file named EXCEPTION
+ * found in the source code files (the "Approved Interfaces"). The files of
+ * Non-GPL Code may instantiate templates or use macros or inline functions from
+ * the Approved Interfaces without causing the resulting work to be covered by
+ * the GNU General Public License. Only Red Hat, Inc. may make changes or
+ * additions to the list of Approved Interfaces. You must obey the GNU General
+ * Public License in all respects for all of the Program code and other code used
+ * in conjunction with the Program except the Non-GPL Code covered by this
+ * exception. If you modify this file, you may extend this exception to your
+ * version of the file, but you are not obligated to do so. If you do not wish to
+ * provide this exception without modification, you must delete this exception
+ * statement from your version and license this file solely under the GPL without
+ * exception.
+ *
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ * All rights reserved.
+ * END COPYRIGHT BLOCK **/
+
+#ifdef HAVE_CONFIG_H
+#  include <config.h>
+#endif
+
+#include "slapi-plugin.h"
+#include <time.h>
+
+/* from time.c */
+int parse_iso8601(const char *iso8601, time_t *out);
+
+#define PLUGIN_NAME               "ipa-otp-lasttoken"
+#define LOG(sev, ...) \
+    slapi_log_error(SLAPI_LOG_ ## sev, PLUGIN_NAME, \
+                    "%s: %s\n", __func__, __VA_ARGS__), -1
+
+static void *plugin_id;
+static const Slapi_PluginDesc preop_desc = {
+    PLUGIN_NAME,
+    "FreeIPA",
+    "FreeIPA/1.0",
+    "Protect the user's last active token"
+};
+
+/* Returns whether or not the token is active. */
+static PRBool
+token_is_active(Slapi_Entry *token)
+{
+    Slapi_Attr* attr = NULL;
+    time_t timespec;
+    time_t current;
+    int err;
+
+    if (slapi_entry_attr_find(token, "ipatokenDisabled", &attr) == 0) {
+        if (attr != NULL)
+            if (slapi_entry_attr_get_bool(token, "ipatokenDisabled"))
+                return PR_FALSE;
+    }
+
+    current = time(NULL);
+    if (current == -1)
+        return PR_TRUE; /* FIXME: Is this correct? */
+
+    if (slapi_entry_attr_find(token, "ipatokenNotBefore", &attr) == 0) {
+        if (attr != NULL) {
+            char *tmp = slapi_entry_attr_get_charptr(token, "ipatokenNotBefore");
+            if (tmp != NULL) {
+                err = parse_iso8601(tmp, &timespec);
+                slapi_ch_free_string(&tmp);
+                if (err == 0 && timespec > current)
+                    return PR_FALSE;
+            }
+        }
+    }
+
+    if (slapi_entry_attr_find(token, "ipatokenNotAfter", &attr) == 0) {
+        if (attr != NULL) {
+            char *tmp = slapi_entry_attr_get_charptr(token, "ipatokenNotAfter");
+            if (tmp != NULL) {
+                err = parse_iso8601(tmp, &timespec);
+                slapi_ch_free_string(&tmp);
+                if (err == 0 && timespec < current)
+                    return PR_FALSE;
+            }
+        }
+    }
+
+    return PR_TRUE;
+}
+
+/*
+ * If the user performing this operation has only one active token, it is
+ * returned. If the user has zero or 2+ active tokens, NULL is returned.
+ */
+static Slapi_DN *
+get_only_enabled_token(Slapi_PBlock *pb, const char *base_dn)
+{
+    Slapi_PBlock *search_pb = NULL;
+    Slapi_Entry **entries = NULL;
+    Slapi_DN *token_sdn = NULL;
+    char *filter = NULL;
+    char *dn = NULL;
+    int result = 0;
+
+    /* Get the current user's DN. */
+    slapi_pblock_get(pb, SLAPI_CONN_DN, &dn);
+    if (dn == NULL)
+        return NULL;
+
+    /* Create the filter. */
+    filter ="(&(objectclass=ipaToken)(ipatokenOwner=%s%s))";
+    filter = slapi_filter_sprintf(filter, ESC_AND_NORM_NEXT_VAL, dn);
+    slapi_ch_free_string(&dn);
+
+    /* Run the search. */
+    search_pb = slapi_pblock_new();
+    slapi_search_internal_set_pb(search_pb, base_dn, LDAP_SCOPE_SUBTREE,
+                                 filter, NULL, 0, NULL, NULL, plugin_id, 0);
+    slapi_search_internal_pb(search_pb);
+    slapi_ch_free_string(&filter);
+
+    /* Get results. */
+    slapi_pblock_get(search_pb, SLAPI_PLUGIN_INTOP_RESULT, &result);
+    if (result == LDAP_SUCCESS)
+        slapi_pblock_get(search_pb, SLAPI_PLUGIN_INTOP_SEARCH_ENTRIES, &entries);
+
+    /* Find active tokens. */
+    for (int i = 0; entries[i] != NULL; i++) {
+        if (!token_is_active(entries[i]))
+            continue;
+
+        if (token_sdn != NULL) {
+            token_sdn = NULL;
+            break;
+        }
+
+        token_sdn = slapi_entry_get_sdn(entries[i]);
+    }
+
+    /* Cleanup. */
+    if (token_sdn != NULL)
+        token_sdn = slapi_sdn_dup(token_sdn);
+    slapi_free_search_results_internal(search_pb);
+    slapi_pblock_destroy(search_pb);
+    return token_sdn;
+}
+
+/*
+ * Take the token dn and determine its base suffix
+ */
+static const char *
+get_basedn(Slapi_DN *sdn)
+{
+    Slapi_DN *suffix = NULL;
+    void *node = NULL;
+
+    for (suffix = slapi_get_first_suffix(&node, 0);
+         suffix != NULL;
+         suffix = slapi_get_next_suffix(&node, 0)) {
+        if (slapi_sdn_issuffix(sdn, suffix))
+            return (char *) slapi_sdn_get_dn(suffix);
+    }
+
+    return NULL;
+}
+
+static PRBool
+target_is_only_enabled_token(Slapi_PBlock *pb)
+{
+    Slapi_DN *target_sdn = NULL;
+    Slapi_DN *token_sdn = NULL;
+    const char *base_dn = NULL;
+    PRBool match;
+
+    /* Ignore internal operations. */
+    if (slapi_op_internal(pb))
+        return PR_FALSE;
+
+    /* Get the target SDN. */
+    slapi_pblock_get(pb, SLAPI_TARGET_SDN, &target_sdn);
+    if (target_sdn == NULL)
+        return PR_FALSE;
+
+    /* Get the root DN of the target operation. */
+    base_dn = get_basedn(target_sdn);
+    if (base_dn == NULL)
+        return PR_FALSE;
+
+    /* Get the only enabled token. */
+    token_sdn = get_only_enabled_token(pb, base_dn);
+    if (token_sdn == NULL)
+        return PR_FALSE;
+
+    /* Does the target SDN match the only enabled token SDN? */
+    match = slapi_sdn_compare(token_sdn, target_sdn) == 0;
+    slapi_sdn_free(&token_sdn);
+    return match;
+}
+
+static inline int
+send_error(Slapi_PBlock *pb, int rc, char *errstr)
+{
+    slapi_send_ldap_result(pb, rc, NULL, errstr, 0, NULL);
+    slapi_pblock_set(pb, SLAPI_RESULT_CODE, &rc);
+    return rc;
+}
+
+static int
+preop_del(Slapi_PBlock *pb)
+{
+    if (!target_is_only_enabled_token(pb))
+        return 0;
+
+    return send_error(pb, LDAP_UNWILLING_TO_PERFORM,
+                      "Can't delete last active token");
+}
+
+static int
+preop_mod(Slapi_PBlock *pb)
+{
+    LDAPMod **mods = NULL;
+
+    if (!target_is_only_enabled_token(pb))
+        return 0;
+
+    /* Do not permit deactivation of the last active token. */
+    slapi_pblock_get(pb, SLAPI_MODIFY_MODS, &mods);
+    for (int i = 0; mods != NULL && mods[i] != NULL; i++) {
+        if (strcasecmp(mods[i]->mod_type, "ipatokenDisabled") == 0) {
+            return send_error(pb, LDAP_UNWILLING_TO_PERFORM,
+                              "Can't disable last active token");
+        }
+
+        if (strcasecmp(mods[i]->mod_type, "ipatokenOwner") == 0) {
+            return send_error(pb, LDAP_UNWILLING_TO_PERFORM,
+                              "Can't change last active token's owner");
+        }
+
+        if (strcasecmp(mods[i]->mod_type, "ipatokenNotBefore") == 0) {
+            return send_error(pb, LDAP_UNWILLING_TO_PERFORM,
+                              "Can't change last active token's start time");
+        }
+
+        if (strcasecmp(mods[i]->mod_type, "ipatokenNotAfter") == 0) {
+            return send_error(pb, LDAP_UNWILLING_TO_PERFORM,
+                              "Can't change last active token's end time");
+        }
+    }
+
+    return 0;
+}
+
+static int
+preop_init(Slapi_PBlock *pb)
+{
+    if (slapi_pblock_set(pb, SLAPI_PLUGIN_VERSION, SLAPI_PLUGIN_VERSION_01))
+        goto error;
+
+    if (slapi_pblock_set(pb, SLAPI_PLUGIN_DESCRIPTION, (void *) &preop_desc))
+        goto error;
+
+    if (slapi_pblock_set(pb, SLAPI_PLUGIN_BE_TXN_PRE_DELETE_FN, preop_del))
+        goto error;
+
+    if (slapi_pblock_set(pb, SLAPI_PLUGIN_BE_TXN_PRE_MODIFY_FN, preop_mod))
+        goto error;
+
+    return 0;
+
+error:
+    return LOG(FATAL, "failed to register be_txn_pre_op plugin");
+}
+
+int
+ipa_otp_lasttoken_init(Slapi_PBlock *pb)
+{
+    slapi_pblock_get(pb, SLAPI_PLUGIN_IDENTITY, &plugin_id);
+
+    if (slapi_register_plugin("betxnpreoperation", 1, __func__, preop_init,
+                              PLUGIN_NAME, NULL, plugin_id))
+        return LOG(FATAL, "failed to register plugin");
+
+    return 0;
+}
diff --git a/daemons/ipa-slapi-plugins/ipa-otp-lasttoken/otp-lasttoken-conf.ldif b/daemons/ipa-slapi-plugins/ipa-otp-lasttoken/otp-lasttoken-conf.ldif
new file mode 100644
index 0000000000000000000000000000000000000000..767883848b7c85d6de5c677d2f8243ab9da76acc
--- /dev/null
+++ b/daemons/ipa-slapi-plugins/ipa-otp-lasttoken/otp-lasttoken-conf.ldif
@@ -0,0 +1,15 @@
+dn: cn=IPA OTP Last Token,cn=plugins,cn=config
+changetype: add
+objectclass: top
+objectclass: nsSlapdPlugin
+objectclass: extensibleObject
+cn: IPA OTP Last Token
+nsslapd-pluginpath: libipa_otp_lasttoken
+nsslapd-plugininitfunc: ipa_otp_lasttoken_init
+nsslapd-plugintype: preoperation
+nsslapd-pluginenabled: on
+nsslapd-pluginid: ipa-otp-lasttoken
+nsslapd-pluginversion: 1.0
+nsslapd-pluginvendor: Red Hat, Inc.
+nsslapd-plugindescription: IPA OTP Last Token plugin
+nsslapd-plugin-depends-on-type: database
diff --git a/daemons/ipa-slapi-plugins/ipa-otp-lasttoken/t_time.c b/daemons/ipa-slapi-plugins/ipa-otp-lasttoken/t_time.c
new file mode 100644
index 0000000000000000000000000000000000000000..bede0b86b7d87c93ca93a93e0ec34767d613ac07
--- /dev/null
+++ b/daemons/ipa-slapi-plugins/ipa-otp-lasttoken/t_time.c
@@ -0,0 +1,313 @@
+/** BEGIN COPYRIGHT BLOCK
+ * This Program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software
+ * Foundation; version 2 of the License.
+ *
+ * This Program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this Program; if not, write to the Free Software Foundation, Inc., 59 Temple
+ * Place, Suite 330, Boston, MA 02111-1307 USA.
+ *
+ * In addition, as a special exception, Red Hat, Inc. gives You the additional
+ * right to link the code of this Program with code not covered under the GNU
+ * General Public License ("Non-GPL Code") and to distribute linked combinations
+ * including the two, subject to the limitations in this paragraph. Non-GPL Code
+ * permitted under this exception must only link to the code of this Program
+ * through those well defined interfaces identified in the file named EXCEPTION
+ * found in the source code files (the "Approved Interfaces"). The files of
+ * Non-GPL Code may instantiate templates or use macros or inline functions from
+ * the Approved Interfaces without causing the resulting work to be covered by
+ * the GNU General Public License. Only Red Hat, Inc. may make changes or
+ * additions to the list of Approved Interfaces. You must obey the GNU General
+ * Public License in all respects for all of the Program code and other code used
+ * in conjunction with the Program except the Non-GPL Code covered by this
+ * exception. If you modify this file, you may extend this exception to your
+ * version of the file, but you are not obligated to do so. If you do not wish to
+ * provide this exception without modification, you must delete this exception
+ * statement from your version and license this file solely under the GPL without
+ * exception.
+ *
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ * All rights reserved.
+ * END COPYRIGHT BLOCK **/
+
+#include <time.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+int parse_iso8601(const char *iso8601, time_t *out);
+
+/* These two MUST be the same. */
+#define TZ "EST+5"
+#define OFFSET 5
+
+#define MINUTES(m) ((m) * 60)
+#define HOURS(h) MINUTES(h * 60)
+#define DAYS(d) HOURS(d * 24)
+#define WEEKS(w) DAYS(w * 7)
+#define VAL(val, add) ((val) == 0 ? 0 : ((val) + (add)))
+#define UTC_OFFSET(val, hours) VAL(val, 0 - HOURS(OFFSET) - HOURS(hours))
+
+#define GMT(str, val) \
+    {str,     val}, \
+    {str "Z", UTC_OFFSET(val, 0)}
+
+#define ZONED(str, val) \
+    GMT(str,       val), \
+    {str "+06",    UTC_OFFSET(val, 6)   }, \
+    {str "+06:30", UTC_OFFSET(val, 6.5) }, \
+    {str "-06",    UTC_OFFSET(val, -6)  }, \
+    {str "-06:30", UTC_OFFSET(val, -6.5)}
+
+#define FRACTION(str, seconds, val) \
+    ZONED(str, val), \
+    ZONED(str ".5", VAL(val, seconds))
+
+/* NOTE: The first line of the DATE() macro generates a non-standard timezone.
+ *       We parse it successfully in the interest of receiver liberality. */
+#define DATE(str, val) \
+    ZONED(str, val), \
+    FRACTION(str "T09",    1800, VAL(val, HOURS(9))), \
+    FRACTION(str "T22",    1800, VAL(val, HOURS(22))), \
+    FRACTION(str "T09:28",   30, VAL(val, HOURS(9)  + MINUTES(28))), \
+    FRACTION(str "T22:59",   30, VAL(val, HOURS(22) + MINUTES(59))), \
+    FRACTION(str "T09:28:12", 0, VAL(val, HOURS(9)  + MINUTES(28) + 12)), \
+    FRACTION(str "T22:59:32", 0, VAL(val, HOURS(22) + MINUTES(59) + 32))
+
+#define WEEKDATE(str, val) \
+    DATE(str "-W01", val), \
+    DATE(str "-W01-1", val), \
+    DATE(str "-W01-4", VAL(val, DAYS(3))), \
+    DATE(str "-W32",   VAL(val, WEEKS(31))), \
+    DATE(str "-W32-1", VAL(val, WEEKS(31))), \
+    DATE(str "-W32-4", VAL(val, WEEKS(31) + DAYS(3)))
+
+struct test_data {
+    const char * string;
+    const time_t value;
+    const bool notrunct;
+};
+
+static const struct test_data TEST_DATA[] = {
+    /* Test year only. */
+    GMT("1965", -157748400L), /* Zone non-standard. */
+    GMT("1989",  599634000L), /* Zone non-standard. */
+
+    /* Test year + month. */
+    GMT("1965-02", -155070000L), /* Zone non-standard. */
+    GMT("1989-02",  602312400L), /* Zone non-standard. */
+
+    /* Test full date (before epoch). */
+    DATE("1965-02-12", -154119600L), /* Standard format. */
+    DATE("1965-234",   -137617200L), /* Ordinal format. */
+
+    /* Test full date (after epoch). */
+    DATE("1989-02-12", 603262800L), /* Standard format. */
+    DATE("1989-234",   619765200L), /* Ordinal format. */
+
+    /* Test weekday format (before epoch). */
+    WEEKDATE("1965", -157489200L), /* Current year. */
+    WEEKDATE("1963", -220993200L), /* Previous year. */
+
+    /* Test weekday format (after epoch). */
+    WEEKDATE("1989", 599720400L), /* Current year. */
+    WEEKDATE("1985", 473317200L), /* Previous year. */
+
+    /* Test midnight on the same day. */
+    {"2000-01-01T00:00:00Z", 946684800L}, /* These should both... */
+    {"1999-12-31T24:00:00Z", 946684800L}, /* ... be the same.     */
+
+    /* Test truncated dates where T is present. */
+    {"2000T00:00:00Z",     946684800L, true},
+    {"2000-01T00:00:00Z",  946684800L, true},
+    {"1965T00:00:00Z",    -157766400L, true},
+    {"1965-01T00:00:00Z", -157766400L, true},
+
+    /* Test leap second. */
+    {"2000-06-30T23:59:59Z", 962409599L},
+    {"2000-06-30T23:59:60Z", 962409600L},
+
+    /* Test invalid dates/times. */
+    GMT("2000-00", 0),
+    GMT("2000-13", 0),
+    DATE("2000-01-00", 0),
+    DATE("2000-01-32", 0),
+    DATE("2012-04-31", 0),
+    FRACTION("2000-01-01T25", 0, 0),
+    ZONED("2000-01-01T24.05", 0),
+    FRACTION("2000-01-01T24:01", 0, 0),
+    FRACTION("2000-01-01T24:00:01", 0, 0),
+    ZONED("2000-01-01T24:00:00.1", 0),
+    FRACTION("2000-01-01T00:60", 0, 0),
+    FRACTION("2000-01-01T00:00:61", 0, 0),
+    {"2000-01-01T00:00:00+25"},
+    {"2000-01-01T00:00:00-25"},
+    {"2000-01-01T00:00:00+00:60"},
+    {"2000-01-01T00:00:00-00:60"},
+
+    /* Test extended years (not supported). */
+    DATE("-5432-01-01", 0),
+    WEEKDATE("-5432", 0),
+    DATE("-54321-01-01", 0),
+    WEEKDATE("-54321", 0),
+    DATE("+5432-01-01", 0),
+    WEEKDATE("+5432", 0),
+    DATE("+54321-01-01", 0),
+    WEEKDATE("+54321", 0),
+
+    /* Test leap year. */
+    DATE("1989-02-29", 0),
+    DATE("1988-02-29", 573109200L),
+
+    {},
+};
+
+bool
+is_ext_year(const char *iso8601)
+{
+    if (iso8601[0] == '-')
+        return true;
+
+    if (iso8601[0] == '+')
+        return true;
+
+    return false;
+}
+
+bool
+test_one(const char *iso8601, time_t expected)
+{
+    time_t tmp = 0;
+    int err;
+
+    err = parse_iso8601(iso8601, &tmp);
+    if (err != 0) {
+        if (expected == 0)
+            return true; /* Expected failure... */
+        fprintf(stderr, "Error (%d): %s\n", err, strerror(err));
+        return false;
+    }
+
+    /* Expected failure didn't happen... */
+    if (expected == 0) {
+        fprintf(stderr, "Expected '%s' to fail, but it succeeded!\n",
+                iso8601);
+        return false;
+    }
+
+    /* Value is incorrect. */
+    if (tmp != expected) {
+        fprintf(stderr, "Expected '%s' => %ld; got %ld!\n",
+                iso8601, expected, tmp);
+        return false;
+    }
+
+    return true;
+}
+
+bool
+test(const struct test_data *data)
+{
+    char basict[1024];
+    char basic[1024];
+    int i, j;
+    const char *all[] = {
+        data->string,
+        basict,
+        data->notrunct ? NULL : basic,
+        NULL
+    };
+
+    /* Basic mode. */
+    strncpy(basict, data->string, sizeof(basict));
+    j = i = is_ext_year(basict) ? 1 : 0;
+    do {
+        basict[j] = basict[i];
+        switch (basict[j]) {
+        case ':':
+            continue;
+        case '-':
+            /*
+             * Handle the weekday case which requires an odd number of digits
+             * after the dash. In the most common case, this number will be 0
+             * or 1. But if 'T' is not present, it can be all the way to the
+             * end of the time section. Take for example, the case of midnight
+             * on January first in local time: 1989W011000000
+             */
+            if (j == 7 && basict[4] == 'W') {
+                int digits;
+                for (digits = 0; basict[i + digits + 1] != '\0'; digits++) {
+                    if (!isdigit(basict[i + digits + 1]))
+                        break;
+                }
+                if (digits % 2 == 1)
+                    continue;
+            }
+
+            /* - is removed from the date but retained in the TZ. */
+            if (j < 7)
+                continue;
+        }
+
+        j++;
+    } while (basict[i++] != '\0');
+
+    /* Basic mode w/o T. */
+    strncpy(basic, basict, sizeof(basic));
+    j = i = is_ext_year(basic) ? 1 : 0;
+    do {
+        basic[j] = basic[i];
+        if (basic[i] != 'T')
+            j++;
+    } while (basict[i++] != '\0');
+
+    /* Do all tests. */
+    for (i = 0; all[i] != NULL; i++) {
+        if (i > 0 && strcmp(all[i], all[i -1 ]) == 0)
+            continue; /* Skip duplicates. */
+
+        fprintf(stderr, "Testing: %s\n", all[i]);
+        if (!test_one(all[i], data->value))
+            return false;
+    }
+
+    return true;
+}
+
+int
+main(int argc, const char **argv)
+{
+    setenv("TZ", TZ, 1);
+
+    for (int i = 0; TEST_DATA[i].string != NULL; i++) {
+        if (!test(&TEST_DATA[i]))
+            return 1;
+    }
+
+    /* Check if we should perform the fuzz test. */
+    for (int i = 1; i < argc; i++) {
+        if (strcmp(argv[i], "-f") == 0) {
+            const char *chars = "0123456789TZW:+-";
+            char buf[64];
+            time_t out;
+
+            while (true) {
+                memset(buf, 0, sizeof(buf));
+                for (int j = rand() % (sizeof(buf) - 1); j >= 0; j--)
+                    buf[j] = chars[rand() % strlen(chars)];
+                fprintf(stderr, "Testing: %s\n", buf);
+                parse_iso8601(buf, &out); /* Ignore return value. */
+            }
+
+            break;
+        }
+    }
+
+    return 0;
+}
diff --git a/daemons/ipa-slapi-plugins/ipa-otp-lasttoken/time.c b/daemons/ipa-slapi-plugins/ipa-otp-lasttoken/time.c
new file mode 100644
index 0000000000000000000000000000000000000000..d88dcfa4793edc9b14d510d9187b76f0c2e06f66
--- /dev/null
+++ b/daemons/ipa-slapi-plugins/ipa-otp-lasttoken/time.c
@@ -0,0 +1,450 @@
+/** BEGIN COPYRIGHT BLOCK
+ * This Program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software
+ * Foundation; version 2 of the License.
+ *
+ * This Program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this Program; if not, write to the Free Software Foundation, Inc., 59 Temple
+ * Place, Suite 330, Boston, MA 02111-1307 USA.
+ *
+ * In addition, as a special exception, Red Hat, Inc. gives You the additional
+ * right to link the code of this Program with code not covered under the GNU
+ * General Public License ("Non-GPL Code") and to distribute linked combinations
+ * including the two, subject to the limitations in this paragraph. Non-GPL Code
+ * permitted under this exception must only link to the code of this Program
+ * through those well defined interfaces identified in the file named EXCEPTION
+ * found in the source code files (the "Approved Interfaces"). The files of
+ * Non-GPL Code may instantiate templates or use macros or inline functions from
+ * the Approved Interfaces without causing the resulting work to be covered by
+ * the GNU General Public License. Only Red Hat, Inc. may make changes or
+ * additions to the list of Approved Interfaces. You must obey the GNU General
+ * Public License in all respects for all of the Program code and other code used
+ * in conjunction with the Program except the Non-GPL Code covered by this
+ * exception. If you modify this file, you may extend this exception to your
+ * version of the file, but you are not obligated to do so. If you do not wish to
+ * provide this exception without modification, you must delete this exception
+ * statement from your version and license this file solely under the GPL without
+ * exception.
+ *
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ * All rights reserved.
+ * END COPYRIGHT BLOCK **/
+
+#include <time.h>
+#include <stdbool.h>
+#include <ctype.h>
+#include <errno.h>
+#include <stdint.h>
+#include <string.h>
+#include <stdlib.h>
+#include <assert.h>
+
+typedef time_t (*maketime_t)(struct tm *);
+
+/*
+ * NOTE: According to ISO 8601:2004, decimal length is potentially unlimited.
+ *       However, the assumed decimal max below exceeds IEEE 754 precision and
+ *       should thus be acceptable for our uses.
+ */
+#define ISO8601_DEC 35
+#define ISO8601_MAX (28 + ISO8601_DEC)
+#define ISO8601_MIN 4
+
+static inline bool
+convert(char *str, int offset, int digits,
+        int min, int max, const char *end,
+        int *out)
+{
+    int tmp = 0;
+
+    for (int i = 0; i < digits; i++) {
+        if (!isdigit(str[offset + i]))
+            return false;
+        tmp *= 10;
+        tmp += str[offset + i] - '0';
+    }
+
+    if (tmp < min || tmp > max)
+        return false;
+
+    /* Consume the characters converted. */
+    if (end != NULL && strncmp(&str[offset + digits], end, strlen(end)) == 0)
+        offset += strlen(end);
+    memmove(str, &str[offset + digits], strlen(&str[offset + digits]) + 1);
+    *out = tmp;
+    return true;
+}
+
+static inline bool
+is_leapyear(int year)
+{
+    return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
+}
+
+static inline uint16_t
+year_length(const struct tm *time)
+{
+    return is_leapyear(time->tm_year + 1900) ? 366 : 365;
+}
+
+static inline uint8_t
+month_length(const struct tm *time)
+{
+    static const uint8_t ndays[12] = {
+        31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
+    };
+
+    if (time->tm_mon > 11 || time->tm_mon < 0)
+        return 0;
+
+    if (time->tm_mon != 1)
+        return ndays[time->tm_mon];
+
+    return is_leapyear(time->tm_year + 1900) ? 29 : 28;
+}
+
+static inline maketime_t
+parse_end_timezone(char *buf, int *offset)
+{
+    int multiplier = 1;
+    int hoff = 0;
+    int moff = 0;
+    size_t len;
+
+    /* We have a date. */
+    len = strlen(buf);
+    if (len < 1)
+        goto local;
+
+    /* Look for Z. */
+    if (buf[len - 1] == 'Z') {
+        buf[len - 1] = '\0';
+        *offset = 0;
+        return timegm;
+    }
+
+    /* Parse the minutes if specified. */
+    if (len >= 5) {
+        switch (buf[len - 5]) {
+        case '-':
+        case '+':
+            if (!convert(&buf[len - 2], 0, 2, 0, 59, NULL, &moff))
+                return NULL;
+            len -= 2;
+        }
+    }
+
+    /* Parse the hours. */
+    if (len >= 3) {
+        switch (buf[len - 3]) {
+        case '-':
+            multiplier = -1;
+        case '+':
+            if (!convert(&buf[len - 3], 1, 2, 0, 24, NULL, &hoff))
+                return NULL;
+
+            *offset = ((hoff * 60 * 60) + (moff * 60)) * multiplier;
+            return timegm;
+        }
+    }
+
+local:
+    *offset = 0;
+    return mktime;
+}
+
+static inline bool
+parse_end_decimal(char *buf, double *out)
+{
+    int len = strlen(buf);
+    for (int i = len - 1; i >= 0 && i >= len - ISO8601_DEC; i--) {
+        if (isdigit(buf[i]))
+            continue;
+
+        if (buf[i] != '.')
+            break;
+
+        *out = strtod(&buf[i], NULL);
+        buf[i] = '\0';
+        return true;
+    }
+
+    *out = 0;
+    return true;
+}
+
+static inline bool
+ordinal_to_date(int ordinal, struct tm *time)
+{
+    if (ordinal < 1 || ordinal > year_length(time))
+        return false;
+
+    while (ordinal > month_length(time)) {
+        ordinal -= month_length(time);
+        time->tm_mon++;
+    }
+
+    time->tm_mday = ordinal;
+    return true;
+}
+
+static inline bool
+parse_ordinal(char *buf, struct tm *time)
+{
+    int ordinal;
+
+    if (!convert(buf, 0, 3, 1, year_length(time), "T", &ordinal))
+        return false;
+
+    return ordinal_to_date(ordinal, time);
+}
+
+static inline bool
+parse_weekdate(char *buf, struct tm *time)
+{
+    struct tm tmp = *time;
+    int wday = 1;
+    int ordinal;
+    int week;
+
+    /* Get the weekday of Jan 1. */
+    assert(mktime(&tmp) != -1); /* This should never fail. */
+
+    /* Convert the week. */
+    if (!convert(buf, 1, 2, 1, 53, "-", &week))
+        return EINVAL;
+
+    switch (buf[0]) {
+    case 'T':
+        memmove(&buf[0], &buf[1], strlen(&buf[1]) + 1); /* Consume the T. */
+    case '\0':
+        break;
+    default:
+        /* Convert the week day, if specified. */
+        if ((strlen(buf) % 2 == 1) == (strchr(buf, 'T') == NULL)) {
+            if (!convert(buf, 0, 1, 1, 7, "T", &wday))
+                return EINVAL;
+        }
+        break;
+    }
+
+    /* Convert the weekdate format to an ordinal. */
+    ordinal = --week * 7 + wday + 1 - tmp.tm_wday;
+    if (tmp.tm_wday > 4)
+        ordinal += 7;
+
+    /* Handle when the weekdate refers to the end of the previous year. */
+    if (ordinal < 1) {
+        time->tm_year--;
+        ordinal = year_length(time) - ordinal;
+    }
+
+    return ordinal_to_date(ordinal, time);
+}
+
+static inline bool
+parse_date(char *buf, struct tm *time)
+{
+    /* Convert the year. */
+    if (!convert(buf, 0, 4, 0, 9999, "-", &time->tm_year))
+        return false;
+    time->tm_year -= 1900;
+
+    switch (buf[0]) {
+    case 'T':
+        memmove(&buf[0], &buf[1], strlen(&buf[1]) + 1); /* Consume the T. */
+    case '\0':
+        return true;
+    case 'W':
+        /* Weekdate Format */
+        return parse_weekdate(buf, time);
+    }
+
+    /* Ordinal Format */
+    if ((strlen(buf) % 2 == 1) == (strchr(buf, 'T') == NULL))
+        return parse_ordinal(buf, time);
+
+    /* Standard Format */
+    if (!convert(buf, 0, 2, 1, 12, "-", &time->tm_mon))
+        return false;
+
+    time->tm_mon--;
+    switch (buf[0]) {
+    case 'T':
+        memmove(&buf[0], &buf[1], strlen(&buf[1]) + 1); /* Consume the T. */
+    case '\0':
+        return true;
+    }
+
+    return convert(buf, 0, 2, 1, month_length(time), "T", &time->tm_mday);
+}
+
+static inline bool
+parse_time(char *buf, double decimal, struct tm *time)
+{
+    time->tm_hour = 0;
+    time->tm_min = 0;
+    time->tm_sec = 0;
+    if (buf[0] == '\0')
+        return true;
+
+    /* Convert hours, possibly including decimal. */
+    if (!convert(buf, 0, 2, 0, 24, ":", &time->tm_hour))
+        return false;
+    if (time->tm_hour == 24 && decimal != 0)
+        return false;
+    if (buf[0] == '\0') {
+        time->tm_min = 60 * decimal;
+        return true;
+    }
+
+    /* Convert minutes, possibly including decimal. */
+    if (!convert(buf, 0, 2, 0, time->tm_hour == 24 ? 0 : 59, ":", &time->tm_min))
+        return false;
+    if (buf[0] == '\0') {
+        time->tm_sec = 60 * decimal;
+        return true;
+    }
+
+    /* Convert seconds, ignoring decimal (due to precision of time_t). */
+    return convert(buf, 0, 2, 0, time->tm_hour == 24 ? 0 : 60, NULL, &time->tm_sec);
+}
+
+static inline bool
+normalize_iso8601(char *buf)
+{
+    int i = 0, j = 0;
+    int decimals = 0;
+
+    do {
+        buf[j] = buf[i];
+
+        switch (buf[j]) {
+        case '-':
+            /*
+             * Handle the weekday case which requires an odd number of digits
+             * after the dash. In the most common case, this number will be 0
+             * or 1. But if 'T' is not present, it can be all the way to the
+             * end of the time section. Take for example, the case of midnight
+             * on January first in local time: 1989W011000000
+             */
+            if (j == 7 && buf[4] == 'W') {
+                int digits;
+                for (digits = 0; buf[i + digits + 1] != '\0'; digits++) {
+                    if (!isdigit(buf[i + digits + 1]))
+                        break;
+                }
+                if (digits % 2 == 1)
+                    continue;
+            }
+
+            /* - is removed from the date but retained in the TZ. */
+            if (j < 7)
+                continue;
+            break;
+
+        case '+':
+            /* + can only appear in the TZ. */
+            if (j < 7)
+                return false;
+            break;
+
+        case 'W':
+            /* W can only appear immediately after the year. */
+            if (j != 4)
+                return false;
+            break;
+
+        case 'Z':
+            /* Z can only come at the end. */
+            if (buf[i + 1] != '\0')
+                return false;
+            break;
+
+        case ':':
+            continue;
+
+        case '.':
+            /* Only one permitted. */
+            if (decimals++ > 0)
+                return false;
+            break;
+
+        case 'T':
+        case '0':
+        case '1':
+        case '2':
+        case '3':
+        case '4':
+        case '5':
+        case '6':
+        case '7':
+        case '8':
+        case '9':
+        case '\0':
+            break;
+
+        default:
+            return false;
+        }
+
+        j++;
+    } while (buf[i++] != '\0');
+
+    buf[j] = '\0';
+    return true;
+}
+
+int
+parse_iso8601(const char *iso8601, time_t *out)
+{
+    struct tm time = { .tm_mday = 1 };
+    char buf[ISO8601_MAX + 1];
+    maketime_t maketime;
+    double decimal = 0;
+    int offset = 0;
+
+    if (iso8601 == NULL)
+        return EINVAL;
+
+    /* Copy the string into a local buffer. */
+    if (strlen(iso8601) < ISO8601_MIN || strlen(iso8601) > ISO8601_MAX)
+        return E2BIG;
+    strcpy(buf, iso8601);
+
+    /* TODO: support extended years. */
+    switch (buf[0]) {
+    case '+':
+    case '-':
+        return ENOTSUP;
+    }
+
+    /* Normalize the string. */
+    if (!normalize_iso8601(buf))
+        return EINVAL;
+
+    /* Parse the timezone off the end. */
+    maketime = parse_end_timezone(buf, &offset);
+    if (maketime == NULL)
+        return EINVAL;
+
+    /* Parse the decimal off the end. */
+    if (!parse_end_decimal(buf, &decimal))
+        return EINVAL;
+
+    /* Parse the date. */
+    if (!parse_date(buf, &time))
+        return EINVAL;
+
+    /* Parse the time. */
+    if (!parse_time(buf, decimal, &time))
+        return EINVAL;
+
+    *out = maketime(&time) - offset;
+    return 0;
+}
diff --git a/freeipa.spec.in b/freeipa.spec.in
index df68be0a195fc435d7d9ed812220aabc2246103c..85301468625e355b076d9342d43c7932405a3094 100644
--- a/freeipa.spec.in
+++ b/freeipa.spec.in
@@ -393,6 +393,7 @@ rm %{buildroot}/%{plugin_dir}/libipa_sidgen.la
 rm %{buildroot}/%{plugin_dir}/libipa_sidgen_task.la
 rm %{buildroot}/%{plugin_dir}/libipa_extdom_extop.la
 rm %{buildroot}/%{plugin_dir}/libipa_range_check.la
+rm %{buildroot}/%{plugin_dir}/libipa_otp_lasttoken.la
 rm %{buildroot}/%{_libdir}/krb5/plugins/kdb/ipadb.la
 rm %{buildroot}/%{_libdir}/samba/pdb/ipasam.la
 
@@ -731,6 +732,7 @@ fi
 %attr(755,root,root) %{plugin_dir}/libipa_cldap.so
 %attr(755,root,root) %{plugin_dir}/libipa_dns.so
 %attr(755,root,root) %{plugin_dir}/libipa_range_check.so
+%attr(755,root,root) %{plugin_dir}/libipa_otp_lasttoken.so
 %dir %{_localstatedir}/lib/ipa
 %attr(700,root,root) %dir %{_localstatedir}/lib/ipa/backup
 %attr(700,root,root) %dir %{_localstatedir}/lib/ipa/sysrestore
diff --git a/ipaserver/install/dsinstance.py b/ipaserver/install/dsinstance.py
index de804059cd29574bed0afcd8fb0d7fb78d16985b..8fa900f8db803d15b1dbe6ba9bbbb5f56f771401 100644
--- a/ipaserver/install/dsinstance.py
+++ b/ipaserver/install/dsinstance.py
@@ -270,6 +270,7 @@ class DsInstance(service.Service):
         self.step("configuring DNS plugin", self.__config_dns_module)
         self.step("enabling entryUSN plugin", self.__enable_entryusn)
         self.step("configuring lockout plugin", self.__config_lockout_module)
+        self.step("configuring OTP last token plugin", self.__config_otp_lasttoken_module)
         self.step("creating indices", self.__create_indices)
         self.step("enabling referential integrity plugin", self.__add_referint_module)
         if enable_ssl:
@@ -571,6 +572,9 @@ class DsInstance(service.Service):
     def __config_lockout_module(self):
         self._ldap_mod("lockout-conf.ldif")
 
+    def __config_otp_lasttoken_module(self):
+        self._ldap_mod("otp-lasttoken-conf.ldif")
+
     def __repoint_managed_entries(self):
         self._ldap_mod("repoint-managed-entries.ldif", self.sub_dict)
 
-- 
1.8.4.2

_______________________________________________
Freeipa-devel mailing list
Freeipa-devel@redhat.com
https://www.redhat.com/mailman/listinfo/freeipa-devel

Reply via email to