[Freeipa-devel] [PATCHES 0001-0005] Profile management commands

Fraser Tweedale ftweedal at redhat.com
Mon May 4 13:36:41 UTC 2015


Hello,

Please review the first cut of the 'certprofile' command and other
changes associated with the Certificate Profiles feature[1].

Custom profiles can't be used yet because 'cert-request' has not
been updated, but you can manage the profiles (find, show, import,
modify, delete).  There's a bit more work to do on profile
management and a lot more to do for using profiles and sub-CAs.  I
am tracking my progress on etherpad[2] so if you are reviewing check
there for the TODO list and some commentary.

If you want to test: for f21, please use Dogtag from my copr[2].
For f22 the required version is in updates-testing (or my copr).

In summary: this is not the whole feature, just the first functional
part.  Since it is my first experience developing in the IPA
framework I want to get patches out so you can point out all the
things I did wrong or overlooked, and I can fix them.  Don't hold
back :)

[1] http://www.freeipa.org/page/V4/Certificate_Profiles
[2] http://idm.etherpad.corp.redhat.com/rhel72-cert-mgmt-progress
[3] http://copr.fedoraproject.org/coprs/ftweedal/freeipa/
-------------- next part --------------
>From 399e9e3cb41fae66b6e448905f7e8d2a3738e30a Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <frase at frase.id.au>
Date: Mon, 20 Apr 2015 23:20:19 -0400
Subject: [PATCH 1/5] 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 8ccfd1a822fab557dc1b6bf6d0e7de3ef495efbb..7241494749480375b548c7fb8930b3f93d728e79 100644
--- a/ipaserver/install/cainstance.py
+++ b/ipaserver/install/cainstance.py
@@ -505,6 +505,7 @@ class CAInstance(DogtagInstance):
         config.set("CA", "pki_restart_configured_instance", "False")
         config.set("CA", "pki_backup_keys", "True")
         config.set("CA", "pki_backup_password", self.admin_password)
+        config.set("CA", "pki_profiles_in_ldap", "True")
 
         # Client security database
         config.set("CA", "pki_client_database_dir", self.agent_db)
-- 
2.1.0

-------------- next part --------------
>From 75feec051438d1a9c13a289db396ef34ebb8f075 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <frase at frase.id.au>
Date: Tue, 21 Apr 2015 02:24:10 -0400
Subject: [PATCH 2/5] 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 | 4 ++++
 install/share/Makefile.am                 | 1 +
 install/share/bootstrap-template.ldif     | 6 ++++++
 ipaserver/install/dsinstance.py           | 1 +
 4 files changed, 12 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..aa98ad2a17e560996477411b666888c2280394b7
--- /dev/null
+++ b/install/share/60certificate-profiles.ldif
@@ -0,0 +1,4 @@
+dn: cn=schema
+attributeTypes: (2.16.840.1.113730.3.8.19.1.2 NAME 'ipaCertProfileSummary' DESC 'Certificate Profile summary' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.44 SINGLE-VALUE X-ORIGIN 'IPA v4.2' )
+attributeTypes: (2.16.840.1.113730.3.8.19.1.3 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 $ ipaCertProfileSummary $ 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..1d74842ed544678dd080e34e1eef88983852f1d2 100644
--- a/install/share/bootstrap-template.ldif
+++ b/install/share/bootstrap-template.ldif
@@ -429,3 +429,9 @@ cn: ${REALM}_id_range
 ipaBaseID: $IDSTART
 ipaIDRangeSize: $IDRANGE_SIZE
 ipaRangeType: ipa-local
+
+dn: cn=certprofiles,cn=etc,$SUFFIX
+changetype: add
+objectClass: nsContainer
+objectClass: top
+cn: certprofiles
diff --git a/ipaserver/install/dsinstance.py b/ipaserver/install/dsinstance.py
index 8a76e773f0a464529331d9e2e459c9cc5ea0522e..b562792f72e6821925911902fd49e0829e461559 100644
--- a/ipaserver/install/dsinstance.py
+++ b/ipaserver/install/dsinstance.py
@@ -55,6 +55,7 @@ IPA_SCHEMA_FILES = ("60kerberos.ldif",
                     "60basev3.ldif",
                     "60ipapk11.ldif",
                     "60ipadns.ldif",
+                    "60certificate-profiles.ldif",
                     "61kerberos-ipav3.ldif",
                     "65ipacertstore.ldif",
                     "65ipasudo.ldif",
-- 
2.1.0

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

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

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

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

Part of: https://fedorahosted.org/freeipa/ticket/57
---
 ipaserver/install/cainstance.py | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py
index 7241494749480375b548c7fb8930b3f93d728e79..81fd8a47d3cebe0659d5b3158928f578c0a75059 100644
--- a/ipaserver/install/cainstance.py
+++ b/ipaserver/install/cainstance.py
@@ -471,6 +471,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:
@@ -942,6 +943,19 @@ class CAInstance(DogtagInstance):
 
         conn.unbind()
 
+    def __configure_profiles_acl(self):
+        """Allow the Certificate Manager Agents group to modify profiles."""
+        dn = DN(('cn', 'aclResources'), self.basedn)
+        modlist = [(ldap.MOD_ADD, 'resourceACLS', [
+            'certServer.profile.configuration:read,modify:allow (read,modify) '
+            'group="Certificate Manager Agents":'
+            'Certificate Manager agents may modify (create/update/delete) and read profiles'
+        ])]
+        conn = ipaldap.IPAdmin(self.fqdn, self.ds_port)
+        conn.do_simple_bind(DN(('cn', 'Directory Manager')), self.dm_password)
+        conn.modify_s(dn, modlist)
+        conn.unbind()
+
     def __run_certutil(self, args, database=None, pwd_file=None, stdin=None):
         if not database:
             database = self.ra_agent_db
-- 
2.1.0

-------------- next part --------------
>From 3a301bbf1584def121c981cf0d8ad1b7a23c4aca Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal at redhat.com>
Date: Thu, 30 Apr 2015 04:55:29 -0400
Subject: [PATCH 5/5] 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 ++
 install/updates/40-delegation.update |   8 ++
 ipalib/constants.py                  |   1 +
 ipalib/plugins/certprofile.py        | 240 +++++++++++++++++++++++++++++++++++
 ipapython/dogtag.py                  |  27 ++--
 ipaserver/plugins/dogtag.py          | 145 ++++++++++++++++++++-
 6 files changed, 417 insertions(+), 12 deletions(-)
 create mode 100644 ipalib/plugins/certprofile.py

diff --git a/ACI.txt b/ACI.txt
index 933b57cb93e833981867953b516a67484f13dca3..5547f4bb4e2b3e4b53813757f3d2f9170e805ae5 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=etc,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=etc,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=etc,dc=ipa,dc=example
+aci: (targetattr = "cn || ipacertprofilestoreissued || ipacertprofilesummary")(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=etc,dc=ipa,dc=example
+aci: (targetattr = "cn || createtimestamp || entryusn || ipacertprofilestoreissued || ipacertprofilesummary || 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/install/updates/40-delegation.update b/install/updates/40-delegation.update
index 32af498190a23ddfd202a5cad75409f60a70d78b..fd474bcd12c935e1485959c5763d6773d31c8bd7 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/ipalib/constants.py b/ipalib/constants.py
index f1e14702ffdf5a3bd23a62b1fdd2ee3cd95d84f8..02ba78e4b024c32b602c74e123ce305fbf247b8f 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', 'etc'))),
 
     # 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..9ef8acc07b8c4608573ccfb52f7519586dfec0ee
--- /dev/null
+++ b/ipalib/plugins/certprofile.py
@@ -0,0 +1,240 @@
+# Authors:
+#   Fraser Tweedale <ftweedal at redhat.com>
+#
+# Copyright (C) 2015  Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+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
+import ipalib.plugins.cert
+
+
+__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()
+
+
+profile_id_pattern = re.compile('^[a-zA-Z]\w*$')
+
+
+def validate_profile_id(ugettext, value):
+    """Ensure profile ID matches form required by CA."""
+    if profile_id_pattern.match(value) is None:
+        return _('invalid Profile ID')
+    else:
+        return None
+
+
+ at register()
+class certprofile(LDAPObject):
+    """
+    Certificate Profile object.
+    """
+    container_dn = api.env.container_certprofile
+    object_name = _('Certificate Profile')
+    object_name_plural = _('Certificate Profiles')
+    object_class = ['ipacertprofile']
+    default_attributes = [
+        'cn', 'ipacertprofilesummary', 'ipacertprofilestoreissued'
+    ]
+    search_attributes = [
+        'cn', 'ipacertprofilesummary', '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('ipacertprofilesummary',
+            required=True,
+            cli_name='summary',
+            label=_('Profile summary'),
+            doc=_('Brief summary 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',
+                'ipacertprofilesummary',
+                'ipacertprofilestoreissued',
+                'objectclass',
+            },
+        },
+        'System: Import Certificate Profile': {
+            'ipapermright': {'add'},
+            'replaces': [
+                '(target = "ldap:///cn=*,cn=certprofiles,cn=etc,$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=etc,$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',
+                'ipacertprofilesummary',
+                'ipacertprofilestoreissued',
+            },
+            'replaces': [
+                '(targetattr = "cn || ipacertprofilesummary || ipacertprofilestoreissued")(target = "ldap:///cn=*,cn=certprofiles,cn=etc,$SUFFIX")(version 3.0;acl "permission:Modify Certificate Profile";allow (write) groupdn = "ldap:///cn=Modify Certificate Profile,cn=permissions,cn=pbac,$SUFFIX";)',
+            ],
+            'default_privileges': {'CA Administrator'},
+        },
+    }
+
+
+
+ at register()
+class certprofile_find(LDAPSearch):
+    __doc__ = _("Search for Certificate Profiles.")
+    msg_summary = ngettext(
+        '%(count)d profile matched', '%(count)d profiles matched', 0
+    )
+
+
+ at register()
+class certprofile_show(LDAPRetrieve):
+    __doc__ = _("Display the properties of a Certificate Profile.")
+
+
+ at register()
+class certprofile_import(LDAPCreate):
+    __doc__ = _("Import a Certificate Profile.")
+    msg_summary = _('Imported profile "%(value)s"')
+    takes_options = (
+        File('profile',
+            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):
+        ipalib.plugins.cert.ca_enabled_check()
+        match = self.PROFILE_ID_PATTERN.search(options['profile'])
+        if match is None:
+            raise errors.ValidationError(name='profile',
+                error=_("Profile ID is not present in profile data"))
+        elif keys[0] != match.group(1):
+            raise errors.ValidationError(name='profile',
+                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.profile as profile_api:
+                profile_api.create_profile(options['profile'])
+                profile_api.enable_profile(keys[0])
+        except:
+            # something went wrong ; delete entry
+            ldap.delete_entry(dn)
+            raise
+
+        return dn
+
+
+ at register()
+class certprofile_del(LDAPDelete):
+    __doc__ = _("Delete a Certificate Profile.")
+    msg_summary = _('Deleted profile "%(value)s"')
+
+    def post_callback(self, ldap, dn, *keys, **options):
+        with self.api.Backend.profile as profile_api:
+            profile_api.disable_profile(keys[0])
+            profile_api.delete_profile(keys[0])
+        return dn
+
+
+ at register()
+class certprofile_mod(LDAPUpdate):
+    __doc__ = _("Modify Certificate Profile configuration.")
+    msg_summary = _('Modified Certificate Profile "%(value)s')
diff --git a/ipapython/dogtag.py b/ipapython/dogtag.py
index c74b8736a4b15f7bf081206b52b9876a8c4928af..563396710d76cdd6953066b4b023a89da6013cd9 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,15 @@ 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..2ba4af490683a305d669d9afca4899d8f3d5ed6c 100644
--- a/ipaserver/plugins/dogtag.py
+++ b/ipaserver/plugins/dogtag.py
@@ -4,8 +4,9 @@
 #   Jason Gerard DeRose <jderose at redhat.com>
 #   Rob Crittenden <rcritten@@redhat.com>
 #   John Dennis <jdennis at redhat.com>
+#   Fraser Tweedale <ftweedal at redhat.com>
 #
-# Copyright (C) 2014  Red Hat
+# Copyright (C) 2014, 2015  Red Hat
 # see file 'COPYING' for use and warranty information
 #
 # This program is free software; you can redistribute it and/or modify
@@ -238,17 +239,20 @@ digits and nothing else follows.
 '''
 
 import datetime
+import json
 from lxml import etree
 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
@@ -1268,7 +1272,7 @@ if api.env.ra_plugin != 'dogtag':
     raise SkipPluginModule(reason='dogtag not selected as RA plugin')
 import os, random
 from ipaserver.plugins import rabase
-from ipalib.errors import CertificateOperationError
+import  ipalib.errors as errors
 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,138 @@ class kra(Backend):
         return KRAClient(connection, crypto)
 
 api.register(kra)
+
+
+class profile(Backend):
+    """
+    Profile management backend plugin.
+    """
+    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(profile, 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"""
+        # TODO logout
+        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
+
+        # perform main request
+        status, status_text, resp_headers, resp_body = dogtag.https_request(
+            self.ca_host, self.env.ca_agent_port, path,
+            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 ''
+            if not explanation: print resp_body  # NOCOMMIT
+            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)
+
+    @staticmethod
+    def _parse_dogtag_error(body):
+        try:
+            return pki.PKIException.from_json(json.loads(body))
+        except:
+            return None
+
+    def create_profile(self, profile_data):
+        """
+        Import the profile into Dogtag
+        """
+        self._ssldo('POST', '/ca/rest/profiles/raw',
+            headers={'Content-type': 'application/xml'},
+            body=profile_data
+        )
+
+    def enable_profile(self, profile_id):
+        """
+        Enable the profile in Dogtag
+        """
+        self._ssldo('POST',
+            '/ca/rest/profiles/{}?action=enable'.format(profile_id))
+
+    def disable_profile(self, profile_id):
+        """
+        Enable the profile in Dogtag
+        """
+        self._ssldo('POST',
+            '/ca/rest/profiles/{}?action=disable'.format(profile_id))
+
+    def delete_profile(self, profile_id):
+        self._ssldo('DELETE', '/ca/rest/profiles/{}'.format(profile_id),
+            headers={'Accept': 'application/json'})
+
+
+api.register(profile)
-- 
2.1.0



More information about the Freeipa-devel mailing list