On Wed, 2013-11-27 at 12:28 +0100, Petr Viktorin wrote:
> >> ipatokenradiusserver is not validated. See validate_searchtimelimit in
> >> the config plugin for an example validator. You can use validate_ipaddr
> >> and validate_hostname from ipalib.util.
> >
> > Fixed.
> 
> Now the validation is too strict, a port is not accepted.

Fixed.

> Should non-FQDN hostnames be allowed?

I agree they should not. Fixed.

> >> ipatokenusermapattribute is also not validated. Not sure if it needs to be.
> >
> > I don't think validation is really possible outside of the permitted
> > characters for an LDAP attribute.
> 
> I think if "$%^&*" is allowed, we'll get a bug from QA soon enough.

Fixed.

> We generally output lists; this should also be a list with one element.

Fixed.

> Attaching updated tests.

A few of these tests are still failing for me, but it is not immediately
obvious why. They seem to be getting answers from previous queries. I'm
not sure if this is something wrong with my code or the tests. Can you
take a look at it?

Nathaniel
>From beb846295d68d2c5ce958bcf08790279761aaf58 Mon Sep 17 00:00:00 2001
From: Nathaniel McCallum <npmccal...@redhat.com>
Date: Mon, 11 Nov 2013 17:58:02 -0400
Subject: [PATCH] Add RADIUS proxy support to ipalib CLI

https://fedorahosted.org/freeipa/ticket/3368
---
 API.txt                            |  95 +++++++++++++++++++--
 VERSION                            |   2 +-
 install/share/70ipaotp.ldif        |   2 +-
 install/updates/20-indices.update  |   7 ++
 install/updates/25-referint.update |   1 +
 install/updates/40-otp.update      |   5 ++
 ipalib/constants.py                |   1 +
 ipalib/plugins/config.py           |   2 +-
 ipalib/plugins/radiusproxy.py      | 166 +++++++++++++++++++++++++++++++++++++
 ipalib/plugins/user.py             |  65 +++++++++++++--
 10 files changed, 328 insertions(+), 18 deletions(-)
 create mode 100644 ipalib/plugins/radiusproxy.py

diff --git a/API.txt b/API.txt
index c29efad3382ff59e4753eff5354cba72bc1fe027..328fcf76ac6c8a8f5349e9f5631ec61bae7b3ea4 100644
--- a/API.txt
+++ b/API.txt
@@ -523,7 +523,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',))
+option: StrEnum('ipauserauthtype', attribute=True, autofill=False, cli_name='user_auth_type', csv=True, multivalue=True, required=False, values=(u'password', u'radius'))
 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')
@@ -2551,6 +2551,81 @@ 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: radiusproxy_add
+args: 1,11,3
+arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=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('description', attribute=True, cli_name='desc', multivalue=False, required=False)
+option: Int('ipatokenradiusretries', attribute=True, cli_name='retries', maxvalue=10, minvalue=0, multivalue=False, required=False)
+option: Password('ipatokenradiussecret', attribute=True, cli_name='secret', confirm=True, multivalue=False, required=True)
+option: Str('ipatokenradiusserver', attribute=True, cli_name='server', multivalue=True, required=True)
+option: Int('ipatokenradiustimeout', attribute=True, cli_name='timeout', minvalue=1, multivalue=False, required=False)
+option: Str('ipatokenusermapattribute', attribute=True, cli_name='userattr', multivalue=False, required=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+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: radiusproxy_del
+args: 1,2,3
+arg: Str('cn', attribute=True, cli_name='name', 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: radiusproxy_find
+args: 1,13,4
+arg: Str('criteria?', noextrawhitespace=False)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('cn', attribute=True, autofill=False, cli_name='name', multivalue=False, primary_key=True, query=True, required=False)
+option: Str('description', attribute=True, autofill=False, cli_name='desc', multivalue=False, query=True, required=False)
+option: Int('ipatokenradiusretries', attribute=True, autofill=False, cli_name='retries', maxvalue=10, minvalue=0, multivalue=False, query=True, required=False)
+option: Password('ipatokenradiussecret', attribute=True, autofill=False, cli_name='secret', confirm=True, multivalue=False, query=True, required=False)
+option: Str('ipatokenradiusserver', attribute=True, autofill=False, cli_name='server', multivalue=True, query=True, required=False)
+option: Int('ipatokenradiustimeout', attribute=True, autofill=False, cli_name='timeout', minvalue=1, multivalue=False, query=True, required=False)
+option: Str('ipatokenusermapattribute', attribute=True, autofill=False, cli_name='userattr', 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: 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: radiusproxy_mod
+args: 1,14,3
+arg: Str('cn', attribute=True, cli_name='name', 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: Int('ipatokenradiusretries', attribute=True, autofill=False, cli_name='retries', maxvalue=10, minvalue=0, multivalue=False, required=False)
+option: Password('ipatokenradiussecret', attribute=True, autofill=False, cli_name='secret', confirm=True, multivalue=False, required=False)
+option: Str('ipatokenradiusserver', attribute=True, autofill=False, cli_name='server', multivalue=True, required=False)
+option: Int('ipatokenradiustimeout', attribute=True, autofill=False, cli_name='timeout', minvalue=1, multivalue=False, required=False)
+option: Str('ipatokenusermapattribute', attribute=True, autofill=False, cli_name='userattr', 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: radiusproxy_show
+args: 1,4,3
+arg: Str('cn', attribute=True, cli_name='name', 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: realmdomains_mod
 args: 0,11,3
 option: Str('add_domain', attribute=True, autofill=False, cli_name='add_domain', multivalue=False, required=False)
@@ -3596,7 +3671,7 @@ output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDA
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: Output('value', <type 'unicode'>, None)
 command: user_add
-args: 1,37,3
+args: 1,39,3
 arg: Str('uid', attribute=True, cli_name='login', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?$', primary_key=True, required=True)
 option: Str('addattr*', cli_name='addattr', exclude='webui')
 option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
@@ -3610,7 +3685,9 @@ option: Str('givenname', attribute=True, cli_name='first', multivalue=False, req
 option: Str('homedirectory', attribute=True, cli_name='homedir', multivalue=False, required=False)
 option: Str('initials', attribute=True, autofill=True, cli_name='initials', multivalue=False, required=False)
 option: Str('ipasshpubkey', attribute=True, cli_name='sshpubkey', csv=True, multivalue=True, required=False)
-option: StrEnum('ipauserauthtype', attribute=True, cli_name='user_auth_type', csv=True, multivalue=True, required=False, values=(u'password',))
+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: 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)
@@ -3661,7 +3738,7 @@ output: Output('result', <type 'bool'>, None)
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: Output('value', <type 'unicode'>, None)
 command: user_find
-args: 1,47,4
+args: 1,49,4
 arg: Str('criteria?', noextrawhitespace=False)
 option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
 option: Str('carlicense', attribute=True, autofill=False, cli_name='carlicense', multivalue=False, query=True, required=False)
@@ -3678,7 +3755,9 @@ option: Str('in_netgroup*', cli_name='in_netgroups', csv=True)
 option: Str('in_role*', cli_name='in_roles', csv=True)
 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: StrEnum('ipauserauthtype', attribute=True, autofill=False, cli_name='user_auth_type', csv=True, multivalue=True, query=True, required=False, values=(u'password',))
+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: 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)
@@ -3715,7 +3794,7 @@ output: ListOfEntries('result', (<type 'list'>, <type 'tuple'>), Gettext('A list
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: Output('truncated', <type 'bool'>, None)
 command: user_mod
-args: 1,38,3
+args: 1,40,3
 arg: Str('uid', attribute=True, cli_name='login', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?$', 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')
@@ -3730,7 +3809,9 @@ option: Str('givenname', attribute=True, autofill=False, cli_name='first', multi
 option: Str('homedirectory', attribute=True, autofill=False, cli_name='homedir', multivalue=False, required=False)
 option: Str('initials', attribute=True, autofill=False, cli_name='initials', multivalue=False, required=False)
 option: Str('ipasshpubkey', attribute=True, autofill=False, cli_name='sshpubkey', csv=True, multivalue=True, required=False)
-option: StrEnum('ipauserauthtype', attribute=True, autofill=False, cli_name='user_auth_type', csv=True, multivalue=True, required=False, values=(u'password',))
+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: 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 c036dc5677aafeae69e967876063c8cc1e2d7545..dc029a286481890ba663e19059acd65cecfa63b8 100644
--- a/VERSION
+++ b/VERSION
@@ -89,4 +89,4 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=68
+IPA_API_VERSION_MINOR=69
diff --git a/install/share/70ipaotp.ldif b/install/share/70ipaotp.ldif
index 824be6e9d1d284e966f05d0ff10f82d0f5cac4af..d257a46c38d2e776147e6c2a5c997a33cd100ef1 100644
--- a/install/share/70ipaotp.ldif
+++ b/install/share/70ipaotp.ldif
@@ -24,5 +24,5 @@ attributeTypes: (2.16.840.1.113730.3.8.16.1.19 NAME 'ipatokenRadiusRetries' DESC
 attributeTypes: (2.16.840.1.113730.3.8.16.1.20 NAME 'ipatokenUserMapAttribute' DESC 'Attribute to map from the user entry for RADIUS server authentication' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'IPA OTP')
 objectClasses:  (2.16.840.1.113730.3.8.16.2.1  NAME 'ipaToken' SUP top ABSTRACT DESC 'Abstract token class for tokens' MUST (ipatokenUniqueID) MAY (description $ ipatokenOwner $ ipatokenDisabled $ ipatokenNotBefore $ ipatokenNotAfter $ ipatokenVendor $ ipatokenModel $ ipatokenSerial) X-ORIGIN 'IPA OTP')
 objectClasses:  (2.16.840.1.113730.3.8.16.2.2  NAME 'ipatokenTOTP' SUP ipaToken STRUCTURAL DESC 'TOTP Token Type' MAY (ipatokenOTPkey $ ipatokenOTPalgorithm $ ipatokenOTPdigits $ ipatokenTOTPclockOffset $ ipatokenTOTPtimeStep) X-ORIGIN 'IPA OTP')
-objectClasses:  (2.16.840.1.113730.3.8.16.2.3  NAME 'ipatokenRadiusProxyUser' SUP top AUXILIARY DESC 'Radius Proxy User' MUST (ipatokenRadiusConfigLink) MAY (ipatokenRadiusUserName) X-ORIGIN 'IPA OTP')
+objectClasses:  (2.16.840.1.113730.3.8.16.2.3  NAME 'ipatokenRadiusProxyUser' SUP top AUXILIARY DESC 'Radius Proxy User' MAY (ipatokenRadiusConfigLink $ ipatokenRadiusUserName) X-ORIGIN 'IPA OTP')
 objectClasses:  (2.16.840.1.113730.3.8.16.2.4  NAME 'ipatokenRadiusConfiguration' SUP top STRUCTURAL DESC 'Proxy Radius Configuration' MUST (cn $ ipatokenRadiusServer $ ipatokenRadiusSecret) MAY (description $ ipatokenRadiusTimeout $ ipatokenRadiusRetries $ ipatokenUserMapAttribute) X-ORIGIN 'IPA OTP')
diff --git a/install/updates/20-indices.update b/install/updates/20-indices.update
index b966a4f7ca5d8d9fc489d94a2e09632fe562a580..5ff6d713d4290d0548751e7ee0d3efa7218c7aae 100644
--- a/install/updates/20-indices.update
+++ b/install/updates/20-indices.update
@@ -136,3 +136,10 @@ default:ObjectClass: top
 default:ObjectClass: nsIndex
 default:nsSystemIndex: false
 default:nsIndexType: eq
+
+dn: cn=ipatokenradiusconfiglink,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
+default:cn: ipatokenradiusconfiglink
+default:ObjectClass: top
+default:ObjectClass: nsIndex
+default:nsSystemIndex: false
+only:nsIndexType: eq,pres,sub
diff --git a/install/updates/25-referint.update b/install/updates/25-referint.update
index 54f3492fae38dbc07c081678f957aaa86152294f..65af05128e433d683d61272cad6145fa8f084b04 100644
--- a/install/updates/25-referint.update
+++ b/install/updates/25-referint.update
@@ -11,3 +11,4 @@ add: nsslapd-pluginArg14: memberallowcmd
 add: nsslapd-pluginArg15: memberdenycmd
 add: nsslapd-pluginArg16: ipasudorunas
 add: nsslapd-pluginArg17: ipasudorunasgroup
+add: nsslapd-pluginArg18: ipatokenradiusconfiglink
diff --git a/install/updates/40-otp.update b/install/updates/40-otp.update
index ff36c87a60c071efc3e2aaee59747635a2477740..83dfab7c03bfeb121756410d79143c3812146de6 100644
--- a/install/updates/40-otp.update
+++ b/install/updates/40-otp.update
@@ -7,3 +7,8 @@ dn: $SUFFIX
 add: aci:'(targetfilter = "(objectClass=ipaToken)")(targetattrs = "objectclass || ipatokenUniqueID || description || ipatokenOwner || ipatokenNotBefore || ipatokenNotAfter || ipatokenVendor || ipatokenModel || ipatokenSerial")(version 3.0; acl "Users can read basic token info"; allow (read, search, compare) userattr = "ipatokenOwner#USERDN";)'
 add: aci:'(targetfilter = "(objectClass=ipaToken)")(targetattrs = "ipatokenUniqueID || description || ipatokenOwner || ipatokenNotBefore || ipatokenNotAfter || ipatokenVendor || ipatokenModel || ipatokenSerial")(version 3.0; acl "Users can write basic token info"; allow (write) userattr = "ipatokenOwner#USERDN";)'
 add: aci:'(targetfilter = "(objectClass=ipatokenTOTP)")(targetattrs = "ipatokenOTPkey || ipatokenOTPalgorithm || ipatokenOTPdigits || ipatokenTOTPclockOffset || ipatokenTOTPtimeStep")(version 3.0; acl "Users can add TOTP token secrets"; allow (write, search) userattr = "ipatokenOwner#USERDN";)'
+
+dn: cn=radiusproxy,$SUFFIX
+default: objectClass: nsContainer
+default: objectClass: top
+default: cn: radiusproxy
diff --git a/ipalib/constants.py b/ipalib/constants.py
index 79885a33a3008bd83908fc34a7340e78ab25e31f..d15babb2fe742dff7ba1f015a8cda406a104cb34 100644
--- a/ipalib/constants.py
+++ b/ipalib/constants.py
@@ -109,6 +109,7 @@ DEFAULT_CONFIG = (
     ('container_dna_posix_ids', DN(('cn', 'posix-ids'), ('cn', 'dna'), ('cn', 'ipa'), ('cn', 'etc'))),
     ('container_realm_domains', DN(('cn', 'Realm Domains'), ('cn', 'ipa'), ('cn', 'etc'))),
     ('container_otp', DN(('cn', 'otp'))),
+    ('container_radiusproxy', DN(('cn', 'radiusproxy'))),
 
     # Ports, hosts, and URIs:
     # FIXME: let's renamed xmlrpc_uri to rpc_xml_uri
diff --git a/ipalib/plugins/config.py b/ipalib/plugins/config.py
index f4e35519f00112c5f3c878b67ebad1c0ab77d01f..e20e5e8016748f063e1a6240a250e1c27986c5cc 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',),
+            values=(u'password', u'radius'),
             csv=True,
         ),
     )
diff --git a/ipalib/plugins/radiusproxy.py b/ipalib/plugins/radiusproxy.py
new file mode 100644
index 0000000000000000000000000000000000000000..ef6e5b4d1085b191b241364e025ebc51fcd633f2
--- /dev/null
+++ b/ipalib/plugins/radiusproxy.py
@@ -0,0 +1,166 @@
+# Authors:
+#   Nathaniel McCallum <npmccal...@redhat.com>
+#
+# Copyright (C) 2013  Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from ipalib.plugins.baseldap import *
+from ipalib import api, Str, Int, Password, _, ngettext
+from ipalib import Command
+from ipalib.plugins import privilege
+from ipalib.plugable import Registry
+from ipalib.util import validate_hostname, validate_ipaddr
+from ipalib.errors import ValidationError
+import sre
+
+__doc__ = _("""
+RADIUS Proxy Servers
+
+Manage RADIUS Proxy Servers.
+
+IPA supports the use of an external RADIUS proxy server for krb5 OTP
+authentications. This permits a great deal of flexibility when
+integrating with third-party authentication services.
+
+EXAMPLES:
+
+ Add a new server:
+   ipa radiusproxy-add MyRADIUS --server=radius.example.com:1812
+
+ Find all servers whose entries include the string "example.com":
+   ipa radiusproxy-find example.com
+
+ Examine the configuration:
+   ipa radiusproxy-show MyRADIUS
+
+ Change the secret:
+   ipa radiusproxy-mod MyRADIUS --secret
+
+ Delete a configuration:
+   ipa radiusproxy-del MyRADIUS
+""")
+
+register = Registry()
+
+LDAP_ATTRIBUTE = sre.compile("^[a-zA-Z][a-zA-Z0-9-]*$")
+def validate_attributename(ugettext, attr):
+    if not LDAP_ATTRIBUTE.match(attr):
+        raise ValidationError(name="userattr", error=_('invalid!'))
+
+def validate_radiusserver(ugettext, server):
+    split = server.rsplit(':', 1)
+    server = split[0]
+    if len(split) == 2:
+        try:
+            port = int(split[1])
+            if (port < 0 or port > 65535):
+                raise ValueError()
+        except ValueError:
+            raise ValidationError(name="server", error=_('invalid!'))
+
+    if not '.' in server.strip('.'):
+        raise ValidationError(name="server", error=_('invalid!'))
+
+    if validate_ipaddr(server):
+        return
+
+    validate_hostname(server, False, True)
+
+@register()
+class radiusproxy(LDAPObject):
+    """
+    RADIUS Server object.
+    """
+    container_dn = api.env.container_radiusproxy
+    object_name = _('RADIUS proxy server')
+    object_name_plural = _('RADIUS proxy servers')
+    object_class = ['ipatokenradiusconfiguration']
+    default_attributes = ['cn', 'description', 'ipatokenradiusserver',
+        'ipatokenradiustimeout', 'ipatokenradiusretries', 'ipatokenusermapattribute'
+    ]
+    search_attributes = ['cn', 'description', 'ipatokenradiusserver']
+    rdn_is_primary_key = True
+    label = _('RADIUS Servers')
+    label_singular = _('RADIUS Server')
+
+    takes_params = (
+        Str('cn',
+            cli_name='name',
+            label=_('RADIUS proxy server name'),
+            primary_key=True,
+        ),
+        Str('description?',
+            cli_name='desc',
+            label=_('Description'),
+            doc=_('A description of this RADIUS proxy server'),
+        ),
+        Str('ipatokenradiusserver+', validate_radiusserver,
+            cli_name='server',
+            label=_('Server'),
+            doc=_('The hostname or IP (with or without port)'),
+        ),
+        Password('ipatokenradiussecret',
+            cli_name='secret',
+            label=_('Secret'),
+            doc=_('The secret used to encrypt data'),
+            confirm=True,
+            flags=['no_option'],
+        ),
+        Int('ipatokenradiustimeout?',
+            cli_name='timeout',
+            label=_('Timeout'),
+            doc=_('The total timeout across all retries (in seconds)'),
+            minvalue=1,
+        ),
+        Int('ipatokenradiusretries?',
+            cli_name='retries',
+            label=_('Retries'),
+            doc=_('The number of times to retry authentication'),
+            minvalue=0,
+            maxvalue=10,
+        ),
+        Str('ipatokenusermapattribute?', validate_attributename,
+            cli_name='userattr',
+            label=_('User attribute'),
+            doc=_('The username attribute on the user object'),
+        ),
+    )
+
+@register()
+class radiusproxy_add(LDAPCreate):
+    __doc__ = _('Add a new RADIUS proxy server.')
+    msg_summary = _('Added RADIUS proxy server "%(value)s"')
+
+@register()
+class radiusproxy_del(LDAPDelete):
+    __doc__ = _('Delete a RADIUS proxy server.')
+    msg_summary = _('Deleted RADIUS proxy server "%(value)s"')
+
+@register()
+class radiusproxy_mod(LDAPUpdate):
+    __doc__ = _('Modify a RADIUS proxy server.')
+    msg_summary = _('Modified RADIUS proxy server "%(value)s"')
+
+@register()
+class radiusproxy_find(LDAPSearch):
+    __doc__ = _('Search for RADIUS proxy servers.')
+    msg_summary = ngettext(
+        '%(count)d RADIUS proxy server matched', '%(count)d RADIUS proxy servers matched', 0
+    )
+
+@register()
+class radiusproxy_show(LDAPRetrieve):
+    __doc__ = _('Display information about a RADIUS proxy server.')
diff --git a/ipalib/plugins/user.py b/ipalib/plugins/user.py
index a7005faf16821e7aba0df0ed1ee04ae23853f17d..c855145392f08e669e1067028aa9c1ca3093ab7d 100644
--- a/ipalib/plugins/user.py
+++ b/ipalib/plugins/user.py
@@ -124,6 +124,12 @@ def validate_nsaccountlock(entry_attrs):
                 raise errors.ValidationError(name='nsaccountlock',
                     error=_('must be TRUE or FALSE'))
 
+def radius_dn2pk(api, entry_attrs):
+    cl = entry_attrs.get('ipatokenradiusconfiglink', None)
+    if cl:
+        pk = api.Object['radiusproxy'].get_primary_key_from_dn(cl[0])
+        entry_attrs['ipatokenradiusconfiglink'] = [pk]
+
 def convert_nsaccountlock(entry_attrs):
     if not 'nsaccountlock' in entry_attrs:
         entry_attrs['nsaccountlock'] = False
@@ -199,7 +205,8 @@ class user(LDAPObject):
     object_class = ['posixaccount']
     object_class_config = 'ipauserobjectclasses'
     possible_objectclasses = [
-        'meporiginentry', 'ipauserauthtypeclass', 'ipauser'
+        'meporiginentry', 'ipauserauthtypeclass', 'ipauser',
+        'ipatokenradiusproxyuser'
     ]
     disallow_object_classes = ['krbticketpolicyaux']
     search_attributes_config = 'ipausersearchfields'
@@ -207,7 +214,8 @@ class user(LDAPObject):
         'uid', 'givenname', 'sn', 'homedirectory', 'loginshell',
         'uidnumber', 'gidnumber', 'mail', 'ou',
         'telephonenumber', 'title', 'memberof', 'nsaccountlock',
-        'memberofindirect', 'ipauserauthtype', 'userclass'
+        'memberofindirect', 'ipauserauthtype', 'userclass',
+        'ipatokenradiusconfiglink', 'ipatokenradiususername'
     ]
     search_display_attributes = [
         'uid', 'givenname', 'sn', 'homedirectory', 'loginshell',
@@ -371,7 +379,7 @@ class user(LDAPObject):
             cli_name='user_auth_type',
             label=_('User authentication types'),
             doc=_('Types of supported user authentication'),
-            values=(u'password',),
+            values=(u'password', u'radius'),
             csv=True,
         ),
         Str('userclass*',
@@ -380,6 +388,14 @@ class user(LDAPObject):
             doc=_('User category (semantics placed on this attribute are for '
                   'local interpretation)'),
         ),
+        Str('ipatokenradiusconfiglink?',
+            cli_name='radius',
+            label=_('RADIUS proxy configuration'),
+        ),
+        Str('ipatokenradiususername?',
+            cli_name='radius_username',
+            label=_('RADIUS proxy username'),
+        ),
     )
 
     def _normalize_and_validate_email(self, email, config=None):
@@ -560,6 +576,19 @@ class user_add(LDAPCreate):
             and 'ipauser' not in entry_attrs['objectclass']):
             entry_attrs['objectclass'].append('ipauser')
 
+        if 'ipatokenradiusconfiglink' in entry_attrs:
+            cl = entry_attrs['ipatokenradiusconfiglink']
+            if cl:
+                if 'objectclass' not in entry_attrs:
+                    _entry = ldap.get_entry(dn, ['objectclass'])
+                    entry_attrs['objectclass'] = _entry['objectclass']
+
+                if 'ipatokenradiusproxyuser' not in entry_attrs['objectclass']:
+                    entry_attrs['objectclass'].append('ipatokenradiusproxyuser')
+
+                answer = self.api.Object['radiusproxy'].get_dn_if_exists(cl)
+                entry_attrs['ipatokenradiusconfiglink'] = answer
+
         return dn
 
     def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
@@ -604,9 +633,8 @@ class user_add(LDAPCreate):
                 pass
 
         self.obj.get_password_attributes(ldap, dn, entry_attrs)
-
         convert_sshpubkey_post(ldap, dn, entry_attrs)
-
+        radius_dn2pk(self.api, entry_attrs)
         return dn
 
 api.register(user_add)
@@ -654,18 +682,31 @@ class user_mod(LDAPUpdate):
             # save the password so it can be displayed in post_callback
             setattr(context, 'randompassword', entry_attrs['userpassword'])
         if ('ipasshpubkey' in entry_attrs or 'ipauserauthtype' in entry_attrs
-            or 'userclass' in entry_attrs):
+            or 'userclass' in entry_attrs or 'ipatokenradiusconfiglink' in entry_attrs):
             if 'objectclass' in entry_attrs:
                 obj_classes = entry_attrs['objectclass']
             else:
-                (_dn, _entry_attrs) = ldap.get_entry(dn, ['objectclass'])
+                _entry_attrs = ldap.get_entry(dn, ['objectclass'])
                 obj_classes = entry_attrs['objectclass'] = _entry_attrs['objectclass']
+
             if 'ipasshpubkey' in entry_attrs and 'ipasshuser' not in obj_classes:
                 obj_classes.append('ipasshuser')
-            if 'ipauserauthtype' in entry_attrs and 'ipauserauthtype' not in obj_classes:
+
+            if 'ipauserauthtype' in entry_attrs and 'ipauserauthtypeclass' not in obj_classes:
                 obj_classes.append('ipauserauthtypeclass')
+
             if 'userclass' in entry_attrs and 'ipauser' not in obj_classes:
                 obj_classes.append('ipauser')
+
+            if 'ipatokenradiusconfiglink' in entry_attrs:
+                cl = entry_attrs['ipatokenradiusconfiglink']
+                if cl:
+                    if 'ipatokenradiusproxyuser' not in obj_classes:
+                        obj_classes.append('ipatokenradiusproxyuser')
+
+                    answer = self.api.Object['radiusproxy'].get_dn_if_exists(cl)
+                    entry_attrs['ipatokenradiusconfiglink'] = answer
+
         return dn
 
     def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
@@ -680,6 +721,7 @@ class user_mod(LDAPUpdate):
         self.obj._convert_manager(entry_attrs, **options)
         self.obj.get_password_attributes(ldap, dn, entry_attrs)
         convert_sshpubkey_post(ldap, dn, entry_attrs)
+        radius_dn2pk(self.api, entry_attrs)
         return dn
 
 api.register(user_mod)
@@ -703,6 +745,12 @@ class user_find(LDAPSearch):
         manager = options.get('manager')
         if manager is not None:
             options['manager'] = self.obj._normalize_manager(manager)
+
+        # Ensure that the RADIUS config link is a dn, not just the name
+        cl = 'ipatokenradiusconfiglink'
+        if cl in options:
+            options[cl] = self.api.Object['radiusproxy'].get_dn(options[cl])
+
         return super(user_find, self).execute(self, *args, **options)
 
     def pre_callback(self, ldap, filter, attrs_list, base_dn, scope, *keys, **options):
@@ -742,6 +790,7 @@ class user_show(LDAPRetrieve):
         self.obj._convert_manager(entry_attrs, **options)
         self.obj.get_password_attributes(ldap, dn, entry_attrs)
         convert_sshpubkey_post(ldap, dn, entry_attrs)
+        radius_dn2pk(self.api, entry_attrs)
         return dn
 
 api.register(user_show)
-- 
1.8.4.2

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

Reply via email to