From b6384886973c652c0161a9caeac6f31066edace1 Mon Sep 17 00:00:00 2001
From: Terry Howe <terrylhowe@gmail.com>
Date: Fri, 30 May 2014 10:38:20 -0600
Subject: [PATCH] Domain administrator cannot do project operations

Domain administrator cannot do project operations because the
require access to the domain API (which they don't have).  When
attempting to find a domain for project operations, ignore errors
because the API returns nothing without indicating there is a
problem.  The domain administrators will have to use a domain id,
but they will still be able to do project operations.  If the user
does not have permission to read the domain table, they cannot
use domain names.

Change-Id: Ieed5d420022a407c8296a0bb3569d9469c89d752
Closes-Bug: #1317478
Closes-Bug: #1317485
---
 openstackclient/identity/common.py            | 21 ++++++
 openstackclient/identity/v3/project.py        | 18 ++----
 .../tests/identity/v3/test_project.py         | 64 +++++++++++++++++++
 3 files changed, 91 insertions(+), 12 deletions(-)

diff --git a/openstackclient/identity/common.py b/openstackclient/identity/common.py
index 6aeaa3c387..48dc0c89b8 100644
--- a/openstackclient/identity/common.py
+++ b/openstackclient/identity/common.py
@@ -16,6 +16,7 @@
 """Common identity code"""
 
 from keystoneclient import exceptions as identity_exc
+from keystoneclient.v3 import domains
 from openstackclient.common import exceptions
 from openstackclient.common import utils
 
@@ -36,3 +37,23 @@ def find_service(identity_client, name_type_or_id):
             msg = ("No service with a type, name or ID of '%s' exists."
                    % name_type_or_id)
             raise exceptions.CommandError(msg)
+
+
+def find_domain(identity_client, name_or_id):
+    """Find a domain.
+
+       If the user does not have permssions to access the v3 domain API,
+       assume that domain given is the id rather than the name.  This
+       method is used by the project list command, so errors access the
+       domain will be ignored and if the user has access to the project
+       API, everything will work fine.
+
+       Closes bugs #1317478 and #1317485.
+    """
+    try:
+        dom = utils.find_resource(identity_client.domains, name_or_id)
+        if dom is not None:
+            return dom
+    except identity_exc.Forbidden:
+        pass
+    return domains.Domain(None, {'id': name_or_id})
diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py
index 00a98d19a6..fa935f0bf5 100644
--- a/openstackclient/identity/v3/project.py
+++ b/openstackclient/identity/v3/project.py
@@ -24,6 +24,7 @@ from cliff import show
 
 from openstackclient.common import parseractions
 from openstackclient.common import utils
+from openstackclient.identity import common
 
 
 class CreateProject(show.ShowOne):
@@ -73,10 +74,7 @@ class CreateProject(show.ShowOne):
         identity_client = self.app.client_manager.identity
 
         if parsed_args.domain:
-            domain = utils.find_resource(
-                identity_client.domains,
-                parsed_args.domain,
-            ).id
+            domain = common.find_domain(identity_client, parsed_args.domain).id
         else:
             domain = None
 
@@ -156,10 +154,8 @@ class ListProject(lister.Lister):
             columns = ('ID', 'Name')
         kwargs = {}
         if parsed_args.domain:
-            kwargs['domain'] = utils.find_resource(
-                identity_client.domains,
-                parsed_args.domain,
-            ).id
+            domain = common.find_domain(identity_client, parsed_args.domain)
+            kwargs['domain'] = domain.id
         data = identity_client.projects.list(**kwargs)
         return (columns,
                 (utils.get_item_properties(
@@ -236,10 +232,8 @@ class SetProject(command.Command):
         if parsed_args.name:
             kwargs['name'] = parsed_args.name
         if parsed_args.domain:
-            kwargs['domain'] = utils.find_resource(
-                identity_client.domains,
-                parsed_args.domain,
-            ).id
+            domain = common.find_domain(identity_client, parsed_args.domain)
+            kwargs['domain'] = domain.id
         if parsed_args.description:
             kwargs['description'] = parsed_args.description
         if parsed_args.enable:
diff --git a/openstackclient/tests/identity/v3/test_project.py b/openstackclient/tests/identity/v3/test_project.py
index e0420a1e80..2e7bc54b5c 100644
--- a/openstackclient/tests/identity/v3/test_project.py
+++ b/openstackclient/tests/identity/v3/test_project.py
@@ -14,6 +14,7 @@
 #
 
 import copy
+import mock
 
 from openstackclient.identity.v3 import project
 from openstackclient.tests import fakes
@@ -172,6 +173,45 @@ class TestProjectCreate(TestProject):
         )
         self.assertEqual(data, datalist)
 
+    def test_project_create_domain_no_perms(self):
+        arglist = [
+            '--domain', identity_fakes.domain_id,
+            identity_fakes.project_name,
+        ]
+        verifylist = [
+            ('domain', identity_fakes.domain_id),
+            ('enable', False),
+            ('disable', False),
+            ('name', identity_fakes.project_name),
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+        mocker = mock.Mock()
+        mocker.return_value = None
+
+        with mock.patch("openstackclient.common.utils.find_resource", mocker):
+            columns, data = self.cmd.take_action(parsed_args)
+
+        # Set expected values
+        kwargs = {
+            'name': identity_fakes.project_name,
+            'domain': identity_fakes.domain_id,
+            'description': None,
+            'enabled': True,
+        }
+        self.projects_mock.create.assert_called_with(
+            **kwargs
+        )
+        collist = ('description', 'domain_id', 'enabled', 'id', 'name')
+        self.assertEqual(columns, collist)
+        datalist = (
+            identity_fakes.project_description,
+            identity_fakes.domain_id,
+            True,
+            identity_fakes.project_id,
+            identity_fakes.project_name,
+        )
+        self.assertEqual(data, datalist)
+
     def test_project_create_enable(self):
         arglist = [
             '--enable',
@@ -411,6 +451,30 @@ class TestProjectList(TestProject):
         ), )
         self.assertEqual(tuple(data), datalist)
 
+    def test_project_list_domain_no_perms(self):
+        arglist = [
+            '--domain', identity_fakes.domain_id,
+        ]
+        verifylist = [
+            ('domain', identity_fakes.domain_id),
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+        mocker = mock.Mock()
+        mocker.return_value = None
+
+        with mock.patch("openstackclient.common.utils.find_resource", mocker):
+            columns, data = self.cmd.take_action(parsed_args)
+
+        self.projects_mock.list.assert_called_with(
+            domain=identity_fakes.domain_id)
+        collist = ('ID', 'Name')
+        self.assertEqual(columns, collist)
+        datalist = ((
+            identity_fakes.project_id,
+            identity_fakes.project_name,
+        ), )
+        self.assertEqual(tuple(data), datalist)
+
 
 class TestProjectSet(TestProject):