Revoke a host's certificate (if any) when it is deleted or disabled.

Disable any services when its host is disabled.

This also adds displaying the certificate attributes (subject, etc)
a bit more universal and centralized in a single function.

ticket 297
This commit is contained in:
Rob Crittenden
2010-11-05 15:16:53 -04:00
committed by Adam Young
parent eead9eec99
commit 2046eddb7a
5 changed files with 276 additions and 96 deletions

View File

@@ -4,7 +4,7 @@ dn: $SUFFIX
changetype: modify
add: aci
aci: (targetattr != "userPassword || krbPrincipalKey || sambaLMPassword || sambaNTPassword || passwordHistory || krbMKey")(version 3.0; acl "Enable Anonymous access"; allow (read, search, compare) userdn = "ldap:///anyone";)
aci: (targetattr != "userPassword || krbPrincipalKey || sambaLMPassword || sambaNTPassword || passwordHistory || krbMKey || ipaUniqueId || memberOf || serverHostName || enrolledBy")(version 3.0; acl "Admin can manage any entry"; allow (all) groupdn = "ldap:///cn=admins,cn=groups,cn=accounts,$SUFFIX";)
aci: (targetattr != "userPassword || krbPrincipalKey || sambaLMPassword || sambaNTPassword || passwordHistory || krbMKey || ipaUniqueId || memberOf || serverHostName")(version 3.0; acl "Admin can manage any entry"; allow (all) groupdn = "ldap:///cn=admins,cn=groups,cn=accounts,$SUFFIX";)
aci: (targetattr = "userPassword || krbPrincipalKey || sambaLMPassword || sambaNTPassword")(version 3.0; acl "Self can write own password"; allow (write) userdn="ldap:///self";)
aci: (targetattr = "userPassword || krbPrincipalKey || sambaLMPassword || sambaNTPassword || passwordHistory")(version 3.0; acl "Admins can write passwords"; allow (add,delete,write) groupdn="ldap:///cn=admins,cn=groups,cn=accounts,$SUFFIX";)
aci: (targetattr = "userPassword || krbPrincipalKey || sambaLMPassword || sambaNTPassword || passwordHistory")(version 3.0; acl "Password change service can read/write passwords"; allow (read, write) userdn="ldap:///krbprincipalname=kadmin/changepw@$REALM,cn=$REALM,cn=kerberos,$SUFFIX";)

View File

@@ -76,6 +76,8 @@ from ipalib import Str, Flag, Bytes
from ipalib.plugins.baseldap import *
from ipalib.plugins.service import split_principal
from ipalib.plugins.service import validate_certificate
from ipalib.plugins.service import normalize_certificate
from ipalib.plugins.service import set_certificate_attrs
from ipalib import _, ngettext
from ipalib import x509
from ipapython.ipautil import ipa_generate_password
@@ -92,6 +94,36 @@ def validate_host(ugettext, fqdn):
return _('Fully-qualified hostname required')
return None
host_output_params = (
Flag('has_keytab',
label=_('Keytab'),
),
Str('subject',
label=_('Subject'),
),
Str('serial_number',
label=_('Serial Number'),
),
Str('issuer',
label=_('Issuer'),
),
Str('valid_not_before',
label=_('Not Before'),
),
Str('valid_not_after',
label=_('Not After'),
),
Str('md5_fingerprint',
label=_('Fingerprint (MD5)'),
),
Str('sha1_fingerprint',
label=_('Fingerprint (SHA1)'),
),
Str('revocation_reason?',
label=_('Revocation reason'),
)
)
class host(LDAPObject):
"""
Host object.
@@ -199,6 +231,7 @@ class host_add(LDAPCreate):
Add a new host.
"""
has_output_params = LDAPCreate.has_output_params + host_output_params
msg_summary = _('Added host "%(value)s"')
takes_options = (
Flag('force',
@@ -241,6 +274,7 @@ class host_add(LDAPCreate):
# On the off-chance some other extension deletes this from the
# context, don't crash.
pass
set_certificate_attrs(entry_attrs)
return dn
api.register(host_add)
@@ -275,6 +309,31 @@ class host_del(LDAPDelete):
(service, hostname, realm) = split_principal(principal)
if hostname.lower() == fqdn:
api.Command['service_del'](principal)
(dn, entry_attrs) = ldap.get_entry(dn, ['usercertificate'])
if 'usercertificate' in entry_attrs:
cert = normalize_certificate(entry_attrs.get('usercertificate')[0])
try:
serial = unicode(x509.get_serial_number(cert, x509.DER))
try:
result = api.Command['cert_show'](unicode(serial))['result'
]
if 'revocation_reason' not in result:
try:
api.Command['cert_revoke'](unicode(serial), revocation_reason=4)
except errors.NotImplementedError:
# some CA's might not implement revoke
pass
except errors.NotImplementedError:
# some CA's might not implement revoke
pass
except NSPRError, nsprerr:
if nsprerr.errno == -8183:
# If we can't decode the cert them proceed with
# removing the host.
self.log.info("Problem decoding certificate %s" % nsprerr.args[1])
else:
raise nsprerr
return dn
api.register(host_del)
@@ -285,6 +344,7 @@ class host_mod(LDAPUpdate):
Modify information about a host.
"""
has_output_params = LDAPUpdate.has_output_params + host_output_params
msg_summary = _('Modified host "%(value)s"')
takes_options = LDAPUpdate.takes_options + (
@@ -312,17 +372,33 @@ class host_mod(LDAPUpdate):
if 'krbprincipalaux' not in obj_classes:
obj_classes.append('krbprincipalaux')
entry_attrs['objectclass'] = obj_classes
cert = entry_attrs.get('usercertificate')
cert = normalize_certificate(entry_attrs.get('usercertificate'))
if cert:
(dn, entry_attrs_old) = ldap.get_entry(dn, ['usercertificate'])
if 'usercertificate' in entry_attrs_old:
# FIXME: what to do here? do we revoke the old cert?
fmt = 'entry already has a certificate, serial number: %s' % (
x509.get_serial_number(entry_attrs_old['usercertificate'][0], x509.DER)
)
raise errors.GenericError(format=fmt)
# FIXME: decoding should be in normalizer; see service_add
entry_attrs['usercertificate'] = base64.b64decode(cert)
oldcert = normalize_certificate(entry_attrs_old.get('usercertificate')[0])
try:
serial = unicode(x509.get_serial_number(oldcert, x509.DER))
try:
result = api.Command['cert_show'](unicode(serial))['result']
if 'revocation_reason' not in result:
try:
api.Command['cert_revoke'](unicode(serial), revocation_reason=4)
except errors.NotImplementedError:
# some CA's might not implement revoke
pass
except errors.NotImplementedError:
# some CA's might not implement revoke
pass
except NSPRError, nsprerr:
if nsprerr.errno == -8183:
# If we can't decode the cert them proceed with
# modifying the host.
self.log.info("Problem decoding certificate %s" % nsprerr.args[1])
else:
raise nsprerr
entry_attrs['usercertificate'] = cert
if 'random' in options:
if options.get('random'):
entry_attrs['userpassword'] = ipa_generate_password()
@@ -335,6 +411,7 @@ class host_mod(LDAPUpdate):
def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
if options.get('random', False):
entry_attrs['randompassword'] = unicode(getattr(context, 'randompassword'))
set_certificate_attrs(entry_attrs)
return dn
api.register(host_mod)
@@ -345,6 +422,7 @@ class host_find(LDAPSearch):
Search for hosts.
"""
has_output_params = LDAPSearch.has_output_params + host_output_params
msg_summary = ngettext(
'%(count)d host matched', '%(count)d hosts matched'
)
@@ -355,6 +433,11 @@ class host_find(LDAPSearch):
attrs_list.append('l')
return filter.replace('locality', 'l')
def post_callback(self, ldap, entries, truncated, *args, **options):
for entry in entries:
entry_attrs = entry[1]
set_certificate_attrs(entry_attrs)
api.register(host_find)
@@ -362,35 +445,7 @@ class host_show(LDAPRetrieve):
"""
Display information about a host.
"""
has_output_params = (
Flag('has_keytab',
label=_('Keytab'),
),
Str('subject',
label=_('Subject'),
),
Str('serial_number',
label=_('Serial Number'),
),
Str('issuer',
label=_('Issuer'),
),
Str('valid_not_before',
label=_('Not Before'),
),
Str('valid_not_after',
label=_('Not After'),
),
Str('md5_fingerprint',
label=_('Fingerprint (MD5)'),
),
Str('sha1_fingerprint',
label=_('Fingerprint (SHA1)'),
),
Str('revocation_reason?',
label=_('Revocation reason'),
)
)
has_output_params = LDAPRetrieve.has_output_params + host_output_params
def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
if 'krblastpwdchange' in entry_attrs:
@@ -400,15 +455,7 @@ class host_show(LDAPRetrieve):
else:
entry_attrs['has_keytab'] = False
if 'usercertificate' in entry_attrs:
cert = x509.load_certificate(entry_attrs['usercertificate'][0], datatype=x509.DER)
entry_attrs['subject'] = unicode(cert.subject)
entry_attrs['serial_number'] = unicode(cert.serial_number)
entry_attrs['issuer'] = unicode(cert.issuer)
entry_attrs['valid_not_before'] = unicode(cert.valid_not_before_str)
entry_attrs['valid_not_after'] = unicode(cert.valid_not_after_str)
entry_attrs['md5_fingerprint'] = unicode(nss.data_to_hex(nss.md5_digest(cert.der_data), 64)[0])
entry_attrs['sha1_fingerprint'] = unicode(nss.data_to_hex(nss.sha1_digest(cert.der_data), 64)[0])
set_certificate_attrs(entry_attrs)
return dn
@@ -420,19 +467,77 @@ class host_disable(LDAPQuery):
Disable the kerberos key of a host.
"""
has_output = output.standard_value
msg_summary = _('Removed kerberos key from "%(value)s"')
msg_summary = _('Removed kerberos key and disabled all services for "%(value)s"')
def execute(self, *keys, **options):
ldap = self.obj.backend
# If we aren't given a fqdn, find it
if validate_host(None, keys[-1]) is not None:
hostentry = api.Command['host_show'](keys[-1])['result']
fqdn = hostentry['fqdn'][0]
else:
fqdn = keys[-1]
# See if we actually do anthing here, and if not raise an exception
done_work = False
dn = self.obj.get_dn(*keys, **options)
(dn, entry_attrs) = ldap.get_entry(dn, ['krblastpwdchange'])
(dn, entry_attrs) = ldap.get_entry(dn, ['krblastpwdchange', 'usercertificate'])
if 'krblastpwdchange' not in entry_attrs:
error_msg = _('Host principal has no kerberos key')
raise errors.NotFound(reason=error_msg)
truncated = True
while truncated:
try:
ret = api.Command['service_find'](fqdn)
truncated = ret['truncated']
services = ret['result']
except errors.NotFound:
break
else:
for entry_attrs in services:
principal = entry_attrs['krbprincipalname'][0]
(service, hostname, realm) = split_principal(principal)
if hostname.lower() == fqdn:
try:
api.Command['service_disable'](principal)
done_work = True
except errors.AlreadyInactive:
pass
if 'usercertificate' in entry_attrs:
cert = normalize_certificate(entry_attrs.get('usercertificate')[0])
try:
serial = unicode(x509.get_serial_number(cert, x509.DER))
try:
result = api.Command['cert_show'](unicode(serial))['result'
]
if 'revocation_reason' not in result:
try:
api.Command['cert_revoke'](unicode(serial), revocation_reason=4)
except errors.NotImplementedError:
# some CA's might not implement revoke
pass
except errors.NotImplementedError:
# some CA's might not implement revoke
pass
except NSPRError, nsprerr:
if nsprerr.errno == -8183:
# If we can't decode the cert them proceed with
# disabling the host.
self.log.info("Problem decoding certificate %s" % nsprerr.args[1])
else:
raise nsprerr
ldap.remove_principal_key(dn)
# Remove the usercertificate altogether
ldap.update_entry(dn, {'usercertificate': None})
done_work = True
if 'krblastpwdchange' in entry_attrs:
ldap.remove_principal_key(dn)
api.Command['host_mod'](fqdn=keys[-1], setattr=u'enrolledby=')
done_work = True
if not done_work:
raise errors.AlreadyInactive()
return dict(
result=True,

View File

@@ -87,6 +87,30 @@ output_params = (
Str('managedby_host',
label='Managed by',
),
Str('subject',
label=_('Subject'),
),
Str('serial_number',
label=_('Serial Number'),
),
Str('issuer',
label=_('Issuer'),
),
Str('valid_not_before',
label=_('Not Before'),
),
Str('valid_not_after',
label=_('Not After'),
),
Str('md5_fingerprint',
label=_('Fingerprint (MD5)'),
),
Str('sha1_fingerprint',
label=_('Fingerprint (SHA1)'),
),
Str('revocation_reason?',
label=_('Revocation reason'),
)
)
def split_principal(principal):
@@ -171,6 +195,30 @@ def normalize_certificate(cert):
return cert
def set_certificate_attrs(entry_attrs):
"""
Set individual attributes from some values from a certificate.
entry_attrs is a dict of an entry
returns nothing
"""
if not 'usercertificate' in entry_attrs:
return
if type(entry_attrs['usercertificate']) in (list, tuple):
cert = entry_attrs['usercertificate'][0]
else:
cert = entry_attrs['usercertificate']
cert = normalize_certificate(cert)
cert = x509.load_certificate(cert, datatype=x509.DER)
entry_attrs['subject'] = unicode(cert.subject)
entry_attrs['serial_number'] = unicode(cert.serial_number)
entry_attrs['issuer'] = unicode(cert.issuer)
entry_attrs['valid_not_before'] = unicode(cert.valid_not_before_str)
entry_attrs['valid_not_after'] = unicode(cert.valid_not_after_str)
entry_attrs['md5_fingerprint'] = unicode(nss.data_to_hex(nss.md5_digest(cert.der_data), 64)[0])
entry_attrs['sha1_fingerprint'] = unicode(nss.data_to_hex(nss.sha1_digest(cert.der_data), 64)[0])
class service(LDAPObject):
"""
@@ -313,6 +361,9 @@ class service_mod(LDAPUpdate):
entry_attrs['usercertificate'] = None
return dn
def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
set_certificate_attrs(entry_attrs)
api.register(service_mod)
@@ -348,6 +399,7 @@ class service_find(LDAPSearch):
del entry_attrs['krblastpwdchange']
else:
entry_attrs['has_keytab'] = False
set_certificate_attrs(entry_attrs)
api.register(service_find)
@@ -359,33 +411,6 @@ class service_show(LDAPRetrieve):
member_attributes = ['managedby']
takes_options = LDAPRetrieve.takes_options
has_output_params = LDAPRetrieve.has_output_params + output_params + (
Str('subject',
label=_('Subject'),
),
Str('serial_number',
label=_('Serial Number'),
),
Str('issuer',
label=_('Issuer'),
),
Str('valid_not_before',
label=_('Not Before'),
),
Str('valid_not_after',
label=_('Not After'),
),
Str('md5_fingerprint',
label=_('Fingerprint (MD5)'),
),
Str('sha1_fingerprint',
label=_('Fingerprint (SHA1)'),
),
Str('revocation_reason?',
label=_('Revocation reason'),
)
)
def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
if 'krblastpwdchange' in entry_attrs:
entry_attrs['has_keytab'] = True
@@ -394,15 +419,7 @@ class service_show(LDAPRetrieve):
else:
entry_attrs['has_keytab'] = False
if 'usercertificate' in entry_attrs:
cert = x509.load_certificate(entry_attrs['usercertificate'][0], datatype=x509.DER)
entry_attrs['subject'] = unicode(cert.subject)
entry_attrs['serial_number'] = unicode(cert.serial_number)
entry_attrs['issuer'] = unicode(cert.issuer)
entry_attrs['valid_not_before'] = unicode(cert.valid_not_before_str)
entry_attrs['valid_not_after'] = unicode(cert.valid_not_after_str)
entry_attrs['md5_fingerprint'] = unicode(nss.data_to_hex(nss.md5_digest(cert.der_data), 64)[0])
entry_attrs['sha1_fingerprint'] = unicode(nss.data_to_hex(nss.sha1_digest(cert.der_data), 64)[0])
set_certificate_attrs(entry_attrs)
return dn
@@ -440,13 +457,44 @@ class service_disable(LDAPQuery):
ldap = self.obj.backend
dn = self.obj.get_dn(*keys, **options)
(dn, entry_attrs) = ldap.get_entry(dn, ['krblastpwdchange'])
(dn, entry_attrs) = ldap.get_entry(dn, ['krblastpwdchange', 'usercertificate'])
if 'krblastpwdchange' not in entry_attrs:
error_msg = _('Service principal has no kerberos key')
raise errors.NotFound(reason=error_msg)
# See if we do any work at all here and if not raise an exception
done_work = False
ldap.remove_principal_key(dn)
if 'usercertificate' in entry_attrs:
cert = normalize_certificate(entry_attrs.get('usercertificate')[0])
try:
serial = unicode(x509.get_serial_number(cert, x509.DER))
try:
result = api.Command['cert_show'](unicode(serial))['result']
if 'revocation_reason' not in result:
try:
api.Command['cert_revoke'](unicode(serial), revocation_reason=4)
except errors.NotImplementedError:
# some CA's might not implement revoke
pass
except errors.NotImplementedError:
# some CA's might not implement revoke
pass
except NSPRError, nsprerr:
if nsprerr.errno == -8183:
# If we can't decode the cert them proceed with
# disabling the service
self.log.info("Problem decoding certificate %s" % nsprerr.args[1])
else:
raise nsprerr
# Remove the usercertificate altogether
ldap.update_entry(dn, {'usercertificate': None})
done_work = True
if 'krblastpwdchange' in entry_attrs:
ldap.remove_principal_key(dn)
done_work = True
if not done_work:
raise errors.AlreadyInactive()
return dict(
result=True,

View File

@@ -25,6 +25,7 @@ Test the `ipalib.plugins.host` module.
from ipalib import api, errors
from tests.test_xmlrpc.xmlrpc_test import Declarative, fuzzy_uuid
from tests.test_xmlrpc import objectclasses
import base64
fqdn1 = u'testhost1.%s' % api.env.domain
@@ -35,6 +36,8 @@ service1dn = u'krbprincipalname=%s,cn=services,cn=accounts,%s' % (service1.lower
fqdn2 = u'shouldnotexist.%s' % api.env.domain
dn2 = u'fqdn=%s,cn=computers,cn=accounts,%s' % (fqdn2, api.env.basedn)
servercert = 'MIICbzCCAdigAwIBAgICA/4wDQYJKoZIhvcNAQEFBQAwKTEnMCUGA1UEAxMeSVBBIFRlc3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MB4XDTEwMDgwOTE1MDIyN1oXDTIwMDgwOTE1MDIyN1owKTEMMAoGA1UEChMDSVBBMRkwFwYDVQQDExBwdW1hLmdyZXlvYWsuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwYbfEOQPgGenPn9vt1JFKvWm/Je3y2tawGWA3LXDuqfFJyYtZ8ib3TcBUOnLk9WK5g2qCwHaNlei7bj8ggIfr5hegAVe10cun+wYErjnYo7hsHYd+57VZezeipWrXu+7NoNd4+c4A5lk4A/xJay9j3bYx2oOM8BEox4xWYoWge1ljPrc5JK46f0X7AGW4F2VhnKPnf8rwSuzI1U8VGjutyM9TWNy3m9KMWeScjyG/ggIpOjUDMV7HkJL0Di61lznR9jXubpiEC7gWGbTp84eGl/Nn9bgK1AwHfJ2lHwfoY4uiL7ge1gyP6EvuUlHoBzdb7pekiX28iePjW3iEG9IawIDAQABoyIwIDARBglghkgBhvhCAQEEBAMCBkAwCwYDVR0PBAQDAgUgMA0GCSqGSIb3DQEBBQUAA4GBACRESLemRV9BPxfEgbALuxH5oE8jQm8WZ3pm2pALbpDlAd9wQc3yVf6RtkfVthyDnM18bg7IhxKpd77/p3H8eCnS8w5MLVRda6ktUC6tGhFTS4QKAf0WyDGTcIgkXbeDw0OPAoNHivoXbIXIIRxlw/XgaSaMzJQDBG8iROsN4kCv'
class test_host(Declarative):
@@ -201,7 +204,8 @@ class test_host(Declarative):
dict(
desc='Update %r' % fqdn1,
command=('host_mod', [fqdn1], dict(description=u'Updated host 1')),
command=('host_mod', [fqdn1], dict(description=u'Updated host 1',
usercertificate=servercert)),
expected=dict(
value=fqdn1,
summary=u'Modified host "%s"' % fqdn1,
@@ -210,6 +214,14 @@ class test_host(Declarative):
fqdn=[fqdn1],
l=[u'Undisclosed location 1'],
krbprincipalname=[u'host/%s@%s' % (fqdn1, api.env.realm)],
usercertificate=[base64.b64decode(servercert)],
valid_not_before=u'Mon Aug 09 15:02:27 2010 UTC',
valid_not_after=u'Sun Aug 09 15:02:27 2020 UTC',
subject=u'CN=puma.greyoak.com,O=IPA',
serial_number=u'1022',
md5_fingerprint=u'ef:63:31:e4:33:54:8d:fd:fe:c8:66:57:09:03:5f:09',
sha1_fingerprint=u'e3:33:2c:d9:7c:e9:77:74:2a:ac:3b:b8:76:b0:86:29:98:43:58:11',
issuer=u'CN=IPA Test Certificate Authority',
),
),
),
@@ -227,7 +239,15 @@ class test_host(Declarative):
description=[u'Updated host 1'],
l=[u'Undisclosed location 1'],
krbprincipalname=[u'host/%s@%s' % (fqdn1, api.env.realm)],
has_keytab=False
has_keytab=False,
usercertificate=[base64.b64decode(servercert)],
valid_not_before=u'Mon Aug 09 15:02:27 2010 UTC',
valid_not_after=u'Sun Aug 09 15:02:27 2020 UTC',
subject=u'CN=puma.greyoak.com,O=IPA',
serial_number=u'1022',
md5_fingerprint=u'ef:63:31:e4:33:54:8d:fd:fe:c8:66:57:09:03:5f:09',
sha1_fingerprint=u'e3:33:2c:d9:7c:e9:77:74:2a:ac:3b:b8:76:b0:86:29:98:43:58:11',
issuer=u'CN=IPA Test Certificate Authority',
),
),
),

View File

@@ -234,6 +234,13 @@ class test_host(Declarative):
usercertificate=[base64.b64decode(servercert)],
krbprincipalname=[service1],
managedby_host=[fqdn1],
valid_not_before=u'Mon Aug 09 15:02:27 2010 UTC',
valid_not_after=u'Sun Aug 09 15:02:27 2020 UTC',
subject=u'CN=puma.greyoak.com,O=IPA',
serial_number=u'1022',
md5_fingerprint=u'ef:63:31:e4:33:54:8d:fd:fe:c8:66:57:09:03:5f:09',
sha1_fingerprint=u'e3:33:2c:d9:7c:e9:77:74:2a:ac:3b:b8:76:b0:86:29:98:43:58:11',
issuer=u'CN=IPA Test Certificate Authority',
),
),
),