On Fri, 2014-10-17 at 12:05 +0200, Martin Kosek wrote:
> On 10/16/2014 11:53 PM, Nathaniel McCallum wrote:
> > On Thu, 2014-10-16 at 21:02 +0200, Martin Kosek wrote:
> >> On 10/15/2014 09:22 AM, Martin Kosek wrote:
> >>> On 10/14/2014 09:01 PM, Nathaniel McCallum wrote:
> >>>> On Thu, 2014-10-09 at 18:48 +0200, thierry bordaz wrote:
> >>>>> On 10/09/2014 05:51 PM, Nathaniel McCallum wrote:
> >>>>>
> >>>>>> On Thu, 2014-10-09 at 11:44 +0200, thierry bordaz wrote:
> >>>>>>> On 10/09/2014 12:15 AM, Nathaniel McCallum wrote:
> >>>>>>>
> >>>>>>>> On Wed, 2014-10-08 at 17:19 -0400, Simo Sorce wrote:
> >>>>>>>>> On Wed, 08 Oct 2014 15:53:39 -0400
> >>>>>>>>> Nathaniel McCallum <npmccal...@redhat.com> wrote:
> >>>>>>>>>
> >>>>>>>>>> As I understand my code, all servers will have csnD. Some servers 
> >>>>>>>>>> will
> >>>>>>>>>> have valueB and others will have valueD, but valueB == valueD.
> >>>>>>>>>>
> >>>>>>>>>> We *never* discard a CSN. We only discard the counter/watermark 
> >>>>>>>>>> mods
> >>>>>>>>>> in the replication operation.
> >>>>>>>>> What Thierry is saying is that the individual attributes in the 
> >>>>>>>>> entry
> >>>>>>>>> have associate the last CSN that modified them. Because you remove 
> >>>>>>>>> the
> >>>>>>>>> mods when ValueD == ValueB the counter attribute will not have the
> >>>>>>>>> associated CSN changed. But it doesn't really matter because the 
> >>>>>>>>> plugin
> >>>>>>>>> will always keep things consistent.
> >>>>>>>> Attached is a new version. It removes this optimization. If server X 
> >>>>>>>> has
> >>>>>>>> valueB/csnB and receives valueD/csnD and valueB == valueD, the
> >>>>>>>> replication will be applied without any modification. However, if 
> >>>>>>>> valueB
> >>>>>>>>> valueD and csnD > csnB, the counter mods will still be stripped.
> >>>>>>>> It also collapses the error check from betxnpre to bepre now that we
> >>>>>>>> have a fix for https://fedorahosted.org/389/ticket/47919 committed. 
> >>>>>>>> The
> >>>>>>>> betxnpre functions are completely removed. Also, a dependency on 389
> >>>>>>>> 1.3.3.4 (not yet released) is added.
> >>>>>>>>
> >>>>>>>> Nathaniel
> >>>>>>> Hello Nathaniel,
> >>>>>>>
> >>>>>>>          For me the code is fine. Ack.
> >>>>>> New attached patch.
> >>>>>>
> >>>>>>>          I have two minor comments:
> >>>>>>>                * in preop_mod, when a direct update moves the counter
> >>>>>>>                  backward you send UNWILLING to perform with a 
> >>>>>>> message.
> >>>>>>>                  The message is allocated with slapi_ch_smprintf, you
> >>>>>>>                  may free it with slapi_ch_free_string (rather than
> >>>>>>>                  'free').
> >>>>>> Fixed.
> >>>>>>
> >>>>>>>                * About this message, for example when you have these
> >>>>>>>                  MODS (I admit they make no sens):
> >>>>>>>
> >>>>>>>                  changetype: modify
> >>>>>>>                  ipatokenHOTPcounter: MOD_DELETE
> >>>>>>>                  -
> >>>>>>>                  ipatokenHOTPcounter: MOD_INCREMENT
> >>>>>>>
> >>>>>>>                  The returned message will be "Will not decrement
> >>>>>>>                  ipatokenHOTPcounter", because 'simulate' will return
> >>>>>>>                  'COUNTER_UNSET+1'.
> >>>>>>>                  Is it the message you expected ?
> >>>>>> I changed the logic in simulate(). Please review it.
> >>>>>>
> >>>>>> Nathaniel
> >>>>>>
> >>>>> Hello Nathaniel,
> >>>>>
> >>>>>
> >>>>>          The patch is ok for me. Ack.
> >>>>
> >>>> Since the ACK, the upstream 389 fix actually landed in 1.3.3.5. This
> >>>> patch changes nothing except the dependency version. I have tested it
> >>>> against the 1.3.3.5 build.
> >>>>
> >>>> Nathaniel
> >>>
> >>> Great! As soon as the new build land in Fedora 21 (and we add it to our 
> >>> Copr),
> >>> the patch can be pushed.
> >>>
> >>> Martin
> >>
> >> Ok, 1.3.3.5 is in koji and our Copr repo.
> > 
> > +1
> > 
> >> I almost pushed the patch, but I just 
> >> spotted you forgot to solve the upgrades - so NACK.
> > 
> > You asked me to do that in another patch, not this one. :)
> 
> I know, but that does not change the fact it is still missing :-)
> 
> > 
> >> "cn=IPA OTP Counter,cn=plugins,cn=config" plugin configuration also needs 
> >> to be 
> >> added in some update file.
> > 
> > So I'm generally confused then. If we have to add the plugin config to
> > an update file, why bother creating
> > daemons/ipa-slapi-plugins/ipa-otp-counter/otp-counter-conf.ldif or
> > modifying ipaserver/install/dsinstance.py at all? Should these changes
> > be removed?
> 
> Good question. Most LDAP update related changes are added to update plugins
> only. But plugin registration seemed to be still on 2 places, like 'cn=IPA
> Range-Check,cn=plugins,cn=config' registration.
> 
> Up to you/additional developer discussion.

Fixed.

Also note that the same problem was present in the OTP Last Token plugin
(committed for 4.0). I have made a patch which fixes this as well:

https://www.redhat.com/archives/freeipa-devel/2014-October/msg00358.html

Nathaniel
From 7220238f3489dfb84e537dd2debda53ab7ffa14d Mon Sep 17 00:00:00 2001
From: Nathaniel McCallum <npmccal...@redhat.com>
Date: Wed, 10 Sep 2014 17:31:37 -0400
Subject: [PATCH] Create ipa-otp-counter 389DS plugin

This plugin ensures that all counter/watermark operations are atomic
and never decrement. Also, deletion is not permitted.

Because this plugin also ensures internal operations behave properly,
this also gives ipa-pwd-extop the appropriate behavior for OTP
authentication.

https://fedorahosted.org/freeipa/ticket/4493
https://fedorahosted.org/freeipa/ticket/4494
---
 daemons/configure.ac                               |   1 +
 daemons/ipa-slapi-plugins/Makefile.am              |   1 +
 .../ipa-slapi-plugins/ipa-otp-counter/Makefile.am  |  21 +
 daemons/ipa-slapi-plugins/ipa-otp-counter/berval.c |  96 +++++
 daemons/ipa-slapi-plugins/ipa-otp-counter/berval.h |  66 +++
 .../ipa-otp-counter/ipa-otp-counter.sym            |   1 +
 .../ipa-otp-counter/ipa_otp_counter.c              | 454 +++++++++++++++++++++
 .../ipa-slapi-plugins/ipa-otp-counter/ldapmod.c    | 110 +++++
 .../ipa-slapi-plugins/ipa-otp-counter/ldapmod.h    |  54 +++
 freeipa.spec.in                                    |   8 +-
 install/updates/40-otp.update                      |  15 +
 11 files changed, 824 insertions(+), 3 deletions(-)
 create mode 100644 daemons/ipa-slapi-plugins/ipa-otp-counter/Makefile.am
 create mode 100644 daemons/ipa-slapi-plugins/ipa-otp-counter/berval.c
 create mode 100644 daemons/ipa-slapi-plugins/ipa-otp-counter/berval.h
 create mode 100644 daemons/ipa-slapi-plugins/ipa-otp-counter/ipa-otp-counter.sym
 create mode 100644 daemons/ipa-slapi-plugins/ipa-otp-counter/ipa_otp_counter.c
 create mode 100644 daemons/ipa-slapi-plugins/ipa-otp-counter/ldapmod.c
 create mode 100644 daemons/ipa-slapi-plugins/ipa-otp-counter/ldapmod.h

diff --git a/daemons/configure.ac b/daemons/configure.ac
index b4507a6d972f854331925e72869898576bdfd76f..bfcdeadcd1dc73762d8c773ee50210d9bdb91e92 100644
--- a/daemons/configure.ac
+++ b/daemons/configure.ac
@@ -314,6 +314,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-counter/Makefile
     ipa-slapi-plugins/ipa-otp-lasttoken/Makefile
     ipa-slapi-plugins/ipa-pwd-extop/Makefile
     ipa-slapi-plugins/ipa-extdom-extop/Makefile
diff --git a/daemons/ipa-slapi-plugins/Makefile.am b/daemons/ipa-slapi-plugins/Makefile.am
index 06e6ee8b86f138cce05f2184ac98c39ffaf9757f..07733921e43ac2eb9e248b276351d915a854bf7e 100644
--- a/daemons/ipa-slapi-plugins/Makefile.am
+++ b/daemons/ipa-slapi-plugins/Makefile.am
@@ -7,6 +7,7 @@ SUBDIRS =			\
 	ipa-enrollment		\
 	ipa-lockout		\
 	ipa-modrdn		\
+	ipa-otp-counter		\
 	ipa-otp-lasttoken	\
 	ipa-pwd-extop		\
 	ipa-extdom-extop	\
diff --git a/daemons/ipa-slapi-plugins/ipa-otp-counter/Makefile.am b/daemons/ipa-slapi-plugins/ipa-otp-counter/Makefile.am
new file mode 100644
index 0000000000000000000000000000000000000000..a5ee14527c965b046689394e780c2a5056253e21
--- /dev/null
+++ b/daemons/ipa-slapi-plugins/ipa-otp-counter/Makefile.am
@@ -0,0 +1,21 @@
+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)
+
+plugindir = $(libdir)/dirsrv/plugins
+plugin_LTLIBRARIES = libipa_otp_counter.la
+libipa_otp_counter_la_SOURCES = berval.c berval.h ldapmod.c ldapmod.h ipa_otp_counter.c
+libipa_otp_counter_la_LDFLAGS = -avoid-version -export-symbols ipa-otp-counter.sym
+libipa_otp_counter_la_LIBADD = $(LDAP_LIBS)
diff --git a/daemons/ipa-slapi-plugins/ipa-otp-counter/berval.c b/daemons/ipa-slapi-plugins/ipa-otp-counter/berval.c
new file mode 100644
index 0000000000000000000000000000000000000000..884e1a21004c5440f3bbad9da57d43bba8649d5f
--- /dev/null
+++ b/daemons/ipa-slapi-plugins/ipa-otp-counter/berval.c
@@ -0,0 +1,96 @@
+/** 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, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ *
+ * Additional permission under GPLv3 section 7:
+ *
+ * In the following paragraph, "GPL" means the GNU General Public
+ * License, version 3 or any later version, and "Non-GPL Code" means
+ * code that is governed neither by the GPL nor a license
+ * compatible with the GPL.
+ *
+ * You may link the code of this Program with Non-GPL Code and convey
+ * linked combinations including the two, provided that such Non-GPL
+ * Code only links 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 GPL. Only the copyright holders of this
+ * Program may make changes or additions to the list of Approved
+ * Interfaces.
+ *
+ * Authors:
+ * Nathaniel McCallum <npmccal...@redhat.com>
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ * All rights reserved.
+ * END COPYRIGHT BLOCK **/
+
+#include "berval.h"
+
+#include <slapi-plugin.h>
+
+#include <limits.h>
+
+struct berval *
+berval_new_longlong(long long value)
+{
+    struct berval *bv;
+
+    bv = (struct berval*) slapi_ch_malloc(sizeof(struct berval*));
+    bv->bv_val = slapi_ch_smprintf("%lld", value);
+    bv->bv_len = strlen(bv->bv_val);
+
+    return bv;
+}
+
+void
+berval_free(struct berval **bv)
+{
+    if (*bv == NULL)
+        return;
+
+    slapi_ch_free((void **) &(*bv)->bv_val);
+    slapi_ch_free((void **) bv);
+}
+
+long long
+berval_to_longlong(const struct berval *bv)
+{
+    char buf[bv->bv_len + 1];
+    memcpy(buf, bv->bv_val, bv->bv_len);
+    buf[sizeof(buf)-1] = '\0';
+
+    return strtoll(buf, NULL, 10);
+}
+
+struct berval **
+bervals_new_longlong(long long value)
+{
+    struct berval **bvs;
+
+    bvs = (struct berval**) slapi_ch_calloc(2, sizeof(struct berval*));
+    bvs[0] = berval_new_longlong(value);
+
+    return bvs;
+}
+
+void
+bervals_free(struct berval ***bvals)
+{
+    for (struct berval **itr = *bvals; *itr != NULL; itr++)
+        berval_free(itr);
+
+    slapi_ch_free((void**) bvals);
+}
diff --git a/daemons/ipa-slapi-plugins/ipa-otp-counter/berval.h b/daemons/ipa-slapi-plugins/ipa-otp-counter/berval.h
new file mode 100644
index 0000000000000000000000000000000000000000..5ad4ebc147043bd61dbcd53b6675bf3b170d19ae
--- /dev/null
+++ b/daemons/ipa-slapi-plugins/ipa-otp-counter/berval.h
@@ -0,0 +1,66 @@
+/** 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, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ *
+ * Additional permission under GPLv3 section 7:
+ *
+ * In the following paragraph, "GPL" means the GNU General Public
+ * License, version 3 or any later version, and "Non-GPL Code" means
+ * code that is governed neither by the GPL nor a license
+ * compatible with the GPL.
+ *
+ * You may link the code of this Program with Non-GPL Code and convey
+ * linked combinations including the two, provided that such Non-GPL
+ * Code only links 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 GPL. Only the copyright holders of this
+ * Program may make changes or additions to the list of Approved
+ * Interfaces.
+ *
+ * Authors:
+ * Nathaniel McCallum <npmccal...@redhat.com>
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ * All rights reserved.
+ * END COPYRIGHT BLOCK **/
+
+#pragma once
+
+#include <lber.h>
+#include <stdbool.h>
+
+/* Creates a new berval from a long long. */
+struct berval *
+berval_new_longlong(long long value);
+
+/* Frees a berval. */
+void
+berval_free(struct berval **bv);
+
+/* Converts a berval to a long long.
+ * If the value will not fit in a long long,
+ * LLONG_MIN or LLONG_MAX will be returned.
+ */
+long long
+berval_to_longlong(const struct berval *bv);
+
+/* Creates a NULL-terminated array with a single berval. */
+struct berval **
+bervals_new_longlong(long long value);
+
+/* Frees a NULL-terminated array of bervals. */
+void
+bervals_free(struct berval ***bvals);
diff --git a/daemons/ipa-slapi-plugins/ipa-otp-counter/ipa-otp-counter.sym b/daemons/ipa-slapi-plugins/ipa-otp-counter/ipa-otp-counter.sym
new file mode 100644
index 0000000000000000000000000000000000000000..acc5e9394b8f7e7cc6eb767d700073f7745416ea
--- /dev/null
+++ b/daemons/ipa-slapi-plugins/ipa-otp-counter/ipa-otp-counter.sym
@@ -0,0 +1 @@
+ipa_otp_counter_init
diff --git a/daemons/ipa-slapi-plugins/ipa-otp-counter/ipa_otp_counter.c b/daemons/ipa-slapi-plugins/ipa-otp-counter/ipa_otp_counter.c
new file mode 100644
index 0000000000000000000000000000000000000000..24ef9e2401d62d7d63b55afb9aa3ba2f41642839
--- /dev/null
+++ b/daemons/ipa-slapi-plugins/ipa-otp-counter/ipa_otp_counter.c
@@ -0,0 +1,454 @@
+/** 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, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ *
+ * Additional permission under GPLv3 section 7:
+ *
+ * In the following paragraph, "GPL" means the GNU General Public
+ * License, version 3 or any later version, and "Non-GPL Code" means
+ * code that is governed neither by the GPL nor a license
+ * compatible with the GPL.
+ *
+ * You may link the code of this Program with Non-GPL Code and convey
+ * linked combinations including the two, provided that such Non-GPL
+ * Code only links 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 GPL. Only the copyright holders of this
+ * Program may make changes or additions to the list of Approved
+ * Interfaces.
+ *
+ * Authors:
+ * Nathaniel McCallum <npmccal...@redhat.com>
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ * All rights reserved.
+ * END COPYRIGHT BLOCK **/
+
+/**
+ * The purpose of this plugin is to ensure that counter/watermark values:
+ * 1. Have atomic operations.
+ * 2. Never go backwards.
+ * 3. Never get deleted.
+ */
+
+#ifdef HAVE_CONFIG_H
+#  include <config.h>
+#endif
+
+#include "berval.h"
+#include "ldapmod.h"
+
+#include <limits.h>
+
+#include <plstr.h>
+
+#define COUNTER_UNSET LLONG_MIN
+
+static void *plugin_id;
+
+static long long
+get_counter(Slapi_Entry *entry, const char *attr)
+{
+    Slapi_Attr *sattr = NULL;
+
+    if (slapi_entry_attr_find(entry, attr, &sattr) == 0)
+        return slapi_entry_attr_get_longlong(entry, attr);
+
+    return COUNTER_UNSET;
+}
+
+/**
+ * Determines the name of the counter or watermark attribute based
+ * upon the objectClass of the entry.
+ *
+ * If no match is found, this function returns NULL indicating that
+ * this entry is not a known token type.
+ */
+static const char *
+find_counter_name(Slapi_Entry *entry)
+{
+    static struct {
+        const char *clss;
+        const char *attr;
+    } table[] = {
+        { "ipatokenHOTP", "ipatokenHOTPcounter" },
+        { "ipatokenTOTP", "ipatokenTOTPwatermark" },
+        { NULL, NULL }
+    };
+
+    const char *attr = NULL;
+    char **clsses = NULL;
+
+    clsses = slapi_entry_attr_get_charray(entry, "objectClass");
+    if (clsses == NULL)
+        return NULL;
+
+    for (size_t i = 0; attr == NULL && clsses[i] != NULL; i++) {
+        for (size_t j = 0; attr == NULL && table[j].clss != NULL; j++) {
+            if (PL_strcasecmp(table[j].clss, clsses[i]) == 0)
+                attr = table[j].attr;
+        }
+    }
+
+    slapi_ch_array_free(clsses);
+    return attr;
+}
+
+/**
+ * Normalizes the input values of counter/watermark modifications.
+ *
+ * 1. All INCREMENT and REPLACE operations need to be replace by
+ *    equivalent DELETE/ADD combination operations. This ensures
+ *    atomicity.
+ *
+ * 2. Any incoming DELETE operations need to be sanity checked.
+ *
+ *    If no value is specified, the current counter value is added
+ *    to the operation. Without this, we cannot guarantee that the
+ *    operation will not causes a decrement.
+ *
+ * This function returns the size of the new LDAPMod* array or zero
+ * if there are no counter/watermark operations.
+ */
+static size_t
+normalize_input(LDAPMod ***mods, const char *attr, long long ctr)
+{
+    LDAPMod **tmp;
+    size_t o; /* Counts the number of operations. */
+    size_t c; /* Counts the number of counter operations. */
+    size_t e; /* Counts the number of expansions. */
+
+    /* Get the size of the mods when all expansions are performed. */
+    for (o = c = e = 0; (*mods)[o] != NULL; o++) {
+        if (PL_strcasecmp((*mods)[o]->mod_type, attr) != 0)
+            continue;
+
+        switch ((*mods)[o]->mod_op & LDAP_MOD_OP) {
+        case LDAP_MOD_REPLACE:
+        case LDAP_MOD_INCREMENT:
+            e++;
+        default:
+            c++;
+        }
+    }
+
+    if (c == 0)
+        return 0;
+
+    /* Filter the modify operations. */
+    tmp = (LDAPMod **) slapi_ch_calloc(o + e + 1, sizeof(LDAPMod*));
+    for (size_t i = 0, j = 0; (*mods)[i] != NULL; tmp[j++] = (*mods)[i++]) {
+        LDAPMod *mod = (*mods)[i];
+
+        if (PL_strcasecmp(mod->mod_type, attr) != 0)
+            continue;
+
+        /* This is not strictly needed, but simplifies the code. */
+        ldapmod_convert_bvalues(mod);
+
+        switch (mod->mod_op & LDAP_MOD_OP) {
+        case LDAP_MOD_DELETE:
+            /* Normalize input: if an empty array is allocated, free it. */
+            if (mod->mod_bvalues != NULL && mod->mod_bvalues[0] == NULL)
+                bervals_free(&mod->mod_bvalues);
+
+            if (mod->mod_bvalues == NULL)
+                mod->mod_bvalues = bervals_new_longlong(ctr);
+
+            ctr = COUNTER_UNSET;
+            break;
+
+        case LDAP_MOD_INCREMENT:
+            if (ctr != COUNTER_UNSET)
+                tmp[j++] = ldapmod_new_longlong(LDAP_MOD_DELETE, attr, ctr);
+
+            ctr += ldapmod_get_value(mod, 1);
+
+            bervals_free(&mod->mod_bvalues);
+            mod->mod_op = LDAP_MOD_ADD | LDAP_MOD_BVALUES;
+            mod->mod_bvalues = bervals_new_longlong(ctr);
+            break;
+
+        case LDAP_MOD_REPLACE:
+            if (ctr != COUNTER_UNSET)
+                tmp[j++] = ldapmod_new_longlong(LDAP_MOD_DELETE, attr, ctr);
+
+            mod->mod_op = LDAP_MOD_ADD | LDAP_MOD_BVALUES;
+
+            /* Fall through. */
+
+        case LDAP_MOD_ADD:
+            ctr = ldapmod_get_value(mod, 0);
+            break;
+        }
+    }
+
+    slapi_ch_free((void **) mods);
+    *mods = tmp;
+    return o + e;
+}
+
+/**
+ * Simulates how the specified mods will impact the counter.
+ */
+static bool
+simulate(LDAPMod **mods, const char *attr, long long ctr, long long *out)
+{
+    bool success = true;
+
+    for (size_t i = 0; mods[i] != NULL; i++) {
+        if (PL_strcasecmp(mods[i]->mod_type, attr) != 0)
+            continue;
+
+        switch (mods[i]->mod_op & LDAP_MOD_OP) {
+        case LDAP_MOD_DELETE:
+            if (ctr == COUNTER_UNSET)
+                success = false;
+
+            ctr = COUNTER_UNSET;
+            break;
+
+        case LDAP_MOD_INCREMENT:
+            if (ctr == COUNTER_UNSET)
+                success = false;
+
+            ctr = ldapmod_get_value(mods[i], ctr + 1);
+            break;
+
+        case LDAP_MOD_ADD:
+            if (ctr != COUNTER_UNSET)
+                success = false;
+
+            /* Fall through. */
+
+        case LDAP_MOD_REPLACE:
+            ctr = ldapmod_get_value(mods[i], 0);
+            break;
+        }
+    }
+
+    *out = ctr;
+    return success;
+}
+
+/**
+ * Modifies input to ensure correct counter behavior.
+ *
+ * For non-replication operations, we change all REPLACE and INCREMENT
+ * operations into DELETE/ADD pair operations. We also sanity check
+ * incoming DELETE operations. If the request would cause the counter to
+ * decrement or delete, fail the operation.
+ *
+ * For replication operations, if the transaction would decrement the
+ * counter, delete it or modify it to the same value, we remove all mods
+ * related to the counter and let the replication request continue. In
+ * the first two cases, this enforces correct behavior. In the last case,
+ * this reduces write contention on the counter when a replica-set-wide
+ * authentication collision has occurred.
+ */
+static int
+preop_mod(Slapi_PBlock *pb)
+{
+    Slapi_Entry *epre = NULL;
+    const char *attr = NULL;
+    LDAPMod **mods = NULL;
+    char *msg = NULL;
+    long long cpost;
+    long long cpre;
+    int repl = 0;
+    int rc = 0;
+
+    rc |= slapi_pblock_get(pb, SLAPI_IS_REPLICATED_OPERATION, &repl);
+    rc |= slapi_pblock_get(pb, SLAPI_ENTRY_PRE_OP, &epre);
+    rc |= slapi_pblock_get(pb, SLAPI_MODIFY_MODS, &mods);
+    if (rc != 0 || epre == NULL || mods == NULL)
+        return 0;
+
+    attr = find_counter_name(epre);
+    if (attr == NULL)
+        return 0; /* Not a token. */
+
+    cpre = get_counter(epre, attr);
+
+    if (repl == 0) {
+        if (normalize_input(&mods, attr, cpre) != 0)
+            slapi_pblock_set(pb, SLAPI_MODIFY_MODS, mods);
+    }
+
+    if (!simulate(mods, attr, cpre, &cpost) && repl == 0) {
+        msg = slapi_ch_smprintf("Invalid operation sequence on %s", attr);
+        goto error;
+    }
+
+    if (cpost < cpre) {
+        if (repl == 0) {
+            msg = slapi_ch_smprintf("Will not %s %s",
+                cpost == COUNTER_UNSET ? "delete" : "decrement", attr);
+            goto error;
+        }
+
+        /* Remove counter attribute modifications. */
+        for (size_t i = 0, j = 0 ; ; i++, j++) {
+            mods[j] = mods[i];
+            if (mods[j] == NULL)
+                break;
+
+            if (PL_strcasecmp(mods[j]->mod_type, attr) == 0)
+                ldapmod_free(&mods[j--]);
+        }
+    }
+
+    return 0;
+
+error:
+    rc = LDAP_UNWILLING_TO_PERFORM;
+    slapi_send_ldap_result(pb, rc, NULL, msg, 0, NULL);
+    slapi_pblock_set(pb, SLAPI_RESULT_CODE, &rc);
+
+    slapi_ch_free_string(&msg);
+    return rc;
+}
+
+static void
+writeback(Slapi_Entry *entry, const char *attr,
+          long long cold, long long cnew)
+{
+    Slapi_PBlock *pb = NULL;
+    char dbuf[32];
+    char abuf[32];
+
+    LDAPMod *mods[] = {
+        &(LDAPMod) {
+            LDAP_MOD_DELETE, (char *) attr,
+            .mod_values = (char *[]) { dbuf, NULL }
+        },
+        &(LDAPMod) {
+            LDAP_MOD_ADD, (char *) attr,
+            .mod_values = (char *[]) { abuf, NULL }
+        },
+        NULL
+    };
+
+    snprintf(dbuf, sizeof(dbuf), "%lld", cold);
+    snprintf(abuf, sizeof(abuf), "%lld", cnew);
+
+    pb = slapi_pblock_new();
+    slapi_modify_internal_set_pb(pb, slapi_entry_get_dn_const(entry),
+                                 mods, NULL, NULL, plugin_id, 0);
+    slapi_modify_internal_pb(pb);
+    slapi_pblock_destroy(pb);
+}
+
+/**
+ * Ensures replications receive the highest value seen.
+ *
+ * A replication request that arrives at the server may be internally
+ * discarded, even if it has a higher counter value, because of a lower
+ * CSN. However, we always want to record the highest value seen.
+ *
+ * We solve this problem by checking the value of the replication request
+ * against the value of the entry after the replication. If the replication
+ * request contained a higher value than what the entry contains, we create
+ * a new modification to bump up the counter to the highest value.
+ *
+ * This check is only for replication operations.
+ */
+static int
+postop_mod(Slapi_PBlock *pb)
+{
+    Slapi_Entry *epost = NULL;
+    Slapi_Entry *epre = NULL;
+    const char *attr = NULL;
+    LDAPMod **mods = NULL;
+    long long cpost;
+    long long cpre;
+    long long csim;
+    int repl = 0;
+    int rc = 0;
+
+    rc |= slapi_pblock_get(pb, SLAPI_IS_REPLICATED_OPERATION, &repl);
+    rc |= slapi_pblock_get(pb, SLAPI_ENTRY_POST_OP, &epost);
+    rc |= slapi_pblock_get(pb, SLAPI_ENTRY_PRE_OP, &epre);
+    rc |= slapi_pblock_get(pb, SLAPI_MODIFY_MODS, &mods);
+    if (rc != 0 || epost == NULL || epre == NULL || mods == NULL)
+        return 0;
+
+    if (repl == 0)
+        return 0;
+
+    attr = find_counter_name(epost);
+    if (attr == NULL)
+        return 0; /* Not a token. */
+
+    cpost = get_counter(epost, attr);
+    cpre = get_counter(epre, attr);
+
+    if (simulate(mods, attr, cpre, &csim) && csim > cpost)
+        writeback(epost, attr, cpost, csim);
+
+    return 0;
+}
+
+static int
+preop_init(Slapi_PBlock *pb)
+{
+    return slapi_pblock_set(pb, SLAPI_PLUGIN_BE_PRE_MODIFY_FN, preop_mod);
+}
+
+static int
+postop_init(Slapi_PBlock *pb)
+{
+    return slapi_pblock_set(pb, SLAPI_PLUGIN_BE_POST_MODIFY_FN, postop_mod);
+}
+
+static int
+start_fn(Slapi_PBlock *pb)
+{
+    return 0;
+}
+
+static int
+close_fn(Slapi_PBlock *pb)
+{
+    return 0;
+}
+
+int
+ipa_otp_counter_init(Slapi_PBlock *pb)
+{
+    static const Slapi_PluginDesc desc = {
+        "ipa-otp-counter",
+        "FreeIPA",
+        "FreeIPA/1.0",
+        "Ensure proper OTP token counter operation"
+    };
+
+    int ret = 0;
+
+    ret |= slapi_pblock_get(pb, SLAPI_PLUGIN_IDENTITY, &plugin_id);
+    ret |= slapi_pblock_set(pb, SLAPI_PLUGIN_VERSION, SLAPI_PLUGIN_VERSION_01);
+    ret |= slapi_pblock_set(pb, SLAPI_PLUGIN_DESCRIPTION, (void *) &desc);
+    ret |= slapi_pblock_set(pb, SLAPI_PLUGIN_START_FN, start_fn);
+    ret |= slapi_pblock_set(pb, SLAPI_PLUGIN_CLOSE_FN, close_fn);
+    ret |= slapi_register_plugin("bepreoperation", 1, __func__, preop_init,
+                                 "ipa-otp-counter bepreoperation", NULL,
+                                 plugin_id);
+    ret |= slapi_register_plugin("bepostoperation", 1, __func__, postop_init,
+                                 "ipa-otp-counter bepostoperation", NULL,
+                                 plugin_id);
+
+    return ret;
+}
diff --git a/daemons/ipa-slapi-plugins/ipa-otp-counter/ldapmod.c b/daemons/ipa-slapi-plugins/ipa-otp-counter/ldapmod.c
new file mode 100644
index 0000000000000000000000000000000000000000..34fb0479475594c823b02bc55321ff426580f6ea
--- /dev/null
+++ b/daemons/ipa-slapi-plugins/ipa-otp-counter/ldapmod.c
@@ -0,0 +1,110 @@
+/** 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, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ *
+ * Additional permission under GPLv3 section 7:
+ *
+ * In the following paragraph, "GPL" means the GNU General Public
+ * License, version 3 or any later version, and "Non-GPL Code" means
+ * code that is governed neither by the GPL nor a license
+ * compatible with the GPL.
+ *
+ * You may link the code of this Program with Non-GPL Code and convey
+ * linked combinations including the two, provided that such Non-GPL
+ * Code only links 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 GPL. Only the copyright holders of this
+ * Program may make changes or additions to the list of Approved
+ * Interfaces.
+ *
+ * Authors:
+ * Nathaniel McCallum <npmccal...@redhat.com>
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ * All rights reserved.
+ * END COPYRIGHT BLOCK **/
+
+#include "ldapmod.h"
+#include "berval.h"
+
+#include <limits.h>
+
+long long
+ldapmod_get_value(const LDAPMod *mod, long long def)
+{
+    long long v;
+
+    if (mod == NULL)
+        return def;
+
+    if (mod->mod_bvalues == NULL)
+        return def;
+
+    if (mod->mod_bvalues[0] == NULL)
+        return def;
+
+    v = berval_to_longlong(mod->mod_bvalues[0]);
+    if (v == LLONG_MIN || v == LLONG_MAX)
+        return def;
+
+    return v;
+}
+
+LDAPMod *
+ldapmod_new_longlong(int op, const char *attr, long long value)
+{
+    LDAPMod *mod;
+
+    mod = (LDAPMod*) slapi_ch_malloc(sizeof(LDAPMod));
+    mod->mod_op = op | LDAP_MOD_BVALUES;
+    mod->mod_type = slapi_ch_strdup(attr);
+    mod->mod_bvalues = bervals_new_longlong(value);
+
+    return mod;
+}
+
+void
+ldapmod_convert_bvalues(LDAPMod *mod)
+{
+    if (mod == NULL || (mod->mod_op & LDAP_MOD_BVALUES))
+        return;
+
+    mod->mod_op |= LDAP_MOD_BVALUES;
+
+    if (mod->mod_values == NULL) {
+        mod->mod_bvalues = NULL;
+        return;
+    }
+
+    for (size_t i = 0; mod->mod_values[i] != NULL; i++) {
+        struct berval *bv;
+        bv = (struct berval*) slapi_ch_malloc(sizeof(struct berval));
+        bv->bv_val = mod->mod_values[i];
+        bv->bv_len = strlen(bv->bv_val);
+        mod->mod_bvalues[i] = bv;
+    }
+}
+
+void
+ldapmod_free(LDAPMod **mod)
+{
+    if (mod == NULL || *mod == NULL)
+        return;
+
+    bervals_free(&(*mod)->mod_bvalues);
+    slapi_ch_free_string(&(*mod)->mod_type);
+    slapi_ch_free((void **) mod);
+}
diff --git a/daemons/ipa-slapi-plugins/ipa-otp-counter/ldapmod.h b/daemons/ipa-slapi-plugins/ipa-otp-counter/ldapmod.h
new file mode 100644
index 0000000000000000000000000000000000000000..45f43904b2288a97802ad2d698a30be972e2d8b7
--- /dev/null
+++ b/daemons/ipa-slapi-plugins/ipa-otp-counter/ldapmod.h
@@ -0,0 +1,54 @@
+/** 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, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ *
+ * Additional permission under GPLv3 section 7:
+ *
+ * In the following paragraph, "GPL" means the GNU General Public
+ * License, version 3 or any later version, and "Non-GPL Code" means
+ * code that is governed neither by the GPL nor a license
+ * compatible with the GPL.
+ *
+ * You may link the code of this Program with Non-GPL Code and convey
+ * linked combinations including the two, provided that such Non-GPL
+ * Code only links 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 GPL. Only the copyright holders of this
+ * Program may make changes or additions to the list of Approved
+ * Interfaces.
+ *
+ * Authors:
+ * Nathaniel McCallum <npmccal...@redhat.com>
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ * All rights reserved.
+ * END COPYRIGHT BLOCK **/
+
+#pragma once
+
+#include <slapi-plugin.h>
+
+long long
+ldapmod_get_value(const LDAPMod *mod, long long def);
+
+LDAPMod *
+ldapmod_new_longlong(int op, const char *attr, long long value);
+
+void
+ldapmod_convert_bvalues(LDAPMod *mod);
+
+void
+ldapmod_free(LDAPMod **mod);
diff --git a/freeipa.spec.in b/freeipa.spec.in
index 008494d8bb37516981e0abd845bbc0001c6cf5a9..a63b2d254fd1de2016be9db0003ff05f90215b06 100644
--- a/freeipa.spec.in
+++ b/freeipa.spec.in
@@ -33,7 +33,7 @@ Source0:        freeipa-%{version}.tar.gz
 BuildRoot:      %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)
 
 %if ! %{ONLY_CLIENT}
-BuildRequires:  389-ds-base-devel >= 1.3.3.2
+BuildRequires:  389-ds-base-devel >= 1.3.3.5
 BuildRequires:  svrcore-devel
 BuildRequires:  policycoreutils >= 2.1.12-5
 BuildRequires:  systemd-units
@@ -102,7 +102,7 @@ Group: System Environment/Base
 Requires: %{name}-python = %{version}-%{release}
 Requires: %{name}-client = %{version}-%{release}
 Requires: %{name}-admintools = %{version}-%{release}
-Requires: 389-ds-base >= 1.3.3.2
+Requires: 389-ds-base >= 1.3.3.5
 Requires: openldap-clients > 2.4.35-4
 Requires: nss >= 3.14.3-12.0
 Requires: nss-tools >= 3.14.3-12.0
@@ -139,7 +139,7 @@ Requires: zip
 Requires: policycoreutils >= 2.1.12-5
 Requires: tar
 Requires(pre): certmonger >= 0.75.13
-Requires(pre): 389-ds-base >= 1.3.3.2
+Requires(pre): 389-ds-base >= 1.3.3.5
 Requires: fontawesome-fonts
 Requires: open-sans-fonts
 
@@ -375,6 +375,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_counter.la
 rm %{buildroot}/%{plugin_dir}/libipa_otp_lasttoken.la
 rm %{buildroot}/%{_libdir}/krb5/plugins/kdb/ipadb.la
 rm %{buildroot}/%{_libdir}/samba/pdb/ipasam.la
@@ -736,6 +737,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_counter.so
 %attr(755,root,root) %{plugin_dir}/libipa_otp_lasttoken.so
 %dir %{_localstatedir}/lib/ipa
 %attr(700,root,root) %dir %{_localstatedir}/lib/ipa/backup
diff --git a/install/updates/40-otp.update b/install/updates/40-otp.update
index caa21c380618dd6450205b0883c6165e9ac40967..92d6f625409e50713f1e59a400487dd310cc036a 100644
--- a/install/updates/40-otp.update
+++ b/install/updates/40-otp.update
@@ -20,3 +20,18 @@ dn: cn=radiusproxy,$SUFFIX
 default: objectClass: nsContainer
 default: objectClass: top
 default: cn: radiusproxy
+
+dn: cn=IPA OTP Counter,cn=plugins,cn=config
+default:objectclass: top
+default:objectclass: nsSlapdPlugin
+default:objectclass: extensibleObject
+default:cn: IPA OTP Counter
+default:nsslapd-pluginpath: libipa_otp_counter
+default:nsslapd-plugininitfunc: ipa_otp_counter_init
+default:nsslapd-plugintype: preoperation
+default:nsslapd-pluginenabled: on
+default:nsslapd-pluginid: ipa-otp-counter
+default:nsslapd-pluginversion: 1.0
+default:nsslapd-pluginvendor: Red Hat, Inc.
+default:nsslapd-plugindescription: IPA OTP Counter plugin
+default:nsslapd-plugin-depends-on-type: database
-- 
2.1.0

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

Reply via email to