From 397b2876e2f9bf1c5b3ad3e2874a92715ccda599 Mon Sep 17 00:00:00 2001 From: Nathaniel McCallum Date: Tue, 1 Oct 2013 14:26:38 -0400 Subject: Add OTP support to ipalib CLI https://fedorahosted.org/freeipa/ticket/3368 --- API.txt | 101 +++++++++++++- VERSION | 2 +- freeipa.spec.in | 2 + ipalib/plugins/config.py | 2 +- ipalib/plugins/otptoken.py | 329 +++++++++++++++++++++++++++++++++++++++++++++ ipalib/plugins/user.py | 10 +- 6 files changed, 439 insertions(+), 7 deletions(-) create mode 100644 ipalib/plugins/otptoken.py diff --git a/API.txt b/API.txt index 342d7ed6..3bb1b76a 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', , Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None)) output: Output('summary', (, ), None) output: Output('value', , 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', , Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None)) +output: Output('summary', (, ), None) +output: Output('value', , 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', , None) +output: Output('summary', (, ), None) +output: Output('value', , 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', , None) +output: ListOfEntries('result', (, ), Gettext('A list of LDAP entries', domain='ipa', localedir=None)) +output: Output('summary', (, ), None) +output: Output('truncated', , 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', , Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None)) +output: Output('summary', (, ), None) +output: Output('value', , 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', , Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None)) +output: Output('summary', (, ), None) +output: Output('value', , None) command: passwd args: 3,1,3 arg: Str('principal', autofill=True, cli_name='user', primary_key=True) @@ -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 6ead76c6..5ce16b52 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 80df44da..df68be0a 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 e20e5e80..e38254cd 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 00000000..67f24859 --- /dev/null +++ b/ipalib/plugins/otptoken.py @@ -0,0 +1,329 @@ +# Authors: +# Nathaniel McCallum +# +# 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 . + +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 _convert_owner(userobj, entry_attrs, options): + if 'ipatokenowner' in entry_attrs and not options.get('raw', False): + entry_attrs['ipatokenowner'] = map(userobj.get_primary_key_from_dn, + entry_attrs['ipatokenowner']) + +def _normalize_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 + _normalize_owner(self.api.Object.user, entry_attrs) + + # Get the issuer for the URI + owner = entry_attrs.get('ipatokenowner', None) + 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') + _convert_owner(self.api.Object.user, entry_attrs, options) + 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): + _normalize_owner(self.api.Object.user, entry_attrs) + return dn + + def post_callback(self, ldap, dn, entry_attrs, *keys, **options): + _convert_owner(self.api.Object.user, entry_attrs, options) + 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): + entry = super(otptoken_find, self).args_options_2_entry(*args, **options) + _normalize_owner(self.api.Object.user, entry) + return entry + + def post_callback(self, ldap, entries, truncated, *args, **options): + for entry in entries: + _convert_owner(self.api.Object.user, entry, options) + 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): + _convert_owner(self.api.Object.user, entry_attrs, options) + 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 ae927b64..3c8353ff 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) -- cgit