On Wed, 2013-12-11 at 13:24 +0100, Jan Cholasta wrote: > On 14.11.2013 20:23, Nathaniel McCallum wrote: > > On Wed, 2013-10-30 at 08:57 +0100, Jan Cholasta wrote: > >> On 8.10.2013 16:35, Nathaniel McCallum wrote: > >>> On Tue, 2013-10-08 at 09:19 +0200, Jan Cholasta wrote: > >>>> > >>>> +class Base32DecodeError(ExecutionError): > >>>> > >>>> Is this really necessary? Are we going to add <encoding>DecodeError for > >>>> every kind of new encoding in IPA? Can't we just have generic > >>>> DecodeError? (This is not an issue in your patch per se, I'm just > >>>> wondering if we can do it better in the framework.) > >>> > >>> I added the new error due to the existence of a Base64DecodeError. I > >>> figured changing the existing error to be more generic would break api. > >>> > >>> Nathaniel > >>> > >> > >> I think you can use ConversionError instead. I don't see any reason why > >> base32/64 decoding errors should be special cased like this and would > >> like to see Base64DecodeError gone. > > > > Fixed. > > > > > > Thanks. > > format = _("invalid '%(name)s': %(error)s") > > - > class ValidationError(InvocationError): > """ > **3009** Raised when a parameter value fails a validation rule. > @@ -1306,6 +1305,7 @@ class PosixGroupViolation(ExecutionError): > errno = 4030 > format = _('This is already a posix group and cannot be converted > to external one') > > + > class BuiltinError(ExecutionError): > > This white space shuffling is not necessary.
Fixed. I missed this, thanks! > +def _normalize_owner(userobj, entry_attrs): > + if 'ipatokenowner' in entry_attrs: > + entry_attrs['ipatokenowner'] = map(userobj.get_primary_key_from_dn, > + entry_attrs['ipatokenowner']) > > If the --raw option is specified, ipatokenowner value should be full DN. Fixed. > + # 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 > > You have a _normalize_owner function, I think the code above should go > into a _convert_owner function (use the function in > otptoken_{mod,show,find} as well). Fixed for mod and find. Show doesn't make sense. > + # 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 > > Please use "except PublicError" here, we don't want internal errors to > be ignored. Fixed: (NotFound, IndexError) > + # 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) > > This should probably be handled by the referint plugin. See my reply to Martin. Nathaniel
>From 1b843357abfea38c118218636af307addece10bb 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 | 107 ++++++++++++++- VERSION | 2 +- freeipa.spec.in | 2 + ipalib/plugins/config.py | 2 +- ipalib/plugins/otptoken.py | 334 +++++++++++++++++++++++++++++++++++++++++++++ ipalib/plugins/user.py | 10 +- 6 files changed, 447 insertions(+), 10 deletions(-) create mode 100644 ipalib/plugins/otptoken.py diff --git a/API.txt b/API.txt index cc0c54405c46b6b0689b49ffe7353a555119dc0c..14e2d96ef1673913870b27ed346e1baa8ae988d8 100644 --- a/API.txt +++ b/API.txt @@ -524,7 +524,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') @@ -2219,6 +2219,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) @@ -2237,7 +2330,7 @@ option: Str('attrs', attribute=False, cli_name='attrs', multivalue=True, require option: Str('filter', attribute=False, cli_name='filter', multivalue=True, required=False) option: Str('ipapermallowedattr', attribute=True, cli_name='attrs', multivalue=True, required=False) option: StrEnum('ipapermbindruletype', attribute=True, autofill=True, cli_name='bindtype', default=u'permission', multivalue=False, required=True, values=(u'permission',)) -option: DNOrURL('ipapermlocation', alwaysask=True, attribute=True, autofill=False, cli_name='subtree', default=ipapython.dn.DN('dc=idm,dc=lab,dc=eng,dc=brq,dc=redhat,dc=com'), multivalue=False, query=False, required=False) +option: DNOrURL('ipapermlocation', alwaysask=True, attribute=True, autofill=False, cli_name='subtree', default=ipapython.dn.DN('dc=example,dc=com'), multivalue=False, query=False, required=False) option: StrEnum('ipapermright', attribute=True, cli_name='permissions', multivalue=True, required=False, values=(u'read', u'search', u'compare', u'write', u'add', u'delete', u'all')) option: DNParam('ipapermtarget', attribute=True, cli_name='target', multivalue=False, required=False) option: Str('ipapermtargetfilter', attribute=True, cli_name='filter', multivalue=False, required=False) @@ -2293,7 +2386,7 @@ option: Str('cn', attribute=True, autofill=False, cli_name='name', multivalue=Fa option: Str('filter', attribute=False, autofill=False, cli_name='filter', multivalue=True, query=True, required=False) option: Str('ipapermallowedattr', attribute=True, autofill=False, cli_name='attrs', multivalue=True, query=True, required=False) option: StrEnum('ipapermbindruletype', attribute=True, autofill=False, cli_name='bindtype', default=u'permission', multivalue=False, query=True, required=False, values=(u'permission',)) -option: DNOrURL('ipapermlocation', attribute=True, autofill=False, cli_name='subtree', default=ipapython.dn.DN('dc=idm,dc=lab,dc=eng,dc=brq,dc=redhat,dc=com'), multivalue=False, query=True, required=False) +option: DNOrURL('ipapermlocation', attribute=True, autofill=False, cli_name='subtree', default=ipapython.dn.DN('dc=example,dc=com'), multivalue=False, query=True, required=False) option: StrEnum('ipapermright', attribute=True, autofill=False, cli_name='permissions', multivalue=True, query=True, required=False, values=(u'read', u'search', u'compare', u'write', u'add', u'delete', u'all')) option: DNParam('ipapermtarget', attribute=True, autofill=False, cli_name='target', multivalue=False, query=True, required=False) option: Str('ipapermtargetfilter', attribute=True, autofill=False, cli_name='filter', multivalue=False, query=True, required=False) @@ -2322,7 +2415,7 @@ option: Str('delattr*', cli_name='delattr', exclude='webui') option: Str('filter', attribute=False, autofill=False, cli_name='filter', multivalue=True, required=False) option: Str('ipapermallowedattr', attribute=True, autofill=False, cli_name='attrs', multivalue=True, required=False) option: StrEnum('ipapermbindruletype', attribute=True, autofill=False, cli_name='bindtype', default=u'permission', multivalue=False, required=False, values=(u'permission',)) -option: DNOrURL('ipapermlocation', attribute=True, autofill=False, cli_name='subtree', default=ipapython.dn.DN('dc=idm,dc=lab,dc=eng,dc=brq,dc=redhat,dc=com'), multivalue=False, required=False) +option: DNOrURL('ipapermlocation', attribute=True, autofill=False, cli_name='subtree', default=ipapython.dn.DN('dc=example,dc=com'), multivalue=False, required=False) option: StrEnum('ipapermright', attribute=True, autofill=False, cli_name='permissions', multivalue=True, required=False, values=(u'read', u'search', u'compare', u'write', u'add', u'delete', u'all')) option: DNParam('ipapermtarget', attribute=True, autofill=False, cli_name='target', multivalue=False, required=False) option: Str('ipapermtargetfilter', attribute=True, autofill=False, cli_name='filter', multivalue=False, required=False) @@ -3707,7 +3800,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) @@ -3777,7 +3870,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) @@ -3831,7 +3924,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 6ead76c68b5973496811604ac1793e7539432009..5ce16b5224fd95910a221e251b2d740318bded95 100644 --- a/VERSION +++ b/VERSION @@ -89,4 +89,4 @@ IPA_DATA_VERSION=20100614120000 # # ######################################################## IPA_API_VERSION_MAJOR=2 -IPA_API_VERSION_MINOR=71 +IPA_API_VERSION_MINOR=72 diff --git a/freeipa.spec.in b/freeipa.spec.in index 80df44daaafd2ec28b18fb1bb606c0112c44e70c..df68be0a195fc435d7d9ed812220aabc2246103c 100644 --- a/freeipa.spec.in +++ b/freeipa.spec.in @@ -67,6 +67,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 @@ -130,6 +131,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/plugins/config.py b/ipalib/plugins/config.py index e20e5e8016748f063e1a6240a250e1c27986c5cc..e38254cd397019d5109cf2ce7a824173023fdf7f 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..2d7ac12a3d3cdcf47aedab0e2c01bdc44fd813f7 --- /dev/null +++ b/ipalib/plugins/otptoken.py @@ -0,0 +1,334 @@ +# 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, ConversionError, LastMemberError, NotFound +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 ConversionError(name=self.name, index=index, error=str(e)) + + return Bytes._convert_scalar(value, index) + +def _normalize_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, + entry_attrs['ipatokenowner']) + +def _convert_owner(userobj, entry_attrs): + owner = entry_attrs.get('ipatokenowner', None) + if owner is not None: + entry_attrs['ipatokenowner'] = userobj.get_dn(owner) + + +@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 (NotFound, IndexError): + 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?%s' % (issuer, 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): + _convert_owner(self.api.Object.user, entry_attrs) + 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) + + _convert_owner(self.api.Object.user, entry_attrs) + 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 ae927b642c607165549f1882d1b37b29df2b7d9f..3c8353ffaac8cde6164ad03afde2fc85b99524f2 100644 --- a/ipalib/plugins/user.py +++ b/ipalib/plugins/user.py @@ -379,7 +379,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('userclass*', @@ -648,6 +648,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.4.2
_______________________________________________ Freeipa-devel mailing list Freeipa-devel@redhat.com https://www.redhat.com/mailman/listinfo/freeipa-devel