From 235e4fe1907d4a39e24f1f6b854bf36658071441 Mon Sep 17 00:00:00 2001 From: Harikrishna Date: Tue, 31 Oct 2023 13:25:28 +0530 Subject: [PATCH] Oauth2 integration with CloudStack (#7996) OAuth2, the industry-standard authorization or authentication framework, simplifies the process of granting access to resources. CloudStack supports OAuth2 authentication wherein users can login into CloudStack without using a username and password. Support for Google and Github providers has been added. Other OAuth2 providers can be easily integrated with CloudStack using its plugin framework. The login page will show provider options when the OAuth2 is enabled and corresponding providers are configured. "OAuth configuration" sub-section is present under "Configuration" where admins can register the corresponding OAuth providers. --- .../java/com/cloud/user/AccountService.java | 2 + api/src/main/java/com/cloud/user/User.java | 2 +- .../apache/cloudstack/api/ApiConstants.java | 5 + .../command/user/ssh/CreateSSHKeyPairCmd.java | 3 +- .../user/userdata/ListUserDataCmd.java | 3 +- .../user/userdata/RegisterUserDataCmd.java | 3 +- .../auth/UserOAuth2Authenticator.java | 53 ++++ client/pom.xml | 5 + .../spring-core-registry-core-context.xml | 4 +- .../com/cloud/user/dao/UserAccountDao.java | 2 + .../cloud/user/dao/UserAccountDaoImpl.java | 12 + .../META-INF/db/schema-41810to41900.sql | 28 +++ .../management/MockAccountManager.java | 5 + plugins/pom.xml | 1 + plugins/user-authenticators/oauth2/pom.xml | 63 +++++ .../cloudstack/oauth2/OAuth2AuthManager.java | 61 +++++ .../oauth2/OAuth2AuthManagerImpl.java | 233 +++++++++++++++++ .../oauth2/OAuth2UserAuthenticator.java | 78 ++++++ .../api/command/DeleteOAuthProviderCmd.java | 87 +++++++ .../api/command/ListOAuthProvidersCmd.java | 147 +++++++++++ .../OauthLoginAPIAuthenticatorCmd.java | 234 ++++++++++++++++++ .../api/command/RegisterOAuthProviderCmd.java | 109 ++++++++ .../api/command/UpdateOAuthProviderCmd.java | 141 +++++++++++ .../command/VerifyOAuthCodeAndGetUserCmd.java | 130 ++++++++++ .../api/response/OauthProviderResponse.java | 127 ++++++++++ .../oauth2/dao/OauthProviderDao.java | 26 ++ .../oauth2/dao/OauthProviderDaoImpl.java | 44 ++++ .../oauth2/github/GithubOAuth2Provider.java | 179 ++++++++++++++ .../oauth2/google/GoogleOAuth2Provider.java | 141 +++++++++++ .../cloudstack/oauth2/vo/OauthProviderVO.java | 128 ++++++++++ .../cloudstack/oauth2/module.properties | 18 ++ .../oauth2/spring-oauth2-context.xml | 56 +++++ .../oauth2/OAuth2AuthManagerImplTest.java | 191 ++++++++++++++ .../oauth2/OAuth2UserAuthenticatorTest.java | 153 ++++++++++++ .../command/DeleteOAuthProviderCmdTest.java | 79 ++++++ .../OauthLoginAPIAuthenticatorCmdTest.java | 85 +++++++ .../command/RegisterOAuthProviderCmdTest.java | 61 +++++ .../VerifyOAuthCodeAndGetUserCmdTest.java | 108 ++++++++ .../google/GoogleOAuth2ProviderTest.java | 148 +++++++++++ .../auth/APIAuthenticationManagerImpl.java | 1 - .../com/cloud/user/AccountManagerImpl.java | 26 +- .../cloud/user/AccountManagerImplTest.java | 27 +- .../cloud/user/MockAccountManagerImpl.java | 5 + tools/apidoc/gen_toc.py | 5 + ui/package.json | 4 +- ui/public/assets/github.svg | 1 + ui/public/assets/google.svg | 1 + ui/public/locales/en.json | 8 + ui/src/api/index.js | 25 ++ ui/src/components/view/DetailsTab.vue | 2 +- ui/src/components/view/ListView.vue | 6 +- ui/src/config/router.js | 9 + ui/src/config/section/config.js | 42 ++++ ui/src/permission.js | 21 +- ui/src/store/modules/user.js | 59 ++++- ui/src/store/mutation-types.js | 2 + ui/src/views/AutogenView.vue | 4 +- ui/src/views/auth/Login.vue | 162 +++++++++++- ui/src/views/dashboard/VerifyOauth.vue | 93 +++++++ 59 files changed, 3427 insertions(+), 31 deletions(-) create mode 100644 api/src/main/java/org/apache/cloudstack/auth/UserOAuth2Authenticator.java create mode 100644 plugins/user-authenticators/oauth2/pom.xml create mode 100644 plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OAuth2AuthManager.java create mode 100644 plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OAuth2AuthManagerImpl.java create mode 100644 plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OAuth2UserAuthenticator.java create mode 100644 plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/DeleteOAuthProviderCmd.java create mode 100644 plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/ListOAuthProvidersCmd.java create mode 100644 plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/OauthLoginAPIAuthenticatorCmd.java create mode 100644 plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/RegisterOAuthProviderCmd.java create mode 100644 plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/UpdateOAuthProviderCmd.java create mode 100644 plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/VerifyOAuthCodeAndGetUserCmd.java create mode 100644 plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/response/OauthProviderResponse.java create mode 100644 plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/dao/OauthProviderDao.java create mode 100644 plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/dao/OauthProviderDaoImpl.java create mode 100644 plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/github/GithubOAuth2Provider.java create mode 100644 plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java create mode 100644 plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/vo/OauthProviderVO.java create mode 100644 plugins/user-authenticators/oauth2/src/main/resources/META-INF/cloudstack/oauth2/module.properties create mode 100644 plugins/user-authenticators/oauth2/src/main/resources/META-INF/cloudstack/oauth2/spring-oauth2-context.xml create mode 100644 plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/OAuth2AuthManagerImplTest.java create mode 100644 plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/OAuth2UserAuthenticatorTest.java create mode 100644 plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/api/command/DeleteOAuthProviderCmdTest.java create mode 100644 plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/api/command/OauthLoginAPIAuthenticatorCmdTest.java create mode 100644 plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/api/command/RegisterOAuthProviderCmdTest.java create mode 100644 plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/api/command/VerifyOAuthCodeAndGetUserCmdTest.java create mode 100644 plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2ProviderTest.java create mode 100644 ui/public/assets/github.svg create mode 100644 ui/public/assets/google.svg create mode 100644 ui/src/views/dashboard/VerifyOauth.vue diff --git a/api/src/main/java/com/cloud/user/AccountService.java b/api/src/main/java/com/cloud/user/AccountService.java index 77a5b442e86..63f5455cfd0 100644 --- a/api/src/main/java/com/cloud/user/AccountService.java +++ b/api/src/main/java/com/cloud/user/AccountService.java @@ -70,6 +70,8 @@ public interface AccountService { UserAccount getActiveUserAccount(String username, Long domainId); + List getActiveUserAccountByEmail(String email, Long domainId); + UserAccount updateUser(UpdateUserCmd updateUserCmd); Account getActiveAccountById(long accountId); diff --git a/api/src/main/java/com/cloud/user/User.java b/api/src/main/java/com/cloud/user/User.java index c50ed3f28af..422e264f10b 100644 --- a/api/src/main/java/com/cloud/user/User.java +++ b/api/src/main/java/com/cloud/user/User.java @@ -24,7 +24,7 @@ public interface User extends OwnedBy, InternalIdentity { // UNKNOWN and NATIVE can be used interchangeably public enum Source { - LDAP, SAML2, SAML2DISABLED, UNKNOWN, NATIVE + OAUTH2, LDAP, SAML2, SAML2DISABLED, UNKNOWN, NATIVE } public static final long UID_SYSTEM = 1; diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index c95e5c8bddb..b65485dcfe4 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -594,6 +594,8 @@ public class ApiConstants { public static final String SERVICE_CAPABILITY_LIST = "servicecapabilitylist"; public static final String CAN_CHOOSE_SERVICE_CAPABILITY = "canchooseservicecapability"; public static final String PROVIDER = "provider"; + public static final String OAUTH_PROVIDER = "oauthprovider"; + public static final String OAUTH_SECRET_KEY = "secretkey"; public static final String MANAGED = "managed"; public static final String CAPACITY_BYTES = "capacitybytes"; public static final String CAPACITY_IOPS = "capacityiops"; @@ -1056,6 +1058,9 @@ public class ApiConstants { public static final String VNF_CONFIGURE_MANAGEMENT = "vnfconfiguremanagement"; public static final String VNF_CIDR_LIST = "vnfcidrlist"; + public static final String CLIENT_ID = "clientid"; + public static final String REDIRECT_URI = "redirecturi"; + /** * This enum specifies IO Drivers, each option controls specific policies on I/O. * Qemu guests support "threads" and "native" options Since 0.8.8 ; "io_uring" is supported Since 6.3.0 (QEMU 5.0). diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/ssh/CreateSSHKeyPairCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/ssh/CreateSSHKeyPairCmd.java index 28bdd4d57f6..521148b596d 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/ssh/CreateSSHKeyPairCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/ssh/CreateSSHKeyPairCmd.java @@ -95,5 +95,4 @@ public class CreateSSHKeyPairCmd extends BaseCmd { response.setObjectName("keypair"); setResponseObject(response); } - - } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/userdata/ListUserDataCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/userdata/ListUserDataCmd.java index aa30066c2a3..87d8883e2e3 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/userdata/ListUserDataCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/userdata/ListUserDataCmd.java @@ -76,5 +76,4 @@ public class ListUserDataCmd extends BaseListProjectAndAccountResourcesCmd { response.setResponseName(getCommandName()); setResponseObject(response); } - - } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/userdata/RegisterUserDataCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/userdata/RegisterUserDataCmd.java index a8a87c40725..f294f7dd8e0 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/userdata/RegisterUserDataCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/userdata/RegisterUserDataCmd.java @@ -142,5 +142,4 @@ public class RegisterUserDataCmd extends BaseCmd { response.setObjectName(ApiConstants.USER_DATA); setResponseObject(response); } - - } +} diff --git a/api/src/main/java/org/apache/cloudstack/auth/UserOAuth2Authenticator.java b/api/src/main/java/org/apache/cloudstack/auth/UserOAuth2Authenticator.java new file mode 100644 index 00000000000..ee3b98b8a4b --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/auth/UserOAuth2Authenticator.java @@ -0,0 +1,53 @@ +// 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 org.apache.cloudstack.auth; + +import com.cloud.utils.component.Adapter; +import com.cloud.utils.exception.CloudRuntimeException; + +public interface UserOAuth2Authenticator extends Adapter { + /** + * Returns the unique name of the provider + * @return returns provider name + */ + String getName(); + + /** + * Returns description about the OAuth2 provider plugin + * @return returns description + */ + String getDescription(); + + /** + * Verifies if the logged in user is + * @return returns true if its valid user + */ + boolean verifyUser(String email, String secretCode); + + /** + * Verifies the code provided by provider and fetches email + * @return returns email + */ + String verifyCodeAndFetchEmail(String secretCode); + + + /** + * Fetches email using the accessToken + * @return returns email + */ + String getUserEmailAddress() throws CloudRuntimeException; +} diff --git a/client/pom.xml b/client/pom.xml index c9b6e9172c1..22cca63e0c8 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -161,6 +161,11 @@ cloud-plugin-user-authenticator-md5 ${project.version} + + org.apache.cloudstack + cloud-plugin-user-authenticator-oauth2 + ${project.version} + org.apache.cloudstack cloud-plugin-user-authenticator-pbkdf2 diff --git a/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml b/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml index 2b2caeaaa66..a36d1243155 100644 --- a/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml +++ b/core/src/main/resources/META-INF/cloudstack/core/spring-core-registry-core-context.xml @@ -33,7 +33,7 @@ class="org.apache.cloudstack.spring.lifecycle.registry.ExtensionRegistry"> - + - + { UserAccount getUserAccount(String username, Long domainId); + List getUserAccountByEmail(String email, Long domainId); + boolean validateUsernameInDomain(String username, Long domainId); UserAccount getUserByApiKey(String apiKey); diff --git a/engine/schema/src/main/java/com/cloud/user/dao/UserAccountDaoImpl.java b/engine/schema/src/main/java/com/cloud/user/dao/UserAccountDaoImpl.java index e0cf48d44a8..c9de9a367ee 100644 --- a/engine/schema/src/main/java/com/cloud/user/dao/UserAccountDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/user/dao/UserAccountDaoImpl.java @@ -59,6 +59,18 @@ public class UserAccountDaoImpl extends GenericDaoBase impl return findOneBy(sc); } + @Override + public List getUserAccountByEmail(String email, Long domainId) { + if (email == null) { + return null; + } + + SearchCriteria sc = createSearchCriteria(); + sc.addAnd("email", SearchCriteria.Op.EQ, email); + sc.addAnd("domainId", SearchCriteria.Op.EQ, domainId); + return listBy(sc); + } + @Override public boolean validateUsernameInDomain(String username, Long domainId) { UserAccount userAcct = getUserAccount(username, domainId); diff --git a/engine/schema/src/main/resources/META-INF/db/schema-41810to41900.sql b/engine/schema/src/main/resources/META-INF/db/schema-41810to41900.sql index 88ff76f0c8c..60b200c6613 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-41810to41900.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-41810to41900.sql @@ -557,3 +557,31 @@ CREATE VIEW `cloud`.`snapshot_view` AS OR (`snapshot_zone_ref`.`zone_id` = `data_center`.`id`)))) LEFT JOIN `resource_tags` ON ((`resource_tags`.`resource_id` = `snapshots`.`id`) AND (`resource_tags`.`resource_type` = 'Snapshot'))); + +UPDATE `cloud`.`configuration` SET + `options` = concat(`options`, ',OAUTH2'), + `default_value` = concat(`default_value`, ',OAUTH2'), + `value` = concat(`value`, ',OAUTH2') +WHERE `name` = 'user.authenticators.order' ; + +UPDATE `cloud`.`configuration` SET + `options` = concat(`options`, ',OAUTH2Auth'), + `default_value` = concat(`default_value`, ',OAUTH2Auth'), + `value` = concat(`value`, ',OAUTH2Auth') +where `name` = 'pluggableApi.authenticators.order' ; + +-- Create table for OAuth provider details +DROP TABLE IF EXISTS `cloud`.`oauth_provider`; +CREATE TABLE `cloud`.`oauth_provider` ( + `id` bigint unsigned NOT NULL auto_increment COMMENT 'id', + `uuid` varchar(40) NOT NULL COMMENT 'unique identifier', + `description` varchar(1024) COMMENT 'description of the provider', + `provider` varchar(40) NOT NULL COMMENT 'name of the provider', + `client_id` varchar(255) NOT NULL COMMENT 'client id which is configured in the provider', + `secret_key` varchar(255) NOT NULL COMMENT 'secret key which is configured in the provider', + `redirect_uri` varchar(255) NOT NULL COMMENT 'redirect uri which is configured in the provider', + `enabled` int(1) NOT NULL DEFAULT 1 COMMENT 'Enabled or disabled', + `created` datetime NOT NULL COMMENT 'date created', + `removed` datetime COMMENT 'date removed if not null', + PRIMARY KEY (`id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java b/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java index bf0b94a71ac..67cfe1df3e1 100644 --- a/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java +++ b/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java @@ -176,6 +176,11 @@ public class MockAccountManager extends ManagerBase implements AccountManager { return null; } + @Override + public List getActiveUserAccountByEmail(String email, Long domainId) { + return null; + } + @Override public User getActiveUser(long arg0) { return _systemUser; diff --git a/plugins/pom.xml b/plugins/pom.xml index af131f08669..06c5fab6a12 100755 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -139,6 +139,7 @@ user-authenticators/ldap user-authenticators/md5 + user-authenticators/oauth2 user-authenticators/pbkdf2 user-authenticators/plain-text user-authenticators/saml2 diff --git a/plugins/user-authenticators/oauth2/pom.xml b/plugins/user-authenticators/oauth2/pom.xml new file mode 100644 index 00000000000..04ea71eacdf --- /dev/null +++ b/plugins/user-authenticators/oauth2/pom.xml @@ -0,0 +1,63 @@ + + + 4.0.0 + cloud-plugin-user-authenticator-oauth2 + Apache CloudStack Plugin - User Authenticator OAuth2 + + org.apache.cloudstack + cloudstack-plugins + 4.19.0.0-SNAPSHOT + ../../pom.xml + + + + org.apache.cloudstack + cloud-utils + ${project.version} + + + org.apache.cloudstack + cloud-framework-config + ${project.version} + + + com.google.apis + google-api-services-docs + v1-rev20220609-1.32.1 + + + com.google.apis + google-api-services-oauth2 + v2-rev20200213-1.32.1 + + + com.google.oauth-client + google-oauth-client-servlet + 1.34.1 + + + com.google.http-client + google-http-client-jackson2 + 1.20.0 + compile + + + diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OAuth2AuthManager.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OAuth2AuthManager.java new file mode 100644 index 00000000000..ece012db3a4 --- /dev/null +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OAuth2AuthManager.java @@ -0,0 +1,61 @@ +// +// 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 org.apache.cloudstack.oauth2; + +import com.cloud.utils.component.PluggableService; +import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator; +import org.apache.cloudstack.auth.UserOAuth2Authenticator; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.oauth2.api.command.RegisterOAuthProviderCmd; +import org.apache.cloudstack.oauth2.api.command.UpdateOAuthProviderCmd; +import org.apache.cloudstack.oauth2.vo.OauthProviderVO; + +import java.util.List; + +public interface OAuth2AuthManager extends PluggableAPIAuthenticator, PluggableService { + public static ConfigKey OAuth2IsPluginEnabled = new ConfigKey("Advanced", Boolean.class, "oauth2.enabled", "false", + "Indicates whether OAuth plugin is enabled or not", false); + public static final ConfigKey OAuth2Plugins = new ConfigKey("Advanced", String.class, "oauth2.plugins", "google,github", + "List of OAuth plugins", true); + public static final ConfigKey OAuth2PluginsExclude = new ConfigKey("Advanced", String.class, "oauth2.plugins.exclude", "", + "List of OAuth plugins which are excluded", true); + + /** + * Lists user OAuth2 provider plugins + * @return list of providers + */ + List listUserOAuth2AuthenticationProviders(); + + /** + * Finds user OAuth2 provider by name + * @param providerName name of the provider + * @return OAuth2 provider + */ + UserOAuth2Authenticator getUserOAuth2AuthenticationProvider(final String providerName); + + String verifyCodeAndFetchEmail(String code, String provider); + + OauthProviderVO registerOauthProvider(RegisterOAuthProviderCmd cmd); + + List listOauthProviders(String provider, String uuid); + + boolean deleteOauthProvider(Long id); + + OauthProviderVO updateOauthProvider(UpdateOAuthProviderCmd cmd); +} diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OAuth2AuthManagerImpl.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OAuth2AuthManagerImpl.java new file mode 100644 index 00000000000..85730651248 --- /dev/null +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OAuth2AuthManagerImpl.java @@ -0,0 +1,233 @@ +// +// 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 org.apache.cloudstack.oauth2; + +import com.cloud.user.dao.UserDao; +import com.cloud.utils.component.Manager; +import com.cloud.utils.component.ManagerBase; +import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.cloudstack.auth.UserOAuth2Authenticator; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; +import org.apache.cloudstack.oauth2.api.command.DeleteOAuthProviderCmd; +import org.apache.cloudstack.oauth2.api.command.ListOAuthProvidersCmd; +import org.apache.cloudstack.oauth2.api.command.OauthLoginAPIAuthenticatorCmd; +import org.apache.cloudstack.oauth2.api.command.RegisterOAuthProviderCmd; +import org.apache.cloudstack.oauth2.api.command.UpdateOAuthProviderCmd; +import org.apache.cloudstack.oauth2.api.command.VerifyOAuthCodeAndGetUserCmd; +import org.apache.cloudstack.oauth2.dao.OauthProviderDao; +import org.apache.cloudstack.oauth2.vo.OauthProviderVO; +import org.apache.commons.lang3.StringUtils; +import org.apache.log4j.Logger; + +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class OAuth2AuthManagerImpl extends ManagerBase implements OAuth2AuthManager, Manager, Configurable { + private static final Logger s_logger = Logger.getLogger(OAuth2AuthManagerImpl.class); + @Inject + private UserDao _userDao; + + @Inject + protected OauthProviderDao _oauthProviderDao; + + protected static Map userOAuth2AuthenticationProvidersMap = new HashMap<>(); + + private List userOAuth2AuthenticationProviders; + + @Override + public List> getAuthCommands() { + List> cmdList = new ArrayList>(); + cmdList.add(OauthLoginAPIAuthenticatorCmd.class); + cmdList.add(ListOAuthProvidersCmd.class); + cmdList.add(VerifyOAuthCodeAndGetUserCmd.class); + return cmdList; + } + + @Override + public boolean start() { + if (isOAuthPluginEnabled()) { + s_logger.info("OAUTH plugin loaded"); + initializeUserOAuth2AuthenticationProvidersMap(); + } else { + s_logger.info("OAUTH plugin not enabled so not loading"); + } + return true; + } + + protected boolean isOAuthPluginEnabled() { + return OAuth2IsPluginEnabled.value(); + } + + @Override + public boolean stop() { + return false; + } + + @Override + public List> getCommands() { + List> cmdList = new ArrayList>(); + cmdList.add(RegisterOAuthProviderCmd.class); + cmdList.add(DeleteOAuthProviderCmd.class); + cmdList.add(UpdateOAuthProviderCmd.class); + + return cmdList; + } + + @Override + public List listUserOAuth2AuthenticationProviders() { + return userOAuth2AuthenticationProviders; + } + + @Override + public UserOAuth2Authenticator getUserOAuth2AuthenticationProvider(String providerName) { + if (StringUtils.isEmpty(providerName)) { + throw new CloudRuntimeException("OAuth2 authentication provider name is empty"); + } + if (!userOAuth2AuthenticationProvidersMap.containsKey(providerName.toLowerCase())) { + throw new CloudRuntimeException(String.format("Failed to find OAuth2 authentication provider by the name: %s.", providerName)); + } + return userOAuth2AuthenticationProvidersMap.get(providerName.toLowerCase()); + } + + public List getUserOAuth2AuthenticationProviders() { + return userOAuth2AuthenticationProviders; + } + + public void setUserOAuth2AuthenticationProviders(final List userOAuth2AuthenticationProviders) { + this.userOAuth2AuthenticationProviders = userOAuth2AuthenticationProviders; + } + + protected void initializeUserOAuth2AuthenticationProvidersMap() { + if (userOAuth2AuthenticationProviders != null) { + for (final UserOAuth2Authenticator userOAuth2Authenticator : userOAuth2AuthenticationProviders) { + userOAuth2AuthenticationProvidersMap.put(userOAuth2Authenticator.getName().toLowerCase(), userOAuth2Authenticator); + } + } + } + + @Override + public String verifyCodeAndFetchEmail(String code, String provider) { + UserOAuth2Authenticator authenticator = getUserOAuth2AuthenticationProvider(provider); + String email = authenticator.verifyCodeAndFetchEmail(code); + + return email; + } + + @Override + public OauthProviderVO registerOauthProvider(RegisterOAuthProviderCmd cmd) { + String description = cmd.getDescription(); + String provider = cmd.getProvider(); + String clientId = cmd.getClientId(); + String redirectUri = cmd.getRedirectUri(); + String secretKey = cmd.getSecretKey(); + + if (!isOAuthPluginEnabled()) { + throw new CloudRuntimeException("OAuth is not enabled, please enable to register"); + } + OauthProviderVO providerVO = _oauthProviderDao.findByProvider(provider); + if (providerVO != null) { + throw new CloudRuntimeException(String.format("Provider with the name %s is already registered", provider)); + } + + return saveOauthProvider(provider, description, clientId, secretKey, redirectUri); + } + + @Override + public List listOauthProviders(String provider, String uuid) { + List providers; + if (uuid != null) { + providers = Collections.singletonList(_oauthProviderDao.findByUuid(uuid)); + } else if (StringUtils.isNotBlank(provider)) { + providers = Collections.singletonList(_oauthProviderDao.findByProvider(provider)); + } else { + providers = _oauthProviderDao.listAll(); + } + return providers; + } + + @Override + public OauthProviderVO updateOauthProvider(UpdateOAuthProviderCmd cmd) { + Long id = cmd.getId(); + String description = cmd.getDescription(); + String clientId = cmd.getClientId(); + String redirectUri = cmd.getRedirectUri(); + String secretKey = cmd.getSecretKey(); + Boolean enabled = cmd.getEnabled(); + + OauthProviderVO providerVO = _oauthProviderDao.findById(id); + if (providerVO == null) { + throw new CloudRuntimeException("Provider with the given id is not there"); + } + + if (StringUtils.isNotEmpty(description)) { + providerVO.setDescription(description); + } + if (StringUtils.isNotEmpty(clientId)) { + providerVO.setClientId(clientId); + } + if (StringUtils.isNotEmpty(redirectUri)) { + providerVO.setRedirectUri(redirectUri); + } + if (StringUtils.isNotEmpty(secretKey)) { + providerVO.setSecretKey(secretKey); + } + if (enabled != null) { + providerVO.setEnabled(enabled); + } + + _oauthProviderDao.update(id, providerVO); + + return _oauthProviderDao.findById(id); + } + + private OauthProviderVO saveOauthProvider(String provider, String description, String clientId, String secretKey, String redirectUri) { + final OauthProviderVO oauthProviderVO = new OauthProviderVO(); + + oauthProviderVO.setProvider(provider); + oauthProviderVO.setDescription(description); + oauthProviderVO.setClientId(clientId); + oauthProviderVO.setSecretKey(secretKey); + oauthProviderVO.setRedirectUri(redirectUri); + oauthProviderVO.setEnabled(true); + + _oauthProviderDao.persist(oauthProviderVO); + + return oauthProviderVO; + } + + @Override + public boolean deleteOauthProvider(Long id) { + return _oauthProviderDao.remove(id); + } + + @Override + public String getConfigComponentName() { + return "OAUTH2-PLUGIN"; + } + + @Override + public ConfigKey[] getConfigKeys() { + return new ConfigKey[] {OAuth2IsPluginEnabled, OAuth2Plugins, OAuth2PluginsExclude}; + } +} diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OAuth2UserAuthenticator.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OAuth2UserAuthenticator.java new file mode 100644 index 00000000000..8484a5ef798 --- /dev/null +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OAuth2UserAuthenticator.java @@ -0,0 +1,78 @@ +// +// 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 org.apache.cloudstack.oauth2; + +import com.cloud.user.User; +import com.cloud.user.UserAccount; +import com.cloud.user.dao.UserAccountDao; +import com.cloud.user.dao.UserDao; +import com.cloud.utils.Pair; +import com.cloud.utils.component.AdapterBase; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.auth.UserAuthenticator; +import org.apache.cloudstack.auth.UserOAuth2Authenticator; +import org.apache.log4j.Logger; + +import javax.inject.Inject; +import java.util.Map; + +public class OAuth2UserAuthenticator extends AdapterBase implements UserAuthenticator { + public static final Logger s_logger = Logger.getLogger(OAuth2UserAuthenticator.class); + + @Inject + private UserAccountDao _userAccountDao; + @Inject + private UserDao _userDao; + + @Inject + private OAuth2AuthManager _userOAuth2mgr; + + @Override + public Pair authenticate(String username, String password, Long domainId, Map requestParameters) { + if (s_logger.isDebugEnabled()) { + s_logger.debug("Trying OAuth2 auth for user: " + username); + } + + final UserAccount userAccount = _userAccountDao.getUserAccount(username, domainId); + if (userAccount == null) { + s_logger.debug("Unable to find user with " + username + " in domain " + domainId + ", or user source is not OAUTH2"); + return new Pair(false, null); + } else { + User user = _userDao.getUser(userAccount.getId()); + final String[] provider = (String[])requestParameters.get(ApiConstants.PROVIDER); + final String[] emailArray = (String[])requestParameters.get(ApiConstants.EMAIL); + final String[] secretCodeArray = (String[])requestParameters.get(ApiConstants.SECRET_CODE); + String oauthProvider = ((provider == null) ? null : provider[0]); + String email = ((emailArray == null) ? null : emailArray[0]); + String secretCode = ((secretCodeArray == null) ? null : secretCodeArray[0]); + + UserOAuth2Authenticator authenticator = _userOAuth2mgr.getUserOAuth2AuthenticationProvider(oauthProvider); + if (user != null && authenticator.verifyUser(email, secretCode)) { + return new Pair(true, null); + } + } + // Deny all by default + return new Pair(false, ActionOnFailedAuthentication.INCREMENT_INCORRECT_LOGIN_ATTEMPT_COUNT); + } + + @Override + public String encode(String password) { + return null; + } +} diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/DeleteOAuthProviderCmd.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/DeleteOAuthProviderCmd.java new file mode 100644 index 00000000000..6cd3156f68a --- /dev/null +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/DeleteOAuthProviderCmd.java @@ -0,0 +1,87 @@ +// 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 org.apache.cloudstack.oauth2.api.command; + +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.oauth2.OAuth2AuthManager; +import org.apache.cloudstack.oauth2.api.response.OauthProviderResponse; +import org.apache.log4j.Logger; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.context.CallContext; + +import javax.inject.Inject; + +@APICommand(name = "deleteOauthProvider", description = "Deletes the registered OAuth provider", responseObject = SuccessResponse.class, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.19.0") +public class DeleteOAuthProviderCmd extends BaseCmd { + public static final Logger s_logger = Logger.getLogger(DeleteOAuthProviderCmd.class.getName()); + + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = OauthProviderResponse.class, required = true, description = "id of the OAuth provider to be deleted") + private Long id; + + @Inject + OAuth2AuthManager _oauthMgr; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getId() { + return id; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public Long getApiResourceId() { + return id; + } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.User; + } + + @Override + public void execute() { + boolean result = _oauthMgr.deleteOauthProvider(getId()); + if (result) { + SuccessResponse response = new SuccessResponse(getCommandName()); + this.setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to delete the OAuth provider"); + } + } +} diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/ListOAuthProvidersCmd.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/ListOAuthProvidersCmd.java new file mode 100644 index 00000000000..597283ae33e --- /dev/null +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/ListOAuthProvidersCmd.java @@ -0,0 +1,147 @@ +// 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 org.apache.cloudstack.oauth2.api.command; + +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import com.cloud.api.response.ApiResponseSerializer; +import com.cloud.user.Account; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseListCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.auth.APIAuthenticationType; +import org.apache.cloudstack.api.auth.APIAuthenticator; +import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.auth.UserOAuth2Authenticator; +import org.apache.cloudstack.oauth2.OAuth2AuthManager; +import org.apache.cloudstack.oauth2.api.response.OauthProviderResponse; +import org.apache.cloudstack.oauth2.vo.OauthProviderVO; +import org.apache.commons.lang.ArrayUtils; +import org.apache.log4j.Logger; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +@APICommand(name = "listOauthProvider", description = "List OAuth providers registered", responseObject = OauthProviderResponse.class, entityType = {}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}, since = "4.19.0") +public class ListOAuthProvidersCmd extends BaseListCmd implements APIAuthenticator { + public static final Logger s_logger = Logger.getLogger(ListOAuthProvidersCmd.class.getName()); + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = OauthProviderResponse.class, description = "the ID of the OAuth provider") + private String id; + + @Parameter(name = ApiConstants.PROVIDER, type = CommandType.STRING, description = "Name of the provider") + private String provider; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + public String getId() { + return id; + } + + public String getProvider() { + return provider; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + OAuth2AuthManager _oauth2mgr; + + @Override + public long getEntityOwnerId() { + return Account.Type.NORMAL.ordinal(); + } + + @Override + public void execute() throws ServerApiException { + // We should never reach here + throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "This is an authentication api, cannot be used directly"); + } + + @Override + public String authenticate(String command, Map params, HttpSession session, InetAddress remoteAddress, String responseType, StringBuilder auditTrailSb, HttpServletRequest req, HttpServletResponse resp) throws ServerApiException { + final String[] idArray = (String[])params.get(ApiConstants.ID); + final String[] providerArray = (String[])params.get(ApiConstants.PROVIDER); + if (ArrayUtils.isNotEmpty(idArray)) { + id = idArray[0]; + } + if (ArrayUtils.isNotEmpty(providerArray)) { + provider = providerArray[0]; + } + + List resultList = _oauth2mgr.listOauthProviders(provider, id); + List userOAuth2AuthenticatorPlugins = _oauth2mgr.listUserOAuth2AuthenticationProviders(); + List authenticatorPluginNames = new ArrayList<>(); + for (UserOAuth2Authenticator authenticator : userOAuth2AuthenticatorPlugins) { + String name = authenticator.getName(); + authenticatorPluginNames.add(name); + } + List responses = new ArrayList<>(); + for (OauthProviderVO result : resultList) { + OauthProviderResponse r = new OauthProviderResponse(result.getUuid(), result.getProvider(), + result.getDescription(), result.getClientId(), result.getSecretKey(), result.getRedirectUri()); + if (OAuth2AuthManager.OAuth2IsPluginEnabled.value() && authenticatorPluginNames.contains(result.getProvider()) && result.isEnabled()) { + r.setEnabled(true); + } else { + r.setEnabled(false); + } + r.setObjectName(ApiConstants.OAUTH_PROVIDER); + responses.add(r); + } + + ListResponse response = new ListResponse<>(); + response.setResponses(responses, resultList.size()); + response.setResponseName(getCommandName()); + setResponseObject(response); + + return ApiResponseSerializer.toSerializedString(response, responseType); + } + + @Override + public APIAuthenticationType getAPIType() { + return null; + } + + @Override + public void setAuthenticators(List authenticators) { + for (PluggableAPIAuthenticator authManager: authenticators) { + if (authManager != null && authManager instanceof OAuth2AuthManager) { + _oauth2mgr = (OAuth2AuthManager) authManager; + } + } + if (_oauth2mgr == null) { + s_logger.error("No suitable Pluggable Authentication Manager found for listing OAuth providers"); + } + } +} diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/OauthLoginAPIAuthenticatorCmd.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/OauthLoginAPIAuthenticatorCmd.java new file mode 100644 index 00000000000..928fa76780a --- /dev/null +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/OauthLoginAPIAuthenticatorCmd.java @@ -0,0 +1,234 @@ +// 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 org.apache.cloudstack.oauth2.api.command; + +import com.cloud.api.ApiServlet; +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; +import com.cloud.user.Account; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.auth.APIAuthenticationType; +import org.apache.cloudstack.api.auth.APIAuthenticator; +import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator; +import org.apache.cloudstack.api.response.LoginCmdResponse; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.log4j.Logger; +import org.jetbrains.annotations.Nullable; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.util.List; +import java.util.Map; +import java.net.InetAddress; + +import static org.apache.cloudstack.oauth2.OAuth2AuthManager.OAuth2IsPluginEnabled; + +@APICommand(name = "oauthlogin", description = "Logs a user into the CloudStack after successful verification of OAuth secret code from the particular provider." + + "A successful login attempt will generate a JSESSIONID cookie value that can be passed in subsequent Query command calls until the \"logout\" command has been issued or the session has expired.", + requestHasSensitiveInfo = true, responseObject = LoginCmdResponse.class, entityType = {}, since = "4.19.0") +public class OauthLoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthenticator { + + public static final Logger s_logger = Logger.getLogger(OauthLoginAPIAuthenticatorCmd.class.getName()); + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + @Parameter(name = ApiConstants.PROVIDER, type = CommandType.STRING, description = "Name of the provider", required = true) + private String provider; + + @Parameter(name = ApiConstants.EMAIL, type = CommandType.STRING, description = "Email id with which user tried to login using OAuth provider", required = true) + private String email; + + @Parameter(name = ApiConstants.DOMAIN, type = CommandType.STRING, description = "Path of the domain that the user belongs to. Example: domain=/com/cloud/internal. If no domain is passed in, the ROOT (/) domain is assumed.") + private String domain; + + @Parameter(name = ApiConstants.DOMAIN__ID, type = CommandType.LONG, description = "The id of the domain that the user belongs to. If both domain and domainId are passed in, \"domainId\" parameter takes precedence.") + private Long domainId; + + @Parameter(name = ApiConstants.SECRET_CODE, type = CommandType.STRING, description = "Code that is provided by OAuth provider (Eg. google, github) after successful login") + private String secretCode; + + @Inject + ApiServerService _apiServer; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public String getProvider() { + return provider; + } + + public String getEmail() { + return email; + } + + public String getDomainName() { + return domain; + } + + public Long getDomainId() { + return domainId; + } + + public String getSecretCode() { + return secretCode; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public long getEntityOwnerId() { + return Account.Type.NORMAL.ordinal(); + } + + @Override + public void execute() throws ServerApiException { + // We should never reach here + throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "This is an authentication api, cannot be used directly"); + } + + @Override + public String authenticate(String command, Map params, HttpSession session, InetAddress remoteAddress, String responseType, StringBuilder auditTrailSb, final HttpServletRequest req, final HttpServletResponse resp) throws ServerApiException { + if (!OAuth2IsPluginEnabled.value()) { + throw new CloudAuthenticationException("OAuth is not enabled in CloudStack, users cannot login using OAuth"); + } + final String[] provider = (String[])params.get(ApiConstants.PROVIDER); + final String[] emailArray = (String[])params.get(ApiConstants.EMAIL); + final String[] secretCodeArray = (String[])params.get(ApiConstants.SECRET_CODE); + + String oauthProvider = ((provider == null) ? null : provider[0]); + String email = ((emailArray == null) ? null : emailArray[0]); + String secretCode = ((secretCodeArray == null) ? null : secretCodeArray[0]); + if (StringUtils.isAnyEmpty(oauthProvider, email, secretCode)) { + throw new CloudAuthenticationException("OAuth provider, email, secretCode any of these cannot be null"); + } + + Long domainId = getDomainIdFromParams(params, auditTrailSb, responseType); + final String[] domainName = (String[])params.get(ApiConstants.DOMAIN); + String domain = getDomainName(auditTrailSb, domainName); + + return doOauthAuthentication(session, domainId, domain, email, params, remoteAddress, responseType, auditTrailSb); + } + + private String doOauthAuthentication(HttpSession session, Long domainId, String domain, String email, Map params, InetAddress remoteAddress, String responseType, StringBuilder auditTrailSb) { + String serializedResponse = null; + + 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 List userAccounts = _accountService.getActiveUserAccountByEmail(email, domainId); + if (CollectionUtils.isEmpty(userAccounts)) { + throw new CloudAuthenticationException("User not found in CloudStack to login. If user belongs to any domain, please provide it."); + } + if (userAccounts.size() > 1) { + throw new CloudAuthenticationException("Multiple Users found in CloudStack. If user belongs to any specific domain, please provide it."); + } + UserAccount userAccount = userAccounts.get(0); + if (userAccount != null && User.Source.SAML2 == userAccount.getSource()) { + throw new CloudAuthenticationException("User is not allowed CloudStack login"); + } + return ApiResponseSerializer.toSerializedString(_apiServer.loginUser(session, userAccount.getUsername(), null, domainId, domain, remoteAddress, params), + responseType); + } catch (final CloudAuthenticationException ex) { + ApiServlet.invalidateHttpSession(session, "fall through to API key,"); + String msg = String.format("%s", ex.getMessage() != null ? + ex.getMessage() : + "failed to authenticate user, check if username/password are correct"); + auditTrailSb.append(" " + ApiErrorCode.ACCOUNT_ERROR + " " + msg); + serializedResponse = _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(), msg, params, responseType); + if (s_logger.isTraceEnabled()) { + s_logger.trace(msg); + } + } + + // We should not reach here and if we do we throw an exception + throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, serializedResponse); + } + + protected Long getDomainIdFromParams(Map params, StringBuilder auditTrailSb, String responseType) { + String[] domainIdArr = (String[])params.get(ApiConstants.DOMAIN_ID); + + if (domainIdArr == null) { + domainIdArr = (String[])params.get(ApiConstants.DOMAIN__ID); + } + Long domainId = null; + if ((domainIdArr != null) && (domainIdArr.length > 0)) { + try { + //check if UUID is passed in for domain + domainId = _apiServer.fetchDomainId(domainIdArr[0]); + if (domainId == null) { + domainId = Long.parseLong(domainIdArr[0]); + } + auditTrailSb.append(" domainid=" + domainId);// building the params for POST call + } catch (final NumberFormatException e) { + s_logger.warn("Invalid domain id entered by user"); + auditTrailSb.append(" " + HttpServletResponse.SC_UNAUTHORIZED + " " + "Invalid domain id entered, please enter a valid one"); + throw new ServerApiException(ApiErrorCode.UNAUTHORIZED, + _apiServer.getSerializedApiError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid domain id entered, please enter a valid one", params, + responseType)); + } + } + return domainId; + } + + @Nullable + protected String getDomainName(StringBuilder auditTrailSb, String[] domainName) { + String domain = null; + if (domainName != null) { + domain = domainName[0]; + auditTrailSb.append(" domain=" + domain); + if (domain != null) { + // ensure domain starts with '/' and ends with '/' + if (!domain.endsWith("/")) { + domain += '/'; + } + if (!domain.startsWith("/")) { + domain = "/" + domain; + } + } + } + return domain; + } + + @Override + public APIAuthenticationType getAPIType() { + return APIAuthenticationType.LOGIN_API; + } + + @Override + public void setAuthenticators(List authenticators) { + } +} diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/RegisterOAuthProviderCmd.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/RegisterOAuthProviderCmd.java new file mode 100644 index 00000000000..b31cbde97c5 --- /dev/null +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/RegisterOAuthProviderCmd.java @@ -0,0 +1,109 @@ +//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 org.apache.cloudstack.oauth2.api.command; + +import javax.inject.Inject; +import javax.persistence.EntityExistsException; + +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.oauth2.OAuth2AuthManager; +import org.apache.cloudstack.oauth2.api.response.OauthProviderResponse; +import org.apache.cloudstack.oauth2.vo.OauthProviderVO; +import org.apache.commons.collections.MapUtils; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.context.CallContext; + +import com.cloud.exception.ConcurrentOperationException; + +import java.util.Collection; +import java.util.Map; + +@APICommand(name = "registerOauthProvider", responseObject = SuccessResponse.class, description = "Register the OAuth2 provider in CloudStack", since = "4.19.0") +public class RegisterOAuthProviderCmd extends BaseCmd { + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, required = true, description = "Description of the OAuth Provider") + private String description; + + @Parameter(name = ApiConstants.PROVIDER, type = CommandType.STRING, description = "Name of the provider from the list of OAuth providers supported in CloudStack", required = true) + private String provider; + + @Parameter(name = ApiConstants.CLIENT_ID, type = CommandType.STRING, description = "Client ID pre-registered in the specific OAuth provider", required = true) + private String clientId; + + @Parameter(name = ApiConstants.OAUTH_SECRET_KEY, type = CommandType.STRING, description = "Secret Key pre-registered in the specific OAuth provider", required = true) + private String secretKey; + + @Parameter(name = ApiConstants.REDIRECT_URI, type = CommandType.STRING, description = "Redirect URI pre-registered in the specific OAuth provider", required = true) + private String redirectUri; + + @Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP, + description = "Any OAuth provider details in key/value pairs using format details[i].keyname=keyvalue. Example: details[0].clientsecret=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") + protected Map details; + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + public String getDescription() { + return description; + } + + public String getProvider() { + return provider; + } + + public String getClientId() { + return clientId; + } + + public String getSecretKey() { + return secretKey; + } + + public String getRedirectUri() { + return redirectUri; + } + + public Map getDetails() { + if (MapUtils.isEmpty(details)) { + return null; + } + Collection paramsCollection = this.details.values(); + return (Map) (paramsCollection.toArray())[0]; + } + + @Inject + OAuth2AuthManager _oauth2mgr; + + @Override + public void execute() throws ServerApiException, ConcurrentOperationException, EntityExistsException { + OauthProviderVO provider = _oauth2mgr.registerOauthProvider(this); + + OauthProviderResponse response = new OauthProviderResponse(provider.getUuid(), provider.getProvider(), + provider.getDescription(), provider.getClientId(), provider.getSecretKey(), provider.getRedirectUri()); + response.setResponseName(getCommandName()); + response.setObjectName(ApiConstants.OAUTH_PROVIDER); + setResponseObject(response); + } +} diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/UpdateOAuthProviderCmd.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/UpdateOAuthProviderCmd.java new file mode 100644 index 00000000000..b38423ffd48 --- /dev/null +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/UpdateOAuthProviderCmd.java @@ -0,0 +1,141 @@ +// 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 org.apache.cloudstack.oauth2.api.command; + +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.auth.UserOAuth2Authenticator; +import org.apache.cloudstack.oauth2.OAuth2AuthManager; +import org.apache.cloudstack.oauth2.api.response.OauthProviderResponse; +import org.apache.cloudstack.oauth2.vo.OauthProviderVO; +import org.apache.log4j.Logger; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.context.CallContext; + +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.List; + +@APICommand(name = "updateOauthProvider", description = "Updates the registered OAuth provider details", responseObject = OauthProviderResponse.class, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.19.0") +public final class UpdateOAuthProviderCmd extends BaseCmd { + public static final Logger s_logger = Logger.getLogger(UpdateOAuthProviderCmd.class.getName()); + + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = OauthProviderResponse.class, required = true, description = "id of the OAuth provider to be updated") + private Long id; + + @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, description = "Description of the OAuth Provider") + private String description; + + @Parameter(name = ApiConstants.CLIENT_ID, type = CommandType.STRING, description = "Client ID pre-registered in the specific OAuth provider") + private String clientId; + + @Parameter(name = ApiConstants.OAUTH_SECRET_KEY, type = CommandType.STRING, description = "Secret Key pre-registered in the specific OAuth provider") + private String secretKey; + + @Parameter(name = ApiConstants.REDIRECT_URI, type = CommandType.STRING, description = "Redirect URI pre-registered in the specific OAuth provider") + private String redirectUri; + + @Parameter(name = ApiConstants.ENABLED, type = CommandType.BOOLEAN, description = "OAuth provider will be enabled or disabled based on this value") + private Boolean enabled; + + @Inject + OAuth2AuthManager _oauthMgr; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getId() { + return id; + } + + public String getDescription() { + return description; + } + + public String getClientId() { + return clientId; + } + + public String getSecretKey() { + return secretKey; + } + + public String getRedirectUri() { + return redirectUri; + } + + public Boolean getEnabled() { + return enabled; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public long getEntityOwnerId() { + return CallContext.current().getCallingAccount().getId(); + } + + @Override + public Long getApiResourceId() { + return id; + } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.User; + } + + @Override + public void execute() { + OauthProviderVO result = _oauthMgr.updateOauthProvider(this); + if (result != null) { + OauthProviderResponse r = new OauthProviderResponse(result.getUuid(), result.getProvider(), + result.getDescription(), result.getClientId(), result.getSecretKey(), result.getRedirectUri()); + + List userOAuth2AuthenticatorPlugins = _oauthMgr.listUserOAuth2AuthenticationProviders(); + List authenticatorPluginNames = new ArrayList<>(); + for (UserOAuth2Authenticator authenticator : userOAuth2AuthenticatorPlugins) { + String name = authenticator.getName(); + authenticatorPluginNames.add(name); + } + if (OAuth2AuthManager.OAuth2IsPluginEnabled.value() && authenticatorPluginNames.contains(result.getProvider()) && result.isEnabled()) { + r.setEnabled(true); + } else { + r.setEnabled(false); + } + + r.setObjectName(ApiConstants.OAUTH_PROVIDER); + r.setResponseName(getCommandName()); + this.setResponseObject(r); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to update OAuth provider"); + } + } +} diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/VerifyOAuthCodeAndGetUserCmd.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/VerifyOAuthCodeAndGetUserCmd.java new file mode 100644 index 00000000000..5dbeef10dcb --- /dev/null +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/VerifyOAuthCodeAndGetUserCmd.java @@ -0,0 +1,130 @@ +// 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 org.apache.cloudstack.oauth2.api.command; + +import java.net.InetAddress; +import java.util.List; +import java.util.Map; + +import com.cloud.api.response.ApiResponseSerializer; +import com.cloud.user.Account; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseListCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.auth.APIAuthenticationType; +import org.apache.cloudstack.api.auth.APIAuthenticator; +import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator; +import org.apache.cloudstack.api.response.UserResponse; +import org.apache.cloudstack.oauth2.OAuth2AuthManager; +import org.apache.cloudstack.oauth2.api.response.OauthProviderResponse; +import org.apache.commons.lang.ArrayUtils; +import org.apache.log4j.Logger; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +@APICommand(name = "verifyOAuthCodeAndGetUser", description = "Verify the OAuth Code and fetch the corresponding user from provider", responseObject = OauthProviderResponse.class, entityType = {}, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}, since = "4.19.0") +public class VerifyOAuthCodeAndGetUserCmd extends BaseListCmd implements APIAuthenticator { + public static final Logger s_logger = Logger.getLogger(VerifyOAuthCodeAndGetUserCmd.class.getName()); + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.PROVIDER, type = CommandType.STRING, description = "Name of the provider", required = true) + private String provider; + + @Parameter(name = ApiConstants.SECRET_CODE, type = CommandType.STRING, description = "Code that is provided by OAuth provider (Eg. google, github) after successful login") + private String secretCode; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public String getProvider() { + return provider; + } + + public String getSecretCode() { + return secretCode; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + protected OAuth2AuthManager _oauth2mgr; + + @Override + public long getEntityOwnerId() { + return Account.Type.NORMAL.ordinal(); + } + + @Override + public void execute() throws ServerApiException { + // We should never reach here + throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "This is an authentication api, cannot be used directly"); + } + + @Override + public String authenticate(String command, Map params, HttpSession session, InetAddress remoteAddress, String responseType, StringBuilder auditTrailSb, HttpServletRequest req, HttpServletResponse resp) throws ServerApiException { + final String[] secretcodeArray = (String[])params.get(ApiConstants.SECRET_CODE); + final String[] providerArray = (String[])params.get(ApiConstants.PROVIDER); + if (ArrayUtils.isNotEmpty(secretcodeArray)) { + secretCode = secretcodeArray[0]; + } + if (ArrayUtils.isNotEmpty(providerArray)) { + provider = providerArray[0]; + } + + String email = _oauth2mgr.verifyCodeAndFetchEmail(secretCode, provider); + if (email != null) { + UserResponse response = new UserResponse(); + response.setEmail(email); + response.setResponseName(getCommandName()); + response.setObjectName("oauthemail"); + + return ApiResponseSerializer.toSerializedString(response, responseType); + } + + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Unable to verify the code provided"); + } + + @Override + public APIAuthenticationType getAPIType() { + return null; + } + + @Override + public void setAuthenticators(List authenticators) { + for (PluggableAPIAuthenticator authManager: authenticators) { + if (authManager != null && authManager instanceof OAuth2AuthManager) { + _oauth2mgr = (OAuth2AuthManager) authManager; + } + } + if (_oauth2mgr == null) { + s_logger.error("No suitable Pluggable Authentication Manager found for listing OAuth providers"); + } + } +} diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/response/OauthProviderResponse.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/response/OauthProviderResponse.java new file mode 100644 index 00000000000..e0c40bef9b4 --- /dev/null +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/response/OauthProviderResponse.java @@ -0,0 +1,127 @@ +// 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 org.apache.cloudstack.oauth2.api.response; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.EntityReference; +import org.apache.cloudstack.oauth2.vo.OauthProviderVO; + +@EntityReference(value = OauthProviderVO.class) +public class OauthProviderResponse extends BaseResponse { + + @SerializedName(ApiConstants.ID) + @Param(description = "ID of the provider") + private String id; + + @SerializedName(ApiConstants.PROVIDER) + @Param(description = "Name of the provider") + private String provider; + + @SerializedName(ApiConstants.NAME) + @Param(description = "Name of the provider") + private String name; + + @SerializedName(ApiConstants.DESCRIPTION) + @Param(description = "Description of the provider registered") + private String description; + + @SerializedName(ApiConstants.CLIENT_ID) + @Param(description = "Client ID registered in the OAuth provider") + private String clientId; + + @SerializedName(ApiConstants.OAUTH_SECRET_KEY) + @Param(description = "Secret key registered in the OAuth provider") + private String secretKey; + + @SerializedName(ApiConstants.REDIRECT_URI) + @Param(description = "Redirect URI registered in the OAuth provider") + private String redirectUri; + + @SerializedName(ApiConstants.ENABLED) + @Param(description = "Whether the OAuth provider is enabled or not") + private boolean enabled; + + public OauthProviderResponse(String id, String provider, String description, String clientId, String secretKey, String redirectUri) { + this.id = id; + this.provider = provider; + this.name = provider; + this.description = description; + this.clientId = clientId; + this.secretKey = secretKey; + this.redirectUri = redirectUri; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getProvider() { + return provider; + } + + public void setProvider(String provider) { + this.provider = provider; + } + + public String getDescription() { + return description; + } + + + public void setDescription(String description) { + this.description = description; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public boolean getEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getRedirectUri() { + return redirectUri; + } + + public void setRedirectUri(String redirectUri) { + this.redirectUri = redirectUri; + } + + public String getSecretKey() { + return secretKey; + } + + public void setSecretKey(String secretKey) { + this.secretKey = secretKey; + } +} diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/dao/OauthProviderDao.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/dao/OauthProviderDao.java new file mode 100644 index 00000000000..31738ac75a0 --- /dev/null +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/dao/OauthProviderDao.java @@ -0,0 +1,26 @@ +// 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 org.apache.cloudstack.oauth2.dao; + +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.oauth2.vo.OauthProviderVO; + +public interface OauthProviderDao extends GenericDao { + + public OauthProviderVO findByProvider(String provider); + +} diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/dao/OauthProviderDaoImpl.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/dao/OauthProviderDaoImpl.java new file mode 100644 index 00000000000..27eea4d22a6 --- /dev/null +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/dao/OauthProviderDaoImpl.java @@ -0,0 +1,44 @@ +// 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 org.apache.cloudstack.oauth2.dao; + +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import org.apache.cloudstack.oauth2.vo.OauthProviderVO; + +public class OauthProviderDaoImpl extends GenericDaoBase implements OauthProviderDao { + + private final SearchBuilder oauthProviderSearchByName; + + public OauthProviderDaoImpl() { + super(); + + oauthProviderSearchByName = createSearchBuilder(); + oauthProviderSearchByName.and("provider", oauthProviderSearchByName.entity().getProvider(), SearchCriteria.Op.EQ); + oauthProviderSearchByName.done(); + } + + @Override + public OauthProviderVO findByProvider(String provider) { + SearchCriteria sc = oauthProviderSearchByName.create(); + sc.setParameters("provider", provider); + + return findOneBy(sc); + } +} diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/github/GithubOAuth2Provider.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/github/GithubOAuth2Provider.java new file mode 100644 index 00000000000..e4a7fae101f --- /dev/null +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/github/GithubOAuth2Provider.java @@ -0,0 +1,179 @@ +//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 +//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 org.apache.cloudstack.oauth2.github; + +import com.cloud.utils.component.AdapterBase; +import com.cloud.utils.exception.CloudRuntimeException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.cloudstack.auth.UserOAuth2Authenticator; +import org.apache.cloudstack.oauth2.dao.OauthProviderDao; +import org.apache.cloudstack.oauth2.vo.OauthProviderVO; +import org.apache.commons.lang3.StringUtils; + +import javax.inject.Inject; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class GithubOAuth2Provider extends AdapterBase implements UserOAuth2Authenticator { + + @Inject + OauthProviderDao _oauthProviderDao; + + private String accessToken = null; + + @Override + public String getName() { + return "github"; + } + + @Override + public String getDescription() { + return "Github OAuth2 Provider Plugin"; + } + + @Override + public boolean verifyUser(String email, String secretCode) { + if (StringUtils.isAnyEmpty(email, secretCode)) { + throw new CloudRuntimeException(String.format("Either email or secretcode should not be null/empty")); + } + + OauthProviderVO providerVO = _oauthProviderDao.findByProvider(getName()); + if (providerVO == null) { + throw new CloudRuntimeException("Github provider is not registered, so user cannot be verified"); + } + + String verifiedEmail = getUserEmailAddress(); + if (verifiedEmail == null || !email.equals(verifiedEmail)) { + throw new CloudRuntimeException("Unable to verify the email address with the provided secret"); + } + + clearAccessToken(); + + return true; + } + + @Override + public String verifyCodeAndFetchEmail(String secretCode) { + String accessToken = getAccessToken(secretCode); + if (accessToken == null) { + return null; + } + return getUserEmailAddress(); + } + + protected String getAccessToken(String secretCode) throws CloudRuntimeException { + OauthProviderVO githubProvider = _oauthProviderDao.findByProvider(getName()); + String tokenUrl = "https://github.com/login/oauth/access_token"; + String generatedAccessToken = null; + try { + URL url = new URL(tokenUrl); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setDoOutput(true); + + String jsonParams = "{\"client_id\":\"" + githubProvider.getClientId() + "\",\"client_secret\":\"" + githubProvider.getSecretKey() + "\",\"code\":\"" + secretCode + "\"}"; + + try (OutputStream os = connection.getOutputStream()) { + byte[] input = jsonParams.getBytes("utf-8"); + os.write(input, 0, input.length); + } + + int responseCode = connection.getResponseCode(); + if (responseCode == HttpURLConnection.HTTP_OK) { + try (BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()))) { + String inputLine; + StringBuilder response = new StringBuilder(); + while ((inputLine = in.readLine()) != null) { + response.append(inputLine); + } + String regexPattern = "access_token=([^&]+)"; + Pattern pattern = Pattern.compile(regexPattern); + Matcher matcher = pattern.matcher(response); + if (matcher.find()) { + generatedAccessToken = matcher.group(1); + } else { + throw new CloudRuntimeException("Could not fetch access token from the given code"); + } + } + } else { + throw new CloudRuntimeException("HTTP Request while fetching access token from github failed with error code: " + responseCode); + } + } catch (IOException e) { + throw new CloudRuntimeException(String.format("Error while trying to fetch the github access token : %s", e.getMessage())); + } + + accessToken = generatedAccessToken; + return accessToken; + } + + public String getUserEmailAddress() throws CloudRuntimeException { + if (accessToken == null) { + throw new CloudRuntimeException("Access Token not found to fetch the email address"); + } + + String apiUrl = "https://api.github.com/user/emails"; + String email = null; + try { + URL url = new URL(apiUrl); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + connection.setRequestProperty("Authorization", "token " + accessToken); + + int responseCode = connection.getResponseCode(); + if (responseCode == HttpURLConnection.HTTP_OK) { + try (BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()))) { + String inputLine; + StringBuilder response = new StringBuilder(); + while ((inputLine = in.readLine()) != null) { + response.append(inputLine); + } + + try { + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode jsonNode = objectMapper.readTree(response.toString()); + if (jsonNode != null && jsonNode.isArray()) { + JsonNode firstObject = jsonNode.get(0); + email = firstObject.get("email").asText(); + } else { + throw new CloudRuntimeException("Invalid JSON format found while accessing email from github"); + } + } catch (Exception e) { + throw new CloudRuntimeException(String.format("Error occurred while accessing email from github: %s", e.getMessage())); + } } + } else { + throw new CloudRuntimeException(String.format("HTTP Request Failed with error code: %s", responseCode)); + } + } catch (IOException e) { + throw new CloudRuntimeException(String.format("Error while trying to fetch email from github : %s", e.getMessage())); + } + + return email; + } + + private void clearAccessToken() { + accessToken = null; + } +} diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java new file mode 100644 index 00000000000..aa0fc93776d --- /dev/null +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java @@ -0,0 +1,141 @@ +//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 +//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 org.apache.cloudstack.oauth2.google; + +import com.cloud.exception.CloudAuthenticationException; +import com.cloud.utils.component.AdapterBase; +import com.cloud.utils.exception.CloudRuntimeException; +import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow; +import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets; +import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; +import com.google.api.client.googleapis.auth.oauth2.GoogleTokenResponse; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.services.oauth2.Oauth2; +import com.google.api.services.oauth2.model.Userinfo; +import org.apache.cloudstack.auth.UserOAuth2Authenticator; +import org.apache.cloudstack.oauth2.dao.OauthProviderDao; +import org.apache.cloudstack.oauth2.vo.OauthProviderVO; +import org.apache.commons.lang3.StringUtils; +import org.apache.log4j.Logger; + +import javax.inject.Inject; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +public class GoogleOAuth2Provider extends AdapterBase implements UserOAuth2Authenticator { + private static final Logger s_logger = Logger.getLogger(GoogleOAuth2Provider.class); + + protected String accessToken = null; + protected String refreshToken = null; + + @Inject + OauthProviderDao _oauthProviderDao; + + @Override + public String getName() { + return "google"; + } + + @Override + public String getDescription() { + return "Google OAuth2 Provider Plugin"; + } + + @Override + public boolean verifyUser(String email, String secretCode) { + if (StringUtils.isAnyEmpty(email, secretCode)) { + throw new CloudAuthenticationException("Either email or secret code should not be null/empty"); + } + + OauthProviderVO providerVO = _oauthProviderDao.findByProvider(getName()); + if (providerVO == null) { + throw new CloudAuthenticationException("Google provider is not registered, so user cannot be verified"); + } + + String verifiedEmail = verifyCodeAndFetchEmail(secretCode); + if (verifiedEmail == null || !email.equals(verifiedEmail)) { + throw new CloudRuntimeException("Unable to verify the email address with the provided secret"); + } + clearAccessAndRefreshTokens(); + + return true; + } + + @Override + public String verifyCodeAndFetchEmail(String secretCode) { + OauthProviderVO githubProvider = _oauthProviderDao.findByProvider(getName()); + String clientId = githubProvider.getClientId(); + String secret = githubProvider.getSecretKey(); + String redirectURI = githubProvider.getRedirectUri(); + GoogleClientSecrets clientSecrets = new GoogleClientSecrets() + .setWeb(new GoogleClientSecrets.Details() + .setClientId(clientId) + .setClientSecret(secret)); + + NetHttpTransport httpTransport = new NetHttpTransport(); + JsonFactory jsonFactory = new JacksonFactory(); + List scopes = Arrays.asList( + "https://www.googleapis.com/auth/userinfo.profile", + "https://www.googleapis.com/auth/userinfo.email"); + GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder( + httpTransport, jsonFactory, clientSecrets, scopes) + .build(); + + if (StringUtils.isAnyEmpty(accessToken, refreshToken)) { + GoogleTokenResponse tokenResponse = null; + try { + tokenResponse = flow.newTokenRequest(secretCode) + .setRedirectUri(redirectURI) + .execute(); + } catch (IOException e) { + throw new RuntimeException(e); + } + accessToken = tokenResponse.getAccessToken(); + refreshToken = tokenResponse.getRefreshToken(); + } + + GoogleCredential credential = new GoogleCredential.Builder() + .setTransport(httpTransport) + .setJsonFactory(jsonFactory) + .setClientSecrets(clientSecrets) + .build() + .setAccessToken(accessToken) + .setRefreshToken(refreshToken); + + Oauth2 oauth2 = new Oauth2.Builder(httpTransport, jsonFactory, credential).build(); + Userinfo userinfo = null; + try { + userinfo = oauth2.userinfo().get().execute(); + } catch (IOException e) { + throw new CloudRuntimeException(String.format("Failed to fetch the email address with the provided secret: %s" + e.getMessage())); + } + return userinfo.getEmail(); + } + + protected void clearAccessAndRefreshTokens() { + accessToken = null; + refreshToken = null; + } + + @Override + public String getUserEmailAddress() throws CloudRuntimeException { + return null; + } +} diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/vo/OauthProviderVO.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/vo/OauthProviderVO.java new file mode 100644 index 00000000000..efd6004e8f9 --- /dev/null +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/vo/OauthProviderVO.java @@ -0,0 +1,128 @@ +// 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 org.apache.cloudstack.oauth2.vo; + +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.api.Identity; +import org.apache.cloudstack.api.InternalIdentity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import java.util.Date; +import java.util.UUID; + +@Entity +@Table(name = "oauth_provider") +public class OauthProviderVO implements Identity, InternalIdentity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "uuid") + private String uuid; + + @Column(name = "description") + private String description; + + @Column(name = "provider") + private String provider; + + @Column(name = "client_id") + private String clientId; + + @Column(name = "secret_key") + private String secretKey; + + @Column(name = "redirect_uri") + private String redirectUri; + + @Column(name = GenericDao.CREATED_COLUMN) + private Date created; + + @Column(name = GenericDao.REMOVED_COLUMN) + private Date removed; + + @Column(name = "enabled") + private boolean enabled = true; + + public OauthProviderVO () { + uuid = UUID.randomUUID().toString(); + } + + @Override + public String getUuid() { + return uuid; + } + + @Override + public long getId() { + return id; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getProvider() { + return provider; + } + + public void setProvider(String provider) { + this.provider = provider; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getRedirectUri() { + return redirectUri; + } + + public void setRedirectUri(String redirectUri) { + this.redirectUri = redirectUri; + } + + public String getSecretKey() { + return secretKey; + } + + public void setSecretKey(String secretKey) { + this.secretKey = secretKey; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } +} diff --git a/plugins/user-authenticators/oauth2/src/main/resources/META-INF/cloudstack/oauth2/module.properties b/plugins/user-authenticators/oauth2/src/main/resources/META-INF/cloudstack/oauth2/module.properties new file mode 100644 index 00000000000..17844de0454 --- /dev/null +++ b/plugins/user-authenticators/oauth2/src/main/resources/META-INF/cloudstack/oauth2/module.properties @@ -0,0 +1,18 @@ +# 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. +name=oauth2 +parent=api diff --git a/plugins/user-authenticators/oauth2/src/main/resources/META-INF/cloudstack/oauth2/spring-oauth2-context.xml b/plugins/user-authenticators/oauth2/src/main/resources/META-INF/cloudstack/oauth2/spring-oauth2-context.xml new file mode 100644 index 00000000000..04a6c8dabfe --- /dev/null +++ b/plugins/user-authenticators/oauth2/src/main/resources/META-INF/cloudstack/oauth2/spring-oauth2-context.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/OAuth2AuthManagerImplTest.java b/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/OAuth2AuthManagerImplTest.java new file mode 100644 index 00000000000..3fd5636102c --- /dev/null +++ b/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/OAuth2AuthManagerImplTest.java @@ -0,0 +1,191 @@ +// +// 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 org.apache.cloudstack.oauth2; + +import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.cloudstack.oauth2.api.command.DeleteOAuthProviderCmd; +import org.apache.cloudstack.oauth2.api.command.RegisterOAuthProviderCmd; +import org.apache.cloudstack.oauth2.api.command.UpdateOAuthProviderCmd; +import org.apache.cloudstack.oauth2.dao.OauthProviderDao; +import org.apache.cloudstack.oauth2.vo.OauthProviderVO; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; + +public class OAuth2AuthManagerImplTest { + + @Spy + @InjectMocks + private OAuth2AuthManagerImpl _authManager; + + @Mock + OauthProviderDao _oauthProviderDao; + + AutoCloseable closeable; + @Before + public void setUp() { + closeable = MockitoAnnotations.openMocks(this); + } + + @After + public void tearDown() throws Exception { + closeable.close(); + } + + @Test + public void testRegisterOauthProvider() { + when(_authManager.isOAuthPluginEnabled()).thenReturn(false); + RegisterOAuthProviderCmd cmd = Mockito.mock(RegisterOAuthProviderCmd.class); + try { + _authManager.registerOauthProvider(cmd); + Assert.fail("Expected CloudRuntimeException was not thrown"); + } catch (CloudRuntimeException e) { + assertEquals("OAuth is not enabled, please enable to register", e.getMessage()); + } + + // Test when provider is already registered + when(_authManager.isOAuthPluginEnabled()).thenReturn(true); + OauthProviderVO providerVO = new OauthProviderVO(); + providerVO.setProvider("testProvider"); + when(_authManager._oauthProviderDao.findByProvider(Mockito.anyString())).thenReturn(providerVO); + when(cmd.getProvider()).thenReturn("testProvider"); + + try { + _authManager.registerOauthProvider(cmd); + Assert.fail("Expected CloudRuntimeException was not thrown"); + } catch (CloudRuntimeException e) { + assertEquals("Provider with the name testProvider is already registered", e.getMessage()); + } + + // Test when provider is github and secret key is not null + when(cmd.getSecretKey()).thenReturn("testSecretKey"); + providerVO = null; + when(_authManager._oauthProviderDao.findByProvider(Mockito.anyString())).thenReturn(providerVO); + OauthProviderVO savedProviderVO = new OauthProviderVO(); + when(cmd.getProvider()).thenReturn("github"); + when(_authManager._oauthProviderDao.persist(Mockito.any(OauthProviderVO.class))).thenReturn(savedProviderVO); + OauthProviderVO result = _authManager.registerOauthProvider(cmd); + assertEquals("github", result.getProvider()); + assertEquals("testSecretKey", result.getSecretKey()); + } + + @Test + public void testUpdateOauthProvider() { + Long id = 1L; + String description = "updated description"; + String clientId = "updated client id"; + String redirectUri = "updated redirect uri"; + String secretKey = "updated secret key"; + + UpdateOAuthProviderCmd cmd = Mockito.mock(UpdateOAuthProviderCmd.class); + when(cmd.getId()).thenReturn(id); + when(cmd.getDescription()).thenReturn(description); + when(cmd.getClientId()).thenReturn(clientId); + when(cmd.getRedirectUri()).thenReturn(redirectUri); + when(cmd.getSecretKey()).thenReturn(secretKey); + + OauthProviderVO providerVO = new OauthProviderVO(); + providerVO.setDescription("old description"); + providerVO.setClientId("old client id"); + providerVO.setRedirectUri("old redirect uri"); + providerVO.setSecretKey("old secret key"); + + when(_oauthProviderDao.findById(id)).thenReturn(providerVO); + + OauthProviderVO updatedProviderVO = new OauthProviderVO(); + updatedProviderVO.setDescription(description); + updatedProviderVO.setClientId(clientId); + updatedProviderVO.setRedirectUri(redirectUri); + updatedProviderVO.setSecretKey(secretKey); + + when(_oauthProviderDao.update(id, providerVO)).thenReturn(true); + + OauthProviderVO result = _authManager.updateOauthProvider(cmd); + + assertEquals(description, result.getDescription()); + assertEquals(clientId, result.getClientId()); + assertEquals(redirectUri, result.getRedirectUri()); + assertEquals(secretKey, result.getSecretKey()); + } + + @Test + public void testListOauthProviders() { + String uuid = "1234-5678-9101"; + String provider = "testProvider"; + OauthProviderVO providerVO = new OauthProviderVO(); + providerVO.setProvider(provider); + List providerList = Collections.singletonList(providerVO); + + // Test when uuid is not null + when(_oauthProviderDao.findByUuid(uuid)).thenReturn(providerVO); + List result = _authManager.listOauthProviders(null, uuid); + assertEquals(providerList, result); + + // Test when provider is not blank + when(_oauthProviderDao.findByProvider(provider)).thenReturn(providerVO); + result = _authManager.listOauthProviders(provider, null); + assertEquals(providerList, result); + + // Test when both uuid and provider are null + when(_oauthProviderDao.listAll()).thenReturn(providerList); + result = _authManager.listOauthProviders(null, null); + assertEquals(providerList, result); + } + + @Test + public void testGetCommands() { + List> expectedCmdList = new ArrayList<>(); + expectedCmdList.add(RegisterOAuthProviderCmd.class); + expectedCmdList.add(DeleteOAuthProviderCmd.class); + expectedCmdList.add(UpdateOAuthProviderCmd.class); + + List> cmdList = _authManager.getCommands(); + + assertEquals(expectedCmdList, cmdList); + } + + @Test + public void testStart() { + when(_authManager.isOAuthPluginEnabled()).thenReturn(true); + doNothing().when(_authManager).initializeUserOAuth2AuthenticationProvidersMap(); + boolean result = _authManager.start(); + assertTrue(result); + + when(_authManager.isOAuthPluginEnabled()).thenReturn(false); + result = _authManager.start(); + assertTrue(result); + } + +} diff --git a/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/OAuth2UserAuthenticatorTest.java b/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/OAuth2UserAuthenticatorTest.java new file mode 100644 index 00000000000..06aa04d729c --- /dev/null +++ b/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/OAuth2UserAuthenticatorTest.java @@ -0,0 +1,153 @@ +// +// 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 org.apache.cloudstack.oauth2; + +import com.cloud.user.UserAccount; +import com.cloud.user.UserVO; +import com.cloud.user.dao.UserAccountDao; +import com.cloud.user.dao.UserDao; +import com.cloud.utils.Pair; +import org.apache.cloudstack.auth.UserOAuth2Authenticator; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class OAuth2UserAuthenticatorTest { + + @Mock + private UserAccountDao userAccountDao; + + @Mock + private UserDao userDao; + + @Mock + private OAuth2AuthManager userOAuth2mgr; + + @InjectMocks + private OAuth2UserAuthenticator authenticator; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testAuthenticateWithValidCredentials() { + String username = "testuser"; + Long domainId = 1L; + String[] provider = {"testprovider"}; + String[] email = {"testemail"}; + String[] secretCode = {"testsecretcode"}; + + UserAccount userAccount = mock(UserAccount.class); + UserVO user = mock(UserVO.class); + UserOAuth2Authenticator userOAuth2Authenticator = mock(UserOAuth2Authenticator.class); + + when(userAccountDao.getUserAccount(username, domainId)).thenReturn(userAccount); + when(userDao.getUser(userAccount.getId())).thenReturn(user); + when(userOAuth2mgr.getUserOAuth2AuthenticationProvider(provider[0])).thenReturn(userOAuth2Authenticator); + when(userOAuth2Authenticator.verifyUser(email[0], secretCode[0])).thenReturn(true); + + Map requestParameters = new HashMap<>(); + requestParameters.put("provider", provider); + requestParameters.put("email", email); + requestParameters.put("secretcode", secretCode); + + Pair result = authenticator.authenticate(username, null, domainId, requestParameters); + + verify(userAccountDao).getUserAccount(username, domainId); + verify(userDao).getUser(userAccount.getId()); + verify(userOAuth2mgr).getUserOAuth2AuthenticationProvider(provider[0]); + verify(userOAuth2Authenticator).verifyUser(email[0], secretCode[0]); + + assertEquals(true, result.first().booleanValue()); + assertEquals(null, result.second()); + } + + @Test + public void testAuthenticateWithInvalidCredentials() { + String username = "testuser"; + Long domainId = 1L; + String[] provider = {"testprovider"}; + String[] email = {"testemail"}; + String[] secretCode = {"testsecretcode"}; + + UserAccount userAccount = mock(UserAccount.class); + UserVO user = mock(UserVO.class); + UserOAuth2Authenticator userOAuth2Authenticator = mock(UserOAuth2Authenticator.class); + + when(userAccountDao.getUserAccount(username, domainId)).thenReturn(userAccount); + when(userDao.getUser(userAccount.getId())).thenReturn( user); + when(userOAuth2mgr.getUserOAuth2AuthenticationProvider(provider[0])).thenReturn(userOAuth2Authenticator); + when(userOAuth2Authenticator.verifyUser(email[0], secretCode[0])).thenReturn(false); + + Map requestParameters = new HashMap<>(); + requestParameters.put("provider", provider); + requestParameters.put("email", email); + requestParameters.put("secretcode", secretCode); + + Pair result = authenticator.authenticate(username, null, domainId, requestParameters); + + verify(userAccountDao).getUserAccount(username, domainId); + verify(userDao).getUser(userAccount.getId()); + verify(userOAuth2mgr).getUserOAuth2AuthenticationProvider(provider[0]); + verify(userOAuth2Authenticator).verifyUser(email[0], secretCode[0]); + + assertEquals(false, result.first().booleanValue()); + assertEquals(OAuth2UserAuthenticator.ActionOnFailedAuthentication.INCREMENT_INCORRECT_LOGIN_ATTEMPT_COUNT, result.second()); + } + + @Test + public void testAuthenticateWithInvalidUserAccount() { + String username = "testuser"; + Long domainId = 1L; + String[] provider = {"testprovider"}; + String[] email = {"testemail"}; + String[] secretCode = {"testsecretcode"}; + + when(userAccountDao.getUserAccount(username, domainId)).thenReturn(null); + + Map requestParameters = new HashMap<>(); + requestParameters.put("provider", provider); + requestParameters.put("email", email); + requestParameters.put("secretcode", secretCode); + + Pair result = authenticator.authenticate(username, null, domainId, requestParameters); + + verify(userAccountDao).getUserAccount(username, domainId); + verify(userDao, never()).getUser(anyLong()); + verify(userOAuth2mgr, never()).getUserOAuth2AuthenticationProvider(anyString()); + + assertEquals(false, result.first().booleanValue()); + assertEquals(null, result.second()); + } +} diff --git a/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/api/command/DeleteOAuthProviderCmdTest.java b/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/api/command/DeleteOAuthProviderCmdTest.java new file mode 100644 index 00000000000..be8670c1af9 --- /dev/null +++ b/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/api/command/DeleteOAuthProviderCmdTest.java @@ -0,0 +1,79 @@ +/* + * 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 org.apache.cloudstack.oauth2.api.command; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; + +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.oauth2.OAuth2AuthManager; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.runners.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class DeleteOAuthProviderCmdTest { + + @Mock + private OAuth2AuthManager _oauthMgr; + + @InjectMocks + private DeleteOAuthProviderCmd cmd; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test(expected = ServerApiException.class) + public void testExecuteFailure() { + when(_oauthMgr.deleteOauthProvider(cmd.getId())).thenReturn(false); + cmd.execute(); + } + + @Test + public void testExecuteSuccess() { + when(_oauthMgr.deleteOauthProvider(cmd.getId())).thenReturn(true); + cmd.execute(); + } + + @Test + public void testGetApiResourceType() { + assert (cmd.getApiResourceType() == org.apache.cloudstack.api.ApiCommandResourceType.User); + } + + @Test + public void testDeleteOAuthProvider() { + when(_oauthMgr.deleteOauthProvider(null)).thenReturn(true); + cmd.execute(); + + assertTrue(cmd.getResponseObject() instanceof SuccessResponse); + } + + @Test(expected = ServerApiException.class) + public void testDeleteOAuthProviderExpectFailure() { + when(_oauthMgr.deleteOauthProvider(null)).thenReturn(false); + cmd.execute(); + } +} diff --git a/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/api/command/OauthLoginAPIAuthenticatorCmdTest.java b/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/api/command/OauthLoginAPIAuthenticatorCmdTest.java new file mode 100644 index 00000000000..07df66f2026 --- /dev/null +++ b/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/api/command/OauthLoginAPIAuthenticatorCmdTest.java @@ -0,0 +1,85 @@ +// 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 org.apache.cloudstack.oauth2.api.command; + +import com.cloud.api.ApiServer; +import org.apache.cloudstack.api.ApiConstants; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.MockitoAnnotations; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class OauthLoginAPIAuthenticatorCmdTest { + @InjectMocks + private OauthLoginAPIAuthenticatorCmd cmd; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + @Test + public void testGetDomainNameWhenDomainNameIsNull() { + StringBuilder auditTrailSb = new StringBuilder(); + String[] domainName = null; + String domain = cmd.getDomainName(auditTrailSb, domainName); + assertNull(domain); + assertEquals("", auditTrailSb.toString()); + } + + @Test + public void testGetDomainNameWithStartingSlash() { + StringBuilder auditTrailSb = new StringBuilder(); + String[] domainName = {"/example"}; + String domain = cmd.getDomainName(auditTrailSb, domainName); + assertEquals("/example/", domain); + assertEquals(" domain=/example", auditTrailSb.toString()); + } + + @Test + public void testGetDomainNameWithEndingSlash() { + StringBuilder auditTrailSb = new StringBuilder(); + String[] domainName = {"example/"}; + String domain = cmd.getDomainName(auditTrailSb, domainName); + assertEquals("/example/", domain); + assertEquals(" domain=example/", auditTrailSb.toString()); + } + + @Test + public void testGetDomainIdFromParams() { + StringBuilder auditTrailSb = new StringBuilder(); + String responseType = "json"; + Map params = new HashMap<>(); + params.put(ApiConstants.DOMAIN_ID, new String[]{"1234"}); + ApiServer apiServer = mock(ApiServer.class); + cmd._apiServer = apiServer; + when(apiServer.fetchDomainId("1234")).thenReturn(5678L); + + Long domainId = cmd.getDomainIdFromParams(params, auditTrailSb, responseType); + + assertEquals(Long.valueOf(5678), domainId); + assertEquals(" domainid=5678", auditTrailSb.toString()); + } +} diff --git a/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/api/command/RegisterOAuthProviderCmdTest.java b/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/api/command/RegisterOAuthProviderCmdTest.java new file mode 100644 index 00000000000..987c7a5d01e --- /dev/null +++ b/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/api/command/RegisterOAuthProviderCmdTest.java @@ -0,0 +1,61 @@ +/* + * 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 org.apache.cloudstack.oauth2.api.command; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.oauth2.OAuth2AuthManager; +import org.apache.cloudstack.oauth2.api.response.OauthProviderResponse; +import org.apache.cloudstack.oauth2.vo.OauthProviderVO; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.runners.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class RegisterOAuthProviderCmdTest { + + @Mock + private OAuth2AuthManager _oauth2mgr; + + @InjectMocks + private RegisterOAuthProviderCmd _cmd; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testExecute() throws ServerApiException { + OauthProviderVO provider = mock(OauthProviderVO.class); + when(_oauth2mgr.registerOauthProvider(_cmd)).thenReturn(provider); + + _cmd.execute(); + assertEquals(ApiConstants.OAUTH_PROVIDER, ((OauthProviderResponse)_cmd.getResponseObject()).getObjectName()); + } +} diff --git a/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/api/command/VerifyOAuthCodeAndGetUserCmdTest.java b/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/api/command/VerifyOAuthCodeAndGetUserCmdTest.java new file mode 100644 index 00000000000..59245a4027a --- /dev/null +++ b/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/api/command/VerifyOAuthCodeAndGetUserCmdTest.java @@ -0,0 +1,108 @@ +/* + * 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 org.apache.cloudstack.oauth2.api.command; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator; +import org.apache.cloudstack.oauth2.OAuth2AuthManager; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class VerifyOAuthCodeAndGetUserCmdTest { + + private VerifyOAuthCodeAndGetUserCmd cmd; + private OAuth2AuthManager oauth2mgr; + private HttpSession session; + private InetAddress remoteAddress; + private StringBuilder auditTrailSb; + private HttpServletRequest req; + private HttpServletResponse resp; + + @Before + public void setUp() { + cmd = new VerifyOAuthCodeAndGetUserCmd(); + oauth2mgr = mock(OAuth2AuthManager.class); + session = mock(HttpSession.class); + remoteAddress = mock(InetAddress.class); + auditTrailSb = new StringBuilder(); + req = mock(HttpServletRequest.class); + resp = mock(HttpServletResponse.class); + cmd._oauth2mgr = oauth2mgr; + } + + @Test + public void testAuthenticate() { + final String[] secretcodeArray = new String[] { "secretcode" }; + final String[] providerArray = new String[] { "provider" }; + final String responseType = "json"; + + Map params = new HashMap<>(); + params.put("secretcode", secretcodeArray); + params.put("provider", providerArray); + + when(oauth2mgr.verifyCodeAndFetchEmail("secretcode", "provider")).thenReturn("test@example.com"); + + String response = cmd.authenticate("command", params, session, remoteAddress, responseType, auditTrailSb, req, resp); + + Assert.assertNotNull(response); + Assert.assertTrue(response.contains("test@example.com")); + } + + @Test(expected = ServerApiException.class) + public void testAuthenticateWithInvalidCode() throws Exception { + final String[] secretcodeArray = new String[] { "invalidcode" }; + final String[] providerArray = new String[] { "provider" }; + final String responseType = "json"; + + Map params = new HashMap<>(); + params.put("secretcode", secretcodeArray); + params.put("provider", providerArray); + + when(oauth2mgr.verifyCodeAndFetchEmail("invalidcode", "provider")).thenReturn(null); + + cmd.authenticate("command", params, session, remoteAddress, responseType, auditTrailSb, req, resp); + } + + @Test + public void testSetAuthenticators() { + VerifyOAuthCodeAndGetUserCmd cmd = new VerifyOAuthCodeAndGetUserCmd(); + OAuth2AuthManager oauth2mgr = mock(OAuth2AuthManager.class); + List authenticators = new ArrayList<>(); + authenticators.add(mock(PluggableAPIAuthenticator.class)); + authenticators.add(oauth2mgr); + authenticators.add(null); + cmd.setAuthenticators(authenticators); + Assert.assertEquals(oauth2mgr, cmd._oauth2mgr); + } +} diff --git a/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2ProviderTest.java b/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2ProviderTest.java new file mode 100644 index 00000000000..b8b1abcc4be --- /dev/null +++ b/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2ProviderTest.java @@ -0,0 +1,148 @@ +//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 +//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 org.apache.cloudstack.oauth2.google; + +import com.cloud.exception.CloudAuthenticationException; +import com.cloud.utils.exception.CloudRuntimeException; +import com.google.api.services.oauth2.Oauth2; +import com.google.api.services.oauth2.model.Userinfo; +import org.apache.cloudstack.oauth2.dao.OauthProviderDao; +import org.apache.cloudstack.oauth2.vo.OauthProviderVO; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedConstruction; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; + +import java.io.IOException; + +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class GoogleOAuth2ProviderTest { + + @Mock + private OauthProviderDao _oauthProviderDao; + + @Spy + @InjectMocks + private GoogleOAuth2Provider _googleOAuth2Provider; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test(expected = CloudAuthenticationException.class) + public void testVerifyUserWithNullEmail() { + _googleOAuth2Provider.verifyUser(null, "secretCode"); + } + + @Test(expected = CloudAuthenticationException.class) + public void testVerifyUserWithNullSecretCode() { + _googleOAuth2Provider.verifyUser("email@example.com", null); + } + + @Test(expected = CloudAuthenticationException.class) + public void testVerifyUserWithUnregisteredProvider() { + when(_oauthProviderDao.findByProvider(anyString())).thenReturn(null); + _googleOAuth2Provider.verifyUser("email@example.com", "secretCode"); + } + + @Test(expected = CloudRuntimeException.class) + public void testVerifyUserWithInvalidSecretCode() throws IOException { + OauthProviderVO providerVO = mock(OauthProviderVO.class); + when(_oauthProviderDao.findByProvider(anyString())).thenReturn(providerVO); + when(providerVO.getProvider()).thenReturn("testProvider"); + when(providerVO.getSecretKey()).thenReturn("testSecret"); + when(providerVO.getClientId()).thenReturn("testClientid"); + _googleOAuth2Provider.accessToken = "testAccessToken"; + _googleOAuth2Provider.refreshToken = "testRefreshToken"; + Oauth2 oauth2 = mock(Oauth2.class); + try (MockedConstruction ignored = Mockito.mockConstruction(Oauth2.Builder.class, + (mock, context) -> when(mock.build()).thenReturn(oauth2))) { + Userinfo userinfo = mock(Userinfo.class); + Oauth2.Userinfo userinfo1 = mock(Oauth2.Userinfo.class); + when(oauth2.userinfo()).thenReturn(userinfo1); + Oauth2.Userinfo.Get userinfoGet = mock(Oauth2.Userinfo.Get.class); + when(userinfo1.get()).thenReturn(userinfoGet); + when(userinfoGet.execute()).thenReturn(userinfo); + when(userinfo.getEmail()).thenReturn(null); + + _googleOAuth2Provider.verifyUser("email@example.com", "secretCode"); + } + } + + @Test(expected = CloudRuntimeException.class) + public void testVerifyUserWithMismatchedEmail() throws IOException { + OauthProviderVO providerVO = mock(OauthProviderVO.class); + when(_oauthProviderDao.findByProvider(anyString())).thenReturn(providerVO); + when(providerVO.getProvider()).thenReturn("testProvider"); + when(providerVO.getSecretKey()).thenReturn("testSecret"); + when(providerVO.getClientId()).thenReturn("testClientid"); + _googleOAuth2Provider.accessToken = "testAccessToken"; + _googleOAuth2Provider.refreshToken = "testRefreshToken"; + Oauth2 oauth2 = mock(Oauth2.class); + try (MockedConstruction ignored = Mockito.mockConstruction(Oauth2.Builder.class, + (mock, context) -> when(mock.build()).thenReturn(oauth2))) { + Userinfo userinfo = mock(Userinfo.class); + Oauth2.Userinfo userinfo1 = mock(Oauth2.Userinfo.class); + when(oauth2.userinfo()).thenReturn(userinfo1); + Oauth2.Userinfo.Get userinfoGet = mock(Oauth2.Userinfo.Get.class); + when(userinfo1.get()).thenReturn(userinfoGet); + when(userinfoGet.execute()).thenReturn(userinfo); + when(userinfo.getEmail()).thenReturn("otheremail@example.com"); + + _googleOAuth2Provider.verifyUser("email@example.com", "secretCode"); + } + } + + @Test + public void testVerifyUserEmail() throws IOException { + OauthProviderVO providerVO = mock(OauthProviderVO.class); + when(_oauthProviderDao.findByProvider(anyString())).thenReturn(providerVO); + when(providerVO.getProvider()).thenReturn("testProvider"); + when(providerVO.getSecretKey()).thenReturn("testSecret"); + when(providerVO.getClientId()).thenReturn("testClientid"); + _googleOAuth2Provider.accessToken = "testAccessToken"; + _googleOAuth2Provider.refreshToken = "testRefreshToken"; + Oauth2 oauth2 = mock(Oauth2.class); + try (MockedConstruction ignored = Mockito.mockConstruction(Oauth2.Builder.class, + (mock, context) -> when(mock.build()).thenReturn(oauth2))) { + Userinfo userinfo = mock(Userinfo.class); + Oauth2.Userinfo userinfo1 = mock(Oauth2.Userinfo.class); + when(oauth2.userinfo()).thenReturn(userinfo1); + Oauth2.Userinfo.Get userinfoGet = mock(Oauth2.Userinfo.Get.class); + when(userinfo1.get()).thenReturn(userinfoGet); + when(userinfoGet.execute()).thenReturn(userinfo); + when(userinfo.getEmail()).thenReturn("email@example.com"); + + boolean result = _googleOAuth2Provider.verifyUser("email@example.com", "secretCode"); + + assertTrue(result); + assertNull(_googleOAuth2Provider.accessToken); + assertNull(_googleOAuth2Provider.refreshToken); + } + } +} diff --git a/server/src/main/java/com/cloud/api/auth/APIAuthenticationManagerImpl.java b/server/src/main/java/com/cloud/api/auth/APIAuthenticationManagerImpl.java index 6ec9ff9c1ce..1b8c2689063 100644 --- a/server/src/main/java/com/cloud/api/auth/APIAuthenticationManagerImpl.java +++ b/server/src/main/java/com/cloud/api/auth/APIAuthenticationManagerImpl.java @@ -82,7 +82,6 @@ public class APIAuthenticationManagerImpl extends ManagerBase implements APIAuth cmdList.add(ValidateUserTwoFactorAuthenticationCodeCmd.class); cmdList.add(SetupUserTwoFactorAuthenticationCmd.class); - for (PluggableAPIAuthenticator apiAuthenticator: _apiAuthenticators) { List> commands = apiAuthenticator.getAuthCommands(); if (commands != null) { diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java index 04785077936..86a359a3348 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -32,6 +32,7 @@ import java.util.UUID; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import javax.crypto.KeyGenerator; import javax.crypto.Mac; @@ -52,6 +53,7 @@ import org.apache.cloudstack.affinity.AffinityGroup; import org.apache.cloudstack.affinity.dao.AffinityGroupDao; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.command.admin.account.CreateAccountCmd; import org.apache.cloudstack.api.command.admin.account.UpdateAccountCmd; import org.apache.cloudstack.api.command.admin.user.DeleteUserCmd; @@ -2329,6 +2331,15 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M return _userAccountDao.getUserAccount(username, domainId); } + @Override + public List getActiveUserAccountByEmail(String email, Long domainId) { + List userAccountByEmail = _userAccountDao.getUserAccountByEmail(email, domainId); + List userAccounts = userAccountByEmail.stream() + .map(userAccountVO -> (UserAccount) userAccountVO) + .collect(Collectors.toList()); + return userAccounts; + } + @Override public Account getActiveAccountById(long accountId) { return _accountDao.findById(accountId); @@ -2473,7 +2484,13 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M @Override public UserAccount authenticateUser(final String username, final String password, final Long domainId, final InetAddress loginIpAddress, final Map requestParameters) { UserAccount user = null; - if (password != null && !password.isEmpty()) { + final String[] oAuthProviderArray = (String[])requestParameters.get(ApiConstants.PROVIDER); + final String[] secretCodeArray = (String[])requestParameters.get(ApiConstants.SECRET_CODE); + String oauthProvider = ((oAuthProviderArray == null) ? null : oAuthProviderArray[0]); + String secretCode = ((secretCodeArray == null) ? null : secretCodeArray[0]); + + + if ((password != null && !password.isEmpty()) || (oauthProvider != null && secretCode != null)) { user = getUserAccount(username, password, domainId, requestParameters); } else { String key = _configDao.getValue("security.singlesignon.key"); @@ -2626,11 +2643,16 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M HashSet actionsOnFailedAuthenticaion = new HashSet(); User.Source userSource = userAccount != null ? userAccount.getSource() : User.Source.UNKNOWN; for (UserAuthenticator authenticator : _userAuthenticators) { - if (userSource != User.Source.UNKNOWN) { + final String[] secretCodeArray = (String[])requestParameters.get(ApiConstants.SECRET_CODE); + String secretCode = ((secretCodeArray == null) ? null : secretCodeArray[0]); + if (userSource != User.Source.UNKNOWN && secretCode == null) { if (!authenticator.getName().equalsIgnoreCase(userSource.name())) { continue; } } + if (secretCode != null && !authenticator.getName().equals("oauth2")) { + continue; + } Pair result = authenticator.authenticate(username, password, domainId, requestParameters); if (result.first()) { authenticated = true; diff --git a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java index f61cc028b31..6d9211dd526 100644 --- a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java +++ b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java @@ -200,24 +200,24 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { userAccountVO.setSource(User.Source.UNKNOWN); userAccountVO.setState(Account.State.DISABLED.toString()); Mockito.when(userAccountDaoMock.getUserAccount("test", 1L)).thenReturn(userAccountVO); - Mockito.when(userAuthenticator.authenticate("test", "fail", 1L, null)).thenReturn(failureAuthenticationPair); - Mockito.lenient().when(userAuthenticator.authenticate("test", null, 1L, null)).thenReturn(successAuthenticationPair); - Mockito.lenient().when(userAuthenticator.authenticate("test", "", 1L, null)).thenReturn(successAuthenticationPair); + Mockito.when(userAuthenticator.authenticate("test", "fail", 1L, new HashMap<>())).thenReturn(failureAuthenticationPair); + Mockito.lenient().when(userAuthenticator.authenticate("test", null, 1L, new HashMap<>())).thenReturn(successAuthenticationPair); + Mockito.lenient().when(userAuthenticator.authenticate("test", "", 1L, new HashMap<>())).thenReturn(successAuthenticationPair); //Test for incorrect password. authentication should fail - UserAccount userAccount = accountManagerImpl.authenticateUser("test", "fail", 1L, InetAddress.getByName("127.0.0.1"), null); + UserAccount userAccount = accountManagerImpl.authenticateUser("test", "fail", 1L, InetAddress.getByName("127.0.0.1"), new HashMap<>()); Assert.assertNull(userAccount); //Test for null password. authentication should fail - userAccount = accountManagerImpl.authenticateUser("test", null, 1L, InetAddress.getByName("127.0.0.1"), null); + userAccount = accountManagerImpl.authenticateUser("test", null, 1L, InetAddress.getByName("127.0.0.1"), new HashMap<>()); Assert.assertNull(userAccount); //Test for empty password. authentication should fail - userAccount = accountManagerImpl.authenticateUser("test", "", 1L, InetAddress.getByName("127.0.0.1"), null); + userAccount = accountManagerImpl.authenticateUser("test", "", 1L, InetAddress.getByName("127.0.0.1"), new HashMap<>()); Assert.assertNull(userAccount); //Verifying that the authentication method is only called when password is specified - Mockito.verify(userAuthenticator, Mockito.times(1)).authenticate("test", "fail", 1L, null); + Mockito.verify(userAuthenticator, Mockito.times(1)).authenticate("test", "fail", 1L, new HashMap<>()); Mockito.verify(userAuthenticator, Mockito.never()).authenticate("test", null, 1L, null); Mockito.verify(userAuthenticator, Mockito.never()).authenticate("test", "", 1L, null); } @@ -974,4 +974,17 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase { Assert.assertEquals("345543", response.getSecretCode()); } + + @Test + public void testGetActiveUserAccountByEmail() { + String email = "test@example.com"; + Long domainId = 1L; + List userAccountVOList = new ArrayList<>(); + UserAccountVO userAccountVO = new UserAccountVO(); + userAccountVOList.add(userAccountVO); + Mockito.when(userAccountDaoMock.getUserAccountByEmail(email, domainId)).thenReturn(userAccountVOList); + List userAccounts = accountManagerImpl.getActiveUserAccountByEmail(email, domainId); + Assert.assertEquals(userAccountVOList.size(), userAccounts.size()); + Assert.assertEquals(userAccountVOList.get(0), userAccounts.get(0)); + } } diff --git a/server/src/test/java/com/cloud/user/MockAccountManagerImpl.java b/server/src/test/java/com/cloud/user/MockAccountManagerImpl.java index 1c613720155..fe7748b8581 100644 --- a/server/src/test/java/com/cloud/user/MockAccountManagerImpl.java +++ b/server/src/test/java/com/cloud/user/MockAccountManagerImpl.java @@ -176,6 +176,11 @@ public class MockAccountManagerImpl extends ManagerBase implements Manager, Acco return null; } + @Override + public List getActiveUserAccountByEmail(String email, Long domainId) { + return null; + } + @Override public Account getActiveAccountById(long accountId) { // TODO Auto-generated method stub diff --git a/tools/apidoc/gen_toc.py b/tools/apidoc/gen_toc.py index 75ff7e1d29a..d08918a66bc 100644 --- a/tools/apidoc/gen_toc.py +++ b/tools/apidoc/gen_toc.py @@ -158,6 +158,11 @@ known_categories = { 'listIdps': 'Authentication', 'authorizeSamlSso': 'Authentication', 'listSamlAuthorization': 'Authentication', + 'oauthlogin': 'Authentication', + 'deleteOauthProvider': 'Oauth', + 'listOauthProvider': 'Oauth', + 'registerOauthProvider': 'Oauth', + 'updateOauthProvider': 'Oauth', 'quota': 'Quota', 'emailTemplate': 'Quota', 'Capacity': 'System Capacity', diff --git a/ui/package.json b/ui/package.json index 04303753993..1c6e42a321e 100644 --- a/ui/package.json +++ b/ui/package.json @@ -43,7 +43,7 @@ "ant-design-vue": "^3.2.20", "antd": "^4.21.4", "antd-theme-webpack-plugin": "^1.3.9", - "axios": "^0.21.1", + "axios": "^0.21.4", "babel-plugin-require-context-hook": "^1.0.0", "chart.js": "^3.7.1", "chartjs-adapter-moment": "^1.0.0", @@ -67,9 +67,11 @@ "vue-loader": "^16.2.0", "vue-qrious": "^3.1.0", "vue-router": "^4.0.14", + "vue-social-auth": "^1.4.9", "vue-uuid": "^3.0.0", "vue-web-storage": "^6.1.0", "vue3-clipboard": "^1.0.0", + "vue3-google-login": "^2.0.20", "vuedraggable": "^4.0.3", "vuex": "^4.0.0-0" }, diff --git a/ui/public/assets/github.svg b/ui/public/assets/github.svg new file mode 100644 index 00000000000..fd8a6c750be --- /dev/null +++ b/ui/public/assets/github.svg @@ -0,0 +1 @@ + diff --git a/ui/public/assets/google.svg b/ui/public/assets/google.svg new file mode 100644 index 00000000000..6ce064d2ef7 --- /dev/null +++ b/ui/public/assets/google.svg @@ -0,0 +1 @@ + diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 2118fa2e525..e5c61ce4440 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -81,6 +81,7 @@ "label.action.delete.network.static.route": "Remove Tungsten Fabric network static route", "label.action.delete.network.permission": "Delete network permission", "label.action.delete.node": "Delete node", +"label.action.delete.oauth.provider": "Delete OAuth provider", "label.action.delete.physical.network": "Delete physical network", "label.action.delete.pod": "Delete Pod", "label.action.delete.primary.storage": "Delete primary storage", @@ -450,6 +451,7 @@ "label.clear": "Clear", "label.clear.list": "Clear list", "label.clear.notification": "Clear notification", +"label.clientid": "Provider Client ID", "label.close": "Close", "label.cloud.managed": "CloudManaged", "label.cloudian.storage": "Cloudian storage", @@ -797,6 +799,7 @@ "label.enable.autoscale.vmgroup": "Enable AutoScale VM Group", "label.enable.host": "Enable Host", "label.enable.network.offering": "Enable network offering", +"label.enable.oauth": "Enable OAuth Login", "label.enable.provider": "Enable provider", "label.enable.storage": "Enable storage pool", "label.enable.vpc.offering": "Enable VPC offering", @@ -1397,6 +1400,8 @@ "label.number": "#Rule", "label.numretries": "Number of retries", "label.nvpdeviceid": "ID", +"label.oauth.configuration": "OAuth configuration", +"label.oauth.verification": "OAuth verification", "label.ocfs2": "OCFS2", "label.of": "of", "label.of.month": "of month", @@ -1615,11 +1620,13 @@ "label.receivedbytes": "Bytes received", "label.recover.vm": "Recover VM", "label.redirect": "Redirect to:", +"label.redirecturi": "Redirect URI", "label.redundantrouter": "Redundant router", "label.redundantstate": "Redundant state", "label.redundantvpcrouter": "Redundant VPC", "label.refresh": "Refresh", "label.region": "Region", +"label.register.oauth": "Register OAuth", "label.register.template": "Register template", "label.register.user.data": "Register a userdata", "label.reinstall.vm": "Reinstall VM", @@ -2328,6 +2335,7 @@ "message.action.delete.network.static.route": "Please confirm that you want to remove this network Static Route", "message.action.delete.nexusvswitch": "Please confirm that you want to delete this nexus 1000v", "message.action.delete.node": "Please confirm that you want to delete this node.", +"message.action.delete.oauth.provider": "Please confirm that you want to delete the OAuth provider.", "message.action.delete.physical.network": "Please confirm that you want to delete this physical network.", "message.action.delete.pod": "Please confirm that you want to delete this pod.", "message.action.delete.secondary.storage": "Please confirm that you want to delete this secondary storage.", diff --git a/ui/src/api/index.js b/ui/src/api/index.js index 5e428628987..1db41661276 100644 --- a/ui/src/api/index.js +++ b/ui/src/api/index.js @@ -70,3 +70,28 @@ export function logout () { notification.destroy() return api('logout') } + +export function oauthlogin (arg) { + if (!sourceToken.checkExistSource()) { + sourceToken.init() + } + + // Logout before login is called to purge any duplicate sessionkey cookies + api('logout') + + const params = new URLSearchParams() + params.append('command', 'oauthlogin') + params.append('email', arg.email) + params.append('secretcode', arg.secretcode) + params.append('provider', arg.provider) + params.append('domain', arg.domain) + params.append('response', 'json') + return axios({ + url: '/', + method: 'post', + data: params, + headers: { + 'content-type': 'application/x-www-form-urlencoded' + } + }) +} diff --git a/ui/src/components/view/DetailsTab.vue b/ui/src/components/view/DetailsTab.vue index a6f77eacdd0..ebdc459c439 100644 --- a/ui/src/components/view/DetailsTab.vue +++ b/ui/src/components/view/DetailsTab.vue @@ -162,7 +162,7 @@ export default { }, computed: { customDisplayItems () { - return ['ip6routes', 'privatemtu', 'publicmtu'] + return ['ip6routes', 'privatemtu', 'publicmtu', 'provider'] }, vnfAccessMethods () { if (this.resource.templatetype === 'VNF' && ['vm', 'vnfapp'].includes(this.$route.meta.name)) { diff --git a/ui/src/components/view/ListView.vue b/ui/src/components/view/ListView.vue index 6ab6967cc6f..cafed924e4c 100644 --- a/ui/src/components/view/ListView.vue +++ b/ui/src/components/view/ListView.vue @@ -38,7 +38,7 @@