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
>From 6dc9d669542110ad16786b767d8c457b2670dff6 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 | 332 +++++++++++++++++++++++++++++++++++++++++++++
 ipalib/plugins/user.py     |  10 +-
 7 files changed, 458 insertions(+), 7 deletions(-)
 create mode 100644 ipalib/plugins/otptoken.py

diff --git a/API.txt b/API.txt
index 6d5d1a191a52f0b748720c607e4a65d735394b48..79f2a4342e77c315315d64c3d9c11bb2935ea2ff 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', multivalue=True, required=False)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui', multivalue=False, required=True)
+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, cli_name='qrcode', default=False, multivalue=False, required=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui', multivalue=False, required=True)
+option: Str('setattr', cli_name='setattr', exclude='webui', multivalue=True, required=False)
+option: StrEnum('type', attribute=False, cli_name='type', multivalue=False, required=False, values=(u'totp',))
+option: Str('version', cli_name='version', exclude='webui', multivalue=False, required=False)
+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)
@@ -3590,7 +3683,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)
@@ -3659,7 +3752,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)
@@ -3712,7 +3805,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 cb2a818b25c7c353a3ccdd00010ee0539d972cb5..cb619df7ab10f07c2b18d1cc50415359b0b10cb9 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..3ca99fb412f8926767f57fa19f4e99eba4cc72dc 100644
--- a/ipalib/errors.py
+++ b/ipalib/errors.py
@@ -1305,6 +1305,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):
     """
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..c2beac594509fdeeff2957b6d1db171e39684fc1
--- /dev/null
+++ b/ipalib/plugins/otptoken.py
@@ -0,0 +1,332 @@
+# 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."""
+
+    kwargs = Bytes.kwargs + (
+        ('confirm', bool, True),
+    )
+
+    def __init__(self, name, *rules, **kw):
+        self.password = True
+        super(OTPTokenKey, self).__init__(name, *rules, **kw)
+
+    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