On 10/06/15 13:57, Martin Kosek wrote:
On 06/10/2015 01:50 PM, Jan Cholasta wrote:
Dne 10.6.2015 v 13:44 Martin Basti napsal(a):
On 10/06/15 06:40, Fraser Tweedale wrote:
On Tue, Jun 09, 2015 at 04:37:56PM +0200, Martin Basti wrote:
On 09/06/15 08:58, Fraser Tweedale wrote:
On Mon, Jun 08, 2015 at 08:49:06AM +0200, Martin Kosek wrote:
On 06/08/2015 03:31 AM, Fraser Tweedale wrote:
New patches attached.  Comments inline.
Thanks Fraser!

...
5)
Missing referint plugin configuration for attribute
'ipacaaclmembercertprofile'
Please add it into install/updates/25-referint.update (+ other
member
attributes if missing)

Added this.  There is a comment in 25-referint.update:

      # pres and eq indexes defined in 20-indices.update must be set
      # for all the attributes

Can you explain what is required here?  Is it just to add: I see
things for memberUser and memberHost in indices.ldif but nothing for
memberService.  Do I need to add to indices.ldif:

      dn: cn=memberProfile,cn=index,cn=userRoot,cn=ldbm
database,cn=plugins,cn=config
      changetype: add
      cn: memberProfile
      ObjectClass: top
      ObjectClass: nsIndex
      nsSystemIndex: false
      nsIndexType: eq
      nsIndexType: pres
      nsIndexType: sub

, and similarly for memberCa?  Sorry I do not know much about LDAP
indexing.
AFAIR, yes. BTW, where does the "sub" index come from? It is quite
an expensive
index to use and I now cannot think of memberProfile search where
you would
need a substring...

Thanks,
Martin
Updated patch attached, which adds the indices.  (Also rebased).

There is a commit that seems to indicate that substring index is
needed, so I have included substring indices in this patchset.
Copied Honza in case he wants to comment.

      commit a10521a1dcf69960d6ce0bf5657180b709c297c0
      Author: Jan Cholasta <jchol...@redhat.com>
      Date:   Tue Jun 25 13:16:40 2013 +0000

          Add missing substring indices for attributes managed by the
referint plugin.

          The referint plugin does a substring search on these
attributes each time an
          entry is deleted, which causes a noticable slowdown for
large directories if
          the attributes are not indexed.

          https://fedorahosted.org/freeipa/ticket/3706

Cheers,
Fraser
ACK

Please send the upgrade patch ASAP :)

--
Martin Basti

Thank you for the ACK \o/

Since the patches have not been pushed, here is an updated patchset
which adds the upgrade behaviour.  There are no changes apart from
the additions to ipaserver/install/server/upgrade.py.

Cheers,
Fraser
ACK
NACK, the new OIDs are not registered.

BTW all new attribute names should have the "ipa" prefix. Also I would prefer
"CertProfile" instead of just "Profile" in certificate profile related names.
Please rename the attributes as follows:

     memberCa -> ipaMemberCa
     memberProfile -> ipaMemberCertProfile
     caCategory -> ipaCaCategory
     profileCategory -> ipaCertProfileCategory

Honza

+1. I see that other attributes from this feature use the ipa prefix already:

dn: cn=schema
attributeTypes: (2.16.840.1.113730.3.8.21.1.1 NAME 'ipaCertProfileStoreIssued'
DESC 'Store certificates issued using this profile' EQUALITY booleanMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE X-ORIGIN 'IPA v4.2' )
objectClasses: (2.16.840.1.113730.3.8.21.2.1 NAME 'ipaCertProfile' SUP top
STRUCTURAL MUST ( cn $ description $ ipaCertProfileStoreIssued ) X-ORIGIN 'IPA
v4.2' )

Those OIDs should be BTW registered as well, if not already
OID registered.

Patches with updated names attached.
Can you Fraser check if I didn't break anything? :)

--
Martin Basti

From e026cbfeb750d359ac2783b3ff3f88fbf3e7e5e3 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftwee...@redhat.com>
Date: Mon, 25 May 2015 08:39:07 -0400
Subject: [PATCH 1/2] Add CA ACL plugin

Implement the caacl commands, which are used to indicate which
principals may be issued certificates from which (sub-)CAs, using
which profiles.

At this commit, and until sub-CAs are implemented, all rules refer
to the top-level CA (represented as ".") and no ca-ref argument is
exposed.

Also, during install and upgrade add a default CA ACL that permits
certificate issuance for all hosts and services using the profile
'caIPAserviceCert' on the top-level CA.

Part of: https://fedorahosted.org/freeipa/ticket/57
Part of: https://fedorahosted.org/freeipa/ticket/4559
---
 ACI.txt                                   |  10 +
 API.txt                                   | 184 ++++++++++++
 VERSION                                   |   4 +-
 install/share/60certificate-profiles.ldif |   5 +
 install/share/Makefile.am                 |   1 +
 install/share/bootstrap-template.ldif     |   6 +
 install/share/default-caacl.ldif          |  11 +
 install/share/indices.ldif                |  20 ++
 install/updates/20-indices.update         |  18 ++
 install/updates/25-referint.update        |   2 +
 install/updates/41-caacl.update           |   4 +
 install/updates/Makefile.am               |   1 +
 ipalib/constants.py                       |   1 +
 ipalib/plugins/caacl.py                   | 477 ++++++++++++++++++++++++++++++
 ipaserver/install/dsinstance.py           |   4 +
 ipaserver/install/server/upgrade.py       |  25 ++
 16 files changed, 771 insertions(+), 2 deletions(-)
 create mode 100644 install/share/default-caacl.ldif
 create mode 100644 install/updates/41-caacl.update
 create mode 100644 ipalib/plugins/caacl.py

diff --git a/ACI.txt b/ACI.txt
index 59173ac1b593f15e079c7b1fce43ec9b0084ec91..60e9ebb10bc9b7266ff0d42a05d4d165d4ed2d55 100644
--- a/ACI.txt
+++ b/ACI.txt
@@ -22,6 +22,16 @@ 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=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
+aci: (targetfilter = "(objectclass=ipacaacl)")(version 3.0;acl "permission:System: Delete CA ACL";allow (delete) groupdn = "ldap:///cn=System: Delete CA ACL,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=caacls,cn=ca,dc=ipa,dc=example
+aci: (targetattr = "hostcategory || ipacacategory || ipacertprofilecategory || ipamemberca || ipamembercertprofile || memberhost || memberservice || memberuser || servicecategory || usercategory")(targetfilter = "(objectclass=ipacaacl)")(version 3.0;acl "permission:System: Manage CA ACL Membership";allow (write) groupdn = "ldap:///cn=System: Manage CA ACL Membership,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=caacls,cn=ca,dc=ipa,dc=example
+aci: (targetattr = "cn || description || ipaenabledflag")(targetfilter = "(objectclass=ipacaacl)")(version 3.0;acl "permission:System: Modify CA ACL";allow (write) groupdn = "ldap:///cn=System: Modify CA ACL,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=caacls,cn=ca,dc=ipa,dc=example
+aci: (targetattr = "cn || createtimestamp || description || entryusn || hostcategory || ipacacategory || ipacertprofilecategory || ipaenabledflag || ipamemberca || ipamembercertprofile || ipauniqueid || member || memberhost || memberservice || memberuser || modifytimestamp || objectclass || servicecategory || usercategory")(targetfilter = "(objectclass=ipacaacl)")(version 3.0;acl "permission:System: Read CA ACLs";allow (compare,read,search) userdn = "ldap:///all";;)
 dn: cn=certprofiles,cn=ca,dc=ipa,dc=example
 aci: (targetfilter = "(objectclass=ipacertprofile)")(version 3.0;acl "permission:System: Delete Certificate Profile";allow (delete) groupdn = "ldap:///cn=System: Delete Certificate Profile,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: cn=certprofiles,cn=ca,dc=ipa,dc=example
diff --git a/API.txt b/API.txt
index 9e3f223b7ac338840d7090299f9108e951ea920a..2a975e9f0f113ac6ed09f07fdd691a592e88eaa6 100644
--- a/API.txt
+++ b/API.txt
@@ -456,6 +456,190 @@ option: Str('version?', exclude='webui')
 output: Output('result', <type 'bool'>, None)
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: PrimaryKey('value', None, None)
+command: caacl_add
+args: 1,12,3
+arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, required=True)
+option: Str('addattr*', cli_name='addattr', exclude='webui')
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('description', attribute=True, cli_name='desc', multivalue=False, required=False)
+option: StrEnum('hostcategory', attribute=True, cli_name='hostcat', multivalue=False, required=False, values=(u'all',))
+option: StrEnum('ipacertprofilecategory', attribute=True, cli_name='profilecat', multivalue=False, required=False, values=(u'all',))
+option: Bool('ipaenabledflag', attribute=True, cli_name='ipaenabledflag', multivalue=False, required=False)
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: StrEnum('servicecategory', attribute=True, cli_name='servicecat', multivalue=False, required=False, values=(u'all',))
+option: Str('setattr*', cli_name='setattr', exclude='webui')
+option: StrEnum('usercategory', attribute=True, cli_name='usercat', multivalue=False, required=False, values=(u'all',))
+option: Str('version?', exclude='webui')
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: PrimaryKey('value', None, None)
+command: caacl_add_host
+args: 1,6,3
+arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('host*', alwaysask=True, cli_name='hosts', csv=True)
+option: Str('hostgroup*', alwaysask=True, cli_name='hostgroups', csv=True)
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('version?', exclude='webui')
+output: Output('completed', <type 'int'>, None)
+output: Output('failed', <type 'dict'>, None)
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+command: caacl_add_profile
+args: 1,5,3
+arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('certprofile*', alwaysask=True, cli_name='certprofiles', csv=True)
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('version?', exclude='webui')
+output: Output('completed', <type 'int'>, None)
+output: Output('failed', <type 'dict'>, None)
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+command: caacl_add_service
+args: 1,5,3
+arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('service*', alwaysask=True, cli_name='services', csv=True)
+option: Str('version?', exclude='webui')
+output: Output('completed', <type 'int'>, None)
+output: Output('failed', <type 'dict'>, None)
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+command: caacl_add_user
+args: 1,6,3
+arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('group*', alwaysask=True, cli_name='groups', csv=True)
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('user*', alwaysask=True, cli_name='users', csv=True)
+option: Str('version?', exclude='webui')
+output: Output('completed', <type 'int'>, None)
+output: Output('failed', <type 'dict'>, None)
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+command: caacl_del
+args: 1,2,3
+arg: Str('cn', attribute=True, cli_name='name', multivalue=True, primary_key=True, query=True, required=True)
+option: Flag('continue', autofill=True, cli_name='continue', default=False)
+option: Str('version?', exclude='webui')
+output: Output('result', <type 'dict'>, None)
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: ListOfPrimaryKeys('value', None, None)
+command: caacl_disable
+args: 1,1,3
+arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
+option: Str('version?', exclude='webui')
+output: Output('result', <type 'bool'>, None)
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: PrimaryKey('value', None, None)
+command: caacl_enable
+args: 1,1,3
+arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
+option: Str('version?', exclude='webui')
+output: Output('result', <type 'bool'>, None)
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: PrimaryKey('value', None, None)
+command: caacl_find
+args: 1,14,4
+arg: Str('criteria?', noextrawhitespace=False)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('cn', attribute=True, autofill=False, cli_name='name', multivalue=False, primary_key=True, query=True, required=False)
+option: Str('description', attribute=True, autofill=False, cli_name='desc', multivalue=False, query=True, required=False)
+option: StrEnum('hostcategory', attribute=True, autofill=False, cli_name='hostcat', multivalue=False, query=True, required=False, values=(u'all',))
+option: StrEnum('ipacertprofilecategory', attribute=True, autofill=False, cli_name='profilecat', multivalue=False, query=True, required=False, values=(u'all',))
+option: Bool('ipaenabledflag', attribute=True, autofill=False, cli_name='ipaenabledflag', multivalue=False, query=True, required=False)
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Flag('pkey_only?', autofill=True, default=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: StrEnum('servicecategory', attribute=True, autofill=False, cli_name='servicecat', multivalue=False, query=True, required=False, values=(u'all',))
+option: Int('sizelimit?', autofill=False, minvalue=0)
+option: Int('timelimit?', autofill=False, minvalue=0)
+option: StrEnum('usercategory', attribute=True, autofill=False, cli_name='usercat', multivalue=False, query=True, required=False, values=(u'all',))
+option: Str('version?', exclude='webui')
+output: Output('count', <type 'int'>, None)
+output: ListOfEntries('result', (<type 'list'>, <type 'tuple'>), Gettext('A list of LDAP entries', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: Output('truncated', <type 'bool'>, None)
+command: caacl_mod
+args: 1,14,3
+arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
+option: Str('addattr*', cli_name='addattr', exclude='webui')
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('delattr*', cli_name='delattr', exclude='webui')
+option: Str('description', attribute=True, autofill=False, cli_name='desc', multivalue=False, required=False)
+option: StrEnum('hostcategory', attribute=True, autofill=False, cli_name='hostcat', multivalue=False, required=False, values=(u'all',))
+option: StrEnum('ipacertprofilecategory', attribute=True, autofill=False, cli_name='profilecat', multivalue=False, required=False, values=(u'all',))
+option: Bool('ipaenabledflag', attribute=True, autofill=False, cli_name='ipaenabledflag', multivalue=False, required=False)
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Flag('rights', autofill=True, default=False)
+option: StrEnum('servicecategory', attribute=True, autofill=False, cli_name='servicecat', multivalue=False, required=False, values=(u'all',))
+option: Str('setattr*', cli_name='setattr', exclude='webui')
+option: StrEnum('usercategory', attribute=True, autofill=False, cli_name='usercat', multivalue=False, required=False, values=(u'all',))
+option: Str('version?', exclude='webui')
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: PrimaryKey('value', None, None)
+command: caacl_remove_host
+args: 1,6,3
+arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('host*', alwaysask=True, cli_name='hosts', csv=True)
+option: Str('hostgroup*', alwaysask=True, cli_name='hostgroups', csv=True)
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('version?', exclude='webui')
+output: Output('completed', <type 'int'>, None)
+output: Output('failed', <type 'dict'>, None)
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+command: caacl_remove_profile
+args: 1,5,3
+arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('certprofile*', alwaysask=True, cli_name='certprofiles', csv=True)
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('version?', exclude='webui')
+output: Output('completed', <type 'int'>, None)
+output: Output('failed', <type 'dict'>, None)
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+command: caacl_remove_service
+args: 1,5,3
+arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('service*', alwaysask=True, cli_name='services', csv=True)
+option: Str('version?', exclude='webui')
+output: Output('completed', <type 'int'>, None)
+output: Output('failed', <type 'dict'>, None)
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+command: caacl_remove_user
+args: 1,6,3
+arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('group*', alwaysask=True, cli_name='groups', csv=True)
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('user*', alwaysask=True, cli_name='users', csv=True)
+option: Str('version?', exclude='webui')
+output: Output('completed', <type 'int'>, None)
+output: Output('failed', <type 'dict'>, None)
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+command: caacl_show
+args: 1,5,3
+arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Flag('rights', autofill=True, default=False)
+option: Str('version?', exclude='webui')
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: PrimaryKey('value', None, None)
 command: cert_find
 args: 0,17,4
 option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
diff --git a/VERSION b/VERSION
index 535b3e228a3500f2013ea793b19a97d9fbd05021..911470c2d83d81e6abee3e8e41496cf472a11d19 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=126
-# Last change: edewata - added vault-archive and vault-retrieve
+IPA_API_VERSION_MINOR=127
+# Last change: ftweedal - add caacl plugin
diff --git a/install/share/60certificate-profiles.ldif b/install/share/60certificate-profiles.ldif
index f1281949e53386e5bfe8b35e0c15858c693c5467..798c3a3b0e3ff2148a1ec8c2d4aed6522f4735e3 100644
--- a/install/share/60certificate-profiles.ldif
+++ b/install/share/60certificate-profiles.ldif
@@ -1,3 +1,8 @@
 dn: cn=schema
 attributeTypes: (2.16.840.1.113730.3.8.21.1.1 NAME 'ipaCertProfileStoreIssued' DESC 'Store certificates issued using this profile' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE X-ORIGIN 'IPA v4.2' )
+attributeTypes: (2.16.840.1.113730.3.8.21.1.2 NAME 'ipaMemberCa' DESC 'Reference to a CA 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.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' )
 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' )
diff --git a/install/share/Makefile.am b/install/share/Makefile.am
index 31f391be25c58b76cc71971852074d80c5514745..e97a89ca93f7f188e06dc982bd69e251f8082df3 100644
--- a/install/share/Makefile.am
+++ b/install/share/Makefile.am
@@ -29,6 +29,7 @@ app_DATA =				\
 	bootstrap-template.ldif		\
 	caJarSigningCert.cfg.template	\
 	default-aci.ldif		\
+	default-caacl.ldif		\
 	default-hbac.ldif		\
 	default-smb-group.ldif		\
 	default-trust-view.ldif		\
diff --git a/install/share/bootstrap-template.ldif b/install/share/bootstrap-template.ldif
index c5d4bad8b80640881f4631e4873a12c82b0ea48a..2387f220fd4fe6e3ccd59f4b592f2473d7acfa44 100644
--- a/install/share/bootstrap-template.ldif
+++ b/install/share/bootstrap-template.ldif
@@ -441,3 +441,9 @@ changetype: add
 objectClass: nsContainer
 objectClass: top
 cn: certprofiles
+
+dn: cn=caacls,cn=ca,$SUFFIX
+changetype: add
+objectClass: nsContainer
+objectClass: top
+cn: caacls
diff --git a/install/share/default-caacl.ldif b/install/share/default-caacl.ldif
new file mode 100644
index 0000000000000000000000000000000000000000..f3cd5b4d4e3a79bc6638dc1ffdd7028596ded254
--- /dev/null
+++ b/install/share/default-caacl.ldif
@@ -0,0 +1,11 @@
+# default CA ACL that grants use of caIPAserviceCert on top-level CA to all hosts and services
+dn: ipauniqueid=autogenerate,cn=caacls,cn=ca,$SUFFIX
+changetype: add
+objectclass: ipaassociation
+objectclass: ipacaacl
+ipauniqueid: autogenerate
+cn: hosts_services_caIPAserviceCert
+ipaenabledflag: TRUE
+ipamembercertprofile: cn=caIPAserviceCert,cn=certprofiles,cn=ca,$SUFFIX
+hostcategory: all
+servicecategory: all
diff --git a/install/share/indices.ldif b/install/share/indices.ldif
index ad678e0b2123d961c957d3071ba48ff70bf27e7a..70a587d7a2c3d29955f4f95f6b475c4f90fb73a7 100644
--- a/install/share/indices.ldif
+++ b/install/share/indices.ldif
@@ -227,3 +227,23 @@ ObjectClass: top
 ObjectClass: nsIndex
 nsSystemIndex: false
 nsIndexType: eq
+
+dn: cn=ipaMemberCa,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
+changetype: add
+cn: ipaMemberCa
+ObjectClass: top
+ObjectClass: nsIndex
+nsSystemIndex: false
+nsIndexType: eq
+nsIndexType: pres
+nsIndexType: sub
+
+dn: cn=ipaMemberCertProfile,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
+changetype: add
+cn: ipaMemberCertProfile
+ObjectClass: top
+ObjectClass: nsIndex
+nsSystemIndex: false
+nsIndexType: eq
+nsIndexType: pres
+nsIndexType: sub
diff --git a/install/updates/20-indices.update b/install/updates/20-indices.update
index 880e73f3bb1b2a32c2fa40f65666cfd594cdc659..ed855b295ee2f9a02effc72bc2ffe52b4c5730df 100644
--- a/install/updates/20-indices.update
+++ b/install/updates/20-indices.update
@@ -191,3 +191,21 @@ default:nsSystemIndex: false
 only:nsIndexType: eq
 only:nsIndexType: pres
 only:nsIndexType: sub
+
+dn: cn=ipaMemberCa,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
+default:cn: ipaMemberCa
+default:ObjectClass: top
+default:ObjectClass: nsIndex
+default:nsSystemIndex: false
+only:nsIndexType: eq
+only:nsIndexType: pres
+only:nsIndexType: sub
+
+dn: cn=ipaMemberCertProfile,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
+default:cn: ipaMemberCertProfile
+default:ObjectClass: top
+default:ObjectClass: nsIndex
+default:nsSystemIndex: false
+only:nsIndexType: eq
+only:nsIndexType: pres
+only:nsIndexType: sub
diff --git a/install/updates/25-referint.update b/install/updates/25-referint.update
index 005cd0376d82c83b1b7ab368f992e209b0da5e9a..3f78ee9755823fb3d5838d3069f4506c57a69d05 100644
--- a/install/updates/25-referint.update
+++ b/install/updates/25-referint.update
@@ -17,3 +17,5 @@ add: referint-membership-attr: ipasudorunasgroup
 add: referint-membership-attr: ipatokenradiusconfiglink
 add: referint-membership-attr: ipaassignedidview
 add: referint-membership-attr: ipaallowedtarget
+add: referint-membership-attr: ipamemberca
+add: referint-membership-attr: ipamembercertprofile
diff --git a/install/updates/41-caacl.update b/install/updates/41-caacl.update
new file mode 100644
index 0000000000000000000000000000000000000000..a18b6ec946855c194077d9ac01a8adcfddf8542e
--- /dev/null
+++ b/install/updates/41-caacl.update
@@ -0,0 +1,4 @@
+dn: cn=caacls,cn=ca,$SUFFIX
+default: objectClass: nsContainer
+default: objectClass: top
+default: cn: caacls
diff --git a/install/updates/Makefile.am b/install/updates/Makefile.am
index fc6bd624eac619cdddeba29b85440571d85fd69f..eddf4d850ed4b47d5526dc152149fa21b14779d4 100644
--- a/install/updates/Makefile.am
+++ b/install/updates/Makefile.am
@@ -35,6 +35,7 @@ app_DATA =				\
 	40-certprofile.update		\
 	40-otp.update			\
 	40-vault.update			\
+	41-caacl.update			\
 	45-roles.update			\
 	50-7_bit_check.update	        \
 	50-dogtag10-migration.update	\
diff --git a/ipalib/constants.py b/ipalib/constants.py
index 93d7aaa7b0b5f0b47b8839e764ef168c1fe08c97..86b1ce8bd501845e7b5871773e86521d3c5d2ad9 100644
--- a/ipalib/constants.py
+++ b/ipalib/constants.py
@@ -120,6 +120,7 @@ DEFAULT_CONFIG = (
     ('container_masters', DN(('cn', 'masters'), ('cn', 'ipa'), ('cn', 'etc'))),
     ('container_certprofile', DN(('cn', 'certprofiles'), ('cn', 'ca'))),
     ('container_topology', DN(('cn', 'topology'), ('cn', 'ipa'), ('cn', 'etc'))),
+    ('container_caacl', DN(('cn', 'caacls'), ('cn', 'ca'))),
 
     # Ports, hosts, and URIs:
     ('xmlrpc_uri', 'http://localhost:8888/ipa/xml'),
diff --git a/ipalib/plugins/caacl.py b/ipalib/plugins/caacl.py
new file mode 100644
index 0000000000000000000000000000000000000000..f0dc9ae35896ae2b818847693812e68c62749e4a
--- /dev/null
+++ b/ipalib/plugins/caacl.py
@@ -0,0 +1,477 @@
+#
+# Copyright (C) 2015  FreeIPA Contributors see COPYING for license
+#
+
+from ipalib import api, errors, output
+from ipalib import Bool, Str, StrEnum
+from ipalib.plugable import Registry
+from ipalib.plugins.baseldap import (
+    LDAPObject, LDAPSearch, LDAPCreate, LDAPDelete, LDAPQuery,
+    LDAPUpdate, LDAPRetrieve, LDAPAddMember, LDAPRemoveMember,
+    global_output_params, pkey_to_value)
+from ipalib.plugins.hbacrule import is_all
+from ipalib import _, ngettext
+from ipapython.dn import DN
+
+
+__doc__ = _("""
+Manage CA ACL rules.
+
+This plugin is used to define rules governing which principals are
+permitted to have certificates issued using a given certificate
+profile.
+
+PROFILE ID SYNTAX:
+
+A Profile ID is a string without spaces or punctuation starting with a letter
+and followed by a sequence of letters, digits or underscore ("_").
+
+EXAMPLES:
+
+  Create a CA ACL "test" that grants all users access to the
+  "UserCert" profile:
+    ipa caacl-add test --usercat=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:
+    ipa caacl-add-profile alice_dnp3 --certprofiles DNP3
+    ipa caacl-add-user alice_dnp3 --user=alice
+
+  Disable a CA ACL:
+    ipa caacl-disable test
+
+  Remove a CA ACL:
+    ipa caacl-del test
+""")
+
+register = Registry()
+
+
+@register()
+class caacl(LDAPObject):
+    """
+    CA ACL object.
+    """
+    container_dn = api.env.container_caacl
+    object_name = _('CA ACL')
+    object_name_plural = _('CA ACLs')
+    object_class = ['ipaassociation', 'ipacaacl']
+    permission_filter_objectclasses = ['ipacaacl']
+    default_attributes = [
+        'cn', 'description', 'ipaenabledflag',
+        'ipacacategory', 'ipamemberca',
+        'ipacertprofilecategory', 'ipamembercertprofile',
+        'usercategory', 'memberuser',
+        'hostcategory', 'memberhost',
+        'servicecategory', 'memberservice',
+    ]
+    uuid_attribute = 'ipauniqueid'
+    rdn_attribute = 'ipauniqueid'
+    attribute_members = {
+        'memberuser': ['user', 'group'],
+        'memberhost': ['host', 'hostgroup'],
+        'memberservice': ['service'],
+        'ipamembercertprofile': ['certprofile'],
+    }
+    managed_permissions = {
+        'System: Read CA ACLs': {
+            'replaces_global_anonymous_aci': True,
+            'ipapermbindruletype': 'all',
+            'ipapermright': {'read', 'search', 'compare'},
+            'ipapermdefaultattr': {
+                'cn', 'description', 'ipaenabledflag',
+                'ipacacategory', 'ipamemberca',
+                'ipacertprofilecategory', 'ipamembercertprofile',
+                'usercategory', 'memberuser',
+                'hostcategory', 'memberhost',
+                'servicecategory', 'memberservice',
+                'ipauniqueid',
+                'objectclass', 'member',
+            },
+        },
+        'System: Add CA ACL': {
+            'ipapermright': {'add'},
+            'replaces': [
+                '(target = "ldap:///ipauniqueid=*,cn=caacls,cn=ca,$SUFFIX";)(version 3.0;acl "permission:Add CA ACL";allow (add) groupdn = "ldap:///cn=Add CA ACL,cn=permissions,cn=pbac,$SUFFIX";)',
+            ],
+            'default_privileges': {'CA Administrator'},
+        },
+        'System: Delete CA ACL': {
+            'ipapermright': {'delete'},
+            'replaces': [
+                '(target = "ldap:///ipauniqueid=*,cn=caacls,cn=ca,$SUFFIX";)(version 3.0;acl "permission:Delete CA ACL";allow (delete) groupdn = "ldap:///cn=Delete CA ACL,cn=permissions,cn=pbac,$SUFFIX";)',
+            ],
+            'default_privileges': {'CA Administrator'},
+        },
+        'System: Manage CA ACL Membership': {
+            'ipapermright': {'write'},
+            'ipapermdefaultattr': {
+                'ipacacategory', 'ipamemberca',
+                'ipacertprofilecategory', 'ipamembercertprofile',
+                'usercategory', 'memberuser',
+                'hostcategory', 'memberhost',
+                'servicecategory', 'memberservice'
+            },
+            'replaces': [
+                '(targetattr = "ipamemberca || ipamembercertprofile || memberuser || memberservice || memberhost || ipacacategory || ipacertprofilecategory || usercategory || hostcategory || servicecategory")(target = "ldap:///ipauniqueid=*,cn=caacls,cn=ca,$SUFFIX";)(version 3.0;acl "permission:Manage CA ACL membership";allow (write) groupdn = "ldap:///cn=Manage CA ACL membership,cn=permissions,cn=pbac,$SUFFIX";)',
+            ],
+            'default_privileges': {'CA Administrator'},
+        },
+        'System: Modify CA ACL': {
+            'ipapermright': {'write'},
+            'ipapermdefaultattr': {
+                'cn', 'description', 'ipaenabledflag',
+            },
+            'replaces': [
+                '(targetattr = "cn || description || ipaenabledflag")(target = "ldap:///ipauniqueid=*,cn=caacls,cn=ca,$SUFFIX";)(version 3.0;acl "permission:Modify CA ACL";allow (write) groupdn = "ldap:///cn=Modify CA ACL,cn=permissions,cn=pbac,$SUFFIX";)',
+            ],
+            'default_privileges': {'CA Administrator'},
+        },
+    }
+
+    label = _('CA ACLs')
+    label_singular = _('CA ACL')
+
+    takes_params = (
+        Str('cn',
+            cli_name='name',
+            label=_('ACL name'),
+            primary_key=True,
+        ),
+        Str('description?',
+            cli_name='desc',
+            label=_('Description'),
+        ),
+        Bool('ipaenabledflag?',
+             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('ipacertprofilecategory?',
+            cli_name='profilecat',
+            label=_('Profile category'),
+            doc=_('Profile category the ACL applies to'),
+            values=(u'all', ),
+        ),
+        StrEnum('usercategory?',
+            cli_name='usercat',
+            label=_('User category'),
+            doc=_('User category the ACL applies to'),
+            values=(u'all', ),
+        ),
+        StrEnum('hostcategory?',
+            cli_name='hostcat',
+            label=_('Host category'),
+            doc=_('Host category the ACL applies to'),
+            values=(u'all', ),
+        ),
+        StrEnum('servicecategory?',
+            cli_name='servicecat',
+            label=_('Service category'),
+            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('ipamembercertprofile_certprofile?',
+            label=_('Profiles'),
+            flags=['no_create', 'no_update', 'no_search'],
+        ),
+        Str('memberuser_user?',
+            label=_('Users'),
+            flags=['no_create', 'no_update', 'no_search'],
+        ),
+        Str('memberuser_group?',
+            label=_('User Groups'),
+            flags=['no_create', 'no_update', 'no_search'],
+        ),
+        Str('memberhost_host?',
+            label=_('Hosts'),
+            flags=['no_create', 'no_update', 'no_search'],
+        ),
+        Str('memberhost_hostgroup?',
+            label=_('Host Groups'),
+            flags=['no_create', 'no_update', 'no_search'],
+        ),
+        Str('memberservice_service?',
+            label=_('Services'),
+            flags=['no_create', 'no_update', 'no_search'],
+        ),
+    )
+
+
+@register()
+class caacl_add(LDAPCreate):
+    __doc__ = _('Create a new CA ACL.')
+
+    msg_summary = _('Added CA ACL "%(value)s"')
+
+    def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
+        # CA ACLs are enabled by default
+        entry_attrs['ipaenabledflag'] = ['TRUE']
+        return dn
+
+
+@register()
+class caacl_del(LDAPDelete):
+    __doc__ = _('Delete a CA ACL.')
+
+    msg_summary = _('Deleted CA ACL "%(value)s"')
+
+
+@register()
+class caacl_mod(LDAPUpdate):
+    __doc__ = _('Modify a CA ACL.')
+
+    msg_summary = _('Modified CA ACL "%(value)s"')
+
+    def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
+        assert isinstance(dn, DN)
+        try:
+            entry_attrs = ldap.get_entry(dn, attrs_list)
+            dn = entry_attrs.dn
+        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, 'ipacertprofilecategory')
+                and 'ipamembercertprofile' in entry_attrs):
+            raise errors.MutuallyExclusiveError(reason=_(
+                "profile category cannot be set to 'all' "
+                "while there are allowed profiles"))
+        if is_all(options, 'usercategory') and 'memberuser' in entry_attrs:
+            raise errors.MutuallyExclusiveError(reason=_(
+                "user category cannot be set to 'all' "
+                "while there are allowed users"))
+        if is_all(options, 'hostcategory') and 'memberhost' in entry_attrs:
+            raise errors.MutuallyExclusiveError(reason=_(
+                "host category cannot be set to 'all' "
+                "while there are allowed hosts"))
+        if is_all(options, 'servicecategory') and 'memberservice' in entry_attrs:
+            raise errors.MutuallyExclusiveError(reason=_(
+                "service category cannot be set to 'all' "
+                "while there are allowed services"))
+        return dn
+
+
+@register()
+class caacl_find(LDAPSearch):
+    __doc__ = _('Search for CA ACLs.')
+
+    msg_summary = ngettext(
+        '%(count)d CA ACL matched', '%(count)d CA ACLs matched', 0
+    )
+
+
+@register()
+class caacl_show(LDAPRetrieve):
+    __doc__ = _('Display the properties of a CA ACL.')
+
+
+@register()
+class caacl_enable(LDAPQuery):
+    __doc__ = _('Enable a CA ACL.')
+
+    msg_summary = _('Enabled CA ACL "%(value)s"')
+    has_output = output.standard_value
+
+    def execute(self, cn, **options):
+        ldap = self.obj.backend
+
+        dn = self.obj.get_dn(cn)
+        try:
+            entry_attrs = ldap.get_entry(dn, ['ipaenabledflag'])
+        except errors.NotFound:
+            self.obj.handle_not_found(cn)
+
+        entry_attrs['ipaenabledflag'] = ['TRUE']
+
+        try:
+            ldap.update_entry(entry_attrs)
+        except errors.EmptyModlist:
+            pass
+
+        return dict(
+            result=True,
+            value=pkey_to_value(cn, options),
+        )
+
+
+@register()
+class caacl_disable(LDAPQuery):
+    __doc__ = _('Disable a CA ACL.')
+
+    msg_summary = _('Disabled CA ACL "%(value)s"')
+    has_output = output.standard_value
+
+    def execute(self, cn, **options):
+        ldap = self.obj.backend
+
+        dn = self.obj.get_dn(cn)
+        try:
+            entry_attrs = ldap.get_entry(dn, ['ipaenabledflag'])
+        except errors.NotFound:
+            self.obj.handle_not_found(cn)
+
+        entry_attrs['ipaenabledflag'] = ['FALSE']
+
+        try:
+            ldap.update_entry(entry_attrs)
+        except errors.EmptyModlist:
+            pass
+
+        return dict(
+            result=True,
+            value=pkey_to_value(cn, options),
+        )
+
+
+@register()
+class caacl_add_user(LDAPAddMember):
+    __doc__ = _('Add users and groups to a CA ACL.')
+
+    member_attributes = ['memberuser']
+    member_count_out = (
+        _('%i user or group added.'),
+        _('%i users or groups 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, 'usercategory'):
+            raise errors.MutuallyExclusiveError(
+                reason=_("users cannot be added when user category='all'"))
+        return dn
+
+
+@register()
+class caacl_remove_user(LDAPRemoveMember):
+    __doc__ = _('Remove users and groups from a CA ACL.')
+
+    member_attributes = ['memberuser']
+    member_count_out = (
+        _('%i user or group removed.'),
+        _('%i users or groups removed.'))
+
+
+@register()
+class caacl_add_host(LDAPAddMember):
+    __doc__ = _('Add target hosts and hostgroups to a CA ACL.')
+
+    member_attributes = ['memberhost']
+    member_count_out = (
+        _('%i host or hostgroup added.'),
+        _('%i hosts or hostgroups 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, 'hostcategory'):
+            raise errors.MutuallyExclusiveError(
+                reason=_("hosts cannot be added when host category='all'"))
+        return dn
+
+
+@register()
+class caacl_remove_host(LDAPRemoveMember):
+    __doc__ = _('Remove target hosts and hostgroups from a CA ACL.')
+
+    member_attributes = ['memberhost']
+    member_count_out = (
+        _('%i host or hostgroup removed.'),
+        _('%i hosts or hostgroups removed.'))
+
+
+@register()
+class caacl_add_service(LDAPAddMember):
+    __doc__ = _('Add services to a CA ACL.')
+
+    member_attributes = ['memberservice']
+    member_count_out = (_('%i service added.'), _('%i services 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, 'servicecategory'):
+            raise errors.MutuallyExclusiveError(reason=_(
+                "services cannot be added when service category='all'"))
+        return dn
+
+
+@register()
+class caacl_remove_service(LDAPRemoveMember):
+    __doc__ = _('Remove services from a CA ACL.')
+
+    member_attributes = ['memberservice']
+    member_count_out = (_('%i service removed.'), _('%i services removed.'))
+
+
+caacl_output_params = global_output_params + (
+    Str('ipamembercertprofile',
+        label=_('Failed profiles'),
+    ),
+    # Commented until caacl plugin arrives
+    #Str('ipamemberca',
+    #    label=_('Failed CAs'),
+    #),
+)
+
+
+@register()
+class caacl_add_profile(LDAPAddMember):
+    __doc__ = _('Add profiles to a CA ACL.')
+
+    has_output_params = caacl_output_params
+
+    member_attributes = ['ipamembercertprofile']
+    member_count_out = (_('%i profile added.'), _('%i profiles 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, 'ipacertprofilecategory'):
+            raise errors.MutuallyExclusiveError(reason=_(
+                "profiles cannot be added when profile category='all'"))
+        return dn
+
+
+@register()
+class caacl_remove_profile(LDAPRemoveMember):
+    __doc__ = _('Remove profiles from a CA ACL.')
+
+    has_output_params = caacl_output_params
+
+    member_attributes = ['ipamembercertprofile']
+    member_count_out = (_('%i profile removed.'), _('%i profiles removed.'))
diff --git a/ipaserver/install/dsinstance.py b/ipaserver/install/dsinstance.py
index 2acab13f247ed18a750f0e1cbbd98f4e63718c03..9f24189b6e442e0c55d5de41d15a03f89ecc9578 100644
--- a/ipaserver/install/dsinstance.py
+++ b/ipaserver/install/dsinstance.py
@@ -307,6 +307,7 @@ class DsInstance(service.Service):
         self.step("adding range check plugin", self.__add_range_check_plugin)
         if hbac_allow:
             self.step("creating default HBAC rule allow_all", self.add_hbac)
+        self.step("creating default CA ACL rule", self.add_caacl)
         self.step("adding entries for topology management", self.__add_topology_entries)
 
         self.__common_post_setup()
@@ -741,6 +742,9 @@ class DsInstance(service.Service):
     def add_hbac(self):
         self._ldap_mod("default-hbac.ldif", self.sub_dict)
 
+    def add_caacl(self):
+        self._ldap_mod("default-caacl.ldif", self.sub_dict)
+
     def change_admin_password(self, password):
         root_logger.debug("Changing admin password")
         dirname = config_dirname(self.serverid)
diff --git a/ipaserver/install/server/upgrade.py b/ipaserver/install/server/upgrade.py
index c5f4d37cc02658334d5c26f269ec5dd5e386df1d..306d1d27cda7a517117110ad3e6a760108f0fe19 100644
--- a/ipaserver/install/server/upgrade.py
+++ b/ipaserver/install/server/upgrade.py
@@ -1254,6 +1254,30 @@ def update_mod_nss_protocol(http):
     sysupgrade.set_upgrade_state('nss.conf', 'protocol_updated_tls12', True)
 
 
+def add_default_caacl(ca):
+    root_logger.info('[Add default CA ACL]')
+
+    if sysupgrade.get_upgrade_state('caacl', 'add_default_caacl'):
+        root_logger.info('Default CA ACL already added')
+        return
+
+    if ca.is_configured():
+        if not api.Backend.ldap2.isconnected():
+            try:
+                api.Backend.ldap2.connect(autobind=True)
+            except ipalib.errors.PublicError as e:
+                root_logger.error("Cannot connect to LDAP to add CA ACLs: %s", e)
+                return
+
+        if not api.Command.caacl_find()['result']:
+            api.Command.caacl_add(u'hosts_services_caIPAserviceCert',
+                hostcategory=u'all', usercategory=u'all')
+            api.Command.caacl_add_profile(u'hosts_services_caIPAserviceCert',
+                certprofile=(u'caIPAserviceCert',))
+
+    sysupgrade.set_upgrade_state('caacl', 'add_default_caacl', True)
+
+
 def upgrade_configuration():
     """
     Execute configuration upgrade of the IPA services
@@ -1431,6 +1455,7 @@ def upgrade_configuration():
     # itself require a restart.
     #
     ca_import_included_profiles(ca)
+    add_default_caacl(ca)
 
     set_sssd_domain_option('ipa_server_mode', 'True')
 
-- 
2.1.0

From cd635ba2d1061766ebac46cc0a77a36b2edcc4e6 Mon Sep 17 00:00:00 2001
From: Fraser Tweedale <ftwee...@redhat.com>
Date: Tue, 26 May 2015 04:44:20 -0400
Subject: [PATCH 2/2] Enforce CA ACLs in cert-request command

This commit adds CA ACL enforcement to the cert-request command and
uses the pyhbac machinery.

It is planned to implement ACL enforcement in Dogtag in a future
release, and remove certificate issuance privileges and CA ACL
enforcement responsibility from the framework.  See
https://fedorahosted.org/freeipa/ticket/5011 for more information.

Part of: https://fedorahosted.org/freeipa/ticket/57
Part of: https://fedorahosted.org/freeipa/ticket/4559
---
 ipalib/plugins/caacl.py | 76 +++++++++++++++++++++++++++++++++++++++++++++++++
 ipalib/plugins/cert.py  | 17 +++++++++++
 2 files changed, 93 insertions(+)

diff --git a/ipalib/plugins/caacl.py b/ipalib/plugins/caacl.py
index f0dc9ae35896ae2b818847693812e68c62749e4a..6bf39d2330c8999726484e1e9fb44fdb7c755767 100644
--- a/ipalib/plugins/caacl.py
+++ b/ipalib/plugins/caacl.py
@@ -2,6 +2,8 @@
 # Copyright (C) 2015  FreeIPA Contributors see COPYING for license
 #
 
+import pyhbac
+
 from ipalib import api, errors, output
 from ipalib import Bool, Str, StrEnum
 from ipalib.plugable import Registry
@@ -10,6 +12,7 @@ from ipalib.plugins.baseldap import (
     LDAPUpdate, LDAPRetrieve, LDAPAddMember, LDAPRemoveMember,
     global_output_params, pkey_to_value)
 from ipalib.plugins.hbacrule import is_all
+from ipalib.plugins.service import normalize_principal, split_any_principal
 from ipalib import _, ngettext
 from ipapython.dn import DN
 
@@ -50,6 +53,79 @@ EXAMPLES:
 register = Registry()
 
 
+def _acl_make_request(principal_type, principal, ca_ref, profile_id):
+    """Construct HBAC request for the given principal, CA and profile"""
+    req = pyhbac.HbacRequest()
+    req.targethost.name = ca_ref
+    req.service.name = profile_id
+    if principal_type == 'user':
+        req.user.name = principal
+    elif principal_type == 'host':
+        req.user.name = principal[:5]  # strip 'host/'
+    elif principal_type == 'service':
+        req.user.name = normalize_principal(principal)
+    groups = []
+    if principal_type == 'user':
+        user_obj = api.Command.user_show(principal)['result']
+        groups = user_obj.get('memberof_group', [])
+        groups += user_obj.get('memberofindirect_group', [])
+    elif principal_type == 'host':
+        service, hostname, realm = split_any_principal(principal)
+        host_obj = api.Command.host_show(hostname)['result']
+        groups = host_obj.get('memberof_hostgroup', [])
+        groups += host_obj.get('memberofindirect_hostgroup', [])
+    req.user.groups = sorted(set(groups))
+    return req
+
+
+def _acl_make_rule(principal_type, obj):
+    """Turn CA ACL object into HBAC rule.
+
+    ``principal_type``
+        String in {'user', 'host', 'service'}
+    """
+    rule = pyhbac.HbacRule(obj['cn'][0])
+    rule.enabled = obj['ipaenabledflag'][0]
+    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', [])
+
+    # add profiles
+    if ('ipacertprofilecategory' in obj
+            and obj['ipacertprofilecategory'][0].lower() == 'all'):
+        rule.services.category = {pyhbac.HBAC_CATEGORY_ALL}
+    else:
+        attr = 'ipamembercertprofile_certprofile'
+        rule.services.names = obj.get(attr, [])
+
+    # add principals and principal's groups
+    m = {'user': 'group', 'host': 'hostgroup', 'service': None}
+    category_attr = '{}category'.format(principal_type)
+    if category_attr in obj and obj[category_attr][0].lower() == 'all':
+        rule.users.category = {pyhbac.HBAC_CATEGORY_ALL}
+    else:
+        principal_attr = 'member{}_{}'.format(principal_type, principal_type)
+        rule.users.names = obj.get(principal_attr, [])
+        if m[principal_type] is not None:
+            group_attr = 'member{}_{}'.format(principal_type, m[principal_type])
+            rule.users.groups = obj.get(group_attr, [])
+
+    return rule
+
+
+def acl_evaluate(principal_type, principal, ca_ref, profile_id):
+    req = _acl_make_request(principal_type, principal, ca_ref, profile_id)
+    acls = api.Command.caacl_find()['result']
+    rules = [_acl_make_rule(principal_type, obj) for obj in acls]
+    return req.evaluate(rules) == pyhbac.HBAC_EVAL_ALLOW
+
+
 @register()
 class caacl(LDAPObject):
     """
diff --git a/ipalib/plugins/cert.py b/ipalib/plugins/cert.py
index d122900175db41ba5af429fd47af6cac6533cb6f..1878e5ad5f80fa93e1a77b0a88711c1da0016681 100644
--- a/ipalib/plugins/cert.py
+++ b/ipalib/plugins/cert.py
@@ -33,6 +33,7 @@ from ipalib.plugins.virtual import *
 from ipalib.plugins.baseldap import pkey_to_value
 from ipalib.plugins.service import split_any_principal
 from ipalib.plugins.certprofile import validate_profile_id
+import ipalib.plugins.caacl
 import base64
 import traceback
 from ipalib.text import _
@@ -326,6 +327,22 @@ class cert_request(VirtualCommand):
         else:
             principal_type = SERVICE
 
+        principal_type_map = {USER: 'user', HOST: 'host', SERVICE: 'service'}
+        ca = '.'  # top-level CA hardcoded until subca plugin implemented
+        if not ipalib.plugins.caacl.acl_evaluate(
+                principal_type_map[principal_type],
+                principal_string, ca, profile_id):
+            raise errors.ACIError(info=_(
+                    "Principal '%(principal)s' "
+                    "is not permitted to use CA '%(ca)s' "
+                    "with profile '%(profile_id)s' for certificate issuance."
+                ) % dict(
+                    principal=principal_string,
+                    ca=ca or '.',
+                    profile_id=profile_id
+                )
+            )
+
         bind_principal = split_any_principal(getattr(context, 'principal'))
         bind_service, bind_name, bind_realm = bind_principal
 
-- 
2.1.0

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