CLOUDSTACK-9369: Restrict default login to ldap/native users

- Restricts default login auth handler to ldap and native-cloudstack users
- Refactors and create re-usable method to find domain by id/path
- Adds unit test for refactored method in DomainManagerImpl
- Adds smoke test for login handler

Signed-off-by: Rohit Yadav <rohit.ya...@shapeblue.com>


Project: http://git-wip-us.apache.org/repos/asf/cloudstack/repo
Commit: http://git-wip-us.apache.org/repos/asf/cloudstack/commit/566e7d9f
Tree: http://git-wip-us.apache.org/repos/asf/cloudstack/tree/566e7d9f
Diff: http://git-wip-us.apache.org/repos/asf/cloudstack/diff/566e7d9f

Branch: refs/heads/4.7
Commit: 566e7d9fac59bae1122c1718f23e8fd3fc1f18ab
Parents: d9429f6
Author: Rohit Yadav <rohit.ya...@shapeblue.com>
Authored: Wed Apr 27 17:34:14 2016 +0530
Committer: Will Stevens <williamstev...@gmail.com>
Committed: Fri May 27 15:00:05 2016 -0400

----------------------------------------------------------------------
 api/src/com/cloud/user/DomainService.java       |  10 ++
 server/src/com/cloud/api/ApiServer.java         |  16 +-
 .../auth/DefaultLoginAPIAuthenticatorCmd.java   |  13 ++
 .../src/com/cloud/user/DomainManagerImpl.java   |  20 +++
 .../com/cloud/user/DomainManagerImplTest.java   | 137 ++++++++++++++++++
 .../com/cloud/user/MockDomainManagerImpl.java   |   5 +
 test/integration/smoke/test_login.py            | 145 +++++++++++++++++++
 7 files changed, 335 insertions(+), 11 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cloudstack/blob/566e7d9f/api/src/com/cloud/user/DomainService.java
----------------------------------------------------------------------
diff --git a/api/src/com/cloud/user/DomainService.java 
b/api/src/com/cloud/user/DomainService.java
index 4c1f93d..3ccfcbc 100644
--- a/api/src/com/cloud/user/DomainService.java
+++ b/api/src/com/cloud/user/DomainService.java
@@ -56,4 +56,14 @@ public interface DomainService {
      */
     Domain findDomainByPath(String domainPath);
 
+    /**
+     * finds the domain by either id or provided path
+     *
+     * @param id the domain id
+     * @param domainPath the domain path use to lookup a domain
+     *
+     * @return domainId the long value of the domain ID, or null if no domain 
id exists with provided id/path
+     */
+    Domain findDomainByIdOrPath(Long id, String domainPath);
+
 }

http://git-wip-us.apache.org/repos/asf/cloudstack/blob/566e7d9f/server/src/com/cloud/api/ApiServer.java
----------------------------------------------------------------------
diff --git a/server/src/com/cloud/api/ApiServer.java 
b/server/src/com/cloud/api/ApiServer.java
index e5ae097..689ae9c 100644
--- a/server/src/com/cloud/api/ApiServer.java
+++ b/server/src/com/cloud/api/ApiServer.java
@@ -993,17 +993,11 @@ public class ApiServer extends ManagerBase implements 
HttpRequestHandler, ApiSer
             final Map<String, Object[]> requestParameters) throws 
CloudAuthenticationException {
         // We will always use domainId first. If that does not exist, we will 
use domain name. If THAT doesn't exist
         // we will default to ROOT
-        if (domainId == null) {
-            if (domainPath == null || domainPath.trim().length() == 0) {
-                domainId = Domain.ROOT_DOMAIN;
-            } else {
-                final Domain domainObj = 
_domainMgr.findDomainByPath(domainPath);
-                if (domainObj != null) {
-                    domainId = domainObj.getId();
-                } else { // if an unknown path is passed in, fail the login 
call
-                    throw new CloudAuthenticationException("Unable to find the 
domain from the path " + domainPath);
-                }
-            }
+        final Domain userDomain = _domainMgr.findDomainByIdOrPath(domainId, 
domainPath);
+        if (userDomain == null || userDomain.getId() < 1L) {
+            throw new CloudAuthenticationException("Unable to find the domain 
from the path " + domainPath);
+        } else {
+            domainId = userDomain.getId();
         }
 
         final UserAccount userAcct = _accountMgr.authenticateUser(username, 
password, domainId, loginIpAddress, requestParameters);

http://git-wip-us.apache.org/repos/asf/cloudstack/blob/566e7d9f/server/src/com/cloud/api/auth/DefaultLoginAPIAuthenticatorCmd.java
----------------------------------------------------------------------
diff --git a/server/src/com/cloud/api/auth/DefaultLoginAPIAuthenticatorCmd.java 
b/server/src/com/cloud/api/auth/DefaultLoginAPIAuthenticatorCmd.java
index d6eac78..c83e708 100644
--- a/server/src/com/cloud/api/auth/DefaultLoginAPIAuthenticatorCmd.java
+++ b/server/src/com/cloud/api/auth/DefaultLoginAPIAuthenticatorCmd.java
@@ -16,6 +16,9 @@
 // under the License.
 package com.cloud.api.auth;
 
+import com.cloud.domain.Domain;
+import com.cloud.user.User;
+import com.cloud.user.UserAccount;
 import org.apache.cloudstack.api.ApiServerService;
 import com.cloud.api.response.ApiResponseSerializer;
 import com.cloud.exception.CloudAuthenticationException;
@@ -156,6 +159,16 @@ public class DefaultLoginAPIAuthenticatorCmd extends 
BaseCmd implements APIAuthe
         if (username != null) {
             final String pwd = ((password == null) ? null : password[0]);
             try {
+                final Domain userDomain = 
_domainService.findDomainByIdOrPath(domainId, domain);
+                if (userDomain != null) {
+                    domainId = userDomain.getId();
+                } else {
+                    throw new CloudAuthenticationException("Unable to find the 
domain from the path " + domain);
+                }
+                final UserAccount userAccount = 
_accountService.getActiveUserAccount(username[0], domainId);
+                if (userAccount == null || 
!(User.Source.UNKNOWN.equals(userAccount.getSource()) || 
User.Source.LDAP.equals(userAccount.getSource()))) {
+                    throw new CloudAuthenticationException("User is not 
allowed CloudStack login");
+                }
                 return 
ApiResponseSerializer.toSerializedString(_apiServer.loginUser(session, 
username[0], pwd, domainId, domain, remoteAddress, params),
                         responseType);
             } catch (final CloudAuthenticationException ex) {

http://git-wip-us.apache.org/repos/asf/cloudstack/blob/566e7d9f/server/src/com/cloud/user/DomainManagerImpl.java
----------------------------------------------------------------------
diff --git a/server/src/com/cloud/user/DomainManagerImpl.java 
b/server/src/com/cloud/user/DomainManagerImpl.java
index 912aa8b..71f9b67 100644
--- a/server/src/com/cloud/user/DomainManagerImpl.java
+++ b/server/src/com/cloud/user/DomainManagerImpl.java
@@ -73,6 +73,7 @@ import com.cloud.utils.exception.CloudRuntimeException;
 import com.cloud.utils.net.NetUtils;
 import com.cloud.vm.ReservationContext;
 import com.cloud.vm.ReservationContextImpl;
+import com.google.common.base.Strings;
 
 @Component
 public class DomainManagerImpl extends ManagerBase implements DomainManager, 
DomainService {
@@ -219,6 +220,25 @@ public class DomainManagerImpl extends ManagerBase 
implements DomainManager, Dom
     }
 
     @Override
+    public Domain findDomainByIdOrPath(final Long id, final String domainPath) 
{
+        Long domainId = id;
+        if (domainId == null || domainId < 1L) {
+            if (Strings.isNullOrEmpty(domainPath) || 
domainPath.trim().isEmpty()) {
+                domainId = Domain.ROOT_DOMAIN;
+            } else {
+                final Domain domainVO = findDomainByPath(domainPath.trim());
+                if (domainVO != null) {
+                    return domainVO;
+                }
+            }
+        }
+        if (domainId != null && domainId > 0L) {
+            return _domainDao.findById(domainId);
+        }
+        return null;
+    }
+
+    @Override
     public Set<Long> getDomainParentIds(long domainId) {
         return _domainDao.getDomainParentIds(domainId);
     }

http://git-wip-us.apache.org/repos/asf/cloudstack/blob/566e7d9f/server/test/com/cloud/user/DomainManagerImplTest.java
----------------------------------------------------------------------
diff --git a/server/test/com/cloud/user/DomainManagerImplTest.java 
b/server/test/com/cloud/user/DomainManagerImplTest.java
new file mode 100644
index 0000000..82d5491
--- /dev/null
+++ b/server/test/com/cloud/user/DomainManagerImplTest.java
@@ -0,0 +1,137 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package com.cloud.user;
+
+import com.cloud.configuration.dao.ResourceCountDao;
+import com.cloud.configuration.dao.ResourceLimitDao;
+import com.cloud.dc.dao.DedicatedResourceDao;
+import com.cloud.domain.DomainVO;
+import com.cloud.domain.dao.DomainDao;
+import com.cloud.network.dao.NetworkDomainDao;
+import com.cloud.projects.ProjectManager;
+import com.cloud.projects.dao.ProjectDao;
+import com.cloud.service.dao.ServiceOfferingDao;
+import com.cloud.storage.dao.DiskOfferingDao;
+import com.cloud.user.dao.AccountDao;
+import 
org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
+import org.apache.cloudstack.framework.messagebus.MessageBus;
+import org.apache.cloudstack.region.RegionManager;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import javax.inject.Inject;
+import java.lang.reflect.Field;
+
+@RunWith(MockitoJUnitRunner.class)
+public class DomainManagerImplTest {
+    @Mock
+    DomainDao _domainDao;
+    @Mock
+    AccountManager _accountMgr;
+    @Mock
+    ResourceCountDao _resourceCountDao;
+    @Mock
+    AccountDao _accountDao;
+    @Mock
+    DiskOfferingDao _diskOfferingDao;
+    @Mock
+    ServiceOfferingDao _offeringsDao;
+    @Mock
+    ProjectDao _projectDao;
+    @Mock
+    ProjectManager _projectMgr;
+    @Mock
+    RegionManager _regionMgr;
+    @Mock
+    ResourceLimitDao _resourceLimitDao;
+    @Mock
+    DedicatedResourceDao _dedicatedDao;
+    @Mock
+    NetworkOrchestrationService _networkMgr;
+    @Mock
+    NetworkDomainDao _networkDomainDao;
+    @Mock
+    MessageBus _messageBus;
+
+    DomainManagerImpl domainManager;
+
+    @Before
+    public void setup() throws NoSuchFieldException, SecurityException,
+            IllegalArgumentException, IllegalAccessException {
+        domainManager = new DomainManagerImpl();
+        for (Field field : DomainManagerImpl.class.getDeclaredFields()) {
+            if (field.getAnnotation(Inject.class) != null) {
+                field.setAccessible(true);
+                try {
+                    Field mockField = this.getClass().getDeclaredField(
+                            field.getName());
+                    field.set(domainManager, mockField.get(this));
+                } catch (Exception ignored) {
+                }
+            }
+        }
+    }
+
+    @Test
+    public void testFindDomainByIdOrPathNullOrEmpty() {
+        final DomainVO domain = new DomainVO("someDomain", 123, 1L, 
"network.domain");
+        Mockito.when(_domainDao.findById(1L)).thenReturn(domain);
+        Assert.assertEquals(domain, domainManager.findDomainByIdOrPath(null, 
null));
+        Assert.assertEquals(domain, domainManager.findDomainByIdOrPath(0L, 
""));
+        Assert.assertEquals(domain, domainManager.findDomainByIdOrPath(-1L, " 
"));
+        Assert.assertEquals(domain, domainManager.findDomainByIdOrPath(null, " 
      "));
+    }
+
+    @Test
+    public void testFindDomainByIdOrPathValidPathAndInvalidId() {
+        final DomainVO domain = new DomainVO("someDomain", 123, 1L, 
"network.domain");
+        
Mockito.when(_domainDao.findDomainByPath(Mockito.eq("/someDomain/"))).thenReturn(domain);
+        Assert.assertEquals(domain, domainManager.findDomainByIdOrPath(null, 
"/someDomain/"));
+        Assert.assertEquals(domain, domainManager.findDomainByIdOrPath(0L, " 
/someDomain/"));
+        Assert.assertEquals(domain, domainManager.findDomainByIdOrPath(-1L, 
"/someDomain/ "));
+        Assert.assertEquals(domain, domainManager.findDomainByIdOrPath(null, " 
  /someDomain/   "));
+    }
+
+    @Test
+    public void testFindDomainByIdOrPathInvalidPathAndInvalidId() {
+        
Mockito.when(_domainDao.findDomainByPath(Mockito.anyString())).thenReturn(null);
+        Assert.assertNull(domainManager.findDomainByIdOrPath(null, 
"/nonExistingDomain/"));
+        Assert.assertNull(domainManager.findDomainByIdOrPath(0L, " 
/nonExistingDomain/"));
+        Assert.assertNull(domainManager.findDomainByIdOrPath(-1L, 
"/nonExistingDomain/ "));
+        Assert.assertNull(domainManager.findDomainByIdOrPath(null, "   
/nonExistingDomain/   "));
+    }
+
+
+    @Test
+    public void testFindDomainByIdOrPathValidId() {
+        final DomainVO domain = new DomainVO("someDomain", 123, 1L, 
"network.domain");
+        Mockito.when(_domainDao.findById(1L)).thenReturn(domain);
+        
Mockito.when(_domainDao.findDomainByPath(Mockito.eq("/validDomain/"))).thenReturn(new
 DomainVO());
+        Assert.assertEquals(domain, domainManager.findDomainByIdOrPath(1L, 
null));
+        Assert.assertEquals(domain, domainManager.findDomainByIdOrPath(1L, 
""));
+        Assert.assertEquals(domain, domainManager.findDomainByIdOrPath(1L, " 
"));
+        Assert.assertEquals(domain, domainManager.findDomainByIdOrPath(1L, "   
    "));
+        Assert.assertEquals(domain, domainManager.findDomainByIdOrPath(1L, 
"/validDomain/"));
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/cloudstack/blob/566e7d9f/server/test/com/cloud/user/MockDomainManagerImpl.java
----------------------------------------------------------------------
diff --git a/server/test/com/cloud/user/MockDomainManagerImpl.java 
b/server/test/com/cloud/user/MockDomainManagerImpl.java
index cdae887..394c3e2 100644
--- a/server/test/com/cloud/user/MockDomainManagerImpl.java
+++ b/server/test/com/cloud/user/MockDomainManagerImpl.java
@@ -92,6 +92,11 @@ public class MockDomainManagerImpl extends ManagerBase 
implements DomainManager,
     }
 
     @Override
+    public DomainVO findDomainByIdOrPath(Long id, String domainPath) {
+        return null;
+    }
+
+    @Override
     public Set<Long> getDomainParentIds(long domainId) {
         // TODO Auto-generated method stub
         return null;

http://git-wip-us.apache.org/repos/asf/cloudstack/blob/566e7d9f/test/integration/smoke/test_login.py
----------------------------------------------------------------------
diff --git a/test/integration/smoke/test_login.py 
b/test/integration/smoke/test_login.py
new file mode 100644
index 0000000..5855fea
--- /dev/null
+++ b/test/integration/smoke/test_login.py
@@ -0,0 +1,145 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from nose.plugins.attrib import attr
+from marvin.cloudstackTestCase import *
+from marvin.cloudstackAPI import *
+from marvin.lib.utils import *
+from marvin.lib.base import *
+from marvin.lib.common import *
+
+import requests
+
+
+class TestLogin(cloudstackTestCase):
+    """
+        Tests default login API handler
+    """
+
+    def setUp(self):
+        self.apiclient = self.testClient.getApiClient()
+        self.dbclient = self.testClient.getDbConnection()
+        self.server_details = self.config.__dict__["mgtSvr"][0].__dict__
+        self.server_url = "http://%s:8080/client/api"; % 
self.server_details['mgtSvrIp']
+        self.testdata = {
+            "account": {
+                "email": "login-user@test.cloud",
+                "firstname": "TestLoginFirstName",
+                "lastname": "TestLoginLastName",
+                "username": "testloginuser-",
+                "password": "password123",
+            }
+        }
+        self.cleanup = []
+
+
+    def tearDown(self):
+        try:
+            cleanup_resources(self.apiclient, self.cleanup)
+        except Exception as e:
+            raise Exception("Warning: Exception during cleanup : %s" % e)
+
+
+    def login(self, username, password, domain="/"):
+        """
+            Logs in and returns a session to be used for subsequent API calls
+        """
+        args = {}
+        args["command"] = 'login'
+        args["username"] = username
+        args["password"] = password
+        args["domain"] = domain
+        args["response"] = "json"
+
+        session = requests.Session()
+
+        try:
+            resp = session.post(self.server_url, params=args, verify=False)
+        except requests.exceptions.ConnectionError, e:
+            self.fail("Failed to attempt login request to mgmt server")
+            return None, None
+
+        return resp, session
+
+
+    @attr(tags = ["devcloud", "advanced", "advancedns", "advancedsg", "smoke",
+                  "basic", "sg"], required_hardware="false")
+    def login_test_saml_user(self):
+        """
+            Tests that SAML users are not allowed CloudStack local log in
+
+            Creates account across various account types and converts them to
+            a SAML user and tests that they are not able to log in; then
+            converts them back as a CloudStack user account and verifies that
+            they are allowed to log in and make API requests
+        """
+        # Tests across various account types: 0=User, 1=Root Admin, 2=Domain 
Admin
+        for account_type in range(0, 3):
+            account = Account.create(
+                self.apiclient,
+                self.testdata['account'],
+                admin=account_type
+            )
+            self.cleanup.append(account)
+
+            username = account.user[0].username
+            password = self.testdata['account']['password']
+
+            # Convert newly created account user to SAML user
+            user_id = self.dbclient.execute("select id from user where 
uuid='%s'" % account.user[0].id)[0][0]
+            self.dbclient.execute("update user set source='SAML2' where id=%d" 
% user_id)
+
+            response, session = self.login(username, password)
+            self.assertEqual(
+                response.json()['loginresponse']['errorcode'],
+                531,
+                "SAML user should not be allowed to log in, error code 531 not 
returned"
+            )
+            self.assertEqual(
+                response.json()['loginresponse']['errortext'],
+                "User is not allowed CloudStack login",
+                "Invalid error message returned, SAML user should not be 
allowed to log in"
+            )
+
+            # Convert newly created account user back to normal source
+            self.dbclient.execute("update user set source='UNKNOWN' where 
id=%d" % user_id)
+
+            response, session = self.login(username, password)
+            self.assertEqual(
+                response.status_code,
+                200,
+                "Login response code was not 200"
+            )
+            self.assertTrue(
+                len(response.json()['loginresponse']['sessionkey']) > 0,
+                "Invalid session key received"
+            )
+
+            args = {}
+            args["command"] = 'listUsers'
+            args["listall"] = 'true'
+            args["response"] = "json"
+            response = session.get(self.server_url, params=args)
+            self.assertEqual(
+                response.status_code,
+                200,
+                "listUsers response code was not 200"
+            )
+            self.assertTrue(
+                len(response.json()['listusersresponse']['user']) > 0,
+                "listUsers list is empty or zero"
+            )

Reply via email to