diff --git a/api/src/main/java/com/cloud/user/AccountService.java b/api/src/main/java/com/cloud/user/AccountService.java index 77a5b442e861..63f5455cfd09 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 @@ User createUser(String userName, String password, String firstName, String lastN 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 c50ed3f28afc..422e264f10be 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 c95e5c8bddbe..b65485dcfe45 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 28bdd4d57f6c..521148b596d9 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 void execute() { 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 aa30066c2a39..87d8883e2e30 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 void execute() { 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 a8a87c407252..f294f7dd8e09 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 void execute() throws ResourceUnavailableException, InsufficientCapacityE 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 000000000000..ee3b98b8a4b6 --- /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 c9b6e9172c1f..22cca63e0c8c 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 2b2caeaaa66f..a36d12431551 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 e0cf48d44a8b..c9de9a367eed 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 UserAccount getUserAccount(String username, Long domainId) { 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 88ff76f0c8c1..60b200c66131 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 bf0b94a71acc..67cfe1df3e1b 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 UserAccount getActiveUserAccount(String username, Long domainId) { 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 af131f08669d..06c5fab6a129 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 000000000000..04ea71eacdf0 --- /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 000000000000..ece012db3a40 --- /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 000000000000..85730651248b --- /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 000000000000..8484a5ef798d --- /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 000000000000..6cd3156f68a2 --- /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 000000000000..597283ae33e1 --- /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 000000000000..928fa76780a3 --- /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 000000000000..b31cbde97c52 --- /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 000000000000..b38423ffd485 --- /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 000000000000..5dbeef10dcb4 --- /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 000000000000..e0c40bef9b4d --- /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 000000000000..31738ac75a0f --- /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 000000000000..27eea4d22a6b --- /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 000000000000..e4a7fae101f0 --- /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 000000000000..aa0fc93776dd --- /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 000000000000..efd6004e8f97 --- /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 000000000000..17844de0454b --- /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 000000000000..04a6c8dabfe7 --- /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 000000000000..3fd5636102ce --- /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 000000000000..06aa04d729cb --- /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 000000000000..be8670c1af96 --- /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 000000000000..07df66f20265 --- /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 000000000000..987c7a5d01e8 --- /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 000000000000..59245a4027aa --- /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 000000000000..b8b1abcc4bee --- /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 6ec9ff9c1cec..1b8c2689063c 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 List> getCommands() { 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 047850779362..86a359a33487 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.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.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 UserAccount getActiveUserAccount(String username, Long domainId) { 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 void logoutUser(long userId) { @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 @@ private UserAccount getUserAccount(String username, String password, Long domain 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 f61cc028b311..6d9211dd526d 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 void testAuthenticateUser() throws UnknownHostException { 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 void testEnable2FAcode() { 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 1c6137201551..fe7748b85815 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 UserAccount getActiveUserAccount(String username, Long domainId) { 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 75ff7e1d29a0..d08918a66bc1 100644 --- a/tools/apidoc/gen_toc.py +++ b/tools/apidoc/gen_toc.py @@ -158,6 +158,11 @@ '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 04303753993e..1c6e42a321ef 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 000000000000..fd8a6c750bea --- /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 000000000000..6ce064d2ef7c --- /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 2118fa2e5255..e5c61ce44406 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 5e4286289870..1db416612766 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 a6f77eacdd03..ebdc459c4393 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 6ab6967cc6f1..cafed924e4c0 100644 --- a/ui/src/components/view/ListView.vue +++ b/ui/src/components/view/ListView.vue @@ -38,7 +38,7 @@