On Fri, Jun 10, 2016 at 12:48:00AM +1000, Fraser Tweedale wrote:
> On Thu, Jun 09, 2016 at 12:36:35PM +0200, Jan Cholasta wrote:
> > On 9.6.2016 11:10, Fraser Tweedale wrote:
> > > On Thu, Jun 09, 2016 at 10:12:40AM +0200, Jan Cholasta wrote:
> > > > On 9.6.2016 08:44, Fraser Tweedale wrote:
> > > > > On Thu, Jun 09, 2016 at 01:21:29AM +1000, Fraser Tweedale wrote:
> > > > > > On Wed, Jun 08, 2016 at 01:00:36PM +0200, Jan Cholasta wrote:
> > > > > > > On 8.6.2016 05:15, Fraser Tweedale wrote:
> > > > > > > > On Tue, Jun 07, 2016 at 03:42:22PM +1000, Fraser Tweedale wrote:
> > > > > > > > > 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
> > > > > > > > > 
> > > > > > > > Updated patch 0064-3; adds --issuer option to cert-show and --ca
> > > > > > > > option to cert-show and cert-find.
> > > > > > > 
> > > > > > > Patch 0059:
> > > > > > > 
> > > > > > > 1) On upgrade, why is the lightweight CA container created twice 
> > > > > > > - once in
> > > > > > > 41-subca.update, once using ensure_entry() call? It should be 
> > > > > > > done only
> > > > > > > once.
> > > > > > > 
> > > > > > I'll remove 41-subca.update; the routine in cainstance is the one
> > > > > > that's needed.
> > > > > > 
> > > > > > > 2) In ca_del, every CA specified in args[0] should be deleted, 
> > > > > > > not just the
> > > > > > > first one.
> > > > > > > 
> > > > > > > 3) Do not use NonFatalError, issue a warning instead:
> > > > > > > 
> > > > > > >     self.add_message(MyNewWarningClass(name=...))
> > > > > > > 
> > > > > > > 4) Can it actually happen that ca_show does not return ipacaid? I 
> > > > > > > guess not,
> > > > > > > so you should be able to remove the check altogether and don't 
> > > > > > > bother with
> > > > > > > the warning.
> > > > > > > 
> > > > > > ipacaid is mandatory now, so I'll remove the check.
> > > > > > 
> > > > > > > 
> > > > > > > Patch 0060-0062: LGTM
> > > > > > > 
> > > > > > Yippee \o/
> > > > > > 
> > > > > > > 
> > > > > > > Patch 0063:
> > > > > > > 
> > > > > > > Could you please define the CA param as follows:
> > > > > > > 
> > > > > > >     Str('cacn?',
> > > > > > >         cli_name='ca',
> > > > > > >         query=True,
> > > > > > >         label=_("CA"),
> > > > > > >         doc=_("CA to use"),
> > > > > > >     ),
> > > > > > > 
> > > > > > > ?
> > > > > > > 
> > > > > > > This is for consitency with framework-generated parent key 
> > > > > > > params, which
> > > > > > > unfortunately we cannot leverage in cert_request currently.
> > > > > > > 
> > > > > > No problemo.
> > > > > > 
> > > > > > > 
> > > > > > > Patch 0064:
> > > > > > > 
> > > > > > > 1) See my comment for patch 0063, it applies here as well.
> > > > > > > 
> > > > > > > 2) The --issuer option should not be included in cert_show - show 
> > > > > > > commands
> > > > > > > are supposed to retrieve an object given primary key(s), and the 
> > > > > > > primary key
> > > > > > > of CA objects is just their cn.
> > > > > > > 
> > > > > > The --issuer argument is because primary key for a cert is really
> > > > > > (issuer, serial).  So it show the cert _with_ that issuer (and
> > > > > > serial), not the cert _for_ that issuer.
> > > > 
> > > > Correct, but in IPA the issuer is represented by the CA object, so in 
> > > > IPA
> > > > the primary key for a certificate is actually (CA name, serial).
> > > > 
> > > > Certificate lookup by issuer name and serial is actually a search 
> > > > operation,
> > > > analogical to how CA lookup by subject name is also a search operation, 
> > > > so
> > > > it should be done by cert-find.
> > > > 
> > > OK, I will remove the --issuer option for cert-find.
> > > 
> > > > > > 
> > > > > > > 3) In find commands, the options form a filter, so instead of 
> > > > > > > raising
> > > > > > > MutuallyExclusiveError in cert-find, return an empty result, as 
> > > > > > > with any
> > > > > > > other unmatched filter.
> > > > > > > 
> > > > > > Here, --issuer and --ca are two different ways to specify the
> > > > > > issuer.  --issuer lets you give the issuer DN straight up; --ca
> > > > > > takes the name of an IPA CA object and looks up its issuer DN.
> > > > > > (Thus it makes no sense to give both options at once).
> > > > 
> > > > That's one way to look at it, but it's true only if you assume that
> > > > cert-find can only search certificates in Dogtag. This will very soon 
> > > > became
> > > > untrue, as we will allow cert-find to also search certificates anywhere 
> > > > in
> > > > LDAP (the server part of ticket #5381). There, the difference between 
> > > > the
> > > > options would be that with --ca you search for certificates issued by 
> > > > the
> > > > specified managed CA, but with --issuer you search for certificates 
> > > > with the
> > > > given issuer name, be it managed CA or not.
> > > > 
> > > --ca is just a "shorthand" for --issuer - it merely looks up subject
> > > DN of the specified CA, and uses that as the issuer option.
> > > 
> > > > For now, IMO the correct behavior should be that if both are specified 
> > > > and
> > > > the issuer name of the specified CA does not match the specified issuer
> > > > name, empty result is returned, otherwise carry on with the search in
> > > > Dogtag.
> > > > 
> > > If I allow both, the behaviour will then be:
> > > 
> > > specify issuer DN only)
> > >     search using given issuer DN
> > > specify CA only)
> > >     search using subject DN of specified CA.  If no such CA, error.
> > > specify issuer DN and CA)
> > >     search using given issuer DN, and ensure that result (if any)
> > >     matches subject DN of specified CA.  If no such CA, error.
> > > 
> > > I'm happy to implement this if you confirm that you think it's the
> > > correct behaviour.
> > 
> > Looks correct to me.
> > 
> Thanks; updated patches attached.  Martin, this patch should also
> fix the upgrade issue.
> 
> Cheers,
> Fraser
>
Another rebase to fix conflicts in VERSION file.  No other changes.
From 4659e2a64d1d5906f9457d9c25387dea566b6b5b 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 
4247dd77c38fc17be8639a988049803d5b1f558f..eeda016b67e7df4cad8471c33e0254fe4200c22c
 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 
8945ae54888a21f6335389c26859ea2cb6353cd6..45bdeb816751cf4f4c553f90019ebd18152fa9a7
 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: abbra - adtrust: remove nttrustpartner parameter
+IPA_API_VERSION_MINOR=178
+# 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 5494f0acc70507f33f867a69dab76a95d1dcc4e7 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 43c0c4106a7a6de2c06cc0cb75ec49f97695a654 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 
eeda016b67e7df4cad8471c33e0254fe4200c22c..cc91dc318850052931e8692afb03405ea086306d
 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 
45bdeb816751cf4f4c553f90019ebd18152fa9a7..3347046622821758d029c5db1923176715b6f312
 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 - add lightweight CAs plugin
+IPA_API_VERSION_MINOR=179
+# 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 fef7a8ade9f977eb09105b0118df3bfa378ea999 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 18acd708b250d136e6b56ff5f47bb1e2259174cf 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 
cc91dc318850052931e8692afb03405ea086306d..189c89db6c88441fda71a8ce6ac11aec83daea9b
 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 
3347046622821758d029c5db1923176715b6f312..a76be09c36e38ac314addeb06c467ed7f5046395
 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 - update caacl plugin for lightweight CAs
+IPA_API_VERSION_MINOR=180
+# 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 bd638829e0f7c8b3fa58d6760f87d1195c86d2fa 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 
189c89db6c88441fda71a8ce6ac11aec83daea9b..ba9d050a5576fbb3459f7e45d88e39fd288fb6eb
 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 
a76be09c36e38ac314addeb06c467ed7f5046395..046c30f5b6a5b52be9e42a5d6f13fe532d35cdca
 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: ftweedal - add --ca option to cert-request
+IPA_API_VERSION_MINOR=181
+# 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

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