On Wed, 2014-01-22 at 15:59 -0500, Nathaniel McCallum wrote:
> In attempting to write an OTP synchronization client, I've noticed it
> doesn't fit into the framework very well. The job of the client is to
> perform the synchronization extended operation. The format of the
> request is this:
> 
>      OTPSyncRequestValue ::= SEQUENCE {
>          userDN            OCTET STRING,
>          tokenDN      [0]  OCTET STRING OPTIONAL,
>          firstFactor  [1]  OCTET STRING OPTIONAL,
>          firstCode         INTEGER,
>          secondCode        INTEGER
>      }
> 
> In all cases, the user MUST provide two token codes and MAY provide the
> DN of a token to sync.
> 
> >From here two cases exist: bound and unbound.
> 
> In the unbound case, both the userDN and firstFactor fields are required
> and authentication is performed internally.
> 
> In the bound case, the client has already bound (usually via a kerberos
> ticket). In this case, the client must provide userDN only. There are
> two options here. First, the client can generate the userDN
> automatically from the kerberos ticket metadata. Second, the extop
> plugin can make the userDN field optional and simply rely on the
> internal bind DN. This is my preferred route, and will require a new
> revision of the otp sync patch (no problem). In this second case, if the
> user is bound, the DS plugin would ignore the values of
> userDN/firstFactor.
> 
> Assuming the second case to be true, how do I write a command in the
> framework that will attempt a krb5 bind and then prompt for
> username/password if the bind fails? Also, how do I, on the client side,
> without any bind to LDAP translate the username to the userDN? The same
> is true for the token ID to DN translation? Would it be better to write
> this code independently of the FreeIPA client command framework?

Attached is a first attempt at implementing a sync client. It works, but
isn't ready for merger. Please help me suggest ways to overcome the
various problems:

1. This requires a krb5 TGT in order to perform the command. I am
assuming that everything in execute() is happening on the server-side
and the server wants authentication. Obviously, we can't require a TGT
to sync the token.

2. If a TGT *is* present, it would be really nice to bypass the
username/password options entirely and do a SASL bind (from the client?
Is 389DS guaranteed to be available to the client?

3. There has to be a better way to get the LDAP URI than what I'm doing.

4. I have concerns about passing the password over the wire. We are
doing this two places:
  A. client => freeipa
  B. freeipa => 389DS

Thoughts?

5. What should I do about the return value of the command?

Nathaniel
>From e2b383493a4950febd6d7d6b82ded16b8495dabe Mon Sep 17 00:00:00 2001
From: Nathaniel McCallum <npmccal...@redhat.com>
Date: Thu, 20 Feb 2014 16:51:23 -0500
Subject: [PATCH] First stab at sync client

---
 ipalib/parameters.py       |  7 +++--
 ipalib/plugins/otptoken.py | 78 +++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 81 insertions(+), 4 deletions(-)

diff --git a/ipalib/parameters.py b/ipalib/parameters.py
index b4fb3402df0ab1af8fb71086ccf22ee3a704b322..33282f08db641127b71a1ea353e7388784b29cbe 100644
--- a/ipalib/parameters.py
+++ b/ipalib/parameters.py
@@ -1045,10 +1045,11 @@ class Int(Number):
     kwargs = Param.kwargs + (
         ('minvalue', (int, long), int(MININT)),
         ('maxvalue', (int, long), int(MAXINT)),
+        ('base', int, 0),
     )
 
     @staticmethod
-    def convert_int(value):
+    def convert_int(value, base=0):
         if type(value) in Int.allowed_types:
             return value
 
@@ -1058,7 +1059,7 @@ class Int(Number):
         if type(value) is unicode:
             if u'.' in value:
                 return int(float(value))
-            return int(value, 0)
+            return int(value, base)
 
         raise ValueError(value)
 
@@ -1076,7 +1077,7 @@ class Int(Number):
         Convert a single scalar value.
         """
         try:
-            return Int.convert_int(value)
+            return Int.convert_int(value, self.base)
         except ValueError:
             raise ConversionError(name=self.get_param_name(),
                                   index=index,
diff --git a/ipalib/plugins/otptoken.py b/ipalib/plugins/otptoken.py
index 89091b9b2ae04c66bdc541c50da378e87e76800d..74710a316be68eb30e120ba486a1065beba0eb1d 100644
--- a/ipalib/plugins/otptoken.py
+++ b/ipalib/plugins/otptoken.py
@@ -18,15 +18,20 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 from ipalib.plugins.baseldap import DN, LDAPObject, LDAPCreate, LDAPDelete, LDAPUpdate, LDAPSearch, LDAPRetrieve
-from ipalib import api, Int, Str, Bool, Flag, Bytes, IntEnum, StrEnum, _, ngettext
+from ipalib import api, Int, Str, Bool, Flag, Bytes, IntEnum, Password, StrEnum, _, ngettext
 from ipalib.plugable import Registry
 from ipalib.errors import PasswordMismatch, ConversionError, LastMemberError, NotFound
 from ipalib.request import context
+from ipalib import Command
+from pyasn1.type import univ, namedtype
+from pyasn1.codec.ber import encoder
 import base64
 import uuid
 import random
 import urllib
 import qrcode
+import ldap.controls
+import ConfigParser
 
 __doc__ = _("""
 OTP Tokens
@@ -85,6 +90,15 @@ class OTPTokenKey(Bytes):
 
         return Bytes._convert_scalar(self, value, index)
 
+class OTPSyncRequest(univ.Sequence):
+    OID = "2.16.840.1.113730.3.8.10.6"
+
+    componentType = namedtype.NamedTypes(
+        namedtype.NamedType('firstCode', univ.Integer()),
+        namedtype.NamedType('secondCode', univ.Integer()),
+        namedtype.OptionalNamedType('tokenDN', univ.OctetString()),
+    )
+
 def _convert_owner(userobj, entry_attrs, options):
     if 'ipatokenowner' in entry_attrs and not options.get('raw', False):
         entry_attrs['ipatokenowner'] = map(userobj.get_primary_key_from_dn,
@@ -349,3 +363,65 @@ class otptoken_show(LDAPRetrieve):
     def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
         _convert_owner(self.api.Object.user, entry_attrs, options)
         return super(otptoken_show, self).post_callback(ldap, dn, entry_attrs, *keys, **options)
+
+@register()
+class otptoken_sync(Command):
+    __doc__ = _('Synchronize an OTP token.')
+
+    takes_options = (
+        Str('username',
+            label=_('Username'),
+        ),
+        Password('password',
+            label=_('Password'),
+            confirm=False,
+        ),
+        Str('token?',
+            label=_('OTP token'),
+        ),
+    )
+
+    takes_args = (
+        Int('first_code',
+            label=_('First OTP token code'),
+            base=10,
+        ),
+        Int('second_code',
+            label=_('Second OTP token code'),
+            base=10,
+        ),
+    )
+
+    def execute(self, *args, **options):
+        userdn = str(api.Object['user'].get_dn(options['username']))
+        passwd = options['password']
+
+        # Create the request sequence
+        req = OTPSyncRequest()
+        req.setComponentByName("firstCode", args[0])
+        req.setComponentByName("secondCode", args[1])
+        if 'token' in options:
+            tokendn = str(api.Object['otptoken'].get_dn(options['token']))
+            req.setComponentByName('tokenDN', tokendn)
+
+        # Create the control
+        req = encoder.encode(req)
+        ctrl = ldap.controls.RequestControl(OTPSyncRequest.OID, True, req)
+
+        # Load configuration
+        cp = ConfigParser.ConfigParser()
+        cp.read("/etc/ipa/default.conf")
+        uri = cp.get('global', 'ldap_uri')
+
+        # Perform sync
+        try:
+            conn = ldap.initialize(uri)
+            try:
+                conn.simple_bind_s(userdn, passwd, serverctrls=[ctrl])
+                result = True
+            finally:
+                conn.unbind_s()
+        except ldap.LDAPError:
+            result = False
+
+        return dict(result=result)
-- 
1.8.5.3

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

Reply via email to