Add support for SSH public keys to user and host objects.

This patch adds a new multivalue param "sshpubkey" for specifying SSH public
keys to both user and host objects. The accepted value is base64-encoded
public key blob as specified in RFC4253, section 6.6.

Additionaly, host commands automatically update DNS SSHFP records when
requested by user.

https://fedorahosted.org/freeipa/ticket/754
This commit is contained in:
Jan Cholasta
2011-12-07 02:50:31 -05:00
committed by Rob Crittenden
parent 9b6baf9bee
commit 3c2b0fc28a
8 changed files with 193 additions and 36 deletions

View File

@@ -22,6 +22,8 @@ import platform
import os
import sys
from nss.error import NSPRError
import nss.nss as nss
import netaddr
from ipalib import api, errors, util
from ipalib import Str, Flag, Bytes
@@ -34,11 +36,9 @@ from ipalib.plugins.dns import add_forward_record
from ipalib import _, ngettext
from ipalib import x509
from ipalib.dn import *
from ipapython.ipautil import ipa_generate_password, CheckedIPAddress
from ipalib.request import context
import base64
import nss.nss as nss
import netaddr
from ipalib.util import validate_sshpubkey, output_sshpubkey
from ipapython.ipautil import ipa_generate_password, CheckedIPAddress, make_sshfp
__doc__ = _("""
Hosts/Machines
@@ -87,6 +87,9 @@ EXAMPLES:
Modify information about a host:
ipa host-mod --os='Fedora 12' test.example.com
Remove SSH public keys of a host and update DNS to reflect this change:
ipa host-mod --sshpubkey= --updatedns test.example.com
Disable the host Kerberos key, SSL certificate and all of its services:
ipa host-disable test.example.com
@@ -162,6 +165,22 @@ def remove_fwd_ptr(ipaddr, host, domain, recordtype):
except errors.NotFound:
pass
def update_sshfp_record(zone, record, entry_attrs):
if 'ipasshpubkey' not in entry_attrs:
return
pubkeys = entry_attrs['ipasshpubkey'] or ()
sshfps=[]
for pubkey in pubkeys:
sshfp = unicode(make_sshfp(pubkey))
if sshfp is not None:
sshfps.append(sshfp)
try:
api.Command['dnsrecord_mod'](zone, record, sshfprecord=sshfps)
except errors.EmptyModlist:
pass
host_output_params = (
Flag('has_keytab',
label=_('Keytab'),
@@ -226,6 +245,7 @@ class host(LDAPObject):
'fqdn', 'description', 'l', 'nshostlocation', 'krbprincipalname',
'nshardwareplatform', 'nsosversion', 'usercertificate', 'memberof',
'managedby', 'memberindirect', 'memberofindirect', 'macaddress',
'sshpubkeyfp',
]
uuid_attribute = 'ipauniqueid'
attribute_members = {
@@ -315,6 +335,15 @@ class host(LDAPObject):
label=_('MAC address'),
doc=_('Hardware MAC address(es) on this host'),
),
Bytes('ipasshpubkey*', validate_sshpubkey,
cli_name='sshpubkey',
label=_('Base-64 encoded SSH public key'),
flags=['no_search'],
),
Str('sshpubkeyfp*',
label=_('SSH public key fingerprint'),
flags=['virtual_attribute', 'no_create', 'no_update', 'no_search'],
),
)
def get_dn(self, *keys, **options):
@@ -452,33 +481,37 @@ class host_add(LDAPCreate):
entry_attrs['usercertificate'] = cert
entry_attrs['managedby'] = dn
entry_attrs['objectclass'].append('ieee802device')
entry_attrs['objectclass'].append('ipasshhost')
return dn
def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
exc = None
try:
if 'ip_address' in options and dns_container_exists(ldap):
if dns_container_exists(ldap):
try:
parts = keys[-1].split('.')
domain = unicode('.'.join(parts[1:]))
ip = CheckedIPAddress(options['ip_address'], match_local=False)
add_forward_record(domain, parts[0], unicode(ip))
if 'ip_address' in options:
ip = CheckedIPAddress(options['ip_address'], match_local=False)
add_forward_record(domain, parts[0], unicode(ip))
if not options.get('no_reverse', False):
try:
prefixlen = None
if not ip.defaultnet:
prefixlen = ip.prefixlen
revzone, revname = get_reverse_zone(ip, prefixlen)
addkw = { 'ptrrecord' : keys[-1]+'.' }
api.Command['dnsrecord_add'](revzone, revname, **addkw)
except errors.EmptyModlist:
# the entry already exists and matches
pass
if not options.get('no_reverse', False):
try:
prefixlen = None
if not ip.defaultnet:
prefixlen = ip.prefixlen
revzone, revname = get_reverse_zone(ip, prefixlen)
addkw = { 'ptrrecord' : keys[-1]+'.' }
api.Command['dnsrecord_add'](revzone, revname, **addkw)
except errors.EmptyModlist:
# the entry already exists and matches
pass
del options['ip_address']
except Exception, e:
exc = e
del options['ip_address']
update_sshfp_record(domain, unicode(parts[0]), entry_attrs)
except Exception, e:
exc = e
if options.get('random', False):
try:
entry_attrs['randompassword'] = unicode(getattr(context, 'randompassword'))
@@ -493,13 +526,15 @@ class host_add(LDAPCreate):
set_certificate_attrs(entry_attrs)
if options.get('all', False):
entry_attrs['managing'] = self.obj.get_managed_hosts(dn)
entry_attrs['managing'] = self.obj.get_managed_hosts(dn)
self.obj.get_password_attributes(ldap, dn, entry_attrs)
if entry_attrs['has_password']:
# If an OTP is set there is no keytab, at least not one
# fetched anywhere.
entry_attrs['has_keytab'] = False
output_sshpubkey(ldap, dn, entry_attrs)
return dn
api.register(host_add)
@@ -632,6 +667,10 @@ class host_mod(LDAPUpdate):
doc=_('Kerberos principal name for this host'),
attribute=True,
),
Flag('updatedns?',
doc=_('Update DNS entries'),
default=False,
),
)
def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
@@ -688,6 +727,7 @@ class host_mod(LDAPUpdate):
raise nsprerr
entry_attrs['usercertificate'] = cert
if options.get('random'):
entry_attrs['userpassword'] = ipa_generate_password()
setattr(context, 'randompassword', entry_attrs['userpassword'])
@@ -703,6 +743,30 @@ class host_mod(LDAPUpdate):
obj_classes.append('ieee802device')
entry_attrs['objectclass'] = obj_classes
if options.get('updatedns', False) and dns_container_exists(ldap):
parts = keys[-1].split('.')
domain = unicode('.'.join(parts[1:]))
result = api.Command['dnszone_find']()['result']
match = False
for zone in result:
if domain == zone['idnsname'][0]:
match = True
break
if not match:
raise errors.NotFound(
reason=_('DNS zone %(zone)s not found') % dict(zone=domain)
)
update_sshfp_record(domain, unicode(parts[0]), entry_attrs)
if 'ipasshpubkey' in entry_attrs:
if 'objectclass' in entry_attrs:
obj_classes = entry_attrs['objectclass']
else:
(_dn, _entry_attrs) = ldap.get_entry(dn, ['objectclass'])
obj_classes = entry_attrs['objectclass'] = _entry_attrs['objectclass']
if 'ipasshhost' not in obj_classes:
obj_classes.append('ipasshhost')
return dn
def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
@@ -720,6 +784,8 @@ class host_mod(LDAPUpdate):
self.obj.suppress_netgroup_memberof(entry_attrs)
output_sshpubkey(ldap, dn, entry_attrs)
return dn
api.register(host_mod)
@@ -802,6 +868,8 @@ class host_find(LDAPSearch):
if options.get('all', False):
entry_attrs['managing'] = self.obj.get_managed_hosts(entry[0])
output_sshpubkey(ldap, dn, entry_attrs)
api.register(host_find)
@@ -831,6 +899,8 @@ class host_show(LDAPRetrieve):
self.obj.suppress_netgroup_memberof(entry_attrs)
output_sshpubkey(ldap, dn, entry_attrs)
return dn
def forward(self, *keys, **options):

View File

@@ -18,16 +18,18 @@
# 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 import api, errors
from ipalib import Flag, Int, Password, Str, Bool
from ipalib.plugins.baseldap import *
from ipalib.request import context
from time import gmtime, strftime
import copy
import string
from ipalib import api, errors
from ipalib import Flag, Int, Password, Str, Bool, Bytes
from ipalib.plugins.baseldap import *
from ipalib.request import context
from ipalib import _, ngettext
from ipapython.ipautil import ipa_generate_password
import string
import posixpath
from ipalib.util import validate_sshpubkey, output_sshpubkey
__doc__ = _("""
Users
@@ -154,12 +156,12 @@ class user(LDAPObject):
'uid', 'givenname', 'sn', 'homedirectory', 'loginshell',
'uidnumber', 'gidnumber', 'mail', 'ou',
'telephonenumber', 'title', 'memberof', 'nsaccountlock',
'memberofindirect',
'memberofindirect', 'sshpubkeyfp',
]
search_display_attributes = [
'uid', 'givenname', 'sn', 'homedirectory', 'loginshell',
'mail', 'telephonenumber', 'title', 'nsaccountlock',
'uidnumber', 'gidnumber',
'uidnumber', 'gidnumber', 'sshpubkeyfp',
]
uuid_attribute = 'ipauniqueid'
attribute_members = {
@@ -310,6 +312,15 @@ class user(LDAPObject):
label=_('Account disabled'),
flags=['no_create', 'no_update', 'no_search'],
),
Bytes('ipasshpubkey*', validate_sshpubkey,
cli_name='sshpubkey',
label=_('Base-64 encoded SSH public key'),
flags=['no_search'],
),
Str('sshpubkeyfp*',
label=_('SSH public key fingerprint'),
flags=['virtual_attribute', 'no_create', 'no_update', 'no_search'],
),
)
def _normalize_email(self, email, config=None):
@@ -489,6 +500,9 @@ class user_add(LDAPCreate):
pass
self.obj.get_password_attributes(ldap, dn, entry_attrs)
output_sshpubkey(ldap, dn, entry_attrs)
return dn
api.register(user_add)
@@ -522,6 +536,14 @@ class user_mod(LDAPUpdate):
entry_attrs['userpassword'] = ipa_generate_password(user_pwdchars)
# save the password so it can be displayed in post_callback
setattr(context, 'randompassword', entry_attrs['userpassword'])
if 'ipasshpubkey' in entry_attrs:
if 'objectclass' in entry_attrs:
obj_classes = entry_attrs['objectclass']
else:
(_dn, _entry_attrs) = ldap.get_entry(dn, ['objectclass'])
obj_classes = entry_attrs['objectclass'] = _entry_attrs['objectclass']
if 'ipasshuser' not in obj_classes:
obj_classes.append('ipasshuser')
return dn
def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
@@ -534,6 +556,7 @@ class user_mod(LDAPUpdate):
convert_nsaccountlock(entry_attrs)
self.obj._convert_manager(entry_attrs, **options)
self.obj.get_password_attributes(ldap, dn, entry_attrs)
output_sshpubkey(ldap, dn, entry_attrs)
return dn
api.register(user_mod)
@@ -567,6 +590,7 @@ class user_find(LDAPSearch):
self.obj._convert_manager(attrs, **options)
self.obj.get_password_attributes(ldap, dn, attrs)
convert_nsaccountlock(attrs)
output_sshpubkey(ldap, dn, attrs)
msg_summary = ngettext(
'%(count)d user matched', '%(count)d users matched', 0
@@ -584,6 +608,7 @@ class user_show(LDAPRetrieve):
convert_nsaccountlock(entry_attrs)
self.obj._convert_manager(entry_attrs, **options)
self.obj.get_password_attributes(ldap, dn, entry_attrs)
output_sshpubkey(ldap, dn, entry_attrs)
return dn
api.register(user_show)

View File

@@ -32,6 +32,7 @@ from weakref import WeakKeyDictionary
from ipalib import errors
from ipalib.text import _
from ipapython import dnsclient
from ipapython.ipautil import decode_ssh_pubkey
def json_serialize(obj):
@@ -278,6 +279,37 @@ def validate_hostname(hostname, check_fqdn=True):
raise ValueError(_('only letters, numbers, and - are allowed. ' \
'- must not be the last name character'))
def validate_sshpubkey(ugettext, pubkey):
try:
algo, data, fp = decode_ssh_pubkey(pubkey)
except ValueError:
return _('invalid SSH public key')
def output_sshpubkey(ldap, dn, entry_attrs):
if 'ipasshpubkey' in entry_attrs:
pubkeys = entry_attrs.get('ipasshpubkey')
else:
entry = ldap.get_entry(dn, ['ipasshpubkey'])
pubkeys = entry[1].get('ipasshpubkey')
if pubkeys is None:
return
fingerprints = []
for pubkey in pubkeys:
try:
algo, data, fp = decode_ssh_pubkey(pubkey)
fp = u':'.join([fp[j:j+2] for j in range(0, len(fp), 2)])
fingerprints.append(u'%s (%s)' % (fp, algo))
except ValueError:
pass
if fingerprints:
entry_attrs['sshpubkeyfp'] = fingerprints
def normalize_sshpubkeyfp(value):
value = value.split()[0]
value = unicode(c for c in value if c in '0123456789ABCDEFabcdef')
return value
class cachedproperty(object):
"""
A property-like attribute that caches the return value of a method call.