Add support for tracking and counting entitlements

Adds a plugin, entitle, to register to the entitlement server, consume
entitlements and to count and track them. It is also possible to
import an entitlement certificate (if for example the remote entitlement
server is unaviailable).

This uses the candlepin server from https://fedorahosted.org/candlepin/wiki
for entitlements.

Add a cron job to validate the entitlement status and syslog the results.

tickets 28, 79, 278
This commit is contained in:
Rob Crittenden
2011-02-01 14:24:46 -05:00
parent f3d04bfc40
commit 275998f6bd
15 changed files with 1116 additions and 26 deletions

View File

@@ -266,6 +266,8 @@ mkdir -p %{buildroot}/%{_localstatedir}/lib/ipa-client/sysrestore
%if ! %{ONLY_CLIENT}
mkdir -p %{buildroot}%{_sysconfdir}/bash_completion.d
install -pm 644 contrib/completion/ipa.bash_completion %{buildroot}%{_sysconfdir}/bash_completion.d/ipa
mkdir -p %{buildroot}%{_sysconfdir}/cron.d
install -pm 644 ipa-compliance.cron %{buildroot}%{_sysconfdir}/cron.d/ipa-compliance
%endif
%clean
@@ -348,6 +350,8 @@ fi
%{_sbindir}/ipa_kpasswd
%{_sbindir}/ipactl
%{_sbindir}/ipa-upgradeconfig
%{_sbindir}/ipa-compliance
%{_sysconfdir}/cron.d/ipa-compliance
%attr(755,root,root) %{_initrddir}/ipa
%attr(755,root,root) %{_initrddir}/ipa_kpasswd
%dir %{python_sitelib}/ipaserver
@@ -410,6 +414,7 @@ fi
%{_mandir}/man1/ipa-dns-install.1.gz
%{_mandir}/man8/ipa_kpasswd.8.gz
%{_mandir}/man8/ipactl.8.gz
%{_mandir}/man1/ipa-compliance.1.gz
%files server-selinux
%defattr(-,root,root,-)

View File

@@ -11,8 +11,10 @@ attributeTypes: (2.16.840.1.113730.3.8.3.2 NAME 'ipaClientVersion' DESC 'Text st
attributeTypes: (2.16.840.1.113730.3.8.3.3 NAME 'enrolledBy' DESC 'DN of administrator who performed manual enrollment of the host' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 X-ORIGIN 'IPA v2' )
attributeTypes: (2.16.840.1.113730.3.8.3.4 NAME 'fqdn' DESC 'FQDN' EQUALITY caseIgnoreMatch ORDERING caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'IPA v2' )
attributeTypes: (2.16.840.1.113730.3.8.3.18 NAME 'managedBy' DESC 'DNs of entries allowed to manage' SUP distinguishedName EQUALITY distinguishedNameMatch ORDERING distinguishedNameMatch SUBSTR distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 X-ORIGIN 'IPA v2')
attributeTypes: (2.16.840.1.113730.3.8.3.24 NAME 'ipaEntitlementId' DESC 'Entitlement Unique identifier' EQUALITY caseIgnoreMatch ORDERING caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'IPA v2' )
objectClasses: (2.16.840.1.113730.3.8.4.1 NAME 'ipaHost' AUXILIARY MUST ( fqdn ) MAY ( userPassword $ ipaClientVersion $ enrolledBy $ memberOf) X-ORIGIN 'IPA v2' )
objectClasses: (2.16.840.1.113730.3.8.4.12 NAME 'ipaObject' DESC 'IPA objectclass' AUXILIARY MUST ( ipaUniqueId ) X-ORIGIN 'IPA v2' )
objectClasses: (2.16.840.1.113730.3.8.4.14 NAME 'ipaEntitlement' DESC 'IPA Entitlement object' AUXILIARY MUST ( ipaEntitlementId ) MAY ( userPKCS12 $ userCertificate ) X-ORIGIN 'IPA v2' )
objectClasses: (2.16.840.1.113730.3.8.4.15 NAME 'ipaPermission' DESC 'IPA Permission objectclass' AUXILIARY MAY ( ipaPermissionType ) X-ORIGIN 'IPA v2' )
objectClasses: (2.16.840.1.113730.3.8.4.2 NAME 'ipaService' DESC 'IPA service objectclass' AUXILIARY MAY ( memberOf $ managedBy ) X-ORIGIN 'IPA v2' )
objectClasses: (2.16.840.1.113730.3.8.4.3 NAME 'nestedGroup' DESC 'Group that supports nesting' SUP groupOfNames STRUCTURAL MAY memberOf X-ORIGIN 'IPA v2' )

View File

@@ -3,7 +3,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 || userPKCS12")(version 3.0; acl "Enable Anonymous access"; allow (read, search, compare) userdn = "ldap:///anyone";)
aci: (targetattr = "memberOf || memberHost || memberUser")(version 3.0; acl "No anonymous access to member information"; deny (read,search,compare) userdn != "ldap:///all";)
aci: (targetattr != "userPassword || krbPrincipalKey || sambaLMPassword || sambaNTPassword || passwordHistory || krbMKey || krbPrincipalName || krbCanonicalName || krbUPEnabled || krbTicketPolicyReference || krbPrincipalExpiration || krbPasswordExpiration || krbPwdPolicyReference || krbPrincipalType || krbPwdHistory || krbLastPwdChange || krbPrincipalAliases || krbExtraData || krbLastSuccessfulAuth || krbLastFailedAuth || krbLoginFailedCount || krbTicketFlags || 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")(version 3.0; acl "selfservice:Self can write own password"; allow (write) userdn="ldap:///self";)

View File

@@ -37,6 +37,23 @@ objectClass: nestedgroup
cn: helpdesk
description: Helpdesk
dn: cn=Entitlement Management,cn=roles,cn=accounts,$SUFFIX
changetype: add
objectClass: top
objectClass: groupofnames
objectClass: nestedgroup
cn: entitlements
description: Entitlements administrator
dn: cn=Entitlement Compliance,cn=roles,cn=accounts,$SUFFIX
changetype: add
objectClass: top
objectClass: groupofnames
objectClass: nestedgroup
cn: Entitlement Compliance
description: Verify entitlement compliance
member: fqdn=$FQHN,cn=computers,cn=accounts,$SUFFIX
############################################
# Add the default privileges
############################################
@@ -129,13 +146,23 @@ objectClass: nestedgroup
cn: Host Enrollment
description: Host Enrollment
dn: cn=entitlementadmin,cn=privileges,cn=pbac,$SUFFIX
dn: cn=Register and Write Entitlements,cn=privileges,cn=pbac,$SUFFIX
changetype: add
objectClass: top
objectClass: groupofnames
objectClass: nestedgroup
cn: entitlementadmin
description: Entitlement Administrators
cn: Register and Write Entitlements
member: cn=Entitlement Management,cn=roles,cn=accounts,$SUFFIX
dn: cn=Read Entitlements,cn=privileges,cn=pbac,$SUFFIX
changetype: add
objectClass: top
objectClass: groupofnames
objectClass: nestedgroup
cn: Read Entitlements
member: cn=Entitlement Management,cn=roles,cn=accounts,$SUFFIX
member: cn=Entitlement Compliance,cn=roles,cn=accounts,$SUFFIX
############################################
# Default permissions.
@@ -486,30 +513,28 @@ member: cn=Replication Administrators,cn=privileges,cn=pbac,$SUFFIX
# Entitlement management
dn: cn=addentitlements,cn=permissions,cn=pbac,$SUFFIX
dn: cn=Register Entitlements,cn=permissions,cn=pbac,$SUFFIX
changetype: add
objectClass: top
objectClass: groupofnames
objectClass: ipapermission
cn: addentitlements
description: Add Entitlements
member: cn=entitlementadmin,cn=privileges,cn=pbac,$SUFFIX
member: cn=Register and Write Entitlements,cn=privileges,cn=pbac,$SUFFIX
dn: cn=removeentitlements,cn=permissions,cn=pbac,$SUFFIX
dn: cn=Read Entitlements,cn=permissions,cn=pbac,$SUFFIX
changetype: add
objectClass: top
objectClass: groupofnames
cn: removeentitlements
description: Remove Entitlements
member: cn=entitlementadmin,cn=privileges,cn=pbac,$SUFFIX
objectClass: ipapermission
cn: Read Entitlements
member: cn=Read Entitlements,cn=privileges,cn=pbac,$SUFFIX
dn: cn=modifyentitlements,cn=permissions,cn=pbac,$SUFFIX
dn: cn=Write Entitlements,cn=permissions,cn=pbac,$SUFFIX
changetype: add
objectClass: top
objectClass: groupofnames
cn: modifyentitlements
description: Modify Entitlements
member: cn=entitlementadmin,cn=privileges,cn=pbac,$SUFFIX
objectClass: ipapermission
cn: Write Entitlements
member: cn=Register and Write Entitlements,cn=privileges,cn=pbac,$SUFFIX
############################################
# Default permissions (ACIs)
@@ -631,17 +656,17 @@ aci: (targetattr = "enrolledby || objectclass")(target = "ldap:///fqdn=*,cn=comp
dn: $SUFFIX
changetype: modify
add: aci
aci: (target = "ldap:///ipauniqueid=*,cn=entitlements,cn=etc,$SUFFIX")(version 3.0;acl "permission:addentitlements";allow (add) groupdn = "ldap:///cn=addentitlements,cn=permissions,cn=pbac,$SUFFIX";)
aci: (target = "ldap:///ipaentitlementid=*,cn=entitlements,cn=etc,$SUFFIX")(version 3.0;acl "Register Entitlements";allow (add) groupdn = "ldap:///cn=Register Entitlements,cn=permissions,cn=pbac,$SUFFIX";)
dn: $SUFFIX
changetype: modify
add: aci
aci: (targetattr = "usercertificate")(target = "ldap:///ipauniqueid=*,cn=entitlements,cn=etc,$SUFFIX")(version 3.0;acl "permission:modifyentitlements";allow (write) groupdn = "ldap:///cn=modifyentitlements,cn=permissions,cn=pbac,$SUFFIX";)
aci: (targetattr = "usercertificate")(target = "ldap:///ipaentitlement=*,cn=entitlements,cn=etc,$SUFFIX")(version 3.0;acl "Write Entitlements";allow (write) groupdn = "ldap:///cn=Write entitlements,cn=permissions,cn=pbac,$SUFFIX";)
dn: $SUFFIX
changetype: modify
add: aci
aci: (target = "ldap:///ipauniqueid=*,cn=entitlements,cn=etc,$SUFFIX")(version 3.0;acl "permission:removeentitlements";allow (delete) groupdn = "ldap:///cn=removeentitlements,cn=permissions,cn=pbac,$SUFFIX";)
aci: (targetattr = "userpkcs12")(target = "ldap:///ipaentitlementid=*,cn=entitlements,cn=etc,$SUFFIX")(version 3.0;acl "Read Entitlements";allow (read) groupdn = "ldap:///cn=Read Entitlements,cn=permissions,cn=pbac,$SUFFIX";)
# Create virtual operations entry. This is used to control access to
# operations that don't rely on LDAP directly.

View File

@@ -17,6 +17,7 @@ sbin_SCRIPTS = \
ipa-host-net-manage \
ipa-ldap-updater \
ipa-upgradeconfig \
ipa-compliance \
$(NULL)
EXTRA_DIST = \

View File

@@ -0,0 +1,193 @@
#!/usr/bin/env python
#
# Authors:
# Rob Crittenden <rcritten@redhat.com>
#
# Copyright (C) 2010 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/>.
#
# An LDAP client to count entitlements and log to syslog if the number is
# exceeded.
try:
import sys
import os
import syslog
import tempfile
import krbV
import base64
import shutil
from rhsm.certificate import EntitlementCertificate
from ipaserver.plugins.ldap2 import ldap2
from ipalib import api, errors, backend
except ImportError, e:
# If python-rhsm isn't installed exit gracefully and quietly.
if e.args[0] == 'No module named rhsm.certificate':
sys.exit(0)
print >> sys.stderr, """\
There was a problem importing one of the required Python modules. The
error was:
%s
""" % sys.exc_value
sys.exit(1)
# Each IPA server comes with this many entitlements
DEFAULT_ENTITLEMENTS = 25
class client(backend.Executioner):
"""
A simple-minded IPA client that can execute remote commands.
"""
def run(self, method, **kw):
self.create_context()
result = self.execute(method, **kw)
return result
def parse_options():
from optparse import OptionParser
parser = OptionParser()
parser.add_option("--debug", dest="debug", action="store_true",
default=False, help="enable debugging")
options, args = parser.parse_args()
return options, args
def check_compliance(tmpdir, debug=False):
cfg = dict(
context='cli',
in_server=False,
debug=debug,
verbose=0,
)
api.bootstrap(**cfg)
api.register(client)
api.finalize()
from ipalib.plugins.service import normalize_certificate, make_pem
try:
# Create a new credentials cache for this tool. This executes
# using the systems host principal.
ccache_file = 'FILE:%s/ccache' % tmpdir
krbcontext = krbV.default_context()
principal = str('host/%s@%s' % (api.env.host, api.env.realm))
keytab = krbV.Keytab(name='/etc/krb5.keytab', context=krbcontext)
principal = krbV.Principal(name=principal, context=krbcontext)
os.environ['KRB5CCNAME'] = ccache_file
ccache = krbV.CCache(name=ccache_file, context=krbcontext, primary_principal=principal)
ccache.init(principal)
ccache.init_creds_keytab(keytab=keytab, principal=principal)
except krbV.Krb5Error, e:
raise StandardError('Error initializing principal %s in %s: %s' % (principal.name, '/etc/krb5.keytab', str(e)))
# entitle-sync doesn't return any information we want to see, it just
# needs to be done so the LDAP data is correct.
try:
result = api.Backend.client.run('entitle_sync')
except errors.NotRegisteredError:
# Even if not registered they have some default entitlements
pass
ldapuri = 'ldap://%s' % api.env.host
conn = ldap2(shared_instance=False, ldap_uri=ldapuri)
# Bind using GSSAPI
conn.connect(ccache=ccache_file)
hostcount = 0
# Get the hosts first
try:
(entries, truncated) = conn.find_entries('(krblastpwdchange=*)', ['dn'],
'%s,%s' % (api.env.container_host, api.env.basedn),
conn.SCOPE_ONELEVEL,
size_limit = -1)
except errors.NotFound:
# No hosts
pass
if not truncated:
hostcount = len(entries)
else:
# This will not happen unless we bump into a server-side limit.
msg = 'The host count result was truncated, they will be underreported'
syslog.syslog(syslog.LOG_ERR, msg)
if sys.stdin.isatty():
print msg
available = 0
try:
(entries, truncated) = conn.find_entries('(objectclass=ipaentitlement)',
['dn', 'userCertificate'],
'%s,%s' % (api.env.container_entitlements, api.env.basedn),
conn.SCOPE_ONELEVEL,
size_limit = -1)
for entry in entries:
(dn, attrs) = entry
if 'usercertificate' in attrs:
rawcert = attrs['usercertificate'][0]
rawcert = normalize_certificate(rawcert)
cert = make_pem(base64.b64encode(rawcert))
cert = EntitlementCertificate(cert)
order = cert.getOrder()
available += int(order.getQuantityUsed())
except errors.NotFound:
pass
conn.disconnect()
available += DEFAULT_ENTITLEMENTS
if hostcount > available:
syslog.syslog(syslog.LOG_ERR, 'IPA is out of compliance: %d of %d entitlements used.' % (hostcount, available))
if sys.stdin.isatty():
print 'IPA is out of compliance: %d of %d entitlements used.' % (hostcount, available)
else:
if sys.stdin.isatty():
# If run from the command-line display some info
print 'IPA is in compliance: %d of %d entitlements used.' % (hostcount, available)
def main():
if os.getegid() != 0:
sys.exit("Must be root to check compliance")
if not os.path.exists('/etc/ipa/default.conf'):
return 0
options, args = parse_options()
try:
tmpdir = tempfile.mkdtemp(prefix = "tmp-")
try:
check_compliance(tmpdir, options.debug)
finally:
shutil.rmtree(tmpdir)
except KeyboardInterrupt:
return 1
except (StandardError, errors.PublicError), e:
syslog.syslog(syslog.LOG_ERR, 'IPA compliance checking failed: %s' % str(e))
if sys.stdin.isatty():
print 'IPA compliance checking failed: %s' % str(e)
return 1
return 0
sys.exit(main())

View File

@@ -14,7 +14,8 @@ man1_MANS = \
ipa-ldap-updater.1 \
ipa-compat-manage.1 \
ipa-nis-manage.1 \
ipa-host-net-manage.1
ipa-host-net-manage.1 \
ipa-compliance.1
man8_MANS = \
ipactl.8 \

View File

@@ -0,0 +1,45 @@
.\" A man page for ipa-compliance
.\" Copyright (C) 2010 Red Hat, Inc.
.\"
.\" This is free software; you can redistribute it and/or modify it under
.\" the terms of the GNU Library General Public License as published by
.\" the Free Software Foundation; version 2 only
.\"
.\" 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 Library General Public
.\" License along with this program; if not, write to the Free Software
.\" Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
.\"
.\" Author: Rob Crittenden <rcritten@redhat.com>
.\"
.TH "ipa-compliance" "1" "Dec 14 2010" "freeipa" ""
.SH "NAME"
ipa\-compliance \- Check entitlement compliance
.SH "SYNOPSIS"
ipa\-compliance [\fIOPTION\fR]
.SH "DESCRIPTION"
Verify that the IPA installation is in compliance with the number of client entitlements it has.
Entitlements are managed using the ipa entitle command.
An enrolled host is an machine that has a host keytab in the IPA system.
The entitlements take the form of x509v3 certificates. The certificates are examined and the quantities summed. This is compared to the number of enrolled hosts to determine compliance.
The command logs to syslog and if run from a tty will log to the terminal as well.
The IPA server provides 25 entitlements of its own.
.SH "OPTIONS"
.TP
\fB\-\-\-debug\fR
Enable debugging output in the command
.SH "EXIT STATUS"
0 if the command was successful
1 if an error occurred
.SH "NOTES"
Entitlements are not checked if the python\-rhsm package is not installed.

5
ipa-compliance.cron Normal file
View File

@@ -0,0 +1,5 @@
SHELL=/bin/bash
PATH=/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=root
HOME=/
0 0-23/4 * * * root /usr/sbin/ipa-compliance

View File

@@ -1022,15 +1022,21 @@ class cli(backend.Executioner):
"""
for p in cmd.params():
if isinstance(p, File):
# FIXME: this only reads the first file
raw = None
if p.name in kw:
if type(kw[p.name]) in (tuple, list):
fname = kw[p.name][0]
else:
fname = kw[p.name]
try:
f = open(kw[p.name], 'r')
f = open(fname, 'r')
raw = f.read()
f.close()
except IOError, e:
raise ValidationError(
name=to_cli(p.cli_name),
error='%s: %s:' % (kw[p.name], e[1])
error='%s: %s:' % (fname, e[1])
)
elif p.stdin_if_missing:
try:
@@ -1039,6 +1045,10 @@ class cli(backend.Executioner):
raise ValidationError(
name=to_cli(p.cli_name), error=e[1]
)
if not raw:
raise ValidationError(
name=to_cli(p.cli_name), error=_('No file to read')
)
kw[p.name] = self.Backend.textui.decode(raw)

View File

@@ -103,6 +103,7 @@ DEFAULT_CONFIG = (
('container_sudorule', 'cn=sudorules,cn=sudo'),
('container_sudocmd', 'cn=sudocmds,cn=sudo'),
('container_sudocmdgroup', 'cn=sudocmdgroups,cn=sudo'),
('container_entitlements', 'cn=entitlements,cn=etc'),
# Ports, hosts, and URIs:
# FIXME: let's renamed xmlrpc_uri to rpc_xml_uri

View File

@@ -1406,7 +1406,7 @@ class CertificateFormatError(CertificateError):
class MutuallyExclusiveError(ExecutionError):
"""
**4302** Raised when an operation would result in setting two attributes which are mutually exlusive.
**4303** Raised when an operation would result in setting two attributes which are mutually exlusive.
For example:
@@ -1417,13 +1417,13 @@ class MutuallyExclusiveError(ExecutionError):
"""
errno = 4302
errno = 4303
format = _('%(reason)s')
class NonFatalError(ExecutionError):
"""
**4303** Raised when part of an operation succeeds and the part that failed isn't critical.
**4304** Raised when part of an operation succeeds and the part that failed isn't critical.
For example:
@@ -1434,10 +1434,43 @@ class NonFatalError(ExecutionError):
"""
errno = 4303
errno = 4304
format = _('%(reason)s')
class AlreadyRegisteredError(ExecutionError):
"""
**4305** Raised when registering a user that is already registered.
For example:
>>> raise AlreadyRegisteredError()
Traceback (most recent call last):
...
AlreadyRegisteredError: Already registered
"""
errno = 4305
format = _('Already registered')
class NotRegisteredError(ExecutionError):
"""
**4306** Raised when not registered and a registration is required
For example:
>>> raise NotRegisteredError()
Traceback (most recent call last):
...
NotRegisteredError: Not registered yet
"""
errno = 4306
format = _('Not registered yet')
##############################################################################
# 5000 - 5999: Generic errors

750
ipalib/plugins/entitle.py Normal file
View File

@@ -0,0 +1,750 @@
# Authors:
# Rob Crittenden <rcritten@redhat.com>
#
# Copyright (C) 2010 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/>.
"""
Entitlements
Manage entitlements for client machines
Entitlements can be managed either by registering with an entitlement
server with a username and password or by manually importing entitlement
certificates. An entitlement certificate contains embedded information
such as the product being entitled, the quantity and the validity dates.
An entitlement server manages the number of client entitlements available.
To mark these entitlements as used by the IPA server you provide a quantity
and they are marked as consumed on the entitlement server.
Register with an entitlement server:
ipa entitle-register consumer
Import an entitlement certificate:
ipa entitle-import /home/user/ipaclient.pem
Display current entitlements:
ipa entitle-status
Retrieve details on entitlement certificates:
ipa entitle-get
Consume some entitlements from the entitlement server:
ipa entitle-consume 50
The registration ID is a Unique Identifier (UUID). This ID will be
IMPORTED if you have used entitle-import.
Changes to /etc/rhsm/rhsm.conf require a restart of the httpd service.
"""
from ipalib import api, SkipPluginModule
try:
from rhsm.connection import *
from rhsm.certificate import EntitlementCertificate
from ipapython import ipautil
import base64
from ipalib.plugins.service import validate_certificate, normalize_certificate
if api.env.in_server and api.env.context in ['lite', 'server']:
from ipaserver.install.certs import NSS_DIR
except ImportError, e:
raise SkipPluginModule(reason=str(e))
import os
from ipalib import api, errors
from ipalib import Flag, Int, Str, Password, File
from ipalib.plugins.baseldap import *
from ipalib.plugins.virtual import *
from ipalib import _, ngettext
from ipalib.output import Output, standard_list_of_entries
from ipalib.request import context
import tempfile
import shutil
import socket
from OpenSSL import crypto
import M2Crypto
from ipapython.ipautil import run
from ipalib.request import context
import locale
def read_file(filename):
fp = open(filename, 'r')
data = fp.readlines()
fp.close()
data = ''.join(data)
return data
def write_file(filename, pem):
cert_file = open(filename, 'w')
cert_file.write(pem)
cert_file.close()
def read_pkcs12_pin():
pwdfile = '%s/pwdfile.txt' % NSS_DIR
fp = open(pwdfile, 'r')
pwd = fp.read()
fp.close()
return pwd
def make_pem(data):
"""
The M2Crypto/openSSL modules are very picky about PEM format and
require lines split to 64 characters with proper headers.
"""
cert = '\n'.join([data[x:x+64] for x in range(0, len(data), 64)])
return '-----BEGIN CERTIFICATE-----\n' + \
cert + \
'\n-----END CERTIFICATE-----'
def get_pool(ldap):
"""
Get our entitlement pool. Assume there is only one pool.
"""
db = None
try:
(db, uuid, certfile, keyfile) = get_uuid(ldap)
if db is None:
# db is None means manual registration
return (None, uuid)
cp = UEPConnection(handler='/candlepin', cert_file=certfile, key_file=keyfile)
pools = cp.getPoolsList(uuid)
poolid = pools[0]['id']
pool = cp.getPool(poolid)
finally:
if db:
shutil.rmtree(db, ignore_errors=True)
return (pool, uuid)
def get_uuid(ldap):
"""
Retrieve our UUID, certificate and key from LDAP.
Except on error the caller is responsible for removing temporary files
"""
db = None
try:
db = tempfile.mkdtemp(prefix = "tmp-")
registrations = api.Command['entitle_find'](all=True)
if registrations['count'] == 0:
shutil.rmtree(db, ignore_errors=True)
raise errors.NotRegisteredError()
result = registrations['result'][0]
uuid = str(result['ipaentitlementid'][0])
entry_attrs = dict(ipaentitlementid=uuid)
dn = ldap.make_dn(
entry_attrs, 'ipaentitlementid', api.env.container_entitlements,
)
if not ldap.can_read(dn, 'userpkcs12'):
raise errors.ACIError(info='not allowed to perform this command')
if not 'userpkcs12' in result:
return (None, uuid, None, None)
data = result['userpkcs12'][0]
pkcs12 = crypto.load_pkcs12(data, read_pkcs12_pin())
cert = pkcs12.get_certificate()
key = pkcs12.get_privatekey()
write_file(db + '/cert.pem',
crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
write_file(db + '/key.pem',
crypto.dump_privatekey(crypto.FILETYPE_PEM, key))
except Exception, e:
if db is not None:
shutil.rmtree(db, ignore_errors=True)
raise e
return (db, uuid, db + '/cert.pem', db + '/key.pem')
output_params = (
Str('ipaentitlementid?',
label='UUID',
),
Str('usercertificate',
label=_('Certificate'),
),
)
class entitle(LDAPObject):
"""
Entitlement object
"""
container_dn = api.env.container_entitlements
object_name = 'entitlement'
object_name_plural = 'entitlements'
object_class = ['ipaobject', 'ipaentitlement']
search_attributes = ['usercertificate']
default_attributes = ['ipaentitlement']
uuid_attribute = 'ipaentitlementid'
"""
def get_dn(self, *keys, **kwargs):
try:
(dn, entry_attrs) = self.backend.find_entry_by_attr(
self.primary_key.name, keys[-1], self.object_class, [''],
self.container_dn
)
except errors.NotFound:
dn = super(entitle, self).get_dn(*keys, **kwargs)
return dn
"""
api.register(entitle)
class entitle_status(VirtualCommand):
"""
Display current entitlements
"""
operation="show entitlement"
has_output_params = (
Str('uuid',
label=_('UUID'),
),
Str('product',
label=_('Product'),
),
Int('quantity',
label=_('Quantity'),
),
Int('consumed',
label=_('Consumed'),
),
)
has_output = (
Output('result',
type=dict,
doc=_('Dictionary mapping variable name to value'),
),
)
def execute(self, *keys, **kw):
ldap = self.api.Backend.ldap2
os.environ['LANG'] = 'en_US'
locale.setlocale(locale.LC_ALL, '')
(pool, uuid) = get_pool(ldap)
if pool is None:
# This assumes there is only 1 product
quantity = 0
product = ''
registrations = api.Command['entitle_find'](all=True)['result'][0]
if u'usercertificate' in registrations:
certs = registrations['usercertificate']
for cert in certs:
cert = make_pem(base64.b64encode(cert))
try:
pc = EntitlementCertificate(cert)
o = pc.getOrder()
if o.getQuantityUsed():
quantity = quantity + int(o.getQuantityUsed())
product = o.getName()
except M2Crypto.X509.X509Error, e:
self.error('Invalid entitlement certificate, skipping.')
pool = dict(productId=product, quantity=quantity,
consumed=quantity, uuid=unicode(uuid))
result={'product': unicode(pool['productId']),
'quantity': pool['quantity'],
'consumed': pool['consumed'],
'uuid': unicode(uuid),
}
return dict(
result=result
)
api.register(entitle_status)
class entitle_consume(LDAPUpdate):
"""
Consume an entitlement
"""
operation="consume entitlement"
msg_summary = _('Consumed %(value)s entitlement(s).')
takes_args = (
Int('quantity',
label=_('Quantity'),
minvalue=1,
),
)
# We don't want rights or add/setattr
takes_options = (
# LDAPUpdate requires at least one option so autofill one
# This isn't otherwise used.
Int('hidden',
label=_('Quantity'),
minvalue=1,
autofill=True,
default=1,
flags=['no_option', 'no_output']
),
)
has_output_params = output_params + (
Str('product',
label=_('Product'),
),
Int('consumed',
label=_('Consumed'),
),
)
def execute(self, *keys, **options):
"""
Override this so we can set value to the number of entitlements
consumed.
"""
result = super(entitle_consume, self).execute(*keys, **options)
result['value'] = unicode(keys[-1])
return result
def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
quantity = keys[-1]
os.environ['LANG'] = 'en_US'
locale.setlocale(locale.LC_ALL, '')
(db, uuid, certfile, keyfile) = get_uuid(ldap)
entry_attrs['ipaentitlementid'] = uuid
dn = ldap.make_dn(
entry_attrs, self.obj.uuid_attribute, self.obj.container_dn
)
if db is None:
raise errors.NotRegisteredError()
try:
(pool, uuid) = get_pool(ldap)
result=api.Command['entitle_status']()['result']
available = result['quantity'] - result['consumed']
if quantity > available:
raise errors.ValidationError(name='quantity', error='There are only %d entitlements left' % available)
try:
cp = UEPConnection(handler='/candlepin', cert_file=certfile, key_file=keyfile)
cp.bindByEntitlementPool(uuid, pool['id'], quantity=quantity)
except RestlibException, e:
raise errors.ACIError(info=e.msg)
results = cp.getCertificates(uuid)
usercertificate = []
for cert in results:
usercertificate.append(normalize_certificate(cert['cert']))
entry_attrs['usercertificate'] = usercertificate
entry_attrs['ipaentitlementid'] = uuid
finally:
if db:
shutil.rmtree(db, ignore_errors=True)
return dn
def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
"""
Returning the certificates isn't very interesting. Return the
status of entitlements instead.
"""
if 'usercertificate' in entry_attrs:
del entry_attrs['usercertificate']
if 'userpkcs12' in entry_attrs:
del entry_attrs['userpkcs12']
result = api.Command['entitle_status']()
for attr in result['result']:
entry_attrs[attr] = result['result'][attr]
return dn
api.register(entitle_consume)
class entitle_get(VirtualCommand):
"""
Retrieve the entitlement certs
"""
operation="retrieve entitlement"
has_output_params = (
Str('product',
label=_('Product'),
),
Int('quantity',
label=_('Quantity'),
),
Str('start',
label=_('Start'),
),
Str('end',
label=_('End'),
),
Str('serial',
label=_('Serial Number'),
),
)
has_output = output.standard_list_of_entries
def execute(self, *keys, **kw):
ldap = self.api.Backend.ldap2
os.environ['LANG'] = 'en_US'
locale.setlocale(locale.LC_ALL, '')
(db, uuid, certfile, keyfile) = get_uuid(ldap)
if db is None:
quantity = 0
product = ''
registrations = api.Command['entitle_find'](all=True)['result'][0]
certs = []
if u'usercertificate' in registrations:
# make it look like a UEP cert
for cert in registrations['usercertificate']:
certs.append(dict(cert = make_pem(base64.b64encode(cert))))
else:
try:
cp = UEPConnection(handler='/candlepin', cert_file=certfile, key_file=keyfile)
certs = cp.getCertificates(uuid)
finally:
if db:
shutil.rmtree(db, ignore_errors=True)
entries = []
for c in certs:
try:
pc = EntitlementCertificate(c['cert'])
except M2Crypto.X509.X509Error:
raise errors.CertificateFormatError(error=_('Not an entitlement certificate'))
order = pc.getOrder()
quantity = 0
if order.getQuantityUsed():
quantity = order.getQuantityUsed()
result={'product': unicode(order.getName()),
'quantity': int(order.getQuantityUsed()),
'start': unicode(order.getStart()),
'end': unicode(order.getEnd()),
'serial': unicode(pc.serialNumber()),
'certificate': unicode(c['cert']),
}
entries.append(result)
del pc
del order
return dict(
result=entries,
count=len(entries),
truncated=False,
)
api.register(entitle_get)
class entitle_find(LDAPSearch):
"""
Search for entitlement accounts.
"""
has_output_params = output_params
INTERNAL = True
def post_callback(self, ldap, entries, truncated, *args, **options):
if len(entries) == 0:
raise errors.NotRegisteredError()
api.register(entitle_find)
class entitle_register(LDAPCreate):
"""
Register to the entitlement system
"""
operation="register entitlement"
msg_summary = _('Registered to entitlement server.')
takes_args = (
Str('username',
label=_('Username'),
),
)
takes_options = LDAPCreate.takes_options + (
Str('ipaentitlementid?',
label='UUID',
doc=_('Enrollment UUID'),
flags=['no_create', 'no_update'],
),
Password('password',
label=_('Password'),
doc=_('Registration password'),
),
)
"""
has_output_params = (
)
has_output = (
Output('result',
type=dict,
doc=_('Dictionary mapping variable name to value'),
),
)
"""
def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
dn = '%s,%s' % (self.obj.container_dn, self.api.env.basedn)
if not ldap.can_add(dn):
raise errors.ACIError(info='No permission to register')
os.environ['LANG'] = 'en_US'
locale.setlocale(locale.LC_ALL, '')
try:
registrations = api.Command['entitle_find']()
raise errors.AlreadyRegisteredError()
except errors.NotRegisteredError:
pass
try:
admin_cp = UEPConnection(handler='/candlepin', username=keys[-1], password=options.get('password'))
result = admin_cp.registerConsumer(name=api.env.realm, type="domain")
uuid = result['uuid']
db = None
try:
# Create a PKCS#12 file to store the private key and
# certificate in LDAP. Encrypt using the Apache cert
# database password.
db = tempfile.mkdtemp(prefix = "tmp-")
write_file(db + '/in.cert', result['idCert']['cert'])
write_file(db + '/in.key', result['idCert']['key'])
args = ['/usr/bin/openssl', 'pkcs12',
'-export',
'-in', db + '/in.cert',
'-inkey', db + '/in.key',
'-out', db + '/out.p12',
'-name', 'candlepin',
'-passout', 'pass:%s' % read_pkcs12_pin()
]
(stdout, stderr, rc) = run(args, raiseonerr=False)
pkcs12 = read_file(db + '/out.p12')
entry_attrs['ipaentitlementid'] = uuid
entry_attrs['userpkcs12'] = pkcs12
finally:
if db is not None:
shutil.rmtree(db, ignore_errors=True)
except RestlibException, e:
if e.code == 401:
raise errors.ACIError(info=e.msg)
else:
raise e
except socket.gaierror:
raise errors.ACIError(info=e.args[1])
dn = ldap.make_dn(
entry_attrs, self.obj.uuid_attribute, self.obj.container_dn
)
return dn
api.register(entitle_register)
class entitle_import(LDAPUpdate):
"""
Import an entitlement certificate.
"""
has_output_params = (
Str('product',
label=_('Product'),
),
Int('quantity',
label=_('Quantity'),
),
Int('consumed',
label=_('Consumed'),
),
)
has_output = (
Output('result',
type=dict,
doc=_('Dictionary mapping variable name to value'),
),
)
takes_args = (
File('usercertificate*', validate_certificate,
cli_name='certificate_file',
),
)
# any update requires at least 1 option to be set so force an invisible
# one here by setting the uuid.
takes_options = LDAPCreate.takes_options + (
Str('uuid?',
label=_('UUID'),
doc=_('Enrollment UUID'),
flags=['no_create', 'no_update'],
autofill=True,
default=u'IMPORTED',
),
)
def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
try:
(db, uuid, certfile, keyfile) = get_uuid(ldap)
if db is not None:
raise errors.AlreadyRegisteredError()
except errors.NotRegisteredError:
pass
try:
entry_attrs['ipaentitlementid'] = unicode('IMPORTED')
newcert = normalize_certificate(keys[-1][0])
cert = make_pem(base64.b64encode(newcert))
try:
pc = EntitlementCertificate(cert)
o = pc.getOrder()
if o is None:
raise errors.CertificateFormatError(error=_('Not an entitlement certificate'))
except M2Crypto.X509.X509Error:
raise errors.CertificateFormatError(error=_('Not an entitlement certificate'))
dn = 'ipaentitlementid=%s,%s' % (entry_attrs['ipaentitlementid'], dn)
(dn, current_attrs) = ldap.get_entry(
dn, ['*'], normalize=self.obj.normalize_dn
)
entry_attrs['usercertificate'] = current_attrs['usercertificate']
entry_attrs['usercertificate'].append(newcert)
except errors.NotFound:
# First import, create the entry
entry_attrs['ipaentitlementid'] = unicode('IMPORTED')
entry_attrs['objectclass'] = self.obj.object_class
entry_attrs['usercertificate'] = normalize_certificate(keys[-1][0])
ldap.add_entry(dn, entry_attrs)
setattr(context, 'entitle_import', True)
return dn
def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs):
"""
If we are adding the first entry there are no updates so EmptyModlist
will get thrown. Ignore it.
"""
if isinstance(exc, errors.EmptyModlist):
if not getattr(context, 'entitle_import', False):
raise exc
return (call_args, {})
else:
raise exc
def execute(self, *keys, **options):
super(entitle_import, self).execute(*keys, **options)
return dict(
result=api.Command['entitle_status']()['result']
)
api.register(entitle_import)
class entitle_sync(LDAPUpdate):
"""
Re-sync the local entitlement cache with the entitlement server
"""
operation="sync entitlement"
msg_summary = _('Entitlement(s) synchronized.')
# We don't want rights or add/setattr
takes_options = (
# LDAPUpdate requires at least one option so autofill one
# This isn't otherwise used.
Int('hidden',
label=_('Quantity'),
minvalue=1,
autofill=True,
default=1,
flags=['no_option', 'no_output']
),
)
has_output_params = output_params + (
Str('product',
label=_('Product'),
),
Int('consumed',
label=_('Consumed'),
),
)
def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
os.environ['LANG'] = 'en_US'
locale.setlocale(locale.LC_ALL, '')
(db, uuid, certfile, keyfile) = get_uuid(ldap)
if db is None:
raise errors.NotRegisteredError()
try:
(pool, uuid) = get_pool(ldap)
cp = UEPConnection(handler='/candlepin', cert_file=certfile, key_file=keyfile)
results = cp.getCertificates(uuid)
usercertificate = []
for cert in results:
usercertificate.append(normalize_certificate(cert['cert']))
entry_attrs['usercertificate'] = usercertificate
entry_attrs['ipaentitlementid'] = uuid
finally:
if db:
shutil.rmtree(db, ignore_errors=True)
dn = ldap.make_dn(
entry_attrs, self.obj.uuid_attribute, self.obj.container_dn
)
return dn
def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
"""
Returning the certificates isn't very interesting. Return the
status of entitlements instead.
"""
if 'usercertificate' in entry_attrs:
del entry_attrs['usercertificate']
if 'userpkcs12' in entry_attrs:
del entry_attrs['userpkcs12']
result = api.Command['entitle_status']()
for attr in result['result']:
entry_attrs[attr] = result['result'][attr]
return dn
def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs):
if isinstance(exc, errors.EmptyModlist):
# If there is nothing to change we are already synchronized.
return
raise exc
api.register(entitle_sync)

View File

@@ -177,6 +177,11 @@ def normalize_certificate(cert):
if not cert:
return cert
s = cert.find('-----BEGIN CERTIFICATE-----')
if s > -1:
e = cert.find('-----END CERTIFICATE-----')
cert = cert[s+27:e]
if util.isvalid_base64(cert):
try:
cert = base64.b64decode(cert)

View File

@@ -683,6 +683,20 @@ class ldap2(CrudBackend, Encoder):
return False
@encode_args(1, 2)
def can_read(self, dn, attr):
"""Returns True/False if the currently bound user has read permissions
on the attribute. This only operates on a single attribute at a time.
"""
(dn, attrs) = self.get_effective_rights(dn, [attr])
if 'attributelevelrights' in attrs:
attr_rights = attrs.get('attributelevelrights')[0].decode('UTF-8')
(attr, rights) = attr_rights.split(':')
if 'r' in rights:
return True
return False
#
# Entry-level effective rights
#