On Tue, Jun 14, 2016 at 02:19:27AM +1000, Fraser Tweedale wrote:
> On Mon, Jun 13, 2016 at 04:35:54PM +0200, Martin Babinsky wrote:
> > > > > 
> > > > > Hi Fraser,
> > > > > 
> > > > > during functional review I found the following issues:
> > > > > 
> > > > > 1.)
> > > > > 
> > > > > If I create a CAACL rule tied to a specific sub-CA let's say for user
> > > > > certificate issuance:
> > > > > 
> > > > > """
> > > > > ipa caacl-show user_cert_issuance
> > > > >   Enabled: TRUE
> > > > >   User category: all
> > > > >   CAs: user_sub_ca
> > > > >   Profiles: caIPAuserCert
> > > > >   ACL name: user_cert_issuance
> > > > > 
> > > > > """
> > > > > 
> > > > > I can still happily request certificate for a user using root-CA:
> > > > > 
> > > > > """
> > > > >  ipa cert-request cert.csr --principal jdoe --ca ipa
> > > > >   Certificate: MIID9j.../Ov8mkjFA==
> > > > >   Subject: CN=jdoe,O=IPA.TEST
> > > > >   Issuer: CN=Certificate Authority,O=IPA.TEST
> > > > >   ...
> > > > > """
> > > > > 
> > > > > should not this be denied by CA-ACL rule?
> > > > > 
> > > > > The default IPA CAACL rule is like this:
> > > > > 
> > > > > """
> > > > > ipa caacl-show hosts_services_caIPAserviceCert
> > > > >   Enabled: TRUE
> > > > >   Host category: all
> > > > >   Service category: all
> > > > >   Profiles: caIPAserviceCert
> > > > >   ACL name: hosts_services_caIPAserviceCert
> > > > > 
> > > > > """
> > > > > 
> > > > > so the default rule should not allow users to request certs at all.
> > > > > 
> > > > Yes, these should be denied.  Looking into it.
> > > > 
> > > Were you using 'admin' account to request the cert?  admin has
> > > permission 'Request Certificate ignoring CA ACLs' via the
> > > 'Certificate Manager' privilege.
> > > 
> > > If so, please try again with less privileges (e.g. self-service as
> > > jdoe).
> > > 
> > 
> > You were right when I was requesting certs as user principals themselves
> > everything worked as expected.
> > 
> > Regarding usb-CAs not present on replica, both machines run the following
> > version of dogtag CA:
> > 
> > pki-ca-10.3.2-3.fc24.noarch
> > 
> > Here are the last 256 lines of the debug log:
> > 
> > http://paste.fedoraproject.org/378512/65828332
> > 
> Thanks for that.  It seems there are two issues.  The first one is:
> 
>     Unable to read key retriever class from CS.cfg: Property
>     features.authority.keyRetrieverClass missing value
> 
> This happens only on replica, until the first restart of Dogtag
> after installation.  I have attached a patch to fix it.
> 
> The second issue is:
> 
>     Failed to update certificate
>       <stack trace>
> 
> This needs to be fixed in Dogtag, however, the error is not
> triggered if the key retrieval succeeds when the replica first
> observes the addition of a new CA.  The attached patch does help in
> this regard, so apply it and I hope it will see you through the rest
> of the functional testing of my patches... I hope.
> 
> Thank you for testing!
> 
> Cheers,
> Fraser
>
Rebased patches attached (VERSION conflicts; no other changes).
From efef047bec394c0a881a641f518ea41757ca66eb 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/63] 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-lightweight-cas.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                   | 217 ++++++++++++++++++++++++++++++
 ipaserver/plugins/dogtag.py               |  54 +++++++-
 13 files changed, 385 insertions(+), 6 deletions(-)
 create mode 100644 install/updates/41-lightweight-cas.update
 create mode 100644 ipaserver/plugins/ca.py

diff --git a/ACI.txt b/ACI.txt
index 
6f691f2a7b01f834006e3c796c14c256ee87faa6..a26e2dd4bcf7c75b93d2f11cfd470beb17b18873
 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 
68ce3560d17c1fb6b6c50a91b5bf6ba810204922..155299223311dab169a4dead6397ba41d8d863cc
 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 
7c3e46a98607f3b94a0c98406ed13aa278440875..990f647af3dc486a2d0d1632444e845acb6e3d18
 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=180
-# Last change: mbabink - Server Roles: provide an API for setting CA renewal 
master
+IPA_API_VERSION_MINOR=181
+# 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-lightweight-cas.update 
b/install/updates/41-lightweight-cas.update
new file mode 100644
index 
0000000000000000000000000000000000000000..72313e2ab12ab520eb12ade404fb5b6dd55a9d71
--- /dev/null
+++ b/install/updates/41-lightweight-cas.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 
fde69175caaee296577b8b990084e6f7619a8620..455fd209d171888dc94a7f708dc5fa1743f62bf4
 100644
--- a/install/updates/Makefile.am
+++ b/install/updates/Makefile.am
@@ -39,6 +39,7 @@ app_DATA =                            \
        40-otp.update                   \
        40-vault.update                 \
        41-caacl.update                 \
+       41-lightweight-cas.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 
becb0b1728746bb2c432d08e92425ceb95bb261a..3e2576d058733c9859f4c5b003afdacf26408348
 100644
--- a/ipaserver/install/cainstance.py
+++ b/ipaserver/install/cainstance.py
@@ -1629,6 +1629,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..ee98f0a2a9dc469d67676a3123d82ce519ba6d59
--- /dev/null
+++ b/ipaserver/plugins/ca.py
@@ -0,0 +1,217 @@
+#
+# 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"))
+
+        ca_id = self.api.Command.ca_show(keys[0])['result']['ipacaid'][0]
+        with self.api.Backend.ra_lightweight_ca as ca_api:
+            ca_api.disable_ca(ca_id)
+            ca_api.delete_ca(ca_id)
+
+        return dn
+
+
+@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 b70618b7e80185d5d3db18f7d9b203bbdb89652c 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/63] 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     | 37 +++++++++++++++++++++++++++++++++++++
 ipaserver/install/server/upgrade.py |  1 +
 2 files changed, 38 insertions(+)

diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py
index 
3e2576d058733c9859f4c5b003afdacf26408348..c7f3116f62ce1158a04af23f29439c4a1d1a102f
 100644
--- a/ipaserver/install/cainstance.py
+++ b/ipaserver/install/cainstance.py
@@ -433,6 +433,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)
@@ -1900,6 +1901,42 @@ 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', 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 739f19f08bc43c10e155767ae1e61e30f33ce8a7 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/63] 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 
155299223311dab169a4dead6397ba41d8d863cc..3d0174a7dc605be137794de89cd523ab7e2bdef0
 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 
990f647af3dc486a2d0d1632444e845acb6e3d18..a4b24076f6a7fc8f514d703559ed618aba3a1f65
 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=181
-# Last change: ftweedal - add lightweight CAs plugin
+IPA_API_VERSION_MINOR=182
+# 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 7f2dd0354a94878a9f69a79ac954b8ef893d1bbf 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/63] 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 9e6be40e2a55bce49bea380839ea81d1ebdc4c45 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/63] 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 | 18 +++++++++++++++---
 3 files changed, 19 insertions(+), 6 deletions(-)

diff --git a/API.txt b/API.txt
index 
3d0174a7dc605be137794de89cd523ab7e2bdef0..c3fa78c0dc6f23464fbfdfb640365b7a08a69751
 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('cacn?', cli_name='ca')
 option: Str('principal')
 option: Str('profile_id?')
 option: Str('request_type', autofill=True, default=u'pkcs10')
diff --git a/VERSION b/VERSION
index 
a4b24076f6a7fc8f514d703559ed618aba3a1f65..b84b4a9ec0cff31107bdd4c33abf5535310a7500
 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=182
-# Last change: ftweedal - update caacl plugin for lightweight CAs
+IPA_API_VERSION_MINOR=183
+# Last change: ftweedal - add --ca option to cert-request
diff --git a/ipaserver/plugins/cert.py b/ipaserver/plugins/cert.py
index 
8fccb76292b685033f8ebda347b51a4eb599d2bc..63a051fabdcd89d430128d3f06f44f17b9e09a27
 100644
--- a/ipaserver/plugins/cert.py
+++ b/ipaserver/plugins/cert.py
@@ -274,7 +274,13 @@ class cert_request(VirtualCommand):
         Str('profile_id?', validate_profile_id,
             label=_("Profile ID"),
             doc=_("Certificate Profile to use"),
-        )
+        ),
+        Str('cacn?',
+            cli_name='ca',
+            query=True,
+            label=_("CA"),
+            doc=_("CA to use"),
+        ),
     )
 
     has_output_params = (
@@ -321,7 +327,13 @@ 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
+
+        # Check that requested authority exists (done before CA ACL
+        # enforcement so that user gets better error message if
+        # referencing nonexistant CA) and look up authority ID.
+        #
+        ca = kw.get('cacn', IPA_CA_CN)
+        ca_id = api.Command.ca_show(ca)['result']['ipacaid'][0]
 
         """
         Access control is partially handled by the ACI titled
@@ -499,7 +511,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 cc52affcd179d3bb43b5d083c6f52fcd851304bb Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftwee...@redhat.com>
Date: Tue, 10 May 2016 13:56:40 +1000
Subject: [PATCH] Add issuer options to cert-show and cert-find

Add options to cert-show and cert-find for specifying the issuer as
a DN, or a CA name.

Also add the issuer DN to the output of cert-find.

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

diff --git a/API.txt b/API.txt
index 
c3fa78c0dc6f23464fbfdfb640365b7a08a69751..741c64365df35cd4b2c90b2e368faa75860180bf
 100644
--- a/API.txt
+++ b/API.txt
@@ -730,11 +730,13 @@ output: Entry('result')
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
 output: PrimaryKey('value')
 command: cert_find
-args: 0,17,4
+args: 0,19,4
 option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Str('cacn?', autofill=False, cli_name='ca')
 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)
@@ -774,8 +776,9 @@ option: Int('revocation_reason', autofill=True, default=0)
 option: Str('version?')
 output: Output('result')
 command: cert_show
-args: 1,2,1
+args: 1,3,1
 arg: Str('serial_number')
+option: Str('cacn?', autofill=False, cli_name='ca')
 option: Str('out?')
 option: Str('version?')
 output: Output('result')
diff --git a/VERSION b/VERSION
index 
b84b4a9ec0cff31107bdd4c33abf5535310a7500..1f8e8ed14e50f77aae404fa98085ad135d7354b5
 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=183
-# Last change: ftweedal - add --ca option to cert-request
+IPA_API_VERSION_MINOR=184
+# Last change: ftweedal - add issuer options to cert-show and cert-find
diff --git a/ipaserver/plugins/cert.py b/ipaserver/plugins/cert.py
index 
63a051fabdcd89d430128d3f06f44f17b9e09a27..171d08b9df6537e1e0422e461c56e6a170fb820d
 100644
--- a/ipaserver/plugins/cert.py
+++ b/ipaserver/plugins/cert.py
@@ -610,6 +610,13 @@ class cert_show(VirtualCommand):
     )
 
     takes_options = (
+        Str('cacn?',
+            cli_name='ca',
+            query=True,
+            label=_('Issuing CA'),
+            doc=_('Name of issing CA'),
+            autofill=False,
+        ),
         Str('out?',
             label=_('Output filename'),
             doc=_('File to store the certificate in.'),
@@ -631,8 +638,24 @@ class cert_show(VirtualCommand):
                 raise acierr
             hostname = get_host_from_principal(bind_principal)
 
+        issuer_dn = None
+        if 'cacn' in options:
+            ca_obj = api.Command.ca_show(options['cacn'])['result']
+            issuer_dn = ca_obj['ipacasubjectdn'][0]
+
+        # Dogtag lightweight CAs have shared serial number domain, so
+        # we don't tell Dogtag the issuer (but we check the cert after).
+        #
         result=self.Backend.ra.get_certificate(serial_number)
         cert = x509.load_certificate(result['certificate'])
+
+        if issuer_dn is not None and DN(unicode(cert.issuer)) != DN(issuer_dn):
+            # DN of cert differs from what we requested
+            raise errors.NotFound(
+                reason=_("Certificate with serial number %(serial)s "
+                    "issued by CA '%(ca)s' not found")
+                    % dict(serial=serial_number, ca=options['cacn']))
+
         result['subject'] = unicode(cert.subject)
         result['issuer'] = unicode(cert.issuer)
         result['valid_not_before'] = unicode(cert.valid_not_before_str)
@@ -734,6 +757,18 @@ class cert_find(Command):
             doc=_('Subject'),
             autofill=False,
         ),
+        Str('cacn?',
+            cli_name='ca',
+            query=True,
+            label=_('Issuing CA'),
+            doc=_('Name of issing CA'),
+            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 '
@@ -818,6 +853,18 @@ class cert_find(Command):
 
     def execute(self, **options):
         ca_enabled_check()
+
+        if 'cacn' in options:
+            ca_obj = api.Command.ca_show(options['cacn'])['result']
+            ca_sdn = unicode(ca_obj['ipacasubjectdn'][0])
+            if 'issuer' in options:
+                if DN(ca_sdn) != DN(options['issuer']):
+                    # client has provided both 'ca' and 'issuer' but
+                    # issuer DNs don't match; result must be empty
+                    return dict(result=[], count=0, truncated=False)
+            else:
+                options['issuer'] = ca_sdn
+
         ret = dict(
             result=self.Backend.ra.find(options)
         )
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

From 74f22815e3590bd237c6d1c1548760dd58793c27 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftwee...@redhat.com>
Date: Tue, 14 Jun 2016 01:22:41 +1000
Subject: [PATCH] replica-install: configure key retriever before starting
 Dogtag

After installing a replica, Dogtag's Lightweight CA key retrieval
fails until Dogtag is restarted, because the already-running
instance doesn't pick up the changes to CS.cfg.  Configure the key
retriever before the instance is started.

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

diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py
index 
c7f3116f62ce1158a04af23f29439c4a1d1a102f..8dfb71528d2dc020e05ccd7ff42199218a1c0839
 100644
--- a/ipaserver/install/cainstance.py
+++ b/ipaserver/install/cainstance.py
@@ -1344,6 +1344,8 @@ class CAInstance(DogtagInstance):
                   self.enable_pkix)
         self.step("set up client auth to db", self.__client_auth_to_db)
         self.step("destroying installation admin user", self.teardown_admin)
+        self.step("Configure lightweight CA key retrieval",
+                  self.setup_lightweight_ca_key_retrieval)
         self.step("starting instance", self.start_instance)
 
         self.step("importing CA chain to RA certificate database",
@@ -1362,8 +1364,6 @@ class CAInstance(DogtagInstance):
         self.step("updating IPA configuration", update_ipa_conf)
         self.step("Restart HTTP server to pick up changes",
                   self.__restart_http_instance)
-        self.step("Configure lightweight CA key retrieval",
-                  self.setup_lightweight_ca_key_retrieval)
 
         self.step("enabling CA instance", self.__enable_instance)
 
-- 
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