[Freeipa-devel] [PATCH] 0059..0064 Lightweight sub-CAs

Fraser Tweedale ftweedal at redhat.com
Wed Jun 1 04:51:04 UTC 2016


Hi team,

This patchset implements the 'ca' plugin for creating and managing
lightweight sub-CAs, and updates the 'caacl' plugin and
'cert-request' command to support multiple CAs.

A brief overview of the patches:

0059
  'ca' plugin, associated schema changes and container objects,
  Dogtag REST API wrapper
0060
  Add CA entry for the IPA CA on install/upgrade
0061
  Update 'caacl' plugin with CA support (including enforcement)
0062
  Update ra.request_certificate() to support specifying target CA
0063
  Add '--ca' option to 'cert-request' command
0064
  Add '--issuer' option to 'cert-find' command

These patches depend on other pending patches:

    0051, 0052, 0053, 0054, 0055, 0056

Signing key replication depends on unmerged Dogtag patches.  Builds
of Dogtag with the required patches, and of FreeIPA with all
completed sub-CAs work, should be available from my COPR soon:
https://copr.fedorainfracloud.org/coprs/ftweedal/freeipa/

Some parts of the design are not implemented in the current
patchset, including:

- local parent CA (ipaca object) references
- sub-CA certificate renewal
- 'cert-show' command '--ca=NAME' option
- certmonger support for specifying CA
- revocation of deleted CAs

I look forward to your reviews!

Thanks,
Fraser
-------------- next part --------------
From bd30332dee424aebdedec9de1615320c3384bbac Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal at redhat.com>
Date: Thu, 14 May 2015 01:46:06 -0400
Subject: [PATCH 59/64] Add 'ca' plugin

This commit adds the 'ca' plugin for creating and managing
lightweight CAs.  The initial implementation supports a single level
of sub-CAs underneath the IPA CA.

This commit also:

- adds the container for FreeIPA CA objects

- adds schema for the FreeIPA CA objects

- updates ipa-pki-proxy.conf to allow access to the Dogtag
  lightweight CAs REST API.

Part of: https://fedorahosted.org/freeipa/ticket/4559
---
 ACI.txt                                   |   8 ++
 API.txt                                   |  60 +++++++++
 VERSION                                   |   4 +-
 install/conf/ipa-pki-proxy.conf           |   4 +-
 install/share/60certificate-profiles.ldif |   2 +
 install/share/bootstrap-template.ldif     |   6 +
 install/updates/41-subca.update           |   4 +
 install/updates/Makefile.am               |   1 +
 ipalib/constants.py                       |   2 +
 ipalib/plugins/ca.py                      | 206 ++++++++++++++++++++++++++++++
 ipaserver/install/cainstance.py           |   7 +
 ipaserver/install/server/upgrade.py       |  16 ++-
 ipaserver/plugins/dogtag.py               |  44 ++++++-
 13 files changed, 358 insertions(+), 6 deletions(-)
 create mode 100644 install/updates/41-subca.update
 create mode 100644 ipalib/plugins/ca.py

diff --git a/ACI.txt b/ACI.txt
index cea814a0ceb7aea48b709236f0f88677e851ac92..dfa962ad9d43f9e7d7c750b865123150d282d4e5 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=cas,cn=ca,dc=ipa,dc=example
+aci: (targetfilter = "(objectclass=ipaca)")(version 3.0;acl "permission:System: Add CA";allow (add) groupdn = "ldap:///cn=System: Add CA,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=cas,cn=ca,dc=ipa,dc=example
+aci: (targetfilter = "(objectclass=ipaca)")(version 3.0;acl "permission:System: Delete CA";allow (delete) groupdn = "ldap:///cn=System: Delete CA,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=cas,cn=ca,dc=ipa,dc=example
+aci: (targetattr = "cn || description || ipacaid")(targetfilter = "(objectclass=ipaca)")(version 3.0;acl "permission:System: Modify CA";allow (write) groupdn = "ldap:///cn=System: Modify CA,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=cas,cn=ca,dc=ipa,dc=example
+aci: (targetattr = "cn || createtimestamp || description || entryusn || ipacaid || modifytimestamp || objectclass")(targetfilter = "(objectclass=ipaca)")(version 3.0;acl "permission:System: Read CAs";allow (compare,read,search) userdn = "ldap:///all";)
 dn: cn=caacls,cn=ca,dc=ipa,dc=example
 aci: (targetfilter = "(objectclass=ipacaacl)")(version 3.0;acl "permission:System: Add CA ACL";allow (add) groupdn = "ldap:///cn=System: Add CA ACL,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: cn=caacls,cn=ca,dc=ipa,dc=example
diff --git a/API.txt b/API.txt
index 3ad250e74f48ef3c54494ba6bd2d398a7c5d1b69..ad5691a035e261afa329fbdb33461c4cd3a0051a 100644
--- a/API.txt
+++ b/API.txt
@@ -450,12 +450,72 @@ arg: Any('methods*')
 option: Str('version?')
 output: Output('count', type=[<type 'int'>])
 output: Output('results', type=[<type 'list'>, <type 'tuple'>])
+command: ca_add
+args: 1,5,3
+arg: Str('cn', cli_name='name')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Str('description?', cli_name='desc')
+option: Flag('raw', autofill=True, cli_name='raw', default=False)
+option: DNParam('subject')
+option: Str('version?')
+output: Entry('result')
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
+output: PrimaryKey('value')
+command: ca_del
+args: 1,2,3
+arg: Str('cn+', cli_name='name')
+option: Flag('continue', autofill=True, cli_name='continue', default=False)
+option: Str('version?')
+output: Output('result', type=[<type 'dict'>])
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
+output: ListOfPrimaryKeys('value')
+command: ca_find
+args: 1,9,4
+arg: Str('criteria?')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Str('cn?', autofill=False, cli_name='name')
+option: Str('description?', autofill=False, cli_name='desc')
+option: Str('ipacaid?', autofill=False, cli_name='id')
+option: Flag('pkey_only?', autofill=True, default=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False)
+option: Int('sizelimit?', autofill=False)
+option: Int('timelimit?', autofill=False)
+option: Str('version?')
+output: Output('count', type=[<type 'int'>])
+output: ListOfEntries('result')
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
+output: Output('truncated', type=[<type 'bool'>])
 command: ca_is_enabled
 args: 0,1,3
 option: Str('version?')
 output: Output('result', type=[<type 'bool'>])
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
 output: PrimaryKey('value')
+command: ca_mod
+args: 1,9,3
+arg: Str('cn', cli_name='name')
+option: Str('addattr*', cli_name='addattr')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Str('delattr*', cli_name='delattr')
+option: Str('description?', autofill=False, cli_name='desc')
+option: Flag('raw', autofill=True, cli_name='raw', default=False)
+option: Str('rename?', cli_name='rename')
+option: Flag('rights', autofill=True, default=False)
+option: Str('setattr*', cli_name='setattr')
+option: Str('version?')
+output: Entry('result')
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
+output: PrimaryKey('value')
+command: ca_show
+args: 1,4,3
+arg: Str('cn', cli_name='name')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False)
+option: Flag('rights', autofill=True, default=False)
+option: Str('version?')
+output: Entry('result')
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
+output: PrimaryKey('value')
 command: caacl_add
 args: 1,12,3
 arg: Str('cn', cli_name='name')
diff --git a/VERSION b/VERSION
index 45fdb09788dbc6496272da786bb6d6afa45bf118..d7c08b5ccf5840c44f352e7864e5d77a8554b3b3 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=170
-# Last change: mbasti - *-find: do not search for members by default
+IPA_API_VERSION_MINOR=171
+# Last change: ftweedal - add lightweight CAs plugin
diff --git a/install/conf/ipa-pki-proxy.conf b/install/conf/ipa-pki-proxy.conf
index 4b5b6f727105610e01bab033d93b03932008463f..545f21253ec8895397e43a3c9637956e94f40293 100644
--- a/install/conf/ipa-pki-proxy.conf
+++ b/install/conf/ipa-pki-proxy.conf
@@ -1,4 +1,4 @@
-# VERSION 8 - DO NOT REMOVE THIS LINE
+# VERSION 9 - DO NOT REMOVE THIS LINE
 
 ProxyRequests Off
 
@@ -27,7 +27,7 @@ ProxyRequests Off
 </LocationMatch>
 
 # matches for CA REST API
-<LocationMatch "^/ca/rest/account/login|^/ca/rest/account/logout|^/ca/rest/installer/installToken|^/ca/rest/securityDomain/domainInfo|^/ca/rest/securityDomain/installToken|^/ca/rest/profiles|^/ca/rest/admin/kraconnector/remove">
+<LocationMatch "^/ca/rest/account/login|^/ca/rest/account/logout|^/ca/rest/installer/installToken|^/ca/rest/securityDomain/domainInfo|^/ca/rest/securityDomain/installToken|^/ca/rest/profiles|^/ca/rest/authorities|^/ca/rest/admin/kraconnector/remove">
     NSSOptions +StdEnvVars +ExportCertData +StrictRequire +OptRenegotiate
     NSSVerifyClient optional
     ProxyPassMatch ajp://localhost:$DOGTAG_PORT
diff --git a/install/share/60certificate-profiles.ldif b/install/share/60certificate-profiles.ldif
index 798c3a3b0e3ff2148a1ec8c2d4aed6522f4735e3..e9132aa7037cd09c2ff1b3a60a21d516846d3787 100644
--- a/install/share/60certificate-profiles.ldif
+++ b/install/share/60certificate-profiles.ldif
@@ -4,5 +4,7 @@ attributeTypes: (2.16.840.1.113730.3.8.21.1.2 NAME 'ipaMemberCa' DESC 'Reference
 attributeTypes: (2.16.840.1.113730.3.8.21.1.3 NAME 'ipaMemberCertProfile' DESC 'Reference to a certificate profile member' SUP distinguishedName EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 X-ORIGIN 'IPA v4.2' )
 attributeTypes: (2.16.840.1.113730.3.8.21.1.4 NAME 'ipaCaCategory' DESC 'Additional classification for CAs' EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'IPA v4.2' )
 attributeTypes: (2.16.840.1.113730.3.8.21.1.5 NAME 'ipaCertProfileCategory' DESC 'Additional classification for certificate profiles' EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'IPA v4.2' )
+attributeTypes: (2.16.840.1.113730.3.8.21.1.6 NAME 'ipaCaId' DESC 'Dogtag Authority ID' EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'IPA v4.4 Lightweight CAs' )
 objectClasses: (2.16.840.1.113730.3.8.21.2.1 NAME 'ipaCertProfile' SUP top STRUCTURAL MUST ( cn $ description $ ipaCertProfileStoreIssued ) X-ORIGIN 'IPA v4.2' )
 objectClasses: (2.16.840.1.113730.3.8.21.2.2 NAME 'ipaCaAcl' SUP ipaAssociation STRUCTURAL MUST cn MAY ( ipaCaCategory $ ipaCertProfileCategory $ userCategory $ hostCategory $ serviceCategory $ ipaMemberCa $ ipaMemberCertProfile $ memberService ) X-ORIGIN 'IPA v4.2' )
+objectClasses: (2.16.840.1.113730.3.8.21.2.3 NAME 'ipaCa' SUP top STRUCTURAL MUST ( cn $ ipaCaId ) MAY description X-ORIGIN 'IPA v4.4 Lightweight CAs' )
diff --git a/install/share/bootstrap-template.ldif b/install/share/bootstrap-template.ldif
index 456633012510acbce3c73fb9bf9b20f7d80944fe..de1475607a6b36fef45f18758e809349edde0212 100644
--- a/install/share/bootstrap-template.ldif
+++ b/install/share/bootstrap-template.ldif
@@ -470,3 +470,9 @@ changetype: add
 objectClass: nsContainer
 objectClass: top
 cn: caacls
+
+dn: cn=cas,cn=ca,$SUFFIX
+changetype: add
+objectClass: nsContainer
+objectClass: top
+cn: cas
diff --git a/install/updates/41-subca.update b/install/updates/41-subca.update
new file mode 100644
index 0000000000000000000000000000000000000000..72313e2ab12ab520eb12ade404fb5b6dd55a9d71
--- /dev/null
+++ b/install/updates/41-subca.update
@@ -0,0 +1,4 @@
+dn: cn=cas,cn=ca,$SUFFIX
+default: objectClass: nsContainer
+default: objectClass: top
+default: cn: cas
diff --git a/install/updates/Makefile.am b/install/updates/Makefile.am
index 3edc21473d676bd282e9ea2b88769c097fb8a63a..a83f202b390429bdc70c02417d853967be2f53db 100644
--- a/install/updates/Makefile.am
+++ b/install/updates/Makefile.am
@@ -37,6 +37,7 @@ app_DATA =				\
 	40-otp.update			\
 	40-vault.update			\
 	41-caacl.update			\
+	41-subca.update			\
 	45-roles.update			\
 	50-7_bit_check.update	        \
 	50-dogtag10-migration.update	\
diff --git a/ipalib/constants.py b/ipalib/constants.py
index d544595898e52d3910cff94fc3cc0276fe099a98..1478d3a14533e42735586b4610eb7c0df0f9a7cd 100644
--- a/ipalib/constants.py
+++ b/ipalib/constants.py
@@ -121,6 +121,7 @@ DEFAULT_CONFIG = (
     ('container_certprofile', DN(('cn', 'certprofiles'), ('cn', 'ca'))),
     ('container_topology', DN(('cn', 'topology'), ('cn', 'ipa'), ('cn', 'etc'))),
     ('container_caacl', DN(('cn', 'caacls'), ('cn', 'ca'))),
+    ('container_ca', DN(('cn', 'cas'), ('cn', 'ca'))),
 
     # Ports, hosts, and URIs:
     ('xmlrpc_uri', 'http://localhost:8888/ipa/xml'),
@@ -262,3 +263,4 @@ REPL_AGMT_STRIP_ATTRS = ('modifiersName',
 DOMAIN_SUFFIX_NAME = 'domain'
 CA_SUFFIX_NAME = 'ca'
 PKI_GSSAPI_SERVICE_NAME = 'dogtag'
+IPA_CA_CN = u'ipa'
diff --git a/ipalib/plugins/ca.py b/ipalib/plugins/ca.py
new file mode 100644
index 0000000000000000000000000000000000000000..9836b893dfa8733588ee4af258eb38113ccd5c97
--- /dev/null
+++ b/ipalib/plugins/ca.py
@@ -0,0 +1,206 @@
+#
+# Copyright (C) 2016  FreeIPA Contributors see COPYING for license
+#
+
+from ipalib import api, errors, DNParam, Str
+from ipalib.constants import IPA_CA_CN
+from ipalib.plugable import Registry
+from ipalib.plugins.baseldap import (
+    LDAPObject, LDAPSearch, LDAPCreate, LDAPDelete,
+    LDAPUpdate, LDAPRetrieve)
+from ipalib.plugins.cert import ca_enabled_check
+from ipalib import _, ngettext
+
+
+__doc__ = _("""
+Manage Certificate Authorities
+
+Subordinate Certificate Authorities (Sub-CAs) can be added for scoped issuance
+of X.509 certificates.
+
+EXAMPLES:
+
+  Create new CA, subordinate to the IPA CA.
+
+    ipa ca-add puppet --desc "Puppet" \\
+        --subject "CN=Puppet CA,O=EXAMPLE.COM"
+
+""")
+
+
+register = Registry()
+
+
+ at register()
+class ca(LDAPObject):
+    """
+    Lightweight CA Object
+    """
+    container_dn = api.env.container_ca
+    object_name = _('Certificate Authority')
+    object_name_plural = _('Certificate Authorities')
+    object_class = ['ipaca']
+    permission_filter_objectclasses = ['ipaca']
+    default_attributes = ['cn', 'description', 'ipacaid']
+    rdn_attribute = 'cn'
+    rdn_is_primary_key = True
+    label = _('Certificate Authorities')
+    label_singular = _('Certificate Authority')
+
+    takes_params = (
+        Str('cn',
+            primary_key=True,
+            cli_name='name',
+            label=_('Name'),
+            doc=_('Name for referencing the CA'),
+        ),
+        Str('description?',
+            cli_name='desc',
+            label=_('Description'),
+            doc=_('Description of the purpose of the CA'),
+        ),
+        Str('ipacaid?',
+            cli_name='id',
+            label=_('Authority ID'),
+            doc=_('Dogtag Authority ID'),
+            flags=['no_create', 'no_update'],
+        ),
+    )
+
+    permission_filter_objectclasses = ['ipaca']
+    managed_permissions = {
+        'System: Read CAs': {
+            'replaces_global_anonymous_aci': True,
+            'ipapermbindruletype': 'all',
+            'ipapermright': {'read', 'search', 'compare'},
+            'ipapermdefaultattr': {
+                'cn',
+                'description',
+                'ipacaid',
+                'objectclass',
+            },
+        },
+        'System: Add CA': {
+            'ipapermright': {'add'},
+            'replaces': [
+                '(target = "ldap:///cn=*,cn=cas,cn=ca,$SUFFIX")(version 3.0;acl "permission:Add CA";allow (add) groupdn = "ldap:///cn=Add CA,cn=permissions,cn=pbac,$SUFFIX";)',
+            ],
+            'default_privileges': {'CA Administrator'},
+        },
+        'System: Delete CA': {
+            'ipapermright': {'delete'},
+            'replaces': [
+                '(target = "ldap:///cn=*,cn=cas,cn=ca,$SUFFIX")(version 3.0;acl "permission:Delete CA";allow (delete) groupdn = "ldap:///cn=Delete CA,cn=permissions,cn=pbac,$SUFFIX";)',
+            ],
+            'default_privileges': {'CA Administrator'},
+        },
+        'System: Modify CA': {
+            'ipapermright': {'write'},
+            'ipapermdefaultattr': {
+                'cn',
+                'description',
+                'ipacaid',
+            },
+            'replaces': [
+                '(targetattr = "cn || description || ipacaid")(target = "ldap:///cn=*,cn=cas,cn=ca,$SUFFIX")(version 3.0;acl "permission:Modify CA";allow (write) groupdn = "ldap:///cn=Modify CA,cn=permissions,cn=pbac,$SUFFIX";)',
+            ],
+            'default_privileges': {'CA Administrator'},
+        },
+    }
+
+
+ at register()
+class ca_find(LDAPSearch):
+    __doc__ = _("Search for CAs.")
+    msg_summary = ngettext(
+        '%(count)d CA matched', '%(count)d CAs matched', 0
+    )
+
+    def execute(self, *keys, **options):
+        ca_enabled_check()
+        return super(ca_find, self).execute(*keys, **options)
+
+
+ at register()
+class ca_show(LDAPRetrieve):
+    __doc__ = _("Display the properties of a CA.")
+
+    def execute(self, *args, **kwargs):
+        ca_enabled_check()
+        return super(ca_show, self).execute(*args, **kwargs)
+
+
+ at register()
+class ca_add(LDAPCreate):
+    __doc__ = _("Create a CA.")
+    msg_summary = _('Created CA "%(value)s"')
+
+    takes_options = (
+        DNParam('subject',
+            label=_('Subject DN'),
+        ),
+    )
+
+    def pre_callback(self, ldap, dn, entry, entry_attrs, *keys, **options):
+        ca_enabled_check()
+        if not ldap.can_add(dn[1:]):
+            raise errors.ACIError(
+                info=_("Insufficient 'add' privilege for entry '%s'.") % dn)
+
+        # Create the CA in Dogtag.
+        with self.api.Backend.ra_lightweight_ca as ca_api:
+            ca_id = ca_api.create_ca(options['subject'])
+        entry['ipacaid'] = [ca_id]
+        return dn
+
+
+ at register()
+class ca_del(LDAPDelete):
+    __doc__ = _('Delete a CA.')
+
+    msg_summary = _('Deleted CA "%(value)s"')
+
+    def pre_callback(self, ldap, dn, *keys, **options):
+        ca_enabled_check()
+
+        if keys[0] == IPA_CA_CN:
+            raise errors.ProtectedEntryError(
+                label=_("CA"),
+                key=keys[0],
+                reason=_("IPA CA cannot be deleted"))
+
+        return dn
+
+    def execute(self, *args, **kwargs):
+        ca_obj = self.api.Command.ca_show(args[0][0])['result']
+        if 'ipacaid' in ca_obj:
+            have_ca_id = True
+            ca_id = ca_obj['ipacaid'][0]
+            with self.api.Backend.ra_lightweight_ca as ca_api:
+                ca_api.disable_ca(ca_id)
+                ca_api.delete_ca(ca_id)
+        else:
+            have_ca_id = False
+
+        result = super(ca_del, self).execute(*args, **kwargs)
+
+        if have_ca_id:
+            return result
+        else:
+            raise errors.NonFatalError(
+                reason=_(
+                    "The CA was deleted but could not be deleted "
+                    "from Dogtag due to missing 'ipaCaId' attribute"
+                )
+            )
+
+
+ at register()
+class ca_mod(LDAPUpdate):
+    __doc__ = _("Modify CA configuration.")
+    msg_summary = _('Modified CA "%(value)s"')
+
+    def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
+        ca_enabled_check()
+
+        return dn
diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py
index 85cdda61428a9586be940e1aec3648ee3e9b13cc..a153bf371ff31d6da721936b2b17344746d951ff 100644
--- a/ipaserver/install/cainstance.py
+++ b/ipaserver/install/cainstance.py
@@ -1628,6 +1628,13 @@ def ensure_ldap_profiles_container():
         ou=['certificateProfiles'],
     )
 
+def ensure_lightweight_cas_container():
+    ensure_entry(
+        DN(('ou', 'authorities'), ('ou', 'ca'), ('o', 'ipaca')),
+        objectclass=['top', 'organizationalUnit'],
+        ou=['authorities'],
+    )
+
 
 def ensure_entry(dn, **attrs):
     server_id = installutils.realm_to_serverid(api.env.realm)
diff --git a/ipaserver/install/server/upgrade.py b/ipaserver/install/server/upgrade.py
index cd2ad2e112fde7e13b584cb550af4bcf65e781ad..81a49e8afa049aeaaf9abd2199f21e721eef2a20 100644
--- a/ipaserver/install/server/upgrade.py
+++ b/ipaserver/install/server/upgrade.py
@@ -345,6 +345,16 @@ def ca_import_included_profiles(ca):
     return cainstance.import_included_profiles()
 
 
+def ca_ensure_lightweight_cas_container(ca):
+    root_logger.info('[Ensuring Lightweight CAs container exists in Dogtag database]')
+
+    if not ca.is_configured():
+        root_logger.info('CA is not configured')
+        return False
+
+    return cainstance.ensure_lightweight_cas_container()
+
+
 def upgrade_ca_audit_cert_validity(ca):
     """
     Update the Dogtag audit signing certificate.
@@ -1438,7 +1448,10 @@ def ca_upgrade_schema(ca):
         root_logger.info('CA is not configured')
         return False
 
-    schema_files=['/usr/share/pki/server/conf/schema-certProfile.ldif']
+    schema_files=[
+        '/usr/share/pki/server/conf/schema-certProfile.ldif',
+        '/usr/share/pki/server/conf/schema-authority.ldif',
+    ]
     try:
         modified = schemaupdate.update_schema(schema_files, ldapi=True)
     except Exception as e:
@@ -1698,6 +1711,7 @@ def upgrade_configuration():
         except ipautil.CalledProcessError as e:
             root_logger.error("Failed to restart %s: %s", ca.service_name, e)
 
+    ca_ensure_lightweight_cas_container(ca)
     ca_enable_ldap_profile_subsystem(ca)
 
     # This step MUST be done after ca_enable_ldap_profile_subsystem and
diff --git a/ipaserver/plugins/dogtag.py b/ipaserver/plugins/dogtag.py
index acec25982d8f9072a9c7d58256c8253c7e620223..83661fd0f48e82dcca91bc17826653620c3bfe46 100644
--- a/ipaserver/plugins/dogtag.py
+++ b/ipaserver/plugins/dogtag.py
@@ -2071,7 +2071,10 @@ class RestClient(Backend):
         headers = headers or {}
         headers['Cookie'] = self.cookie
 
-        resource = os.path.join('/ca/rest', self.path, path)
+        if path is not None:
+            resource = os.path.join('/ca/rest', self.path, path)
+        else:
+            resource = os.path.join('/ca/rest', self.path)
 
         # perform main request
         status, resp_headers, resp_body = dogtag.https_request(
@@ -2145,3 +2148,42 @@ class ra_certprofile(RestClient):
         Delete the profile from Dogtag
         """
         self._ssldo('DELETE', profile_id, headers={'Accept': 'application/json'})
+
+
+ at register()
+class ra_lightweight_ca(RestClient):
+    """
+    Lightweight CA management backend plugin.
+    """
+    path = 'authorities'
+
+    def create_ca(self, dn):
+        """Create CA with the given DN.
+
+        Will be created as child of IPA CA.  Nested sub-CAs are not
+        yet supported.
+
+        """
+
+        assert isinstance(dn, DN)
+        status, resp_headers, resp_body = self._ssldo(
+            'POST', None,
+            headers={
+                'Content-type': 'application/json',
+                'Accept': 'application/json',
+            },
+            body=json.dumps({"parentID": "host-authority", "dn": unicode(dn)}),
+        )
+        try:
+            return json.loads(resp_body)["id"]
+        except:
+            raise errors.RemoteRetrieveError(reason=_("Response from CA was not valid JSON"))
+
+    def disable_ca(self, ca_id):
+        self._ssldo(
+            'POST', ca_id + '/disable',
+            headers={'Accept': 'application/json'},
+        )
+
+    def delete_ca(self, ca_id):
+        self._ssldo('DELETE', ca_id)
-- 
2.5.5

-------------- next part --------------
From 35302749c7c320c7df21bf715e94f949ca6876ae Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal at redhat.com>
Date: Fri, 6 May 2016 12:07:29 +1000
Subject: [PATCH 60/64] Add IPA CA entry on install / upgrade

In addition to user-created lightweight CAs, CA ACLs need to be able
to refer to the "main" CA.  Add an entry for the IPA CA on
installation and upgrade.

Part of: https://fedorahosted.org/freeipa/ticket/4559
---
 ipaserver/install/cainstance.py     | 33 +++++++++++++++++++++++++++++++++
 ipaserver/install/server/upgrade.py |  1 +
 2 files changed, 34 insertions(+)

diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py
index a153bf371ff31d6da721936b2b17344746d951ff..fb38034ed9d238e2f6e91627a978dc260976a4e1 100644
--- a/ipaserver/install/cainstance.py
+++ b/ipaserver/install/cainstance.py
@@ -432,6 +432,7 @@ class CAInstance(DogtagInstance):
             self.step("importing IPA certificate profiles",
                       import_included_profiles)
             self.step("adding default CA ACL", ensure_default_caacl)
+            self.step("adding 'ipa' CA entry", ensure_ipa_authority_entry)
             self.step("updating IPA configuration", update_ipa_conf)
 
         self.start_creation(runtime=210)
@@ -1899,6 +1900,38 @@ def _create_dogtag_profile(profile_id, profile_data, overwrite):
                 "(it is probably already enabled)")
 
 
+def ensure_ipa_authority_entry():
+    """Add the IPA CA ipaCa object if missing."""
+    is_already_connected = api.Backend.ldap2.isconnected()
+    if not is_already_connected:
+        try:
+            api.Backend.ldap2.connect(autobind=True)
+        except errors.PublicError as e:
+            root_logger.error("Cannot connect to LDAP to add CA: %s", e)
+            return
+
+    ensure_entry(
+        DN(('cn', 'ca'), api.env.basedn),
+        objectclass=['top', 'nsContainer'],
+        cn=['ca'],
+    )
+    ensure_entry(
+        DN(api.env.container_ca, api.env.basedn),
+        objectclass=['top', 'nsContainer'],
+        cn=['cas'],
+    )
+    ensure_entry(
+        DN(('cn', 'ipa'), api.env.container_ca, api.env.basedn),
+        objectclass=['top', 'ipaca'],
+        cn=[ipalib.constants.IPA_CA_CN],
+        description=['IPA CA'],
+        ipacaid='host-authority',
+    )
+
+    if not is_already_connected:
+        api.Backend.ldap2.disconnect()
+
+
 def ensure_default_caacl():
     """Add the default CA ACL if missing."""
     is_already_connected = api.Backend.ldap2.isconnected()
diff --git a/ipaserver/install/server/upgrade.py b/ipaserver/install/server/upgrade.py
index 81a49e8afa049aeaaf9abd2199f21e721eef2a20..cd9b7c4a8aba6e346bd5277328223a5a0b2a83f4 100644
--- a/ipaserver/install/server/upgrade.py
+++ b/ipaserver/install/server/upgrade.py
@@ -1724,6 +1724,7 @@ def upgrade_configuration():
     if ca.is_configured():
         cainstance.repair_profile_caIPAserviceCert()
         ca.setup_lightweight_ca_key_retrieval()
+        cainstance.ensure_ipa_authority_entry()
 
     set_sssd_domain_option('ipa_server_mode', 'True')
 
-- 
2.5.5

-------------- next part --------------
From 18259b719551b316431fa8f7c4fb6af53b6eecc9 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal at redhat.com>
Date: Fri, 6 May 2016 10:36:22 +1000
Subject: [PATCH 61/64] Update 'caacl' plugin to support lightweight CAs

For backwards compatibility, an ACL that has no CAs and no CA
category allows access to the IPA CA (host authority) only.

Part of: https://fedorahosted.org/freeipa/ticket/4559
---
 API.txt                 |  31 +++++++++++++--
 VERSION                 |   4 +-
 ipalib/plugins/caacl.py | 100 ++++++++++++++++++++++++++++++++----------------
 ipalib/plugins/cert.py  |   5 ++-
 4 files changed, 99 insertions(+), 41 deletions(-)

diff --git a/API.txt b/API.txt
index ad5691a035e261afa329fbdb33461c4cd3a0051a..0f0f6616ae9dfd1d34fbecaada98a1de1b7818c6 100644
--- a/API.txt
+++ b/API.txt
@@ -517,12 +517,13 @@ output: Entry('result')
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
 output: PrimaryKey('value')
 command: caacl_add
-args: 1,12,3
+args: 1,13,3
 arg: Str('cn', cli_name='name')
 option: Str('addattr*', cli_name='addattr')
 option: Flag('all', autofill=True, cli_name='all', default=False)
 option: Str('description?', cli_name='desc')
 option: StrEnum('hostcategory?', cli_name='hostcat', values=[u'all'])
+option: StrEnum('ipacacategory?', cli_name='cacat', values=[u'all'])
 option: StrEnum('ipacertprofilecategory?', cli_name='profilecat', values=[u'all'])
 option: Bool('ipaenabledflag?')
 option: Flag('no_members', autofill=True, default=False)
@@ -534,6 +535,17 @@ option: Str('version?')
 output: Entry('result')
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
 output: PrimaryKey('value')
+command: caacl_add_ca
+args: 1,5,3
+arg: Str('cn', cli_name='name')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Str('ca*', alwaysask=True, cli_name='cas')
+option: Flag('no_members', autofill=True, default=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False)
+option: Str('version?')
+output: Output('completed', type=[<type 'int'>])
+output: Output('failed', type=[<type 'dict'>])
+output: Entry('result')
 command: caacl_add_host
 args: 1,6,3
 arg: Str('cn', cli_name='name')
@@ -603,12 +615,13 @@ output: Output('result', type=[<type 'bool'>])
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
 output: PrimaryKey('value')
 command: caacl_find
-args: 1,14,4
+args: 1,15,4
 arg: Str('criteria?')
 option: Flag('all', autofill=True, cli_name='all', default=False)
 option: Str('cn?', autofill=False, cli_name='name')
 option: Str('description?', autofill=False, cli_name='desc')
 option: StrEnum('hostcategory?', autofill=False, cli_name='hostcat', values=[u'all'])
+option: StrEnum('ipacacategory?', autofill=False, cli_name='cacat', values=[u'all'])
 option: StrEnum('ipacertprofilecategory?', autofill=False, cli_name='profilecat', values=[u'all'])
 option: Bool('ipaenabledflag?', autofill=False)
 option: Flag('no_members', autofill=True, default=True)
@@ -624,13 +637,14 @@ output: ListOfEntries('result')
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
 output: Output('truncated', type=[<type 'bool'>])
 command: caacl_mod
-args: 1,14,3
+args: 1,15,3
 arg: Str('cn', cli_name='name')
 option: Str('addattr*', cli_name='addattr')
 option: Flag('all', autofill=True, cli_name='all', default=False)
 option: Str('delattr*', cli_name='delattr')
 option: Str('description?', autofill=False, cli_name='desc')
 option: StrEnum('hostcategory?', autofill=False, cli_name='hostcat', values=[u'all'])
+option: StrEnum('ipacacategory?', autofill=False, cli_name='cacat', values=[u'all'])
 option: StrEnum('ipacertprofilecategory?', autofill=False, cli_name='profilecat', values=[u'all'])
 option: Bool('ipaenabledflag?', autofill=False)
 option: Flag('no_members', autofill=True, default=False)
@@ -643,6 +657,17 @@ option: Str('version?')
 output: Entry('result')
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
 output: PrimaryKey('value')
+command: caacl_remove_ca
+args: 1,5,3
+arg: Str('cn', cli_name='name')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Str('ca*', alwaysask=True, cli_name='cas')
+option: Flag('no_members', autofill=True, default=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False)
+option: Str('version?')
+output: Output('completed', type=[<type 'int'>])
+output: Output('failed', type=[<type 'dict'>])
+output: Entry('result')
 command: caacl_remove_host
 args: 1,6,3
 arg: Str('cn', cli_name='name')
diff --git a/VERSION b/VERSION
index d7c08b5ccf5840c44f352e7864e5d77a8554b3b3..01e570065e5e45634e5c246154e966c28e80d342 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=171
-# Last change: ftweedal - add lightweight CAs plugin
+IPA_API_VERSION_MINOR=172
+# Last change: ftweedal - update caacl plugin for lightweight CAs
diff --git a/ipalib/plugins/caacl.py b/ipalib/plugins/caacl.py
index 60eeb5a334acb7822549ff3530b6ec191f5e5abb..a543a1de780ae7abde342e51f6106271fcb21a9e 100644
--- a/ipalib/plugins/caacl.py
+++ b/ipalib/plugins/caacl.py
@@ -6,6 +6,7 @@ import pyhbac
 
 from ipalib import api, errors, output
 from ipalib import Bool, Str, StrEnum
+from ipalib.constants import IPA_CA_CN
 from ipalib.plugable import Registry
 from .baseldap import (
     LDAPObject, LDAPSearch, LDAPCreate, LDAPDelete, LDAPQuery,
@@ -32,14 +33,16 @@ and followed by a sequence of letters, digits or underscore ("_").
 EXAMPLES:
 
   Create a CA ACL "test" that grants all users access to the
-  "UserCert" profile:
-    ipa caacl-add test --usercat=all
+  "UserCert" profile on all CAs:
+    ipa caacl-add test --usercat=all --cacat=all
     ipa caacl-add-profile test --certprofiles UserCert
 
   Display the properties of a named CA ACL:
     ipa caacl-show test
 
-  Create a CA ACL to let user "alice" use the "DNP3" profile:
+  Create a CA ACL to let user "alice" use the "DNP3" profile on "DNP3-CA":
+    ipa caacl-add alice_dnp3
+    ipa caacl-add-ca alice_dnp3 --cas DNP3-CA
     ipa caacl-add-profile alice_dnp3 --certprofiles DNP3
     ipa caacl-add-user alice_dnp3 --user=alice
 
@@ -53,12 +56,12 @@ EXAMPLES:
 register = Registry()
 
 
-def _acl_make_request(principal_type, principal, ca_ref, profile_id):
+def _acl_make_request(principal_type, principal, ca_id, profile_id):
     """Construct HBAC request for the given principal, CA and profile"""
     service, name, realm = split_any_principal(principal)
 
     req = pyhbac.HbacRequest()
-    req.targethost.name = ca_ref
+    req.targethost.name = ca_id
     req.service.name = profile_id
     if principal_type == 'user':
         req.user.name = name
@@ -90,12 +93,12 @@ def _acl_make_rule(principal_type, obj):
     rule.srchosts.category = {pyhbac.HBAC_CATEGORY_ALL}
 
     # add CA(s)
-    # Hardcoded until caacl plugin arrives
-    rule.targethosts.category = {pyhbac.HBAC_CATEGORY_ALL}
-    #if 'ipacacategory' in obj and obj['ipacacategory'][0].lower() == 'all':
-    #    rule.targethosts.category = {pyhbac.HBAC_CATEGORY_ALL}
-    #else:
-    #    rule.targethosts.names = obj.get('ipacaaclcaref', [])
+    if 'ipacacategory' in obj and obj['ipacacategory'][0].lower() == 'all':
+        rule.targethosts.category = {pyhbac.HBAC_CATEGORY_ALL}
+    else:
+        # For compatibility with pre-lightweight-CAs CA ACLs,
+        # no CA members implies the host authority (only)
+        rule.targethosts.names = obj.get('ipamemberca_ca', [IPA_CA_CN])
 
     # add profiles
     if ('ipacertprofilecategory' in obj
@@ -120,8 +123,8 @@ def _acl_make_rule(principal_type, obj):
     return rule
 
 
-def acl_evaluate(principal_type, principal, ca_ref, profile_id):
-    req = _acl_make_request(principal_type, principal, ca_ref, profile_id)
+def acl_evaluate(principal_type, principal, ca_id, profile_id):
+    req = _acl_make_request(principal_type, principal, ca_id, profile_id)
     acls = api.Command.caacl_find(no_members=False)['result']
     rules = [_acl_make_rule(principal_type, obj) for obj in acls]
     return req.evaluate(rules) == pyhbac.HBAC_EVAL_ALLOW
@@ -151,6 +154,7 @@ class caacl(LDAPObject):
         'memberuser': ['user', 'group'],
         'memberhost': ['host', 'hostgroup'],
         'memberservice': ['service'],
+        'ipamemberca': ['ca'],
         'ipamembercertprofile': ['certprofile'],
     }
     managed_permissions = {
@@ -226,13 +230,12 @@ class caacl(LDAPObject):
              label=_('Enabled'),
              flags=['no_option'],
         ),
-        # Commented until subca plugin arrives
-        #StrEnum('ipacacategory?',
-        #    cli_name='cacat',
-        #    label=_('CA category'),
-        #    doc=_('CA category the ACL applies to'),
-        #    values=(u'all', ),
-        #),
+        StrEnum('ipacacategory?',
+            cli_name='cacat',
+            label=_('CA category'),
+            doc=_('CA category the ACL applies to'),
+            values=(u'all', ),
+        ),
         StrEnum('ipacertprofilecategory?',
             cli_name='profilecat',
             label=_('Profile category'),
@@ -257,11 +260,10 @@ class caacl(LDAPObject):
             doc=_('Service category the ACL applies to'),
             values=(u'all', ),
         ),
-        # Commented until subca plugin arrives
-        #Str('ipamemberca_subca?',
-        #    label=_('CAs'),
-        #    flags=['no_create', 'no_update', 'no_search'],
-        #),
+        Str('ipamemberca_ca?',
+            label=_('CAs'),
+            flags=['no_create', 'no_update', 'no_search'],
+        ),
         Str('ipamembercertprofile_certprofile?',
             label=_('Profiles'),
             flags=['no_create', 'no_update', 'no_search'],
@@ -330,11 +332,10 @@ class caacl_mod(LDAPUpdate):
         except errors.NotFound:
             self.obj.handle_not_found(*keys)
 
-        # Commented until subca plugin arrives
-        #if is_all(options, 'ipacacategory') and 'ipamemberca' in entry_attrs:
-        #    raise errors.MutuallyExclusiveError(reason=_(
-        #        "CA category cannot be set to 'all' "
-        #        "while there are allowed CAs"))
+        if is_all(options, 'ipacacategory') and 'ipamemberca' in entry_attrs:
+            raise errors.MutuallyExclusiveError(reason=_(
+                "CA category cannot be set to 'all' "
+                "while there are allowed CAs"))
         if (is_all(options, 'ipacertprofilecategory')
                 and 'ipamembercertprofile' in entry_attrs):
             raise errors.MutuallyExclusiveError(reason=_(
@@ -523,10 +524,9 @@ caacl_output_params = global_output_params + (
     Str('ipamembercertprofile',
         label=_('Failed profiles'),
     ),
-    # Commented until caacl plugin arrives
-    #Str('ipamemberca',
-    #    label=_('Failed CAs'),
-    #),
+    Str('ipamemberca',
+        label=_('Failed CAs'),
+    ),
 )
 
 
@@ -560,3 +560,35 @@ class caacl_remove_profile(LDAPRemoveMember):
 
     member_attributes = ['ipamembercertprofile']
     member_count_out = (_('%i profile removed.'), _('%i profiles removed.'))
+
+
+ at register()
+class caacl_add_ca(LDAPAddMember):
+    __doc__ = _('Add CAs to a CA ACL.')
+
+    has_output_params = caacl_output_params
+
+    member_attributes = ['ipamemberca']
+    member_count_out = (_('%i CA added.'), _('%i CAs added.'))
+
+    def pre_callback(self, ldap, dn, found, not_found, *keys, **options):
+        assert isinstance(dn, DN)
+        try:
+            entry_attrs = ldap.get_entry(dn, self.obj.default_attributes)
+            dn = entry_attrs.dn
+        except errors.NotFound:
+            self.obj.handle_not_found(*keys)
+        if is_all(entry_attrs, 'ipacacategory'):
+            raise errors.MutuallyExclusiveError(reason=_(
+                "CAs cannot be added when CA category='all'"))
+        return dn
+
+
+ at register()
+class caacl_remove_ca(LDAPRemoveMember):
+    __doc__ = _('Remove CAs from a CA ACL.')
+
+    has_output_params = caacl_output_params
+
+    member_attributes = ['ipamemberca']
+    member_count_out = (_('%i CA removed.'), _('%i CAs removed.'))
diff --git a/ipalib/plugins/cert.py b/ipalib/plugins/cert.py
index c97ca88665d130d4b2f2761fc12d1646a6e10782..aee2dfba15df677bd5292016389bce8b80f89708 100644
--- a/ipalib/plugins/cert.py
+++ b/ipalib/plugins/cert.py
@@ -30,6 +30,7 @@ from ipalib import pkcs10
 from ipalib import x509
 from ipalib import util
 from ipalib import ngettext
+from ipalib.constants import IPA_CA_CN
 from ipalib.plugable import Registry
 from .virtual import VirtualCommand
 from .baseldap import pkey_to_value
@@ -237,7 +238,7 @@ def caacl_check(principal_type, principal_string, ca, profile_id):
                 "with profile '%(profile_id)s' for certificate issuance."
             ) % dict(
                 principal=principal_string,
-                ca=ca or '.',
+                ca=ca,
                 profile_id=profile_id
             )
         )
@@ -319,7 +320,7 @@ class cert_request(VirtualCommand):
         add = kw.get('add')
         request_type = kw.get('request_type')
         profile_id = kw.get('profile_id', self.Backend.ra.DEFAULT_PROFILE)
-        ca = '.'  # top-level CA hardcoded until subca plugin implemented
+        ca = IPA_CA_CN  # hardcoded until --ca option implemented
 
         """
         Access control is partially handled by the ACI titled
-- 
2.5.5

-------------- next part --------------
From 57c267e52f89afb53a6bf94d93b7f71b5e04b277 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal at redhat.com>
Date: Fri, 6 May 2016 13:26:17 +1000
Subject: [PATCH 62/64] Add CA argument to ra.request_certificate

Add the optional 'ca_id' argument to ra.request_certificate(), for
passing an Authority ID to Dogtag.

Part of: https://fedorahosted.org/freeipa/ticket/4559
---
 checks/check-ra.py          |  2 +-
 ipalib/plugins/cert.py      |  2 +-
 ipaserver/plugins/dogtag.py | 21 +++++++++++++--------
 ipaserver/plugins/rabase.py |  4 +++-
 4 files changed, 18 insertions(+), 11 deletions(-)

diff --git a/checks/check-ra.py b/checks/check-ra.py
index bc9cc215b02451f0e75ac3987c4f2c60668227ce..6942804a4f98259b1c6c892f8c3aa4fd2dae2ecf 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, ra.DEFAULT_PROFILE)
+request_result = ra.request_certificate(csr, ra.DEFAULT_PROFILE, None)
 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 aee2dfba15df677bd5292016389bce8b80f89708..377486f1d41dce93d87f8e98b3cb2bded0e7058a 100644
--- a/ipalib/plugins/cert.py
+++ b/ipalib/plugins/cert.py
@@ -498,7 +498,7 @@ class cert_request(VirtualCommand):
 
         # Request the certificate
         result = self.Backend.ra.request_certificate(
-            csr, profile_id, request_type=request_type)
+            csr, profile_id, None, 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/ipaserver/plugins/dogtag.py b/ipaserver/plugins/dogtag.py
index 83661fd0f48e82dcca91bc17826653620c3bfe46..839c0831d3ab6e088eac9e519c828a078f4b15a0 100644
--- a/ipaserver/plugins/dogtag.py
+++ b/ipaserver/plugins/dogtag.py
@@ -1552,10 +1552,12 @@ class ra(rabase.rabase):
         return cmd_result
 
 
-    def request_certificate(self, csr, profile_id, request_type='pkcs10'):
+    def request_certificate(
+            self, csr, profile_id, ca_id, request_type='pkcs10'):
         """
         :param csr: The certificate signing request.
         :param profile_id: The profile to use for the request.
+        :param ca_id: The Authority ID to send request to. ``None`` is allowed.
         :param request_type: The request type (defaults to ``'pkcs10'``).
 
         Submit certificate signing request.
@@ -1584,13 +1586,16 @@ class ra(rabase.rabase):
         self.debug('%s.request_certificate()', self.fullname)
 
         # Call CMS
-        http_status, http_headers, http_body = \
-            self._sslget('/ca/eeca/ca/profileSubmitSSLClient',
-                         self.env.ca_ee_port,
-                         profileId=profile_id,
-                         cert_request_type=request_type,
-                         cert_request=csr,
-                         xml='true')
+        kw = dict(
+            profileId=profile_id,
+            cert_request_type=request_type,
+            cert_request=csr,
+            xml='true')
+        if ca_id:
+            kw['authorityId'] = ca_id
+
+        http_status, http_headers, http_body = self._sslget(
+            '/ca/eeca/ca/profileSubmitSSLClient', self.env.ca_ee_port, **kw)
         # Parse and handle errors
         if http_status != 200:
             self.raise_certificate_operation_error('request_certificate',
diff --git a/ipaserver/plugins/rabase.py b/ipaserver/plugins/rabase.py
index 949f3c37e4b1fac38199d056d8b8a43a81f2926f..736c166982c60e07c0dff50aa41dc304ea427a00 100644
--- a/ipaserver/plugins/rabase.py
+++ b/ipaserver/plugins/rabase.py
@@ -65,12 +65,14 @@ class rabase(Backend):
         """
         raise errors.NotImplementedError(name='%s.get_certificate' % self.name)
 
-    def request_certificate(self, csr, profile_id, request_type='pkcs10'):
+    def request_certificate(
+            self, csr, profile_id, ca_id, request_type='pkcs10'):
         """
         Submit certificate signing request.
 
         :param csr: The certificate signing request.
         :param profile_id: Profile to use for this request.
+        :param ca_id: The Authority ID to send request to. ``None`` is allowed.
         :param request_type: The request type (defaults to ``'pkcs10'``).
         """
         raise errors.NotImplementedError(name='%s.request_certificate' % self.name)
-- 
2.5.5

-------------- next part --------------
From 8ba60c1b34053e159c0fa0e6d5471b3af96f945d Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal at redhat.com>
Date: Fri, 6 May 2016 13:43:41 +1000
Subject: [PATCH 63/64] Update cert-request to allow specifying CA

Add the '--ca' option to the 'ipa cert-request' command, for
specifying the CA to which to direct the request.

Part of: https://fedorahosted.org/freeipa/ticket/4559
---
 API.txt                |  3 ++-
 VERSION                |  4 ++--
 ipalib/plugins/cert.py | 22 +++++++++++++++++++---
 3 files changed, 23 insertions(+), 6 deletions(-)

diff --git a/API.txt b/API.txt
index 0f0f6616ae9dfd1d34fbecaada98a1de1b7818c6..54376faa0ee44a08d817a287e58c2823561446dd 100644
--- a/API.txt
+++ b/API.txt
@@ -754,9 +754,10 @@ arg: Str('serial_number')
 option: Str('version?')
 output: Output('result')
 command: cert_request
-args: 1,5,1
+args: 1,6,1
 arg: File('csr', cli_name='csr_file')
 option: Flag('add', autofill=True, default=False)
+option: Str('ca?')
 option: Str('principal')
 option: Str('profile_id?')
 option: Str('request_type', autofill=True, default=u'pkcs10')
diff --git a/VERSION b/VERSION
index 01e570065e5e45634e5c246154e966c28e80d342..bdb415a521499f37a6496db6638b6984b0973c56 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=172
-# Last change: ftweedal - update caacl plugin for lightweight CAs
+IPA_API_VERSION_MINOR=173
+# Last change: ftweedal - add --ca option to cert-request
diff --git a/ipalib/plugins/cert.py b/ipalib/plugins/cert.py
index 377486f1d41dce93d87f8e98b3cb2bded0e7058a..f74d5e65cedadb7c7305d729a4d7d7dc5f8e2dad 100644
--- a/ipalib/plugins/cert.py
+++ b/ipalib/plugins/cert.py
@@ -273,7 +273,11 @@ class cert_request(VirtualCommand):
         Str('profile_id?', validate_profile_id,
             label=_("Profile ID"),
             doc=_("Certificate Profile to use"),
-        )
+        ),
+        Str('ca?',
+            label=_("CA"),
+            doc=_("CA to use"),
+        ),
     )
 
     has_output_params = (
@@ -320,7 +324,19 @@ class cert_request(VirtualCommand):
         add = kw.get('add')
         request_type = kw.get('request_type')
         profile_id = kw.get('profile_id', self.Backend.ra.DEFAULT_PROFILE)
-        ca = IPA_CA_CN  # hardcoded until --ca option implemented
+        ca = kw.get('ca', IPA_CA_CN)
+
+        # Check that requested authority exists (done before CA ACL
+        # enforcement so that user gets better error message if
+        # referencing nonexistant CA)
+        #
+        ca_obj = api.Command.ca_show(ca)['result']
+
+        # Look up Dogtag authority ID.
+        #
+        ca_id = None
+        if ca != IPA_CA_CN and 'ipacaid' in ca_obj:
+            ca_id = ca_obj['ipacaid'][0]
 
         """
         Access control is partially handled by the ACI titled
@@ -498,7 +514,7 @@ class cert_request(VirtualCommand):
 
         # Request the certificate
         result = self.Backend.ra.request_certificate(
-            csr, profile_id, None, request_type=request_type)
+            csr, profile_id, ca_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)
-- 
2.5.5

-------------- next part --------------
From 357d8c12e8e36c96d9f6423fcfedd2352afca20e Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftweedal at redhat.com>
Date: Tue, 10 May 2016 13:56:40 +1000
Subject: [PATCH 64/64] Add --issuer option to cert-find

Add the --issuer option to the cert-find command, for filtering the
search by issuer DN.

Also add the issuer DN to the output of the command.

Part of: https://fedorahosted.org/freeipa/ticket/4559
---
 API.txt                     | 3 ++-
 VERSION                     | 4 ++--
 ipalib/plugins/cert.py      | 5 +++++
 ipaserver/plugins/dogtag.py | 9 +++++++++
 4 files changed, 18 insertions(+), 3 deletions(-)

diff --git a/API.txt b/API.txt
index 54376faa0ee44a08d817a287e58c2823561446dd..40268d50dcf197b7c961f674a965c34f30bbd2f6 100644
--- a/API.txt
+++ b/API.txt
@@ -726,11 +726,12 @@ output: Entry('result')
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
 output: PrimaryKey('value')
 command: cert_find
-args: 0,17,4
+args: 0,18,4
 option: Flag('all', autofill=True, cli_name='all', default=False)
 option: Flag('exactly?', autofill=True, default=False)
 option: Str('issuedon_from?', autofill=False)
 option: Str('issuedon_to?', autofill=False)
+option: Str('issuer?', autofill=False)
 option: Int('max_serial_number?', autofill=False)
 option: Int('min_serial_number?', autofill=False)
 option: Flag('raw', autofill=True, cli_name='raw', default=False)
diff --git a/VERSION b/VERSION
index bdb415a521499f37a6496db6638b6984b0973c56..52960b21c1e417795e8c499cb1426c8860a4931b 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=173
-# Last change: ftweedal - add --ca option to cert-request
+IPA_API_VERSION_MINOR=174
+# Last change: ftweedal - add --issuer option to cert-find
diff --git a/ipalib/plugins/cert.py b/ipalib/plugins/cert.py
index f74d5e65cedadb7c7305d729a4d7d7dc5f8e2dad..5b82abf331eb278ac9a81dac2888d4ad8257f70a 100644
--- a/ipalib/plugins/cert.py
+++ b/ipalib/plugins/cert.py
@@ -748,6 +748,11 @@ class cert_find(Command):
             doc=_('Subject'),
             autofill=False,
         ),
+        Str('issuer?',
+            label=_('Issuer'),
+            doc=_('Issuer DN'),
+            autofill=False,
+        ),
         Int('revocation_reason?',
             label=_('Reason'),
             doc=_('Reason for revoking the certificate (0-10)'),
diff --git a/ipaserver/plugins/dogtag.py b/ipaserver/plugins/dogtag.py
index 839c0831d3ab6e088eac9e519c828a078f4b15a0..92cfadf347d00e670fc56a7884fe8d76db3969a1 100644
--- a/ipaserver/plugins/dogtag.py
+++ b/ipaserver/plugins/dogtag.py
@@ -1807,6 +1807,10 @@ class ra(rabase.rabase):
             node.text = options['subject']
             booloptions['subjectInUse'] = True
 
+        if 'issuer' in options:
+            node = etree.SubElement(page, 'issuerDN')
+            node.text = options['issuer']
+
         if 'revocation_reason' in options:
             node = etree.SubElement(page, 'revocationReason')
             node.text = unicode(options['revocation_reason'])
@@ -1895,6 +1899,11 @@ class ra(rabase.rabase):
             dn = cert.xpath('SubjectDN')
             if len(dn) == 1:
                 response_request['subject'] = unicode(dn[0].text)
+
+            issuer_dn = cert.xpath('IssuerDN')
+            if len(dn) == 1:
+                response_request['issuer'] = unicode(issuer_dn[0].text)
+
             status = cert.xpath('Status')
             if len(status) == 1:
                 response_request['status'] = unicode(status[0].text)
-- 
2.5.5



More information about the Freeipa-devel mailing list