summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--freeipa.spec.in5
-rw-r--r--install/share/60basev2.ldif2
-rw-r--r--install/share/default-aci.ldif2
-rw-r--r--install/share/delegation.ldif61
-rw-r--r--install/tools/Makefile.am1
-rw-r--r--install/tools/ipa-compliance193
-rw-r--r--install/tools/man/Makefile.am3
-rw-r--r--install/tools/man/ipa-compliance.145
-rw-r--r--ipa-compliance.cron5
-rw-r--r--ipalib/cli.py14
-rw-r--r--ipalib/constants.py1
-rw-r--r--ipalib/errors.py41
-rw-r--r--ipalib/plugins/entitle.py750
-rw-r--r--ipalib/plugins/service.py5
-rw-r--r--ipaserver/plugins/ldap2.py14
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
#