On Fri, 2013-10-04 at 16:16 -0400, Nathaniel McCallum wrote:
> This patch supersedes my patch 0017 and requires patches 0020-0023. I
> believe I have solved all of the outstanding issues from the review of
> patch 0017, unless otherwise noted:
> 
> 1. I'm not actually sure what the format of the date parameters is.
> Could someone clarify this for me? Should I do something differently
> here?
> 
> 2. In this new version of the patch, we are writing default values for
> many of the token attributes. It would be nice to have some global
> defaults for these default values, but this is not currently
> implemented. I think this would make a clean secondary patch on top of
> this current patch.
> 
> 3. Dmitri brought up the idea of having tokens automatically expire by
> default. Is this a good idea? I think this dovetails nicely with #2
> above.
> 
> 4. This patch does not currently protect the deletion of the last token
> as previously discussed. Here is why I think this is still needed, but
> in the form of a DS plugin:
> 
> We need to account for a state when the user is enabled for OTP but has
> not yet configured any tokens. I believe this state should be when the
> "otp" user auth type is set, but the user has no assigned tokens. In
> this state, the user should be able to log in with single factor
> authentication.
> 
> Once the user has added tokens, however, should we allow the user to
> remove all his own tokens and return to single factor authentication? If
> yes, nothing further is needed. If no, then protection in the FreeIPA
> framework is not sufficient and this needs to be checked at the DS
> plugin level. I suspect Dmitri might answer that this needs to be a
> matter of policy.
> 
> 5. There appears to be some sort of permissions issue with users and
> adding their own tokens. I have not looked into this yet, but I will
> review this early next week. Since this is a small bug fix to an
> existing feature, I figured it was out of scope for this patch.
> 
> 6. When a user is deleted, all his tokens are deleted as well. This is
> sensible default behavior. However, in the case of hardware tokens, it
> may be more desirable to orphan these objects for future assignment to
> new users. Does anyone have any opinions on this topic?
> 
> Nathaniel

v2. Fixes API.txt and OTPTokenKey issues caused by bugs in previous
patches. Whitespace cleanup.
>From 761c579829741b9716a5e07f6a02062234e4cf64 Mon Sep 17 00:00:00 2001
From: Nathaniel McCallum <npmccal...@redhat.com>
Date: Tue, 1 Oct 2013 14:26:38 -0400
Subject: [PATCH] Add OTP support to ipalib CLI

https://fedorahosted.org/freeipa/ticket/3368
---
 API.txt                    | 101 +++++++++++++-
 VERSION                    |   2 +-
 freeipa.spec.in            |   2 +
 ipalib/errors.py           |  16 +++
 ipalib/plugins/config.py   |   2 +-
 ipalib/plugins/otptoken.py | 330 +++++++++++++++++++++++++++++++++++++++++++++
 ipalib/plugins/user.py     |  10 +-
 7 files changed, 456 insertions(+), 7 deletions(-)
 create mode 100644 ipalib/plugins/otptoken.py

diff --git a/API.txt b/API.txt
index e7e4f0b01d551cec691dfc748282866f2f3ac3e1..1dd3f877f5d8cb40c9e58b4c47f413e404131bbd 100644
--- a/API.txt
+++ b/API.txt
@@ -514,7 +514,7 @@ option: Int('ipasearchrecordslimit', attribute=True, autofill=False, cli_name='s
 option: Int('ipasearchtimelimit', attribute=True, autofill=False, cli_name='searchtimelimit', minvalue=-1, multivalue=False, required=False)
 option: Str('ipaselinuxusermapdefault', attribute=True, autofill=False, cli_name='ipaselinuxusermapdefault', multivalue=False, required=False)
 option: Str('ipaselinuxusermaporder', attribute=True, autofill=False, cli_name='ipaselinuxusermaporder', multivalue=False, required=False)
-option: StrEnum('ipauserauthtype', attribute=True, autofill=False, cli_name='user_auth_type', csv=True, multivalue=True, required=False, values=(u'password', u'radius'))
+option: StrEnum('ipauserauthtype', attribute=True, autofill=False, cli_name='user_auth_type', csv=True, multivalue=True, required=False, values=(u'password', u'radius', u'otp'))
 option: Str('ipauserobjectclasses', attribute=True, autofill=False, cli_name='userobjectclasses', csv=True, multivalue=True, required=False)
 option: IA5Str('ipausersearchfields', attribute=True, autofill=False, cli_name='usersearch', multivalue=False, required=False)
 option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
@@ -2208,6 +2208,99 @@ option: Str('version?', exclude='webui')
 output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: Output('value', <type 'unicode'>, None)
+command: otptoken_add
+args: 1,20,3
+arg: Str('ipatokenuniqueid', attribute=True, cli_name='id', multivalue=False, primary_key=True, required=False)
+option: Str('addattr*', cli_name='addattr', exclude='webui')
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('description', attribute=True, cli_name='desc', multivalue=False, required=False)
+option: Bool('ipatokendisabled', attribute=True, cli_name='disabled', multivalue=False, required=False)
+option: Str('ipatokenmodel', attribute=True, cli_name='model', multivalue=False, required=False)
+option: Str('ipatokennotafter', attribute=True, cli_name='not_after', multivalue=False, required=False)
+option: Str('ipatokennotbefore', attribute=True, cli_name='not_before', multivalue=False, required=False)
+option: StrEnum('ipatokenotpalgorithm', attribute=True, cli_name='algo', multivalue=False, required=False, values=(u'sha1', u'sha256', u'sha384', u'sha512'))
+option: IntEnum('ipatokenotpdigits', attribute=True, cli_name='digits', multivalue=False, required=False, values=(6, 8))
+option: OTPTokenKey('ipatokenotpkey', attribute=True, cli_name='key', multivalue=False, required=False)
+option: Str('ipatokenowner', attribute=True, cli_name='owner', multivalue=False, required=False)
+option: Str('ipatokenserial', attribute=True, cli_name='serial', multivalue=False, required=False)
+option: Int('ipatokentotpclockoffset', attribute=True, cli_name='offset', multivalue=False, required=False)
+option: Int('ipatokentotptimestep', attribute=True, cli_name='interval', minvalue=5, multivalue=False, required=False)
+option: Str('ipatokenvendor', attribute=True, cli_name='vendor', multivalue=False, required=False)
+option: Flag('qrcode?', autofill=True, default=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('setattr*', cli_name='setattr', exclude='webui')
+option: StrEnum('type', attribute=False, cli_name='type', multivalue=False, required=False, values=(u'totp',))
+option: Str('version?', exclude='webui')
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: Output('value', <type 'unicode'>, None)
+command: otptoken_del
+args: 1,2,3
+arg: Str('ipatokenuniqueid', attribute=True, cli_name='id', multivalue=True, primary_key=True, query=True, required=True)
+option: Flag('continue', autofill=True, cli_name='continue', default=False)
+option: Str('version?', exclude='webui')
+output: Output('result', <type 'dict'>, None)
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: Output('value', <type 'unicode'>, None)
+command: otptoken_find
+args: 1,20,4
+arg: Str('criteria?', noextrawhitespace=False)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('description', attribute=True, autofill=False, cli_name='desc', multivalue=False, query=True, required=False)
+option: Bool('ipatokendisabled', attribute=True, autofill=False, cli_name='disabled', multivalue=False, query=True, required=False)
+option: Str('ipatokenmodel', attribute=True, autofill=False, cli_name='model', multivalue=False, query=True, required=False)
+option: Str('ipatokennotafter', attribute=True, autofill=False, cli_name='not_after', multivalue=False, query=True, required=False)
+option: Str('ipatokennotbefore', attribute=True, autofill=False, cli_name='not_before', multivalue=False, query=True, required=False)
+option: StrEnum('ipatokenotpalgorithm', attribute=True, autofill=False, cli_name='algo', multivalue=False, query=True, required=False, values=(u'sha1', u'sha256', u'sha384', u'sha512'))
+option: IntEnum('ipatokenotpdigits', attribute=True, autofill=False, cli_name='digits', multivalue=False, query=True, required=False, values=(6, 8))
+option: Str('ipatokenowner', attribute=True, autofill=False, cli_name='owner', multivalue=False, query=True, required=False)
+option: Str('ipatokenserial', attribute=True, autofill=False, cli_name='serial', multivalue=False, query=True, required=False)
+option: Int('ipatokentotpclockoffset', attribute=True, autofill=False, cli_name='offset', multivalue=False, query=True, required=False)
+option: Int('ipatokentotptimestep', attribute=True, autofill=False, cli_name='interval', minvalue=5, multivalue=False, query=True, required=False)
+option: Str('ipatokenuniqueid', attribute=True, autofill=False, cli_name='id', multivalue=False, primary_key=True, query=True, required=False)
+option: Str('ipatokenvendor', attribute=True, autofill=False, cli_name='vendor', multivalue=False, query=True, required=False)
+option: Flag('pkey_only?', autofill=True, default=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Int('sizelimit?', autofill=False, minvalue=0)
+option: Int('timelimit?', autofill=False, minvalue=0)
+option: StrEnum('type', attribute=False, autofill=False, cli_name='type', multivalue=False, query=True, required=False, values=(u'totp',))
+option: Str('version?', exclude='webui')
+output: Output('count', <type 'int'>, None)
+output: ListOfEntries('result', (<type 'list'>, <type 'tuple'>), Gettext('A list of LDAP entries', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: Output('truncated', <type 'bool'>, None)
+command: otptoken_mod
+args: 1,16,3
+arg: Str('ipatokenuniqueid', attribute=True, cli_name='id', multivalue=False, primary_key=True, query=True, required=True)
+option: Str('addattr*', cli_name='addattr', exclude='webui')
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('delattr*', cli_name='delattr', exclude='webui')
+option: Str('description', attribute=True, autofill=False, cli_name='desc', multivalue=False, required=False)
+option: Bool('ipatokendisabled', attribute=True, autofill=False, cli_name='disabled', multivalue=False, required=False)
+option: Str('ipatokenmodel', attribute=True, autofill=False, cli_name='model', multivalue=False, required=False)
+option: Str('ipatokennotafter', attribute=True, autofill=False, cli_name='not_after', multivalue=False, required=False)
+option: Str('ipatokennotbefore', attribute=True, autofill=False, cli_name='not_before', multivalue=False, required=False)
+option: Str('ipatokenowner', attribute=True, autofill=False, cli_name='owner', multivalue=False, required=False)
+option: Str('ipatokenserial', attribute=True, autofill=False, cli_name='serial', multivalue=False, required=False)
+option: Str('ipatokenvendor', attribute=True, autofill=False, cli_name='vendor', multivalue=False, required=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('rename', cli_name='rename', multivalue=False, primary_key=True, required=False)
+option: Flag('rights', autofill=True, default=False)
+option: Str('setattr*', cli_name='setattr', exclude='webui')
+option: Str('version?', exclude='webui')
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: Output('value', <type 'unicode'>, None)
+command: otptoken_show
+args: 1,4,3
+arg: Str('ipatokenuniqueid', attribute=True, cli_name='id', multivalue=False, primary_key=True, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Flag('rights', autofill=True, default=False)
+option: Str('version?', exclude='webui')
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: Output('value', <type 'unicode'>, None)
 command: passwd
 args: 3,1,3
 arg: Str('principal', autofill=True, cli_name='user', primary_key=True)
@@ -3678,7 +3771,7 @@ option: Str('initials', attribute=True, autofill=True, cli_name='initials', mult
 option: Str('ipasshpubkey', attribute=True, cli_name='sshpubkey', csv=True, multivalue=True, required=False)
 option: Str('ipatokenradiusconfiglink', attribute=True, cli_name='radius', multivalue=False, required=False)
 option: Str('ipatokenradiususername', attribute=True, cli_name='radius_username', multivalue=False, required=False)
-option: StrEnum('ipauserauthtype', attribute=True, cli_name='user_auth_type', csv=True, multivalue=True, required=False, values=(u'password', u'radius'))
+option: StrEnum('ipauserauthtype', attribute=True, cli_name='user_auth_type', csv=True, multivalue=True, required=False, values=(u'password', u'radius', u'otp'))
 option: Str('krbprincipalname', attribute=True, autofill=True, cli_name='principal', multivalue=False, required=False)
 option: Str('l', attribute=True, cli_name='city', multivalue=False, required=False)
 option: Str('loginshell', attribute=True, cli_name='shell', multivalue=False, required=False)
@@ -3747,7 +3840,7 @@ option: Str('in_sudorule*', cli_name='in_sudorules', csv=True)
 option: Str('initials', attribute=True, autofill=False, cli_name='initials', multivalue=False, query=True, required=False)
 option: Str('ipatokenradiusconfiglink', attribute=True, autofill=False, cli_name='radius', multivalue=False, query=True, required=False)
 option: Str('ipatokenradiususername', attribute=True, autofill=False, cli_name='radius_username', multivalue=False, query=True, required=False)
-option: StrEnum('ipauserauthtype', attribute=True, autofill=False, cli_name='user_auth_type', csv=True, multivalue=True, query=True, required=False, values=(u'password', u'radius'))
+option: StrEnum('ipauserauthtype', attribute=True, autofill=False, cli_name='user_auth_type', csv=True, multivalue=True, query=True, required=False, values=(u'password', u'radius', u'otp'))
 option: Str('krbprincipalname', attribute=True, autofill=False, cli_name='principal', multivalue=False, query=True, required=False)
 option: Str('l', attribute=True, autofill=False, cli_name='city', multivalue=False, query=True, required=False)
 option: Str('loginshell', attribute=True, autofill=False, cli_name='shell', multivalue=False, query=True, required=False)
@@ -3800,7 +3893,7 @@ option: Str('initials', attribute=True, autofill=False, cli_name='initials', mul
 option: Str('ipasshpubkey', attribute=True, autofill=False, cli_name='sshpubkey', csv=True, multivalue=True, required=False)
 option: Str('ipatokenradiusconfiglink', attribute=True, autofill=False, cli_name='radius', multivalue=False, required=False)
 option: Str('ipatokenradiususername', attribute=True, autofill=False, cli_name='radius_username', multivalue=False, required=False)
-option: StrEnum('ipauserauthtype', attribute=True, autofill=False, cli_name='user_auth_type', csv=True, multivalue=True, required=False, values=(u'password', u'radius'))
+option: StrEnum('ipauserauthtype', attribute=True, autofill=False, cli_name='user_auth_type', csv=True, multivalue=True, required=False, values=(u'password', u'radius', u'otp'))
 option: Str('l', attribute=True, autofill=False, cli_name='city', multivalue=False, required=False)
 option: Str('loginshell', attribute=True, autofill=False, cli_name='shell', multivalue=False, required=False)
 option: Str('mail', attribute=True, autofill=False, cli_name='email', multivalue=True, required=False)
diff --git a/VERSION b/VERSION
index 0dacb97041d903733d3005045cac850e21f56d65..c036dc5677aafeae69e967876063c8cc1e2d7545 100644
--- a/VERSION
+++ b/VERSION
@@ -89,4 +89,4 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=67
+IPA_API_VERSION_MINOR=68
diff --git a/freeipa.spec.in b/freeipa.spec.in
index 5b37c4c43850d34fa9c11ef0ceae2b6340526a98..8a53f1778751aed48ea994381979678277972cbf 100644
--- a/freeipa.spec.in
+++ b/freeipa.spec.in
@@ -63,6 +63,7 @@ BuildRequires:  python-memcached
 BuildRequires:  sssd >= 1.9.2
 BuildRequires:  python-lxml
 BuildRequires:  python-pyasn1 >= 0.0.9a
+BuildRequires:  python-qrcode
 BuildRequires:  python-dns
 BuildRequires:  m2crypto
 BuildRequires:  check
@@ -126,6 +127,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/errors.py b/ipalib/errors.py
index 716decb2b41baf5470a1dc23c0cfb5d1c995e5ff..71f9d8b43ce0a2ae85710c539cfc033d2e760717 100644
--- a/ipalib/errors.py
+++ b/ipalib/errors.py
@@ -1306,6 +1306,22 @@ class PosixGroupViolation(ExecutionError):
     errno = 4030
     format = _('This is already a posix group and cannot be converted to external one')
 
+class Base32DecodeError(ExecutionError):
+    """
+    **4031** Raised when a base32-encoded blob cannot decoded
+
+    For example:
+
+    >>> raise Base32DecodeError(reason=_('Incorrect padding'))
+    Traceback (most recent call last):
+      ...
+    Base32DecodeError: Base32 decoding failed: Incorrect padding
+
+    """
+
+    errno = 4031
+    format = _('Base32 decoding failed: %(reason)s')
+
 class BuiltinError(ExecutionError):
     """
     **4100** Base class for builtin execution errors (*4100 - 4199*).
diff --git a/ipalib/plugins/config.py b/ipalib/plugins/config.py
index 6a98cd0cd75b058bf36a59544e077db59c294692..b3f5d1e18a2747f46ca129376fcba3f98e6fbbee 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/otptoken.py b/ipalib/plugins/otptoken.py
new file mode 100644
index 0000000000000000000000000000000000000000..10001994748402865e16e0c4bbfc2779058c14a1
--- /dev/null
+++ b/ipalib/plugins/otptoken.py
@@ -0,0 +1,330 @@
+# 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 DN, LDAPObject, LDAPCreate, LDAPDelete, LDAPUpdate, LDAPSearch, LDAPRetrieve
+from ipalib import api, Int, Str, Bool, Flag, Bytes, IntEnum, StrEnum, _, ngettext
+from ipalib.plugable import Registry
+from ipalib.errors import PasswordMismatch, Base32DecodeError, LastMemberError
+from ipalib.request import context
+import base64
+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
+
+class OTPTokenKey(Bytes):
+    """A binary password type specified in base32."""
+
+    password = True
+
+    kwargs = Bytes.kwargs + (
+        ('confirm', bool, True),
+    )
+
+    def _convert_scalar(self, value, index=None):
+        if isinstance(value, (tuple, list)) and len(value) == 2:
+            (p1, p2) = value
+            if p1 != p2:
+                raise PasswordMismatch(name=self.name, index=index)
+            value = p1
+
+        if isinstance(value, unicode):
+            try:
+                value = base64.b32decode(value, True)
+            except TypeError, e:
+                raise Base32DecodeError(reason=str(e))
+
+        return Bytes._convert_scalar(value, index)
+
+def _normalize_owner(userobj, entry_attrs):
+    if 'ipatokenowner' in entry_attrs:
+        entry_attrs['ipatokenowner'] = map(userobj.get_primary_key_from_dn,
+                                           entry_attrs['ipatokenowner'])
+
+@register()
+class otptoken(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=_('Unique ID'),
+            primary_key=True,
+            flags=('optional_create'),
+        ),
+        StrEnum('type?',
+            label=_('Type'),
+            values=TOKEN_TYPES,
+            flags=('virtual_attribute', 'no_update'),
+        ),
+        Str('description?',
+            cli_name='desc',
+            label=_('Description'),
+        ),
+        Str('ipatokenowner?',
+            cli_name='owner',
+            label=_('Owner'),
+        ),
+        Bool('ipatokendisabled?',
+            cli_name='disabled',
+            label=_('Disabled state')
+        ),
+        Str('ipatokennotbefore?',
+            cli_name='not_before',
+            label=_('Validity start'),
+        ),
+        Str('ipatokennotafter?',
+            cli_name='not_after',
+            label=_('Validity end'),
+        ),
+        Str('ipatokenvendor?',
+            cli_name='vendor',
+            label=_('Vendor'),
+        ),
+        Str('ipatokenmodel?',
+            cli_name='model',
+            label=_('Model'),
+        ),
+        Str('ipatokenserial?',
+            cli_name='serial',
+            label=_('Serial'),
+        ),
+        OTPTokenKey('ipatokenotpkey?',
+            cli_name='key',
+            label=_('Key'),
+            flags=('no_display', 'no_update', 'no_search'),
+        ),
+        StrEnum('ipatokenotpalgorithm?',
+            cli_name='algo',
+            label=_('Algorithm'),
+            flags=('no_update'),
+            values=(u'sha1', u'sha256', u'sha384', u'sha512'),
+        ),
+        IntEnum('ipatokenotpdigits?',
+            cli_name='digits',
+            label=_('Display length'),
+            values=(6, 8),
+            flags=('no_update'),
+        ),
+        Int('ipatokentotpclockoffset?',
+            cli_name='offset',
+            label=_('Clock offset'),
+            flags=('no_update'),
+        ),
+        Int('ipatokentotptimestep?',
+            cli_name='interval',
+            label=_('Clock interval'),
+            minvalue=5,
+            flags=('no_update'),
+        ),
+    )
+
+
+@register()
+class otptoken_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)')),
+    )
+
+    has_output_params = LDAPCreate.has_output_params + (
+        Str('uri?', label=_('URI')),
+    )
+
+    def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
+        # Set defaults. This needs to happen on the server side because we may
+        # have global configurable defaults in the near future.
+        options.setdefault('type', TOKEN_TYPES[0])
+        if entry_attrs.get('ipatokenuniqueid', None) is None:
+            entry_attrs['ipatokenuniqueid'] = str(uuid.uuid4())
+            dn = DN("ipatokenuniqueid=%s" % entry_attrs['ipatokenuniqueid'], dn)
+        entry_attrs.setdefault('ipatokenvendor', u'FreeIPA')
+        entry_attrs.setdefault('ipatokenmodel', options['type'])
+        entry_attrs.setdefault('ipatokenserial', entry_attrs['ipatokenuniqueid'])
+        entry_attrs.setdefault('ipatokenotpalgorithm', u'sha1')
+        entry_attrs.setdefault('ipatokenotpdigits', 6)
+        entry_attrs.setdefault('ipatokentotpclockoffset', 0)
+        entry_attrs.setdefault('ipatokentotptimestep', 30)
+        entry_attrs.setdefault('ipatokenotpkey',
+            "".join(map(chr, random.SystemRandom().sample(range(255), KEY_LENGTH))))
+
+        # Set the object class
+        if options['type'] == 'totp':
+            entry_attrs['objectclass'] = otptoken.object_class + ['ipatokentotp']
+
+        # Resolve the user's dn
+        owner = entry_attrs.get('ipatokenowner', None)
+        if owner is not None:
+            owner = self.api.Object.user.get_dn(owner)
+            entry_attrs['ipatokenowner'] = owner
+
+        # 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['ipatokenotpdigits']
+        args['period'] = entry_attrs['ipatokentotptimestep']
+        args['algorithm'] = entry_attrs['ipatokenotpalgorithm']
+
+        # Build the URI
+        label = urllib.quote(entry_attrs['ipatokenuniqueid'])
+        parameters = urllib.urlencode(args)
+        uri = u'otpauth://totp/%s?%s' % (label, parameters)
+        setattr(context, 'uri', uri)
+
+        return dn
+
+    def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+        entry_attrs['uri'] = getattr(context, 'uri')
+        _normalize_owner(self.api.Object.user, entry_attrs)
+        return super(otptoken_add, self).post_callback(ldap, dn, entry_attrs, *keys, **options)
+
+    def output_for_cli(self, textui, output, *args, **options):
+        uri = output['result'].get('uri', None)
+        rv = super(otptoken_add, self).output_for_cli(textui, output, *args, **options)
+
+        # Print QR code to terminal if specified
+        if uri and options.get('qrcode', False):
+            print "\n"
+            qr = qrcode.QRCode()
+            qr.add_data(uri)
+            qr.make()
+            qr.print_tty()
+            print "\n"
+
+        return rv
+
+
+@register()
+class otptoken_del(LDAPDelete):
+    __doc__ = _('Delete an OTP token.')
+    msg_summary = _('Deleted OTP token "%(value)s"')
+
+
+@register()
+class otptoken_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:
+            owner = self.api.Object.user.get_dn(owner)
+            entry_attrs['ipatokenowner'] = owner
+        return dn
+
+    def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+        _normalize_owner(self.api.Object.user, entry_attrs)
+        return super(otptoken_mod, self).post_callback(ldap, dn, entry_attrs, *keys, **options)
+
+
+@register()
+class otptoken_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, *args, **kwargs):
+        # This is a hack, but there is no other way to
+        # replace the objectClass when searching
+        type = kwargs.get('type', '')
+        if type not in TOKEN_TYPES:
+            type = ''
+        filters = filters.replace("(objectclass=ipatoken)",
+                                  "(objectclass=ipatoken%s)" % type)
+
+        return super(otptoken_find, self).pre_callback(ldap, filters, *args, **kwargs)
+
+    def args_options_2_entry(self, *args, **options):
+        o = 'ipatokenowner'
+        if o in options:
+            options[o] = self.api.Object.user.get_dn(options[o])
+
+        return super(otptoken_find, self).args_options_2_entry(*args, **options)
+
+    def post_callback(self, ldap, entries, truncated, *args, **options):
+        for entry in entries:
+            _normalize_owner(self.api.Object.user, entry)
+        return super(otptoken_find, self).post_callback(ldap, entries, truncated, *args, **options)
+
+
+@register()
+class otptoken_show(LDAPRetrieve):
+    __doc__ = _('Display information about an OTP token.')
+
+    def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+        _normalize_owner(self.api.Object.user, entry_attrs)
+        return super(otptoken_show, self).post_callback(ldap, dn, entry_attrs, *keys, **options)
diff --git a/ipalib/plugins/user.py b/ipalib/plugins/user.py
index 845dd663fc05c3145afa94e483ad02d953a92f4d..9bb22b601426a461b9d60c39eaa78320bde1f7bb 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?',
@@ -630,6 +630,14 @@ class user_del(LDAPDelete):
     def pre_callback(self, ldap, dn, *keys, **options):
         assert isinstance(dn, DN)
         check_protected_member(keys[-1])
+
+        # Delete all tokens owned by this user
+        owner = self.api.Object.user.get_primary_key_from_dn(dn)
+        results = self.api.Command.otptoken_find(ipatokenowner=owner)['result']
+        for token in results:
+            token = self.api.Object.otptoken.get_primary_key_from_dn(token['dn'])
+            self.api.Command.otptoken_del(token)
+
         return dn
 
 api.register(user_del)
-- 
1.8.3.1

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

Reply via email to