[Freeipa-devel] [PATCHES 0001-0013 v4] Profile management

Fraser Tweedale ftweedal at redhat.com
Tue May 26 15:59:30 UTC 2015


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
-------------- next part --------------
>From aa197b04c0337ab6244702cbf212fe9f233aecb7 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
---
 ipaserver/install/cainstance.py | 1 +
 1 file changed, 1 insertion(+)

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 05a74613d05dd63713539dec91c430f876687f9f 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 2fac21c1722fed25031ea515af1fe088857b4034 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 32d4d23426d8285bd282033814627e9a0b088b59 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.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.py b/ipaserver/install/server.py
index c08b74828bf811c560963cc4f9e267d3afbeedb3..7bfcb48ea48c0eb108055321d9a00b14ffbfe790 100644
--- a/ipaserver/install/server.py
+++ b/ipaserver/install/server.py
@@ -284,6 +284,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
@@ -1364,6 +1374,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 a9ff8cf670ce7c339c6a0edcea5047428fcb6595 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 3c4ebde5b3ac2eb0b8e9465c5f2bd74f5bdbfb01..035a545511e4f56dcb492c97514de59cfc028405 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 da69f32de5c12c0d85a7d61d9027385aa3c0ee05..7d221b839a7058e2bc7c6c95f7121e79a30ebcfd 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 fc4d8d4b7b7c2ab9a4c58ea243daf1ddaf337893 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.py | 40 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 40 insertions(+)

diff --git a/ipaserver/install/server.py b/ipaserver/install/server.py
index 7bfcb48ea48c0eb108055321d9a00b14ffbfe790..52d68877c64245b683bf5b310eff637e412cd146 100644
--- a/ipaserver/install/server.py
+++ b/ipaserver/install/server.py
@@ -294,6 +294,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 == '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,
+            '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
@@ -1375,6 +1414,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 adfb0d3e3dc790f693f2b72fb2f27a29801fa0ee 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-server-install            |   6 +
 install/tools/ipa-upgradeconfig             |   1 -
 ipapython/dogtag.py                         |   7 +-
 ipaserver/install/cainstance.py             | 253 +++++++---------------------
 ipaserver/install/ipa_server_upgrade.py     |   1 +
 ipaserver/install/server.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 7dc576256865fb04b3f322b2094a5e3ece7776a5..23459a5ee78c4f65242d1ebeaca72a6f8de7d535 100644
--- a/freeipa.spec.in
+++ b/freeipa.spec.in
@@ -719,6 +719,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-server-install b/install/tools/ipa-server-install
index c7d7c7bff7d5a5e818eaa4a8cb93be94eae7d0c1..b0fc86521ef52c110d2f3860d9c24d0065f13756 100755
--- a/install/tools/ipa-server-install
+++ b/install/tools/ipa-server-install
@@ -1139,6 +1139,9 @@ def main():
         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
@@ -1291,6 +1294,9 @@ def main():
         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_installer.install(False, False, options)
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 31772dc7147e53d2bf07eea191295a7fe30155e4..d3524a4159a17141d989b2f320dbfbeaa3ab305d 100644
--- a/ipaserver/install/ipa_server_upgrade.py
+++ b/ipaserver/install/ipa_server_upgrade.py
@@ -53,6 +53,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()
 
         options = self.options
diff --git a/ipaserver/install/server.py b/ipaserver/install/server.py
index 52d68877c64245b683bf5b310eff637e412cd146..db4d9fbb7271e53efde7fba7f746d3037e717f28 100644
--- a/ipaserver/install/server.py
+++ b/ipaserver/install/server.py
@@ -333,32 +333,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():
@@ -1410,7 +1406,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),
@@ -1424,4 +1420,10 @@ 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 d7220c976518ba93b44ca0d811e5418fbd17ba19 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 b37dc7b4bf56b69df204fd29e9487f1390197bbe..5af320286bab98535e1f7118840afc4d525be401 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 54e99b0db2ce92a429a532f11ab6e7a2aad847bc 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
---
 API.txt                     | 3 ++-
 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 ++-
 7 files changed, 14 insertions(+), 7 deletions(-)

diff --git a/API.txt b/API.txt
index 7d221b839a7058e2bc7c6c95f7121e79a30ebcfd..ec414a97c5913c0f4cc812716c72670dae7432da 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/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..659751e57bf3deead0966e748c7302600db0f56c 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 = '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 10a75249f0df9cbe5a6e776d17c91fa195bee60f 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 035a545511e4f56dcb492c97514de59cfc028405..fca5b269522c4ab80858c23b1a61e229eee0048e 100644
--- a/ACI.txt
+++ b/ACI.txt
@@ -281,7 +281,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 ec414a97c5913c0f4cc812716c72670dae7432da..a59e99ba31bf901b960c36b05541c3e400d4ec7c 100644
--- a/API.txt
+++ b/API.txt
@@ -3808,7 +3808,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')
@@ -3850,6 +3850,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')
@@ -3865,7 +3866,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)
@@ -3916,6 +3917,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')
@@ -3924,7 +3926,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')
@@ -3967,6 +3969,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')
@@ -4594,7 +4597,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')
@@ -4637,6 +4640,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')
@@ -4668,7 +4672,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)
@@ -4721,6 +4725,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')
@@ -4730,7 +4735,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')
@@ -4774,6 +4779,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 dfd3883a184638a3cfa9275c750a0f9bb9d7624c 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
---
 ipalib/pkcs10.py       |   1 +
 ipalib/plugins/cert.py | 178 ++++++++++++++++++++++++++-----------------------
 2 files changed, 97 insertions(+), 82 deletions(-)

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..aab2db770c3aae6fbc7d13ae7707f06ba583ba09 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,21 @@ 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_principal != principal:
+            # Can the bound principal request certs for another principal?
             self.check_access()
 
         try:
@@ -323,57 +341,54 @@ 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_service != '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:
+
+        # 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:
+            pass  # TODO require cn / emailAddress to match user
 
         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
+        principal_obj = 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']
+            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 or not add:
+                raise errors.NotFound(
+                    reason=_("The principal for this request doesn't exist."))
+            principal_obj = api.Command['service_add'](principal_string, force=True)
+        principal_obj = principal_obj['result']
+        dn = principal_obj['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 +397,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 +418,35 @@ 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:
                     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:
+                    pass  # TODO require match to user email address
+                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 +454,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'))
+            if principal_type == SERVICE:
+                api.Command['service_mod'](principal_string, usercertificate=cert)
+            elif principal_type == HOST:
+                api.Command['host_mod'](principal_name, usercertificate=cert)
+            elif principal_type == USER:
+                api.Command['user_mod'](principal_name,
+                    addattr=u'usercertificate={}'.format(cert))
 
         return dict(
             result=result
-- 
2.1.0

-------------- next part --------------
>From 5469b65b83d18daa4c22dd9a40b6848292ac3145 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.

Part of: https://fedorahosted.org/freeipa/ticket/57
Part of: https://fedorahosted.org/freeipa/ticket/4559
---
 ACI.txt                                   |  10 +
 API.txt                                   | 171 +++++++++++++++
 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                   | 343 ++++++++++++++++++++++++++++++
 ipaserver/install/dsinstance.py           |   4 +
 11 files changed, 561 insertions(+)
 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 fca5b269522c4ab80858c23b1a61e229eee0048e..c92e341834c1134cab007e61d2953b8e52f375b5 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 || ipacaaclallservices || ipacaaclallusers || 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 || ipacaaclallprofiles || ipacaaclcaref || ipacaaclprofileid || 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 || ipacaaclprofileid || 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 a59e99ba31bf901b960c36b05541c3e400d4ec7c..1b3727059c362d32ac6cc56d7277ebb8763f6943 100644
--- a/API.txt
+++ b/API.txt
@@ -456,6 +456,177 @@ 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,15,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: Str('ipacaaclprofileid', attribute=True, cli_name='profile_id', 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_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,17,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: Str('ipacaaclprofileid', attribute=True, autofill=False, cli_name='profile_id', 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,17,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: Str('ipacaaclprofileid', attribute=True, autofill=False, cli_name='profile_id', 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_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/install/share/60certificate-profiles.ldif b/install/share/60certificate-profiles.ldif
index f1281949e53386e5bfe8b35e0c15858c693c5467..d8f008c5baed4e0021944bcbaa7256aa250e5c23 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 'ipaCaAclProfileId' DESC 'Certificate Profile ID' 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.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 $ ipaCaAclProfileId $ ipaCaAclAllCAs $ ipaCaAclAllProfiles $ ipaCaAclAllUsers $ ipaCaAclAllHosts $ ipaCaAclAllServices $ 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..b902c2f84a6898ae51e8123956496d64b6356d3e
--- /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: .
+ipacaaclprofileid: caIPAserviceCert
+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..84dc6d3c4db5f46d76bed560023b7cb63c5513b1
--- /dev/null
+++ b/ipalib/plugins/caacl.py
@@ -0,0 +1,343 @@
+#
+# 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,
+    pkey_to_value)
+from ipalib.plugins.certprofile import validate_profile_id
+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',
+        'ipacaaclprofileid', 'ipacaaclcaref',
+        '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'],
+    }
+    managed_permissions = {
+        'System: Read CA ACLs': {
+            'replaces_global_anonymous_aci': True,
+            'ipapermbindruletype': 'all',
+            'ipapermright': {'read', 'search', 'compare'},
+            'ipapermdefaultattr': {
+                'cn', 'description', 'ipaenabledflag',
+                'ipacaaclprofileid', 'ipacaaclcaref',
+                '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': {
+                'memberhost', 'memberservice', 'memberuser',
+                'ipacaaclallusers', 'ipacaaclallhosts', 'ipacaaclallservices',
+            },
+            'replaces': [
+                '(targetattr = "memberuser || memberservice || memberhost || 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', 'ipacaaclallprofiles',
+                'ipacaaclprofileid', 'ipacaaclcaref',
+            },
+            'replaces': [
+                '(targetattr = "cn || description || ipaenabledflag || ipacaaclprofileid || 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('ipacaaclprofileid*', validate_profile_id,
+            cli_name='profile_id',
+            label=_('Profile ID'),
+        ),
+        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'],
+        ),
+    )
+
+
+ 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 service and service groups from a CA ACL.')
+
+    member_attributes = ['memberservice']
+    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 0c8b27921ee6c134b09a2a31a5f49ad9bec4bd2d 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 | 72 +++++++++++++++++++++++++++++++++++++++++++++++++
 ipalib/plugins/cert.py  | 17 ++++++++++++
 2 files changed, 89 insertions(+)

diff --git a/ipalib/plugins/caacl.py b/ipalib/plugins/caacl.py
index 84dc6d3c4db5f46d76bed560023b7cb63c5513b1..c09df863847f0e2b0e029b5c7f82d978a7e159bf 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
@@ -10,6 +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 import _, ngettext
 from ipapython.dn import DN
 
@@ -49,6 +52,75 @@ 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':
+        hostname = principal[5:]
+        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:
+        rule.services.names = obj.get('ipacaaclprofileid', [])
+
+    # 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 aab2db770c3aae6fbc7d13ae7707f06ba583ba09..062c1b8d8e7f8f04ab531ffb449b3d60aa971fed 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