[Freeipa-devel] [PATCHES 0001-0013 v7] Profiles and CA ACLs

Fraser Tweedale ftweedal at redhat.com
Thu Jun 4 06:59:44 UTC 2015


On Wed, Jun 03, 2015 at 06:49:13PM +0200, Martin Basti wrote:
> On 03/06/15 16:17, Fraser Tweedale wrote:
> >On Tue, Jun 02, 2015 at 06:37:42PM +0200, Martin Basti wrote:
> >>On 02/06/15 14:11, Fraser Tweedale wrote:
> >>>On Mon, Jun 01, 2015 at 05:22:28PM +1000, Fraser Tweedale wrote:
> >>>>On Mon, Jun 01, 2015 at 05:10:58PM +1000, Fraser Tweedale wrote:
> >>>>>On Fri, May 29, 2015 at 01:03:46PM +0200, Martin Kosek wrote:
> >>>>>>On 05/29/2015 11:21 AM, Martin Basti wrote:
> >>>>>>>On 29/05/15 06:17, Fraser Tweedale wrote:
> >>>>>>>>On Thu, May 28, 2015 at 02:42:53PM +0200, Martin Basti wrote:
> >>>>>>>>>On 28/05/15 11:48, Martin Basti wrote:
> >>>>>>>>>>On 27/05/15 16:04, Fraser Tweedale wrote:
> >>>>>>>>>>>Hello all,
> >>>>>>>>>>>
> >>>>>>>>>>>Fresh certificate management patchset; Changelog:
> >>>>>>>>>>>
> >>>>>>>>>>>- Now depends on patch freeipa-ftweedal-0014 for correct
> >>>>>>>>>>>cert-request behaviour with host and service principals.
> >>>>>>>>>>>
> >>>>>>>>>>>- Updated Dogtag dependency to 10.2.4-1.  Should should be in
> >>>>>>>>>>>f22 soon, but for f22 right now or for f21, please grab from my
> >>>>>>>>>>>copr: https://copr.fedoraproject.org/coprs/ftweedal/freeipa/
> >>>>>>>>>>>
> >>>>>>>>>>>   Martin^1 could you please add to the quasi-official freeipa
> >>>>>>>>>>>   copr?  SRPM lives at
> >>>>>>>>>>>   https://frase.id.au/pki-core-10.2.4-1.fc21.src.rpm.
> >>>>>>>>>>>
> >>>>>>>>>>>- cert-request now verifies that for user principals, CSR CN
> >>>>>>>>>>>matches uid and, DN emailAddress and SAN rfc822Name match user's
> >>>>>>>>>>>email address, if either of those is present.
> >>>>>>>>>>>
> >>>>>>>>>>>- Fixed one or two other sneaky little bugs.
> >>>>>>>>>>>
> >>>>>>>>>>>On Wed, May 27, 2015 at 01:59:30AM +1000, Fraser Tweedale wrote:
> >>>>>>>>>>>>Hi all,
> >>>>>>>>>>>>
> >>>>>>>>>>>>Please find attached the latest certificate management
> >>>>>>>>>>>>patchset, which introduces the `caacl' plugin and various fixes
> >>>>>>>>>>>>and improvement to earlier patches.
> >>>>>>>>>>>>
> >>>>>>>>>>>>One important change to earlier patches is reverting the name
> >>>>>>>>>>>>of the default profile to 'caIPAserviceCert' and using the
> >>>>>>>>>>>>existing instance of this profile on upgrade (but not install)
> >>>>>>>>>>>>in case it has been modified.
> >>>>>>>>>>>>
> >>>>>>>>>>>>Other notes:
> >>>>>>>>>>>>
> >>>>>>>>>>>>- Still have changes in ipa-server-install (fewer lines now,
> >>>>>>>>>>>>though)
> >>>>>>>>>>>>
> >>>>>>>>>>>>- Still have the ugly import hack.  It is not a high priority
> >>>>>>>>>>>>for me, i.e. I think it should wait until after alpha
> >>>>>>>>>>>>
> >>>>>>>>>>>>- Still need to update 'service' and 'host' plugins to support
> >>>>>>>>>>>>multiple certificates.  (The userCertificate attribute schema
> >>>>>>>>>>>>itself is multi-valued, so there are no schema issues here)
> >>>>>>>>>>>>
> >>>>>>>>>>>>- The TODOs in [1]; mostly certprofile CLI conveniences and
> >>>>>>>>>>>>supporting multiple profiles for hosts and services (which
> >>>>>>>>>>>>requires changes to framework only, not schema).  [1]:
> >>>>>>>>>>>>http://idm.etherpad.corp.redhat.com/rhel72-cert-mgmt-progress
> >>>>>>>>>>>>
> >>>>>>>>>>>>Happy reviewing!  I am pleased with the initial cut of the
> >>>>>>>>>>>>caacl plugin but I'm sure you will find some things to be fixed
> >>>>>>>>>>>>:)
> >>>>>>>>>>>>
> >>>>>>>>>>>>Cheers, Fraser
> >>>>>>>>>>[root at vm-093 ~]#  ipa-replica-prepare vm-094.example.com
> >>>>>>>>>>--ip-address 10.34.78.94 Directory Manager (existing master)
> >>>>>>>>>>password:
> >>>>>>>>>>
> >>>>>>>>>>Preparing replica for vm-094.example.com from vm-093.example.com
> >>>>>>>>>>Creating SSL certificate for the Directory Server not well-formed
> >>>>>>>>>>(invalid token): line 2, column 14
> >>>>>>>>>>
> >>>>>>>>>>I cannot create replica file.  It work on the upgraded server,
> >>>>>>>>>>but it doesn't work on the newly installed server.  I'm not sure
> >>>>>>>>>>if this causes your patches which modifies the ca-installer, or
> >>>>>>>>>>the newer version of dogtag.
> >>>>>>>>>>
> >>>>>>>>>>Or if there was any other changes in master, I will continue to
> >>>>>>>>>>investigate with new RPM from master branch.
> >>>>>>>>>>
> >>>>>>>>>>Martin^2
> >>>>>>>>>>
> >>>>>>>>>ipa-replica-prepare works for: * master branch * master branch +
> >>>>>>>>>pki-ca 10.2.4-1
> >>>>>>>>>
> >>>>>>>>>So something in your patches is breaking it
> >>>>>>>>>
> >>>>>>>>>Martin^2
> >>>>>>>>>
> >>>>>>>>Martin, master + my patches with pki 10.2.4-1 is working for me on
> >>>>>>>>f21 and f22.  Can you provide ipa-replica-prepare --debug output and
> >>>>>>>>Dogtag debug log?  ( /var/log/pki/pki-tomcat/ca/debug )
> >>>>>>>>
> >>>>>>>>Thanks,
> >>>>>>>>Fraser
> >>>>>>>I can not reproduce it today. And I already recycled the VMs from yesterday. :-(
> >>>>>>>
> >>>>>>In that case I would suggest ACKing&pushing the patch and fixing the bug if
> >>>>>>it comes again. The tree may now be a bit unstable, given the number of
> >>>>>>patches going in.
> >>>>>>
> >>>>>>My main motivation here is to unblock Fraser.
> >>>>>>
> >>>>>>Thanks,
> >>>>>>Martin
> >>>>>Rebased patchset attached; no other changes.
> >>>>Heads up: I just discovered I have introduced a bug with
> >>>>ipa-replica-install, when it is spawning the CA instance.  I think
> >>>>replication it only causes issues with ``--setup-ca``.
> >>>>
> >>>>I will try and sort it out tomorrow or later tonight (I have to head
> >>>>out for a few hours now, though); and I'm not suggesting it should
> >>>>block the push but it's something to be aware of.
> >>>>
> >>>>Cheers,
> >>>>Fraser
> >>>>
> >>>New patchset attached ; haven't gotten to the bottom of the
> >>>ipa-replica-install issue mentioned above, but it fixes an upgrade
> >>>bug.
> >>>
> >>>The change is:
> >>>
> >>>diff --git a/ipaserver/install/server/upgrade.py b/ipaserver/install/server/upgrade.py
> >>>index c288282..c5f4d37 100644
> >>>--- a/ipaserver/install/server/upgrade.py
> >>>+++ b/ipaserver/install/server/upgrade.py
> >>>@@ -316,7 +316,7 @@ def ca_enable_ldap_profile_subsystem(ca):
> >>>                  caconfig.CS_CFG_PATH,
> >>>                  directive,
> >>>                  separator='=')
> >>>-            if value == 'ProfileSubsystem':
> >>>+            if value == 'com.netscape.cmscore.profile.ProfileSubsystem':
> >>>                  needs_update = True
> >>>                  break
> >>>      except OSError, e:
> >>>@@ -328,7 +328,7 @@ def ca_enable_ldap_profile_subsystem(ca):
> >>>          installutils.set_directive(
> >>>              caconfig.CS_CFG_PATH,
> >>>              directive,
> >>>-            'LDAPProfileSubsystem',
> >>>+            'com.netscape.cmscore.profile.LDAPProfileSubsystem',
> >>>              quotes=False,
> >>>              separator='=')
> >>>
> >>>Cheers,
> >>>Fraser
> >>>
> >>>
> >>Thank you,
> >>
> >>1)
> >>ipa-getcert request  (getcert -c IPA)
> >>doesnt work,
> >>
> >>Request ID '20150602145845':
> >>     status: CA_REJECTED
> >>     ca-error: Server at https://vm-137.example.com/ipa/xml denied our
> >>request, giving up: 3007 (RPC failed at server.  'profile_id' is required).
> >>
> >>2)
> >>Error from rpm install
> >>Unexpected error - see /var/log/ipaupgrade.log for details:
> >>SkipPluginModule: dogtag not selected as RA plugin
> >>
> >>Just for record as known issue, this will be fixed later in a new patch.
> >>
> >>3)
> >>+        Str('profile_id', validate_profile_id,
> >>+            label=_("Profile ID"),
> >>+            doc=_("Certificate Profile to use"),
> >>+        )
> >>Please mark this param as optional. ('profile_id?')
> >>This will fix issue 1, but 1 will need a option to specify profile_id
> >>
> >>Also move API related change from patch 9 to patch 11 + increment VERSION
> >>
> >>4)
> >>* Maybe I do everything wrong :)
> >>
> >>  I'm not able to create certificate stored in FILE, via ipa-getcert request.
> >>I'm getting error:
> >>status: CA_UNREACHABLE
> >>     ca-error: Server at https://vm-137.example.com/ipa/xml failed request,
> >>will retry: 4001 (RPC failed at server. vm-137.example.com at example.com: host
> >>not found).
> >>
> >>or error:
> >>Request ID '20150602154115':
> >>     status: CA_REJECTED
> >>     ca-error: Server at https://vm-137.example.com/ipa/xml denied our
> >>request, giving up: 2100 (RPC failed at server.  Insufficient access: not
> >>allowed to perform this command).
> >>(I'm root and kinited as admin)
> >>
> >>Maybe additional ACI is required for cert_request as it is VirtualCommand
> >>
> >>
> >>-- 
> >>Martin Basti
> >>
> >Thanks for report.  Attached patchset should fix the certmonger
> >issues, and also makes cert-request --profile-id argument optional.
> >
> >The changes were fixup'd into the appropriate patches but the
> >combined diff follows.  (Note that the API.txt and VERSION changes
> >you recommended were executed but are missing from this diff.)
> >
> >Thanks,
> >Fraser
> >
> >diff --git a/ipalib/plugins/caacl.py b/ipalib/plugins/caacl.py
> >index c09df86..a9dde86 100644
> >--- a/ipalib/plugins/caacl.py
> >+++ b/ipalib/plugins/caacl.py
> >@@ -12,7 +12,7 @@ from ipalib.plugins.baseldap import (
> >      LDAPUpdate, LDAPRetrieve, LDAPAddMember, LDAPRemoveMember,
> >      pkey_to_value)
> >  from ipalib.plugins.certprofile import validate_profile_id
> >-from ipalib.plugins.service import normalize_principal
> >+from ipalib.plugins.service import normalize_principal, split_any_principal
> >  from ipalib import _, ngettext
> >  from ipapython.dn import DN
> >@@ -69,7 +69,7 @@ def _acl_make_request(principal_type, principal, ca_ref, profile_id):
> >          groups = user_obj.get('memberof_group', [])
> >          groups += user_obj.get('memberofindirect_group', [])
> >      elif principal_type == 'host':
> >-        hostname = principal[5:]
> >+        service, hostname, realm = split_any_principal(principal)
> >          host_obj = api.Command.host_show(hostname)['result']
> >          groups = host_obj.get('memberof_hostgroup', [])
> >          groups += host_obj.get('memberofindirect_hostgroup', [])
> >diff --git a/ipalib/plugins/cert.py b/ipalib/plugins/cert.py
> >index 70ae610..1878e5a 100644
> >--- a/ipalib/plugins/cert.py
> >+++ b/ipalib/plugins/cert.py
> >@@ -247,7 +247,7 @@ class cert_request(VirtualCommand):
> >              default=False,
> >              autofill=True
> >          ),
> >-        Str('profile_id', validate_profile_id,
> >+        Str('profile_id?', validate_profile_id,
> >              label=_("Profile ID"),
> >              doc=_("Certificate Profile to use"),
> >          )
> >@@ -346,7 +346,14 @@ class cert_request(VirtualCommand):
> >          bind_principal = split_any_principal(getattr(context, 'principal'))
> >          bind_service, bind_name, bind_realm = bind_principal
> >-        if bind_principal != principal:
> >+        if bind_service is None:
> >+            bind_principal_type = USER
> >+        elif bind_service == 'host':
> >+            bind_principal_type = HOST
> >+        else:
> >+            bind_principal_type = SERVICE
> >+
> >+        if bind_principal != principal and bind_principal_type != HOST:
> >              # Can the bound principal request certs for another principal?
> >              self.check_access()
> >@@ -359,7 +366,7 @@ class cert_request(VirtualCommand):
> >                  error=_("Failure decoding Certificate Signing Request: %s") % e)
> >          # host principals may bypass allowed ext check
> >-        if bind_service != 'host':
> >+        if bind_principal_type != HOST:
> >              for ext in extensions:
> >                  operation = self._allowed_extensions.get(ext)
> >                  if operation:
> >diff --git a/ipapython/dogtag.py b/ipapython/dogtag.py
> >index 659751e..53085f7 100644
> >--- a/ipapython/dogtag.py
> >+++ b/ipapython/dogtag.py
> >@@ -47,7 +47,7 @@ INCLUDED_PROFILES = {
> >      (u'caIPAserviceCert', u'Standard profile for network services', True),
> >      }
> >-DEFAULT_PROFILE = 'caIPAserviceCert'
> >+DEFAULT_PROFILE = u'caIPAserviceCert'
> >  class Dogtag10Constants(object):
> >      DOGTAG_VERSION = 10
> 
> Should the user certificates behave in the same way as host and service
> certificates, i.e should be revoked after user-del or user-mod operation??
> If yes it would be an additional patch.
> 
> Please move API.txt fragment from patch 9 to patch 11
> With this change ACK for patches 1-11, to unblock testing. For patches 12-13
> I need more time.
> 
> -- 
> Martin Basti
> 
Updated patches attached.  Only your requested change for 1-11.  For
12-13 (caacl plugin) it was updated to LDAPAddMember and
LDAPRemoveMember functionality for adding profiles to ACL - this has
the desirable effect of making sure the profile actually exists :)

Thanks,
Fraser
-------------- next part --------------
From 21763ac6410533ab49d036b698623f8617b6d2db Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal at redhat.com>
Date: Mon, 20 Apr 2015 23:20:19 -0400
Subject: [PATCH 01/13] Install CA with LDAP profiles backend

Install the Dogtag CA to use the LDAPProfileSubsystem instead of the
default (file-based) ProfileSubsystem.

Part of: https://fedorahosted.org/freeipa/ticket/4560
---
 freeipa.spec.in                 | 6 +++---
 ipaserver/install/cainstance.py | 1 +
 2 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/freeipa.spec.in b/freeipa.spec.in
index 09dd66eec71cec714a31a42809c940ac08a5a84e..2f259234945be874aede64ca7c3ce04bdf467b64 100644
--- a/freeipa.spec.in
+++ b/freeipa.spec.in
@@ -92,7 +92,7 @@ BuildRequires:  python-backports-ssl_match_hostname
 BuildRequires:  softhsm-devel >= 2.0.0b1-3
 BuildRequires:  openssl-devel
 BuildRequires:  p11-kit-devel
-BuildRequires:  pki-base >= 10.2.1-0.1
+BuildRequires:  pki-base >= 10.2.4-1
 BuildRequires:  python-pytest-multihost >= 0.5
 BuildRequires:  python-pytest-sourceorder
 
@@ -135,8 +135,8 @@ Requires(post): systemd-units
 Requires: selinux-policy >= %{selinux_policy_version}
 Requires(post): selinux-policy-base
 Requires: slapi-nis >= 0.54.2-1
-Requires: pki-ca >= 10.2.1-0.2
-Requires: pki-kra >= 10.2.1-0.1
+Requires: pki-ca >= 10.2.4-1
+Requires: pki-kra >= 10.2.4-1
 Requires(preun): python systemd-units
 Requires(postun): python systemd-units
 Requires: python-dns >= 1.11.1
diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py
index 5133940687204b615eec56b6a89542ddd5617539..030c9f12daba4b38b748da8940e38d3cf2109788 100644
--- a/ipaserver/install/cainstance.py
+++ b/ipaserver/install/cainstance.py
@@ -503,6 +503,7 @@ class CAInstance(DogtagInstance):
         config.set("CA", "pki_restart_configured_instance", "False")
         config.set("CA", "pki_backup_keys", "True")
         config.set("CA", "pki_backup_password", self.admin_password)
+        config.set("CA", "pki_profiles_in_ldap", "True")
 
         # Client security database
         config.set("CA", "pki_client_database_dir", self.agent_db)
-- 
2.1.0

-------------- next part --------------
From a4dcac5c7d431fedac0ca3105bd7e4659f76512e Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal at redhat.com>
Date: Tue, 21 Apr 2015 02:24:10 -0400
Subject: [PATCH 02/13] Add schema for certificate profiles

The certprofile object class is used to track IPA-managed
certificate profiles in Dogtag and store IPA-specific settings.

Part of: https://fedorahosted.org/freeipa/ticket/57
---
 install/share/60certificate-profiles.ldif |  3 +++
 install/share/Makefile.am                 |  1 +
 install/share/bootstrap-template.ldif     | 12 ++++++++++++
 ipaserver/install/dsinstance.py           |  1 +
 4 files changed, 17 insertions(+)
 create mode 100644 install/share/60certificate-profiles.ldif

diff --git a/install/share/60certificate-profiles.ldif b/install/share/60certificate-profiles.ldif
new file mode 100644
index 0000000000000000000000000000000000000000..f1281949e53386e5bfe8b35e0c15858c693c5467
--- /dev/null
+++ b/install/share/60certificate-profiles.ldif
@@ -0,0 +1,3 @@
+dn: cn=schema
+attributeTypes: (2.16.840.1.113730.3.8.21.1.1 NAME 'ipaCertProfileStoreIssued' DESC 'Store certificates issued using this profile' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE X-ORIGIN 'IPA v4.2' )
+objectClasses: (2.16.840.1.113730.3.8.21.2.1 NAME 'ipaCertProfile' SUP top STRUCTURAL MUST ( cn $ description $ ipaCertProfileStoreIssued ) X-ORIGIN 'IPA v4.2' )
diff --git a/install/share/Makefile.am b/install/share/Makefile.am
index 8d336690f184025f8199ed1d2c57d8274f0d3886..f44772b20c173c6fe43503716f40454f6f6b6f11 100644
--- a/install/share/Makefile.am
+++ b/install/share/Makefile.am
@@ -16,6 +16,7 @@ app_DATA =				\
 	60basev3.ldif			\
 	60ipadns.ldif			\
 	60ipapk11.ldif			\
+	60certificate-profiles.ldif	\
 	61kerberos-ipav3.ldif		\
 	65ipacertstore.ldif		\
 	65ipasudo.ldif			\
diff --git a/install/share/bootstrap-template.ldif b/install/share/bootstrap-template.ldif
index 06b82aa4ae74e7766d0c09a63aa75fa222e7ab7d..c5d4bad8b80640881f4631e4873a12c82b0ea48a 100644
--- a/install/share/bootstrap-template.ldif
+++ b/install/share/bootstrap-template.ldif
@@ -429,3 +429,15 @@ cn: ${REALM}_id_range
 ipaBaseID: $IDSTART
 ipaIDRangeSize: $IDRANGE_SIZE
 ipaRangeType: ipa-local
+
+dn: cn=ca,$SUFFIX
+changetype: add
+objectClass: nsContainer
+objectClass: top
+cn: ca
+
+dn: cn=certprofiles,cn=ca,$SUFFIX
+changetype: add
+objectClass: nsContainer
+objectClass: top
+cn: certprofiles
diff --git a/ipaserver/install/dsinstance.py b/ipaserver/install/dsinstance.py
index 064a2ab1db61b465638a77e13e1d9ea43b1cce63..2acab13f247ed18a750f0e1cbbd98f4e63718c03 100644
--- a/ipaserver/install/dsinstance.py
+++ b/ipaserver/install/dsinstance.py
@@ -57,6 +57,7 @@ IPA_SCHEMA_FILES = ("60kerberos.ldif",
                     "60basev3.ldif",
                     "60ipapk11.ldif",
                     "60ipadns.ldif",
+                    "60certificate-profiles.ldif",
                     "61kerberos-ipav3.ldif",
                     "65ipacertstore.ldif",
                     "65ipasudo.ldif",
-- 
2.1.0

-------------- next part --------------
From 7871cfc930520cdb030fb8288c484e480f529e44 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal at redhat.com>
Date: Wed, 29 Apr 2015 06:07:58 -0400
Subject: [PATCH 03/13] ipa-pki-proxy: provide access to profiles REST API

Part of: https://fedorahosted.org/freeipa/ticket/57
---
 install/conf/ipa-pki-proxy.conf | 12 ++++++++++--
 1 file changed, 10 insertions(+), 2 deletions(-)

diff --git a/install/conf/ipa-pki-proxy.conf b/install/conf/ipa-pki-proxy.conf
index 5d21156848f3b5ddf14c42d92a26a30a9f94af36..366ca15a1868758547f9f1d3334fddba38793083 100644
--- a/install/conf/ipa-pki-proxy.conf
+++ b/install/conf/ipa-pki-proxy.conf
@@ -1,4 +1,4 @@
-# VERSION 5 - DO NOT REMOVE THIS LINE
+# VERSION 6 - DO NOT REMOVE THIS LINE
 
 ProxyRequests Off
 
@@ -11,7 +11,7 @@ ProxyRequests Off
 </LocationMatch>
 
 # matches for admin port and installer
-<LocationMatch "^/ca/admin/ca/getCertChain|^/ca/admin/ca/getConfigEntries|^/ca/admin/ca/getCookie|^/ca/admin/ca/getStatus|^/ca/admin/ca/securityDomainLogin|^/ca/admin/ca/getDomainXML|^/ca/rest/installer/installToken|^/ca/admin/ca/updateNumberRange|^/ca/rest/securityDomain/domainInfo|^/ca/rest/account/login|^/ca/admin/ca/tokenAuthenticate|^/ca/admin/ca/updateNumberRange|^/ca/admin/ca/updateDomainXML|^/ca/rest/account/logout|^/ca/rest/securityDomain/installToken|^/ca/admin/ca/updateConnector|^/ca/admin/ca/getSubsystemCert|^/kra/admin/kra/updateNumberRange|^/kra/admin/kra/getConfigEntries|^/kra/rest/config/cert/transport">
+<LocationMatch "^/ca/admin/ca/getCertChain|^/ca/admin/ca/getConfigEntries|^/ca/admin/ca/getCookie|^/ca/admin/ca/getStatus|^/ca/admin/ca/securityDomainLogin|^/ca/admin/ca/getDomainXML|^/ca/rest/installer/installToken|^/ca/admin/ca/updateNumberRange|^/ca/rest/securityDomain/domainInfo|^/ca/admin/ca/tokenAuthenticate|^/ca/admin/ca/updateNumberRange|^/ca/admin/ca/updateDomainXML|^/ca/rest/securityDomain/installToken|^/ca/admin/ca/updateConnector|^/ca/admin/ca/getSubsystemCert|^/kra/admin/kra/updateNumberRange|^/kra/admin/kra/getConfigEntries|^/kra/rest/config/cert/transport">
     NSSOptions +StdEnvVars +ExportCertData +StrictRequire +OptRenegotiate
     NSSVerifyClient none
     ProxyPassMatch ajp://localhost:$DOGTAG_PORT
@@ -26,5 +26,13 @@ ProxyRequests Off
     ProxyPassReverse ajp://localhost:$DOGTAG_PORT
 </LocationMatch>
 
+# matches for REST API
+<LocationMatch "^/ca/rest/account/login|^/ca/rest/account/logout|^/ca/rest/profiles">
+    NSSOptions +StdEnvVars +ExportCertData +StrictRequire +OptRenegotiate
+    NSSVerifyClient require
+    ProxyPassMatch ajp://localhost:$DOGTAG_PORT
+    ProxyPassReverse ajp://localhost:$DOGTAG_PORT
+</LocationMatch>
+
 # Only enable this on servers that are not generating a CRL
 ${CLONE}RewriteRule ^/ipa/crl/MasterCRL.bin https://$FQDN/ca/ee/ca/getCRL?op=getCRL&crlIssuingPoint=MasterCRL [L,R=301,NC]
-- 
2.1.0

-------------- next part --------------
From eeb025018019910f56e9395bb3d94f50e85497e2 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal at redhat.com>
Date: Thu, 30 Apr 2015 23:50:41 -0400
Subject: [PATCH 04/13] Add ACL to allow CA agent to modify profiles

Part of: https://fedorahosted.org/freeipa/ticket/57
---
 ipaserver/install/cainstance.py     | 29 +++++++++++++++++++++++++++++
 ipaserver/install/server/upgrade.py | 11 +++++++++++
 2 files changed, 40 insertions(+)

diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py
index 030c9f12daba4b38b748da8940e38d3cf2109788..871581b4afc5df854b9a090ef51bb0ad3b3871ee 100644
--- a/ipaserver/install/cainstance.py
+++ b/ipaserver/install/cainstance.py
@@ -469,6 +469,7 @@ class CAInstance(DogtagInstance):
                 self.step("requesting RA certificate from CA", self.__request_ra_certificate)
                 self.step("issuing RA agent certificate", self.__issue_ra_cert)
                 self.step("adding RA agent as a trusted user", self.__configure_ra)
+                self.step("authorizing RA to modify profiles", self.__configure_profiles_acl)
             self.step("configure certmonger for renewals", self.configure_certmonger_renewal)
             self.step("configure certificate renewals", self.configure_renewal)
             if not self.clone:
@@ -940,6 +941,10 @@ class CAInstance(DogtagInstance):
 
         conn.unbind()
 
+    def __configure_profiles_acl(self):
+        """Allow the Certificate Manager Agents group to modify profiles."""
+        configure_profiles_acl()
+
     def __run_certutil(self, args, database=None, pwd_file=None, stdin=None):
         if not database:
             database = self.ra_agent_db
@@ -1825,6 +1830,30 @@ def update_people_entry(dercert):
 
     return True
 
+def configure_profiles_acl():
+    server_id = installutils.realm_to_serverid(api.env.realm)
+    dogtag_uri = 'ldapi://%%2fvar%%2frun%%2fslapd-%s.socket' % server_id
+    updated = False
+
+    dn = DN(('cn', 'aclResources'), ('o', 'ipaca'))
+    rule = (
+        'certServer.profile.configuration:read,modify:allow (read,modify) '
+        'group="Certificate Manager Agents":'
+        'Certificate Manager agents may modify (create/update/delete) and read profiles'
+    )
+    modlist = [(ldap.MOD_ADD, 'resourceACLS', [rule])]
+
+    conn = ldap2.ldap2(shared_instance=False, ldap_uri=dogtag_uri)
+    if not conn.isconnected():
+        conn.connect(autobind=True)
+    rules = conn.get_entry(dn).get('resourceACLS', [])
+    if rule not in rules:
+        conn.conn.modify_s(str(dn), modlist)
+        updated = True
+
+    conn.disconnect()
+    return updated
+
 if __name__ == "__main__":
     standard_logging_setup("install.log")
     ds = dsinstance.DsInstance()
diff --git a/ipaserver/install/server/upgrade.py b/ipaserver/install/server/upgrade.py
index 9d1fd92b73eaf673ddfef01dc86b8dae5efc028a..0ea6bd7b4db70caf43637a60ddd1ad1f58b6e48e 100644
--- a/ipaserver/install/server/upgrade.py
+++ b/ipaserver/install/server/upgrade.py
@@ -289,6 +289,16 @@ def setup_firefox_extension(fstore):
     http.setup_firefox_extension(realm, domain)
 
 
+def ca_configure_profiles_acl(ca):
+    root_logger.info('[Authorizing RA Agent to modify profiles]')
+
+    if not ca.is_configured():
+        root_logger.info('CA is not configured')
+        return False
+
+    return cainstance.configure_profiles_acl()
+
+
 def upgrade_ipa_profile(ca, domain, fqdn):
     """
     Update the IPA Profile provided by dogtag
@@ -1370,6 +1380,7 @@ def upgrade_configuration():
         upgrade_ipa_profile(ca, api.env.domain, fqdn),
         certificate_renewal_update(ca),
         ca_enable_pkix(ca),
+        ca_configure_profiles_acl(ca),
     ])
 
     if ca_restart:
-- 
2.1.0

-------------- next part --------------
From e02b1388cf8f3d3ba1a991172f0cd7d6535e5b34 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal at redhat.com>
Date: Thu, 30 Apr 2015 04:55:29 -0400
Subject: [PATCH 05/13] Add certprofile plugin

Add the 'certprofile' plugin which defines the commands for managing
certificate profiles and associated permissions.

Also update Dogtag network code in 'ipapython.dogtag' to support
headers and arbitrary request bodies, to facilitate use of the
Dogtag profiles REST API.

Part of: https://fedorahosted.org/freeipa/ticket/57
---
 ACI.txt                               |   8 ++
 API.txt                               |  62 +++++++++
 install/updates/40-certprofile.update |   9 ++
 install/updates/40-delegation.update  |   8 ++
 install/updates/Makefile.am           |   1 +
 ipalib/constants.py                   |   1 +
 ipalib/plugins/certprofile.py         | 253 ++++++++++++++++++++++++++++++++++
 ipapython/dogtag.py                   |  29 ++--
 ipaserver/plugins/dogtag.py           | 176 ++++++++++++++++++++++-
 9 files changed, 534 insertions(+), 13 deletions(-)
 create mode 100644 install/updates/40-certprofile.update
 create mode 100644 ipalib/plugins/certprofile.py

diff --git a/ACI.txt b/ACI.txt
index 1821696fda912fdd11149062f9feaf4edcf0adfd..543d8da69fb2adf79dc9821fb24028717670326a 100644
--- a/ACI.txt
+++ b/ACI.txt
@@ -22,6 +22,14 @@ dn: cn=automount,dc=ipa,dc=example
 aci: (targetattr = "automountmapname || description")(targetfilter = "(objectclass=automountmap)")(version 3.0;acl "permission:System: Modify Automount Maps";allow (write) groupdn = "ldap:///cn=System: Modify Automount Maps,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: cn=automount,dc=ipa,dc=example
 aci: (targetfilter = "(objectclass=automountmap)")(version 3.0;acl "permission:System: Remove Automount Maps";allow (delete) groupdn = "ldap:///cn=System: Remove Automount Maps,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=certprofiles,cn=ca,dc=ipa,dc=example
+aci: (targetfilter = "(objectclass=ipacertprofile)")(version 3.0;acl "permission:System: Delete Certificate Profile";allow (delete) groupdn = "ldap:///cn=System: Delete Certificate Profile,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=certprofiles,cn=ca,dc=ipa,dc=example
+aci: (targetfilter = "(objectclass=ipacertprofile)")(version 3.0;acl "permission:System: Import Certificate Profile";allow (add) groupdn = "ldap:///cn=System: Import Certificate Profile,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=certprofiles,cn=ca,dc=ipa,dc=example
+aci: (targetattr = "cn || description || ipacertprofilestoreissued")(targetfilter = "(objectclass=ipacertprofile)")(version 3.0;acl "permission:System: Modify Certificate Profile";allow (write) groupdn = "ldap:///cn=System: Modify Certificate Profile,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=certprofiles,cn=ca,dc=ipa,dc=example
+aci: (targetattr = "cn || createtimestamp || description || entryusn || ipacertprofilestoreissued || modifytimestamp || objectclass")(targetfilter = "(objectclass=ipacertprofile)")(version 3.0;acl "permission:System: Read Certificate Profiles";allow (compare,read,search) userdn = "ldap:///all";)
 dn: cn=ipaconfig,cn=etc,dc=ipa,dc=example
 aci: (targetattr = "cn || createtimestamp || entryusn || ipacertificatesubjectbase || ipaconfigstring || ipacustomfields || ipadefaultemaildomain || ipadefaultloginshell || ipadefaultprimarygroup || ipagroupobjectclasses || ipagroupsearchfields || ipahomesrootdir || ipakrbauthzdata || ipamaxusernamelength || ipamigrationenabled || ipapwdexpadvnotify || ipasearchrecordslimit || ipasearchtimelimit || ipaselinuxusermapdefault || ipaselinuxusermaporder || ipauserauthtype || ipauserobjectclasses || ipausersearchfields || modifytimestamp || objectclass")(targetfilter = "(objectclass=ipaguiconfig)")(version 3.0;acl "permission:System: Read Global Configuration";allow (compare,read,search) userdn = "ldap:///all";)
 dn: cn=costemplates,cn=accounts,dc=ipa,dc=example
diff --git a/API.txt b/API.txt
index 6520f2c428342cdd30b0db830ed4ddbc89e4302a..81aca14afcaa5234ad218b8d84f3bc8efc734c9d 100644
--- a/API.txt
+++ b/API.txt
@@ -509,6 +509,68 @@ args: 1,1,1
 arg: Str('request_id')
 option: Str('version?', exclude='webui')
 output: Output('result', None, None)
+command: certprofile_del
+args: 1,2,3
+arg: Str('cn', attribute=True, cli_name='id', multivalue=True, primary_key=True, query=True, required=True)
+option: Flag('continue', autofill=True, cli_name='continue', default=False)
+option: Str('version?', exclude='webui')
+output: Output('result', <type 'dict'>, None)
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: ListOfPrimaryKeys('value', None, None)
+command: certprofile_find
+args: 1,9,4
+arg: Str('criteria?', noextrawhitespace=False)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('cn', attribute=True, autofill=False, cli_name='id', multivalue=False, primary_key=True, query=True, required=False)
+option: Str('description', attribute=True, autofill=False, cli_name='desc', multivalue=False, query=True, required=False)
+option: Bool('ipacertprofilestoreissued', attribute=True, autofill=False, cli_name='store', default=True, multivalue=False, query=True, required=False)
+option: Flag('pkey_only?', autofill=True, default=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Int('sizelimit?', autofill=False, minvalue=0)
+option: Int('timelimit?', autofill=False, minvalue=0)
+option: Str('version?', exclude='webui')
+output: Output('count', <type 'int'>, None)
+output: ListOfEntries('result', (<type 'list'>, <type 'tuple'>), Gettext('A list of LDAP entries', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: Output('truncated', <type 'bool'>, None)
+command: certprofile_import
+args: 1,6,3
+arg: Str('cn', attribute=True, cli_name='id', multivalue=False, primary_key=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('description', attribute=True, cli_name='desc', multivalue=False, required=True)
+option: File('file', cli_name='file')
+option: Bool('ipacertprofilestoreissued', attribute=True, cli_name='store', default=True, multivalue=False, required=True)
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('version?', exclude='webui')
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: PrimaryKey('value', None, None)
+command: certprofile_mod
+args: 1,10,3
+arg: Str('cn', attribute=True, cli_name='id', multivalue=False, primary_key=True, query=True, required=True)
+option: Str('addattr*', cli_name='addattr', exclude='webui')
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('delattr*', cli_name='delattr', exclude='webui')
+option: Str('description', attribute=True, autofill=False, cli_name='desc', multivalue=False, required=False)
+option: Bool('ipacertprofilestoreissued', attribute=True, autofill=False, cli_name='store', default=True, multivalue=False, required=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('rename', cli_name='rename', multivalue=False, primary_key=True, required=False)
+option: Flag('rights', autofill=True, default=False)
+option: Str('setattr*', cli_name='setattr', exclude='webui')
+option: Str('version?', exclude='webui')
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: PrimaryKey('value', None, None)
+command: certprofile_show
+args: 1,4,3
+arg: Str('cn', attribute=True, cli_name='id', multivalue=False, primary_key=True, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Flag('rights', autofill=True, default=False)
+option: Str('version?', exclude='webui')
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: PrimaryKey('value', None, None)
 command: compat_is_enabled
 args: 0,1,1
 option: Str('version?', exclude='webui')
diff --git a/install/updates/40-certprofile.update b/install/updates/40-certprofile.update
new file mode 100644
index 0000000000000000000000000000000000000000..6b0a81d0ff6d69dabe82138227d105fc780ee17d
--- /dev/null
+++ b/install/updates/40-certprofile.update
@@ -0,0 +1,9 @@
+dn: cn=ca,$SUFFIX
+default: objectClass: nsContainer
+default: objectClass: top
+default: cn: ca
+
+dn: cn=certprofiles,cn=ca,$SUFFIX
+default: objectClass: nsContainer
+default: objectClass: top
+default: cn: certprofiles
diff --git a/install/updates/40-delegation.update b/install/updates/40-delegation.update
index 975929bd70400b2f9cf407d6faedb246003d7f58..bc0736c5b6c07747586a56c2cbde9596c7522d1c 100644
--- a/install/updates/40-delegation.update
+++ b/install/updates/40-delegation.update
@@ -237,3 +237,11 @@ default:ipapermissiontype: SYSTEM
 
 dn: cn=config
 add:aci: (version 3.0;acl "permission:Add Configuration Sub-Entries";allow (add) groupdn = "ldap:///cn=Add Configuration Sub-Entries,cn=permissions,cn=pbac,$SUFFIX";)
+
+# CA Administrators
+dn: cn=CA Administrator,cn=privileges,cn=pbac,$SUFFIX
+default:objectClass: nestedgroup
+default:objectClass: groupofnames
+default:objectClass: top
+default:cn: CA Administrator
+default:description: CA Administrator
diff --git a/install/updates/Makefile.am b/install/updates/Makefile.am
index 4e2da05d61a41543914e79c4634331df6018c041..fc6bd624eac619cdddeba29b85440571d85fd69f 100644
--- a/install/updates/Makefile.am
+++ b/install/updates/Makefile.am
@@ -32,6 +32,7 @@ app_DATA =				\
 	40-replication.update		\
 	40-dns.update			\
 	40-automember.update		\
+	40-certprofile.update		\
 	40-otp.update			\
 	40-vault.update			\
 	45-roles.update			\
diff --git a/ipalib/constants.py b/ipalib/constants.py
index 95dec54a51f38ae63eba667daacf35dcd7500cf3..96396a236b8694b3dd988dfe28c1b0c3cc9e3180 100644
--- a/ipalib/constants.py
+++ b/ipalib/constants.py
@@ -118,6 +118,7 @@ DEFAULT_CONFIG = (
     ('container_radiusproxy', DN(('cn', 'radiusproxy'))),
     ('container_views', DN(('cn', 'views'), ('cn', 'accounts'))),
     ('container_masters', DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'))),
+    ('container_certprofile', DN(('cn', 'certprofiles'), ('cn', 'ca'))),
 
     # Ports, hosts, and URIs:
     ('xmlrpc_uri', 'http://localhost:8888/ipa/xml'),
diff --git a/ipalib/plugins/certprofile.py b/ipalib/plugins/certprofile.py
new file mode 100644
index 0000000000000000000000000000000000000000..1a2d143882469858f225b37ba4ff2dd368fb8853
--- /dev/null
+++ b/ipalib/plugins/certprofile.py
@@ -0,0 +1,253 @@
+#
+# Copyright (C) 2015  FreeIPA Contributors see COPYING for license
+#
+
+import re
+
+from ipalib import api, Bool, File, Str
+from ipalib import output
+from ipalib.plugable import Registry
+from ipalib.plugins.virtual import VirtualCommand
+from ipalib.plugins.baseldap import (
+    LDAPObject, LDAPSearch, LDAPCreate,
+    LDAPDelete, LDAPUpdate, LDAPRetrieve)
+from ipalib import ngettext
+from ipalib.text import _
+
+from ipalib import errors
+
+
+__doc__ = _("""
+Manage Certificate Profiles
+
+Certificate Profiles are used by Certificate Authority (CA) in the signing of
+certificates to determine if a Certificate Signing Request (CSR) is acceptable,
+and if so what features and extensions will be present on the certificate.
+
+The Certificate Profile format is the property-list format understood by the
+Dogtag or Red Hat Certificate System CA.
+
+PROFILE ID SYNTAX:
+
+A Profile ID is a string without spaces or punctuation starting with a letter
+and followed by a sequence of letters, digits or underscore ("_").
+
+EXAMPLES:
+
+  Import a profile that will not store issued certificates:
+    ipa certprofile-import ShortLivedUserCert \\
+      --file UserCert.profile --summary "User Certificates" \\
+      --store=false
+
+  Delete a certificate profile:
+    ipa certprofile-del ShortLivedUserCert
+
+  Show information about a profile:
+    ipa certprofile-show ShortLivedUserCert
+
+  Search for profiles that do not store certificates:
+    ipa certprofile-find --store=false
+
+""")
+
+
+register = Registry()
+
+
+def ca_enabled_check():
+    """Raise NotFound if CA is not enabled.
+
+    This function is defined in multiple plugins to avoid circular imports
+    (cert depends on certprofile, so we cannot import cert here).
+
+    """
+    if not api.Command.ca_is_enabled()['result']:
+        raise errors.NotFound(reason=_('CA is not configured'))
+
+
+profile_id_pattern = re.compile('^[a-zA-Z]\w*$')
+
+
+def validate_profile_id(ugettext, value):
+    """Ensure profile ID matches form required by CA."""
+    if profile_id_pattern.match(value) is None:
+        return _('invalid Profile ID')
+    else:
+        return None
+
+
+ at register()
+class certprofile(LDAPObject):
+    """
+    Certificate Profile object.
+    """
+    container_dn = api.env.container_certprofile
+    object_name = _('Certificate Profile')
+    object_name_plural = _('Certificate Profiles')
+    object_class = ['ipacertprofile']
+    default_attributes = [
+        'cn', 'description', 'ipacertprofilestoreissued'
+    ]
+    search_attributes = [
+        'cn', 'description', 'ipacertprofilestoreissued'
+    ]
+    rdn_is_primary_key = True
+    label = _('Certificate Profiles')
+    label_singular = _('Certificate Profile')
+
+    takes_params = (
+        Str('cn', validate_profile_id,
+            primary_key=True,
+            cli_name='id',
+            label=_('Profile ID'),
+            doc=_('Profile ID for referring to this profile'),
+        ),
+        Str('description',
+            required=True,
+            cli_name='desc',
+            label=_('Profile description'),
+            doc=_('Brief description of this profile'),
+        ),
+        Bool('ipacertprofilestoreissued',
+            default=True,
+            cli_name='store',
+            label=_('Store issued certificates'),
+            doc=_('Whether to store certs issued using this profile'),
+        ),
+    )
+
+    permission_filter_objectclasses = ['ipacertprofile']
+    managed_permissions = {
+        'System: Read Certificate Profiles': {
+            'replaces_global_anonymous_aci': True,
+            'ipapermbindruletype': 'all',
+            'ipapermright': {'read', 'search', 'compare'},
+            'ipapermdefaultattr': {
+                'cn',
+                'description',
+                'ipacertprofilestoreissued',
+                'objectclass',
+            },
+        },
+        'System: Import Certificate Profile': {
+            'ipapermright': {'add'},
+            'replaces': [
+                '(target = "ldap:///cn=*,cn=certprofiles,cn=ca,$SUFFIX")(version 3.0;acl "permission:Import Certificate Profile";allow (add) groupdn = "ldap:///cn=Import Certificate Profile,cn=permissions,cn=pbac,$SUFFIX";)',
+            ],
+            'default_privileges': {'CA Administrator'},
+        },
+        'System: Delete Certificate Profile': {
+            'ipapermright': {'delete'},
+            'replaces': [
+                '(target = "ldap:///cn=*,cn=certprofiles,cn=ca,$SUFFIX")(version 3.0;acl "permission:Delete Certificate Profile";allow (delete) groupdn = "ldap:///cn=Delete Certificate Profile,cn=permissions,cn=pbac,$SUFFIX";)',
+            ],
+            'default_privileges': {'CA Administrator'},
+        },
+        'System: Modify Certificate Profile': {
+            'ipapermright': {'write'},
+            'ipapermdefaultattr': {
+                'cn',
+                'description',
+                'ipacertprofilestoreissued',
+            },
+            'replaces': [
+                '(targetattr = "cn || description || ipacertprofilestoreissued")(target = "ldap:///cn=*,cn=certprofiles,cn=ca,$SUFFIX")(version 3.0;acl "permission:Modify Certificate Profile";allow (write) groupdn = "ldap:///cn=Modify Certificate Profile,cn=permissions,cn=pbac,$SUFFIX";)',
+            ],
+            'default_privileges': {'CA Administrator'},
+        },
+    }
+
+
+
+ at register()
+class certprofile_find(LDAPSearch):
+    __doc__ = _("Search for Certificate Profiles.")
+    msg_summary = ngettext(
+        '%(count)d profile matched', '%(count)d profiles matched', 0
+    )
+
+    def execute(self, *args, **kwargs):
+        ca_enabled_check()
+        return super(certprofile_find, self).execute(*args, **kwargs)
+
+
+ at register()
+class certprofile_show(LDAPRetrieve):
+    __doc__ = _("Display the properties of a Certificate Profile.")
+
+    def execute(self, *args, **kwargs):
+        ca_enabled_check()
+        return super(certprofile_show, self).execute(*args, **kwargs)
+
+
+ at register()
+class certprofile_import(LDAPCreate):
+    __doc__ = _("Import a Certificate Profile.")
+    msg_summary = _('Imported profile "%(value)s"')
+    takes_options = (
+        File('file',
+            label=_('Filename'),
+            cli_name='file',
+            flags=('virtual_attribute',),
+        ),
+    )
+
+    PROFILE_ID_PATTERN = re.compile('^profileId=([a-zA-Z]\w*)', re.MULTILINE)
+
+    def pre_callback(self, ldap, dn, entry, entry_attrs, *keys, **options):
+        ca_enabled_check()
+
+        match = self.PROFILE_ID_PATTERN.search(options['file'])
+        if match is None:
+            raise errors.ValidationError(name='file',
+                error=_("Profile ID is not present in profile data"))
+        elif keys[0] != match.group(1):
+            raise errors.ValidationError(name='file',
+                error=_("Profile ID '%(cli_value)s' does not match profile data '%(file_value)s'")
+                    % {'cli_value': keys[0], 'file_value': match.group(1)}
+            )
+        return dn
+
+
+    def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+        """Import the profile into Dogtag and enable it.
+
+        If the operation succeeds, update the LDAP entry to 'enabled'.
+        If the operation fails, remove the LDAP entry.
+        """
+        try:
+            with self.api.Backend.ra_certprofile as profile_api:
+                profile_api.create_profile(options['file'])
+                profile_api.enable_profile(keys[0])
+        except:
+            # something went wrong ; delete entry
+            ldap.delete_entry(dn)
+            raise
+
+        return dn
+
+
+ at register()
+class certprofile_del(LDAPDelete):
+    __doc__ = _("Delete a Certificate Profile.")
+    msg_summary = _('Deleted profile "%(value)s"')
+
+    def execute(self, *args, **kwargs):
+        ca_enabled_check()
+        return super(certprofile_del, self).execute(*args, **kwargs)
+
+    def post_callback(self, ldap, dn, *keys, **options):
+        with self.api.Backend.ra_certprofile as profile_api:
+            profile_api.disable_profile(keys[0])
+            profile_api.delete_profile(keys[0])
+        return dn
+
+
+ at register()
+class certprofile_mod(LDAPUpdate):
+    __doc__ = _("Modify Certificate Profile configuration.")
+    msg_summary = _('Modified Certificate Profile "%(value)s')
+
+    def execute(self, *args, **kwargs):
+        ca_enabled_check()
+        return super(certprofile_mod, self).execute(*args, **kwargs)
diff --git a/ipapython/dogtag.py b/ipapython/dogtag.py
index c74b8736a4b15f7bf081206b52b9876a8c4928af..11311cf7b55d7b84e9434a698dbfd60b0eb142a1 100644
--- a/ipapython/dogtag.py
+++ b/ipapython/dogtag.py
@@ -233,9 +233,12 @@ def ca_status(ca_host=None, use_proxy=True):
     return _parse_ca_status(body)
 
 
-def https_request(host, port, url, secdir, password, nickname, **kw):
+def https_request(host, port, url, secdir, password, nickname,
+        method='POST', headers=None, body=None, **kw):
     """
+    :param method: HTTP request method (defalut: 'POST')
     :param url: The path (not complete URL!) to post to.
+    :param body: The request body (encodes kw if None)
     :param kw:  Keyword arguments to encode into POST body.
     :return:   (http_status, http_reason_phrase, http_headers, http_body)
                as (integer, unicode, dict, str)
@@ -254,9 +257,11 @@ def https_request(host, port, url, secdir, password, nickname, **kw):
             nickname, password, nss.get_default_certdb())
         return conn
 
-    body = urlencode(kw)
+    if body is None:
+        body = urlencode(kw)
     return _httplib_request(
-        'https', host, port, url, connection_factory, body)
+        'https', host, port, url, connection_factory, body,
+        method=method, headers=headers)
 
 
 def http_request(host, port, url, **kw):
@@ -288,11 +293,13 @@ def unauthenticated_https_request(host, port, url, **kw):
 
 
 def _httplib_request(
-        protocol, host, port, path, connection_factory, request_body):
+        protocol, host, port, path, connection_factory, request_body,
+        method='POST', headers=None):
     """
     :param request_body: Request body
     :param connection_factory: Connection class to use. Will be called
         with the host and port arguments.
+    :param method: HTTP request method (default: 'POST')
 
     Perform a HTTP(s) request.
     """
@@ -301,13 +308,17 @@ def _httplib_request(
     uri = '%s://%s%s' % (protocol, ipautil.format_netloc(host, port), path)
     root_logger.debug('request %r', uri)
     root_logger.debug('request body %r', request_body)
+
+    headers = headers or {}
+    if (
+        method == 'POST'
+        and 'content-type' not in (str(k).lower() for k in headers.viewkeys())
+    ):
+        headers['content-type'] = 'application/x-www-form-urlencoded'
+
     try:
         conn = connection_factory(host, port)
-        conn.request(
-            'POST', uri,
-            body=request_body,
-            headers={'Content-type': 'application/x-www-form-urlencoded'},
-        )
+        conn.request(method, uri, body=request_body, headers=headers)
         res = conn.getresponse()
 
         http_status = res.status
diff --git a/ipaserver/plugins/dogtag.py b/ipaserver/plugins/dogtag.py
index 52bdb0d4245594785e718c63242e27cee0e59322..9654123b16d8e417398d49bf1305fd41880bc3a7 100644
--- a/ipaserver/plugins/dogtag.py
+++ b/ipaserver/plugins/dogtag.py
@@ -4,8 +4,9 @@
 #   Jason Gerard DeRose <jderose at redhat.com>
 #   Rob Crittenden <rcritten@@redhat.com>
 #   John Dennis <jdennis at redhat.com>
+#   Fraser Tweedale <ftweedal at redhat.com>
 #
-# Copyright (C) 2014  Red Hat
+# Copyright (C) 2014, 2015  Red Hat
 # see file 'COPYING' for use and warranty information
 #
 # This program is free software; you can redistribute it and/or modify
@@ -238,17 +239,21 @@ digits and nothing else follows.
 '''
 
 import datetime
+import json
 from lxml import etree
+import os
 import tempfile
 import time
 import urllib2
 
+import pki
 from pki.client import PKIConnection
 import pki.crypto as cryptoutil
 from pki.kra import KRAClient
 
 from ipalib import Backend
 from ipapython.dn import DN
+import ipapython.cookie
 import ipapython.dogtag
 from ipapython import ipautil
 from ipaserver.install.certs import CertDB
@@ -1262,13 +1267,12 @@ def select_any_master(ldap2, service='CA'):
 
 #-------------------------------------------------------------------------------
 
-from ipalib import api, SkipPluginModule
+from ipalib import api, errors, SkipPluginModule
 if api.env.ra_plugin != 'dogtag':
     # In this case, abort loading this plugin module...
     raise SkipPluginModule(reason='dogtag not selected as RA plugin')
 import os, random
 from ipaserver.plugins import rabase
-from ipalib.errors import CertificateOperationError
 from ipalib.constants import TYPE_ERROR
 from ipalib.util import cachedproperty
 from ipapython import dogtag
@@ -1318,7 +1322,7 @@ class ra(rabase.rabase):
             err_msg = u'%s (%s)' % (err_msg, detail)
 
         self.error('%s.%s(): %s', self.fullname, func_name, err_msg)
-        raise CertificateOperationError(error=err_msg)
+        raise errors.CertificateOperationError(error=err_msg)
 
     @cachedproperty
     def ca_host(self):
@@ -1923,3 +1927,167 @@ class kra(Backend):
         return KRAClient(connection, crypto)
 
 api.register(kra)
+
+
+class RestClient(Backend):
+    """Simple Dogtag REST client to be subclassed by other backends.
+
+    This class is a context manager.  Authenticated calls must be
+    executed in a ``with`` suite::
+
+        class ra_certprofile(RestClient):
+            path = 'profile'
+            ...
+
+        api.register(ra_certprofile)
+
+        with api.Backend.ra_certprofile as profile_api:
+            # REST client is now logged in
+            profile_api.create_profile(...)
+
+    """
+    path = None
+
+    @staticmethod
+    def _parse_dogtag_error(body):
+        try:
+            return pki.PKIException.from_json(json.loads(body))
+        except:
+            return None
+
+    def __init__(self):
+        if api.env.in_tree:
+            self.sec_dir = api.env.dot_ipa + os.sep + 'alias'
+            self.pwd_file = self.sec_dir + os.sep + '.pwd'
+        else:
+            self.sec_dir = paths.HTTPD_ALIAS_DIR
+            self.pwd_file = paths.ALIAS_PWDFILE_TXT
+        self.noise_file = self.sec_dir + os.sep + '.noise'
+        self.ipa_key_size = "2048"
+        self.ipa_certificate_nickname = "ipaCert"
+        self.ca_certificate_nickname = "caCert"
+        try:
+            f = open(self.pwd_file, "r")
+            self.password = f.readline().strip()
+            f.close()
+        except IOError:
+            self.password = ''
+        super(RestClient, self).__init__()
+
+        # session cookie
+        self.cookie = None
+
+    @cachedproperty
+    def ca_host(self):
+        """
+        :return:   host
+                   as str
+
+        Select our CA host.
+        """
+        ldap2 = self.api.Backend.ldap2
+        if host_has_service(api.env.ca_host, ldap2, "CA"):
+            return api.env.ca_host
+        if api.env.host != api.env.ca_host:
+            if host_has_service(api.env.host, ldap2, "CA"):
+                return api.env.host
+        host = select_any_master(ldap2)
+        if host:
+            return host
+        else:
+            return api.env.ca_host
+
+    def __enter__(self):
+        """Log into the REST API"""
+        if self.cookie is not None:
+            return
+        status, status_text, resp_headers, resp_body = dogtag.https_request(
+            self.ca_host, self.env.ca_agent_port, '/ca/rest/account/login',
+            self.sec_dir, self.password, self.ipa_certificate_nickname,
+            method='GET'
+        )
+        cookies = ipapython.cookie.Cookie.parse(resp_headers.get('set-cookie', ''))
+        if status != 200 or len(cookies) == 0:
+            raise errors.RemoteRetrieveError(reason=_('Failed to authenticate to CA REST API'))
+        self.cookie = str(cookies[0])
+        return self
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        """Log out of the REST API"""
+        dogtag.https_request(
+            self.ca_host, self.env.ca_agent_port, '/ca/rest/account/logout',
+            self.sec_dir, self.password, self.ipa_certificate_nickname,
+            method='GET'
+        )
+        self.cookie = None
+
+    def _ssldo(self, method, path, headers=None, body=None):
+        """
+        :param url: The URL to post to.
+        :param kw:  Keyword arguments to encode into POST body.
+        :return:   (http_status, http_reason_phrase, http_headers, http_body)
+                   as (integer, unicode, dict, str)
+
+        Perform an HTTPS request
+        """
+        if self.cookie is None:
+            raise errors.RemoteRetrieveError(
+                reason=_("REST API is not logged in."))
+
+        headers = headers or {}
+        headers['Cookie'] = self.cookie
+
+        resource = os.path.join('/ca/rest', self.path, path)
+
+        # perform main request
+        status, status_text, resp_headers, resp_body = dogtag.https_request(
+            self.ca_host, self.env.ca_agent_port, resource,
+            self.sec_dir, self.password, self.ipa_certificate_nickname,
+            method=method, headers=headers, body=body
+        )
+        if status < 200 or status >= 300:
+            explanation = self._parse_dogtag_error(resp_body) or ''
+            raise errors.RemoteRetrieveError(
+                reason=_('Non-2xx response from CA REST API: %(status)d %(status_text)s. %(explanation)s')
+                % {'status': status, 'status_text': status_text, 'explanation': explanation}
+            )
+        return (status, status_text, resp_headers, resp_body)
+
+
+class ra_certprofile(RestClient):
+    """
+    Profile management backend plugin.
+    """
+    path = 'profiles'
+
+    def create_profile(self, profile_data):
+        """
+        Import the profile into Dogtag
+        """
+        self._ssldo('POST', 'raw',
+            headers={
+                'Content-type': 'application/xml',
+                'Accept': 'application/json',
+            },
+            body=profile_data
+        )
+
+    def enable_profile(self, profile_id):
+        """
+        Enable the profile in Dogtag
+        """
+        self._ssldo('POST', profile_id + '?action=enable')
+
+    def disable_profile(self, profile_id):
+        """
+        Enable the profile in Dogtag
+        """
+        self._ssldo('POST', profile_id + '?action=disable')
+
+    def delete_profile(self, profile_id):
+        """
+        Delete the profile from Dogtag
+        """
+        self._ssldo('DELETE', profile_id, headers={'Accept': 'application/json'})
+
+api.register(ra_certprofile)
-- 
2.1.0

-------------- next part --------------
From 76a7bf36532d33633f4bf4b9a42cb02f2e726d99 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal at redhat.com>
Date: Mon, 11 May 2015 23:38:41 -0400
Subject: [PATCH 06/13] Enable LDAP-based profiles in CA on upgrade

Part of: https://fedorahosted.org/freeipa/ticket/4560
---
 ipaserver/install/server/upgrade.py | 40 +++++++++++++++++++++++++++++++++++++
 1 file changed, 40 insertions(+)

diff --git a/ipaserver/install/server/upgrade.py b/ipaserver/install/server/upgrade.py
index 0ea6bd7b4db70caf43637a60ddd1ad1f58b6e48e..820533d6fa34218282941b8dcfcd3c0a192fdfb7 100644
--- a/ipaserver/install/server/upgrade.py
+++ b/ipaserver/install/server/upgrade.py
@@ -299,6 +299,45 @@ def ca_configure_profiles_acl(ca):
     return cainstance.configure_profiles_acl()
 
 
+def ca_enable_ldap_profile_subsystem(ca):
+    root_logger.info('[Ensuring CA is using LDAPProfileSubsystem]')
+    if not ca.is_configured():
+        root_logger.info('CA is not configured')
+        return False
+
+    caconfig = dogtag.configured_constants()
+
+    needs_update = False
+    directive = None
+    try:
+        for i in range(15):
+            directive = "subsystem.{}.class".format(i)
+            value = installutils.get_directive(
+                caconfig.CS_CFG_PATH,
+                directive,
+                separator='=')
+            if value == 'com.netscape.cmscore.profile.ProfileSubsystem':
+                needs_update = True
+                break
+    except OSError, e:
+        root_logger.error('Cannot read CA configuration file "%s": %s',
+                caconfig.CS_CFG_PATH, e)
+        return False
+
+    if needs_update:
+        installutils.set_directive(
+            caconfig.CS_CFG_PATH,
+            directive,
+            'com.netscape.cmscore.profile.LDAPProfileSubsystem',
+            quotes=False,
+            separator='=')
+
+    # TODO import file-based profiles into Dogtag
+    # More code needed on Dogtag side for this.
+
+    return needs_update
+
+
 def upgrade_ipa_profile(ca, domain, fqdn):
     """
     Update the IPA Profile provided by dogtag
@@ -1381,6 +1420,7 @@ def upgrade_configuration():
         certificate_renewal_update(ca),
         ca_enable_pkix(ca),
         ca_configure_profiles_acl(ca),
+        ca_enable_ldap_profile_subsystem(ca),
     ])
 
     if ca_restart:
-- 
2.1.0

-------------- next part --------------
From bb4fd946dbcd33110eff4e8346e892e5581ec93d Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal at redhat.com>
Date: Mon, 11 May 2015 21:17:48 -0400
Subject: [PATCH 07/13] Import included profiles during install or upgrade

Add a default service profile template as part of FreeIPA and format
and import it as part of installation or upgrade process.

Also remove the code that modifies the old (file-based)
`caIPAserviceCert' profile.

Fixes https://fedorahosted.org/freeipa/ticket/4002
---
 freeipa.spec.in                             |   2 +
 install/configure.ac                        |   1 +
 install/share/Makefile.am                   |   1 +
 install/share/profiles/Makefile.am          |  14 ++
 install/share/profiles/caIPAserviceCert.cfg | 109 ++++++++++++
 install/tools/ipa-upgradeconfig             |   1 -
 ipapython/dogtag.py                         |   7 +-
 ipaserver/install/cainstance.py             | 253 +++++++---------------------
 ipaserver/install/ipa_server_upgrade.py     |   1 +
 ipaserver/install/server/install.py         |   6 +
 ipaserver/install/server/upgrade.py         |  42 ++---
 ipaserver/plugins/dogtag.py                 |  14 +-
 12 files changed, 228 insertions(+), 223 deletions(-)
 create mode 100644 install/share/profiles/Makefile.am
 create mode 100644 install/share/profiles/caIPAserviceCert.cfg

diff --git a/freeipa.spec.in b/freeipa.spec.in
index 2f259234945be874aede64ca7c3ce04bdf467b64..a9757a194b1bf3bdcced4fd29e7fbae8b0211c94 100644
--- a/freeipa.spec.in
+++ b/freeipa.spec.in
@@ -720,6 +720,8 @@ fi
 %dir %{_usr}/share/ipa/advise
 %dir %{_usr}/share/ipa/advise/legacy
 %{_usr}/share/ipa/advise/legacy/*.template
+%dir %{_usr}/share/ipa/profiles
+%{_usr}/share/ipa/profiles/*.cfg
 %dir %{_usr}/share/ipa/ffextension
 %{_usr}/share/ipa/ffextension/bootstrap.js
 %{_usr}/share/ipa/ffextension/install.rdf
diff --git a/install/configure.ac b/install/configure.ac
index 2e48aa5cc67b30f2582de987a12d4e7043256679..57f4219b66bbe1dadaed3e89c3e84b1c8240399e 100644
--- a/install/configure.ac
+++ b/install/configure.ac
@@ -88,6 +88,7 @@ AC_CONFIG_FILES([
     share/Makefile
     share/advise/Makefile
     share/advise/legacy/Makefile
+    share/profiles/Makefile
     ui/Makefile
     ui/css/Makefile
     ui/src/Makefile
diff --git a/install/share/Makefile.am b/install/share/Makefile.am
index f44772b20c173c6fe43503716f40454f6f6b6f11..31f391be25c58b76cc71971852074d80c5514745 100644
--- a/install/share/Makefile.am
+++ b/install/share/Makefile.am
@@ -2,6 +2,7 @@ NULL =
 
 SUBDIRS =  				\
 	advise				\
+	profiles			\
 	$(NULL)
 
 appdir = $(IPA_DATA_DIR)
diff --git a/install/share/profiles/Makefile.am b/install/share/profiles/Makefile.am
new file mode 100644
index 0000000000000000000000000000000000000000..4e6cf975a0f51d02ec29bd07ac8cb9ccc8320818
--- /dev/null
+++ b/install/share/profiles/Makefile.am
@@ -0,0 +1,14 @@
+NULL =
+
+appdir = $(IPA_DATA_DIR)/profiles
+app_DATA =				\
+	caIPAserviceCert.cfg		\
+	$(NULL)
+
+EXTRA_DIST =				\
+	$(app_DATA)			\
+	$(NULL)
+
+MAINTAINERCLEANFILES =			\
+	*~				\
+	Makefile.in
diff --git a/install/share/profiles/caIPAserviceCert.cfg b/install/share/profiles/caIPAserviceCert.cfg
new file mode 100644
index 0000000000000000000000000000000000000000..6c5102f0dbd6bd6c6eaf2fa22e87ed4a5f34553c
--- /dev/null
+++ b/install/share/profiles/caIPAserviceCert.cfg
@@ -0,0 +1,109 @@
+profileId=caIPAserviceCert
+classId=caEnrollImpl
+desc=This certificate profile is for enrolling server certificates with IPA-RA agent authentication.
+visible=false
+enable=true
+enableBy=admin
+auth.instance_id=raCertAuth
+name=IPA-RA Agent-Authenticated Server Certificate Enrollment
+input.list=i1,i2
+input.i1.class_id=certReqInputImpl
+input.i2.class_id=submitterInfoInputImpl
+output.list=o1
+output.o1.class_id=certOutputImpl
+policyset.list=serverCertSet
+policyset.serverCertSet.list=1,2,3,4,5,6,7,8,9,10,11
+policyset.serverCertSet.1.constraint.class_id=subjectNameConstraintImpl
+policyset.serverCertSet.1.constraint.name=Subject Name Constraint
+policyset.serverCertSet.1.constraint.params.pattern=CN=[^,]+,.+
+policyset.serverCertSet.1.constraint.params.accept=true
+policyset.serverCertSet.1.default.class_id=subjectNameDefaultImpl
+policyset.serverCertSet.1.default.name=Subject Name Default
+policyset.serverCertSet.1.default.params.name=CN=$$request.req_subject_name.cn$$, $SUBJECT_DN_O
+policyset.serverCertSet.2.constraint.class_id=validityConstraintImpl
+policyset.serverCertSet.2.constraint.name=Validity Constraint
+policyset.serverCertSet.2.constraint.params.range=740
+policyset.serverCertSet.2.constraint.params.notBeforeCheck=false
+policyset.serverCertSet.2.constraint.params.notAfterCheck=false
+policyset.serverCertSet.2.default.class_id=validityDefaultImpl
+policyset.serverCertSet.2.default.name=Validity Default
+policyset.serverCertSet.2.default.params.range=731
+policyset.serverCertSet.2.default.params.startTime=0
+policyset.serverCertSet.3.constraint.class_id=keyConstraintImpl
+policyset.serverCertSet.3.constraint.name=Key Constraint
+policyset.serverCertSet.3.constraint.params.keyType=RSA
+policyset.serverCertSet.3.constraint.params.keyParameters=1024,2048,3072,4096
+policyset.serverCertSet.3.default.class_id=userKeyDefaultImpl
+policyset.serverCertSet.3.default.name=Key Default
+policyset.serverCertSet.4.constraint.class_id=noConstraintImpl
+policyset.serverCertSet.4.constraint.name=No Constraint
+policyset.serverCertSet.4.default.class_id=authorityKeyIdentifierExtDefaultImpl
+policyset.serverCertSet.4.default.name=Authority Key Identifier Default
+policyset.serverCertSet.5.constraint.class_id=noConstraintImpl
+policyset.serverCertSet.5.constraint.name=No Constraint
+policyset.serverCertSet.5.default.class_id=authInfoAccessExtDefaultImpl
+policyset.serverCertSet.5.default.name=AIA Extension Default
+policyset.serverCertSet.5.default.params.authInfoAccessADEnable_0=true
+policyset.serverCertSet.5.default.params.authInfoAccessADLocationType_0=URIName
+policyset.serverCertSet.5.default.params.authInfoAccessADLocation_0=http://$IPA_CA_RECORD.$DOMAIN/ca/ocsp
+policyset.serverCertSet.5.default.params.authInfoAccessADMethod_0=1.3.6.1.5.5.7.48.1
+policyset.serverCertSet.5.default.params.authInfoAccessCritical=false
+policyset.serverCertSet.5.default.params.authInfoAccessNumADs=1
+policyset.serverCertSet.6.constraint.class_id=keyUsageExtConstraintImpl
+policyset.serverCertSet.6.constraint.name=Key Usage Extension Constraint
+policyset.serverCertSet.6.constraint.params.keyUsageCritical=true
+policyset.serverCertSet.6.constraint.params.keyUsageDigitalSignature=true
+policyset.serverCertSet.6.constraint.params.keyUsageNonRepudiation=true
+policyset.serverCertSet.6.constraint.params.keyUsageDataEncipherment=true
+policyset.serverCertSet.6.constraint.params.keyUsageKeyEncipherment=true
+policyset.serverCertSet.6.constraint.params.keyUsageKeyAgreement=false
+policyset.serverCertSet.6.constraint.params.keyUsageKeyCertSign=false
+policyset.serverCertSet.6.constraint.params.keyUsageCrlSign=false
+policyset.serverCertSet.6.constraint.params.keyUsageEncipherOnly=false
+policyset.serverCertSet.6.constraint.params.keyUsageDecipherOnly=false
+policyset.serverCertSet.6.default.class_id=keyUsageExtDefaultImpl
+policyset.serverCertSet.6.default.name=Key Usage Default
+policyset.serverCertSet.6.default.params.keyUsageCritical=true
+policyset.serverCertSet.6.default.params.keyUsageDigitalSignature=true
+policyset.serverCertSet.6.default.params.keyUsageNonRepudiation=true
+policyset.serverCertSet.6.default.params.keyUsageDataEncipherment=true
+policyset.serverCertSet.6.default.params.keyUsageKeyEncipherment=true
+policyset.serverCertSet.6.default.params.keyUsageKeyAgreement=false
+policyset.serverCertSet.6.default.params.keyUsageKeyCertSign=false
+policyset.serverCertSet.6.default.params.keyUsageCrlSign=false
+policyset.serverCertSet.6.default.params.keyUsageEncipherOnly=false
+policyset.serverCertSet.6.default.params.keyUsageDecipherOnly=false
+policyset.serverCertSet.7.constraint.class_id=noConstraintImpl
+policyset.serverCertSet.7.constraint.name=No Constraint
+policyset.serverCertSet.7.default.class_id=extendedKeyUsageExtDefaultImpl
+policyset.serverCertSet.7.default.name=Extended Key Usage Extension Default
+policyset.serverCertSet.7.default.params.exKeyUsageCritical=false
+policyset.serverCertSet.7.default.params.exKeyUsageOIDs=1.3.6.1.5.5.7.3.1,1.3.6.1.5.5.7.3.2
+policyset.serverCertSet.8.constraint.class_id=signingAlgConstraintImpl
+policyset.serverCertSet.8.constraint.name=No Constraint
+policyset.serverCertSet.8.constraint.params.signingAlgsAllowed=SHA1withRSA,SHA256withRSA,SHA512withRSA,MD5withRSA,MD2withRSA,SHA1withDSA,SHA1withEC,SHA256withEC,SHA384withEC,SHA512withEC
+policyset.serverCertSet.8.default.class_id=signingAlgDefaultImpl
+policyset.serverCertSet.8.default.name=Signing Alg
+policyset.serverCertSet.8.default.params.signingAlg=-
+policyset.serverCertSet.9.constraint.class_id=noConstraintImpl
+policyset.serverCertSet.9.constraint.name=No Constraint
+policyset.serverCertSet.9.default.class_id=crlDistributionPointsExtDefaultImpl
+policyset.serverCertSet.9.default.name=CRL Distribution Points Extension Default
+policyset.serverCertSet.9.default.params.crlDistPointsCritical=false
+policyset.serverCertSet.9.default.params.crlDistPointsNum=1
+policyset.serverCertSet.9.default.params.crlDistPointsEnable_0=true
+policyset.serverCertSet.9.default.params.crlDistPointsIssuerName_0=$CRL_ISSUER
+policyset.serverCertSet.9.default.params.crlDistPointsIssuerType_0=DirectoryName
+policyset.serverCertSet.9.default.params.crlDistPointsPointName_0=http://$IPA_CA_RECORD.$DOMAIN/ipa/crl/MasterCRL.bin
+policyset.serverCertSet.9.default.params.crlDistPointsPointType_0=URIName
+policyset.serverCertSet.9.default.params.crlDistPointsReasons_0=
+policyset.serverCertSet.10.constraint.class_id=noConstraintImpl
+policyset.serverCertSet.10.constraint.name=No Constraint
+policyset.serverCertSet.10.default.class_id=subjectKeyIdentifierExtDefaultImpl
+policyset.serverCertSet.10.default.name=Subject Key Identifier Extension Default
+policyset.serverCertSet.10.default.params.critical=false
+policyset.serverCertSet.11.constraint.class_id=noConstraintImpl
+policyset.serverCertSet.11.constraint.name=No Constraint
+policyset.serverCertSet.11.default.class_id=userExtensionDefaultImpl
+policyset.serverCertSet.11.default.name=User Supplied Extension Default
+policyset.serverCertSet.11.default.params.userExtOID=2.5.29.17
diff --git a/install/tools/ipa-upgradeconfig b/install/tools/ipa-upgradeconfig
index 43292966a29c9077443913bdda1c81aa3de06a10..5f3a2b4a2f3864c2809178815c244c2012333fc8 100755
--- a/install/tools/ipa-upgradeconfig
+++ b/install/tools/ipa-upgradeconfig
@@ -21,7 +21,6 @@
 
 import sys
 
-
 if __name__ == '__main__':
     sys.exit("Please run the 'ipa-server-upgrade' command to upgrade the "
              "IPA server.")
diff --git a/ipapython/dogtag.py b/ipapython/dogtag.py
index 11311cf7b55d7b84e9434a698dbfd60b0eb142a1..2b4d233354b974884c88d13a3a1b437915ba0776 100644
--- a/ipapython/dogtag.py
+++ b/ipapython/dogtag.py
@@ -42,6 +42,11 @@ from ipapython.ipa_log_manager import *
 # the configured version.
 
 
+INCLUDED_PROFILES = {
+    # ( profile_id    ,         description      ,      store_issued)
+    (u'caIPAserviceCert', u'Standard profile for network services', True),
+    }
+
 class Dogtag10Constants(object):
     DOGTAG_VERSION = 10
     UNSECURE_PORT = 8080
@@ -71,7 +76,6 @@ class Dogtag10Constants(object):
 
     RACERT_LINE_SEP = '\n'
 
-    IPA_SERVICE_PROFILE = '%s/caIPAserviceCert.cfg' % SERVICE_PROFILE_DIR
     SIGN_PROFILE = '%s/caJarSigningCert.cfg' % SERVICE_PROFILE_DIR
     SHARED_DB = True
     DS_USER = "dirsrv"
@@ -110,7 +114,6 @@ class Dogtag9Constants(object):
     EE_CLIENT_AUTH_PORT = 9446
     TOMCAT_SERVER_PORT = 9701
 
-    IPA_SERVICE_PROFILE = '%s/caIPAserviceCert.cfg' % SERVICE_PROFILE_DIR
     SIGN_PROFILE = '%s/caJarSigningCert.cfg' % SERVICE_PROFILE_DIR
     SHARED_DB = False
     DS_USER = "pkisrv"
diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py
index 871581b4afc5df854b9a090ef51bb0ad3b3871ee..ca0b6df5db80bc842a78f614872831ddd82330b1 100644
--- a/ipaserver/install/cainstance.py
+++ b/ipaserver/install/cainstance.py
@@ -459,10 +459,6 @@ class CAInstance(DogtagInstance):
             self.step("importing CA chain to RA certificate database", self.__import_ca_chain)
             self.step("fixing RA database permissions", self.fix_ra_perms)
             self.step("setting up signing cert profile", self.__setup_sign_profile)
-            self.step("set certificate subject base", self.__set_subject_in_config)
-            self.step("enabling Subject Key Identifier", self.enable_subject_key_identifier)
-            self.step("enabling Subject Alternative Name", self.enable_subject_alternative_name)
-            self.step("enabling CRL and OCSP extensions for certificates", self.__set_crl_ocsp_extensions)
             self.step("setting audit signing renewal to 2 years", self.set_audit_renewal)
             if not self.clone:
                 self.step("restarting certificate server", self.restart_instance)
@@ -1125,94 +1121,6 @@ class CAInstance(DogtagInstance):
 
         return publishdir
 
-    def __set_crl_ocsp_extensions(self):
-        self.set_crl_ocsp_extensions(self.domain, self.fqdn)
-
-    def set_crl_ocsp_extensions(self, domain, fqdn):
-        """
-        Configure CRL and OCSP extensions in default IPA certificate profile
-        if not done already.
-        """
-        changed = False
-
-        # OCSP extension
-        ocsp_url = 'http://%s.%s/ca/ocsp' % (IPA_CA_RECORD, ipautil.format_netloc(domain))
-
-        ocsp_location_0 = installutils.get_directive(
-            self.dogtag_constants.IPA_SERVICE_PROFILE,
-            'policyset.serverCertSet.5.default.params.authInfoAccessADLocation_0',
-            separator='=')
-
-        if ocsp_location_0 != ocsp_url:
-            # Set the first OCSP URI
-            installutils.set_directive(self.dogtag_constants.IPA_SERVICE_PROFILE,
-                'policyset.serverCertSet.5.default.params.authInfoAccessADLocation_0',
-                ocsp_url, quotes=False, separator='=')
-            changed = True
-
-        ocsp_profile_count = installutils.get_directive(
-            self.dogtag_constants.IPA_SERVICE_PROFILE,
-            'policyset.serverCertSet.5.default.params.authInfoAccessNumADs',
-            separator='=')
-
-        if ocsp_profile_count != '1':
-            installutils.set_directive(self.dogtag_constants.IPA_SERVICE_PROFILE,
-                'policyset.serverCertSet.5.default.params.authInfoAccessNumADs',
-                '1', quotes=False, separator='=')
-            changed = True
-
-
-        # CRL extension
-        crl_url = 'http://%s.%s/ipa/crl/MasterCRL.bin'% (IPA_CA_RECORD, ipautil.format_netloc(domain))
-
-        crl_point_0 = installutils.get_directive(
-            self.dogtag_constants.IPA_SERVICE_PROFILE,
-            'policyset.serverCertSet.9.default.params.crlDistPointsPointName_0',
-            separator='=')
-
-        if crl_point_0 != crl_url:
-            installutils.set_directive(self.dogtag_constants.IPA_SERVICE_PROFILE,
-                'policyset.serverCertSet.9.default.params.crlDistPointsIssuerName_0',
-                'CN=Certificate Authority,o=ipaca', quotes=False, separator='=')
-            installutils.set_directive(self.dogtag_constants.IPA_SERVICE_PROFILE,
-                'policyset.serverCertSet.9.default.params.crlDistPointsIssuerType_0',
-                'DirectoryName', quotes=False, separator='=')
-            installutils.set_directive(self.dogtag_constants.IPA_SERVICE_PROFILE,
-                'policyset.serverCertSet.9.default.params.crlDistPointsPointName_0',
-                crl_url, quotes=False, separator='=')
-            changed = True
-
-        crl_profile_count = installutils.get_directive(
-            self.dogtag_constants.IPA_SERVICE_PROFILE,
-            'policyset.serverCertSet.9.default.params.crlDistPointsNum',
-            separator='=')
-
-        if crl_profile_count != '1':
-            installutils.set_directive(self.dogtag_constants.IPA_SERVICE_PROFILE,
-                'policyset.serverCertSet.9.default.params.crlDistPointsNum',
-                '1', quotes=False, separator='=')
-            changed = True
-
-        # CRL extension is not enabled by default
-        setlist = installutils.get_directive(self.dogtag_constants.IPA_SERVICE_PROFILE,
-            'policyset.serverCertSet.list', separator='=')
-        new_set_list = None
-
-        if setlist == '1,2,3,4,5,6,7,8':
-            new_set_list = '1,2,3,4,5,6,7,8,9'
-        elif setlist == '1,2,3,4,5,6,7,8,10':
-            new_set_list = '1,2,3,4,5,6,7,8,9,10'
-        elif setlist == '1,2,3,4,5,6,7,8,10,11':
-            new_set_list = '1,2,3,4,5,6,7,8,9,10,11'
-
-        if new_set_list:
-            installutils.set_directive(self.dogtag_constants.IPA_SERVICE_PROFILE,
-                'policyset.serverCertSet.list',
-                new_set_list, quotes=False, separator='=')
-            changed = True
-
-        return changed
-
 
     def __enable_crl_publish(self):
         """
@@ -1267,13 +1175,6 @@ class CAInstance(DogtagInstance):
             installutils.set_directive(caconfig, 'ca.crl.MasterCRL.enableCRLUpdates', 'false', quotes=False, separator='=')
             installutils.set_directive(caconfig, 'ca.listenToCloneModifications', 'false', quotes=False, separator='=')
 
-    def __set_subject_in_config(self):
-        # dogtag ships with an IPA-specific profile that forces a subject
-        # format. We need to update that template with our base subject
-        if installutils.update_file(self.dogtag_constants.IPA_SERVICE_PROFILE,
-                'OU=pki-ipa, O=IPA', str(self.subject_base)):
-            print "Updating subject_base in CA template failed"
-
     def uninstall(self):
         # just eat state
         self.restore_state("enabled")
@@ -1407,100 +1308,6 @@ class CAInstance(DogtagInstance):
 
         services.knownservices.certmonger.stop()
 
-    def enable_subject_key_identifier(self):
-        """
-        See if Subject Key Identifier is set in the profile and if not, add it.
-        """
-        setlist = installutils.get_directive(
-            self.dogtag_constants.IPA_SERVICE_PROFILE,
-            'policyset.serverCertSet.list', separator='=')
-
-        # this is the default setting from pki-ca/pki-tomcat. Don't touch it
-        # if a user has manually modified it.
-        if setlist == '1,2,3,4,5,6,7,8' or setlist == '1,2,3,4,5,6,7,8,9':
-            setlist += ',10'
-            installutils.set_directive(
-                self.dogtag_constants.IPA_SERVICE_PROFILE,
-                'policyset.serverCertSet.list',
-                setlist,
-                quotes=False, separator='=')
-            installutils.set_directive(
-                self.dogtag_constants.IPA_SERVICE_PROFILE,
-                'policyset.serverCertSet.10.constraint.class_id',
-                'noConstraintImpl',
-                quotes=False, separator='=')
-            installutils.set_directive(
-                self.dogtag_constants.IPA_SERVICE_PROFILE,
-                'policyset.serverCertSet.10.constraint.name',
-                'No Constraint',
-                quotes=False, separator='=')
-            installutils.set_directive(
-                self.dogtag_constants.IPA_SERVICE_PROFILE,
-                'policyset.serverCertSet.10.default.class_id',
-                'subjectKeyIdentifierExtDefaultImpl',
-                quotes=False, separator='=')
-            installutils.set_directive(
-                self.dogtag_constants.IPA_SERVICE_PROFILE,
-                'policyset.serverCertSet.10.default.name',
-                'Subject Key Identifier Extension Default',
-                quotes=False, separator='=')
-            installutils.set_directive(
-                self.dogtag_constants.IPA_SERVICE_PROFILE,
-                'policyset.serverCertSet.10.default.params.critical',
-                'false',
-                quotes=False, separator='=')
-            return True
-
-        # No update was done
-        return False
-
-    def enable_subject_alternative_name(self):
-        """
-        See if Subject Alternative Name is set in the profile and if not, add
-        it.
-        """
-        setlist = installutils.get_directive(
-            self.dogtag_constants.IPA_SERVICE_PROFILE,
-            'policyset.serverCertSet.list', separator='=')
-
-        # this is the default setting from pki-ca/pki-tomcat. Don't touch it
-        # if a user has manually modified it.
-        if setlist == '1,2,3,4,5,6,7,8,10' or setlist == '1,2,3,4,5,6,7,8,9,10':
-            setlist += ',11'
-            installutils.set_directive(
-                self.dogtag_constants.IPA_SERVICE_PROFILE,
-                'policyset.serverCertSet.list',
-                setlist,
-                quotes=False, separator='=')
-            installutils.set_directive(
-                self.dogtag_constants.IPA_SERVICE_PROFILE,
-                'policyset.serverCertSet.11.constraint.class_id',
-                'noConstraintImpl',
-                quotes=False, separator='=')
-            installutils.set_directive(
-                self.dogtag_constants.IPA_SERVICE_PROFILE,
-                'policyset.serverCertSet.11.constraint.name',
-                'No Constraint',
-                quotes=False, separator='=')
-            installutils.set_directive(
-                self.dogtag_constants.IPA_SERVICE_PROFILE,
-                'policyset.serverCertSet.11.default.class_id',
-                'userExtensionDefaultImpl',
-                quotes=False, separator='=')
-            installutils.set_directive(
-                self.dogtag_constants.IPA_SERVICE_PROFILE,
-                'policyset.serverCertSet.11.default.name',
-                'User Supplied Extension Default',
-                quotes=False, separator='=')
-            installutils.set_directive(
-                self.dogtag_constants.IPA_SERVICE_PROFILE,
-                'policyset.serverCertSet.11.default.params.userExtOID',
-                '2.5.29.17',
-                quotes=False, separator='=')
-            return True
-
-        # No update was done
-        return False
 
     def set_audit_renewal(self):
         """
@@ -1586,7 +1393,6 @@ class CAInstance(DogtagInstance):
             master_entry['ipaConfigString'].append('caRenewalMaster')
             self.admin_conn.update_entry(master_entry)
 
-
     @staticmethod
     def update_cert_config(nickname, cert, dogtag_constants=None):
         """
@@ -1854,6 +1660,65 @@ def configure_profiles_acl():
     conn.disconnect()
     return updated
 
+def import_included_profiles():
+    sub_dict = dict(
+        DOMAIN=ipautil.format_netloc(api.env.domain),
+        IPA_CA_RECORD=IPA_CA_RECORD,
+        CRL_ISSUER='CN=Certificate Authority,o=ipaca',
+        SUBJECT_DN_O=str(DN(('O', api.env.realm))),
+    )
+
+    server_id = installutils.realm_to_serverid(api.env.realm)
+    dogtag_uri = 'ldapi://%%2fvar%%2frun%%2fslapd-%s.socket' % server_id
+    conn = ldap2.ldap2(shared_instance=False, ldap_uri=dogtag_uri)
+    if not conn.isconnected():
+        conn.connect(autobind=True)
+
+    for (profile_id, desc, store_issued) in dogtag.INCLUDED_PROFILES:
+        dn = DN(('cn', profile_id),
+            api.env.container_certprofile, api.env.basedn)
+        try:
+            conn.get_entry(dn)
+            continue  # the profile is present
+        except errors.NotFound:
+            # profile not found; add it
+            profile_data = ipautil.template_file(
+                '/usr/share/ipa/profiles/{}.cfg'.format(profile_id), sub_dict)
+
+            entry = conn.make_entry(
+                dn,
+                objectclass=['ipacertprofile'],
+                cn=[profile_id],
+                description=[desc],
+                ipacertprofilestoreissued=['TRUE' if store_issued else 'FALSE'],
+            )
+            conn.add_entry(entry)
+            api.Backend.ra_certprofile._read_password()
+            with api.Backend.ra_certprofile as profile_api:
+                # import the profile
+                try:
+                    profile_api.create_profile(profile_data)
+                except errors.RemoteRetrieveError:
+                    # conflicting profile; replace it if we are
+                    # installing IPA, but keep it for upgrades
+                    if api.env.context == 'installer':
+                        try:
+                            profile_api.disable_profile(profile_id)
+                        except errors.RemoteRetrieveError:
+                            pass
+                        profile_api.delete_profile(profile_id)
+                        profile_api.create_profile(profile_data)
+
+                # enable the profile
+                try:
+                    profile_api.enable_profile(profile_id)
+                except errors.RemoteRetrieveError:
+                    pass
+
+            root_logger.info("Imported profile '%s'", profile_id)
+
+    conn.disconnect()
+
 if __name__ == "__main__":
     standard_logging_setup("install.log")
     ds = dsinstance.DsInstance()
diff --git a/ipaserver/install/ipa_server_upgrade.py b/ipaserver/install/ipa_server_upgrade.py
index d0a839d0a316317622894e5b56896f91a9e29bb8..8373b213411c34c59e838c586ff46e99efb43f58 100644
--- a/ipaserver/install/ipa_server_upgrade.py
+++ b/ipaserver/install/ipa_server_upgrade.py
@@ -41,6 +41,7 @@ class ServerUpgrade(admintool.AdminTool):
         super(ServerUpgrade, self).run()
 
         api.bootstrap(in_server=True, context='updates')
+        import ipaserver.plugins.dogtag  # ensure profile backend gets loaded
         api.finalize()
 
         try:
diff --git a/ipaserver/install/server/install.py b/ipaserver/install/server/install.py
index aea1f9915f16a55c44183b0cebb41c04622be503..955e4cc11fba20475a07126f4101edbf1024290e 100644
--- a/ipaserver/install/server/install.py
+++ b/ipaserver/install/server/install.py
@@ -748,6 +748,9 @@ def install(options):
         api.env.ca_host = host_name
 
     api.bootstrap(**cfg)
+    if setup_ca:
+        # ensure profile backend is available
+        import ipaserver.plugins.dogtag
     api.finalize()
 
     # Create DS user/group if it doesn't exist yet
@@ -903,6 +906,9 @@ def install(options):
         service.print_msg("Restarting the certificate server")
         ca.restart(dogtag.configured_constants().PKI_INSTANCE_NAME)
 
+        service.print_msg("Importing certificate profiles")
+        cainstance.import_included_profiles()
+
     if options.setup_dns:
         api.Backend.ldap2.connect(autobind=True)
         dns.install(False, False, options)
diff --git a/ipaserver/install/server/upgrade.py b/ipaserver/install/server/upgrade.py
index 820533d6fa34218282941b8dcfcd3c0a192fdfb7..c5f4d37cc02658334d5c26f269ec5dd5e386df1d 100644
--- a/ipaserver/install/server/upgrade.py
+++ b/ipaserver/install/server/upgrade.py
@@ -338,32 +338,28 @@ def ca_enable_ldap_profile_subsystem(ca):
     return needs_update
 
 
-def upgrade_ipa_profile(ca, domain, fqdn):
+def ca_import_included_profiles(ca):
+    root_logger.info('[Ensuring presence of included profiles]')
+
+    if not ca.is_configured():
+        root_logger.info('CA is not configured')
+        return False
+
+    return cainstance.import_included_profiles()
+
+
+def upgrade_ca_audit_cert_validity(ca):
     """
-    Update the IPA Profile provided by dogtag
+    Update the Dogtag audit signing certificate.
 
     Returns True if restart is needed, False otherwise.
     """
-    root_logger.info('[Verifying that CA service certificate profile is updated]')
+    root_logger.info('[Verifying that CA audit signing cert has 2 year validity]')
     if ca.is_configured():
-        ski = ca.enable_subject_key_identifier()
-        if ski:
-            root_logger.debug('Subject Key Identifier updated.')
-        else:
-            root_logger.debug('Subject Key Identifier already set.')
-        san = ca.enable_subject_alternative_name()
-        if san:
-            root_logger.debug('Subject Alternative Name updated.')
-        else:
-            root_logger.debug('Subject Alternative Name already set.')
-        audit = ca.set_audit_renewal()
-        uri = ca.set_crl_ocsp_extensions(domain, fqdn)
-        if audit or ski or san or uri:
-            return True
+        return ca.set_audit_renewal()
     else:
         root_logger.info('CA is not configured')
-
-    return False
+        return False
 
 
 def named_remove_deprecated_options():
@@ -1416,7 +1412,7 @@ def upgrade_configuration():
 
     ca_restart = any([
         ca_restart,
-        upgrade_ipa_profile(ca, api.env.domain, fqdn),
+        upgrade_ca_audit_cert_validity(ca),
         certificate_renewal_update(ca),
         ca_enable_pkix(ca),
         ca_configure_profiles_acl(ca),
@@ -1430,6 +1426,12 @@ def upgrade_configuration():
         except ipautil.CalledProcessError as e:
             root_logger.error("Failed to restart %s: %s", ca.service_name, e)
 
+    # This step MUST be done after ca_enable_ldap_profile_subsystem and
+    # ca_configure_profiles_acl, and the consequent restart, but does not
+    # itself require a restart.
+    #
+    ca_import_included_profiles(ca)
+
     set_sssd_domain_option('ipa_server_mode', 'True')
 
 
diff --git a/ipaserver/plugins/dogtag.py b/ipaserver/plugins/dogtag.py
index 9654123b16d8e417398d49bf1305fd41880bc3a7..880b319d68728a40f4479626d5a7c2b8c56ced02 100644
--- a/ipaserver/plugins/dogtag.py
+++ b/ipaserver/plugins/dogtag.py
@@ -1966,17 +1966,19 @@ class RestClient(Backend):
         self.ipa_key_size = "2048"
         self.ipa_certificate_nickname = "ipaCert"
         self.ca_certificate_nickname = "caCert"
-        try:
-            f = open(self.pwd_file, "r")
-            self.password = f.readline().strip()
-            f.close()
-        except IOError:
-            self.password = ''
+        self._read_password()
         super(RestClient, self).__init__()
 
         # session cookie
         self.cookie = None
 
+    def _read_password(self):
+        try:
+            with open(self.pwd_file) as f:
+                self.password = f.readline().strip()
+        except IOError:
+            self.password = ''
+
     @cachedproperty
     def ca_host(self):
         """
-- 
2.1.0

-------------- next part --------------
From e07fe7a89023185d4ef1ab12c26017010f316421 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal at redhat.com>
Date: Thu, 7 May 2015 21:26:24 -0400
Subject: [PATCH 08/13] Add generic split_any_principal method

There exist methods to split user or service/host principals, but
there is no method to split any kind of principal and allow the
caller to decide what to do.

Generalize ``ipalib.plugins.service.split_principal`` to return a
service of ``None`` if the principal is a user principal, rename it
``split_any_principal`` and reimplement ``split_principal`` to
preserve existing behaviour.

Part of: https://fedorahosted.org/freeipa/ticket/4938
---
 ipalib/plugins/service.py | 27 +++++++++++++++++++--------
 1 file changed, 19 insertions(+), 8 deletions(-)

diff --git a/ipalib/plugins/service.py b/ipalib/plugins/service.py
index 2d34eac7db5b97b7a969175f488a547dde54010a..166d978a248e7c5da6f8df4b534edad0a0799b7e 100644
--- a/ipalib/plugins/service.py
+++ b/ipalib/plugins/service.py
@@ -185,19 +185,24 @@ _ticket_flags_map = {
 
 _ticket_flags_default = _ticket_flags_map['ipakrbrequirespreauth']
 
-def split_principal(principal):
+def split_any_principal(principal):
     service = hostname = realm = None
 
     # Break down the principal into its component parts, which may or
     # may not include the realm.
     sp = principal.split('/')
-    if len(sp) != 2:
-        raise errors.MalformedServicePrincipal(reason=_('missing service'))
+    name_and_realm = None
+    if len(sp) > 2:
+        raise errors.MalformedServicePrincipal(reason=_('unable to determine service'))
+    elif len(sp) == 2:
+        service = sp[0]
+        if len(service) == 0:
+            raise errors.MalformedServicePrincipal(reason=_('blank service'))
+        name_and_realm = sp[1]
+    else:
+        name_and_realm = sp[0]
 
-    service = sp[0]
-    if len(service) == 0:
-        raise errors.MalformedServicePrincipal(reason=_('blank service'))
-    sr = sp[1].split('@')
+    sr = name_and_realm.split('@')
     if len(sr) > 2:
         raise errors.MalformedServicePrincipal(
             reason=_('unable to determine realm'))
@@ -212,7 +217,13 @@ def split_principal(principal):
         realm = api.env.realm
 
     # Note that realm may be None.
-    return (service, hostname, realm)
+    return service, hostname, realm
+
+def split_principal(principal):
+    service, name, realm = split_any_principal(principal)
+    if service is None:
+        raise errors.MalformedServicePrincipal(reason=_('missing service'))
+    return service, name, realm
 
 def validate_principal(ugettext, principal):
     (service, hostname, principal) = split_principal(principal)
-- 
2.1.0

-------------- next part --------------
From ab743104189fd1bad9a4b2066c94ed2600deedbe Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal at redhat.com>
Date: Fri, 8 May 2015 02:23:24 -0400
Subject: [PATCH 09/13] Add profile_id parameter to 'request_certificate'

Add the profile_id parameter to the 'request_certificate' function
and update call sites.

Also remove multiple occurrences of the default profile ID
'caIPAserviceCert'.

Part of: https://fedorahosted.org/freeipa/ticket/57
---
 checks/check-ra.py          | 2 +-
 ipalib/plugins/cert.py      | 2 +-
 ipapython/dogtag.py         | 2 ++
 ipaserver/install/certs.py  | 2 +-
 ipaserver/plugins/dogtag.py | 7 +++++--
 ipaserver/plugins/rabase.py | 3 ++-
 6 files changed, 12 insertions(+), 6 deletions(-)

diff --git a/checks/check-ra.py b/checks/check-ra.py
index a1df50ba4a4ad7fc0b6d2118e40977b1da6edf65..28929545ab7f0a63e47a3829c53cf08d784c9524 100755
--- a/checks/check-ra.py
+++ b/checks/check-ra.py
@@ -90,7 +90,7 @@ def assert_equal(trial, reference):
 
 
 api.log.info('******** Testing ra.request_certificate() ********')
-request_result = ra.request_certificate(csr)
+request_result = ra.request_certificate(csr, ra.DEFAULT_PROFILE)
 if verbose: print "request_result=\n%s" % request_result
 assert_equal(request_result,
              {'subject' : subject,
diff --git a/ipalib/plugins/cert.py b/ipalib/plugins/cert.py
index 7e2c77622b3627e9e57bbcb69291f723ecf509bf..e4cb6dc0aa8b89368806b08674aae277b3653e8f 100644
--- a/ipalib/plugins/cert.py
+++ b/ipalib/plugins/cert.py
@@ -436,7 +436,7 @@ class cert_request(VirtualCommand):
 
         # Request the certificate
         result = self.Backend.ra.request_certificate(
-            csr, request_type=request_type)
+            csr, 'caIPAserviceCert', request_type=request_type)
         cert = x509.load_certificate(result['certificate'])
         result['issuer'] = unicode(cert.issuer)
         result['valid_not_before'] = unicode(cert.valid_not_before_str)
diff --git a/ipapython/dogtag.py b/ipapython/dogtag.py
index 2b4d233354b974884c88d13a3a1b437915ba0776..53085f7762fc828ed9fc6621fbf3a0c67ec6a656 100644
--- a/ipapython/dogtag.py
+++ b/ipapython/dogtag.py
@@ -47,6 +47,8 @@ INCLUDED_PROFILES = {
     (u'caIPAserviceCert', u'Standard profile for network services', True),
     }
 
+DEFAULT_PROFILE = u'caIPAserviceCert'
+
 class Dogtag10Constants(object):
     DOGTAG_VERSION = 10
     UNSECURE_PORT = 8080
diff --git a/ipaserver/install/certs.py b/ipaserver/install/certs.py
index bc7dccf805386e9fa84b58d2ff9346085e1b93b1..564332e6fde0698a23884922c5018fab59da7e4d 100644
--- a/ipaserver/install/certs.py
+++ b/ipaserver/install/certs.py
@@ -386,7 +386,7 @@ class CertDB(object):
         # We just want the CSR bits, make sure there is nothing else
         csr = pkcs10.strip_header(csr)
 
-        params = {'profileId': 'caIPAserviceCert',
+        params = {'profileId': dogtag.DEFAULT_PROFILE,
                 'cert_request_type': 'pkcs10',
                 'requestor_name': 'IPA Installer',
                 'cert_request': csr,
diff --git a/ipaserver/plugins/dogtag.py b/ipaserver/plugins/dogtag.py
index 880b319d68728a40f4479626d5a7c2b8c56ced02..e6668bb43b994863a14fdd347635753422ed9388 100644
--- a/ipaserver/plugins/dogtag.py
+++ b/ipaserver/plugins/dogtag.py
@@ -1284,6 +1284,8 @@ class ra(rabase.rabase):
     """
     Request Authority backend plugin.
     """
+    DEFAULT_PROFILE = dogtag.DEFAULT_PROFILE
+
     def __init__(self):
         if api.env.in_tree:
             self.sec_dir = api.env.dot_ipa + os.sep + 'alias'
@@ -1541,9 +1543,10 @@ class ra(rabase.rabase):
         return cmd_result
 
 
-    def request_certificate(self, csr, request_type='pkcs10'):
+    def request_certificate(self, csr, profile_id, request_type='pkcs10'):
         """
         :param csr: The certificate signing request.
+        :param profile_id: The profile to use for the request.
         :param request_type: The request type (defaults to ``'pkcs10'``).
 
         Submit certificate signing request.
@@ -1575,7 +1578,7 @@ class ra(rabase.rabase):
         http_status, http_reason_phrase, http_headers, http_body = \
             self._sslget('/ca/eeca/ca/profileSubmitSSLClient',
                          self.env.ca_ee_port,
-                         profileId='caIPAserviceCert',
+                         profileId=profile_id,
                          cert_request_type=request_type,
                          cert_request=csr,
                          xml='true')
diff --git a/ipaserver/plugins/rabase.py b/ipaserver/plugins/rabase.py
index e14969970ef5b402d06b766f895200c6eb4fc76f..cf4426235b02866a3f565c51c52c44aabbdc1153 100644
--- a/ipaserver/plugins/rabase.py
+++ b/ipaserver/plugins/rabase.py
@@ -67,11 +67,12 @@ class rabase(Backend):
         """
         raise errors.NotImplementedError(name='%s.get_certificate' % self.name)
 
-    def request_certificate(self, csr, request_type='pkcs10'):
+    def request_certificate(self, csr, profile_id, request_type='pkcs10'):
         """
         Submit certificate signing request.
 
         :param csr: The certificate signing request.
+        :param profile_id: Profile to use for this request.
         :param request_type: The request type (defaults to ``'pkcs10'``).
         """
         raise errors.NotImplementedError(name='%s.request_certificate' % self.name)
-- 
2.1.0

-------------- next part --------------
From dc50cb1b0881df76c2dddbe5c29e0b412d53d44d Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal at redhat.com>
Date: Mon, 18 May 2015 22:11:52 -0400
Subject: [PATCH 10/13] Add usercertificate attribute to user plugin

Part of: https://fedorahosted.org/freeipa/tickets/4938
---
 ACI.txt                        |  2 +-
 API.txt                        | 18 ++++++++++++------
 install/share/default-aci.ldif |  1 +
 install/updates/20-aci.update  |  4 ++++
 ipalib/plugins/baseuser.py     | 10 ++++++++--
 ipalib/plugins/user.py         |  2 +-
 6 files changed, 27 insertions(+), 10 deletions(-)

diff --git a/ACI.txt b/ACI.txt
index 543d8da69fb2adf79dc9821fb24028717670326a..59173ac1b593f15e079c7b1fce43ec9b0084ec91 100644
--- a/ACI.txt
+++ b/ACI.txt
@@ -297,7 +297,7 @@ aci: (targetattr = "krbprincipalkey || passwordhistory || sambalmpassword || sam
 dn: cn=users,cn=accounts,dc=ipa,dc=example
 aci: (targetattr = "ipasshpubkey")(targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:System: Manage User SSH Public Keys";allow (write) groupdn = "ldap:///cn=System: Manage User SSH Public Keys,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: cn=users,cn=accounts,dc=ipa,dc=example
-aci: (targetattr = "businesscategory || carlicense || cn || description || displayname || employeetype || facsimiletelephonenumber || gecos || givenname || homephone || inetuserhttpurl || initials || l || labeleduri || loginshell || manager || mepmanagedentry || mobile || objectclass || ou || pager || postalcode || preferredlanguage || roomnumber || secretary || seealso || sn || st || street || telephonenumber || title || userclass")(targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:System: Modify Users";allow (write) groupdn = "ldap:///cn=System: Modify Users,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+aci: (targetattr = "businesscategory || carlicense || cn || description || displayname || employeetype || facsimiletelephonenumber || gecos || givenname || homephone || inetuserhttpurl || initials || l || labeleduri || loginshell || manager || mepmanagedentry || mobile || objectclass || ou || pager || postalcode || preferredlanguage || roomnumber || secretary || seealso || sn || st || street || telephonenumber || title || usercertificate || userclass")(targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:System: Modify Users";allow (write) groupdn = "ldap:///cn=System: Modify Users,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: cn=UPG Definition,cn=Definitions,cn=Managed Entries,cn=etc,dc=ipa,dc=example
 aci: (targetattr = "*")(target = "ldap:///cn=UPG Definition,cn=Definitions,cn=Managed Entries,cn=etc,dc=ipa,dc=example")(version 3.0;acl "permission:System: Read UPG Definition";allow (compare,read,search) groupdn = "ldap:///cn=System: Read UPG Definition,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: cn=users,cn=accounts,dc=ipa,dc=example
diff --git a/API.txt b/API.txt
index 81aca14afcaa5234ad218b8d84f3bc8efc734c9d..abd9407af31aa511d767afd6dcc4f3470c7bcae9 100644
--- a/API.txt
+++ b/API.txt
@@ -3960,7 +3960,7 @@ output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDA
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: PrimaryKey('value', None, None)
 command: stageuser_add
-args: 1,43,3
+args: 1,44,3
 arg: Str('uid', attribute=True, cli_name='login', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?$', primary_key=True, required=True)
 option: Str('addattr*', cli_name='addattr', exclude='webui')
 option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
@@ -4002,6 +4002,7 @@ option: Str('street', attribute=True, cli_name='street', multivalue=False, requi
 option: Str('telephonenumber', attribute=True, cli_name='phone', multivalue=True, required=False)
 option: Str('title', attribute=True, cli_name='title', multivalue=False, required=False)
 option: Int('uidnumber', attribute=True, cli_name='uid', minvalue=1, multivalue=False, required=False)
+option: Bytes('usercertificate', attribute=True, cli_name='certificate', multivalue=True, required=False)
 option: Str('userclass', attribute=True, cli_name='class', multivalue=True, required=False)
 option: Password('userpassword', attribute=True, cli_name='password', exclude='webui', multivalue=False, required=False)
 option: Str('version?', exclude='webui')
@@ -4017,7 +4018,7 @@ output: Output('result', <type 'dict'>, None)
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: ListOfPrimaryKeys('value', None, None)
 command: stageuser_find
-args: 1,52,4
+args: 1,53,4
 arg: Str('criteria?', noextrawhitespace=False)
 option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
 option: Str('carlicense', attribute=True, autofill=False, cli_name='carlicense', multivalue=True, query=True, required=False)
@@ -4068,6 +4069,7 @@ option: Int('timelimit?', autofill=False, minvalue=0)
 option: Str('title', attribute=True, autofill=False, cli_name='title', multivalue=False, query=True, required=False)
 option: Str('uid', attribute=True, autofill=False, cli_name='login', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?$', primary_key=True, query=True, required=False)
 option: Int('uidnumber', attribute=True, autofill=False, cli_name='uid', minvalue=1, multivalue=False, query=True, required=False)
+option: Bytes('usercertificate', attribute=True, autofill=False, cli_name='certificate', multivalue=True, query=True, required=False)
 option: Str('userclass', attribute=True, autofill=False, cli_name='class', multivalue=True, query=True, required=False)
 option: Password('userpassword', attribute=True, autofill=False, cli_name='password', exclude='webui', multivalue=False, query=True, required=False)
 option: Str('version?', exclude='webui')
@@ -4076,7 +4078,7 @@ output: ListOfEntries('result', (<type 'list'>, <type 'tuple'>), Gettext('A list
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: Output('truncated', <type 'bool'>, None)
 command: stageuser_mod
-args: 1,44,3
+args: 1,45,3
 arg: Str('uid', attribute=True, cli_name='login', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?$', primary_key=True, query=True, required=True)
 option: Str('addattr*', cli_name='addattr', exclude='webui')
 option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
@@ -4119,6 +4121,7 @@ option: Str('street', attribute=True, autofill=False, cli_name='street', multiva
 option: Str('telephonenumber', attribute=True, autofill=False, cli_name='phone', multivalue=True, required=False)
 option: Str('title', attribute=True, autofill=False, cli_name='title', multivalue=False, required=False)
 option: Int('uidnumber', attribute=True, autofill=False, cli_name='uid', minvalue=1, multivalue=False, required=False)
+option: Bytes('usercertificate', attribute=True, autofill=False, cli_name='certificate', multivalue=True, required=False)
 option: Str('userclass', attribute=True, autofill=False, cli_name='class', multivalue=True, required=False)
 option: Password('userpassword', attribute=True, autofill=False, cli_name='password', exclude='webui', multivalue=False, required=False)
 option: Str('version?', exclude='webui')
@@ -4746,7 +4749,7 @@ output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDA
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: PrimaryKey('value', None, None)
 command: user_add
-args: 1,44,3
+args: 1,45,3
 arg: Str('uid', attribute=True, cli_name='login', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?$', primary_key=True, required=True)
 option: Str('addattr*', cli_name='addattr', exclude='webui')
 option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
@@ -4789,6 +4792,7 @@ option: Str('street', attribute=True, cli_name='street', multivalue=False, requi
 option: Str('telephonenumber', attribute=True, cli_name='phone', multivalue=True, required=False)
 option: Str('title', attribute=True, cli_name='title', multivalue=False, required=False)
 option: Int('uidnumber', attribute=True, cli_name='uid', minvalue=1, multivalue=False, required=False)
+option: Bytes('usercertificate', attribute=True, cli_name='certificate', multivalue=True, required=False)
 option: Str('userclass', attribute=True, cli_name='class', multivalue=True, required=False)
 option: Password('userpassword', attribute=True, cli_name='password', exclude='webui', multivalue=False, required=False)
 option: Str('version?', exclude='webui')
@@ -4820,7 +4824,7 @@ output: Output('result', <type 'bool'>, None)
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: PrimaryKey('value', None, None)
 command: user_find
-args: 1,55,4
+args: 1,56,4
 arg: Str('criteria?', noextrawhitespace=False)
 option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
 option: Str('carlicense', attribute=True, autofill=False, cli_name='carlicense', multivalue=True, query=True, required=False)
@@ -4873,6 +4877,7 @@ option: Int('timelimit?', autofill=False, minvalue=0)
 option: Str('title', attribute=True, autofill=False, cli_name='title', multivalue=False, query=True, required=False)
 option: Str('uid', attribute=True, autofill=False, cli_name='login', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?$', primary_key=True, query=True, required=False)
 option: Int('uidnumber', attribute=True, autofill=False, cli_name='uid', minvalue=1, multivalue=False, query=True, required=False)
+option: Bytes('usercertificate', attribute=True, autofill=False, cli_name='certificate', multivalue=True, query=True, required=False)
 option: Str('userclass', attribute=True, autofill=False, cli_name='class', multivalue=True, query=True, required=False)
 option: Password('userpassword', attribute=True, autofill=False, cli_name='password', exclude='webui', multivalue=False, query=True, required=False)
 option: Str('version?', exclude='webui')
@@ -4882,7 +4887,7 @@ output: ListOfEntries('result', (<type 'list'>, <type 'tuple'>), Gettext('A list
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: Output('truncated', <type 'bool'>, None)
 command: user_mod
-args: 1,45,3
+args: 1,46,3
 arg: Str('uid', attribute=True, cli_name='login', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?$', primary_key=True, query=True, required=True)
 option: Str('addattr*', cli_name='addattr', exclude='webui')
 option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
@@ -4926,6 +4931,7 @@ option: Str('street', attribute=True, autofill=False, cli_name='street', multiva
 option: Str('telephonenumber', attribute=True, autofill=False, cli_name='phone', multivalue=True, required=False)
 option: Str('title', attribute=True, autofill=False, cli_name='title', multivalue=False, required=False)
 option: Int('uidnumber', attribute=True, autofill=False, cli_name='uid', minvalue=1, multivalue=False, required=False)
+option: Bytes('usercertificate', attribute=True, autofill=False, cli_name='certificate', multivalue=True, required=False)
 option: Str('userclass', attribute=True, autofill=False, cli_name='class', multivalue=True, required=False)
 option: Password('userpassword', attribute=True, autofill=False, cli_name='password', exclude='webui', multivalue=False, required=False)
 option: Str('version?', exclude='webui')
diff --git a/install/share/default-aci.ldif b/install/share/default-aci.ldif
index af7eedb0b92375f893a61ad1fb6e2d7b176389f9..7b174e774aae3ea012a431fe4a2535fb4230e402 100644
--- a/install/share/default-aci.ldif
+++ b/install/share/default-aci.ldif
@@ -10,6 +10,7 @@ changetype: modify
 add: aci
 aci: (targetattr = "givenname || sn || cn || displayname || title || initials || loginshell || gecos || homephone || mobile || pager || facsimiletelephonenumber || telephonenumber || street || roomnumber || l || st || postalcode || manager || secretary || description || carlicense || labeleduri || inetuserhttpurl || seealso || employeetype  || businesscategory || ou")(version 3.0;acl "selfservice:User Self service";allow (write) userdn = "ldap:///self";)
 aci: (targetattr = "ipasshpubkey")(version 3.0;acl "selfservice:Users can manage their own SSH public keys";allow (write) userdn = "ldap:///self";)
+aci: (targetattr = "usercertificate")(version 3.0;acl "selfservice:Users can manage their own X.509 certificates";allow (write) userdn = "ldap:///self";)
 
 dn: cn=etc,$SUFFIX
 changetype: modify
diff --git a/install/updates/20-aci.update b/install/updates/20-aci.update
index fde3afeee59e4d4dc0bd6a9c0eb24ab255c4e637..4a8b67c6579da4dab74d02861640264446153f87 100644
--- a/install/updates/20-aci.update
+++ b/install/updates/20-aci.update
@@ -79,3 +79,7 @@ add:aci: (targetattr="ipaProtectedOperation;write_keys")(version 3.0; acl "Group
 add:aci: (targetattr="ipaProtectedOperation;write_keys")(version 3.0; acl "Entities are allowed to rekey themselves"; allow(write) userdn="ldap:///self";)
 add:aci: (targetattr="ipaProtectedOperation;write_keys")(version 3.0; acl "Admins are allowed to rekey any entity"; allow(write) groupdn = "ldap:///cn=admins,cn=groups,cn=accounts,$SUFFIX";)
 add:aci: (targetfilter="(|(objectclass=ipaHost)(objectclass=ipaService))")(targetattr="ipaProtectedOperation;write_keys")(version 3.0; acl "Entities are allowed to rekey managed entries"; allow(write) userattr="managedby#USERDN";)
+
+# User certificates
+dn: $SUFFIX
+add:aci:(targetattr = "usercertificate")(version 3.0;acl "selfservice:Users can manage their own X.509 certificates";allow (write) userdn = "ldap:///self";)
diff --git a/ipalib/plugins/baseuser.py b/ipalib/plugins/baseuser.py
index a1be29d83550a0412ed37cfde47ac74c6ca478fd..d2bc68f45ad9a3632a237c01961a30592514d96d 100644
--- a/ipalib/plugins/baseuser.py
+++ b/ipalib/plugins/baseuser.py
@@ -23,10 +23,11 @@ import posixpath
 import os
 
 from ipalib import api, errors
-from ipalib import Flag, Int, Password, Str, Bool, StrEnum, DateTime
+from ipalib import Flag, Int, Password, Str, Bool, StrEnum, DateTime, Bytes
 from ipalib.plugable import Registry
 from ipalib.plugins.baseldap import DN, LDAPObject, \
     LDAPCreate, LDAPUpdate, LDAPSearch, LDAPDelete, LDAPRetrieve
+from ipalib.plugins.service import validate_certificate
 from ipalib.plugins import baseldap
 from ipalib.request import context
 from ipalib import _, ngettext
@@ -188,7 +189,7 @@ class baseuser(LDAPObject):
         'telephonenumber', 'title', 'memberof', 'nsaccountlock',
         'memberofindirect', 'ipauserauthtype', 'userclass',
         'ipatokenradiusconfiglink', 'ipatokenradiususername',
-        'krbprincipalexpiration'
+        'krbprincipalexpiration', 'usercertificate',
     ]
     search_display_attributes = [
         'uid', 'givenname', 'sn', 'homedirectory', 'loginshell',
@@ -383,6 +384,11 @@ class baseuser(LDAPObject):
              + '(\s*,\s*[a-zA-Z]{1,8}(-[a-zA-Z]{1,8})?(;q\=((0(\.[0-9]{0,3})?)|(1(\.0{0,3})?)))?)*)|(\*))$',
             pattern_errmsg='must match RFC 2068 - 14.4, e.g., "da, en-gb;q=0.8, en;q=0.7"',
         ),
+        Bytes('usercertificate*', validate_certificate,
+            cli_name='certificate',
+            label=_('Certificate'),
+            doc=_('Base-64 encoded server certificate'),
+        ),
     )
 
     def normalize_and_validate_email(self, email, config=None):
diff --git a/ipalib/plugins/user.py b/ipalib/plugins/user.py
index 54d47bb01450ec462577e552315e3d680b7648c3..119294b19f54a395a2df65c6cfd47cd8eb844297 100644
--- a/ipalib/plugins/user.py
+++ b/ipalib/plugins/user.py
@@ -267,7 +267,7 @@ class user(baseuser):
                 'mepmanagedentry', 'mobile', 'objectclass', 'ou', 'pager',
                 'postalcode', 'roomnumber', 'secretary', 'seealso', 'sn', 'st',
                 'street', 'telephonenumber', 'title', 'userclass',
-                'preferredlanguage',
+                'preferredlanguage', 'usercertificate',
             },
             'replaces': [
                 '(targetattr = "givenname || sn || cn || displayname || title || initials || loginshell || gecos || homephone || mobile || pager || facsimiletelephonenumber || telephonenumber || street || roomnumber || l || st || postalcode || manager || secretary || description || carlicense || labeleduri || inetuserhttpurl || seealso || employeetype || businesscategory || ou || mepmanagedentry || objectclass")(target = "ldap:///uid=*,cn=users,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Modify Users";allow (write) groupdn = "ldap:///cn=Modify Users,cn=permissions,cn=pbac,$SUFFIX";)',
-- 
2.1.0

-------------- next part --------------
From 82ebf9d81d7363adf87996e100fc72349444447a Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal at redhat.com>
Date: Thu, 14 May 2015 01:45:16 -0400
Subject: [PATCH 11/13] Update cert-request to support user certs and profiles

Part of: https://fedorahosted.org/freeipa/ticket/57
Part of: https://fedorahosted.org/freeipa/ticket/4938
---
 API.txt                |   3 +-
 ipalib/pkcs10.py       |   1 +
 ipalib/plugins/cert.py | 220 +++++++++++++++++++++++++++++--------------------
 3 files changed, 135 insertions(+), 89 deletions(-)

diff --git a/API.txt b/API.txt
index abd9407af31aa511d767afd6dcc4f3470c7bcae9..7574bc900e7a962b8e67fd773743879e4e5b8c7e 100644
--- a/API.txt
+++ b/API.txt
@@ -485,10 +485,11 @@ arg: Str('serial_number')
 option: Str('version?', exclude='webui')
 output: Output('result', None, None)
 command: cert_request
-args: 1,4,1
+args: 1,5,1
 arg: File('csr', cli_name='csr_file')
 option: Flag('add', autofill=True, default=False)
 option: Str('principal')
+option: Str('profile_id?')
 option: Str('request_type', autofill=True, default=u'pkcs10')
 option: Str('version?', exclude='webui')
 output: Output('result', <type 'dict'>, None)
diff --git a/ipalib/pkcs10.py b/ipalib/pkcs10.py
index f35e200a2c1b47e2a2c8cffcf9b723f398fe3221..6299dfea43b7a3f4104f0b0ec78c4f105d9daf62 100644
--- a/ipalib/pkcs10.py
+++ b/ipalib/pkcs10.py
@@ -30,6 +30,7 @@ PEM = 0
 DER = 1
 
 SAN_DNSNAME = 'DNS name'
+SAN_RFC822NAME = 'RFC822 Name'
 SAN_OTHERNAME_UPN = 'Other Name (OID.1.3.6.1.4.1.311.20.2.3)'
 SAN_OTHERNAME_KRB5PRINCIPALNAME = 'Other Name (OID.1.3.6.1.5.2.2)'
 
diff --git a/ipalib/plugins/cert.py b/ipalib/plugins/cert.py
index e4cb6dc0aa8b89368806b08674aae277b3653e8f..d122900175db41ba5af429fd47af6cac6533cb6f 100644
--- a/ipalib/plugins/cert.py
+++ b/ipalib/plugins/cert.py
@@ -31,7 +31,8 @@ from ipalib import ngettext
 from ipalib.plugable import Registry
 from ipalib.plugins.virtual import *
 from ipalib.plugins.baseldap import pkey_to_value
-from ipalib.plugins.service import split_principal
+from ipalib.plugins.service import split_any_principal
+from ipalib.plugins.certprofile import validate_profile_id
 import base64
 import traceback
 from ipalib.text import _
@@ -122,6 +123,8 @@ http://www.ietf.org/rfc/rfc5280.txt
 
 """)
 
+USER, HOST, SERVICE = range(3)
+
 register = Registry()
 
 def validate_pkidate(ugettext, value):
@@ -232,7 +235,7 @@ class cert_request(VirtualCommand):
     takes_options = (
         Str('principal',
             label=_('Principal'),
-            doc=_('Service principal for this certificate (e.g. HTTP/test.example.com)'),
+            doc=_('Principal for this certificate (e.g. HTTP/test.example.com)'),
         ),
         Str('request_type',
             default=u'pkcs10',
@@ -243,6 +246,10 @@ class cert_request(VirtualCommand):
             default=False,
             autofill=True
         ),
+        Str('profile_id?', validate_profile_id,
+            label=_("Profile ID"),
+            doc=_("Certificate Profile to use"),
+        )
     )
 
     has_output_params = (
@@ -294,10 +301,9 @@ class cert_request(VirtualCommand):
         ca_enabled_check()
 
         ldap = self.api.Backend.ldap2
-        principal = kw.get('principal')
         add = kw.get('add')
         request_type = kw.get('request_type')
-        service = None
+        profile_id = kw.get('profile_id', self.Backend.ra.DEFAULT_PROFILE)
 
         """
         Access control is partially handled by the ACI titled
@@ -310,9 +316,28 @@ class cert_request(VirtualCommand):
         taskgroup (directly or indirectly via role membership).
         """
 
-        bind_principal = getattr(context, 'principal')
-        # Can this user request certs?
-        if not bind_principal.startswith('host/'):
+        principal_string = kw.get('principal')
+        principal = split_any_principal(principal_string)
+        servicename, principal_name, realm = principal
+        if servicename is None:
+            principal_type = USER
+        elif servicename == 'host':
+            principal_type = HOST
+        else:
+            principal_type = SERVICE
+
+        bind_principal = split_any_principal(getattr(context, 'principal'))
+        bind_service, bind_name, bind_realm = bind_principal
+
+        if bind_service is None:
+            bind_principal_type = USER
+        elif bind_service == 'host':
+            bind_principal_type = HOST
+        else:
+            bind_principal_type = SERVICE
+
+        if bind_principal != principal and bind_principal_type != HOST:
+            # Can the bound principal request certs for another principal?
             self.check_access()
 
         try:
@@ -323,57 +348,71 @@ class cert_request(VirtualCommand):
             raise errors.CertificateOperationError(
                 error=_("Failure decoding Certificate Signing Request: %s") % e)
 
-        if not bind_principal.startswith('host/'):
+        # host principals may bypass allowed ext check
+        if bind_principal_type != HOST:
             for ext in extensions:
                 operation = self._allowed_extensions.get(ext)
                 if operation:
                     self.check_access(operation)
 
-        # Ensure that the hostname in the CSR matches the principal
-        subject_host = subject.common_name  #pylint: disable=E1101
-        if not subject_host:
+        dn = None
+        principal_obj = None
+        # See if the service exists and punt if it doesn't and we aren't
+        # going to add it
+        try:
+            if principal_type == SERVICE:
+                principal_obj = api.Command['service_show'](principal_string, all=True)
+            elif principal_type == HOST:
+                principal_obj = api.Command['host_show'](principal_name, all=True)
+            elif principal_type == USER:
+                principal_obj = api.Command['user_show'](principal_name, all=True)
+        except errors.NotFound as e:
+            if principal_type == SERVICE and add:
+                principal_obj = api.Command['service_add'](principal_string, force=True)
+            else:
+                raise errors.NotFound(
+                    reason=_("The principal for this request doesn't exist."))
+        principal_obj = principal_obj['result']
+        dn = principal_obj['dn']
+
+        # Ensure that the DN in the CSR matches the principal
+        cn = subject.common_name  #pylint: disable=E1101
+        if not cn:
             raise errors.ValidationError(name='csr',
-                error=_("No hostname was found in subject of request."))
+                error=_("No Common Name was found in subject of request."))
 
-        (servicename, hostname, realm) = split_principal(principal)
-        if subject_host.lower() != hostname.lower():
-            raise errors.ACIError(
-                info=_("hostname in subject of request '%(subject_host)s' "
-                    "does not match principal hostname '%(hostname)s'") % dict(
-                        subject_host=subject_host, hostname=hostname))
+        if principal_type in (SERVICE, HOST):
+            if cn.lower() != principal_name.lower():
+                raise errors.ACIError(
+                    info=_("hostname in subject of request '%(cn)s' "
+                        "does not match principal hostname '%(hostname)s'")
+                        % dict(cn=cn, hostname=principal_name))
+        elif principal_type == USER:
+            # check user name
+            if cn != principal_name:
+                raise errors.ValidationError(
+                    name='csr',
+                    error=_(
+                        "DN commonName does not match "
+                        "any of user's email addresses")
+                )
+
+            # check email address
+            mail = subject.email_address  #pylint: disable=E1101
+            if mail is not None and mail not in principal_obj.get('mail', []):
+                raise errors.ValidationError(
+                    name='csr',
+                    error=_(
+                        "DN emailAddress does not match "
+                        "any of user's email addresses")
+                )
 
         for ext in extensions:
             if ext not in self._allowed_extensions:
                 raise errors.ValidationError(
                     name='csr', error=_("extension %s is forbidden") % ext)
 
-        for name_type, name in subjectaltname:
-            if name_type not in (pkcs10.SAN_DNSNAME,
-                                 pkcs10.SAN_OTHERNAME_KRB5PRINCIPALNAME,
-                                 pkcs10.SAN_OTHERNAME_UPN):
-                raise errors.ValidationError(
-                    name='csr',
-                    error=_("subject alt name type %s is forbidden") %
-                          name_type)
-
-        dn = None
-        service = None
-        # See if the service exists and punt if it doesn't and we aren't
-        # going to add it
-        try:
-            if servicename != 'host':
-                service = api.Command['service_show'](principal, all=True)
-            else:
-                service = api.Command['host_show'](hostname, all=True)
-        except errors.NotFound, e:
-            if not add:
-                raise errors.NotFound(reason=_("The service principal for "
-                    "this request doesn't exist."))
-            service = api.Command['service_add'](principal, force=True)
-        service = service['result']
-        dn = service['dn']
-
-        # We got this far so the service entry exists, can we write it?
+        # We got this far so the principal entry exists, can we write it?
         if not ldap.can_write(dn, "usercertificate"):
             raise errors.ACIError(info=_("Insufficient 'write' privilege "
                 "to the 'userCertificate' attribute of entry '%s'.") % dn)
@@ -382,13 +421,20 @@ class cert_request(VirtualCommand):
         for name_type, name in subjectaltname:
             if name_type == pkcs10.SAN_DNSNAME:
                 name = unicode(name)
+                alt_principal_obj = None
                 try:
-                    if servicename == 'host':
-                        altservice = api.Command['host_show'](name, all=True)
-                    else:
+                    if principal_type == HOST:
+                        alt_principal_obj = api.Command['host_show'](name, all=True)
+                    elif principal_type == SERVICE:
                         altprincipal = '%s/%s@%s' % (servicename, name, realm)
-                        altservice = api.Command['service_show'](
+                        alt_principal_obj = api.Command['service_show'](
                             altprincipal, all=True)
+                    elif principal_type == USER:
+                        raise errors.ValidationError(
+                            name='csr',
+                            error=_("subject alt name type %s is forbidden "
+                                "for user principals") % name_type
+                        )
                 except errors.NotFound:
                     # We don't want to issue any certificates referencing
                     # machines we don't know about. Nothing is stored in this
@@ -396,47 +442,41 @@ class cert_request(VirtualCommand):
                     raise errors.NotFound(reason=_('The service principal for '
                         'subject alt name %s in certificate request does not '
                         'exist') % name)
-                altdn = altservice['result']['dn']
-                if not ldap.can_write(altdn, "usercertificate"):
-                    raise errors.ACIError(info=_(
-                        "Insufficient privilege to create a certificate with "
-                        "subject alt name '%s'.") % name)
+                if alt_principal_obj is not None:
+                    altdn = alt_principal_obj['result']['dn']
+                    if not ldap.can_write(altdn, "usercertificate"):
+                        raise errors.ACIError(info=_(
+                            "Insufficient privilege to create a certificate "
+                            "with subject alt name '%s'.") % name)
             elif name_type in (pkcs10.SAN_OTHERNAME_KRB5PRINCIPALNAME,
                                pkcs10.SAN_OTHERNAME_UPN):
-                if name != principal:
+                if name != principal_string:
                     raise errors.ACIError(
                         info=_("Principal '%s' in subject alt name does not "
-                               "match requested service principal") % name)
+                               "match requested principal") % name)
+            elif name_type == pkcs10.SAN_RFC822NAME:
+                if principal_type == USER:
+                    if name not in principal_obj.get('mail', []):
+                        raise errors.ValidationError(
+                            name='csr',
+                            error=_(
+                                "RFC822Name does not match "
+                                "any of user's email addresses")
+                        )
+                else:
+                    raise errors.ValidationError(
+                        name='csr',
+                        error=_("subject alt name type %s is forbidden "
+                            "for non-user principals") % name_type
+                    )
             else:
                 raise errors.ACIError(
                     info=_("Subject alt name type %s is forbidden") %
                          name_type)
 
-        if 'usercertificate' in service:
-            serial = x509.get_serial_number(service['usercertificate'][0], datatype=x509.DER)
-            # revoke the certificate and remove it from the service
-            # entry before proceeding. First we retrieve the certificate to
-            # see if it is already revoked, if not then we revoke it.
-            try:
-                result = api.Command['cert_show'](unicode(serial))['result']
-                if 'revocation_reason' not in result:
-                    try:
-                        api.Command['cert_revoke'](unicode(serial), revocation_reason=4)
-                    except errors.NotImplementedError:
-                        # some CA's might not implement revoke
-                        pass
-            except errors.NotImplementedError:
-                # some CA's might not implement get
-                pass
-            if not principal.startswith('host/'):
-                api.Command['service_mod'](principal, usercertificate=None)
-            else:
-                hostname = get_host_from_principal(principal)
-                api.Command['host_mod'](hostname, usercertificate=None)
-
         # Request the certificate
         result = self.Backend.ra.request_certificate(
-            csr, 'caIPAserviceCert', request_type=request_type)
+            csr, profile_id, request_type=request_type)
         cert = x509.load_certificate(result['certificate'])
         result['issuer'] = unicode(cert.issuer)
         result['valid_not_before'] = unicode(cert.valid_not_before_str)
@@ -444,15 +484,19 @@ class cert_request(VirtualCommand):
         result['md5_fingerprint'] = unicode(nss.data_to_hex(nss.md5_digest(cert.der_data), 64)[0])
         result['sha1_fingerprint'] = unicode(nss.data_to_hex(nss.sha1_digest(cert.der_data), 64)[0])
 
-        # Success? Then add it to the service entry.
-        if 'certificate' in result:
-            if not principal.startswith('host/'):
-                skw = {"usercertificate": str(result.get('certificate'))}
-                api.Command['service_mod'](principal, **skw)
-            else:
-                hostname = get_host_from_principal(principal)
-                skw = {"usercertificate": str(result.get('certificate'))}
-                api.Command['host_mod'](hostname, **skw)
+        # Success? Then add it to the principal's entry
+        # (unless the profile tells us not to)
+        profile = api.Command['certprofile_show'](profile_id)
+        store = profile['result']['ipacertprofilestoreissued'][0] == 'TRUE'
+        if store and 'certificate' in result:
+            cert = str(result.get('certificate'))
+            kwargs = dict(addattr=u'usercertificate={}'.format(cert))
+            if principal_type == SERVICE:
+                api.Command['service_mod'](principal_string, **kwargs)
+            elif principal_type == HOST:
+                api.Command['host_mod'](principal_name, **kwargs)
+            elif principal_type == USER:
+                api.Command['user_mod'](principal_name, **kwargs)
 
         return dict(
             result=result
-- 
2.1.0

-------------- next part --------------
From c00f627594a9c7e25495ab730c3559a08770724f Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal at redhat.com>
Date: Mon, 25 May 2015 08:39:07 -0400
Subject: [PATCH 12/13] Add CA ACL plugin

Implement the caacl commands, which are used to indicate which
principals may be issued certificates from which (sub-)CAs, using
which profiles.

At this commit, and until sub-CAs are implemented, all rules refer
to the top-level CA (represented as ".") and no ca-ref argument is
exposed.

Also add a default CA ACL that permits certificate issuance for all
hosts and services using the profile 'caIPAserviceCert' on the
top-level CA.  This rule is added during install but not upgrade.

Part of: https://fedorahosted.org/freeipa/ticket/57
Part of: https://fedorahosted.org/freeipa/ticket/4559
---
 ACI.txt                                   |  10 +
 API.txt                                   | 190 +++++++++++++++
 VERSION                                   |   4 +-
 install/share/60certificate-profiles.ldif |   8 +
 install/share/Makefile.am                 |   1 +
 install/share/bootstrap-template.ldif     |   6 +
 install/share/default-caacl.ldif          |  12 +
 install/updates/41-caacl.update           |   4 +
 install/updates/Makefile.am               |   1 +
 ipalib/constants.py                       |   1 +
 ipalib/plugins/caacl.py                   | 371 ++++++++++++++++++++++++++++++
 ipaserver/install/dsinstance.py           |   4 +
 12 files changed, 610 insertions(+), 2 deletions(-)
 create mode 100644 install/share/default-caacl.ldif
 create mode 100644 install/updates/41-caacl.update
 create mode 100644 ipalib/plugins/caacl.py

diff --git a/ACI.txt b/ACI.txt
index 59173ac1b593f15e079c7b1fce43ec9b0084ec91..316fb34faba18d77b820ff2fb730ea07a4c5b8ec 100644
--- a/ACI.txt
+++ b/ACI.txt
@@ -22,6 +22,16 @@ dn: cn=automount,dc=ipa,dc=example
 aci: (targetattr = "automountmapname || description")(targetfilter = "(objectclass=automountmap)")(version 3.0;acl "permission:System: Modify Automount Maps";allow (write) groupdn = "ldap:///cn=System: Modify Automount Maps,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: cn=automount,dc=ipa,dc=example
 aci: (targetfilter = "(objectclass=automountmap)")(version 3.0;acl "permission:System: Remove Automount Maps";allow (delete) groupdn = "ldap:///cn=System: Remove Automount Maps,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=caacls,cn=ca,dc=ipa,dc=example
+aci: (targetfilter = "(objectclass=ipacaacl)")(version 3.0;acl "permission:System: Add CA ACL";allow (add) groupdn = "ldap:///cn=System: Add CA ACL,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=caacls,cn=ca,dc=ipa,dc=example
+aci: (targetfilter = "(objectclass=ipacaacl)")(version 3.0;acl "permission:System: Delete CA ACL";allow (delete) groupdn = "ldap:///cn=System: Delete CA ACL,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=caacls,cn=ca,dc=ipa,dc=example
+aci: (targetattr = "ipacaaclallhosts || ipacaaclallprofiles || ipacaaclallservices || ipacaaclallusers || ipacaaclmembercertprofile || memberhost || memberservice || memberuser")(targetfilter = "(objectclass=ipacaacl)")(version 3.0;acl "permission:System: Manage CA ACL Membership";allow (write) groupdn = "ldap:///cn=System: Manage CA ACL Membership,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=caacls,cn=ca,dc=ipa,dc=example
+aci: (targetattr = "cn || description || ipacaaclallcas || ipacaaclcaref || ipaenabledflag")(targetfilter = "(objectclass=ipacaacl)")(version 3.0;acl "permission:System: Modify CA ACL";allow (write) groupdn = "ldap:///cn=System: Modify CA ACL,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=caacls,cn=ca,dc=ipa,dc=example
+aci: (targetattr = "cn || createtimestamp || description || entryusn || ipacaaclallcas || ipacaaclallhosts || ipacaaclallprofiles || ipacaaclallservices || ipacaaclallusers || ipacaaclcaref || ipacaaclmembercertprofile || ipaenabledflag || ipauniqueid || member || memberhost || memberservice || memberuser || modifytimestamp || objectclass")(targetfilter = "(objectclass=ipacaacl)")(version 3.0;acl "permission:System: Read CA ACLs";allow (compare,read,search) userdn = "ldap:///all";)
 dn: cn=certprofiles,cn=ca,dc=ipa,dc=example
 aci: (targetfilter = "(objectclass=ipacertprofile)")(version 3.0;acl "permission:System: Delete Certificate Profile";allow (delete) groupdn = "ldap:///cn=System: Delete Certificate Profile,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: cn=certprofiles,cn=ca,dc=ipa,dc=example
diff --git a/API.txt b/API.txt
index 7574bc900e7a962b8e67fd773743879e4e5b8c7e..1b3044d1737df59f7cb16f98fd5bbdfd88ccf5c1 100644
--- a/API.txt
+++ b/API.txt
@@ -456,6 +456,196 @@ option: Str('version?', exclude='webui')
 output: Output('result', <type 'bool'>, None)
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: PrimaryKey('value', None, None)
+command: caacl_add
+args: 1,14,3
+arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, required=True)
+option: Str('addattr*', cli_name='addattr', exclude='webui')
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('description', attribute=True, cli_name='desc', multivalue=False, required=False)
+option: Bool('ipacaaclallcas', attribute=True, cli_name='allcas', multivalue=False, required=False)
+option: Bool('ipacaaclallhosts', attribute=True, cli_name='allhosts', multivalue=False, required=False)
+option: Bool('ipacaaclallprofiles', attribute=True, cli_name='allprofiles', multivalue=False, required=False)
+option: Bool('ipacaaclallservices', attribute=True, cli_name='allservices', multivalue=False, required=False)
+option: Bool('ipacaaclallusers', attribute=True, cli_name='allusers', multivalue=False, required=False)
+option: Str('ipacaaclcaref', attribute=True, cli_name='ca_ref', multivalue=True, required=False)
+option: Bool('ipaenabledflag', attribute=True, cli_name='ipaenabledflag', multivalue=False, required=False)
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('setattr*', cli_name='setattr', exclude='webui')
+option: Str('version?', exclude='webui')
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: PrimaryKey('value', None, None)
+command: caacl_add_host
+args: 1,6,3
+arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('host*', alwaysask=True, cli_name='hosts', csv=True)
+option: Str('hostgroup*', alwaysask=True, cli_name='hostgroups', csv=True)
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('version?', exclude='webui')
+output: Output('completed', <type 'int'>, None)
+output: Output('failed', <type 'dict'>, None)
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+command: caacl_add_profile
+args: 1,5,3
+arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('certprofile*', alwaysask=True, cli_name='certprofiles', csv=True)
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('version?', exclude='webui')
+output: Output('completed', <type 'int'>, None)
+output: Output('failed', <type 'dict'>, None)
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+command: caacl_add_service
+args: 1,5,3
+arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('service*', alwaysask=True, cli_name='services', csv=True)
+option: Str('version?', exclude='webui')
+output: Output('completed', <type 'int'>, None)
+output: Output('failed', <type 'dict'>, None)
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+command: caacl_add_user
+args: 1,6,3
+arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('group*', alwaysask=True, cli_name='groups', csv=True)
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('user*', alwaysask=True, cli_name='users', csv=True)
+option: Str('version?', exclude='webui')
+output: Output('completed', <type 'int'>, None)
+output: Output('failed', <type 'dict'>, None)
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+command: caacl_del
+args: 1,2,3
+arg: Str('cn', attribute=True, cli_name='name', multivalue=True, primary_key=True, query=True, required=True)
+option: Flag('continue', autofill=True, cli_name='continue', default=False)
+option: Str('version?', exclude='webui')
+output: Output('result', <type 'dict'>, None)
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: ListOfPrimaryKeys('value', None, None)
+command: caacl_disable
+args: 1,1,3
+arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
+option: Str('version?', exclude='webui')
+output: Output('result', <type 'bool'>, None)
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: PrimaryKey('value', None, None)
+command: caacl_enable
+args: 1,1,3
+arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
+option: Str('version?', exclude='webui')
+output: Output('result', <type 'bool'>, None)
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: PrimaryKey('value', None, None)
+command: caacl_find
+args: 1,16,4
+arg: Str('criteria?', noextrawhitespace=False)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('cn', attribute=True, autofill=False, cli_name='name', multivalue=False, primary_key=True, query=True, required=False)
+option: Str('description', attribute=True, autofill=False, cli_name='desc', multivalue=False, query=True, required=False)
+option: Bool('ipacaaclallcas', attribute=True, autofill=False, cli_name='allcas', multivalue=False, query=True, required=False)
+option: Bool('ipacaaclallhosts', attribute=True, autofill=False, cli_name='allhosts', multivalue=False, query=True, required=False)
+option: Bool('ipacaaclallprofiles', attribute=True, autofill=False, cli_name='allprofiles', multivalue=False, query=True, required=False)
+option: Bool('ipacaaclallservices', attribute=True, autofill=False, cli_name='allservices', multivalue=False, query=True, required=False)
+option: Bool('ipacaaclallusers', attribute=True, autofill=False, cli_name='allusers', multivalue=False, query=True, required=False)
+option: Str('ipacaaclcaref', attribute=True, autofill=False, cli_name='ca_ref', multivalue=True, query=True, required=False)
+option: Bool('ipaenabledflag', attribute=True, autofill=False, cli_name='ipaenabledflag', multivalue=False, query=True, required=False)
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Flag('pkey_only?', autofill=True, default=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Int('sizelimit?', autofill=False, minvalue=0)
+option: Int('timelimit?', autofill=False, minvalue=0)
+option: Str('version?', exclude='webui')
+output: Output('count', <type 'int'>, None)
+output: ListOfEntries('result', (<type 'list'>, <type 'tuple'>), Gettext('A list of LDAP entries', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: Output('truncated', <type 'bool'>, None)
+command: caacl_mod
+args: 1,16,3
+arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
+option: Str('addattr*', cli_name='addattr', exclude='webui')
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('delattr*', cli_name='delattr', exclude='webui')
+option: Str('description', attribute=True, autofill=False, cli_name='desc', multivalue=False, required=False)
+option: Bool('ipacaaclallcas', attribute=True, autofill=False, cli_name='allcas', multivalue=False, required=False)
+option: Bool('ipacaaclallhosts', attribute=True, autofill=False, cli_name='allhosts', multivalue=False, required=False)
+option: Bool('ipacaaclallprofiles', attribute=True, autofill=False, cli_name='allprofiles', multivalue=False, required=False)
+option: Bool('ipacaaclallservices', attribute=True, autofill=False, cli_name='allservices', multivalue=False, required=False)
+option: Bool('ipacaaclallusers', attribute=True, autofill=False, cli_name='allusers', multivalue=False, required=False)
+option: Str('ipacaaclcaref', attribute=True, autofill=False, cli_name='ca_ref', multivalue=True, required=False)
+option: Bool('ipaenabledflag', attribute=True, autofill=False, cli_name='ipaenabledflag', multivalue=False, required=False)
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Flag('rights', autofill=True, default=False)
+option: Str('setattr*', cli_name='setattr', exclude='webui')
+option: Str('version?', exclude='webui')
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: PrimaryKey('value', None, None)
+command: caacl_remove_host
+args: 1,6,3
+arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('host*', alwaysask=True, cli_name='hosts', csv=True)
+option: Str('hostgroup*', alwaysask=True, cli_name='hostgroups', csv=True)
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('version?', exclude='webui')
+output: Output('completed', <type 'int'>, None)
+output: Output('failed', <type 'dict'>, None)
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+command: caacl_remove_profile
+args: 1,5,3
+arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('certprofile*', alwaysask=True, cli_name='certprofiles', csv=True)
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('version?', exclude='webui')
+output: Output('completed', <type 'int'>, None)
+output: Output('failed', <type 'dict'>, None)
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+command: caacl_remove_service
+args: 1,5,3
+arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('service*', alwaysask=True, cli_name='services', csv=True)
+option: Str('version?', exclude='webui')
+output: Output('completed', <type 'int'>, None)
+output: Output('failed', <type 'dict'>, None)
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+command: caacl_remove_user
+args: 1,6,3
+arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('group*', alwaysask=True, cli_name='groups', csv=True)
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('user*', alwaysask=True, cli_name='users', csv=True)
+option: Str('version?', exclude='webui')
+output: Output('completed', <type 'int'>, None)
+output: Output('failed', <type 'dict'>, None)
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+command: caacl_show
+args: 1,5,3
+arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Flag('rights', autofill=True, default=False)
+option: Str('version?', exclude='webui')
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: PrimaryKey('value', None, None)
 command: cert_find
 args: 0,17,4
 option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
diff --git a/VERSION b/VERSION
index 2ad3827923bc0f404513300edc8498ed6717c571..3dad789f0b673c1dc11cf6e938c5f7096078027e 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=123
-# Last change: rcritten - added service constraint delegation plugin
+IPA_API_VERSION_MINOR=124
+# Last change: ftweedal - add certprofile and caacl plugins
diff --git a/install/share/60certificate-profiles.ldif b/install/share/60certificate-profiles.ldif
index f1281949e53386e5bfe8b35e0c15858c693c5467..2afd93ee9c0f92dc4073d4be9164734f524423de 100644
--- a/install/share/60certificate-profiles.ldif
+++ b/install/share/60certificate-profiles.ldif
@@ -1,3 +1,11 @@
 dn: cn=schema
 attributeTypes: (2.16.840.1.113730.3.8.21.1.1 NAME 'ipaCertProfileStoreIssued' DESC 'Store certificates issued using this profile' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE X-ORIGIN 'IPA v4.2' )
+attributeTypes: (2.16.840.1.113730.3.8.21.1.2 NAME 'ipaCaAclCaRef' DESC 'Certificate Authority Reference' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.44 X-ORIGIN 'IPA v4.2' )
+attributeTypes: (2.16.840.1.113730.3.8.21.1.3 NAME 'ipaCaAclMemberCertprofile' DESC 'CA ACL certificate profile members' SUP distinguishedName EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 X-ORIGIN 'IPA v4.2' )
+attributeTypes: (2.16.840.1.113730.3.8.21.1.4 NAME 'ipaCaAclAllCAs' DESC 'Allow use of all CAs' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE X-ORIGIN 'IPA v4.2' )
+attributeTypes: (2.16.840.1.113730.3.8.21.1.5 NAME 'ipaCaAclAllProfiles' DESC 'Allow ues of all profiles' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE X-ORIGIN 'IPA v4.2' )
+attributeTypes: (2.16.840.1.113730.3.8.21.1.6 NAME 'ipaCaAclAllUsers' DESC 'Allow all users' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE X-ORIGIN 'IPA v4.2' )
+attributeTypes: (2.16.840.1.113730.3.8.21.1.7 NAME 'ipaCaAclAllHosts' DESC 'Allow all hosts' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE X-ORIGIN 'IPA v4.2' )
+attributeTypes: (2.16.840.1.113730.3.8.21.1.8 NAME 'ipaCaAclAllServices' DESC 'Allow all services' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE X-ORIGIN 'IPA v4.2' )
 objectClasses: (2.16.840.1.113730.3.8.21.2.1 NAME 'ipaCertProfile' SUP top STRUCTURAL MUST ( cn $ description $ ipaCertProfileStoreIssued ) X-ORIGIN 'IPA v4.2' )
+objectClasses: (2.16.840.1.113730.3.8.21.2.2 NAME 'ipaCaAcl' SUP ipaAssociation STRUCTURAL MUST cn MAY ( ipaCaAclCaRef $ ipaCaAclAllCAs $ ipaCaAclAllProfiles $ ipaCaAclAllUsers $ ipaCaAclAllHosts $ ipaCaAclAllServices $ ipaCaAclMemberCertprofile $ memberService ) X-ORIGIN 'IPA v4.2' )
diff --git a/install/share/Makefile.am b/install/share/Makefile.am
index 31f391be25c58b76cc71971852074d80c5514745..e97a89ca93f7f188e06dc982bd69e251f8082df3 100644
--- a/install/share/Makefile.am
+++ b/install/share/Makefile.am
@@ -29,6 +29,7 @@ app_DATA =				\
 	bootstrap-template.ldif		\
 	caJarSigningCert.cfg.template	\
 	default-aci.ldif		\
+	default-caacl.ldif		\
 	default-hbac.ldif		\
 	default-smb-group.ldif		\
 	default-trust-view.ldif		\
diff --git a/install/share/bootstrap-template.ldif b/install/share/bootstrap-template.ldif
index c5d4bad8b80640881f4631e4873a12c82b0ea48a..2387f220fd4fe6e3ccd59f4b592f2473d7acfa44 100644
--- a/install/share/bootstrap-template.ldif
+++ b/install/share/bootstrap-template.ldif
@@ -441,3 +441,9 @@ changetype: add
 objectClass: nsContainer
 objectClass: top
 cn: certprofiles
+
+dn: cn=caacls,cn=ca,$SUFFIX
+changetype: add
+objectClass: nsContainer
+objectClass: top
+cn: caacls
diff --git a/install/share/default-caacl.ldif b/install/share/default-caacl.ldif
new file mode 100644
index 0000000000000000000000000000000000000000..4b6613cb216057d91533832e675bdb0d2007e995
--- /dev/null
+++ b/install/share/default-caacl.ldif
@@ -0,0 +1,12 @@
+# default CA ACL that grants use of caIPAserviceCert on top-level CA to all hosts and services
+dn: ipauniqueid=autogenerate,cn=caacls,cn=ca,$SUFFIX
+changetype: add
+objectclass: ipaassociation
+objectclass: ipacaacl
+ipauniqueid: autogenerate
+cn: hosts_services_caIPAserviceCert
+ipaenabledflag: TRUE
+ipacaaclcaref: .
+ipacaaclmembercertprofile: cn=caIPAserviceCert,cn=certprofiles,cn=ca,$SUFFIX
+ipacaaclallhosts: TRUE
+ipacaaclallservices: TRUE
diff --git a/install/updates/41-caacl.update b/install/updates/41-caacl.update
new file mode 100644
index 0000000000000000000000000000000000000000..a18b6ec946855c194077d9ac01a8adcfddf8542e
--- /dev/null
+++ b/install/updates/41-caacl.update
@@ -0,0 +1,4 @@
+dn: cn=caacls,cn=ca,$SUFFIX
+default: objectClass: nsContainer
+default: objectClass: top
+default: cn: caacls
diff --git a/install/updates/Makefile.am b/install/updates/Makefile.am
index fc6bd624eac619cdddeba29b85440571d85fd69f..eddf4d850ed4b47d5526dc152149fa21b14779d4 100644
--- a/install/updates/Makefile.am
+++ b/install/updates/Makefile.am
@@ -35,6 +35,7 @@ app_DATA =				\
 	40-certprofile.update		\
 	40-otp.update			\
 	40-vault.update			\
+	41-caacl.update			\
 	45-roles.update			\
 	50-7_bit_check.update	        \
 	50-dogtag10-migration.update	\
diff --git a/ipalib/constants.py b/ipalib/constants.py
index 96396a236b8694b3dd988dfe28c1b0c3cc9e3180..9812f843e1e4ced9244f3efd6a9bb6f4c2769655 100644
--- a/ipalib/constants.py
+++ b/ipalib/constants.py
@@ -119,6 +119,7 @@ DEFAULT_CONFIG = (
     ('container_views', DN(('cn', 'views'), ('cn', 'accounts'))),
     ('container_masters', DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'))),
     ('container_certprofile', DN(('cn', 'certprofiles'), ('cn', 'ca'))),
+    ('container_caacl', DN(('cn', 'caacls'), ('cn', 'ca'))),
 
     # Ports, hosts, and URIs:
     ('xmlrpc_uri', 'http://localhost:8888/ipa/xml'),
diff --git a/ipalib/plugins/caacl.py b/ipalib/plugins/caacl.py
new file mode 100644
index 0000000000000000000000000000000000000000..f9bf7a3e411c110a0a103d9681fd9ee58fc38b72
--- /dev/null
+++ b/ipalib/plugins/caacl.py
@@ -0,0 +1,371 @@
+#
+# Copyright (C) 2015  FreeIPA Contributors see COPYING for license
+#
+
+from ipalib import api, errors, output
+from ipalib import Bool, Str
+from ipalib.plugable import Registry
+from ipalib.plugins.baseldap import (
+    LDAPObject, LDAPSearch, LDAPCreate, LDAPDelete, LDAPQuery,
+    LDAPUpdate, LDAPRetrieve, LDAPAddMember, LDAPRemoveMember,
+    global_output_params, pkey_to_value)
+from ipalib import _, ngettext
+from ipapython.dn import DN
+
+
+__doc__ = _("""
+Manage CA ACL rules.
+
+This plugin is used to define rules governing which principals are
+permitted to have certificates issued using a given certificate
+profile.
+
+PROFILE ID SYNTAX:
+
+A Profile ID is a string without spaces or punctuation starting with a letter
+and followed by a sequence of letters, digits or underscore ("_").
+
+EXAMPLES:
+
+  Create a CA ACL "test" that grants all users access to the
+  "UserCert" profile:
+    ipa caacl-add test --profile-id=UserCert --allusers=1
+
+  Display the properties of a named CA ACL:
+    ipa caacl-show test
+
+  Create a CA ACL to let user "alice" use the "DNP3" profile:
+    ipa caacl-add john_dnp3 --profile-id=DNP3
+    ipa caacl-add-user --user=alice
+
+  Disable a CA ACL:
+    ipa caacl-disable test
+
+  Remove a CA ACL:
+    ipa caacl-del test
+""")
+
+register = Registry()
+
+
+ at register()
+class caacl(LDAPObject):
+    """
+    CA ACL object.
+    """
+    container_dn = api.env.container_caacl
+    object_name = _('CA ACL')
+    object_name_plural = _('CA ACLs')
+    object_class = ['ipaassociation', 'ipacaacl']
+    permission_filter_objectclasses = ['ipacaacl']
+    default_attributes = [
+        'cn', 'description', 'ipaenabledflag',
+        'ipacaaclcaref', 'ipacaaclmembercertprofile',
+        'ipacaaclallcas', 'ipacaaclallprofiles',
+        'ipacaaclallusers', 'ipacaaclallhosts', 'ipacaaclallservices',
+        'memberuser', 'memberhost', 'memberservice', 'memberhostgroup',
+    ]
+    uuid_attribute = 'ipauniqueid'
+    rdn_attribute = 'ipauniqueid'
+    attribute_members = {
+        'memberuser': ['user', 'group'],
+        'memberhost': ['host', 'hostgroup'],
+        'memberservice': ['service'],
+        'ipacaaclmembercertprofile': ['certprofile'],
+    }
+    managed_permissions = {
+        'System: Read CA ACLs': {
+            'replaces_global_anonymous_aci': True,
+            'ipapermbindruletype': 'all',
+            'ipapermright': {'read', 'search', 'compare'},
+            'ipapermdefaultattr': {
+                'cn', 'description', 'ipaenabledflag',
+                'ipacaaclcaref', 'ipacaaclmembercertprofile',
+                'ipacaaclallcas', 'ipacaaclallprofiles',
+                'ipacaaclallusers', 'ipacaaclallhosts', 'ipacaaclallservices',
+                'ipauniqueid', 'memberhost', 'memberservice', 'memberuser',
+                'objectclass', 'member',
+            },
+        },
+        'System: Add CA ACL': {
+            'ipapermright': {'add'},
+            'replaces': [
+                '(target = "ldap:///ipauniqueid=*,cn=caacls,cn=ca,$SUFFIX")(version 3.0;acl "permission:Add CA ACL";allow (add) groupdn = "ldap:///cn=Add CA ACL,cn=permissions,cn=pbac,$SUFFIX";)',
+            ],
+            'default_privileges': {'CA Administrator'},
+        },
+        'System: Delete CA ACL': {
+            'ipapermright': {'delete'},
+            'replaces': [
+                '(target = "ldap:///ipauniqueid=*,cn=caacls,cn=ca,$SUFFIX")(version 3.0;acl "permission:Delete CA ACL";allow (delete) groupdn = "ldap:///cn=Delete CA ACL,cn=permissions,cn=pbac,$SUFFIX";)',
+            ],
+            'default_privileges': {'CA Administrator'},
+        },
+        'System: Manage CA ACL Membership': {
+            'ipapermright': {'write'},
+            'ipapermdefaultattr': {
+                'memberuser', 'ipacaaclallusers',
+                'memberhost', 'ipacaaclallhosts',
+                'memberservice', 'ipacaaclallservices',
+                'ipacaaclmembercertprofile', 'ipacaaclallprofiles',
+            },
+            'replaces': [
+                '(targetattr = "memberuser || memberservice || memberhost || ipacaaclmembercertprofile || ipacaaclallprofiles || ipacaaclallusers || ipacaaclallhosts || ipacaaclallservices")(target = "ldap:///ipauniqueid=*,cn=caacls,cn=ca,$SUFFIX")(version 3.0;acl "permission:Manage CA ACL membership";allow (write) groupdn = "ldap:///cn=Manage CA ACL membership,cn=permissions,cn=pbac,$SUFFIX";)',
+            ],
+            'default_privileges': {'CA Administrator'},
+        },
+        'System: Modify CA ACL': {
+            'ipapermright': {'write'},
+            'ipapermdefaultattr': {
+                'cn', 'description', 'ipaenabledflag',
+                'ipacaaclallcas', 'ipacaaclcaref',
+            },
+            'replaces': [
+                '(targetattr = "cn || description || ipaenabledflag || ipacaaclcaref || ipacaaclallcas || ipacaaclallprofiles")(target = "ldap:///ipauniqueid=*,cn=caacls,cn=ca,$SUFFIX")(version 3.0;acl "permission:Modify CA ACL";allow (write) groupdn = "ldap:///cn=Modify CA ACL,cn=permissions,cn=pbac,$SUFFIX";)',
+            ],
+            'default_privileges': {'CA Administrator'},
+        },
+    }
+
+    label = _('CA ACLs')
+    label_singular = _('CA ACL')
+
+    takes_params = (
+        Str('cn',
+            cli_name='name',
+            label=_('ACL name'),
+            primary_key=True,
+        ),
+        Str('description?',
+            cli_name='desc',
+            label=_('Description'),
+        ),
+        Bool('ipaenabledflag?',
+             label=_('Enabled'),
+             flags=['no_option'],
+        ),
+        Str('ipacaaclcaref*',  # validate sub-CA handle syntax
+            cli_name='ca_ref',
+            label=_('CA Reference'),
+            flags=['no_option', 'no_output'],  # until sub-CAs are implemented
+        ),
+        Bool('ipacaaclallcas?',
+            cli_name='allcas',
+            label=_('Allow use of all CAs'),
+            flags=['no_option', 'no_output'],  # until sub-CAs are implemented
+        ),
+        Bool('ipacaaclallprofiles?',
+            cli_name='allprofiles',
+            label=_('Allow use of all profiles'),
+        ),
+        Bool('ipacaaclallusers?',
+            cli_name='allusers',
+            label=_('Allow all users'),
+        ),
+        Bool('ipacaaclallhosts?',
+            cli_name='allhosts',
+            label=_('Allow all hosts'),
+        ),
+        Bool('ipacaaclallservices?',
+            cli_name='allservices',
+            label=_('Allow all services'),
+        ),
+        Str('memberuser_user?',
+            label=_('Users'),
+            flags=['no_create', 'no_update', 'no_search'],
+        ),
+        Str('memberuser_group?',
+            label=_('User Groups'),
+            flags=['no_create', 'no_update', 'no_search'],
+        ),
+        Str('memberhost_host?',
+            label=_('Hosts'),
+            flags=['no_create', 'no_update', 'no_search'],
+        ),
+        Str('memberhost_hostgroup?',
+            label=_('Host Groups'),
+            flags=['no_create', 'no_update', 'no_search'],
+        ),
+        Str('memberservice_service?',
+            label=_('Services'),
+            flags=['no_create', 'no_update', 'no_search'],
+        ),
+        Str('ipacaaclmembercertprofile_certprofile?',
+            label=_('Profiles'),
+            flags=['no_create', 'no_update', 'no_search'],
+        ),
+    )
+
+
+ at register()
+class caacl_add(LDAPCreate):
+    __doc__ = _('Create a new CA ACL.')
+
+    msg_summary = _('Added CA ACL "%(value)s"')
+
+    def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
+        # CA ACLs are enabled by default
+        entry_attrs['ipaenabledflag'] = ['TRUE']
+        entry_attrs['ipacaaclcaref'] = ['.']
+        return dn
+
+
+ at register()
+class caacl_del(LDAPDelete):
+    __doc__ = _('Delete a CA ACL.')
+
+    msg_summary = _('Deleted CA ACL "%(value)s"')
+
+
+ at register()
+class caacl_mod(LDAPUpdate):
+    __doc__ = _('Modify a CA ACL.')
+
+    msg_summary = _('Modified CA ACL "%(value)s"')
+
+
+ at register()
+class caacl_find(LDAPSearch):
+    __doc__ = _('Search for CA ACLs.')
+
+    msg_summary = ngettext(
+        '%(count)d CA ACL matched', '%(count)d CA ACLs matched', 0
+    )
+
+
+ at register()
+class caacl_show(LDAPRetrieve):
+    __doc__ = _('Display the properties of a CA ACL.')
+
+
+ at register()
+class caacl_enable(LDAPQuery):
+    __doc__ = _('Enable a CA ACL.')
+
+    msg_summary = _('Enabled CA ACL "%(value)s"')
+    has_output = output.standard_value
+
+    def execute(self, cn, **options):
+        ldap = self.obj.backend
+
+        dn = self.obj.get_dn(cn)
+        try:
+            entry_attrs = ldap.get_entry(dn, ['ipaenabledflag'])
+        except errors.NotFound:
+            self.obj.handle_not_found(cn)
+
+        entry_attrs['ipaenabledflag'] = ['TRUE']
+
+        try:
+            ldap.update_entry(entry_attrs)
+        except errors.EmptyModlist:
+            pass
+
+        return dict(
+            result=True,
+            value=pkey_to_value(cn, options),
+        )
+
+
+ at register()
+class caacl_disable(LDAPQuery):
+    __doc__ = _('Disable a CA ACL.')
+
+    msg_summary = _('Disabled CA ACL "%(value)s"')
+    has_output = output.standard_value
+
+    def execute(self, cn, **options):
+        ldap = self.obj.backend
+
+        dn = self.obj.get_dn(cn)
+        try:
+            entry_attrs = ldap.get_entry(dn, ['ipaenabledflag'])
+        except errors.NotFound:
+            self.obj.handle_not_found(cn)
+
+        entry_attrs['ipaenabledflag'] = ['FALSE']
+
+        try:
+            ldap.update_entry(entry_attrs)
+        except errors.EmptyModlist:
+            pass
+
+        return dict(
+            result=True,
+            value=pkey_to_value(cn, options),
+        )
+
+
+ at register()
+class caacl_add_user(LDAPAddMember):
+    __doc__ = _('Add users and groups to a CA ACL.')
+
+    member_attributes = ['memberuser']
+    member_count_out = ('%i object added.', '%i objects added.')
+
+
+ at register()
+class caacl_remove_user(LDAPRemoveMember):
+    __doc__ = _('Remove users and groups from a CA ACL.')
+
+    member_attributes = ['memberuser']
+    member_count_out = ('%i object removed.', '%i objects removed.')
+
+
+ at register()
+class caacl_add_host(LDAPAddMember):
+    __doc__ = _('Add target hosts and hostgroups to a CA ACL.')
+
+    member_attributes = ['memberhost']
+    member_count_out = ('%i object added.', '%i objects added.')
+
+
+ at register()
+class caacl_remove_host(LDAPRemoveMember):
+    __doc__ = _('Remove target hosts and hostgroups from a CA ACL.')
+
+    member_attributes = ['memberhost']
+    member_count_out = ('%i object removed.', '%i objects removed.')
+
+
+ at register()
+class caacl_add_service(LDAPAddMember):
+    __doc__ = _('Add services to a CA ACL.')
+
+    member_attributes = ['memberservice']
+    member_count_out = ('%i object added.', '%i objects added.')
+
+
+ at register()
+class caacl_remove_service(LDAPRemoveMember):
+    __doc__ = _('Remove services from a CA ACL.')
+
+    member_attributes = ['memberservice']
+    member_count_out = ('%i object removed.', '%i objects removed.')
+
+
+caacl_output_params = global_output_params + (
+    Str('ipacaaclmembercertprofile',
+        label=_('Failed profiles'),
+    ),
+)
+
+
+ at register()
+class caacl_add_profile(LDAPAddMember):
+    __doc__ = _('Add profiles to a CA ACL.')
+
+    has_output_params = caacl_output_params
+
+    member_attributes = ['ipacaaclmembercertprofile']
+    member_count_out = ('%i object added.', '%i objects added.')
+
+
+ at register()
+class caacl_remove_profile(LDAPRemoveMember):
+    __doc__ = _('Remove profiles from a CA ACL.')
+
+    has_output_params = caacl_output_params
+
+    member_attributes = ['ipacaaclmembercertprofile']
+    member_count_out = ('%i object removed.', '%i objects removed.')
diff --git a/ipaserver/install/dsinstance.py b/ipaserver/install/dsinstance.py
index 2acab13f247ed18a750f0e1cbbd98f4e63718c03..9f24189b6e442e0c55d5de41d15a03f89ecc9578 100644
--- a/ipaserver/install/dsinstance.py
+++ b/ipaserver/install/dsinstance.py
@@ -307,6 +307,7 @@ class DsInstance(service.Service):
         self.step("adding range check plugin", self.__add_range_check_plugin)
         if hbac_allow:
             self.step("creating default HBAC rule allow_all", self.add_hbac)
+        self.step("creating default CA ACL rule", self.add_caacl)
         self.step("adding entries for topology management", self.__add_topology_entries)
 
         self.__common_post_setup()
@@ -741,6 +742,9 @@ class DsInstance(service.Service):
     def add_hbac(self):
         self._ldap_mod("default-hbac.ldif", self.sub_dict)
 
+    def add_caacl(self):
+        self._ldap_mod("default-caacl.ldif", self.sub_dict)
+
     def change_admin_password(self, password):
         root_logger.debug("Changing admin password")
         dirname = config_dirname(self.serverid)
-- 
2.1.0

-------------- next part --------------
From 2bd1b7146e5fa633a4f2ae6e851b0756930e21be Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal at redhat.com>
Date: Tue, 26 May 2015 04:44:20 -0400
Subject: [PATCH 13/13] Enforce CA ACLs in cert-request command

This commit adds CA ACL enforcement to the cert-request command and
uses the pyhbac machinery.

It is planned to implement ACL enforcement in Dogtag in a future
release, and remove certificate issuance privileges and CA ACL
enforcement responsibility from the framework.  See
https://fedorahosted.org/freeipa/ticket/5011 for more information.

Part of: https://fedorahosted.org/freeipa/ticket/57
Part of: https://fedorahosted.org/freeipa/ticket/4559
---
 ipalib/plugins/caacl.py | 73 +++++++++++++++++++++++++++++++++++++++++++++++++
 ipalib/plugins/cert.py  | 17 ++++++++++++
 2 files changed, 90 insertions(+)

diff --git a/ipalib/plugins/caacl.py b/ipalib/plugins/caacl.py
index f9bf7a3e411c110a0a103d9681fd9ee58fc38b72..6a0c38cf0b39ebcb70452333b2bc4bad9e54ccbf 100644
--- a/ipalib/plugins/caacl.py
+++ b/ipalib/plugins/caacl.py
@@ -2,6 +2,8 @@
 # Copyright (C) 2015  FreeIPA Contributors see COPYING for license
 #
 
+import pyhbac
+
 from ipalib import api, errors, output
 from ipalib import Bool, Str
 from ipalib.plugable import Registry
@@ -9,6 +11,7 @@ from ipalib.plugins.baseldap import (
     LDAPObject, LDAPSearch, LDAPCreate, LDAPDelete, LDAPQuery,
     LDAPUpdate, LDAPRetrieve, LDAPAddMember, LDAPRemoveMember,
     global_output_params, pkey_to_value)
+from ipalib.plugins.service import normalize_principal, split_any_principal
 from ipalib import _, ngettext
 from ipapython.dn import DN
 
@@ -48,6 +51,76 @@ EXAMPLES:
 register = Registry()
 
 
+def _acl_make_request(principal_type, principal, ca_ref, profile_id):
+    """Construct HBAC request for the given principal, CA and profile"""
+    req = pyhbac.HbacRequest()
+    req.targethost.name = ca_ref
+    req.service.name = profile_id
+    if principal_type == 'user':
+        req.user.name = principal
+    elif principal_type == 'host':
+        req.user.name = principal[:5]  # strip 'host/'
+    elif principal_type == 'service':
+        req.user.name = normalize_principal(principal)
+    groups = []
+    if principal_type == 'user':
+        user_obj = api.Command.user_show(principal)['result']
+        groups = user_obj.get('memberof_group', [])
+        groups += user_obj.get('memberofindirect_group', [])
+    elif principal_type == 'host':
+        service, hostname, realm = split_any_principal(principal)
+        host_obj = api.Command.host_show(hostname)['result']
+        groups = host_obj.get('memberof_hostgroup', [])
+        groups += host_obj.get('memberofindirect_hostgroup', [])
+    req.user.groups = sorted(set(groups))
+    return req
+
+
+def _acl_make_rule(principal_type, obj):
+    """Turn CA ACL object into HBAC rule.
+
+    ``principal_type``
+        String in {'user', 'host', 'service'}
+    """
+    rule = pyhbac.HbacRule(obj['cn'][0])
+    rule.enabled = obj['ipaenabledflag'][0]
+    rule.srchosts.category = {pyhbac.HBAC_CATEGORY_ALL}
+
+    # add CA(s)
+    if 'ipacaaclallcas' in obj and obj['ipacaaclallcas'][0] == 'TRUE':
+        rule.targethosts.category = {pyhbac.HBAC_CATEGORY_ALL}
+    else:
+        rule.targethosts.names = obj.get('ipacaaclcaref', [])
+
+    # add profiles
+    if 'ipacaaclallprofiles' in obj and obj['ipacaaclallprofiles'][0] == 'TRUE':
+        rule.services.category = {pyhbac.HBAC_CATEGORY_ALL}
+    else:
+        attr = 'ipacaaclmembercertprofile_certprofile'
+        rule.services.names = obj.get(attr, [])
+
+    # add principals and principal's groups
+    m = {'user': 'group', 'host': 'hostgroup', 'service': None}
+    all_principals_attr = 'ipacaaclall{}s'.format(principal_type)
+    if all_principals_attr in obj and obj[all_principals_attr][0] == 'TRUE':
+        rule.users.category = {pyhbac.HBAC_CATEGORY_ALL}
+    else:
+        principal_attr = 'member{}_{}'.format(principal_type, principal_type)
+        rule.users.names = obj.get(principal_attr, [])
+        if m[principal_type] is not None:
+            group_attr = 'member{}_{}'.format(principal_type, m[principal_type])
+            rule.users.groups = obj.get(group_attr, [])
+
+    return rule
+
+
+def acl_evaluate(principal_type, principal, ca_ref, profile_id):
+    req = _acl_make_request(principal_type, principal, ca_ref, profile_id)
+    acls = api.Command.caacl_find()['result']
+    rules = [_acl_make_rule(principal_type, obj) for obj in acls]
+    return req.evaluate(rules) == pyhbac.HBAC_EVAL_ALLOW
+
+
 @register()
 class caacl(LDAPObject):
     """
diff --git a/ipalib/plugins/cert.py b/ipalib/plugins/cert.py
index d122900175db41ba5af429fd47af6cac6533cb6f..1878e5ad5f80fa93e1a77b0a88711c1da0016681 100644
--- a/ipalib/plugins/cert.py
+++ b/ipalib/plugins/cert.py
@@ -33,6 +33,7 @@ from ipalib.plugins.virtual import *
 from ipalib.plugins.baseldap import pkey_to_value
 from ipalib.plugins.service import split_any_principal
 from ipalib.plugins.certprofile import validate_profile_id
+import ipalib.plugins.caacl
 import base64
 import traceback
 from ipalib.text import _
@@ -326,6 +327,22 @@ class cert_request(VirtualCommand):
         else:
             principal_type = SERVICE
 
+        principal_type_map = {USER: 'user', HOST: 'host', SERVICE: 'service'}
+        ca = '.'  # top-level CA hardcoded until subca plugin implemented
+        if not ipalib.plugins.caacl.acl_evaluate(
+                principal_type_map[principal_type],
+                principal_string, ca, profile_id):
+            raise errors.ACIError(info=_(
+                    "Principal '%(principal)s' "
+                    "is not permitted to use CA '%(ca)s' "
+                    "with profile '%(profile_id)s' for certificate issuance."
+                ) % dict(
+                    principal=principal_string,
+                    ca=ca or '.',
+                    profile_id=profile_id
+                )
+            )
+
         bind_principal = split_any_principal(getattr(context, 'principal'))
         bind_service, bind_name, bind_realm = bind_principal
 
-- 
2.1.0



More information about the Freeipa-devel mailing list