On 30.6.2016 16:25, David Kupka wrote:
On 28/04/16 14:45, Jan Cholasta wrote:
Hi,

I have pushed my thin client WIP branch to GitHub:
<https://github.com/jcholast/freeipa/tree/trac-4739>.

All commits up to "ipalib: use relative imports for cross-plugin
imports" should be good for review. The rest is subject to change
(WARNING: I will force push into this branch).

Honza


Hello!

Patch set:

server: exclude Local commands from RPC
client: add placeholders for required remote plugins
client: ignore override errors in command overrides
plugable: add option to ignore override errors
cert: fix CLI output of cert_remove_hold
frontend: do not ignore client-side output params
user: add object plugin for user_status
server: define missing virtual attributes

contains mostly fixes for some bugs discovered in thin client. Works for
me, ACK.

Thanks, pushed to master: 2beb72ffa4bea5e22c2ba4685a524df36d1f800c

Attaching the patches for reference.

--
Jan Cholasta
From d31aa24657f5fee5cc5498fe8e43fe3926d36f6f Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Thu, 30 Jun 2016 06:37:16 +0200
Subject: [PATCH 1/8] server: define missing virtual attributes

Move virtual attributes defined in output params of methods into params of
the related object.

This fixes the virtual attributes being ommited in CLI output.

https://fedorahosted.org/freeipa/ticket/4739
---
 ipaserver/plugins/aci.py         | 20 +++---------
 ipaserver/plugins/baseuser.py    |  7 ++--
 ipaserver/plugins/certprofile.py | 10 +++---
 ipaserver/plugins/delegation.py  | 14 +++-----
 ipaserver/plugins/dns.py         | 21 +++---------
 ipaserver/plugins/host.py        | 70 +++++++++++++++++++++++-----------------
 ipaserver/plugins/idviews.py     | 24 +++++++-------
 ipaserver/plugins/otptoken.py    |  8 ++---
 ipaserver/plugins/permission.py  | 15 +++------
 ipaserver/plugins/selfservice.py | 15 +++------
 ipaserver/plugins/service.py     | 63 ++++++++++++++++++++----------------
 ipaserver/plugins/trust.py       | 46 ++++++++++++++------------
 12 files changed, 147 insertions(+), 166 deletions(-)

diff --git a/ipaserver/plugins/aci.py b/ipaserver/plugins/aci.py
index dd14d82..5647827 100644
--- a/ipaserver/plugins/aci.py
+++ b/ipaserver/plugins/aci.py
@@ -507,6 +507,10 @@ class aci(Object):
              flags=('virtual_attribute',),
         ),
         _prefix_option,
+        Str('aci',
+            label=_('ACI'),
+            flags={'no_create', 'no_update', 'no_search'},
+        ),
     )
 
 
@@ -611,11 +615,6 @@ class aci_mod(crud.Update):
     Modify ACI.
     """
     NO_CLI = True
-    has_output_params = (
-        Str('aci',
-            label=_('ACI'),
-        ),
-    )
 
     takes_options = (_prefix_option,)
 
@@ -886,12 +885,6 @@ class aci_show(crud.Retrieve):
     """
     NO_CLI = True
 
-    has_output_params = (
-        Str('aci',
-            label=_('ACI'),
-        ),
-    )
-
     takes_options = (
         _prefix_option,
         DNParam('location?',
@@ -932,11 +925,6 @@ class aci_rename(crud.Update):
     Rename an ACI.
     """
     NO_CLI = True
-    has_output_params = (
-        Str('aci',
-            label=_('ACI'),
-        ),
-    )
 
     takes_options = (
         _prefix_option,
diff --git a/ipaserver/plugins/baseuser.py b/ipaserver/plugins/baseuser.py
index 7bb2e8a..8087418 100644
--- a/ipaserver/plugins/baseuser.py
+++ b/ipaserver/plugins/baseuser.py
@@ -59,9 +59,6 @@ baseuser_output_params = (
     Flag('has_keytab',
         label=_('Kerberos keys available'),
     ),
-    Str('sshpubkeyfp*',
-        label=_('SSH public key fingerprint'),
-    ),
    )
 
 status_baseuser_output_params = (
@@ -353,6 +350,10 @@ class baseuser(LDAPObject):
             normalizer=normalize_sshpubkey,
             flags=['no_search'],
         ),
+        Str('sshpubkeyfp*',
+            label=_('SSH public key fingerprint'),
+            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
+        ),
         StrEnum('ipauserauthtype*',
             cli_name='user_auth_type',
             label=_('User authentication types'),
diff --git a/ipaserver/plugins/certprofile.py b/ipaserver/plugins/certprofile.py
index 6f314e1..f446607 100644
--- a/ipaserver/plugins/certprofile.py
+++ b/ipaserver/plugins/certprofile.py
@@ -122,6 +122,10 @@ class certprofile(LDAPObject):
             label=_('Profile ID'),
             doc=_('Profile ID for referring to this profile'),
         ),
+        Str('config',
+            label=_('Profile configuration'),
+            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
+        ),
         Str('description',
             required=True,
             cli_name='desc',
@@ -195,12 +199,6 @@ class certprofile_find(LDAPSearch):
 class certprofile_show(LDAPRetrieve):
     __doc__ = _("Display the properties of a Certificate Profile.")
 
-    has_output_params = LDAPRetrieve.has_output_params + (
-        Str('config',
-            label=_('Profile configuration'),
-        ),
-    )
-
     takes_options = LDAPRetrieve.takes_options + (
         Str('out?',
             doc=_('Write profile configuration to file'),
diff --git a/ipaserver/plugins/delegation.py b/ipaserver/plugins/delegation.py
index 0443f0e..6340d1a 100644
--- a/ipaserver/plugins/delegation.py
+++ b/ipaserver/plugins/delegation.py
@@ -56,11 +56,6 @@ register = Registry()
 
 ACI_PREFIX=u"delegation"
 
-output_params = (
-    Str('aci',
-        label=_('ACI'),
-    ),
-)
 
 @register()
 class delegation(Object):
@@ -102,6 +97,10 @@ class delegation(Object):
             label=_('User group'),
             doc=_('User group ACI grants access to'),
         ),
+        Str('aci',
+            label=_('ACI'),
+            flags={'no_create', 'no_update', 'no_search'},
+        ),
     )
 
     def __json__(self):
@@ -131,7 +130,6 @@ class delegation_add(crud.Create):
     __doc__ = _('Add a new delegation.')
 
     msg_summary = _('Added delegation "%(value)s"')
-    has_output_params = output_params
 
     def execute(self, aciname, **kw):
         if not 'permissions' in kw:
@@ -170,7 +168,6 @@ class delegation_mod(crud.Update):
     __doc__ = _('Modify a delegation.')
 
     msg_summary = _('Modified delegation "%(value)s"')
-    has_output_params = output_params
 
     def execute(self, aciname, **kw):
         kw['aciprefix'] = ACI_PREFIX
@@ -193,7 +190,6 @@ class delegation_find(crud.Search):
     )
 
     takes_options = (gen_pkey_only_option("name"),)
-    has_output_params = output_params
 
     def execute(self, term=None, **kw):
         kw['aciprefix'] = ACI_PREFIX
@@ -214,8 +210,6 @@ class delegation_find(crud.Search):
 class delegation_show(crud.Retrieve):
     __doc__ = _('Display information about a delegation.')
 
-    has_output_params = output_params
-
     def execute(self, aciname, **kw):
         result = api.Command['aci_show'](aciname, aciprefix=ACI_PREFIX, **kw)['result']
         self.obj.postprocess_result(result)
diff --git a/ipaserver/plugins/dns.py b/ipaserver/plugins/dns.py
index fd8d84b..585b28c 100644
--- a/ipaserver/plugins/dns.py
+++ b/ipaserver/plugins/dns.py
@@ -1550,12 +1550,6 @@ def default_zone_update_policy(zone):
     else:
         return get_dns_forward_zone_update_policy(api.env.realm)
 
-dnszone_output_params = (
-    Str('managedby',
-        label=_('Managedby permission'),
-    ),
-)
-
 
 def _convert_to_idna(value):
     """
@@ -1990,7 +1984,10 @@ class DNSZoneBase(LDAPObject):
                   'that case, conditional zone forwarders are disregarded.'),
             values=(u'only', u'first', u'none'),
         ),
-
+        Str('managedby',
+            label=_('Managedby permission'),
+            flags={'virtual_attribute', 'no_create', 'no_search', 'no_update'},
+        ),
     )
 
     def get_dn(self, *keys, **options):
@@ -2081,8 +2078,6 @@ class DNSZoneBase_add(LDAPCreate):
         ),
     )
 
-    has_output_params = LDAPCreate.has_output_params + dnszone_output_params
-
     def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
         assert isinstance(dn, DN)
 
@@ -2127,8 +2122,6 @@ class DNSZoneBase_del(LDAPDelete):
 
 
 class DNSZoneBase_mod(LDAPUpdate):
-    has_output_params = LDAPUpdate.has_output_params + dnszone_output_params
-
     def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
         assert isinstance(dn, DN)
         self.obj._make_zonename_absolute(entry_attrs, **options)
@@ -2138,8 +2131,6 @@ class DNSZoneBase_mod(LDAPUpdate):
 class DNSZoneBase_find(LDAPSearch):
     __doc__ = _('Search for DNS zones (SOA records).')
 
-    has_output_params = LDAPSearch.has_output_params + dnszone_output_params
-
     def args_options_2_params(self, *args, **options):
         # FIXME: Check that name_from_ip is valid. This is necessary because
         #        custom validation rules, including _validate_ipnet, are not
@@ -2178,8 +2169,6 @@ class DNSZoneBase_find(LDAPSearch):
 
 
 class DNSZoneBase_show(LDAPRetrieve):
-    has_output_params = LDAPRetrieve.has_output_params + dnszone_output_params
-
     def pre_callback(self, ldap, dn, attrs_list, *keys, **options):
         assert isinstance(dn, DN)
         if not _check_DN_objectclass(ldap, dn, self.obj.object_class):
@@ -4397,8 +4386,6 @@ class dnsforwardzone_find(DNSZoneBase_find):
 class dnsforwardzone_show(DNSZoneBase_show):
     __doc__ = _('Display information about a DNS forward zone.')
 
-    has_output_params = LDAPRetrieve.has_output_params + dnszone_output_params
-
 
 @register()
 class dnsforwardzone_disable(DNSZoneBase_disable):
diff --git a/ipaserver/plugins/host.py b/ipaserver/plugins/host.py
index 1091f85..5ade112 100644
--- a/ipaserver/plugins/host.py
+++ b/ipaserver/plugins/host.py
@@ -198,39 +198,9 @@ host_output_params = (
     Str('managing_host',
         label='Managing',
     ),
-    Str('subject',
-        label=_('Subject'),
-    ),
-    Str('serial_number',
-        label=_('Serial Number'),
-    ),
-    Str('serial_number_hex',
-        label=_('Serial Number (hex)'),
-    ),
-    Str('issuer',
-        label=_('Issuer'),
-    ),
-    Str('valid_not_before',
-        label=_('Not Before'),
-    ),
-    Str('valid_not_after',
-        label=_('Not After'),
-    ),
-    Str('md5_fingerprint',
-        label=_('Fingerprint (MD5)'),
-    ),
-    Str('sha1_fingerprint',
-        label=_('Fingerprint (SHA1)'),
-    ),
-    Str('revocation_reason?',
-        label=_('Revocation reason'),
-    ),
     Str('managedby',
         label=_('Failed managedby'),
     ),
-    Str('sshpubkeyfp*',
-        label=_('SSH public key fingerprint'),
-    ),
     Str('ipaallowedtoperform_read_keys_user',
         label=_('Users allowed to retrieve keytab'),
     ),
@@ -502,6 +472,42 @@ class host(LDAPObject):
             label=_('Certificate'),
             doc=_('Base-64 encoded host certificate'),
         ),
+        Str('subject',
+            label=_('Subject'),
+            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
+        ),
+        Str('serial_number',
+            label=_('Serial Number'),
+            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
+        ),
+        Str('serial_number_hex',
+            label=_('Serial Number (hex)'),
+            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
+        ),
+        Str('issuer',
+            label=_('Issuer'),
+            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
+        ),
+        Str('valid_not_before',
+            label=_('Not Before'),
+            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
+        ),
+        Str('valid_not_after',
+            label=_('Not After'),
+            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
+        ),
+        Str('md5_fingerprint',
+            label=_('Fingerprint (MD5)'),
+            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
+        ),
+        Str('sha1_fingerprint',
+            label=_('Fingerprint (SHA1)'),
+            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
+        ),
+        Str('revocation_reason?',
+            label=_('Revocation reason'),
+            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
+        ),
         Str('krbprincipalname?',
             label=_('Principal name'),
             flags=['no_create', 'no_update', 'no_search'],
@@ -520,6 +526,10 @@ class host(LDAPObject):
             normalizer=normalize_sshpubkey,
             flags=['no_search'],
         ),
+        Str('sshpubkeyfp*',
+            label=_('SSH public key fingerprint'),
+            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
+        ),
         Str('userclass*',
             cli_name='class',
             label=_('Class'),
diff --git a/ipaserver/plugins/idviews.py b/ipaserver/plugins/idviews.py
index 537f924..755b07c 100644
--- a/ipaserver/plugins/idviews.py
+++ b/ipaserver/plugins/idviews.py
@@ -106,6 +106,18 @@ class idview(LDAPObject):
             cli_name='desc',
             label=_('Description'),
         ),
+        Str('useroverrides',
+            label=_('User object overrides'),
+            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
+        ),
+        Str('groupoverrides',
+            label=_('Group object overrides'),
+            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
+        ),
+        Str('appliedtohosts',
+            label=_('Hosts the view applies to'),
+            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
+        ),
     )
 
     permission_filter_objectclasses = ['nsContainer']
@@ -170,17 +182,7 @@ class idview_show(LDAPRetrieve):
         ),
     )
 
-    has_output_params = global_output_params + (
-        Str('useroverrides',
-            label=_('User object overrides'),
-            ),
-        Str('groupoverrides',
-            label=_('Group object overrides'),
-            ),
-        Str('appliedtohosts',
-            label=_('Hosts the view applies to')
-        ),
-    )
+    has_output_params = global_output_params
 
     def show_id_overrides(self, dn, entry_attrs):
         ldap = self.obj.backend
diff --git a/ipaserver/plugins/otptoken.py b/ipaserver/plugins/otptoken.py
index fda05ce..56b8c91 100644
--- a/ipaserver/plugins/otptoken.py
+++ b/ipaserver/plugins/otptoken.py
@@ -264,6 +264,10 @@ class otptoken(LDAPObject):
             minvalue=0,
             flags=('no_update'),
         ),
+        Str('uri?',
+            label=_('URI'),
+            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
+        ),
     )
 
 
@@ -277,10 +281,6 @@ class otptoken_add(LDAPCreate):
         Flag('no_qrcode', label=_('Do not display QR code'), default=False),
     )
 
-    has_output_params = LDAPCreate.has_output_params + (
-        Str('uri?', label=_('URI')),
-    )
-
     def execute(self, ipatokenuniqueid=None, **options):
         return super(otptoken_add, self).execute(ipatokenuniqueid, **options)
 
diff --git a/ipaserver/plugins/permission.py b/ipaserver/plugins/permission.py
index 801e7fa..830773a 100644
--- a/ipaserver/plugins/permission.py
+++ b/ipaserver/plugins/permission.py
@@ -113,12 +113,6 @@ _DEPRECATED_OPTION_ALIASES = {
 
 KNOWN_FLAGS = {'SYSTEM', 'V2', 'MANAGED'}
 
-output_params = (
-    Str('aci',
-        label=_('ACI'),
-    ),
-)
-
 
 def strip_ldap_prefix(uri):
     prefix = 'ldap:///'
@@ -354,6 +348,10 @@ class permission(baseldap.LDAPObject):
         for old_name, new_name in _DEPRECATED_OPTION_ALIASES.items()
     ) + (
         _ipapermissiontype_param,
+        Str('aci',
+            label=_('ACI'),
+            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
+        ),
     )
 
     def reject_system(self, entry):
@@ -950,7 +948,6 @@ class permission_add_noaci(baseldap.LDAPCreate):
 
     msg_summary = _('Added permission "%(value)s"')
     NO_CLI = True
-    has_output_params = baseldap.LDAPCreate.has_output_params + output_params
 
     takes_options = (
         _ipapermissiontype_param,
@@ -978,7 +975,6 @@ class permission_add(baseldap.LDAPCreate):
     __doc__ = _('Add a new permission.')
 
     msg_summary = _('Added permission "%(value)s"')
-    has_output_params = baseldap.LDAPCreate.has_output_params + output_params
 
     # Need to override execute so that processed options apply to
     # the whole command, not just the callbacks
@@ -1082,7 +1078,6 @@ class permission_mod(baseldap.LDAPUpdate):
     __doc__ = _('Modify a permission.')
 
     msg_summary = _('Modified permission "%(value)s"')
-    has_output_params = baseldap.LDAPUpdate.has_output_params + output_params
 
     def execute(self, *keys, **options):
         context.filter_ops = self.obj.preprocess_options(
@@ -1249,7 +1244,6 @@ class permission_find(baseldap.LDAPSearch):
 
     msg_summary = ngettext(
         '%(count)d permission matched', '%(count)d permissions matched', 0)
-    has_output_params = baseldap.LDAPSearch.has_output_params + output_params
 
     def execute(self, *keys, **options):
         self.obj.preprocess_options(options, merge_targetfilter=True)
@@ -1375,7 +1369,6 @@ class permission_find(baseldap.LDAPSearch):
 @register()
 class permission_show(baseldap.LDAPRetrieve):
     __doc__ = _('Display information about a permission.')
-    has_output_params = baseldap.LDAPRetrieve.has_output_params + output_params
 
     def post_callback(self, ldap, dn, entry, *keys, **options):
         self.obj.upgrade_permission(entry, output_only=True)
diff --git a/ipaserver/plugins/selfservice.py b/ipaserver/plugins/selfservice.py
index 4ff6ac7..9697493 100644
--- a/ipaserver/plugins/selfservice.py
+++ b/ipaserver/plugins/selfservice.py
@@ -57,12 +57,6 @@ register = Registry()
 
 ACI_PREFIX=u"selfservice"
 
-output_params = (
-    Str('aci',
-        label=_('ACI'),
-    ),
-)
-
 
 @register()
 class selfservice(Object):
@@ -96,6 +90,10 @@ class selfservice(Object):
             doc=_('Attributes to which the permission applies.'),
             normalizer=lambda value: value.lower(),
         ),
+        Str('aci',
+            label=_('ACI'),
+            flags={'no_create', 'no_update', 'no_search'},
+        ),
     )
 
     def __json__(self):
@@ -124,7 +122,6 @@ class selfservice_add(crud.Create):
     __doc__ = _('Add a new self-service permission.')
 
     msg_summary = _('Added selfservice "%(value)s"')
-    has_output_params = output_params
 
     def execute(self, aciname, **kw):
         if not 'permissions' in kw:
@@ -164,7 +161,6 @@ class selfservice_mod(crud.Update):
     __doc__ = _('Modify a self-service permission.')
 
     msg_summary = _('Modified selfservice "%(value)s"')
-    has_output_params = output_params
 
     def execute(self, aciname, **kw):
         if 'attrs' in kw and kw['attrs'] is None:
@@ -190,7 +186,6 @@ class selfservice_find(crud.Search):
     )
 
     takes_options = (gen_pkey_only_option("name"),)
-    has_output_params = output_params
 
     def execute(self, term=None, **kw):
         kw['selfaci'] = True
@@ -212,8 +207,6 @@ class selfservice_find(crud.Search):
 class selfservice_show(crud.Retrieve):
     __doc__ = _('Display information about a self-service permission.')
 
-    has_output_params = output_params
-
     def execute(self, aciname, **kw):
         result = api.Command['aci_show'](aciname, aciprefix=ACI_PREFIX, **kw)['result']
         self.obj.postprocess_result(result)
diff --git a/ipaserver/plugins/service.py b/ipaserver/plugins/service.py
index 701314f..bead94d 100644
--- a/ipaserver/plugins/service.py
+++ b/ipaserver/plugins/service.py
@@ -122,33 +122,6 @@ output_params = (
     Str('managedby_host',
         label='Managed by',
     ),
-    Str('subject',
-        label=_('Subject'),
-    ),
-    Str('serial_number',
-        label=_('Serial Number'),
-    ),
-    Str('serial_number_hex',
-        label=_('Serial Number (hex)'),
-    ),
-    Str('issuer',
-        label=_('Issuer'),
-    ),
-    Str('valid_not_before',
-        label=_('Not Before'),
-    ),
-    Str('valid_not_after',
-        label=_('Not After'),
-    ),
-    Str('md5_fingerprint',
-        label=_('Fingerprint (MD5)'),
-    ),
-    Str('sha1_fingerprint',
-        label=_('Fingerprint (SHA1)'),
-    ),
-    Str('revocation_reason?',
-        label=_('Revocation reason'),
-    ),
     Str('ipaallowedtoperform_read_keys_user',
         label=_('Users allowed to retrieve keytab'),
     ),
@@ -497,6 +470,42 @@ class service(LDAPObject):
             doc=_('Base-64 encoded service certificate'),
             flags=['no_search',],
         ),
+        Str('subject',
+            label=_('Subject'),
+            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
+        ),
+        Str('serial_number',
+            label=_('Serial Number'),
+            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
+        ),
+        Str('serial_number_hex',
+            label=_('Serial Number (hex)'),
+            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
+        ),
+        Str('issuer',
+            label=_('Issuer'),
+            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
+        ),
+        Str('valid_not_before',
+            label=_('Not Before'),
+            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
+        ),
+        Str('valid_not_after',
+            label=_('Not After'),
+            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
+        ),
+        Str('md5_fingerprint',
+            label=_('Fingerprint (MD5)'),
+            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
+        ),
+        Str('sha1_fingerprint',
+            label=_('Fingerprint (SHA1)'),
+            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
+        ),
+        Str('revocation_reason?',
+            label=_('Revocation reason'),
+            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
+        ),
         StrEnum('ipakrbauthzdata*',
             cli_name='pac_type',
             label=_('PAC type'),
diff --git a/ipaserver/plugins/trust.py b/ipaserver/plugins/trust.py
index 932089b..8536202 100644
--- a/ipaserver/plugins/trust.py
+++ b/ipaserver/plugins/trust.py
@@ -157,17 +157,6 @@ particular type.
 
 register = Registry()
 
-trust_output_params = (
-    Str('trustdirection',
-        label=_('Trust direction')),
-    Str('trusttype',
-        label=_('Trust type')),
-    Str('truststatus',
-        label=_('Trust status')),
-    Str('ipantadditionalsuffixes*',
-        label=_('UPN suffixes')),
-)
-
 # Trust type is a combination of ipanttrusttype and ipanttrustattributes
 # We shift trust attributes by 3 bits to left so bit 0 becomes bit 3 and
 # 2+(1 << 3) becomes 10.
@@ -551,6 +540,22 @@ class trust(LDAPObject):
             cli_name='sid_blacklist_outgoing',
             label=_('SID blacklist outgoing'),
             flags=['no_create']),
+        Str('trustdirection',
+            label=_('Trust direction'),
+            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
+        ),
+        Str('trusttype',
+            label=_('Trust type'),
+            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
+        ),
+        Str('truststatus',
+            label=_('Trust status'),
+            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
+        ),
+        Str('ipantadditionalsuffixes*',
+            label=_('UPN suffixes'),
+            flags={'no_create', 'no_update', 'no_search'},
+        ),
     )
 
     def validate_sid_blacklists(self, entry_attrs):
@@ -704,7 +709,6 @@ sides.
 
     msg_summary = _('Added Active Directory trust for realm "%(value)s"')
     msg_summary_existing = _('Re-established trust to domain "%(value)s"')
-    has_output_params = LDAPCreate.has_output_params + trust_output_params
 
     def execute(self, *keys, **options):
         ldap = self.obj.backend
@@ -1063,8 +1067,8 @@ class trust_mod(LDAPUpdate):
 @register()
 class trust_find(LDAPSearch):
     __doc__ = _('Search for trusts.')
-    has_output_params = LDAPSearch.has_output_params + trust_output_params +\
-                        (Str('ipanttrusttype'), Str('ipanttrustattributes'))
+    has_output_params = (LDAPSearch.has_output_params +
+                         (Str('ipanttrusttype'), Str('ipanttrustattributes')))
 
     msg_summary = ngettext(
         '%(count)d trust matched', '%(count)d trusts matched', 0
@@ -1102,8 +1106,10 @@ class trust_find(LDAPSearch):
 @register()
 class trust_show(LDAPRetrieve):
     __doc__ = _('Display information about a trust.')
-    has_output_params = LDAPRetrieve.has_output_params + trust_output_params +\
-                        (Str('ipanttrusttype'), Str('ipanttrustdirection'), Str('ipanttrustattributes'))
+    has_output_params = (LDAPRetrieve.has_output_params +
+                         (Str('ipanttrusttype'),
+                          Str('ipanttrustdirection'),
+                          Str('ipanttrustattributes')))
 
     def execute(self, *keys, **options):
         result = super(trust_show, self).execute(*keys, **options)
@@ -1514,6 +1520,10 @@ class trustdomain(LDAPObject):
             cli_name='sid',
             label=_('Domain Security Identifier'),
         ),
+        Flag('domain_enabled',
+            label=_('Domain enabled'),
+            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
+        ),
     )
 
     # LDAPObject.get_dn() only passes all but last element of keys and no kwargs
@@ -1533,10 +1543,6 @@ class trustdomain(LDAPObject):
 class trustdomain_find(LDAPSearch):
     __doc__ = _('Search domains of the trust')
 
-    has_output_params = LDAPSearch.has_output_params + trust_output_params + (
-        Flag('domain_enabled', label= _('Domain enabled')),
-    )
-
     def pre_callback(self, ldap, filters, attrs_list, base_dn, scope, *args, **options):
         return (filters, base_dn, ldap.SCOPE_SUBTREE)
 
-- 
2.7.4

From 8702f04af4a4190c51bcd9addc99d009256eb4f5 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Thu, 30 Jun 2016 06:37:52 +0200
Subject: [PATCH 2/8] user: add object plugin for user_status

Change user_status from a method of user to a method of a new userstatus
class, which defines the extra attributes returned by user_status.

This fixes user_status CLI output.

https://fedorahosted.org/freeipa/ticket/4739
---
 API.txt                        |  6 ++---
 VERSION                        |  4 ++--
 ipaserver/plugins/baseuser.py  | 18 ---------------
 ipaserver/plugins/stageuser.py |  2 --
 ipaserver/plugins/user.py      | 52 ++++++++++++++++++++++++++++++++++++------
 5 files changed, 50 insertions(+), 32 deletions(-)

diff --git a/API.txt b/API.txt
index 1992266..085a7e0 100644
--- a/API.txt
+++ b/API.txt
@@ -5863,10 +5863,9 @@ output: Output('result', type=[<type 'dict'>])
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
 output: ListOfPrimaryKeys('value')
 command: user_status/1
-args: 1,4,4
-arg: Str('uid', cli_name='login')
+args: 1,3,4
+arg: Str('useruid', cli_name='login')
 option: Flag('all', autofill=True, cli_name='all', default=False)
-option: Flag('no_members', autofill=True, default=False)
 option: Flag('raw', autofill=True, cli_name='raw', default=False)
 option: Str('version?')
 output: Output('count', type=[<type 'int'>])
@@ -6615,6 +6614,7 @@ default: user_stage/1
 default: user_status/1
 default: user_undel/1
 default: user_unlock/1
+default: userstatus/1
 default: vault/1
 default: vault_add_internal/1
 default: vault_add_member/1
diff --git a/VERSION b/VERSION
index 5c3aef2..656e472 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=203
-# Last change: host: added authentication indicators
+IPA_API_VERSION_MINOR=204
+# Last change: user: add object plugin for user_status
diff --git a/ipaserver/plugins/baseuser.py b/ipaserver/plugins/baseuser.py
index 8087418..9c4af66 100644
--- a/ipaserver/plugins/baseuser.py
+++ b/ipaserver/plugins/baseuser.py
@@ -61,24 +61,6 @@ baseuser_output_params = (
     ),
    )
 
-status_baseuser_output_params = (
-    Str('server',
-        label=_('Server'),
-    ),
-    Str('krbloginfailedcount',
-        label=_('Failed logins'),
-    ),
-    Str('krblastsuccessfulauth',
-        label=_('Last successful authentication'),
-    ),
-    Str('krblastfailedauth',
-        label=_('Last failed authentication'),
-    ),
-    Str('now',
-        label=_('Time now'),
-    ),
-   )
-
 UPG_DEFINITION_DN = DN(('cn', 'UPG Definition'),
                        ('cn', 'Definitions'),
                        ('cn', 'Managed Entries'),
diff --git a/ipaserver/plugins/stageuser.py b/ipaserver/plugins/stageuser.py
index 9d5d404..3b9388f 100644
--- a/ipaserver/plugins/stageuser.py
+++ b/ipaserver/plugins/stageuser.py
@@ -40,7 +40,6 @@ from .baseuser import (
     NO_UPG_MAGIC,
     baseuser_pwdchars,
     baseuser_output_params,
-    status_baseuser_output_params,
     baseuser_add_manager,
     baseuser_remove_manager)
 from ipalib.request import context
@@ -102,7 +101,6 @@ register = Registry()
 
 stageuser_output_params = baseuser_output_params
 
-status_output_params = status_baseuser_output_params
 
 @register()
 class stageuser(baseuser):
diff --git a/ipaserver/plugins/user.py b/ipaserver/plugins/user.py
index adc59fc..7c5221c 100644
--- a/ipaserver/plugins/user.py
+++ b/ipaserver/plugins/user.py
@@ -38,7 +38,6 @@ from .baseuser import (
     NO_UPG_MAGIC,
     UPG_DEFINITION_DN,
     baseuser_output_params,
-    status_baseuser_output_params,
     baseuser_pwdchars,
     validate_nsaccountlock,
     convert_nsaccountlock,
@@ -48,6 +47,7 @@ from .baseuser import (
 from .idviews import remove_ipaobject_overrides
 from ipalib.plugable import Registry
 from .baseldap import (
+    LDAPObject,
     pkey_to_value,
     LDAPCreate,
     LDAPSearch,
@@ -118,8 +118,6 @@ register = Registry()
 
 user_output_params = baseuser_output_params
 
-status_output_params = status_baseuser_output_params
-
 
 def check_protected_member(user, protected_group_name=u'admins'):
     '''
@@ -990,6 +988,38 @@ class user_unlock(LDAPQuery):
 
 
 @register()
+class userstatus(LDAPObject):
+    parent_object = 'user'
+
+    takes_params = (
+        Bool('preserved?',
+            label=_('Preserved user'),
+            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
+        ),
+        Str('server',
+            label=_('Server'),
+            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
+        ),
+        Str('krbloginfailedcount',
+            label=_('Failed logins'),
+            flags={'no_create', 'no_update', 'no_search'},
+        ),
+        Str('krblastsuccessfulauth',
+            label=_('Last successful authentication'),
+            flags={'no_create', 'no_update', 'no_search'},
+        ),
+        Str('krblastfailedauth',
+            label=_('Last failed authentication'),
+            flags={'no_create', 'no_update', 'no_search'},
+        ),
+        Str('now',
+            label=_('Time now'),
+            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
+        ),
+    )
+
+
+@register()
 class user_status(LDAPQuery):
     __doc__ = _("""
     Lockout status of a user account
@@ -1013,12 +1043,20 @@ class user_status(LDAPQuery):
     login attempt is older than the lockouttime of the password policy. This
     means that the user may attempt a login again. """)
 
+    obj_name = 'userstatus'
+    attr_name = 'find'
+
     has_output = output.standard_list_of_entries
-    has_output_params = LDAPSearch.has_output_params + status_output_params
+
+    def get_args(self):
+        for arg in super(user_status, self).get_args():
+            if arg.name == 'useruid':
+                arg = arg.clone(cli_name='login')
+            yield arg
 
     def execute(self, *keys, **options):
         ldap = self.obj.backend
-        dn = self.obj.get_either_dn(*keys, **options)
+        dn = self.api.Object.user.get_either_dn(*keys, **options)
         attr_list = ['krbloginfailedcount', 'krblastsuccessfulauth', 'krblastfailedauth', 'nsaccountlock']
 
         disabled = False
@@ -1074,11 +1112,11 @@ class user_status(LDAPQuery):
                 convert_nsaccountlock(entry)
                 if 'nsaccountlock' in entry:
                     disabled = entry['nsaccountlock']
-                self.obj.get_preserved_attribute(entry, options)
+                self.api.Object.user.get_preserved_attribute(entry, options)
                 entries.append(newresult)
                 count += 1
             except errors.NotFound:
-                self.obj.handle_not_found(*keys)
+                self.api.Object.user.handle_not_found(*keys)
             except Exception as e:
                 self.error("user_status: Retrieving status for %s failed with %s" % (dn, str(e)))
                 newresult = {'dn': dn}
-- 
2.7.4

From 3232cb4bf20b4fe10f6d9c3d10c55d20adbc9240 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Thu, 30 Jun 2016 10:24:57 +0200
Subject: [PATCH 3/8] frontend: do not ignore client-side output params

Do not ignore output params defined in client-side overrides.

https://fedorahosted.org/freeipa/ticket/4739
---
 ipaclient/frontend.py | 13 ++++++++++++-
 1 file changed, 12 insertions(+), 1 deletion(-)

diff --git a/ipaclient/frontend.py b/ipaclient/frontend.py
index c2d916d..a869e33 100644
--- a/ipaclient/frontend.py
+++ b/ipaclient/frontend.py
@@ -43,7 +43,10 @@ class CommandOverride(Command):
                 yield option
 
     def get_output_params(self):
-        return self.next.output_params()
+        for output_param in self.next.output_params():
+            yield output_param
+        for output_param in super(CommandOverride, self).get_output_params():
+            yield output_param
 
     def _iter_output(self):
         return self.next.output()
@@ -61,3 +64,11 @@ class MethodOverride(CommandOverride, Method):
     @property
     def obj(self):
         return self.next.obj
+
+    def get_output_params(self):
+        seen = set()
+        for output_param in super(MethodOverride, self).get_output_params():
+            if output_param.name in seen:
+                continue
+            seen.add(output_param.name)
+            yield output_param
-- 
2.7.4

From a0bbc831f8e9055cf5b7896d32094e65c1146469 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Wed, 29 Jun 2016 18:11:41 +0200
Subject: [PATCH 4/8] cert: fix CLI output of cert_remove_hold

cert_remove_hold uses output params instead of exceptions to convey
unsuccessful result. Move the output params to the client side before
the command is fixed to use exceptions.

https://fedorahosted.org/freeipa/ticket/4739
---
 ipaclient/plugins/cert.py | 14 +++++++++++++-
 ipaserver/plugins/cert.py |  8 --------
 2 files changed, 13 insertions(+), 9 deletions(-)

diff --git a/ipaclient/plugins/cert.py b/ipaclient/plugins/cert.py
index de4318b..37e894e 100644
--- a/ipaclient/plugins/cert.py
+++ b/ipaclient/plugins/cert.py
@@ -23,7 +23,7 @@ from ipaclient.frontend import MethodOverride
 from ipalib import errors
 from ipalib import x509
 from ipalib import util
-from ipalib.parameters import File
+from ipalib.parameters import File, Flag, Str
 from ipalib.plugable import Registry
 from ipalib.text import _
 
@@ -55,6 +55,18 @@ class cert_show(MethodOverride):
 
 
 @register(override=True)
+class cert_remove_hold(MethodOverride):
+    has_output_params = (
+        Flag('unrevoked',
+            label=_('Unrevoked'),
+        ),
+        Str('error_string',
+            label=_('Error'),
+        ),
+    )
+
+
+@register(override=True)
 class cert_find(MethodOverride):
     takes_options = (
         File(
diff --git a/ipaserver/plugins/cert.py b/ipaserver/plugins/cert.py
index 63351c5..1f60554 100644
--- a/ipaserver/plugins/cert.py
+++ b/ipaserver/plugins/cert.py
@@ -827,14 +827,6 @@ class cert_revoke(PKQuery, CertMethod, VirtualCommand):
 class cert_remove_hold(PKQuery, CertMethod, VirtualCommand):
     __doc__ = _('Take a revoked certificate off hold.')
 
-    has_output_params = (
-        Flag('unrevoked',
-            label=_('Unrevoked'),
-        ),
-        Str('error_string',
-            label=_('Error'),
-        ),
-    )
     operation = "certificate remove hold"
 
     def get_options(self):
-- 
2.7.4

From d68ff9bf3cf5533b15784a65b592edf354adbbf1 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Mon, 27 Jun 2016 09:32:55 +0200
Subject: [PATCH 5/8] plugable: add option to ignore override errors

Add new `no_fail` option to API.add_plugin. When set to True, override
errors are ignored and the affected plugins are skipped.

https://fedorahosted.org/freeipa/ticket/4739
---
 ipalib/plugable.py | 32 +++++++++++++++++++-------------
 1 file changed, 19 insertions(+), 13 deletions(-)

diff --git a/ipalib/plugable.py b/ipalib/plugable.py
index d55a8f7..26fbeaa 100644
--- a/ipalib/plugable.py
+++ b/ipalib/plugable.py
@@ -638,7 +638,7 @@ class API(ReadOnly):
 
         raise errors.PluginModuleError(name=module.__name__)
 
-    def add_plugin(self, plugin, override=False):
+    def add_plugin(self, plugin, override=False, no_fail=False):
         """
         Add the plugin ``plugin``.
 
@@ -662,23 +662,29 @@ class API(ReadOnly):
         prev = self.__plugins_by_key.get(plugin.full_name)
         if prev:
             if not override:
-                # Must use override=True to override:
-                raise errors.PluginOverrideError(
-                    base=base.__name__,
-                    name=plugin.name,
-                    plugin=plugin,
-                )
+                if no_fail:
+                    return
+                else:
+                    # Must use override=True to override:
+                    raise errors.PluginOverrideError(
+                        base=base.__name__,
+                        name=plugin.name,
+                        plugin=plugin,
+                    )
 
             self.__plugins.remove(prev)
             self.__next[plugin] = prev
         else:
             if override:
-                # There was nothing already registered to override:
-                raise errors.PluginMissingOverrideError(
-                    base=base.__name__,
-                    name=plugin.name,
-                    plugin=plugin,
-                )
+                if no_fail:
+                    return
+                else:
+                    # There was nothing already registered to override:
+                    raise errors.PluginMissingOverrideError(
+                        base=base.__name__,
+                        name=plugin.name,
+                        plugin=plugin,
+                    )
 
         # The plugin is okay, add to sub_d:
         self.__plugins.add(plugin)
-- 
2.7.4

From c88fface3027d373865d6109f2eb1e1bea54fbae Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Mon, 27 Jun 2016 09:33:29 +0200
Subject: [PATCH 6/8] client: ignore override errors in command overrides

This fixes API initialization errors when the remote server does not have
the overriden command.

https://fedorahosted.org/freeipa/ticket/4739
---
 ipaclient/plugins/automember.py  |  2 +-
 ipaclient/plugins/automount.py   |  2 +-
 ipaclient/plugins/cert.py        |  8 ++++----
 ipaclient/plugins/certprofile.py |  6 +++---
 ipaclient/plugins/dns.py         | 18 +++++++++---------
 ipaclient/plugins/hbactest.py    |  2 +-
 ipaclient/plugins/host.py        |  2 +-
 ipaclient/plugins/idrange.py     |  2 +-
 ipaclient/plugins/internal.py    |  4 ++--
 ipaclient/plugins/location.py    |  2 +-
 ipaclient/plugins/migration.py   |  2 +-
 ipaclient/plugins/misc.py        |  4 ++--
 ipaclient/plugins/otptoken.py    |  2 +-
 ipaclient/plugins/passwd.py      |  2 +-
 ipaclient/plugins/permission.py  |  6 +++---
 ipaclient/plugins/server.py      |  2 +-
 ipaclient/plugins/service.py     |  2 +-
 ipaclient/plugins/sudorule.py    |  8 ++++----
 ipaclient/plugins/topology.py    |  2 +-
 ipaclient/plugins/trust.py       |  2 +-
 ipaclient/plugins/user.py        |  4 ++--
 ipaclient/plugins/vault.py       |  2 +-
 22 files changed, 43 insertions(+), 43 deletions(-)

diff --git a/ipaclient/plugins/automember.py b/ipaclient/plugins/automember.py
index 98caf93..0b6fdda 100644
--- a/ipaclient/plugins/automember.py
+++ b/ipaclient/plugins/automember.py
@@ -25,7 +25,7 @@ from ipalib.text import _
 register = Registry()
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class automember_add_condition(MethodOverride):
     has_output_params = (
         Str('failed',
diff --git a/ipaclient/plugins/automount.py b/ipaclient/plugins/automount.py
index 2d6b8d9..7ac5413 100644
--- a/ipaclient/plugins/automount.py
+++ b/ipaclient/plugins/automount.py
@@ -39,7 +39,7 @@ DEFAULT_MAPS = (u'auto.direct', )
 DEFAULT_KEYS = (u'/-', )
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class automountlocation_tofiles(MethodOverride):
     def output_for_cli(self, textui, result, *keys, **options):
         maps = result['result']['maps']
diff --git a/ipaclient/plugins/cert.py b/ipaclient/plugins/cert.py
index 37e894e..1075972 100644
--- a/ipaclient/plugins/cert.py
+++ b/ipaclient/plugins/cert.py
@@ -30,7 +30,7 @@ from ipalib.text import _
 register = Registry()
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class cert_request(MethodOverride):
     def get_args(self):
         for arg in super(cert_request, self).get_args():
@@ -39,7 +39,7 @@ class cert_request(MethodOverride):
             yield arg
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class cert_show(MethodOverride):
     def forward(self, *keys, **options):
         if 'out' in options:
@@ -54,7 +54,7 @@ class cert_show(MethodOverride):
             return super(cert_show, self).forward(*keys, **options)
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class cert_remove_hold(MethodOverride):
     has_output_params = (
         Flag('unrevoked',
@@ -66,7 +66,7 @@ class cert_remove_hold(MethodOverride):
     )
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class cert_find(MethodOverride):
     takes_options = (
         File(
diff --git a/ipaclient/plugins/certprofile.py b/ipaclient/plugins/certprofile.py
index f36f271..cde039a 100644
--- a/ipaclient/plugins/certprofile.py
+++ b/ipaclient/plugins/certprofile.py
@@ -11,7 +11,7 @@ from ipalib.text import _
 register = Registry()
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class certprofile_show(MethodOverride):
     def forward(self, *keys, **options):
         if 'out' in options:
@@ -29,7 +29,7 @@ class certprofile_show(MethodOverride):
         return result
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class certprofile_import(MethodOverride):
     def get_options(self):
         for option in super(certprofile_import, self).get_options():
@@ -38,7 +38,7 @@ class certprofile_import(MethodOverride):
             yield option
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class certprofile_mod(MethodOverride):
     def get_options(self):
         for option in super(certprofile_mod, self).get_options():
diff --git a/ipaclient/plugins/dns.py b/ipaclient/plugins/dns.py
index bca5ad7..e17c282 100644
--- a/ipaclient/plugins/dns.py
+++ b/ipaclient/plugins/dns.py
@@ -109,17 +109,17 @@ class DNSZoneMethodOverride(MethodOverride):
             yield option
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class dnszone_add(DNSZoneMethodOverride):
     pass
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class dnszone_mod(DNSZoneMethodOverride):
     pass
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class dnsrecord_add(MethodOverride):
     no_option_msg = 'No options to add a specific record provided.\n' \
             "Command help may be consulted for all supported record types."
@@ -194,7 +194,7 @@ class dnsrecord_add(MethodOverride):
         kw.update(user_options)
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class dnsrecord_mod(MethodOverride):
     no_option_msg = 'No options to modify a specific record provided.'
 
@@ -252,7 +252,7 @@ class dnsrecord_mod(MethodOverride):
                          break
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class dnsrecord_del(MethodOverride):
     no_option_msg = _('Neither --del-all nor options to delete a specific record provided.\n'\
             "Command help may be consulted for all supported record types.")
@@ -309,7 +309,7 @@ class dnsrecord_del(MethodOverride):
                 kw[param.name] = tuple(deleted_values)
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class dnsconfig_mod(MethodOverride):
     def interactive_prompt_callback(self, kw):
 
@@ -322,7 +322,7 @@ class dnsconfig_mod(MethodOverride):
                 _("This may take some time, please wait ..."))
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class dnsforwardzone_add(MethodOverride):
     def interactive_prompt_callback(self, kw):
         # show informative message on client side
@@ -334,7 +334,7 @@ class dnsforwardzone_add(MethodOverride):
                 _("This may take some time, please wait ..."))
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class dnsforwardzone_mod(MethodOverride):
     def interactive_prompt_callback(self, kw):
         # show informative message on client side
@@ -346,7 +346,7 @@ class dnsforwardzone_mod(MethodOverride):
                 _("This may take some time, please wait ..."))
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class dns_update_system_records(MethodOverride):
     def output_for_cli(self, textui, output, *args, **options):
         output_super = copy.deepcopy(output)
diff --git a/ipaclient/plugins/hbactest.py b/ipaclient/plugins/hbactest.py
index 10a640a..2518719 100644
--- a/ipaclient/plugins/hbactest.py
+++ b/ipaclient/plugins/hbactest.py
@@ -28,7 +28,7 @@ if six.PY3:
 register = Registry()
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class hbactest(CommandOverride):
     def output_for_cli(self, textui, output, *args, **options):
         """
diff --git a/ipaclient/plugins/host.py b/ipaclient/plugins/host.py
index a346226..7d8b92d 100644
--- a/ipaclient/plugins/host.py
+++ b/ipaclient/plugins/host.py
@@ -27,7 +27,7 @@ from ipalib import x509
 register = Registry()
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class host_show(MethodOverride):
     def forward(self, *keys, **options):
         if 'out' in options:
diff --git a/ipaclient/plugins/idrange.py b/ipaclient/plugins/idrange.py
index 83ad8fd..1a8d68e 100644
--- a/ipaclient/plugins/idrange.py
+++ b/ipaclient/plugins/idrange.py
@@ -24,7 +24,7 @@ from ipalib import api
 register = Registry()
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class idrange_add(MethodOverride):
     def interactive_prompt_callback(self, kw):
         """
diff --git a/ipaclient/plugins/internal.py b/ipaclient/plugins/internal.py
index 65cbbe7..1a8f369 100644
--- a/ipaclient/plugins/internal.py
+++ b/ipaclient/plugins/internal.py
@@ -30,13 +30,13 @@ from ipalib.plugable import Registry
 register = Registry()
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class json_metadata(CommandOverride):
     def output_for_cli(self, textui, result, *args, **options):
         print(json.dumps(result, default=json_serialize))
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class i18n_messages(CommandOverride):
     def output_for_cli(self, textui, result, *args, **options):
         print(json.dumps(result, default=json_serialize))
diff --git a/ipaclient/plugins/location.py b/ipaclient/plugins/location.py
index b3b6026..e5191e7 100644
--- a/ipaclient/plugins/location.py
+++ b/ipaclient/plugins/location.py
@@ -10,7 +10,7 @@ from ipalib.plugable import Registry
 register = Registry()
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class location_show(MethodOverride):
     def output_for_cli(self, textui, output, *keys, **options):
         rv = super(location_show, self).output_for_cli(
diff --git a/ipaclient/plugins/migration.py b/ipaclient/plugins/migration.py
index b40ddfd..8ac5f66 100644
--- a/ipaclient/plugins/migration.py
+++ b/ipaclient/plugins/migration.py
@@ -30,7 +30,7 @@ if six.PY3:
 register = Registry()
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class migrate_ds(CommandOverride):
     migrate_order = ('user', 'group')
 
diff --git a/ipaclient/plugins/misc.py b/ipaclient/plugins/misc.py
index 05fc542..2c195f8 100644
--- a/ipaclient/plugins/misc.py
+++ b/ipaclient/plugins/misc.py
@@ -8,7 +8,7 @@ from ipalib.plugable import Registry
 register = Registry()
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class env(CommandOverride):
     def output_for_cli(self, textui, output, *args, **options):
         output = dict(output)
@@ -19,7 +19,7 @@ class env(CommandOverride):
                                                *args, **options)
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class plugins(CommandOverride):
     def output_for_cli(self, textui, output, *args, **options):
         options['all'] = True
diff --git a/ipaclient/plugins/otptoken.py b/ipaclient/plugins/otptoken.py
index d7d5356..dd4a718 100644
--- a/ipaclient/plugins/otptoken.py
+++ b/ipaclient/plugins/otptoken.py
@@ -43,7 +43,7 @@ if six.PY3:
 register = Registry()
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class otptoken_add(MethodOverride):
     def _get_qrcode(self, output, uri, version):
         # Print QR code to terminal if specified
diff --git a/ipaclient/plugins/passwd.py b/ipaclient/plugins/passwd.py
index 7382306..b00a459 100644
--- a/ipaclient/plugins/passwd.py
+++ b/ipaclient/plugins/passwd.py
@@ -8,7 +8,7 @@ from ipalib.plugable import Registry
 register = Registry()
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class passwd(CommandOverride):
     def get_args(self):
         for arg in super(passwd, self).get_args():
diff --git a/ipaclient/plugins/permission.py b/ipaclient/plugins/permission.py
index 2ec1eb4..5b7293c 100644
--- a/ipaclient/plugins/permission.py
+++ b/ipaclient/plugins/permission.py
@@ -16,16 +16,16 @@ class PermissionMethodOverride(MethodOverride):
             yield option
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class permission_add(PermissionMethodOverride):
     pass
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class permission_mod(PermissionMethodOverride):
     pass
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class permission_find(PermissionMethodOverride):
     pass
diff --git a/ipaclient/plugins/server.py b/ipaclient/plugins/server.py
index 277a874..725a2ce 100644
--- a/ipaclient/plugins/server.py
+++ b/ipaclient/plugins/server.py
@@ -9,7 +9,7 @@ from ipalib.plugable import Registry
 register = Registry()
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class server_del(MethodOverride):
     def interactive_prompt_callback(self, kw):
         self.api.Backend.textui.print_plain(
diff --git a/ipaclient/plugins/service.py b/ipaclient/plugins/service.py
index 72783b6..c45a2f2 100644
--- a/ipaclient/plugins/service.py
+++ b/ipaclient/plugins/service.py
@@ -29,7 +29,7 @@ from ipalib import util
 register = Registry()
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class service_show(MethodOverride):
     def forward(self, *keys, **options):
         if 'out' in options:
diff --git a/ipaclient/plugins/sudorule.py b/ipaclient/plugins/sudorule.py
index 4098eb8..a876280 100644
--- a/ipaclient/plugins/sudorule.py
+++ b/ipaclient/plugins/sudorule.py
@@ -24,19 +24,19 @@ from ipalib import _
 register = Registry()
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class sudorule_enable(MethodOverride):
     def output_for_cli(self, textui, result, cn, **options):
         textui.print_dashed(_('Enabled Sudo Rule "%s"') % cn)
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class sudorule_disable(MethodOverride):
     def output_for_cli(self, textui, result, cn, **options):
         textui.print_dashed(_('Disabled Sudo Rule "%s"') % cn)
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class sudorule_add_option(MethodOverride):
     def output_for_cli(self, textui, result, cn, **options):
         textui.print_dashed(
@@ -47,7 +47,7 @@ class sudorule_add_option(MethodOverride):
                                                         **options)
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class sudorule_remove_option(MethodOverride):
     def output_for_cli(self, textui, result, cn, **options):
         textui.print_dashed(
diff --git a/ipaclient/plugins/topology.py b/ipaclient/plugins/topology.py
index 522dcfa..c7fbcc3 100644
--- a/ipaclient/plugins/topology.py
+++ b/ipaclient/plugins/topology.py
@@ -14,7 +14,7 @@ if six.PY3:
 register = Registry()
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class topologysuffix_verify(MethodOverride):
     def output_for_cli(self, textui, output, *args, **options):
 
diff --git a/ipaclient/plugins/trust.py b/ipaclient/plugins/trust.py
index 004c870..8e05396 100644
--- a/ipaclient/plugins/trust.py
+++ b/ipaclient/plugins/trust.py
@@ -24,7 +24,7 @@ from ipalib.plugable import Registry
 register = Registry()
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class trust_add(MethodOverride):
     def interactive_prompt_callback(self, kw):
         """
diff --git a/ipaclient/plugins/user.py b/ipaclient/plugins/user.py
index ccff9bb..19eecac 100644
--- a/ipaclient/plugins/user.py
+++ b/ipaclient/plugins/user.py
@@ -29,7 +29,7 @@ from ipalib import x509
 register = Registry()
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class user_del(MethodOverride):
     def get_options(self):
         for option in super(user_del, self).get_options():
@@ -60,7 +60,7 @@ class user_del(MethodOverride):
         return super(user_del, self).forward(*keys, **options)
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class user_show(MethodOverride):
     def forward(self, *keys, **options):
         if 'out' in options:
diff --git a/ipaclient/plugins/vault.py b/ipaclient/plugins/vault.py
index 945f390..2674970 100644
--- a/ipaclient/plugins/vault.py
+++ b/ipaclient/plugins/vault.py
@@ -493,7 +493,7 @@ class vault_mod(Local):
         return response
 
 
-@register(override=True)
+@register(override=True, no_fail=True)
 class vaultconfig_show(MethodOverride):
     def forward(self, *args, **options):
 
-- 
2.7.4

From 303948d6a2c62d4b118344f43a5ced21385a3a17 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Tue, 28 Jun 2016 11:05:01 +0200
Subject: [PATCH 7/8] client: add placeholders for required remote plugins

Add placeholders for remote plugins which are required by client-side
commands. They are used when the remote plugins are not available.

This fixes API initialization error when the remote server does not have
the plugins.

https://fedorahosted.org/freeipa/ticket/4739
---
 ipaclient/plugins/automount.py        | 17 ++++++++++++-
 ipaclient/plugins/otptoken_yubikey.py | 17 ++++++++++++-
 ipaclient/plugins/vault.py            | 47 ++++++++++++++++++++++++++++++++++-
 3 files changed, 78 insertions(+), 3 deletions(-)

diff --git a/ipaclient/plugins/automount.py b/ipaclient/plugins/automount.py
index 7ac5413..8405f9f 100644
--- a/ipaclient/plugins/automount.py
+++ b/ipaclient/plugins/automount.py
@@ -25,7 +25,7 @@ import six
 from ipaclient.frontend import MethodOverride
 from ipalib import api, errors
 from ipalib import Flag, Str
-from ipalib.frontend import Command
+from ipalib.frontend import Command, Method, Object
 from ipalib.plugable import Registry
 from ipalib import _
 from ipapython.dn import DN
@@ -39,8 +39,23 @@ DEFAULT_MAPS = (u'auto.direct', )
 DEFAULT_KEYS = (u'/-', )
 
 
+@register(no_fail=True)
+class _fake_automountlocation(Object):
+    name = 'automountlocation'
+
+
+@register(no_fail=True)
+class _fake_automountlocation_show(Method):
+    name = 'automountlocation_show'
+    NO_CLI = True
+
+
 @register(override=True, no_fail=True)
 class automountlocation_tofiles(MethodOverride):
+    @property
+    def NO_CLI(self):
+        return self.api.Command.automountlocation_show.NO_CLI
+
     def output_for_cli(self, textui, result, *keys, **options):
         maps = result['result']['maps']
         keys = result['result']['keys']
diff --git a/ipaclient/plugins/otptoken_yubikey.py b/ipaclient/plugins/otptoken_yubikey.py
index e9aaba9..5e0d994 100644
--- a/ipaclient/plugins/otptoken_yubikey.py
+++ b/ipaclient/plugins/otptoken_yubikey.py
@@ -25,7 +25,7 @@ import yubico
 
 from ipalib import _, IntEnum
 from ipalib.errors import NotFound
-from ipalib.frontend import Command
+from ipalib.frontend import Command, Method, Object
 from ipalib.plugable import Registry
 
 if six.PY3:
@@ -50,6 +50,17 @@ register = Registry()
 topic = 'otp'
 
 
+@register(no_fail=True)
+class _fake_otptoken(Object):
+    name = 'otptoken'
+
+
+@register(no_fail=True)
+class _fake_otptoken_add(Method):
+    name = 'otptoken_add'
+    NO_CLI = True
+
+
 @register()
 class otptoken_add_yubikey(Command):
     __doc__ = _('Add a new YubiKey OTP token.')
@@ -63,6 +74,10 @@ class otptoken_add_yubikey(Command):
     )
     has_output_params = takes_options
 
+    @property
+    def NO_CLI(self):
+        return self.api.Command.otptoken_add.NO_CLI
+
     def get_args(self):
         for arg in self.api.Command.otptoken_add.args():
             yield arg
diff --git a/ipaclient/plugins/vault.py b/ipaclient/plugins/vault.py
index 2674970..11210d6 100644
--- a/ipaclient/plugins/vault.py
+++ b/ipaclient/plugins/vault.py
@@ -37,7 +37,7 @@ from cryptography.hazmat.primitives.serialization import load_pem_public_key,\
 import nss.nss as nss
 
 from ipaclient.frontend import MethodOverride
-from ipalib.frontend import Local
+from ipalib.frontend import Local, Method, Object
 from ipalib import errors
 from ipalib import Bytes, Flag, Str
 from ipalib.plugable import Registry
@@ -169,6 +169,17 @@ def decrypt(data, symmetric_key=None, private_key=None):
                 message=_('Invalid credentials'))
 
 
+@register(no_fail=True)
+class _fake_vault(Object):
+    name = 'vault'
+
+
+@register(no_fail=True)
+class _fake_vault_add_internal(Method):
+    name = 'vault_add_internal'
+    NO_CLI = True
+
+
 @register()
 class vault_add(Local):
     __doc__ = _('Create a new vault.')
@@ -191,6 +202,10 @@ class vault_add(Local):
         ),
     )
 
+    @property
+    def NO_CLI(self):
+        return self.api.Command.vault_add_internal.NO_CLI
+
     def get_args(self):
         for arg in self.api.Command.vault_add_internal.args():
             yield arg
@@ -327,6 +342,12 @@ class vault_add(Local):
         return response
 
 
+@register(no_fail=True)
+class _fake_vault_mod_internal(Method):
+    name = 'vault_mod_internal'
+    NO_CLI = True
+
+
 @register()
 class vault_mod(Local):
     __doc__ = _('Modify a vault.')
@@ -373,6 +394,10 @@ class vault_mod(Local):
         ),
     )
 
+    @property
+    def NO_CLI(self):
+        return self.api.Command.vault_mod_internal.NO_CLI
+
     def get_args(self):
         for arg in self.api.Command.vault_mod_internal.args():
             yield arg
@@ -512,6 +537,12 @@ class vaultconfig_show(MethodOverride):
         return response
 
 
+@register(no_fail=True)
+class _fake_vault_archive_internal(Method):
+    name = 'vault_archive_internal'
+    NO_CLI = True
+
+
 @register()
 class vault_archive(Local):
     __doc__ = _('Archive data into a vault.')
@@ -541,6 +572,10 @@ class vault_archive(Local):
         ),
     )
 
+    @property
+    def NO_CLI(self):
+        return self.api.Command.vault_archive_internal.NO_CLI
+
     def get_args(self):
         for arg in self.api.Command.vault_archive_internal.args():
             yield arg
@@ -741,6 +776,12 @@ class vault_archive(Local):
         return self.api.Command.vault_archive_internal(*args, **options)
 
 
+@register(no_fail=True)
+class _fake_vault_retrieve_internal(Method):
+    name = 'vault_retrieve_internal'
+    NO_CLI = True
+
+
 @register()
 class vault_retrieve(Local):
     __doc__ = _('Retrieve a data from a vault.')
@@ -779,6 +820,10 @@ class vault_retrieve(Local):
         ),
     )
 
+    @property
+    def NO_CLI(self):
+        return self.api.Command.vault_retrieve_internal.NO_CLI
+
     def get_args(self):
         for arg in self.api.Command.vault_retrieve_internal.args():
             yield arg
-- 
2.7.4

From e91c9beb7a2dc10dcf72c65015ab45f5a079e11f Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Thu, 30 Jun 2016 09:32:00 +0200
Subject: [PATCH 8/8] server: exclude Local commands from RPC

Local API commands are not supposed to be executed over RPC but only
locally on the server. They are already excluded from API schema, exclude
them also from RPC and `batch` and `json_metadata` commands.

https://fedorahosted.org/freeipa/ticket/4739
---
 ipaserver/plugins/batch.py    |  4 +++-
 ipaserver/plugins/internal.py | 19 +++++++++++++------
 ipaserver/rpcserver.py        | 10 +++++++---
 3 files changed, 23 insertions(+), 10 deletions(-)

diff --git a/ipaserver/plugins/batch.py b/ipaserver/plugins/batch.py
index aa4ace9..b0c89ec 100644
--- a/ipaserver/plugins/batch.py
+++ b/ipaserver/plugins/batch.py
@@ -49,6 +49,7 @@ import six
 
 from ipalib import api, errors
 from ipalib import Command
+from ipalib.frontend import Local
 from ipalib.parameters import Str, Dict
 from ipalib.output import Output
 from ipalib.text import _
@@ -98,7 +99,8 @@ class batch(Command):
                 if 'params' not in arg:
                     raise errors.RequirementError(name='params')
                 name = arg['method']
-                if name not in self.Command:
+                if (name not in self.api.Command or
+                        isinstance(self.api.Command[name], Local)):
                     raise errors.CommandError(name=name)
 
                 # If params are not formated as a tuple(list, dict)
diff --git a/ipaserver/plugins/internal.py b/ipaserver/plugins/internal.py
index 5c1cfb8..5eee757 100644
--- a/ipaserver/plugins/internal.py
+++ b/ipaserver/plugins/internal.py
@@ -24,6 +24,7 @@ Plugins not accessible directly through the CLI, commands used internally
 """
 from ipalib import Command
 from ipalib import Str
+from ipalib.frontend import Local
 from ipalib.output import Output
 from ipalib.text import _
 from ipalib.util import json_serialize
@@ -91,13 +92,15 @@ class json_metadata(Command):
         try:
             if not methodname:
                 methodname = options['method']
-            if methodname in self.api.Method:
+            if (methodname in self.api.Method and
+                    not isinstance(self.api.Method[methodname], Local)):
                 m = self.api.Method[methodname]
                 methods = dict([(m.name, json_serialize(m))])
             elif methodname == "all":
                 methods = dict(
                     (m.name, json_serialize(m)) for m in self.api.Method()
-                    if m is self.api.Method[m.name]
+                    if (m is self.api.Method[m.name] and
+                        not isinstance(m, Local))
                 )
             empty = False
         except KeyError:
@@ -105,13 +108,15 @@ class json_metadata(Command):
 
         try:
             cmdname = options['command']
-            if cmdname in self.api.Command:
+            if (cmdname in self.api.Command and
+                    not isinstance(self.api.Command[cmdname], Local)):
                 c = self.api.Command[cmdname]
                 commands = dict([(c.name, json_serialize(c))])
             elif cmdname == "all":
                 commands = dict(
                     (c.name, json_serialize(c)) for c in self.api.Command()
-                    if c is self.api.Command[c.name]
+                    if (c is self.api.Command[c.name] and
+                        not isinstance(c, Local))
                 )
             empty = False
         except KeyError:
@@ -124,11 +129,13 @@ class json_metadata(Command):
             )
             methods = dict(
                 (m.name, json_serialize(m)) for m in self.api.Method()
-                if m is self.api.Method[m.name]
+                if (m is self.api.Method[m.name] and
+                    not isinstance(m, Local))
             )
             commands = dict(
                 (c.name, json_serialize(c)) for c in self.api.Command()
-                if c is self.api.Command[c.name]
+                if (c is self.api.Command[c.name] and
+                    not isinstance(c, Local))
             )
 
         retval = dict([
diff --git a/ipaserver/rpcserver.py b/ipaserver/rpcserver.py
index 6761497..d036f3c 100644
--- a/ipaserver/rpcserver.py
+++ b/ipaserver/rpcserver.py
@@ -40,6 +40,7 @@ from six.moves.urllib.parse import parse_qs
 
 from ipalib import plugable, errors
 from ipalib.capabilities import VERSION_WITHOUT_CAPABILITIES
+from ipalib.frontend import Local
 from ipalib.backend import Executioner
 from ipalib.errors import (PublicError, InternalError, CommandError, JSONError,
     CCacheError, RefererError, InvalidSessionPassword, NotFound, ACIError,
@@ -344,7 +345,8 @@ class WSGIExecutioner(Executioner):
                 (name, args, options, _id) = self.simple_unmarshal(environ)
             if name in self._system_commands:
                 result = self._system_commands[name](self, *args, **options)
-            elif name not in self.Command:
+            elif (name not in self.api.Command or
+                    isinstance(self.api.Command[name], Local)):
                 raise CommandError(name=name)
             else:
                 result = self.Command[name](*args, **options)
@@ -696,7 +698,8 @@ class xmlserver(KerberosWSGIExecutioner):
             # TODO
             # for now let's not go out of our way to document standard XML-RPC
             return u'undef'
-        elif method_name in self.Command:
+        elif (method_name in self.api.Command and
+                not isinstance(self.api.Command[method_name], Local)):
             # All IPA commands return a dict (struct),
             # and take a params, options - list and dict (array, struct)
             return [[u'struct', u'array', u'struct']]
@@ -708,7 +711,8 @@ class xmlserver(KerberosWSGIExecutioner):
         method_name = self._get_method_name('system.methodHelp', *params)
         if method_name in self._system_commands:
             return u''
-        elif method_name in self.Command:
+        elif (method_name in self.api.Command and
+                not isinstance(self.api.Command[method_name], Local)):
             return unicode(self.Command[method_name].doc or '')
         else:
             raise errors.CommandError(name=method_name)
-- 
2.7.4

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