diff options
-rw-r--r-- | freeipa.spec.in | 5 | ||||
-rw-r--r-- | install/share/60basev2.ldif | 2 | ||||
-rw-r--r-- | install/share/default-aci.ldif | 2 | ||||
-rw-r--r-- | install/share/delegation.ldif | 61 | ||||
-rw-r--r-- | install/tools/Makefile.am | 1 | ||||
-rw-r--r-- | install/tools/ipa-compliance | 193 | ||||
-rw-r--r-- | install/tools/man/Makefile.am | 3 | ||||
-rw-r--r-- | install/tools/man/ipa-compliance.1 | 45 | ||||
-rw-r--r-- | ipa-compliance.cron | 5 | ||||
-rw-r--r-- | ipalib/cli.py | 14 | ||||
-rw-r--r-- | ipalib/constants.py | 1 | ||||
-rw-r--r-- | ipalib/errors.py | 41 | ||||
-rw-r--r-- | ipalib/plugins/entitle.py | 750 | ||||
-rw-r--r-- | ipalib/plugins/service.py | 5 | ||||
-rw-r--r-- | ipaserver/plugins/ldap2.py | 14 |
15 files changed, 1116 insertions, 26 deletions
diff --git a/freeipa.spec.in b/freeipa.spec.in index e35f3370b..7e91c42e1 100644 --- a/freeipa.spec.in +++ b/freeipa.spec.in @@ -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,-) diff --git a/install/share/60basev2.ldif b/install/share/60basev2.ldif index f5f7a6563..6f86f3afd 100644 --- a/install/share/60basev2.ldif +++ b/install/share/60basev2.ldif @@ -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' ) diff --git a/install/share/default-aci.ldif b/install/share/default-aci.ldif index e4f767054..7c0ae8bd8 100644 --- a/install/share/default-aci.ldif +++ b/install/share/default-aci.ldif @@ -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";) diff --git a/install/share/delegation.ldif b/install/share/delegation.ldif index 18d045d8d..a15c9ec77 100644 --- a/install/share/delegation.ldif +++ b/install/share/delegation.ldif @@ -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. diff --git a/install/tools/Makefile.am b/install/tools/Makefile.am index 70e65ee73..055a32fca 100644 --- a/install/tools/Makefile.am +++ b/install/tools/Makefile.am @@ -17,6 +17,7 @@ sbin_SCRIPTS = \ ipa-host-net-manage \ ipa-ldap-updater \ ipa-upgradeconfig \ + ipa-compliance \ $(NULL) EXTRA_DIST = \ diff --git a/install/tools/ipa-compliance b/install/tools/ipa-compliance new file mode 100644 index 000000000..8b7ad776b --- /dev/null +++ b/install/tools/ipa-compliance @@ -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()) diff --git a/install/tools/man/Makefile.am b/install/tools/man/Makefile.am index 58959c1b9..3fac378c9 100644 --- a/install/tools/man/Makefile.am +++ b/install/tools/man/Makefile.am @@ -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 \ diff --git a/install/tools/man/ipa-compliance.1 b/install/tools/man/ipa-compliance.1 new file mode 100644 index 000000000..09ce02df8 --- /dev/null +++ b/install/tools/man/ipa-compliance.1 @@ -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. diff --git a/ipa-compliance.cron b/ipa-compliance.cron new file mode 100644 index 000000000..662f560a7 --- /dev/null +++ b/ipa-compliance.cron @@ -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 diff --git a/ipalib/cli.py b/ipalib/cli.py index 5543301c0..5ef812f75 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -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) diff --git a/ipalib/constants.py b/ipalib/constants.py index 2d539fea7..202f5fa93 100644 --- a/ipalib/constants.py +++ b/ipalib/constants.py @@ -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 diff --git a/ipalib/errors.py b/ipalib/errors.py index 20cd52b05..f48ad55aa 100644 --- a/ipalib/errors.py +++ b/ipalib/errors.py @@ -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 diff --git a/ipalib/plugins/entitle.py b/ipalib/plugins/entitle.py new file mode 100644 index 000000000..053de7825 --- /dev/null +++ b/ipalib/plugins/entitle.py @@ -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) diff --git a/ipalib/plugins/service.py b/ipalib/plugins/service.py index 2a2c278d4..bac58d30e 100644 --- a/ipalib/plugins/service.py +++ b/ipalib/plugins/service.py @@ -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) diff --git a/ipaserver/plugins/ldap2.py b/ipaserver/plugins/ldap2.py index f540880bb..b03c8def7 100644 --- a/ipaserver/plugins/ldap2.py +++ b/ipaserver/plugins/ldap2.py @@ -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 # |