Hi Honza, Martin et al,

Latest patches attached.  On top of previous patches (most review
matters addressed**) patches 0008..0011 add support for profiles and
user certificates to `ipa cert-request'.

** those that were not are being tracked at [1]; please add anything
   I missed.

Some points to note:

- usercertificate is not yet a multi-valued attribute for users,
  hosts and services.

  QUESTION - we do want to allow multiple certificates for all
  principal types, not just users?  Or have I got that wrong.

- "DN and SAN match principal" checks are not implemented for users
  yet.

- ACL was added to allow user principals to request their own
  certificates, however, this will be further subject to CA/profile
  ACLs which are to come.

- Pursuant to [2] revocation logic was removed from `cert-request'

[1] http://idm.etherpad.corp.redhat.com/rhel72-cert-mgmt-progress
[2] 
http://www.freeipa.org/page/V4/User_Certificates#Revocation_of_the_Certificates

Thanks,
Fraser
>From 07599bb290fdd23990c978489bc3b08d493caed6 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftwee...@redhat.com>
Date: Mon, 20 Apr 2015 23:20:19 -0400
Subject: [PATCH 01/11] 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

>From c1fafbce7b077c6981381b727f1dc9571ccf2f19 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftwee...@redhat.com>
Date: Tue, 21 Apr 2015 02:24:10 -0400
Subject: [PATCH 02/11] 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..dcf4680589c98dad165141b1e13946c161a6abd7
--- /dev/null
+++ b/install/share/60certificate-profiles.ldif
@@ -0,0 +1,3 @@
+dn: cn=schema
+attributeTypes: (2.16.840.1.113730.3.8.19.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.19.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 
ca6128e2911ab5c0a773dd553f8e67eab944f120..2cae5279079cdd3e0d793667f4d1bf4e44757b9e
 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 
f1d24e49d1b184efde1c8d18ff37d0e329037ccc..210992fc41127de29d41b889e33a312613ae28da
 100644
--- a/ipaserver/install/dsinstance.py
+++ b/ipaserver/install/dsinstance.py
@@ -56,6 +56,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

>From fc1dfcd7f8a9241c81ac97164a6e5699da74aa1b Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftwee...@redhat.com>
Date: Wed, 29 Apr 2015 06:07:58 -0400
Subject: [PATCH 03/11] 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

>From ae5f5dae49ad891864bbdcbc5c2d9f592521fe74 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftwee...@redhat.com>
Date: Thu, 30 Apr 2015 23:50:41 -0400
Subject: [PATCH 04/11] Add ACL to allow CA agent to modify profiles

Part of: https://fedorahosted.org/freeipa/ticket/57
---
 install/tools/ipa-upgradeconfig | 19 +++++++++++++++++++
 ipaserver/install/cainstance.py | 26 ++++++++++++++++++++++++++
 2 files changed, 45 insertions(+)

diff --git a/install/tools/ipa-upgradeconfig b/install/tools/ipa-upgradeconfig
index 
dfef1e0aa8b1507b7aa4907e9b688ce99253b87c..92c4934874f12c7017329bdcb67ac9a888f6e389
 100755
--- a/install/tools/ipa-upgradeconfig
+++ b/install/tools/ipa-upgradeconfig
@@ -31,6 +31,7 @@ import pwd
 import fileinput
 import ConfigParser
 import grp
+import ldap
 
 from ipalib import api
 import SSSDConfig
@@ -40,6 +41,7 @@ from ipaplatform import services
 from ipaplatform.tasks import tasks
 from ipapython import ipautil, sysrestore, version, certdb
 from ipapython.config import IPAOptionParser
+from ipapython.dn import DN
 from ipapython.ipa_log_manager import *
 from ipapython import certmonger
 from ipapython import dogtag
@@ -322,6 +324,22 @@ 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
+
+    if sysupgrade.get_upgrade_state('dogtag', 'agent_allow_profile_modify'):
+        return False
+
+    cainstance.configure_profiles_acl()
+
+    sysupgrade.set_upgrade_state('dogtag', 'agent_allow_profile_modifiy', True)
+    return True
+
+
 def upgrade_ipa_profile(ca, domain, fqdn):
     """
     Update the IPA Profile provided by dogtag
@@ -1420,6 +1438,7 @@ def main():
         upgrade_ipa_profile(ca, api.env.domain, fqdn),
         certificate_renewal_update(ca),
         ca_enable_pkix(ca),
+        ca_configure_profiles_acl(ca),
     ])
 
     if ca_restart:
diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py
index 
030c9f12daba4b38b748da8940e38d3cf2109788..d85210e02a6b4a5e664d57965299d775836ae5c5
 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,27 @@ 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)
+    conn.disconnect()
+
 if __name__ == "__main__":
     standard_logging_setup("install.log")
     ds = dsinstance.DsInstance()
-- 
2.1.0

>From 7302ce777669ab4300c3919abe91565dd3cd1641 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftwee...@redhat.com>
Date: Thu, 30 Apr 2015 04:55:29 -0400
Subject: [PATCH 05/11] 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 
bf539892910f14ebc3fbee88a72d2b57c0d1327b..870a343f0e59fa2075b53e881c224d4965984d08
 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 
0808f3c64595495c8a9e60da5cbd689d5cdc6224..55d2baccaaf7a6cd891aa872935c01c76781c273
 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 
0d63d9ea8d85f1add5f036e7a39f89543586d33b..cd44f08d4cd736ee19caf8ec1b8df604c9c2fa9d
 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                   \
        45-roles.update                 \
        50-7_bit_check.update           \
diff --git a/ipalib/constants.py b/ipalib/constants.py
index 
f1e14702ffdf5a3bd23a62b1fdd2ee3cd95d84f8..3722ab1999938ad021b8789645368633eac7f08b
 100644
--- a/ipalib/constants.py
+++ b/ipalib/constants.py
@@ -116,6 +116,7 @@ DEFAULT_CONFIG = (
     ('container_otp', DN(('cn', 'otp'))),
     ('container_radiusproxy', DN(('cn', 'radiusproxy'))),
     ('container_views', DN(('cn', 'views'), ('cn', 'accounts'))),
+    ('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..3d9e807445f55b5406640a268ab0d50ddedc896d
--- /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
+
+
+@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'},
+        },
+    }
+
+
+
+@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(self, *args, **kwargs)
+
+
+@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(self, *args, **kwargs)
+
+
+@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=(\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
+
+
+@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(self, *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
+
+
+@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(self, *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 <jder...@redhat.com>
 #   Rob Crittenden <rcritten@@redhat.com>
 #   John Dennis <jden...@redhat.com>
+#   Fraser Tweedale <ftwee...@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

>From 3c9a29566e49b01f7017ef861daf1ca1967f59a7 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftwee...@redhat.com>
Date: Mon, 11 May 2015 23:38:41 -0400
Subject: [PATCH 06/11] Enable LDAP-based profiles in CA on upgrade

Part of: https://fedorahosted.org/freeipa/ticket/4560
---
 install/tools/ipa-upgradeconfig | 40 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 40 insertions(+)

diff --git a/install/tools/ipa-upgradeconfig b/install/tools/ipa-upgradeconfig
index 
92c4934874f12c7017329bdcb67ac9a888f6e389..246118fd451fbf57754049258aeaa1895eb11e5e
 100755
--- a/install/tools/ipa-upgradeconfig
+++ b/install/tools/ipa-upgradeconfig
@@ -340,6 +340,45 @@ def ca_configure_profiles_acl(ca):
     return True
 
 
+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
@@ -1439,6 +1478,7 @@ def main():
         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

>From 27ac261f1c67e150855ea1e94d5893138772364b Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftwee...@redhat.com>
Date: Mon, 11 May 2015 21:17:48 -0400
Subject: [PATCH 07/11] 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/DefaultService.cfg | 109 +++++++++++++++
 install/share/profiles/Makefile.am        |  14 ++
 install/tools/ipa-server-install          |   9 ++
 install/tools/ipa-upgradeconfig           |  49 ++++---
 ipapython/dogtag.py                       |   5 +
 ipaserver/install/cainstance.py           | 216 ++++--------------------------
 ipaserver/plugins/dogtag.py               |  14 +-
 10 files changed, 201 insertions(+), 219 deletions(-)
 create mode 100644 install/share/profiles/DefaultService.cfg
 create mode 100644 install/share/profiles/Makefile.am

diff --git a/freeipa.spec.in b/freeipa.spec.in
index 
b14acee638d10d1e153e1f6765ab5902060cb169..6f5479240ba16a8a32ca2924437b4a2e7f37adc4
 100644
--- a/freeipa.spec.in
+++ b/freeipa.spec.in
@@ -718,6 +718,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 
2cae5279079cdd3e0d793667f4d1bf4e44757b9e..1ff2278ebf395057fccc4f0650b7726374cc1cc8
 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/DefaultService.cfg 
b/install/share/profiles/DefaultService.cfg
new file mode 100644
index 
0000000000000000000000000000000000000000..d70fc9867d8af3f49dd6dfebcefdf772772bd100
--- /dev/null
+++ b/install/share/profiles/DefaultService.cfg
@@ -0,0 +1,109 @@
+profileId=DefaultService
+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/share/profiles/Makefile.am 
b/install/share/profiles/Makefile.am
new file mode 100644
index 
0000000000000000000000000000000000000000..8c6b3114a346b3d96d94e75ad330c5f0bbaf7322
--- /dev/null
+++ b/install/share/profiles/Makefile.am
@@ -0,0 +1,14 @@
+NULL =
+
+appdir = $(IPA_DATA_DIR)/profiles
+app_DATA =                             \
+       DefaultService.cfg              \
+       $(NULL)
+
+EXTRA_DIST =                           \
+       $(app_DATA)                     \
+       $(NULL)
+
+MAINTAINERCLEANFILES =                 \
+       *~                              \
+       Makefile.in
diff --git a/install/tools/ipa-server-install b/install/tools/ipa-server-install
index 
cb6e1abe2016c0f8cefc35b1d685373f05b3ef89..7a7743e17c0f3ad9215dfc9d985d0ff021e57277
 100755
--- a/install/tools/ipa-server-install
+++ b/install/tools/ipa-server-install
@@ -1124,6 +1124,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
@@ -1273,6 +1276,12 @@ def main():
         service.print_msg("Restarting the certificate server")
         ca.restart(dogtag.configured_constants().PKI_INSTANCE_NAME)
 
+        service.print_msg("Importing certificate profiles")
+        api.Backend.ra_certprofile._read_password()
+        if not api.Backend.ldap2.isconnected():
+            api.Backend.ldap2.connect(autobind=True)
+        ca.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 
246118fd451fbf57754049258aeaa1895eb11e5e..24cdf466ceff3885b779b7de88681c087792c99e
 100755
--- a/install/tools/ipa-upgradeconfig
+++ b/install/tools/ipa-upgradeconfig
@@ -379,32 +379,34 @@ 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
+
+    if not api.Backend.ldap2.isconnected():
+        try:
+            api.Backend.ldap2.connect(autobind=True)
+        except ipalib.errors.PublicError, e:
+            root_logger.error("Cannot connect to LDAP: %s", e)
+            return
+    ca.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():
@@ -1327,6 +1329,7 @@ def main():
     fstore = sysrestore.FileStore(paths.SYSRESTORE)
 
     api.bootstrap(context='restart', in_server=True)
+    import ipaserver.plugins.dogtag  # ensure profile backend gets loaded
     api.finalize()
 
     fqdn = find_hostname()
@@ -1474,7 +1477,7 @@ def main():
 
     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),
@@ -1488,6 +1491,12 @@ def main():
         except ipautil.CalledProcessError, 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')
 
 if __name__ == '__main__':
diff --git a/ipapython/dogtag.py b/ipapython/dogtag.py
index 
11311cf7b55d7b84e9434a698dbfd60b0eb142a1..e0091ba8747dff3a488b9908f057ae15c6b4bedc
 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'DefaultService', u'Standard profile for network services', True),
+    }
+
 class Dogtag10Constants(object):
     DOGTAG_VERSION = 10
     UNSECURE_PORT = 8080
diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py
index 
d85210e02a6b4a5e664d57965299d775836ae5c5..7fc1572604a09cc1c6bc28f7a068b5b2d49a6a12
 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,6 +1393,29 @@ class CAInstance(DogtagInstance):
             master_entry['ipaConfigString'].append('caRenewalMaster')
             self.admin_conn.update_entry(master_entry)
 
+    def import_included_profiles(self):
+        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))),
+        )
+
+        for (profile_id, desc, store_issued) in dogtag.INCLUDED_PROFILES:
+            try:
+                show_ret = api.Command['certprofile_show'](profile_id)
+                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)
+                api.Command['certprofile_import'](
+                    profile_id,
+                    file=profile_data.decode('utf-8'),
+                    description=desc,
+                    ipacertprofilestoreissued=store_issued,
+                )
+                root_logger.info("Imported profile '%s'", profile_id)
 
     @staticmethod
     def update_cert_config(nickname, cert, dogtag_constants=None):
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

>From 28e8d503b7d5f13a40ca177f15a69696042a996f Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftwee...@redhat.com>
Date: Thu, 7 May 2015 21:26:24 -0400
Subject: [PATCH 08/11] 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

>From 8c3267f2d1026835ace648c0a66566ed1a5dda06 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftwee...@redhat.com>
Date: Fri, 8 May 2015 02:23:24 -0400
Subject: [PATCH 09/11] 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         | 6 ++++--
 ipaserver/install/certs.py  | 2 +-
 ipaserver/plugins/dogtag.py | 7 +++++--
 ipaserver/plugins/rabase.py | 3 ++-
 7 files changed, 16 insertions(+), 9 deletions(-)

diff --git a/API.txt b/API.txt
index 
55d2baccaaf7a6cd891aa872935c01c76781c273..93ef2cdae533ad5564685bb21c8857a807dc8e08
 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 
e0091ba8747dff3a488b9908f057ae15c6b4bedc..6476d7e5813967cb1b812dc8a01eb3a0fae96d4e
 100644
--- a/ipapython/dogtag.py
+++ b/ipapython/dogtag.py
@@ -47,6 +47,8 @@ INCLUDED_PROFILES = {
     (u'DefaultService', u'Standard profile for network services', True),
     }
 
+DEFAULT_PROFILE = 'caIPAserviceCert'
+
 class Dogtag10Constants(object):
     DOGTAG_VERSION = 10
     UNSECURE_PORT = 8080
@@ -76,7 +78,7 @@ class Dogtag10Constants(object):
 
     RACERT_LINE_SEP = '\n'
 
-    IPA_SERVICE_PROFILE = '%s/caIPAserviceCert.cfg' % SERVICE_PROFILE_DIR
+    IPA_SERVICE_PROFILE = '%s/%s.cfg' % (SERVICE_PROFILE_DIR, DEFAULT_PROFILE)
     SIGN_PROFILE = '%s/caJarSigningCert.cfg' % SERVICE_PROFILE_DIR
     SHARED_DB = True
     DS_USER = "dirsrv"
@@ -115,7 +117,7 @@ class Dogtag9Constants(object):
     EE_CLIENT_AUTH_PORT = 9446
     TOMCAT_SERVER_PORT = 9701
 
-    IPA_SERVICE_PROFILE = '%s/caIPAserviceCert.cfg' % SERVICE_PROFILE_DIR
+    IPA_SERVICE_PROFILE = '%s/%s.cfg' % (SERVICE_PROFILE_DIR, DEFAULT_PROFILE)
     SIGN_PROFILE = '%s/caJarSigningCert.cfg' % SERVICE_PROFILE_DIR
     SHARED_DB = False
     DS_USER = "pkisrv"
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

>From 9bf0edfbb1e5458e92c76309c58fade50b9fc0d0 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftwee...@redhat.com>
Date: Mon, 18 May 2015 22:11:52 -0400
Subject: [PATCH 10/11] Add usercertificate attribute to user plugin

---
 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         |  4 ++--
 6 files changed, 28 insertions(+), 11 deletions(-)

diff --git a/ACI.txt b/ACI.txt
index 
870a343f0e59fa2075b53e881c224d4965984d08..279d15d6dcf2a465a07ec27a67b15bf452da6c81
 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 
93ef2cdae533ad5564685bb21c8857a807dc8e08..a1ff2afbd1d29087c4295bf9b58b596f73279c27
 100644
--- a/API.txt
+++ b/API.txt
@@ -3772,7 +3772,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')
@@ -3814,6 +3814,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=False, 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')
@@ -3829,7 +3830,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)
@@ -3880,6 +3881,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=False, 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')
@@ -3888,7 +3890,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')
@@ -3931,6 +3933,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=False, 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')
@@ -4558,7 +4561,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')
@@ -4601,6 +4604,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=False, 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')
@@ -4632,7 +4636,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)
@@ -4685,6 +4689,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=False, 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')
@@ -4694,7 +4699,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')
@@ -4738,6 +4743,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=False, 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..8964876b0f07c664e4855baa3971a2c774073f09
 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 = "ipasshpubkey")(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..5004cbc12f0eaecfffa0d14e8367a5c5ca4a70fd
 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..e0004a5b01bf48887ca1bc429d8b37342e1c9cc0
 100644
--- a/ipalib/plugins/user.py
+++ b/ipalib/plugins/user.py
@@ -267,10 +267,10 @@ 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";)',
+                '(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 || 
usercertificate || 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";)',
             ],
             'default_privileges': {
                 'User Administrators',
-- 
2.1.0

>From a877bad933cfb7454e670a684ce976194946f4c6 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftwee...@redhat.com>
Date: Thu, 14 May 2015 01:45:16 -0400
Subject: [PATCH 11/11] 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 | 174 ++++++++++++++++++++++++++-----------------------
 2 files changed, 94 insertions(+), 81 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..8fe5b9daf88870647067db37e6e3929cbc11626e
 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,20 @@ 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 = split_any_principal(kw.get('principal'))
+        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_principal
+
+        if bind_principal != principal:
+            # Can the bound principal request certs for another principal?
             self.check_access()
 
         try:
@@ -323,57 +340,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, 
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, 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 +396,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 +417,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 +453,18 @@ 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'))}
+        # Success? Then add it to the principal's entry
+        # (unless the profile tells us not to)
+        profile = api.Command['certprofile_show'](profile_id)
+        profile_store_issued = profile['result']['ipacertprofilestoreissued']
+        if profile_store_issued and 'certificate' in result:
+            skw = {"usercertificate": str(result.get('certificate'))}
+            if principal_type == SERVICE:
                 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)
+            elif principal_type == HOST:
+                api.Command['host_mod'](principal_name, **skw)
+            elif principal_type == USER:
+                api.Command['user_mod'](principal_name, **skw)
 
         return dict(
             result=result
-- 
2.1.0

-- 
Manage your subscription for the Freeipa-devel mailing list:
https://www.redhat.com/mailman/listinfo/freeipa-devel
Contribute to FreeIPA: http://www.freeipa.org/page/Contribute/Code

Reply via email to