On Wed, Jun 01, 2016 at 02:51:04PM +1000, Fraser Tweedale wrote:
> 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
>
Rebased and updated patches attached.

Substantive changes:

- add required attributes for issuer DN and subject DN
- prevent rename of IPA CA
- when adding IPA CA entry, contact Dogtag to learn authority id,
  issuer DN and subject DN
- add 'read_ca' method to Dogtag interface
- tighten ACIs to prevent modification of ipacaid attribute

Thanks,
Fraser
From a36659566a06e78b1af83828f01052ecbe5ebbbe Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftwee...@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                                   |  64 ++++++++
 VERSION                                   |   4 +-
 install/conf/ipa-pki-proxy.conf           |   4 +-
 install/share/60certificate-profiles.ldif |   4 +
 install/share/bootstrap-template.ldif     |   6 +
 install/updates/41-subca.update           |   4 +
 install/updates/Makefile.am               |   1 +
 ipalib/constants.py                       |   2 +
 ipaserver/install/cainstance.py           |   7 +
 ipaserver/install/server/upgrade.py       |  16 +-
 ipaserver/plugins/ca.py                   | 235 ++++++++++++++++++++++++++++++
 ipaserver/plugins/dogtag.py               |  54 ++++++-
 13 files changed, 403 insertions(+), 6 deletions(-)
 create mode 100644 install/updates/41-subca.update
 create mode 100644 ipaserver/plugins/ca.py

diff --git a/ACI.txt b/ACI.txt
index 
a09495e5a445d7c3bc51f0b082c538b80bbb425a..586f7343ef13b4b30e23543027220ed37ba2cb48
 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")(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 || ipacaissuerdn || ipacasubjectdn || 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 
f17093022d54d5cd0ccbf1863f6def0589bbf8c9..06e2bb56ad200de599112ffad17b4084e1ca44f3
 100644
--- a/API.txt
+++ b/API.txt
@@ -450,12 +450,76 @@ 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,7,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: DNParam('ipacasubjectdn', cli_name='subject')
+option: Flag('raw', autofill=True, cli_name='raw', 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_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,11,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: DNParam('ipacaissuerdn?', autofill=False, cli_name='issuer')
+option: DNParam('ipacasubjectdn?', autofill=False, cli_name='subject')
+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 
4ada7467a646b4b7162fd56248399a65f8600663..5ab3c9d93db91a25cf4af6f77d678b4879c4910e
 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=176
-# Last change: mbasti - location-show: list servers in the location
+IPA_API_VERSION_MINOR=177
+# 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..a87fe667d56768419dacf57103e347e88c945e2a
 100644
--- a/install/share/60certificate-profiles.ldif
+++ b/install/share/60certificate-profiles.ldif
@@ -4,5 +4,9 @@ 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' )
+attributeTypes: (2.16.840.1.113730.3.8.21.1.7 NAME 'ipaCaIssuerDN' DESC 
'Issuer DN' SUP distinguishedName X-ORIGIN 'IPA v4.4 Lightweight CAs' )
+attributeTypes: (2.16.840.1.113730.3.8.21.1.8 NAME 'ipaCaSubjectDN' DESC 
'Subject DN' SUP distinguishedName 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 $ ipaCaSubjectDN $ ipaCaIssuerDN ) MAY description X-ORIGIN 
'IPA v4.4 Lightweight CAs' )
diff --git a/install/share/bootstrap-template.ldif 
b/install/share/bootstrap-template.ldif
index 
f6ab35495ad7e9377404eb7a6b0bca26906f5421..da12ddf0ca887e8305402048ceed5d5b28816164
 100644
--- a/install/share/bootstrap-template.ldif
+++ b/install/share/bootstrap-template.ldif
@@ -476,3 +476,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 
737a8bbbd1a4915a6aefec2d273b90bb3ca31710..5b308a16bfee94bdab6c36e82862a3b66a0b380c
 100644
--- a/install/updates/Makefile.am
+++ b/install/updates/Makefile.am
@@ -38,6 +38,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 
97dff1d805a4f77469882103ab63cdb0fa55a024..05ba1adbbf215680c9c23963fc8c90c4bfca4ce8
 100644
--- a/ipalib/constants.py
+++ b/ipalib/constants.py
@@ -122,6 +122,7 @@ DEFAULT_CONFIG = (
     ('container_topology', DN(('cn', 'topology'), ('cn', 'ipa'), ('cn', 
'etc'))),
     ('container_caacl', DN(('cn', 'caacls'), ('cn', 'ca'))),
     ('container_locations', DN(('cn', 'locations'), ('cn', 'etc'))),
+    ('container_ca', DN(('cn', 'cas'), ('cn', 'ca'))),
 
     # Ports, hosts, and URIs:
     ('xmlrpc_uri', 'http://localhost:8888/ipa/xml'),
@@ -265,3 +266,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/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/ca.py b/ipaserver/plugins/ca.py
new file mode 100644
index 
0000000000000000000000000000000000000000..e1d0f65c8199b736bfc8e308f4c18d9257389079
--- /dev/null
+++ b/ipaserver/plugins/ca.py
@@ -0,0 +1,235 @@
+#
+# 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 ipaserver.plugins.baseldap import (
+    LDAPObject, LDAPSearch, LDAPCreate, LDAPDelete,
+    LDAPUpdate, LDAPRetrieve)
+from ipaserver.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()
+
+
+@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', 'ipacaissuerdn', 'ipacasubjectdn',
+    ]
+    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'],
+        ),
+        DNParam('ipacasubjectdn',
+            cli_name='subject',
+            label=_('Subject DN'),
+            doc=_('Subject Distinguished Name'),
+            flags=['no_update'],
+        ),
+        DNParam('ipacaissuerdn',
+            cli_name='issuer',
+            label=_('Issuer DN'),
+            doc=_('Issuer Distinguished Name'),
+            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',
+                'ipacaissuerdn',
+                'ipacasubjectdn',
+                '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',
+            },
+            'replaces': [
+                '(targetattr = "cn || description")(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'},
+        },
+    }
+
+
+@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)
+
+
+@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)
+
+
+@register()
+class ca_add(LDAPCreate):
+    __doc__ = _("Create a CA.")
+    msg_summary = _('Created CA "%(value)s"')
+
+    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)
+
+        # check for name collision before creating CA in Dogtag
+        try:
+            api.Object.ca.get_dn_if_exists(keys[-1])
+            self.obj.handle_duplicate_entry(*keys)
+        except errors.NotFound:
+            pass
+
+        # Create the CA in Dogtag.
+        with self.api.Backend.ra_lightweight_ca as ca_api:
+            resp = ca_api.create_ca(options['ipacasubjectdn'])
+        entry['ipacaid'] = [resp['id']]
+        entry['ipacaissuerdn'] = [resp['issuerDN']]
+
+        # In the event that the issued certificate's subject DN
+        # differs from what was requested, record the actual DN.
+        #
+        entry['ipacasubjectdn'] = [resp['dn']]
+        return dn
+
+
+@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"
+                )
+            )
+
+
+@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()
+
+        if 'rename' in options or 'cn' in entry_attrs:
+            if keys[0] == IPA_CA_CN:
+                raise errors.ProtectedEntryError(
+                    label=_("CA"),
+                    key=keys[0],
+                    reason=u'IPA CA cannot be renamed')
+
+        return dn
diff --git a/ipaserver/plugins/dogtag.py b/ipaserver/plugins/dogtag.py
index 
197814c4dc4e8c3ea55a0e1b67870dfb8eee45af..20349b05f02c6e186275a822a487eb4733d75c7d
 100644
--- a/ipaserver/plugins/dogtag.py
+++ b/ipaserver/plugins/dogtag.py
@@ -2073,7 +2073,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(
@@ -2147,3 +2150,52 @@ class ra_certprofile(RestClient):
         Delete the profile from Dogtag
         """
         self._ssldo('DELETE', profile_id, headers={'Accept': 
'application/json'})
+
+
+@register()
+class ra_lightweight_ca(RestClient):
+    """
+    Lightweight CA management backend plugin.
+    """
+    path = 'authorities'
+
+    def create_ca(self, dn):
+        """Create CA with the given DN.
+
+        New CA is issued by IPA CA.  Nested sub-CAs and unrelated
+        root CAs are not yet supported.
+
+        Return the (parsed) JSON response from server.
+
+        """
+
+        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)
+        except:
+            raise errors.RemoteRetrieveError(reason=_("Response from CA was 
not valid JSON"))
+
+    def read_ca(self, ca_id):
+        status, resp_headers, resp_body = self._ssldo(
+            'GET', ca_id, headers={'Accept': 'application/json'})
+        try:
+            return json.loads(resp_body)
+        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

From 8929096fdcb9ee1d329c4551945902144620784f Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftwee...@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     | 47 +++++++++++++++++++++++++++++++++++++
 ipaserver/install/server/upgrade.py |  1 +
 2 files changed, 48 insertions(+)

diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py
index 
a153bf371ff31d6da721936b2b17344746d951ff..d05dae80dfb377460262923a5790bcc06a2ebde3
 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,52 @@ 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."""
+
+    # find out authority id, issuer DN and subject DN of IPA CA
+    #
+    api.Backend.ra_lightweight_ca._read_password()
+    api.Backend.ra_lightweight_ca.override_port = 8443
+    with api.Backend.ra_lightweight_ca as lwca:
+        data = lwca.read_ca('host-authority')
+        attrs = dict(
+            ipacaid=data['id'],
+            ipacaissuerdn=data['issuerDN'],
+            ipacasubjectdn=data['dn'],
+        )
+    api.Backend.ra_lightweight_ca.override_port = None
+
+    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', ipalib.constants.IPA_CA_CN), api.env.container_ca, 
api.env.basedn),
+        objectclass=['top', 'ipaca'],
+        cn=[ipalib.constants.IPA_CA_CN],
+        description=['IPA CA'],
+        **attrs
+    )
+
+    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

From eaa10a58ee674dbf3bfbca8f77eeec8d066cbb78 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftwee...@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 +-
 ipaserver/plugins/caacl.py | 100 ++++++++++++++++++++++++++++++---------------
 ipaserver/plugins/cert.py  |   5 ++-
 4 files changed, 99 insertions(+), 41 deletions(-)

diff --git a/API.txt b/API.txt
index 
06e2bb56ad200de599112ffad17b4084e1ca44f3..f434ebad7cc0fa22388266a1dc696c15c6ec530f
 100644
--- a/API.txt
+++ b/API.txt
@@ -521,12 +521,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)
@@ -538,6 +539,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')
@@ -607,12 +619,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)
@@ -628,13 +641,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)
@@ -647,6 +661,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 
5ab3c9d93db91a25cf4af6f77d678b4879c4910e..9a40fde7593aee748a96417abef25b92221e5a14
 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=177
-# Last change: ftweedal - add lightweight CAs plugin
+IPA_API_VERSION_MINOR=178
+# Last change: ftweedal - update caacl plugin for lightweight CAs
diff --git a/ipaserver/plugins/caacl.py b/ipaserver/plugins/caacl.py
index 
60eeb5a334acb7822549ff3530b6ec191f5e5abb..a543a1de780ae7abde342e51f6106271fcb21a9e
 100644
--- a/ipaserver/plugins/caacl.py
+++ b/ipaserver/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.'))
+
+
+@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
+
+
+@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/ipaserver/plugins/cert.py b/ipaserver/plugins/cert.py
index 
cbb5382fb5217d92c65d4b215b497958a31e978c..ef53608ece00ca6951ddeab08e0915a596116fcf
 100644
--- a/ipaserver/plugins/cert.py
+++ b/ipaserver/plugins/cert.py
@@ -29,6 +29,7 @@ from ipalib import errors
 from ipalib import pkcs10
 from ipalib import x509
 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
@@ -236,7 +237,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
             )
         )
@@ -320,7 +321,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

From e503fa0f73022d24a9288e938d3540f4c5ed463d Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftwee...@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 +-
 ipaserver/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/ipaserver/plugins/cert.py b/ipaserver/plugins/cert.py
index 
ef53608ece00ca6951ddeab08e0915a596116fcf..8fccb76292b685033f8ebda347b51a4eb599d2bc
 100644
--- a/ipaserver/plugins/cert.py
+++ b/ipaserver/plugins/cert.py
@@ -499,7 +499,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 
20349b05f02c6e186275a822a487eb4733d75c7d..43aab92ffe6bba42d21135eb4f87cbde635f86e0
 100644
--- a/ipaserver/plugins/dogtag.py
+++ b/ipaserver/plugins/dogtag.py
@@ -1554,10 +1554,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.
@@ -1586,13 +1588,16 @@ class ra(rabase.rabase):
         self.debug('%s.request_certificate()', type(self).__name__)
 
         # 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

From 5745218e1a2a74e89bad2d7e3d3958e773910d57 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftwee...@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 ++--
 ipaserver/plugins/cert.py | 22 +++++++++++++++++++---
 3 files changed, 23 insertions(+), 6 deletions(-)

diff --git a/API.txt b/API.txt
index 
f434ebad7cc0fa22388266a1dc696c15c6ec530f..571c646c7b973b3107e9abf9f60caa4ded297c2f
 100644
--- a/API.txt
+++ b/API.txt
@@ -758,9 +758,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 
9a40fde7593aee748a96417abef25b92221e5a14..6577c80904f99ab6b30217ddc82625ccda928df0
 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=178
-# Last change: ftweedal - update caacl plugin for lightweight CAs
+IPA_API_VERSION_MINOR=179
+# Last change: ftweedal - add --ca option to cert-request
diff --git a/ipaserver/plugins/cert.py b/ipaserver/plugins/cert.py
index 
8fccb76292b685033f8ebda347b51a4eb599d2bc..f09947337efc3d5b5872937df4a82663dfb6eff9
 100644
--- a/ipaserver/plugins/cert.py
+++ b/ipaserver/plugins/cert.py
@@ -274,7 +274,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 = (
@@ -321,7 +325,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
@@ -499,7 +515,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

From 643ff632fd528e821a80084515b96c93ae9fe51c Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftwee...@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 ++--
 ipaserver/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 
571c646c7b973b3107e9abf9f60caa4ded297c2f..05527b56cf99e971600bd1f0f2e1b6155c58bc19
 100644
--- a/API.txt
+++ b/API.txt
@@ -730,11 +730,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 
6577c80904f99ab6b30217ddc82625ccda928df0..59c74531ef61c2cd9674be20f05bbe58e982571c
 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=179
-# Last change: ftweedal - add --ca option to cert-request
+IPA_API_VERSION_MINOR=180
+# Last change: ftweedal - add --issuer option to cert-find
diff --git a/ipaserver/plugins/cert.py b/ipaserver/plugins/cert.py
index 
f09947337efc3d5b5872937df4a82663dfb6eff9..314191e349260a1119facc2df0428dc542c445f3
 100644
--- a/ipaserver/plugins/cert.py
+++ b/ipaserver/plugins/cert.py
@@ -738,6 +738,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). Type '
diff --git a/ipaserver/plugins/dogtag.py b/ipaserver/plugins/dogtag.py
index 
43aab92ffe6bba42d21135eb4f87cbde635f86e0..919ecfeaca6c3ca41040152157e5d275f230704a
 100644
--- a/ipaserver/plugins/dogtag.py
+++ b/ipaserver/plugins/dogtag.py
@@ -1809,6 +1809,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'])
@@ -1897,6 +1901,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

-- 
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