Hello list,

Patch for: https://fedorahosted.org/freeipa/ticket/4419

Before I start any work on Web UI and tests I would like to gather feedback on:
- the new API
- member attributes with subtypes management approach
- ACI

I did not do any ACI work in the patch yet. I assume that we would like to add the attr into 'System: Read Host|Service' permission. But I think that write right should have it's own permission.

Patch info:
Adds new API:
  ipa host-add-retrieve-keytab HOSTNAME --users=STR --groups=STR
  ipa host-add-write-keytab HOSTNAME --users=STR --groups=STR
  ipa host-remove-retrieve-keytab HOSTNAME --users=STR --groups=STR
  ipa host-remove-write-keytab HOSTNAME --users=STR --groups=STR

  ipa service-add-retrieve-keytab PRINCIPAL --users=STR --groups=STR
  ipa service-add-write-keytab PRINCIPAL --users=STR --groups=STR
  ipa service-remove-retrieve-keytab PRINCIPAL --users=STR --groups=STR
  ipa service-remove-write-keytab PRINCIPAL --users=STR --groups=STR

these methods add or remove user or group DNs in `ipaallowedtoperform` attr with `read_keys` and `write_keys` subtypes.

service|host-mod|show outputs these attrs only with --all option as:

  Users allowed to retrieve keytab: user1
  Groups allowed to retrieve keytab: group1
  Users allowed to write keytab: user1
  Groups allowed to write keytab: group1

1) This patch implements subtypes support for attributes members. It's done to be relatively reusable but it's confined within the RFE boundaries. I.e. it does not contain support for standard attributes or is not integrated into LDAPAddMember or LDAPRemoveMember commands. It's rather as separate opt-ins. One of the reasons was also not to disturb existing code at the end of 4-1
milestone.

2) I tried to keep the command names or attr label short, but they are still long like a novel. Any shorter recommendations are welcome.

3) Adding of object class is implemented as a reusable method since this code is used on many places and most likely will be also used in new features. Older code may be refactored later.

Thanks
--
Petr Vobornik
From a39f57373649abd9e9ac16a2ba3ab685ed4d9bad Mon Sep 17 00:00:00 2001
From: Petr Vobornik <pvobo...@redhat.com>
Date: Wed, 24 Sep 2014 16:21:47 +0200
Subject: [PATCH] keytab manipulation permission management

Adds new API:
  ipa host-add-retrieve-keytab HOSTNAME --users=STR --groups=STR
  ipa host-add-write-keytab HOSTNAME --users=STR --groups=STR
  ipa host-remove-retrieve-keytab HOSTNAME --users=STR --groups=STR
  ipa host-remove-write-keytab HOSTNAME --users=STR --groups=STR

  ipa service-add-retrieve-keytab PRINCIPAL --users=STR --groups=STR
  ipa service-add-write-keytab PRINCIPAL --users=STR --groups=STR
  ipa service-remove-retrieve-keytab PRINCIPAL --users=STR --groups=STR
  ipa service-remove-write-keytab PRINCIPAL --users=STR --groups=STR

these methods add or remove user or group DNs in `ipaallowedtoperform` attr with
`read_keys` and `write_keys` subtypes.

service|host-mod|show outputs these attrs only with --all option as:

  Users allowed to retrieve keytab: user1
  Groups allowed to retrieve keytab: group1
  Users allowed to write keytab: user1
  Groups allowed to write keytab: group1

1) This patch implements subtypes support for attributes members. It's done to
be relatively reusable but it's confined within the RFE boundaries. I.e. it
does not contain support for standard attributes or is not integrated into
LDAPAddMember or LDAPRemoveMember commands. It's rather as separate opt-ins.
One of the reasons was also not to disturb existing code at the end of 4-1
milestone.

2) I tried to keep the command names or attr label short, but they are still
long like a novel. Any shorter recommendations are welcome.

3) Adding of object class is implemented as a reusable method since this code is
used on many places and most likely will be also used in new features. Older
code may be refactored later.

https://fedorahosted.org/freeipa/ticket/4419
---
 API.txt                    | 96 ++++++++++++++++++++++++++++++++++++++++++++++
 VERSION                    |  4 +-
 ipalib/plugins/baseldap.py | 88 ++++++++++++++++++++++++++++++++++++++++++
 ipalib/plugins/host.py     | 71 ++++++++++++++++++++++++++++++++--
 ipalib/plugins/service.py  | 72 +++++++++++++++++++++++++++++++---
 5 files changed, 321 insertions(+), 10 deletions(-)

diff --git a/API.txt b/API.txt
index c5e76c759ec2cb66af2fecd451ca9fa5d1ec9959..54bfc691160709aa27e9bf4b5832e39cdf8d4a8a 100644
--- a/API.txt
+++ b/API.txt
@@ -1819,6 +1819,30 @@ 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: host_add_retrieve_keytab
+args: 1,6,3
+arg: Str('fqdn', attribute=True, cli_name='hostname', 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: host_add_write_keytab
+args: 1,6,3
+arg: Str('fqdn', attribute=True, cli_name='hostname', 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: host_del
 args: 1,3,3
 arg: Str('fqdn', attribute=True, cli_name='hostname', multivalue=True, primary_key=True, query=True, required=True)
@@ -1917,6 +1941,30 @@ 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: host_remove_retrieve_keytab
+args: 1,6,3
+arg: Str('fqdn', attribute=True, cli_name='hostname', 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: host_remove_write_keytab
+args: 1,6,3
+arg: Str('fqdn', attribute=True, cli_name='hostname', 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: host_show
 args: 1,6,3
 arg: Str('fqdn', attribute=True, cli_name='hostname', multivalue=False, primary_key=True, query=True, required=True)
@@ -3462,6 +3510,30 @@ 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: service_add_retrieve_keytab
+args: 1,6,3
+arg: Str('krbprincipalname', attribute=True, cli_name='principal', 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: service_add_write_keytab
+args: 1,6,3
+arg: Str('krbprincipalname', attribute=True, cli_name='principal', 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: service_del
 args: 1,2,3
 arg: Str('krbprincipalname', attribute=True, cli_name='principal', multivalue=True, primary_key=True, query=True, required=True)
@@ -3524,6 +3596,30 @@ 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: service_remove_retrieve_keytab
+args: 1,6,3
+arg: Str('krbprincipalname', attribute=True, cli_name='principal', 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: service_remove_write_keytab
+args: 1,6,3
+arg: Str('krbprincipalname', attribute=True, cli_name='principal', 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: service_show
 args: 1,6,3
 arg: Str('krbprincipalname', attribute=True, cli_name='principal', multivalue=False, primary_key=True, query=True, required=True)
diff --git a/VERSION b/VERSION
index efd4f615d190ee2e0ed9ca1b310c4fef0cf87adc..217ff574273b0647c35b120835f84d3a11b6127a 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=104
-# Last change: mbasti - autofill --admin-email in DNS zone
+IPA_API_VERSION_MINOR=105
+# Last change: pvoborni - manage authorization of keytab operations
diff --git a/ipalib/plugins/baseldap.py b/ipalib/plugins/baseldap.py
index e589a53219766a2aa41bbb4abef7fc3a12fcc267..4b7603dcb2d839124f9341732785ff493be464c4 100644
--- a/ipalib/plugins/baseldap.py
+++ b/ipalib/plugins/baseldap.py
@@ -315,6 +315,51 @@ def validate_externalhost(ugettext, hostname):
         return unicode(e)
 
 
+def set_subtype_members(obj, entry_attrs, *keys, **options):
+    """
+    Transforms members with subtype into regular attrs.
+
+    Format: '$attr_$subtype_$object'
+
+    E.g.'ipaallowedoperations;read_key' which can contain DN of a group or a user
+    into: 'ipaallowedoperations_read_key_group' and
+    'ipaallowedoperations_read_key_user'
+    """
+
+    if options.get('raw', False):
+        return
+
+    for (attr, label) in obj.attr_subtypes:
+        key = '%s;%s' % (attr, label)
+        if key not in entry_attrs:
+            continue
+        for member in entry_attrs[key]:
+            for ldap_obj_name in obj.attribute_members[attr]:
+                ldap_obj = obj.api.Object[ldap_obj_name]
+                container_dn = DN(ldap_obj.container_dn, api.env.basedn)
+                if member.endswith(container_dn):
+                    new_attr = '%s_%s_%s' % (attr, label, ldap_obj.name)
+                    entry_attrs.setdefault(new_attr, []).append(
+                        ldap_obj.get_primary_key_from_dn(member)
+                    )
+        del entry_attrs[key]
+
+
+def add_member_subtypes(self, attr_subtypes, member_dns, failed):
+    """
+    Add subtypes to members
+
+    Works with output of `get_member_dns`.
+    """
+
+    for (attr, label) in attr_subtypes:
+        key = '%s;%s' % (attr, label)
+        member_dns[key] = member_dns[attr]
+        failed[key] = failed[attr]
+        del member_dns[attr]
+        del failed[attr]
+
+
 external_host_param = Str('externalhost*', validate_externalhost,
         label=_('External host'),
         flags=['no_option'],
@@ -494,6 +539,23 @@ def host_is_master(ldap, fqdn):
         return
 
 
+def add_missing_object_class(ldap, objectclass, dn, entry_attrs=None, update=True):
+    """
+    Add object class if missing into entry. Fetches entry if not passed. Updates
+    the entry by default.
+
+    Returns the entry
+    """
+
+    if not entry_attrs:
+        entry_attrs = ldap.get_entry(dn, ['objectclass'])
+    if (objectclass.lower() not in (o.lower() for o in entry_attrs['objectclass'])):
+        entry_attrs['objectclass'].append(objectclass)
+        if update:
+            ldap.update_entry(entry_attrs)
+    return entry_attrs
+
+
 class LDAPObject(Object):
     """
     Object representing a LDAP entry.
@@ -2254,3 +2316,29 @@ class LDAPRemoveReverseMember(LDAPModReverseMember):
 
     def interactive_prompt_callback(self, kw):
         return
+
+
+class LDAPAddSubtypedMember(LDAPAddMember):
+    attr_subtypes = []
+
+    def get_member_dns(self, **options):
+        (member_dns, failed) = super(LDAPAddSubtypedMember, self).get_member_dns(**options)
+        add_member_subtypes(self, self.attr_subtypes, member_dns, failed)
+        return (member_dns, failed)
+
+    def post_callback(self, ldap, completed, failed, dn, entry_attrs, *keys, **options):
+        set_subtype_members(self.obj, entry_attrs, *keys, **options)
+        return (completed, dn)
+
+
+class LDAPRemoveSubtypedMember(LDAPRemoveMember):
+    attr_subtypes = []
+
+    def get_member_dns(self, **options):
+        (member_dns, failed) = super(LDAPRemoveSubtypedMember, self).get_member_dns(**options)
+        add_member_subtypes(self, self.attr_subtypes, member_dns, failed)
+        return (member_dns, failed)
+
+    def post_callback(self, ldap, completed, failed, dn, entry_attrs, *keys, **options):
+        set_subtype_members(self.obj, entry_attrs, *keys, **options)
+        return (completed, dn)
diff --git a/ipalib/plugins/host.py b/ipalib/plugins/host.py
index bbee09395dc55730d400944729a5bed48670f785..33b37362d65e006d155ee3c7bcb635905181ca55 100644
--- a/ipalib/plugins/host.py
+++ b/ipalib/plugins/host.py
@@ -27,8 +27,10 @@ from ipalib.plugable import Registry
 from ipalib.plugins.baseldap import (LDAPQuery, LDAPObject, LDAPCreate,
                                      LDAPDelete, LDAPUpdate, LDAPSearch,
                                      LDAPRetrieve, LDAPAddMember,
-                                     LDAPRemoveMember, host_is_master,
-                                     pkey_to_value)
+                                     LDAPRemoveMember, LDAPAddSubtypedMember,
+                                     LDAPRemoveSubtypedMember,
+                                     add_missing_object_class, host_is_master,
+                                     set_subtype_members, pkey_to_value)
 from ipalib.plugins.service import (split_principal, validate_certificate,
     set_certificate_attrs, ticket_flags_params, update_krbticketflags,
     set_kerberos_attrs)
@@ -102,6 +104,9 @@ EXAMPLES:
 """) + _("""
  Add a host that can manage this host's keytab and certificate:
    ipa host-add-managedby --hosts=test2 test
+""") + _("""
+ Allow user to create keytab:
+   ipa host-add-write-keytab test2 --users=tuser1
 """)
 
 register = Registry()
@@ -201,6 +206,18 @@ host_output_params = (
     Str('sshpubkeyfp*',
         label=_('SSH public key fingerprint'),
     ),
+    Str('ipaallowedtoperform_read_keys_user',
+        label=_('Users allowed to retrieve keytab'),
+    ),
+    Str('ipaallowedtoperform_read_keys_group',
+        label=_('Groups allowed to retrieve keytab'),
+    ),
+    Str('ipaallowedtoperform_write_keys_user',
+        label=_('Users allowed to write keytab'),
+    ),
+    Str('ipaallowedtoperform_write_keys_group',
+        label=_('Groups allowed to write keytab'),
+    ),
 )
 
 
@@ -241,11 +258,12 @@ class host(LDAPObject):
     object_name = _('host')
     object_name_plural = _('hosts')
     object_class = ['ipaobject', 'nshost', 'ipahost', 'pkiuser', 'ipaservice']
+    possible_objectclasses = ['ipaallowedoperations']
     permission_filter_objectclasses = ['ipahost']
     # object_class_config = 'ipahostobjectclasses'
     search_attributes = [
         'fqdn', 'description', 'l', 'nshostlocation', 'krbprincipalname',
-        'nshardwareplatform', 'nsosversion', 'managedby'
+        'nshardwareplatform', 'nsosversion', 'managedby', 'ipaallowedtoperform'
     ]
     default_attributes = [
         'fqdn', 'description', 'l', 'nshostlocation', 'krbprincipalname',
@@ -261,13 +279,19 @@ class host(LDAPObject):
         'managing': ['host'],
         'memberofindirect': ['hostgroup', 'netgroup', 'role', 'hbacrule',
         'sudorule'],
+        'ipaallowedtoperform': ['user', 'group'],
     }
+    attr_subtypes = [
+        ('ipaallowedtoperform', 'read_keys'),
+        ('ipaallowedtoperform', 'write_keys')
+    ]
     bindable = True
     relationships = {
         'memberof': ('Member Of', 'in_', 'not_in_'),
         'enrolledby': ('Enrolled by', 'enroll_by_', 'not_enroll_by_'),
         'managedby': ('Managed by', 'man_by_', 'not_man_by_'),
         'managing': ('Managing', 'man_', 'not_man_'),
+        'ipaallowedtoperform': ('Allowed to perform by', 'allowed_to_', 'not_allowed_to_'),
     }
     password_attributes = [('userpassword', 'has_password'),
                            ('krbprincipalkey', 'has_keytab')]
@@ -888,6 +912,7 @@ class host_mod(LDAPUpdate):
             entry_attrs['randompassword'] = unicode(getattr(context, 'randompassword'))
         set_certificate_attrs(entry_attrs)
         set_kerberos_attrs(entry_attrs, options)
+        set_subtype_members(self.obj, entry_attrs, *keys, **options)
         self.obj.get_password_attributes(ldap, dn, entry_attrs)
         if entry_attrs['has_password']:
             # If an OTP is set there is no keytab, at least not one
@@ -1012,6 +1037,7 @@ class host_show(LDAPRetrieve):
 
         set_certificate_attrs(entry_attrs)
         set_kerberos_attrs(entry_attrs, options)
+        set_subtype_members(self.obj, entry_attrs, *keys, **options)
 
         if options.get('all', False):
             entry_attrs['managing'] = self.obj.get_managed_hosts(dn)
@@ -1159,3 +1185,42 @@ class host_remove_managedby(LDAPRemoveMember):
         self.obj.suppress_netgroup_memberof(ldap, entry_attrs)
         return (completed, dn)
 
+
+@register()
+class host_add_retrieve_keytab(LDAPAddSubtypedMember):
+    __doc__ = _('Add users or groups who can retrieve keytab of this host.')
+    attr_subtypes = [('ipaallowedtoperform', 'read_keys')]
+    member_attributes = ['ipaallowedtoperform']
+    has_output_params = LDAPAddSubtypedMember.has_output_params + host_output_params
+
+    def pre_callback(self, ldap, dn, found, not_found, *keys, **options):
+        add_missing_object_class(ldap, u'ipaallowedoperations', dn)
+        return dn
+
+
+@register()
+class host_remove_retrieve_keytab(LDAPRemoveSubtypedMember):
+    __doc__ = _('Remove users or groups who can retrieve keytab of this host.')
+    attr_subtypes = [('ipaallowedtoperform', 'read_keys')]
+    member_attributes = ['ipaallowedtoperform']
+    has_output_params = LDAPRemoveSubtypedMember.has_output_params + host_output_params
+
+
+@register()
+class host_add_write_keytab(LDAPAddSubtypedMember):
+    __doc__ = _('Add users or groups who can create a keytab of this host.')
+    attr_subtypes = [('ipaallowedtoperform', 'write_keys')]
+    member_attributes = ['ipaallowedtoperform']
+    has_output_params = LDAPAddSubtypedMember.has_output_params + host_output_params
+
+    def pre_callback(self, ldap, dn, found, not_found, *keys, **options):
+        add_missing_object_class(ldap, u'ipaallowedoperations', dn)
+        return dn
+
+
+@register()
+class host_remove_write_keytab(LDAPRemoveSubtypedMember):
+    __doc__ = _('Remove users or groups who can create a keytab of this host.')
+    attr_subtypes = [('ipaallowedtoperform', 'write_keys')]
+    member_attributes = ['ipaallowedtoperform']
+    has_output_params = LDAPRemoveSubtypedMember.has_output_params + host_output_params
diff --git a/ipalib/plugins/service.py b/ipalib/plugins/service.py
index 3ca5066f300a004c993660e0a5e220c364e38cbf..519db9364985702caf9c1fcc0be95b9f72010ad9 100644
--- a/ipalib/plugins/service.py
+++ b/ipalib/plugins/service.py
@@ -86,7 +86,10 @@ EXAMPLES:
 
  Request a certificate for an IPA service:
    ipa cert-request --principal=HTTP/web.example.com example.csr
-
+""") + _("""
+ Allow user to create keytab:
+   ipa service-add-write-keytab HTTP/web.example.com --users=tuser1
+""") + _("""
  Generate and retrieve a keytab for an IPA service:
    ipa-getkeytab -s ipa.example.com -p HTTP/web.example.com -k /etc/httpd/httpd.keytab
 
@@ -127,7 +130,19 @@ output_params = (
     ),
     Str('revocation_reason?',
         label=_('Revocation reason'),
-    )
+    ),
+    Str('ipaallowedtoperform_read_keys_user',
+        label=_('Users allowed to retrieve keytab'),
+    ),
+    Str('ipaallowedtoperform_read_keys_group',
+        label=_('Groups allowed to retrieve keytab'),
+    ),
+    Str('ipaallowedtoperform_write_keys_user',
+        label=_('Users allowed to write keytab'),
+    ),
+    Str('ipaallowedtoperform_write_keys_group',
+        label=_('Groups allowed to write keytab'),
+    ),
 )
 
 ticket_flags_params = (
@@ -302,19 +317,26 @@ class service(LDAPObject):
         'krbprincipal', 'krbprincipalaux', 'krbticketpolicyaux', 'ipaobject',
         'ipaservice', 'pkiuser'
     ]
-    possible_objectclasses = ['ipakrbprincipal']
+    possible_objectclasses = ['ipakrbprincipal', 'ipaallowedoperations']
     permission_filter_objectclasses = ['ipaservice']
-    search_attributes = ['krbprincipalname', 'managedby', 'ipakrbauthzdata']
+    search_attributes = ['krbprincipalname', 'managedby', 'ipakrbauthzdata',
+        'ipaallowedtoperform']
     default_attributes = ['krbprincipalname', 'usercertificate', 'managedby',
         'ipakrbauthzdata', 'memberof']
     uuid_attribute = 'ipauniqueid'
     attribute_members = {
         'managedby': ['host'],
         'memberof': ['role'],
+        'ipaallowedtoperform': ['user', 'group'],
     }
+    attr_subtypes = [
+        ('ipaallowedtoperform', 'read_keys'),
+        ('ipaallowedtoperform', 'write_keys')
+    ]
     bindable = True
     relationships = {
         'managedby': ('Managed by', 'man_by_', 'not_man_by_'),
+        'ipaallowedtoperform': ('Allowed to perform by', 'allowed_to_', 'not_allowed_to_'),
     }
     password_attributes = [('krbprincipalkey', 'has_keytab')]
     managed_permissions = {
@@ -561,6 +583,7 @@ class service_mod(LDAPUpdate):
         assert isinstance(dn, DN)
         set_certificate_attrs(entry_attrs)
         set_kerberos_attrs(entry_attrs, options)
+        set_subtype_members(self.obj, entry_attrs, *keys, **options)
         return dn
 
 
@@ -620,6 +643,7 @@ class service_show(LDAPRetrieve):
 
         set_certificate_attrs(entry_attrs)
         set_kerberos_attrs(entry_attrs, options)
+        set_subtype_members(self.obj, entry_attrs, *keys, **options)
 
         return dn
 
@@ -645,7 +669,6 @@ class service_add_host(LDAPAddMember):
     has_output_params = LDAPAddMember.has_output_params + output_params
 
 
-
 @register()
 class service_remove_host(LDAPRemoveMember):
     __doc__ = _('Remove hosts that can manage this service.')
@@ -654,6 +677,45 @@ class service_remove_host(LDAPRemoveMember):
     has_output_params = LDAPRemoveMember.has_output_params + output_params
 
 
+@register()
+class service_add_retrieve_keytab(LDAPAddSubtypedMember):
+    __doc__ = _('Add users or groups who can retrieve keytab of this service.')
+    attr_subtypes = [('ipaallowedtoperform', 'read_keys')]
+    member_attributes = ['ipaallowedtoperform']
+    has_output_params = LDAPAddSubtypedMember.has_output_params + output_params
+
+    def pre_callback(self, ldap, dn, found, not_found, *keys, **options):
+        add_missing_object_class(ldap, u'ipaallowedoperations', dn)
+        return dn
+
+
+@register()
+class service_remove_retrieve_keytab(LDAPRemoveSubtypedMember):
+    __doc__ = _('Remove users or groups who can retrieve keytab of this service.')
+    attr_subtypes = [('ipaallowedtoperform', 'read_keys')]
+    member_attributes = ['ipaallowedtoperform']
+    has_output_params = LDAPRemoveSubtypedMember.has_output_params + output_params
+
+
+@register()
+class service_add_write_keytab(LDAPAddSubtypedMember):
+    __doc__ = _('Add users or groups who can create a keytab of this service.')
+    attr_subtypes = [('ipaallowedtoperform', 'write_keys')]
+    member_attributes = ['ipaallowedtoperform']
+    has_output_params = LDAPAddSubtypedMember.has_output_params + output_params
+
+    def pre_callback(self, ldap, dn, found, not_found, *keys, **options):
+        add_missing_object_class(ldap, u'ipaallowedoperations', dn)
+        return dn
+
+
+@register()
+class service_remove_write_keytab(LDAPRemoveSubtypedMember):
+    __doc__ = _('Remove users or groups who can create a keytab of this service.')
+    attr_subtypes = [('ipaallowedtoperform', 'write_keys')]
+    member_attributes = ['ipaallowedtoperform']
+    has_output_params = LDAPRemoveSubtypedMember.has_output_params + output_params
+
 
 @register()
 class service_disable(LDAPQuery):
-- 
1.9.3

_______________________________________________
Freeipa-devel mailing list
Freeipa-devel@redhat.com
https://www.redhat.com/mailman/listinfo/freeipa-devel

Reply via email to