This patch has a few problems that I'd like some help with. There are a
few notes here as well.

1. The handling of the 'key' option is insecure. It should probably be
treated like a password (hidden from logs, etc). However, in this case,
it is binary, so I'm not quite sure how to do that. Passing it as a
command line option may be nice for scripting, but is potentially a
security problem if it ends up in bash.history. It would also be nice if
the encoding were base32 instead of base64, since nearly all the OTP
tools use this encoding.

2. The 'key' option also appears in otp-find. I'd like to suppress this.
How?

3. I had to make the 'id' option optional to make the uuid
autogeneration work in otp-add. However, this has the side-effect that
'id' is now optional in all the other commands. This is particularly bad
in the case of otp-del, where calling this command with no ID
transparently removes all tokens. How can I make this optional for
otp-add but required for all other commands?

4. otp-import is not implemented. I spent a few hours looking and I
didn't find any otp tool that actually uses this xml format for
exporting. Should we implement this now or wait until someone can
actually export data to us?

5. otp-del happily deletes the last token for a user. How can I find out
the dn of the user executing the command? Also, what is the right
exception to throw in pre_callback()?

6. user-show does not list the associated tokens for this user. Do we
care? It is a single search: otp-find --owner npmccallum.

7. otp-add only prints the qr code if the --qrcode option is specified.
This is for two reasons. First, and most importantly, the qr code
doesn't fit on a standard 24x80 terminal. I wanted to avoid dumping
garbage on people's screens by default. Second, you may not always want
the qr code output (like for a hard token or manual code entry).

Nathaniel
>From 6f06d8f8f5f92b4fc5601f7f68c19cacfdd67343 Mon Sep 17 00:00:00 2001
From: Nathaniel McCallum <npmccal...@redhat.com>
Date: Wed, 4 Sep 2013 23:47:19 -0400
Subject: [PATCH] Add OTP support to ipalib CLI

https://fedorahosted.org/freeipa/ticket/3368
---
 freeipa.spec.in          |   1 +
 ipalib/__init__.py       |   2 +-
 ipalib/parameters.py     |   8 ++
 ipalib/plugins/config.py |   2 +-
 ipalib/plugins/otp.py    | 267 +++++++++++++++++++++++++++++++++++++++++++++++
 ipalib/plugins/user.py   |   2 +-
 6 files changed, 279 insertions(+), 3 deletions(-)
 create mode 100644 ipalib/plugins/otp.py

diff --git a/freeipa.spec.in b/freeipa.spec.in
index 67bd3667db42ebb3639f50a4dd0c3a9fda000781..ff3f81beb839b03ee2a17f7d4acb41c19f8eaa55 100644
--- a/freeipa.spec.in
+++ b/freeipa.spec.in
@@ -126,6 +126,7 @@ Requires: python-ldap
 Requires: python-krbV
 Requires: acl
 Requires: python-pyasn1
+Requires: python-qrcode
 Requires: memcached
 Requires: python-memcached
 Requires: systemd-units >= 38
diff --git a/ipalib/__init__.py b/ipalib/__init__.py
index d822ba5956d6afb6ef6d88063f8359926e47016b..ab89ab77ec94603d242e56436021c9b6ed8663cb 100644
--- a/ipalib/__init__.py
+++ b/ipalib/__init__.py
@@ -886,7 +886,7 @@ from frontend import Command, LocalOrRemote, Updater, Advice
 from frontend import Object, Method, Property
 from crud import Create, Retrieve, Update, Delete, Search
 from parameters import DefaultFrom, Bool, Flag, Int, Decimal, Bytes, Str, IA5Str, Password, DNParam, DeprecatedParam
-from parameters import BytesEnum, StrEnum, AccessTime, File
+from parameters import BytesEnum, StrEnum, IntEnum, AccessTime, File
 from errors import SkipPluginModule
 from text import _, ngettext, GettextFactory, NGettextFactory
 
diff --git a/ipalib/parameters.py b/ipalib/parameters.py
index ab4b8321686bd88ad122a37ff289a0153e65ea21..cdea9c824932854140c1c2f1cbe679a374e56acf 100644
--- a/ipalib/parameters.py
+++ b/ipalib/parameters.py
@@ -1567,6 +1567,14 @@ class StrEnum(Enum):
     type = unicode
 
 
+class IntEnum(Enum):
+    """
+    Enumerable for integer data (stored in the ``int`` type).
+    """
+
+    type = int
+
+
 class Any(Param):
     """
     A parameter capable of holding values of any type. For internal use only.
diff --git a/ipalib/plugins/config.py b/ipalib/plugins/config.py
index 2a3f190620542952725535c7937dad3419eb720f..d42606a289b5126b213e981e6456cb840d70834a 100644
--- a/ipalib/plugins/config.py
+++ b/ipalib/plugins/config.py
@@ -202,7 +202,7 @@ class config(LDAPObject):
             cli_name='user_auth_type',
             label=_('Default user authentication types'),
             doc=_('Default types of supported user authentication'),
-            values=(u'password', u'radius'),
+            values=(u'password', u'radius', u'otp'),
             csv=True,
         ),
     )
diff --git a/ipalib/plugins/otp.py b/ipalib/plugins/otp.py
new file mode 100644
index 0000000000000000000000000000000000000000..6e0cfc4794e7ccd5f2a77691805f37e9a62a0508
--- /dev/null
+++ b/ipalib/plugins/otp.py
@@ -0,0 +1,267 @@
+# Authors:
+#   Nathaniel McCallum <npmccal...@redhat.com>
+#
+# Copyright (C) 2013  Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# 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/>.
+
+from ipalib.plugins.baseldap import *
+from ipalib import api, Str, Bool, Bytes, IntEnum, StrEnum, _, ngettext
+from ipalib import Command
+from ipalib.plugins import privilege
+from ipalib.plugable import Registry
+import uuid
+import random
+import urllib
+import qrcode
+
+__doc__ = _("""
+OTP Tokens
+
+Manage OTP tokens.
+
+IPA supports the use of OTP tokens for multi-factor authentication. This
+code enables the management of OTP tokens.
+
+EXAMPLES:
+
+ Add a new token:
+   ipa otp-add --type=totp --owner=jdoe --desc="My soft token"
+   
+ Examine the token:
+   ipa otp-show a93db710-a31a-4639-8647-f15b2c70b78a
+   
+ Change the vendor:
+   ipa otp-mod a93db710-a31a-4639-8647-f15b2c70b78a --vendor="Red Hat"
+
+ Delete a token:
+   ipa otp-del a93db710-a31a-4639-8647-f15b2c70b78a
+""")
+
+register = Registry()
+
+TOKEN_TYPES = (u'totp',)
+
+# NOTE: For maximum compatibility, KEY_LENGTH % 5 == 0
+KEY_LENGTH = 10
+
+@register()
+class otp(LDAPObject):
+    """
+    OTP Token object.
+    """
+    container_dn = api.env.container_otp
+    object_name = _('OTP tokens')
+    object_name_plural = _('OTP tokens')
+    object_class = ['ipatoken']
+    possible_objectclasses = ['ipatokentotp']
+    default_attributes = [
+        'ipatokenUniqueID', 'description', 'ipatokenOwner',
+        'ipatokenDisabled', 'ipatokenNotBefore', 'ipatokenNotAfter',
+        'ipatokenVendor', 'ipatokenModel', 'ipatokenSerial'
+    ]
+    rdn_is_primary_key = True
+
+    label = _('OTP tokens')
+    label_singular = _('OTP token')
+
+    takes_params = (
+        Str('ipatokenuniqueid?',
+            cli_name='id',
+            label=_('OTP token unique ID'),
+            primary_key=True,
+        ),
+        StrEnum('type',
+            label=_('OTP token type'),
+            values=TOKEN_TYPES,
+            flags=('virtual_attribute', 'no_update'),
+        ),
+        Str('description?',
+            cli_name='desc',
+            label=_('OTP token description'),
+        ),
+        Str('ipatokenowner?',
+            cli_name='owner',
+            label=_('OTP token owner'),
+        ),
+        Bool('ipatokendisabled?',
+            cli_name='disabled',
+            label=_('OTP token disabled state'),
+        ),
+        Str('ipatokennotbefore?',
+            cli_name='not_before',
+            label=_('OTP token validity start'),
+        ),
+        Str('ipatokennotafter?',
+            cli_name='not_after',
+            label=_('OTP token validity end'),
+        ),
+        Str('ipatokenvendor?',
+            cli_name='vendor',
+            label=_('OTP token vendor'),
+        ),
+        Str('ipatokenmodel?',
+            cli_name='model',
+            label=_('OTP token model'),
+        ),
+        Str('ipatokenserial?',
+            cli_name='serial',
+            label=_('OTP token serial'),
+        ),
+        Bytes('ipatokenotpkey?',
+            cli_name='key',
+            label=_('OTP token key'),
+            flags=('no_display', 'no_update')
+        ),
+        StrEnum('ipatokenotpalgorithm?',
+            cli_name='algo',
+            label=_('OTP token algorithm'),
+            flags=('no_update'),
+            values=(u'sha1', u'sha256', u'sha384', u'sha512')
+        ),
+        IntEnum('ipatokenotpdigits?',
+            cli_name='digits',
+            label=_('OTP token display length'),
+            values=(6, 8),
+            flags=('no_update')
+        ),
+        Int('ipatokentotpclockoffset?',
+            cli_name='offset',
+            label=_('OTP token clock offset'),
+            maxvalue=120,
+            flags=('no_update')
+        ),
+        Int('ipatokentotptimestep?',
+            cli_name='interval',
+            label=_('OTP token interval'),
+            minvalue=5,
+            maxvalue=120,
+            flags=('no_update')
+        ),
+    )
+    
+@register()
+class otp_add(LDAPCreate):
+    __doc__ = _('Add a new OTP token.')
+    msg_summary = _('Added OTP token "%(value)s"')
+    
+    takes_options = LDAPCreate.takes_options + (
+        Flag('qrcode?',
+             label=_('Display QR code (requires wide terminal)'),
+        ),
+    )
+    
+    def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
+        if entry_attrs.get('type', 'totp') == 'totp':
+            entry_attrs['objectClass'] = otp.object_class + ['ipatokentotp']
+        
+        # Resolve the user's dn
+        owner = entry_attrs.get('ipatokenowner', None)
+        if owner is not None:
+            result = self.api.Command.user_show(owner)['result']
+            owner = entry_attrs['ipatokenowner'] = result['dn']
+        
+        # Generate a random uuid
+        if not entry_attrs.get('ipatokenuniqueid', None):
+            entry_attrs['ipatokenuniqueid'] = str(uuid.uuid4())
+            dn = DN("ipatokenuniqueid=%s" % entry_attrs['ipatokenuniqueid'], dn)
+        
+        # Set a random key by default
+        if not entry_attrs.get('ipatokenotpkey', None):
+            key = random.SystemRandom().sample(range(255), KEY_LENGTH)
+            entry_attrs['ipatokenotpkey'] = "".join(map(chr, key))
+
+        # Get the issuer for the URI
+        issuer = api.env.realm
+        if owner is not None:
+            try:
+                issuer = ldap.get_entry(owner, ['krbPrincipalName'])['krbPrincipalName'][0]
+            except:
+                pass
+        
+        # Build the URI parameters
+        args = {}
+        args['issuer'] = issuer
+        args['secret'] = base64.b32encode(entry_attrs['ipatokenotpkey'])
+        args['digits'] = entry_attrs.get('ipatokenotpdigits', 6)
+        args['period'] = entry_attrs.get('ipatokentotptimestep', 30)
+        args['algorithm'] = entry_attrs.get('ipatokenotpalgorithm', 'sha1')
+        
+        # Build the URI
+        label = urllib.quote(entry_attrs['ipatokenuniqueid'])
+        parameters = urllib.urlencode(args)
+        self.uri = "otpauth://totp/%s:%s?%s" % (args['issuer'], label, parameters)
+        
+        return dn
+    
+    def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+        entry_attrs['uri'] = self.uri
+        return LDAPCreate.post_callback(self, ldap, dn, entry_attrs, *keys, **options)
+    
+    def output_for_cli(self, textui, output, *args, **options):
+        rv = LDAPCreate.output_for_cli(self, textui, output, *args, **options)
+        
+        # Print QR code to terminal if specified
+        uri = output['result'].get('uri', None)
+        if uri and options.get('qrcode', False):
+            qr = qrcode.QRCode()
+            qr.add_data(uri)
+            qr.make()
+            qr.print_tty()
+        
+        return rv
+
+@register()
+class otp_del(LDAPDelete):
+    __doc__ = _('Delete an OTP token.')
+    msg_summary = _('Deleted OTP token "%(value)s"')
+
+@register()
+class otp_mod(LDAPUpdate):
+    __doc__ = _('Modify a OTP token.')
+    msg_summary = _('Modified OTP token "%(value)s"')
+    
+    def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
+        owner = entry_attrs.get('ipatokenowner', None)
+        if owner is not None:
+            result = self.api.Command.user_show(owner)['result']
+            entry_attrs['ipatokenowner'] = result['dn']
+        return dn
+
+@register()
+class otp_find(LDAPSearch):
+    __doc__ = _('Search for OTP token.')
+    msg_summary = ngettext(
+        '%(count)d OTP token matched', '%(count)d OTP tokens matched', 0
+    )
+    
+    def pre_callback(self, ldap, filters, attrs_list, base_dn, scope, *args, **options):
+        type = options.get('type', '')
+        if type not in TOKEN_TYPES:
+            type = ''
+        filters = filters.replace("(objectclass=ipatoken)",
+                                  "(objectclass=ipatoken%s)" % type)
+        
+        owner = options.get('ipatokenowner', None)
+        if owner is not None:
+            result = self.api.Command.user_show(owner)['result']
+            filters = filters.replace("(ipatokenowner=%s)" % owner,
+                                      "(ipatokenowner=%s)" % result['dn'])
+        
+        return (filters, base_dn, scope)
+
+@register()
+class otp_show(LDAPRetrieve):
+    __doc__ = _('Display information about an OTP token.')
diff --git a/ipalib/plugins/user.py b/ipalib/plugins/user.py
index 733c9f74074e1b6eb7e79d51528bd63c5848b56b..2c68e76d1b8e570da8bed66354e545007f6a64f9 100644
--- a/ipalib/plugins/user.py
+++ b/ipalib/plugins/user.py
@@ -371,7 +371,7 @@ class user(LDAPObject):
             cli_name='user_auth_type',
             label=_('User authentication types'),
             doc=_('Types of supported user authentication'),
-            values=(u'password', u'radius'),
+            values=(u'password', u'radius', u'otp'),
             csv=True,
         ),
         Str('ipatokenradiusconfiglink?',
-- 
1.8.3.1

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

Reply via email to