From b2dc73d404371e89cecd3b12e10186607f7d9f21 Mon Sep 17 00:00:00 2001 From: Harikrishna Patnala Date: Mon, 25 Sep 2023 00:18:29 +0530 Subject: [PATCH 01/36] Oauth2 integration with CloudStack --- .../java/com/cloud/user/AccountService.java | 2 + api/src/main/java/com/cloud/user/User.java | 2 +- .../apache/cloudstack/api/ApiConstants.java | 4 + .../command/user/ssh/CreateSSHKeyPairCmd.java | 3 +- .../user/userdata/ListUserDataCmd.java | 3 +- .../user/userdata/RegisterUserDataCmd.java | 3 +- .../auth/UserOAuth2Authenticator.java | 40 ++++ client/conf/server.properties.in | 3 + client/pom.xml | 5 + .../spring-core-registry-core-context.xml | 2 +- .../com/cloud/user/dao/UserAccountDao.java | 2 + .../cloud/user/dao/UserAccountDaoImpl.java | 12 + .../META-INF/db/schema-41810to41900.sql | 20 ++ .../management/MockAccountManager.java | 5 + plugins/pom.xml | 1 + plugins/user-authenticators/oauth2/pom.xml | 63 +++++ .../cloudstack/oauth2/OAuth2AuthManager.java | 59 +++++ .../oauth2/OAuth2AuthManagerImpl.java | 192 +++++++++++++++ .../oauth2/OAuth2UserAuthenticator.java | 78 +++++++ .../oauth2/OauthProviderResponse.java | 109 +++++++++ .../api/command/DeleteOAuthProviderCmd.java | 84 +++++++ .../api/command/ListOAuthProvidersCmd.java | 135 +++++++++++ .../OauthLoginAPIAuthenticatorCmd.java | 219 ++++++++++++++++++ .../api/command/RegisterOAuthProviderCmd.java | 109 +++++++++ .../api/response/OauthProviderResponse.java | 31 +++ .../oauth2/dao/OauthProviderDao.java | 26 +++ .../oauth2/dao/OauthProviderDaoImpl.java | 44 ++++ .../oauth2/github/GithubOAuth2Provider.java | 56 +++++ .../oauth2/github/GithubOAuth2Utils.java | 122 ++++++++++ .../oauth2/google/GoogleOAuth2Provider.java | 89 +++++++ .../oauth2/google/GoogleOAuth2Utils.java | 97 ++++++++ .../cloudstack/oauth2/vo/OauthProviderVO.java | 106 +++++++++ .../cloudstack/oauth2/module.properties | 18 ++ .../oauth2/spring-oauth2-context.xml | 55 +++++ .../auth/APIAuthenticationManagerImpl.java | 1 - .../com/cloud/user/AccountManagerImpl.java | 21 +- .../cloud/user/MockAccountManagerImpl.java | 5 + tools/apidoc/gen_toc.py | 2 + ui/package.json | 4 +- ui/public/assets/github.svg | 1 + ui/public/assets/google-color.svg | 1 + ui/public/assets/google.svg | 1 + ui/public/locales/en.json | 5 + ui/src/api/index.js | 24 ++ ui/src/config/router.js | 9 + ui/src/config/section/config.js | 33 +++ ui/src/main.js | 2 + ui/src/permission.js | 2 +- ui/src/store/modules/user.js | 53 ++++- ui/src/views/auth/Login.vue | 121 +++++++++- ui/src/views/dashboard/VerifyOauth.vue | 18 ++ ui/src/views/iam/AddUser.vue | 3 + 52 files changed, 2089 insertions(+), 16 deletions(-) create mode 100644 api/src/main/java/org/apache/cloudstack/auth/UserOAuth2Authenticator.java create mode 100644 plugins/user-authenticators/oauth2/pom.xml create mode 100644 plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OAuth2AuthManager.java create mode 100644 plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OAuth2AuthManagerImpl.java create mode 100644 plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OAuth2UserAuthenticator.java create mode 100644 plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OauthProviderResponse.java create mode 100644 plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/DeleteOAuthProviderCmd.java create mode 100644 plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/ListOAuthProvidersCmd.java create mode 100644 plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/OauthLoginAPIAuthenticatorCmd.java create mode 100644 plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/RegisterOAuthProviderCmd.java create mode 100644 plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/response/OauthProviderResponse.java create mode 100644 plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/dao/OauthProviderDao.java create mode 100644 plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/dao/OauthProviderDaoImpl.java create mode 100644 plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/github/GithubOAuth2Provider.java create mode 100644 plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/github/GithubOAuth2Utils.java create mode 100644 plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java create mode 100644 plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Utils.java create mode 100644 plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/vo/OauthProviderVO.java create mode 100644 plugins/user-authenticators/oauth2/src/main/resources/META-INF/cloudstack/oauth2/module.properties create mode 100644 plugins/user-authenticators/oauth2/src/main/resources/META-INF/cloudstack/oauth2/spring-oauth2-context.xml create mode 100644 ui/public/assets/github.svg create mode 100644 ui/public/assets/google-color.svg create mode 100644 ui/public/assets/google.svg create mode 100644 ui/src/views/dashboard/VerifyOauth.vue diff --git a/api/src/main/java/com/cloud/user/AccountService.java b/api/src/main/java/com/cloud/user/AccountService.java index 77a5b442e861..1bb364fd9ff6 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); + UserAccount 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..62dba56383b1 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,7 @@ 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 MANAGED = "managed"; public static final String CAPACITY_BYTES = "capacitybytes"; public static final String CAPACITY_IOPS = "capacityiops"; @@ -1056,6 +1057,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..6ede512b4e99 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/auth/UserOAuth2Authenticator.java @@ -0,0 +1,40 @@ +// 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; + +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); + +} diff --git a/client/conf/server.properties.in b/client/conf/server.properties.in index 42d98a6eb29c..75d77995781f 100644 --- a/client/conf/server.properties.in +++ b/client/conf/server.properties.in @@ -49,3 +49,6 @@ webapp.dir=/usr/share/cloudstack-management/webapp # The path to access log file access.log=/var/log/cloudstack/management/access.log + +OAUTH_CLIENT_ID=345798102268-cfcpg40k6hnfft2m61mf6jbmjcfg4p82.apps.googleusercontent.com +OAUTH_CLIENT_SECRET=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 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..74d4d946d98f 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); + UserAccount 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..06b90056ca23 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 UserAccount 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 findOneBy(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..e9bb750b514a 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,23 @@ 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 + `kind` = 'Order', + `options` = 'PBKDF2,SHA256SALT,MD5,LDAP,SAML2,PLAINTEXT,OAUTH2' +where `name` = 'user.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', + `redirect_uri` varchar(255) NOT NULL COMMENT 'redirect uri which is configured in the provider', + `created` datetime NOT NULL COMMENT 'date created', + `removed` datetime COMMENT 'date removed if not null', + PRIMARY KEY (`id`), + UNIQUE KEY (`provider`, `client_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..eab8b4a7ec2d 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 UserAccount 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..a30cef573e1a --- /dev/null +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OAuth2AuthManager.java @@ -0,0 +1,59 @@ +// +// 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.ListOAuthProvidersCmd; +import org.apache.cloudstack.oauth2.api.command.RegisterOAuthProviderCmd; +import org.apache.cloudstack.oauth2.vo.OauthProviderVO; + +import java.util.List; + +public interface OAuth2AuthManager extends PluggableAPIAuthenticator, PluggableService { + public static final ConfigKey OAuth2IsPluginEnabled = new ConfigKey("Advanced", Boolean.class, "oauth2.enabled", "false", + "Indicates whether OAuth SSO 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); + + public boolean authorizeUser(Long userId, String oAuthProviderId, boolean enable); + + OauthProviderVO registerOauthProvider(RegisterOAuthProviderCmd cmd); + + List listOauthProviders(ListOAuthProvidersCmd cmd); + + boolean deleteOauthProvider(Long id); +} 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..879c3a925b8e --- /dev/null +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OAuth2AuthManagerImpl.java @@ -0,0 +1,192 @@ +// +// 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.UserVO; +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.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.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 + 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(RegisterOAuthProviderCmd.class); + cmdList.add(ListOAuthProvidersCmd.class); + cmdList.add(DeleteOAuthProviderCmd.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; + } + + private boolean isOAuthPluginEnabled() { + return OAuth2IsPluginEnabled.value(); + } + + @Override + public boolean stop() { + return false; + } + + @Override + public List> getCommands() { + List> cmdList = new ArrayList>(); + 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 boolean authorizeUser(Long userId, String oAuthProviderId, boolean enable) { + UserVO user = _userDao.getUser(userId); + if (user != null) { + if (enable) { + user.setExternalEntity(oAuthProviderId); + user.setSource(User.Source.OAUTH2); + } else { + return false; + } + _userDao.update(user.getId(), user); + return true; + } + return false; + } + + @Override + public OauthProviderVO registerOauthProvider(RegisterOAuthProviderCmd cmd) { + String description = cmd.getDescription(); + String provider = cmd.getProvider(); + String clientId = cmd.getClientId(); + String redirectUri = cmd.getRedirectUri(); + + 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, redirectUri); + } + + @Override + public List listOauthProviders(ListOAuthProvidersCmd cmd) { + if (OAuth2IsPluginEnabled.value()) { + return _oauthProviderDao.listAll(); + } + + return new ArrayList(); + } + + private OauthProviderVO saveOauthProvider(String provider, String description, String clientId, String redirectUri) { + final OauthProviderVO oauthProviderVO = new OauthProviderVO(); + + oauthProviderVO.setProvider(provider); + oauthProviderVO.setDescription(description); + oauthProviderVO.setClientId(clientId); + oauthProviderVO.setRedirectUri(redirectUri); + + _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/OauthProviderResponse.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OauthProviderResponse.java new file mode 100644 index 000000000000..1ccc8499ac0d --- /dev/null +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OauthProviderResponse.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; + +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.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.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 redirectUri) { + this.id = id; + this.provider = provider; + this.description = description; + this.clientId = clientId; + 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; + } +} 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..9bce2119f363 --- /dev/null +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/DeleteOAuthProviderCmd.java @@ -0,0 +1,84 @@ +// 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.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.api.response.UserResponse; +import org.apache.cloudstack.context.CallContext; + +@APICommand(name = "deleteOauthProvider", description = "Deletes the registered OAuth provider", responseObject = SuccessResponse.class, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) +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 = UserResponse.class, required = true, description = "id of the user to be deleted") + private Long id; + + 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..bebd18eddd86 --- /dev/null +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/ListOAuthProvidersCmd.java @@ -0,0 +1,135 @@ +// 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.OauthProviderResponse; +import org.apache.cloudstack.oauth2.vo.OauthProviderVO; +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, since = "4.19", + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +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 Long id; + + @Parameter(name = ApiConstants.PROVIDER, type = CommandType.STRING, description = "Name of the provider") + private String provider; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + public Long 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 { + List resultList = _oauth2mgr.listOauthProviders(this); + 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.getRedirectUri()); + if (authenticatorPluginNames.contains(result.getProvider())) { + r.setEnabled(true); + } + 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..83b7a4e25566 --- /dev/null +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/OauthLoginAPIAuthenticatorCmd.java @@ -0,0 +1,219 @@ +// 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.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. 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 = {}) +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 = "Hashed password (Default is MD5). If you wish to use any other hashing algorithm, you would need to write a custom authentication adapter See Docs section.", required = true) + private String provider; + + @Parameter(name = ApiConstants.EMAIL, type = CommandType.STRING, description = "Hashed password (Default is MD5). If you wish to use any other hashing algorithm, you would need to write a custom authentication adapter See Docs section.", 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 = "The id of the domain that the user belongs to. If both domain and domainId are passed in, \"domainId\" parameter takes precedence.") + 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[] domainIdArr = (String[])params.get(ApiConstants.DOMAIN_ID); + + if (domainIdArr == null) { + domainIdArr = (String[])params.get(ApiConstants.DOMAIN__ID); + } + final String[] domainName = (String[])params.get(ApiConstants.DOMAIN); + 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)); + } + } + + String domain = null; + domain = getDomainName(auditTrailSb, domainName, domain); + + String serializedResponse = null; + 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"); + } + + try { + final Domain userDomain = _domainService.findDomainByIdOrPath(domainId, domain); + if (userDomain != null) { + domainId = userDomain.getId(); + } else { + throw new CloudAuthenticationException("Unable to find the domain from the path " + domain); + } + final UserAccount userAccount = _accountService.getActiveUserAccountByEmail(email, domainId); + if (userAccount == null) { + throw new CloudAuthenticationException("User not found in CloudStack to login"); + } + 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,"); + // TODO: fall through to API key, or just fail here w/ auth error? (HTTP 401) + 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); + } + + @Nullable + private String getDomainName(StringBuilder auditTrailSb, String[] domainName, String domain) { + 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..7eab850426c0 --- /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.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") +public class RegisterOAuthProviderCmd extends BaseCmd { + + private static final String s_name = "ConfigureOAuthProvider"; + + ///////////////////////////////////////////////////// + //////////////// 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.REDIRECT_URI, type = CommandType.STRING, description = "Redicect 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 String getCommandName() { + return s_name; + } + + @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 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.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/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..49556594b283 --- /dev/null +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/response/OauthProviderResponse.java @@ -0,0 +1,31 @@ +package org.apache.cloudstack.oauth2.api.response; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; +import org.apache.cloudstack.api.response.AuthenticationCmdResponse; + +public class OauthProviderResponse extends AuthenticationCmdResponse { + @SerializedName("id") + @Param(description = "The Oauth Provider ID") + private String id; + + @SerializedName("providerName") + @Param(description = "Oauth Provider name") + private String providerName; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getProviderName() { + return providerName; + } + + public void setProviderName(String providerName) { + this.providerName = providerName; + } +} 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..719beb30f933 --- /dev/null +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/github/GithubOAuth2Provider.java @@ -0,0 +1,56 @@ +//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 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; + +public class GithubOAuth2Provider extends AdapterBase implements UserOAuth2Authenticator { + + @Inject + OauthProviderDao _oauthProviderDao; + + @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("Google provider is not registered, so user cannot be verified"); + } + + return true; + } +} diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/github/GithubOAuth2Utils.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/github/GithubOAuth2Utils.java new file mode 100644 index 000000000000..76d858883c58 --- /dev/null +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/github/GithubOAuth2Utils.java @@ -0,0 +1,122 @@ +// +// 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.github; + +import com.cloud.utils.PropertiesUtil; +import com.cloud.utils.server.ServerProperties; +import com.google.api.client.auth.oauth2.Credential; +import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow; +import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken; +import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.gson.GsonFactory; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.client.util.store.MemoryDataStoreFactory; +import com.google.api.services.oauth2.Oauth2; +import com.google.api.services.oauth2.model.Userinfo; +import org.apache.log4j.Logger; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Properties; + +public class GithubOAuth2Utils { + + private static final Logger s_logger = Logger.getLogger(GithubOAuth2Utils.class); + + private static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport(); + + public static GoogleAuthorizationCodeFlow newFlow() throws IOException { + String oauthClientId = null; + String oauthClientSecret = null; + + final File confFile = PropertiesUtil.findConfigFile("server.properties"); + + s_logger.info("Server configuration file found: " + confFile.getAbsolutePath()); + + try { + InputStream is = new FileInputStream(confFile); + final Properties properties = ServerProperties.getServerProperties(is); + + oauthClientId = "345798102268-cfcpg40k6hnfft2m61mf6jbmjcfg4p82.apps.googleusercontent.com"; + oauthClientSecret = "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"; + } catch (final IOException e) { + } + + List scopes = Arrays.asList( + "https://www.googleapis.com/auth/userinfo.profile", + "https://www.googleapis.com/auth/userinfo.email"); + + return new GoogleAuthorizationCodeFlow.Builder( + HTTP_TRANSPORT, JacksonFactory.getDefaultInstance(), oauthClientId, oauthClientSecret, scopes) + .setDataStoreFactory(MemoryDataStoreFactory.getDefaultInstance()) + .build(); + } + + public static boolean isUserLoggedIn(String userCredential) { + try { + Credential credential = newFlow().loadCredential(userCredential); + return credential != null; + } catch (IOException e) { + return false; + } + } + + public static Userinfo getUserInfo(String userCredential) throws IOException { + String appName = "MyCloud"; + Credential credential = newFlow().loadCredential(userCredential); + Oauth2 oauth2Client = + new Oauth2.Builder(HTTP_TRANSPORT, JacksonFactory.getDefaultInstance(), credential) + .setApplicationName(appName) + .build(); + + Userinfo userInfo = oauth2Client.userinfo().get().execute(); + return userInfo; + } + + public static String getEmail(String credential) { + String jwt = credential; + GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(new NetHttpTransport(),new GsonFactory()) + .setAudience(Collections.singletonList("345798102268-cfcpg40k6hnfft2m61mf6jbmjcfg4p82.apps.googleusercontent.com")) + .build(); + + GoogleIdToken idToken; + + try { + idToken = verifier.verify(jwt); + } catch (GeneralSecurityException e) { + throw new RuntimeException("Cannot verify the ID_TOKEN send :" + e.getMessage()); + } catch (IOException e) { + throw new RuntimeException(e); + } + + if(idToken == null){ + throw new RuntimeException("Failed to verify the ID_TOKEN send"); + } + + return idToken.getPayload().getEmail(); + } +} 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..a57ef1761ae2 --- /dev/null +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java @@ -0,0 +1,89 @@ +//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.google.api.client.googleapis.auth.oauth2.GoogleIdToken; +import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.gson.GsonFactory; +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.util.Collections; + +public class GoogleOAuth2Provider extends AdapterBase implements UserOAuth2Authenticator { + private static final Logger s_logger = Logger.getLogger(GoogleOAuth2Provider.class); + + @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(String.format("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 = verifySecretAndGetEmail(secretCode, providerVO.getClientId()); + + if (!verifiedEmail.equals(email)) { + throw new CloudAuthenticationException("Verification of the email and credentials failed as the email "); + } + + return true; + } + + public static String verifySecretAndGetEmail(String credential, String clientId) { + String jwt = credential; + GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(new NetHttpTransport(), new GsonFactory()) + .setAudience(Collections.singletonList(clientId)) + .build(); + + GoogleIdToken idToken; + try { + idToken = verifier.verify(jwt); + } catch (Exception e) { + throw new CloudAuthenticationException("Could not verify the credentials provided, failed with exception:" + e.getMessage()); + } + + if (idToken == null) { + throw new CloudAuthenticationException("Failed to verify the credentials send"); + } + + return idToken.getPayload().getEmail(); + } +} diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Utils.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Utils.java new file mode 100644 index 000000000000..fa654dbe9d59 --- /dev/null +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Utils.java @@ -0,0 +1,97 @@ +// +// 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.google; + +import com.cloud.utils.PropertiesUtil; +import com.cloud.utils.server.ServerProperties; +import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow; +import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken; +import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.gson.GsonFactory; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.client.util.store.MemoryDataStoreFactory; +import org.apache.log4j.Logger; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Properties; + +public class GoogleOAuth2Utils { + + private static final Logger s_logger = Logger.getLogger(GoogleOAuth2Utils.class); + + private static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport(); + + public static GoogleAuthorizationCodeFlow newFlow() throws IOException { + String oauthClientId = null; + String oauthClientSecret = null; + + final File confFile = PropertiesUtil.findConfigFile("server.properties"); + + s_logger.info("Server configuration file found: " + confFile.getAbsolutePath()); + + try { + InputStream is = new FileInputStream(confFile); + final Properties properties = ServerProperties.getServerProperties(is); + + oauthClientId = "345798102268-cfcpg40k6hnfft2m61mf6jbmjcfg4p82.apps.googleusercontent.com"; + oauthClientSecret = "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"; + } catch (final IOException e) { + } + + List scopes = Arrays.asList( + "https://www.googleapis.com/auth/userinfo.profile", + "https://www.googleapis.com/auth/userinfo.email"); + + return new GoogleAuthorizationCodeFlow.Builder( + HTTP_TRANSPORT, JacksonFactory.getDefaultInstance(), oauthClientId, oauthClientSecret, scopes) + .setDataStoreFactory(MemoryDataStoreFactory.getDefaultInstance()) + .build(); + } + public static String getEmail(String credential) { + String jwt = credential; + GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(new NetHttpTransport(),new GsonFactory()) + .setAudience(Collections.singletonList("345798102268-cfcpg40k6hnfft2m61mf6jbmjcfg4p82.apps.googleusercontent.com")) + .build(); + + GoogleIdToken idToken; + + try { + idToken = verifier.verify(jwt); + } catch (GeneralSecurityException e) { + throw new RuntimeException("Cannot verify the ID_TOKEN send :" + e.getMessage()); + } catch (IOException e) { + throw new RuntimeException(e); + } + + if(idToken == null){ + throw new RuntimeException("Failed to verify the ID_TOKEN send"); + } + + return idToken.getPayload().getEmail(); + } +} 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..874e0fc2749e --- /dev/null +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/vo/OauthProviderVO.java @@ -0,0 +1,106 @@ +// 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 = "redirect_uri") + private String redirectUri; + + @Column(name = GenericDao.CREATED_COLUMN) + private Date created; + + @Column(name = GenericDao.REMOVED_COLUMN) + private Date removed; + + 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; + } +} 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..2e84711d31ab --- /dev/null +++ b/plugins/user-authenticators/oauth2/src/main/resources/META-INF/cloudstack/oauth2/spring-oauth2-context.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..4e363344120f 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -52,6 +52,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 +2330,11 @@ public UserAccount getActiveUserAccount(String username, Long domainId) { return _userAccountDao.getUserAccount(username, domainId); } + @Override + public UserAccount getActiveUserAccountByEmail(String email, Long domainId) { + return _userAccountDao.getUserAccountByEmail(email, domainId); + } + @Override public Account getActiveAccountById(long accountId) { return _accountDao.findById(accountId); @@ -2473,7 +2479,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 +2638,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/MockAccountManagerImpl.java b/server/src/test/java/com/cloud/user/MockAccountManagerImpl.java index 1c6137201551..d65342cad78d 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 UserAccount 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..ce945e54aded 100644 --- a/tools/apidoc/gen_toc.py +++ b/tools/apidoc/gen_toc.py @@ -158,6 +158,8 @@ 'listIdps': 'Authentication', 'authorizeSamlSso': 'Authentication', 'listSamlAuthorization': 'Authentication', + 'oauthlogin': 'Authentication', + 'deleteOauthProvider': '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..c4172efa14b1 --- /dev/null +++ b/ui/public/assets/github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/public/assets/google-color.svg b/ui/public/assets/google-color.svg new file mode 100644 index 000000000000..af259abad32b --- /dev/null +++ b/ui/public/assets/google-color.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/public/assets/google.svg b/ui/public/assets/google.svg new file mode 100644 index 000000000000..a67b92781e63 --- /dev/null +++ b/ui/public/assets/google.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 2118fa2e5255..e67b83c14414 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -450,6 +450,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 +798,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 +1399,7 @@ "label.number": "#Rule", "label.numretries": "Number of retries", "label.nvpdeviceid": "ID", +"label.oauth.configuration": "OAuth configuration", "label.ocfs2": "OCFS2", "label.of": "of", "label.of.month": "of month", @@ -1615,11 +1618,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", diff --git a/ui/src/api/index.js b/ui/src/api/index.js index 5e4286289870..32c65af1f765 100644 --- a/ui/src/api/index.js +++ b/ui/src/api/index.js @@ -70,3 +70,27 @@ 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('response', 'json') + return axios({ + url: '/', + method: 'post', + data: params, + headers: { + 'content-type': 'application/x-www-form-urlencoded' + } + }) +} diff --git a/ui/src/config/router.js b/ui/src/config/router.js index 502a0246edfa..eae1b60a6dc7 100644 --- a/ui/src/config/router.js +++ b/ui/src/config/router.js @@ -301,6 +301,15 @@ export const constantRouterMap = [ }, component: () => import('@/views/dashboard/VerifyTwoFa') }, + { + path: '/verifyOauth', + name: 'VerifyOauth', + meta: { + title: 'label.two.factor.authentication', + hidden: true + }, + component: () => import('@/views/dashboard/VerifyOauth') + }, { path: '/setup2FA', name: 'SetupTwoFaAtLogin', diff --git a/ui/src/config/section/config.js b/ui/src/config/section/config.js index 3cddc6396b09..bc3ed35aa27e 100644 --- a/ui/src/config/section/config.js +++ b/ui/src/config/section/config.js @@ -70,6 +70,39 @@ export default { } ] }, + { + name: 'oauthsetting', + title: 'label.oauth.configuration', + icon: 'login-outlined', + docHelp: 'adminguide/accounts.html#using-an-ldap-server-for-user-authentication', + permission: ['listOauthProvider'], + columns: ['provider', 'description', 'clientid', 'redirecturi', 'enabled'], + details: ['provider', 'description', 'clientid', 'redirecturi', 'enabled'], + actions: [ + { + api: 'registerOAuthProvider', + icon: 'plus-outlined', + label: 'label.register.oauth', + listView: true, + args: [ + 'provider', 'description', 'clientid', 'redirecturi' + ], + mapping: { + provider: { + options: ['google', 'github'] + } + } + }, + { + api: 'deleteOauthProvider', + icon: 'delete-outlined', + label: 'label.action.delete.guest.os', + message: 'message.action.delete.guest.os', + dataView: true, + popup: true + } + ] + }, { name: 'hypervisorcapability', title: 'label.hypervisor.capabilities', diff --git a/ui/src/main.js b/ui/src/main.js index 2f1d892fbd86..94732fd52dea 100644 --- a/ui/src/main.js +++ b/ui/src/main.js @@ -39,6 +39,7 @@ import { } from './utils/plugins' import { VueAxios } from './utils/request' import directives from './utils/directives' +import vue3GoogleLogin from 'vue3-google-login' vueApp.use(VueAxios, router) vueApp.use(pollJobPlugin) @@ -53,6 +54,7 @@ vueApp.use(localesPlugin) vueApp.use(genericUtilPlugin) vueApp.use(extensions) vueApp.use(directives) +vueApp.use(vue3GoogleLogin, { clientId: '345798102268-cfcpg40k6hnfft2m61mf6jbmjcfg4p82.apps.googleusercontent.com' }) fetch('config.json').then(response => response.json()).then(config => { vueProps.$config = config diff --git a/ui/src/permission.js b/ui/src/permission.js index b780073a8bac..a31a8a2bd38f 100644 --- a/ui/src/permission.js +++ b/ui/src/permission.js @@ -30,7 +30,7 @@ import { ACCESS_TOKEN, APIS, SERVER_MANAGER, CURRENT_PROJECT } from '@/store/mut NProgress.configure({ showSpinner: false }) // NProgress Configuration -const allowList = ['login'] // no redirect allowlist +const allowList = ['login', 'VerifyOauth'] // no redirect allowlist router.beforeEach((to, from, next) => { // start progress bar diff --git a/ui/src/store/modules/user.js b/ui/src/store/modules/user.js index 0e45ac7e676b..2589f6e72ebd 100644 --- a/ui/src/store/modules/user.js +++ b/ui/src/store/modules/user.js @@ -22,7 +22,7 @@ import notification from 'ant-design-vue/es/notification' import { vueProps } from '@/vue-app' import router from '@/router' import store from '@/store' -import { login, logout, api } from '@/api' +import { oauthlogin, login, logout, api } from '@/api' import { i18n } from '@/locales' import { @@ -168,8 +168,59 @@ const user = { }, Login ({ commit }, userInfo) { return new Promise((resolve, reject) => { + console.log(userInfo) login(userInfo).then(response => { const result = response.loginresponse || {} + console.log(result) + Cookies.set('account', result.account, { expires: 1 }) + Cookies.set('domainid', result.domainid, { expires: 1 }) + Cookies.set('role', result.type, { expires: 1 }) + Cookies.set('timezone', result.timezone, { expires: 1 }) + Cookies.set('timezoneoffset', result.timezoneoffset, { expires: 1 }) + Cookies.set('userfullname', result.firstname + ' ' + result.lastname, { expires: 1 }) + Cookies.set('userid', result.userid, { expires: 1 }) + Cookies.set('username', result.username, { expires: 1 }) + vueProps.$localStorage.set(ACCESS_TOKEN, result.sessionkey, 24 * 60 * 60 * 1000) + commit('SET_TOKEN', result.sessionkey) + commit('SET_TIMEZONE_OFFSET', result.timezoneoffset) + + const cachedUseBrowserTimezone = vueProps.$localStorage.get(USE_BROWSER_TIMEZONE, false) + commit('SET_USE_BROWSER_TIMEZONE', cachedUseBrowserTimezone) + const darkMode = vueProps.$localStorage.get(DARK_MODE, false) + commit('SET_DARK_MODE', darkMode) + const cachedCustomColumns = vueProps.$localStorage.get(CUSTOM_COLUMNS, {}) + commit('SET_CUSTOM_COLUMNS', cachedCustomColumns) + + commit('SET_APIS', {}) + commit('SET_NAME', '') + commit('SET_AVATAR', '') + commit('SET_INFO', {}) + commit('SET_PROJECT', {}) + commit('SET_HEADER_NOTICES', []) + commit('SET_FEATURES', {}) + commit('SET_LDAP', {}) + commit('SET_CLOUDIAN', {}) + commit('SET_DOMAIN_STORE', {}) + commit('SET_LOGOUT_FLAG', false) + commit('SET_2FA_ENABLED', (result.is2faenabled === 'true')) + commit('SET_2FA_PROVIDER', result.providerfor2fa) + commit('SET_2FA_ISSUER', result.issuerfor2fa) + commit('SET_LOGIN_FLAG', false) + notification.destroy() + + resolve() + }).catch(error => { + reject(error) + }) + }) + }, + + OauthLogin ({ commit }, userInfo) { + return new Promise((resolve, reject) => { + console.log(userInfo) + oauthlogin(userInfo).then(response => { + const result = response.loginresponse || {} + console.log(result) Cookies.set('account', result.account, { expires: 1 }) Cookies.set('domainid', result.domainid, { expires: 1 }) Cookies.set('role', result.type, { expires: 1 }) diff --git a/ui/src/views/auth/Login.vue b/ui/src/views/auth/Login.vue index cab9b887f0a7..c9ae8ad886d1 100644 --- a/ui/src/views/auth/Login.vue +++ b/ui/src/views/auth/Login.vue @@ -153,6 +153,19 @@ >{{ $t('label.login') }} +

+
+
+ + +
+ +
@@ -164,6 +177,7 @@ import { mapActions } from 'vuex' import { sourceToken } from '@/utils/request' import { SERVER_MANAGER } from '@/store/mutation-types' import TranslationMenu from '@/components/header/TranslationMenu' +import { decodeCredential } from 'vue3-google-login' export default { components: { @@ -173,7 +187,17 @@ export default { return { idps: [], customActiveKey: 'cs', + customActiveKeyOauth: false, loginBtn: false, + email: '', + secretcode: '', + oauthexclude: '', + googleprovider: false, + githubprovider: false, + googleredirecturi: '', + githubredirecturi: '', + googleclientid: '', + githubclientid: '', loginType: 0, state: { time: 60, @@ -199,7 +223,7 @@ export default { } }, methods: { - ...mapActions(['Login', 'Logout']), + ...mapActions(['Login', 'Logout', 'OauthLogin']), initForm () { this.formRef = ref() this.form = reactive({ @@ -209,7 +233,7 @@ export default { this.setRules() }, setRules () { - if (this.customActiveKey === 'cs') { + if (this.customActiveKey === 'cs' && this.customActiveKeyOauth === false) { this.rules.username = [ { required: true, @@ -245,6 +269,23 @@ export default { this.form.idp = this.idps[0].id || '' } }) + api('listOauthProvider', {}).then(response => { + if (response) { + const oauthproviders = response.listoauthproviderresponse.oauthprovider || [] + oauthproviders.forEach(item => { + if (item.provider === 'google') { + this.googleprovider = true + this.googleclientid = item.clientid + this.googleredirecturi = item.redirecturi + } + if (item.provider === 'github') { + this.githubprovider = true + this.githubclientid = item.clientid + this.githubredirecturi = item.redirecturi + } + }) + } + }) }, // handler async handleUsernameOrEmail (rule, value) { @@ -261,6 +302,48 @@ export default { this.customActiveKey = key this.setRules() }, + callback (response) { + console.log(response) + try { + const user = decodeCredential(response.credential) + this.email = user.email + this.secretcode = response.credential + this.handleSubmitOauth('google') + } catch (e) { + console.log(e) + } + }, + getGitHubUrl (from) { + const rootURl = 'https://github.com/login/oauth/authorize' + const options = { + client_id: this.githubclientid, + scope: 'user:email', + state: from + } + + const qs = new URLSearchParams(options) + + return `${rootURl}?${qs.toString()}` + }, + getGoogleUrl (from) { + const rootUrl = 'https://accounts.google.com/o/oauth2/v2/auth' + const options = { + redirect_uri: this.googleredirecturi, + client_id: this.googleclientid, + access_type: 'offline', + response_type: 'code', + prompt: 'consent', + scope: [ + 'https://www.googleapis.com/auth/userinfo.profile', + 'https://www.googleapis.com/auth/userinfo.email' + ].join(' '), + state: from + } + + const qs = new URLSearchParams(options) + + return `${rootUrl}?${qs.toString()}` + }, handleSubmit (e) { e.preventDefault() if (this.state.loginBtn) return @@ -299,6 +382,27 @@ export default { this.formRef.value.scrollToField(error.errorFields[0].name) }) }, + handleSubmitOauth (provider) { + this.customActiveKeyOauth = true + this.setRules() + this.formRef.value.validate().then(() => { + const values = toRaw(this.form) + const loginParams = { ...values } + delete loginParams.username + loginParams.email = this.email + loginParams.provider = provider + loginParams.secretcode = this.secretcode + if (!loginParams.domain) { + loginParams.domain = '/' + } + this.OauthLogin(loginParams) + .then((res) => this.loginSuccess(res)) + .catch(err => { + this.requestFailed(err) + this.state.loginBtn = false + }) + }) + }, loginSuccess (res) { this.$notification.destroy() this.$store.commit('SET_COUNT_NOTIFY', 0) @@ -372,6 +476,19 @@ export default { .register { float: right; } + + .g-btn-wrapper { + background-color: rgb(221, 75, 57); + height: 40px; + width: 80px; + } } + .center { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 100px; + } } diff --git a/ui/src/views/dashboard/VerifyOauth.vue b/ui/src/views/dashboard/VerifyOauth.vue new file mode 100644 index 000000000000..6ef3a8478f14 --- /dev/null +++ b/ui/src/views/dashboard/VerifyOauth.vue @@ -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. + +console.log('testOauth') diff --git a/ui/src/views/iam/AddUser.vue b/ui/src/views/iam/AddUser.vue index 49bca3278965..0f1594e92bb8 100644 --- a/ui/src/views/iam/AddUser.vue +++ b/ui/src/views/iam/AddUser.vue @@ -169,6 +169,9 @@ + + +
{{ $t('label.cancel') }} {{ $t('label.ok') }} From f91e975e11398b8f0f9b26718392e728154c07a6 Mon Sep 17 00:00:00 2001 From: Harikrishna Patnala Date: Mon, 25 Sep 2023 13:56:13 +0530 Subject: [PATCH 02/36] Fixed API doc --- .../cloudstack/oauth2/api/command/RegisterOAuthProviderCmd.java | 2 +- tools/apidoc/gen_toc.py | 2 ++ ui/src/config/section/config.js | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) 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 index 7eab850426c0..0c1a0171ebf6 100644 --- 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 @@ -34,7 +34,7 @@ import java.util.Collection; import java.util.Map; -@APICommand(name = "registerOAuthProvider", responseObject = SuccessResponse.class, description = "Register the OAuth2 provider in CloudStack") +@APICommand(name = "registerOauthProvider", responseObject = SuccessResponse.class, description = "Register the OAuth2 provider in CloudStack") public class RegisterOAuthProviderCmd extends BaseCmd { private static final String s_name = "ConfigureOAuthProvider"; diff --git a/tools/apidoc/gen_toc.py b/tools/apidoc/gen_toc.py index ce945e54aded..87196edeee57 100644 --- a/tools/apidoc/gen_toc.py +++ b/tools/apidoc/gen_toc.py @@ -160,6 +160,8 @@ 'listSamlAuthorization': 'Authentication', 'oauthlogin': 'Authentication', 'deleteOauthProvider': 'Oauth', + 'listOauthProvider': 'Oauth', + 'registerOauthProvider': 'Oauth', 'quota': 'Quota', 'emailTemplate': 'Quota', 'Capacity': 'System Capacity', diff --git a/ui/src/config/section/config.js b/ui/src/config/section/config.js index bc3ed35aa27e..4446f1bacd78 100644 --- a/ui/src/config/section/config.js +++ b/ui/src/config/section/config.js @@ -80,7 +80,7 @@ export default { details: ['provider', 'description', 'clientid', 'redirecturi', 'enabled'], actions: [ { - api: 'registerOAuthProvider', + api: 'registerOauthProvider', icon: 'plus-outlined', label: 'label.register.oauth', listView: true, From 900e96c7eeac160bd5384daaeff91ffada2f07bd Mon Sep 17 00:00:00 2001 From: Harikrishna Patnala Date: Mon, 25 Sep 2023 14:13:04 +0530 Subject: [PATCH 03/36] Remove unused code and fix tests --- client/conf/server.properties.in | 3 - .../oauth2/OauthProviderResponse.java | 109 ------------------ .../api/command/ListOAuthProvidersCmd.java | 2 +- .../api/command/RegisterOAuthProviderCmd.java | 2 +- .../api/response/OauthProviderResponse.java | 100 ++++++++++++++-- .../cloud/user/AccountManagerImplTest.java | 14 +-- ui/public/assets/google.svg | 1 - ui/src/views/iam/AddUser.vue | 3 - 8 files changed, 98 insertions(+), 136 deletions(-) delete mode 100644 plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OauthProviderResponse.java delete mode 100644 ui/public/assets/google.svg diff --git a/client/conf/server.properties.in b/client/conf/server.properties.in index 75d77995781f..42d98a6eb29c 100644 --- a/client/conf/server.properties.in +++ b/client/conf/server.properties.in @@ -49,6 +49,3 @@ webapp.dir=/usr/share/cloudstack-management/webapp # The path to access log file access.log=/var/log/cloudstack/management/access.log - -OAUTH_CLIENT_ID=345798102268-cfcpg40k6hnfft2m61mf6jbmjcfg4p82.apps.googleusercontent.com -OAUTH_CLIENT_SECRET=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 diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OauthProviderResponse.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OauthProviderResponse.java deleted file mode 100644 index 1ccc8499ac0d..000000000000 --- a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OauthProviderResponse.java +++ /dev/null @@ -1,109 +0,0 @@ -// 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.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.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.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 redirectUri) { - this.id = id; - this.provider = provider; - this.description = description; - this.clientId = clientId; - 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; - } -} 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 index bebd18eddd86..a2b482fe6b5e 100644 --- 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 @@ -36,7 +36,7 @@ import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.auth.UserOAuth2Authenticator; import org.apache.cloudstack.oauth2.OAuth2AuthManager; -import org.apache.cloudstack.oauth2.OauthProviderResponse; +import org.apache.cloudstack.oauth2.api.response.OauthProviderResponse; import org.apache.cloudstack.oauth2.vo.OauthProviderVO; import org.apache.log4j.Logger; 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 index 0c1a0171ebf6..52d293dea2ae 100644 --- 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 @@ -19,7 +19,7 @@ import org.apache.cloudstack.api.response.SuccessResponse; import org.apache.cloudstack.oauth2.OAuth2AuthManager; -import org.apache.cloudstack.oauth2.OauthProviderResponse; +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; 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 index 49556594b283..c0f264f1ef42 100644 --- 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 @@ -1,17 +1,62 @@ +// 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.response.AuthenticationCmdResponse; +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; -public class OauthProviderResponse extends AuthenticationCmdResponse { - @SerializedName("id") - @Param(description = "The Oauth Provider ID") +@EntityReference(value = OauthProviderVO.class) +public class OauthProviderResponse extends BaseResponse { + + @SerializedName(ApiConstants.ID) + @Param(description = "ID of the provider") private String id; - @SerializedName("providerName") - @Param(description = "Oauth Provider name") - private String providerName; + @SerializedName(ApiConstants.PROVIDER) + @Param(description = "Name of the provider") + private String provider; + + @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.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 redirectUri) { + this.id = id; + this.provider = provider; + this.description = description; + this.clientId = clientId; + this.redirectUri = redirectUri; + } public String getId() { return id; @@ -21,11 +66,44 @@ public void setId(String id) { this.id = id; } - public String getProviderName() { - return providerName; + 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 setProviderName(String providerName) { - this.providerName = providerName; + public void setRedirectUri(String redirectUri) { + this.redirectUri = redirectUri; } } diff --git a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java index f61cc028b311..7545559e5898 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); } diff --git a/ui/public/assets/google.svg b/ui/public/assets/google.svg deleted file mode 100644 index a67b92781e63..000000000000 --- a/ui/public/assets/google.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/src/views/iam/AddUser.vue b/ui/src/views/iam/AddUser.vue index 0f1594e92bb8..49bca3278965 100644 --- a/ui/src/views/iam/AddUser.vue +++ b/ui/src/views/iam/AddUser.vue @@ -169,9 +169,6 @@
- - -
{{ $t('label.cancel') }} {{ $t('label.ok') }} From 1eb6f1d5cfb07177cdf7de8f2a8865e94aa3c485 Mon Sep 17 00:00:00 2001 From: Harikrishna Patnala Date: Tue, 26 Sep 2023 12:43:03 +0530 Subject: [PATCH 04/36] Fix plugin defaults --- .../spring-core-registry-core-context.xml | 2 +- .../META-INF/db/schema-41810to41900.sql | 5 +++++ .../oauth2/OAuth2AuthManagerImpl.java | 8 ++++++-- .../api/command/DeleteOAuthProviderCmd.java | 3 +++ .../api/command/RegisterOAuthProviderCmd.java | 9 +-------- .../oauth2/spring-oauth2-context.xml | 5 +++-- ui/src/api/index.js | 1 + ui/src/main.js | 20 +++++++++++++++++++ ui/src/permission.js | 2 ++ ui/src/views/auth/Login.vue | 9 ++++++++- ui/src/views/dashboard/VerifyOauth.vue | 16 ++++++++++++++- 11 files changed, 65 insertions(+), 15 deletions(-) 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 74d4d946d98f..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 @@ -47,7 +47,7 @@ class="org.apache.cloudstack.spring.lifecycle.registry.ExtensionRegistry"> - + > getAuthCommands() { List> cmdList = new ArrayList>(); cmdList.add(OauthLoginAPIAuthenticatorCmd.class); - cmdList.add(RegisterOAuthProviderCmd.class); cmdList.add(ListOAuthProvidersCmd.class); - cmdList.add(DeleteOAuthProviderCmd.class); return cmdList; } @@ -87,6 +85,12 @@ public boolean stop() { @Override public List> getCommands() { List> cmdList = new ArrayList>(); + if (!isOAuthPluginEnabled()) { + return cmdList; + } + cmdList.add(RegisterOAuthProviderCmd.class); + cmdList.add(DeleteOAuthProviderCmd.class); + return cmdList; } 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 index 9bce2119f363..20000c3c4862 100644 --- 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 @@ -30,6 +30,8 @@ import org.apache.cloudstack.api.response.UserResponse; 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) public class DeleteOAuthProviderCmd extends BaseCmd { @@ -42,6 +44,7 @@ public class DeleteOAuthProviderCmd extends BaseCmd { @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = UserResponse.class, required = true, description = "id of the user to be deleted") private Long id; + @Inject OAuth2AuthManager _oauthMgr; ///////////////////////////////////////////////////// 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 index 52d293dea2ae..5139169f5b71 100644 --- 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 @@ -37,8 +37,6 @@ @APICommand(name = "registerOauthProvider", responseObject = SuccessResponse.class, description = "Register the OAuth2 provider in CloudStack") public class RegisterOAuthProviderCmd extends BaseCmd { - private static final String s_name = "ConfigureOAuthProvider"; - ///////////////////////////////////////////////////// //////////////// API parameters ///////////////////// ///////////////////////////////////////////////////// @@ -52,18 +50,13 @@ public class RegisterOAuthProviderCmd extends BaseCmd { @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.REDIRECT_URI, type = CommandType.STRING, description = "Redicect URI pre-registered in the specific OAuth provider", required = true) + @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 String getCommandName() { - return s_name; - } - @Override public long getEntityOwnerId() { return CallContext.current().getCallingAccount().getId(); 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 index 2e84711d31ab..04a6c8dabfe7 100644 --- 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 @@ -25,7 +25,7 @@ http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> - + @@ -36,7 +36,8 @@ - + + diff --git a/ui/src/api/index.js b/ui/src/api/index.js index 32c65af1f765..1db416612766 100644 --- a/ui/src/api/index.js +++ b/ui/src/api/index.js @@ -84,6 +84,7 @@ export function oauthlogin (arg) { 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: '/', diff --git a/ui/src/main.js b/ui/src/main.js index 94732fd52dea..be7a9696b2c7 100644 --- a/ui/src/main.js +++ b/ui/src/main.js @@ -40,6 +40,7 @@ import { import { VueAxios } from './utils/request' import directives from './utils/directives' import vue3GoogleLogin from 'vue3-google-login' +import { api } from '@/api' vueApp.use(VueAxios, router) vueApp.use(pollJobPlugin) @@ -56,6 +57,25 @@ vueApp.use(extensions) vueApp.use(directives) vueApp.use(vue3GoogleLogin, { clientId: '345798102268-cfcpg40k6hnfft2m61mf6jbmjcfg4p82.apps.googleusercontent.com' }) +api('listOauthProvider', {}).then(response => { + console.log('in main.js') + if (response) { + const oauthproviders = response.listoauthproviderresponse.oauthprovider || [] + oauthproviders.forEach(item => { + if (item.provider === 'google') { + this.googleprovider = true + this.googleclientid = item.clientid + this.googleredirecturi = item.redirecturi + } + if (item.provider === 'github') { + this.githubprovider = true + this.githubclientid = item.clientid + this.githubredirecturi = item.redirecturi + } + }) + } +}) + fetch('config.json').then(response => response.json()).then(config => { vueProps.$config = config let basUrl = config.apiBase diff --git a/ui/src/permission.js b/ui/src/permission.js index a31a8a2bd38f..78cbb370521a 100644 --- a/ui/src/permission.js +++ b/ui/src/permission.js @@ -134,6 +134,8 @@ router.beforeEach((to, from, next) => { } } } else { + console.log(to.name) + console.log(to.fullPath) if (allowList.includes(to.name)) { next() } else { diff --git a/ui/src/views/auth/Login.vue b/ui/src/views/auth/Login.vue index c9ae8ad886d1..b7fe6da50e0f 100644 --- a/ui/src/views/auth/Login.vue +++ b/ui/src/views/auth/Login.vue @@ -159,6 +159,12 @@
+
+ + + Sign in with Google + +
-
- - - Sign in with Google - -