User two factor authentication (#6924)

Co-authored-by: Rohit Yadav <rohit.yadav@shapeblue.com>
This commit is contained in:
Harikrishna 2023-02-13 13:44:17 +05:30 committed by GitHub
parent 90c92f2710
commit a3feccf70c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
88 changed files with 3498 additions and 101 deletions

View File

@ -75,6 +75,7 @@ jobs:
smoke/test_list_ids_parameter
smoke/test_loadbalance
smoke/test_login
smoke/test_2fa
smoke/test_metrics_api
smoke/test_migration
smoke/test_multipleips_per_nic

View File

@ -0,0 +1,32 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package com.cloud.exception;
import com.cloud.utils.SerialVersionUID;
import com.cloud.utils.exception.CloudRuntimeException;
public class CloudTwoFactorAuthenticationException extends CloudRuntimeException {
private static final long serialVersionUID = SerialVersionUID.CloudTwoFactorAuthenticationException;
public CloudTwoFactorAuthenticationException(String message) {
super(message);
}
public CloudTwoFactorAuthenticationException(String message, Throwable th) {
super(message, th);
}
}

View File

@ -16,6 +16,7 @@
// under the License.
package com.cloud.user;
import java.util.List;
import java.util.Map;
import org.apache.cloudstack.acl.ControlledEntity;
@ -33,6 +34,7 @@ import com.cloud.network.vpc.VpcOffering;
import com.cloud.offering.DiskOffering;
import com.cloud.offering.NetworkOffering;
import com.cloud.offering.ServiceOffering;
import org.apache.cloudstack.auth.UserTwoFactorAuthenticator;
public interface AccountService {
@ -124,4 +126,18 @@ public interface AccountService {
public Map<String, String> getKeys(GetUserKeysCmd cmd);
public Map<String, String> getKeys(Long userId);
/**
* Lists user two-factor authentication provider plugins
* @return list of providers
*/
List<UserTwoFactorAuthenticator> listUserTwoFactorAuthenticationProviders();
/**
* Finds user two factor authenticator provider by domain ID
* @param domainId domain id
* @return backup provider
*/
UserTwoFactorAuthenticator getUserTwoFactorAuthenticationProvider(final Long domainId);
}

View File

@ -90,4 +90,8 @@ public interface User extends OwnedBy, InternalIdentity {
public String getExternalEntity();
public void setExternalEntity(String entity);
public boolean isUser2faEnabled();
public String getKeyFor2fa();
}

View File

@ -17,6 +17,7 @@
package com.cloud.user;
import java.util.Date;
import java.util.Map;
import org.apache.cloudstack.api.InternalIdentity;
@ -67,4 +68,21 @@ public interface UserAccount extends InternalIdentity {
public String getExternalEntity();
public void setExternalEntity(String entity);
public boolean isUser2faEnabled();
public void setUser2faEnabled(boolean user2faEnabled);
public String getKeyFor2fa();
public void setKeyFor2fa(String keyFor2fa);
public String getUser2faProvider();
public void setUser2faProvider(String user2faProvider);
public Map<String, String> getDetails();
public void setDetails(Map<String, String> details);
}

View File

@ -239,6 +239,10 @@ public class ApiConstants {
public static final String IP_ADDRESSES = "ipaddresses";
public static final String IP6_ADDRESS = "ip6address";
public static final String IP_ADDRESS_ID = "ipaddressid";
public static final String IS_2FA_ENABLED = "is2faenabled";
public static final String IS_2FA_VERIFIED = "is2faverified";
public static final String IS_2FA_MANDATED = "is2famandated";
public static final String IS_ASYNC = "isasync";
public static final String IP_AVAILABLE = "ipavailable";
public static final String IP_LIMIT = "iplimit";
@ -1003,6 +1007,11 @@ public class ApiConstants {
public static final String ADMINS_ONLY = "adminsonly";
public static final String ANNOTATION_FILTER = "annotationfilter";
public static final String CODE_FOR_2FA = "codefor2fa";
public static final String PROVIDER_FOR_2FA = "providerfor2fa";
public static final String ISSUER_FOR_2FA = "issuerfor2fa";
public static final String MANDATE_2FA = "mandate2fa";
public static final String SECRET_CODE = "secretcode";
public static final String LOGIN = "login";
public static final String LOGOUT = "logout";
public static final String LIST_IDPS = "listIdps";
@ -1010,6 +1019,7 @@ public class ApiConstants {
public static final String PUBLIC_MTU = "publicmtu";
public static final String PRIVATE_MTU = "privatemtu";
public static final String MTU = "mtu";
public static final String LIST_APIS = "listApis";
/**
* This enum specifies IO Drivers, each option controls specific policies on I/O.

View File

@ -23,6 +23,7 @@ package org.apache.cloudstack.api;
public enum ApiErrorCode {
UNAUTHORIZED(401),
UNAUTHORIZED2FA(511),
METHOD_NOT_ALLOWED(405),
MALFORMED_PARAMETER_ERROR(430),
PARAM_ERROR(431),

View File

@ -17,5 +17,5 @@
package org.apache.cloudstack.api.auth;
public enum APIAuthenticationType {
LOGIN_API, LOGOUT_API, READONLY_API
LOGIN_API, LOGOUT_API, READONLY_API, LOGIN_2FA_API
}

View File

@ -42,9 +42,11 @@ import com.cloud.exception.InvalidParameterValueException;
import com.cloud.utils.Pair;
import com.cloud.utils.exception.CloudRuntimeException;
@APICommand(name = "listConfigurations", description = "Lists all configurations.", responseObject = ConfigurationResponse.class,
@APICommand(name = ListCfgsByCmd.APINAME, description = "Lists all configurations.", responseObject = ConfigurationResponse.class,
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
public class ListCfgsByCmd extends BaseListCmd {
public static final String APINAME = "listConfigurations";
public static final Logger s_logger = Logger.getLogger(ListCfgsByCmd.class.getName());

View File

@ -79,6 +79,10 @@ public class UpdateUserCmd extends BaseCmd {
@Parameter(name = ApiConstants.USERNAME, type = CommandType.STRING, description = "Unique username")
private String username;
@Parameter(name = ApiConstants.MANDATE_2FA, type = CommandType.BOOLEAN, description = "Provide true to mandate the user to use two factor authentication has to be enabled." +
"This parameter is only used to mandate 2FA, not to disable 2FA", since = "4.18.0.0")
private Boolean mandate2FA;
@Inject
private RegionService _regionService;
@ -126,6 +130,10 @@ public class UpdateUserCmd extends BaseCmd {
return username;
}
public Boolean getMandate2FA() {
return mandate2FA;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////

View File

@ -70,6 +70,22 @@ public class LoginCmdResponse extends AuthenticationCmdResponse {
@Param(description = "Session key that can be passed in subsequent Query command calls", isSensitive = true)
private String sessionKey;
@SerializedName(value = ApiConstants.IS_2FA_ENABLED)
@Param(description = "Is two factor authentication enabled", since = "4.18.0.0")
private String is2FAenabled;
@SerializedName(value = ApiConstants.IS_2FA_VERIFIED)
@Param(description = "Is two factor authentication verified", since = "4.18.0.0")
private String is2FAverified;
@SerializedName(value = ApiConstants.PROVIDER_FOR_2FA)
@Param(description = "Two factor authentication provider", since = "4.18.0.0")
private String providerFor2FA;
@SerializedName(value = ApiConstants.ISSUER_FOR_2FA)
@Param(description = "Two factor authentication issuer", since = "4.18.0.0")
private String issuerFor2FA;
public String getUsername() {
return username;
}
@ -163,4 +179,36 @@ public class LoginCmdResponse extends AuthenticationCmdResponse {
public void setSessionKey(String sessionKey) {
this.sessionKey = sessionKey;
}
public String is2FAenabled() {
return is2FAenabled;
}
public void set2FAenabled(String is2FAenabled) {
this.is2FAenabled = is2FAenabled;
}
public String is2FAverfied() {
return is2FAverified;
}
public void set2FAverfied(String is2FAverified) {
this.is2FAverified = is2FAverified;
}
public String getProviderFor2FA() {
return providerFor2FA;
}
public void setProviderFor2FA(String providerFor2FA) {
this.providerFor2FA = providerFor2FA;
}
public String getIssuerFor2FA() {
return issuerFor2FA;
}
public void setIssuerFor2FA(String issuerFor2FA) {
this.issuerFor2FA = issuerFor2FA;
}
}

View File

@ -120,6 +120,14 @@ public class UserResponse extends BaseResponse implements SetResourceIconRespons
@Param(description = "Base64 string representation of the resource icon", since = "4.16.0.0")
ResourceIconResponse icon;
@SerializedName(ApiConstants.IS_2FA_ENABLED)
@Param(description = "true if user has two factor authentication enabled", since = "4.18.0.0")
private Boolean is2FAenabled;
@SerializedName(ApiConstants.IS_2FA_MANDATED)
@Param(description = "true if user has two factor authentication is mandated", since = "4.18.0.0")
private Boolean is2FAmandated;
@Override
public String getObjectId() {
return this.getId();
@ -285,4 +293,20 @@ public class UserResponse extends BaseResponse implements SetResourceIconRespons
public void setResourceIconResponse(ResourceIconResponse icon) {
this.icon = icon;
}
public Boolean is2FAenabled() {
return is2FAenabled;
}
public void set2FAenabled(Boolean is2FAenabled) {
this.is2FAenabled = is2FAenabled;
}
public Boolean getIs2FAmandated() {
return is2FAmandated;
}
public void set2FAmandated(Boolean is2FAmandated) {
this.is2FAmandated = is2FAmandated;
}
}

View File

@ -0,0 +1,63 @@
/*
* 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.api.response;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseResponse;
import com.cloud.serializer.Param;
import com.google.gson.annotations.SerializedName;
public class UserTwoFactorAuthenticationSetupResponse extends BaseResponse {
@SerializedName("id")
@Param(description = "the user ID")
private String id;
@SerializedName("username")
@Param(description = "the user name")
private String username;
@SerializedName("accountid")
@Param(description = "the account ID of the user")
private String accountId;
@SerializedName(ApiConstants.SECRET_CODE)
@Param(description = "secret code that needs to be registered with authenticator")
private String secretCode;
public void setId(String id) {
this.id = id;
}
public void setUsername(String username) {
this.username = username;
}
public void setAccountId(String accountId) {
this.accountId = accountId;
}
public void setSecretCode(String secretCode) {
this.secretCode = secretCode;
}
public String getSecretCode() {
return secretCode;
}
}

View File

@ -0,0 +1,52 @@
// 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.api.response;
import com.cloud.serializer.Param;
import com.google.gson.annotations.SerializedName;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseResponse;
import org.apache.cloudstack.api.EntityReference;
import org.apache.cloudstack.auth.UserTwoFactorAuthenticator;
@EntityReference(UserTwoFactorAuthenticator.class)
public class UserTwoFactorAuthenticatorProviderResponse extends BaseResponse {
@SerializedName(ApiConstants.NAME)
@Param(description = "the user two factor authenticator provider name")
private String name;
@SerializedName(ApiConstants.DESCRIPTION)
@Param(description = "the description of the user two factor authenticator provider")
private String description;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}

View File

@ -0,0 +1,45 @@
// 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.exception.CloudTwoFactorAuthenticationException;
import com.cloud.user.UserAccount;
import com.cloud.utils.component.Adapter;
public interface UserTwoFactorAuthenticator extends Adapter {
/**
* Returns the unique name of the provider
* @return returns provider name
*/
String getName();
/**
* Returns the description about the user 2FA provider plugin
* @return returns description
*/
String getDescription();
/**
* Verifies the 2FA code provided by user
* @return returns description
*/
void check2FA(String code, UserAccount userAccount) throws CloudTwoFactorAuthenticationException;
String setup2FAKey(UserAccount userAccount);
}

View File

@ -181,6 +181,16 @@
<artifactId>cloud-plugin-user-authenticator-sha256salted</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-plugin-user-two-factor-authenticator-totp</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-plugin-user-two-factor-authenticator-staticpin</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-plugin-metrics</artifactId>

View File

@ -32,7 +32,13 @@
<bean class="org.apache.cloudstack.spring.lifecycle.registry.RegistryLifecycle">
<property name="registry" ref="userAuthenticatorsRegistry" />
<property name="typeClass"
value="com.cloud.server.auth.UserAuthenticator" />
value="org.apache.cloudstack.auth.UserAuthenticator" />
</bean>
<bean class="org.apache.cloudstack.spring.lifecycle.registry.RegistryLifecycle">
<property name="registry" ref="userTwoFactorAuthenticatorsRegistry" />
<property name="typeClass"
value="org.apache.cloudstack.auth.UserTwoFactorAuthenticator" />
</bean>
<bean class="org.apache.cloudstack.spring.lifecycle.registry.RegistryLifecycle">
@ -64,7 +70,7 @@
<bean class="org.apache.cloudstack.spring.lifecycle.registry.RegistryLifecycle">
<property name="registry" ref="userPasswordEncodersRegistry" />
<property name="typeClass" value="com.cloud.server.auth.UserAuthenticator" />
<property name="typeClass" value="org.apache.cloudstack.auth.UserAuthenticator" />
</bean>
</beans>

View File

@ -36,6 +36,13 @@
<property name="orderConfigDefault" value="PBKDF2,SHA256SALT,MD5,LDAP,SAML2,PLAINTEXT" />
</bean>
<bean id="userTwoFactorAuthenticatorsRegistry"
class="org.apache.cloudstack.spring.lifecycle.registry.ExtensionRegistry">
<property name="orderConfigKey" value="user.2fa.authenticators.order" />
<property name="excludeKey" value="user.2fa.authenticators.exclude" />
<property name="orderConfigDefault" value="totp,staticpin" />
</bean>
<bean id="pluggableAPIAuthenticatorsRegistry"
class="org.apache.cloudstack.spring.lifecycle.registry.ExtensionRegistry">
<property name="orderConfigKey" value="pluggableApi.authenticators.order" />

View File

@ -17,6 +17,7 @@
package com.cloud.user;
import java.util.Date;
import java.util.Map;
import javax.persistence.Column;
import javax.persistence.Entity;
@ -28,6 +29,7 @@ import javax.persistence.Id;
import javax.persistence.PrimaryKeyJoinColumn;
import javax.persistence.SecondaryTable;
import javax.persistence.Table;
import javax.persistence.Transient;
import org.apache.cloudstack.api.InternalIdentity;
@ -109,6 +111,21 @@ public class UserAccountVO implements UserAccount, InternalIdentity {
@Column(name = "external_entity", length = 65535)
private String externalEntity = null;
@Column(name = "is_user_2fa_enabled")
private boolean user2faEnabled = false;
@Column(name = "user_2fa_provider")
private String user2faProvider;
@Column(name = "key_for_2fa")
private String keyFor2fa;
@Transient
Map<String, String> details;
public enum Setup2FAstatus {
ENABLED, VERIFIED
}
public UserAccountVO() {
}
@ -311,4 +328,44 @@ public class UserAccountVO implements UserAccount, InternalIdentity {
public void setExternalEntity(String externalEntity) {
this.externalEntity = externalEntity;
}
@Override
public boolean isUser2faEnabled() {
return user2faEnabled;
}
@Override
public void setUser2faEnabled(boolean user2faEnabled) {
this.user2faEnabled = user2faEnabled;
}
@Override
public String getKeyFor2fa() {
return keyFor2fa;
}
@Override
public void setKeyFor2fa(String keyFor2fa) {
this.keyFor2fa = keyFor2fa;
}
@Override
public String getUser2faProvider() {
return user2faProvider;
}
@Override
public void setUser2faProvider(String user2faProvider) {
this.user2faProvider = user2faProvider;
}
@Override
public Map<String, String> getDetails() {
return details;
}
@Override
public void setDetails(Map<String, String> details) {
this.details = details;
}
}

View File

@ -106,6 +106,15 @@ public class UserVO implements User, Identity, InternalIdentity {
@Column(name = "external_entity", length = 65535)
private String externalEntity;
@Column(name = "is_user_2fa_enabled")
private boolean user2faEnabled;
@Column(name = "user_2fa_provider")
private String user2faProvider;
@Column(name = "key_for_2fa")
private String keyFor2fa;
public UserVO() {
this.uuid = UUID.randomUUID().toString();
}
@ -316,4 +325,29 @@ public class UserVO implements User, Identity, InternalIdentity {
public void setExternalEntity(String externalEntity) {
this.externalEntity = externalEntity;
}
public boolean isUser2faEnabled() {
return user2faEnabled;
}
public void setUser2faEnabled(boolean user2faEnabled) {
this.user2faEnabled = user2faEnabled;
}
public String getKeyFor2fa() {
return keyFor2fa;
}
public void setKeyFor2fa(String keyFor2fa) {
this.keyFor2fa = keyFor2fa;
}
public String getUser2faProvider() {
return user2faProvider;
}
public void setUser2faProvider(String user2faProvider) {
this.user2faProvider = user2faProvider;
}
}

View File

@ -45,6 +45,8 @@ public class UserDetailVO implements ResourceDetail {
@Column(name = "display")
private boolean display = true;
public static final String Setup2FADetail = "2FASetupStatus";
public UserDetailVO() {
}
@ -69,6 +71,10 @@ public class UserDetailVO implements ResourceDetail {
return value;
}
public void setValue(String value) {
this.value = value;
}
@Override
public long getResourceId() {
return resourceId;

View File

@ -1520,3 +1520,53 @@ INSERT INTO `cloud`.`role_permissions` (`uuid`, `role_id`, `rule`, `permission`)
-- Increases the precision of the column `quota_used` from 15 to 20, keeping the scale of 8.
ALTER TABLE `cloud_usage`.`quota_usage` MODIFY COLUMN quota_used decimal(20,8) unsigned NOT NULL;
ALTER TABLE `cloud`.`user` ADD COLUMN `is_user_2fa_enabled` tinyint NOT NULL DEFAULT 0;
ALTER TABLE `cloud`.`user` ADD COLUMN `key_for_2fa` varchar(255) default NULL;
ALTER TABLE `cloud`.`user` ADD COLUMN `user_2fa_provider` varchar(255) default NULL;
DROP VIEW IF EXISTS `cloud`.`user_view`;
CREATE VIEW `cloud`.`user_view` AS
select
user.id,
user.uuid,
user.username,
user.password,
user.firstname,
user.lastname,
user.email,
user.state,
user.api_key,
user.secret_key,
user.created,
user.removed,
user.timezone,
user.registration_token,
user.is_registered,
user.incorrect_login_attempts,
user.source,
user.default,
account.id account_id,
account.uuid account_uuid,
account.account_name account_name,
account.type account_type,
account.role_id account_role_id,
domain.id domain_id,
domain.uuid domain_uuid,
domain.name domain_name,
domain.path domain_path,
async_job.id job_id,
async_job.uuid job_uuid,
async_job.job_status job_status,
async_job.account_id job_account_id,
user.is_user_2fa_enabled is_user_2fa_enabled
from
`cloud`.`user`
inner join
`cloud`.`account` ON user.account_id = account.id
inner join
`cloud`.`domain` ON account.domain_id = domain.id
left join
`cloud`.`async_job` ON async_job.instance_id = user.id
and async_job.instance_type = 'User'
and async_job.job_status = 0;

View File

@ -45,6 +45,7 @@ import org.apache.cloudstack.affinity.dao.AffinityGroupDao;
import org.apache.cloudstack.affinity.dao.AffinityGroupDaoImpl;
import org.apache.cloudstack.affinity.dao.AffinityGroupDomainMapDaoImpl;
import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDaoImpl;
import org.apache.cloudstack.auth.UserAuthenticator;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.engine.datacenter.entity.api.db.dao.DcDetailsDaoImpl;
import org.apache.cloudstack.engine.orchestration.NetworkOrchestrator;
@ -241,7 +242,6 @@ import com.cloud.server.ManagementServer;
import com.cloud.server.ResourceMetaDataService;
import com.cloud.server.StatsCollector;
import com.cloud.server.TaggedResourceService;
import com.cloud.server.auth.UserAuthenticator;
import com.cloud.service.dao.ServiceOfferingDaoImpl;
import com.cloud.service.dao.ServiceOfferingDetailsDaoImpl;
import com.cloud.storage.DataStoreProviderApiService;

View File

@ -24,9 +24,12 @@ import java.net.InetAddress;
import javax.inject.Inject;
import javax.naming.ConfigurationException;
import com.cloud.api.auth.SetupUserTwoFactorAuthenticationCmd;
import org.apache.cloudstack.api.command.admin.account.CreateAccountCmd;
import org.apache.cloudstack.api.command.admin.user.GetUserKeysCmd;
import org.apache.cloudstack.api.command.admin.user.MoveUserCmd;
import org.apache.cloudstack.api.response.UserTwoFactorAuthenticationSetupResponse;
import org.apache.cloudstack.auth.UserTwoFactorAuthenticator;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.log4j.Logger;
@ -326,6 +329,20 @@ public class MockAccountManager extends ManagerBase implements AccountManager {
return false;
}
@Override
public UserTwoFactorAuthenticator getUserTwoFactorAuthenticator(Long domainId, Long userAccountId) {
return null;
}
@Override
public void verifyUsingTwoFactorAuthenticationCode(String code, Long domainId, Long userAccountId) {
}
@Override
public UserTwoFactorAuthenticationSetupResponse setupUserTwoFactorAuthentication(SetupUserTwoFactorAuthenticationCmd cmd) {
return null;
}
@Override
public boolean deleteUserAccount(long arg0) {
// TODO Auto-generated method stub
@ -465,6 +482,16 @@ public class MockAccountManager extends ManagerBase implements AccountManager {
return null;
}
@Override
public List<UserTwoFactorAuthenticator> listUserTwoFactorAuthenticationProviders() {
return null;
}
@Override
public UserTwoFactorAuthenticator getUserTwoFactorAuthenticationProvider(Long domainId) {
return null;
}
@Override
public void checkAccess(User user, ControlledEntity entity)
throws PermissionDeniedException {

View File

@ -138,7 +138,11 @@
<module>user-authenticators/plain-text</module>
<module>user-authenticators/saml2</module>
<module>user-authenticators/sha256salted</module>
</modules>
<module>user-two-factor-authenticators/totp</module>
<module>user-two-factor-authenticators/static-pin</module>
</modules>
<dependencies>
<dependency>
<groupId>org.apache.cloudstack</groupId>
@ -160,6 +164,16 @@
<artifactId>cloud-framework-config</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>de.taimos</groupId>
<artifactId>totp</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.2.1</version>
</dependency>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-api</artifactId>

View File

@ -24,10 +24,10 @@ import java.util.UUID;
import javax.inject.Inject;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.auth.UserAuthenticator;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import com.cloud.server.auth.UserAuthenticator;
import com.cloud.user.Account;
import com.cloud.user.AccountManager;
import com.cloud.user.User;

View File

@ -16,7 +16,6 @@
// under the License.
package groovy.org.apache.cloudstack.ldap
import com.cloud.server.auth.UserAuthenticator
import com.cloud.user.Account
import com.cloud.user.AccountManager
import com.cloud.user.User
@ -24,6 +23,7 @@ import com.cloud.user.UserAccount
import com.cloud.user.UserAccountVO
import com.cloud.user.dao.UserAccountDao
import com.cloud.utils.Pair
import org.apache.cloudstack.auth.UserAuthenticator
import org.apache.cloudstack.ldap.LdapAuthenticator
import org.apache.cloudstack.ldap.LdapManager
import org.apache.cloudstack.ldap.LdapTrustMapVO

View File

@ -17,7 +17,6 @@
package org.apache.cloudstack.ldap;
import com.cloud.server.auth.UserAuthenticator;
import com.cloud.user.AccountManager;
import com.cloud.user.AccountVO;
import com.cloud.user.Account;
@ -26,6 +25,7 @@ import com.cloud.user.UserAccount;
import com.cloud.user.UserAccountVO;
import com.cloud.user.dao.UserAccountDao;
import com.cloud.utils.Pair;
import org.apache.cloudstack.auth.UserAuthenticator;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

View File

@ -13,7 +13,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package com.cloud.server.auth;
package org.apache.cloudstack.auth;
import java.math.BigInteger;
import java.security.MessageDigest;
@ -50,20 +50,20 @@ public class MD5UserAuthenticator extends AdapterBase implements UserAuthenticat
if (StringUtils.isAnyEmpty(username, password)) {
s_logger.debug("Username or Password cannot be empty");
return new Pair<Boolean, ActionOnFailedAuthentication>(false, null);
return new Pair<>(false, null);
}
UserAccount user = _userAccountDao.getUserAccount(username, domainId);
if (user == null) {
s_logger.debug("Unable to find user with " + username + " in domain " + domainId);
return new Pair<Boolean, ActionOnFailedAuthentication>(false, null);
return new Pair<>(false, null);
}
if (!user.getPassword().equals(encode(password))) {
s_logger.debug("Password does not match");
return new Pair<Boolean, ActionOnFailedAuthentication>(false, ActionOnFailedAuthentication.INCREMENT_INCORRECT_LOGIN_ATTEMPT_COUNT);
return new Pair<>(false, ActionOnFailedAuthentication.INCREMENT_INCORRECT_LOGIN_ATTEMPT_COUNT);
}
return new Pair<Boolean, ActionOnFailedAuthentication>(true, null);
return new Pair<>(true, null);
}
@Override

View File

@ -27,7 +27,7 @@
http://www.springframework.org/schema/context/spring-context.xsd"
>
<bean id="MD5UserAuthenticator" class="com.cloud.server.auth.MD5UserAuthenticator">
<bean id="MD5UserAuthenticator" class="org.apache.cloudstack.auth.MD5UserAuthenticator">
<property name="name" value="MD5"/>
</bean>

View File

@ -17,7 +17,7 @@
* under the License.
*/
package com.cloud.server.auth;
package org.apache.cloudstack.auth;
import java.lang.reflect.Field;
@ -28,7 +28,6 @@ import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.runners.MockitoJUnitRunner;
import com.cloud.server.auth.UserAuthenticator.ActionOnFailedAuthentication;
import com.cloud.user.UserAccountVO;
import com.cloud.user.dao.UserAccountDao;
import com.cloud.utils.Pair;
@ -53,7 +52,7 @@ public class MD5UserAuthenticatorTest {
UserAccountVO account = new UserAccountVO();
account.setPassword("5f4dcc3b5aa765d61d8327deb882cf99");
Mockito.when(dao.getUserAccount(Mockito.anyString(), Mockito.anyLong())).thenReturn(account);
Pair<Boolean, ActionOnFailedAuthentication> pair = authenticator.authenticate("admin", "password", 1l, null);
Pair<Boolean, UserAuthenticator.ActionOnFailedAuthentication> pair = authenticator.authenticate("admin", "password", 1l, null);
Assert.assertTrue(pair.first());
}
@ -66,7 +65,7 @@ public class MD5UserAuthenticatorTest {
UserAccountVO account = new UserAccountVO();
account.setPassword("surprise");
Mockito.when(dao.getUserAccount(Mockito.anyString(), Mockito.anyLong())).thenReturn(account);
Pair<Boolean, ActionOnFailedAuthentication> pair = authenticator.authenticate("admin", "password", 1l, null);
Pair<Boolean, UserAuthenticator.ActionOnFailedAuthentication> pair = authenticator.authenticate("admin", "password", 1l, null);
Assert.assertFalse(pair.first());
}
@ -77,7 +76,7 @@ public class MD5UserAuthenticatorTest {
daoField.setAccessible(true);
daoField.set(authenticator, dao);
Mockito.when(dao.getUserAccount(Mockito.anyString(), Mockito.anyLong())).thenReturn(null);
Pair<Boolean, ActionOnFailedAuthentication> pair = authenticator.authenticate("admin", "password", 1l, null);
Pair<Boolean, UserAuthenticator.ActionOnFailedAuthentication> pair = authenticator.authenticate("admin", "password", 1l, null);
Assert.assertFalse(pair.first());
}
}

View File

@ -25,6 +25,7 @@ import java.util.Map;
import javax.inject.Inject;
import org.apache.cloudstack.auth.UserAuthenticator;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.bouncycastle.crypto.PBEParametersGenerator;
@ -32,7 +33,6 @@ import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.util.encoders.Base64;
import com.cloud.server.auth.UserAuthenticator;
import com.cloud.user.UserAccount;
import com.cloud.user.dao.UserAccountDao;
import com.cloud.utils.ConstantTimeComparator;

View File

@ -15,10 +15,10 @@
package org.apache.cloudstack.server.auth;
import com.cloud.server.auth.UserAuthenticator;
import com.cloud.user.UserAccountVO;
import com.cloud.user.dao.UserAccountDao;
import com.cloud.utils.Pair;
import org.apache.cloudstack.auth.UserAuthenticator;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;

View File

@ -13,7 +13,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package com.cloud.server.auth;
package org.apache.cloudstack.auth;
import java.util.Map;
@ -41,20 +41,20 @@ public class PlainTextUserAuthenticator extends AdapterBase implements UserAuthe
if (StringUtils.isAnyEmpty(username, password)) {
s_logger.debug("Username or Password cannot be empty");
return new Pair<Boolean, ActionOnFailedAuthentication>(false, null);
return new Pair<>(false, null);
}
UserAccount user = _userAccountDao.getUserAccount(username, domainId);
if (user == null) {
s_logger.debug("Unable to find user with " + username + " in domain " + domainId);
return new Pair<Boolean, ActionOnFailedAuthentication>(false, null);
return new Pair<>(false, null);
}
if (!user.getPassword().equals(password)) {
s_logger.debug("Password does not match");
return new Pair<Boolean, ActionOnFailedAuthentication>(false, ActionOnFailedAuthentication.INCREMENT_INCORRECT_LOGIN_ATTEMPT_COUNT);
return new Pair<>(false, ActionOnFailedAuthentication.INCREMENT_INCORRECT_LOGIN_ATTEMPT_COUNT);
}
return new Pair<Boolean, ActionOnFailedAuthentication>(true, null);
return new Pair<>(true, null);
}
@Override

View File

@ -27,7 +27,7 @@
http://www.springframework.org/schema/context/spring-context.xsd"
>
<bean id="PlainTextUserAuthenticator" class="com.cloud.server.auth.PlainTextUserAuthenticator">
<bean id="PlainTextUserAuthenticator" class="org.apache.cloudstack.auth.PlainTextUserAuthenticator">
<property name="name" value="PLAINTEXT" />
</bean>

View File

@ -18,10 +18,10 @@ import java.util.Map;
import javax.inject.Inject;
import org.apache.cloudstack.auth.UserAuthenticator;
import org.apache.cxf.common.util.StringUtils;
import org.apache.log4j.Logger;
import com.cloud.server.auth.UserAuthenticator;
import com.cloud.user.User;
import com.cloud.user.UserAccount;
import com.cloud.user.dao.UserAccountDao;

View File

@ -60,6 +60,7 @@ import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.response.LoginCmdResponse;
import org.apache.cloudstack.utils.security.CertUtils;
import org.apache.cloudstack.utils.security.ParserUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.bouncycastle.operator.OperatorCreationException;
import org.joda.time.DateTime;
@ -284,6 +285,12 @@ public class SAMLUtils {
resp.addCookie(new Cookie("role", URLEncoder.encode(loginResponse.getType(), HttpUtils.UTF_8)));
resp.addCookie(new Cookie("username", URLEncoder.encode(loginResponse.getUsername(), HttpUtils.UTF_8)));
resp.addCookie(new Cookie("account", URLEncoder.encode(loginResponse.getAccount(), HttpUtils.UTF_8)));
resp.addCookie(new Cookie("isSAML", URLEncoder.encode("true", HttpUtils.UTF_8)));
resp.addCookie(new Cookie("twoFaEnabled", URLEncoder.encode(loginResponse.is2FAenabled(), HttpUtils.UTF_8)));
String providerFor2FA = loginResponse.getProviderFor2FA();
if (StringUtils.isNotEmpty(providerFor2FA)) {
resp.addCookie(new Cookie("twoFaProvider", URLEncoder.encode(loginResponse.getProviderFor2FA(), HttpUtils.UTF_8)));
}
String timezone = loginResponse.getTimeZone();
if (timezone != null) {
resp.addCookie(new Cookie("timezone", URLEncoder.encode(timezone, HttpUtils.UTF_8)));

View File

@ -23,6 +23,7 @@ import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import org.apache.cloudstack.auth.UserAuthenticator.ActionOnFailedAuthentication;
import org.apache.cloudstack.saml.SAML2UserAuthenticator;
import org.apache.cloudstack.saml.SAMLPluginConstants;
import org.junit.Assert;
@ -32,7 +33,6 @@ import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import com.cloud.server.auth.UserAuthenticator.ActionOnFailedAuthentication;
import com.cloud.user.UserAccountVO;
import com.cloud.user.UserVO;
import com.cloud.user.dao.UserAccountDao;

View File

@ -187,6 +187,7 @@ public class ListAndSwitchSAMLAccountCmdTest extends TestCase {
loginCmdResponse.setFirstName("firstName");
loginCmdResponse.setLastName("lastName");
loginCmdResponse.setSessionKey("newSessionKeyString");
loginCmdResponse.set2FAenabled("false");
Mockito.when(apiServer.loginUser(nullable(HttpSession.class), nullable(String.class), nullable(String.class),
nullable(Long.class), nullable(String.class), nullable(InetAddress.class), nullable(Map.class))).thenReturn(loginCmdResponse);
Mockito.doNothing().when(resp).sendRedirect(nullable(String.class));

View File

@ -14,7 +14,7 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package com.cloud.server.auth;
package org.apache.cloudstack.auth;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
@ -53,7 +53,7 @@ public class SHA256SaltedUserAuthenticator extends AdapterBase implements UserAu
if (StringUtils.isAnyEmpty(username, password)) {
s_logger.debug("Username or Password cannot be empty");
return new Pair<Boolean, ActionOnFailedAuthentication>(false, null);
return new Pair<>(false, null);
}
boolean realUser = true;
@ -63,10 +63,10 @@ public class SHA256SaltedUserAuthenticator extends AdapterBase implements UserAu
realUser = false;
}
/* Fake Data */
String realPassword = new String(s_defaultPassword);
byte[] salt = new String(s_defaultSalt).getBytes();
String realPassword = s_defaultPassword;
byte[] salt = s_defaultSalt.getBytes();
if (realUser) {
String storedPassword[] = user.getPassword().split(":");
String[] storedPassword = user.getPassword().split(":");
if (storedPassword.length != 2) {
s_logger.warn("The stored password for " + username + " isn't in the right format for this authenticator");
realUser = false;
@ -83,10 +83,8 @@ public class SHA256SaltedUserAuthenticator extends AdapterBase implements UserAu
if (!result && realUser) {
action = ActionOnFailedAuthentication.INCREMENT_INCORRECT_LOGIN_ATTEMPT_COUNT;
}
return new Pair<Boolean, ActionOnFailedAuthentication>(result, action);
} catch (NoSuchAlgorithmException e) {
throw new CloudRuntimeException("Unable to hash password", e);
} catch (UnsupportedEncodingException e) {
return new Pair<>(result, action);
} catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
throw new CloudRuntimeException("Unable to hash password", e);
}
}
@ -101,7 +99,7 @@ public class SHA256SaltedUserAuthenticator extends AdapterBase implements UserAu
try {
randomGen = SecureRandom.getInstance("SHA1PRNG");
byte salt[] = new byte[s_saltlen];
byte[] salt = new byte[s_saltlen];
randomGen.nextBytes(salt);
String saltString = new String(Base64.encode(salt));
@ -109,9 +107,7 @@ public class SHA256SaltedUserAuthenticator extends AdapterBase implements UserAu
// 3. concatenate the two and return
return saltString + ":" + hashString;
} catch (NoSuchAlgorithmException e) {
throw new CloudRuntimeException("Unable to hash password", e);
} catch (UnsupportedEncodingException e) {
} catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
throw new CloudRuntimeException("Unable to hash password", e);
}
}

View File

@ -27,7 +27,7 @@
http://www.springframework.org/schema/context/spring-context.xsd"
>
<bean id="SHA256SaltedUserAuthenticator" class="com.cloud.server.auth.SHA256SaltedUserAuthenticator">
<bean id="SHA256SaltedUserAuthenticator" class="org.apache.cloudstack.auth.SHA256SaltedUserAuthenticator">
<property name="name" value="SHA256SALT"/>
</bean>

View File

@ -15,7 +15,7 @@
// specific language governing permissions and limitations
// under the License.
package com.cloud.server.auth.test;
package org.apache.cloudstack.auth.test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
@ -29,6 +29,7 @@ import java.util.Map;
import javax.naming.ConfigurationException;
import org.apache.cloudstack.auth.SHA256SaltedUserAuthenticator;
import org.bouncycastle.util.encoders.Base64;
import org.junit.Before;
import org.junit.Test;
@ -37,7 +38,6 @@ import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import com.cloud.server.auth.SHA256SaltedUserAuthenticator;
import com.cloud.user.UserAccount;
import com.cloud.user.dao.UserAccountDao;
@ -80,7 +80,7 @@ public class AuthenticatorTest {
String encodedPassword = authenticator.encode("password");
String storedPassword[] = encodedPassword.split(":");
assertEquals("hash must consist of two components", storedPassword.length, 2);
assertEquals("hash must consist of two components", 2, storedPassword.length);
byte salt[] = Base64.decode(storedPassword[0]);
String hashedPassword = authenticator.encode("password", salt);

View File

@ -0,0 +1,30 @@
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-plugin-user-two-factor-authenticator-staticpin</artifactId>
<name>Apache CloudStack Plugin - User Two Factor Authenticator Static Pin</name>
<parent>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloudstack-plugins</artifactId>
<version>4.18.0.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
</project>

View File

@ -0,0 +1,75 @@
// 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 javax.inject.Inject;
import com.cloud.exception.CloudTwoFactorAuthenticationException;
import com.cloud.user.UserAccount;
import com.cloud.utils.exception.CloudRuntimeException;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import com.cloud.user.dao.UserAccountDao;
import com.cloud.utils.component.AdapterBase;
import java.security.SecureRandom;
public class StaticPinUserTwoFactorAuthenticator extends AdapterBase implements UserTwoFactorAuthenticator {
public static final Logger s_logger = Logger.getLogger(StaticPinUserTwoFactorAuthenticator.class);
@Inject
private UserAccountDao _userAccountDao;
@Override
public String getName() {
return "staticpin";
}
@Override
public String getDescription() {
return "Static Pin user two factor authentication provider Plugin";
}
@Override
public void check2FA(String code, UserAccount userAccount) throws CloudTwoFactorAuthenticationException {
String expectedCode = getStaticPin(userAccount);
if (expectedCode.equals(code)) {
s_logger.info("2FA matches user's input");
return;
}
throw new CloudTwoFactorAuthenticationException("two-factor authentication code provided is invalid");
}
private String getStaticPin(UserAccount userAccount) {
return userAccount.getKeyFor2fa();
}
@Override
public String setup2FAKey(UserAccount userAccount) {
if (StringUtils.isNotEmpty(userAccount.getKeyFor2fa())) {
throw new CloudRuntimeException(String.format("2FA key is already setup for the user account %s", userAccount.getAccountName()));
}
long timeSeed = System.currentTimeMillis();
SecureRandom rng = new SecureRandom();
rng.setSeed(timeSeed);
int number = rng.nextInt(999999);
String key = String.format("%06d", number);
userAccount.setKeyFor2fa(key);
return key;
}
}

View File

@ -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=staticpin
parent=api

View File

@ -0,0 +1,35 @@
<!--
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.
-->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd"
>
<bean id="StaticPinUserTwoFactorAuthenticator" class="org.apache.cloudstack.auth.StaticPinUserTwoFactorAuthenticator">
<property name="name" value="STATICPIN" />
</bean>
</beans>

View File

@ -0,0 +1,30 @@
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-plugin-user-two-factor-authenticator-totp</artifactId>
<name>Apache CloudStack Plugin - User Two Factor Authenticator TOTP</name>
<parent>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloudstack-plugins</artifactId>
<version>4.18.0.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
</project>

View File

@ -0,0 +1,86 @@
// 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 javax.inject.Inject;
import com.cloud.exception.CloudTwoFactorAuthenticationException;
import com.cloud.utils.exception.CloudRuntimeException;
import de.taimos.totp.TOTP;
import com.cloud.user.UserAccount;
import org.apache.commons.codec.binary.Base32;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import com.cloud.user.dao.UserAccountDao;
import com.cloud.utils.component.AdapterBase;
import java.security.SecureRandom;
public class TotpUserTwoFactorAuthenticator extends AdapterBase implements UserTwoFactorAuthenticator {
public static final Logger s_logger = Logger.getLogger(TotpUserTwoFactorAuthenticator.class);
@Inject
private UserAccountDao _userAccountDao;
@Override
public String getName() {
return "totp";
}
@Override
public String getDescription() {
return "TOTP user two factor authentication provider Plugin";
}
@Override
public void check2FA(String code, UserAccount userAccount) throws CloudTwoFactorAuthenticationException {
String expectedCode = get2FACode(get2FAKey(userAccount));
if (expectedCode.equals(code)) {
s_logger.info("2FA matches user's input");
return;
}
throw new CloudTwoFactorAuthenticationException("two-factor authentication code provided is invalid");
}
private String get2FAKey(UserAccount userAccount) {
return userAccount.getKeyFor2fa();
}
private String get2FACode(String secretKey) {
Base32 base32 = new Base32();
byte[] bytes = base32.decode(secretKey);
String hexKey = Hex.encodeHexString(bytes);
return TOTP.getOTP(hexKey);
}
@Override
public String setup2FAKey(UserAccount userAccount) {
if (StringUtils.isNotEmpty(userAccount.getKeyFor2fa())) {
throw new CloudRuntimeException(String.format("2FA key is already setup for the user account %s", userAccount.getAccountName()));
}
SecureRandom random = new SecureRandom();
byte[] bytes = new byte[20];
random.nextBytes(bytes);
Base32 base32 = new Base32();
String key = base32.encodeToString(bytes);
userAccount.setKeyFor2fa(key);
return key;
}
}

View File

@ -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=totp
parent=api

View File

@ -0,0 +1,35 @@
<!--
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.
-->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd"
>
<bean id="TotpUserTwoFactorAuthenticator" class="org.apache.cloudstack.auth.TotpUserTwoFactorAuthenticator">
<property name="name" value="TOTP" />
</bean>
</beans>

View File

@ -160,6 +160,7 @@ import com.cloud.projects.dao.ProjectDao;
import com.cloud.storage.VolumeApiService;
import com.cloud.user.Account;
import com.cloud.user.AccountManager;
import com.cloud.user.AccountManagerImpl;
import com.cloud.user.DomainManager;
import com.cloud.user.User;
import com.cloud.user.UserAccount;
@ -1069,6 +1070,18 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer
if (ApiConstants.SESSIONKEY.equalsIgnoreCase(attrName)) {
response.setSessionKey(attrObj.toString());
}
if (ApiConstants.IS_2FA_ENABLED.equalsIgnoreCase(attrName)) {
response.set2FAenabled(attrObj.toString());
}
if (ApiConstants.IS_2FA_VERIFIED.equalsIgnoreCase(attrName)) {
response.set2FAverfied(attrObj.toString());
}
if (ApiConstants.PROVIDER_FOR_2FA.equalsIgnoreCase(attrName)) {
response.setProviderFor2FA(attrObj.toString());
}
if (ApiConstants.ISSUER_FOR_2FA.equalsIgnoreCase(attrName)) {
response.setIssuerFor2FA(attrObj.toString());
}
}
}
response.setResponseName("loginresponse");
@ -1132,6 +1145,20 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer
session.setAttribute("timezoneoffset", Float.valueOf(offsetInHrs).toString());
}
boolean is2faEnabled = false;
if (userAcct.isUser2faEnabled() || (Boolean.TRUE.equals(AccountManagerImpl.enableUserTwoFactorAuthentication.valueIn(userAcct.getDomainId())) && Boolean.TRUE.equals(AccountManagerImpl.mandateUserTwoFactorAuthentication.valueIn(userAcct.getDomainId())))) {
is2faEnabled = true;
}
String issuerFor2FA = AccountManagerImpl.userTwoFactorAuthenticationIssuer.valueIn(userAcct.getDomainId());
session.setAttribute(ApiConstants.IS_2FA_ENABLED, Boolean.toString(is2faEnabled));
if (!is2faEnabled) {
session.setAttribute(ApiConstants.IS_2FA_VERIFIED, true);
} else {
session.setAttribute(ApiConstants.IS_2FA_VERIFIED, false);
}
session.setAttribute(ApiConstants.PROVIDER_FOR_2FA, userAcct.getUser2faProvider());
session.setAttribute(ApiConstants.ISSUER_FOR_2FA, issuerFor2FA);
// (bug 5483) generate a session key that the user must submit on every request to prevent CSRF, add that
// to the login response so that session-based authenticators know to send the key back
final SecureRandom sesssionKeyRandom = new SecureRandom();

View File

@ -36,6 +36,7 @@ import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.ApiServerService;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.ServerApiException;
@ -51,11 +52,16 @@ import org.jetbrains.annotations.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.web.context.support.SpringBeanAutowiringSupport;
import com.cloud.api.auth.ListUserTwoFactorAuthenticatorProvidersCmd;
import com.cloud.api.auth.SetupUserTwoFactorAuthenticationCmd;
import com.cloud.api.auth.ValidateUserTwoFactorAuthenticationCodeCmd;
import com.cloud.projects.Project;
import com.cloud.projects.dao.ProjectDao;
import com.cloud.user.Account;
import com.cloud.user.AccountManagerImpl;
import com.cloud.user.AccountService;
import com.cloud.user.User;
import com.cloud.user.UserAccount;
import com.cloud.utils.HttpUtils;
import com.cloud.utils.StringUtils;
@ -219,7 +225,7 @@ public class ApiServlet extends HttpServlet {
logName));
}
if (command != null) {
if (command != null && !command.equals(ValidateUserTwoFactorAuthenticationCodeCmd.APINAME)) {
APIAuthenticator apiAuthenticator = authManager.getAPIAuthenticator(command);
if (apiAuthenticator != null) {
@ -293,17 +299,27 @@ public class ApiServlet extends HttpServlet {
// Initialize an empty context and we will update it after we have verified the request below,
// we no longer rely on web-session here, verifyRequest will populate user/account information
// if a API key exists
Long userId = null;
if (isNew && s_logger.isTraceEnabled()) {
s_logger.trace(String.format("new session: %s", session));
}
if (!isNew && (command.equalsIgnoreCase(ValidateUserTwoFactorAuthenticationCodeCmd.APINAME) || (!skip2FAcheckForAPIs(command) && !skip2FAcheckForUser(session)))) {
s_logger.debug("Verifying two factor authentication");
boolean success = verify2FA(session, command, auditTrailSb, params, remoteAddress, responseType, req, resp);
if (!success) {
s_logger.debug("Verification of two factor authentication failed");
return;
}
}
Long userId = null;
if (!isNew) {
userId = (Long)session.getAttribute("userid");
final String account = (String) session.getAttribute("account");
final Object accountObj = session.getAttribute("accountobj");
if (account != null) {
if (invalidateHttpSesseionIfNeeded(req, resp, auditTrailSb, responseType, params, session, account)) return;
if (invalidateHttpSessionIfNeeded(req, resp, auditTrailSb, responseType, params, session, account)) return;
} else {
if (s_logger.isDebugEnabled()) {
s_logger.debug("no account, this request will be validated through apikey(%s)/signature");
@ -360,6 +376,100 @@ public class ApiServlet extends HttpServlet {
}
}
private boolean checkIfAuthenticatorIsOf2FA(String command) {
boolean verify2FA = false;
APIAuthenticator apiAuthenticator = authManager.getAPIAuthenticator(command);
if (apiAuthenticator != null && apiAuthenticator.getAPIType().equals(APIAuthenticationType.LOGIN_2FA_API)) {
verify2FA = true;
} else {
verify2FA = false;
}
return verify2FA;
}
protected boolean skip2FAcheckForAPIs(String command) {
boolean skip2FAcheck = false;
if (command.equalsIgnoreCase(ApiConstants.LIST_IDPS)
|| command.equalsIgnoreCase(ApiConstants.LIST_APIS)
|| command.equalsIgnoreCase(ListUserTwoFactorAuthenticatorProvidersCmd.APINAME)
|| command.equalsIgnoreCase(SetupUserTwoFactorAuthenticationCmd.APINAME)) {
skip2FAcheck = true;
}
return skip2FAcheck;
}
protected boolean skip2FAcheckForUser(HttpSession session) {
boolean skip2FAcheck = false;
Long userId = (Long) session.getAttribute("userid");
boolean is2FAverified = (boolean) session.getAttribute(ApiConstants.IS_2FA_VERIFIED);
if (is2FAverified) {
s_logger.debug(String.format("Two factor authentication is already verified for the user %d, so skipping", userId));
skip2FAcheck = true;
} else {
UserAccount userAccount = accountMgr.getUserAccountById(userId);
boolean is2FAenabled = userAccount.isUser2faEnabled();
if (is2FAenabled) {
skip2FAcheck = false;
} else {
Long domainId = userAccount.getDomainId();
boolean is2FAmandated = Boolean.TRUE.equals(AccountManagerImpl.enableUserTwoFactorAuthentication.valueIn(domainId)) && Boolean.TRUE.equals(AccountManagerImpl.mandateUserTwoFactorAuthentication.valueIn(domainId));
if (is2FAmandated) {
skip2FAcheck = false;
} else {
skip2FAcheck = true;
}
}
}
return skip2FAcheck;
}
protected boolean verify2FA(HttpSession session, String command, StringBuilder auditTrailSb, Map<String, Object[]> params,
InetAddress remoteAddress, String responseType, HttpServletRequest req, HttpServletResponse resp) {
boolean verify2FA = false;
if (command.equals(ValidateUserTwoFactorAuthenticationCodeCmd.APINAME)) {
APIAuthenticator apiAuthenticator = authManager.getAPIAuthenticator(command);
if (apiAuthenticator != null) {
String responseString = apiAuthenticator.authenticate(command, params, session, remoteAddress, responseType, auditTrailSb, req, resp);
session.setAttribute(ApiConstants.IS_2FA_VERIFIED, true);
HttpUtils.writeHttpResponse(resp, responseString, HttpServletResponse.SC_OK, responseType, ApiServer.JSONcontentType.value());
verify2FA = true;
} else {
s_logger.error("Cannot find API authenticator while verifying 2FA");
auditTrailSb.append(" Cannot find API authenticator while verifying 2FA");
verify2FA = false;
}
} else {
// invalidate the session
Long userId = (Long) session.getAttribute("userid");
UserAccount userAccount = accountMgr.getUserAccountById(userId);
boolean is2FAenabled = userAccount.isUser2faEnabled();
String keyFor2fa = userAccount.getKeyFor2fa();
String providerFor2fa = userAccount.getUser2faProvider();
String errorMsg;
if (is2FAenabled) {
if (org.apache.commons.lang3.StringUtils.isEmpty(keyFor2fa) || org.apache.commons.lang3.StringUtils.isEmpty(providerFor2fa)) {
errorMsg = "Two factor authentication is mandated by admin, user needs to setup 2FA using setupUserTwoFactorAuthentication API and" +
" then verify 2FA using validateUserTwoFactorAuthenticationCode API before calling other APIs. Existing session is invalidated.";
} else {
errorMsg = "Two factor authentication 2FA is enabled but not verified, please verify 2FA using validateUserTwoFactorAuthenticationCode API before calling other APIs. Existing session is invalidated.";
}
} else {
// when (is2FAmandated) is true
errorMsg = "Two factor authentication is mandated by admin, user needs to setup 2FA using setupUserTwoFactorAuthentication API and" +
" then verify 2FA using validateUserTwoFactorAuthenticationCode API before calling other APIs. Existing session is invalidated.";
}
s_logger.error(errorMsg);
invalidateHttpSession(session, String.format("Unable to process the API request for %s from %s due to %s", userId, remoteAddress.getHostAddress(), errorMsg));
auditTrailSb.append(" " + ApiErrorCode.UNAUTHORIZED2FA + " " + errorMsg);
final String serializedResponse = apiServer.getSerializedApiError(ApiErrorCode.UNAUTHORIZED2FA.getHttpCode(), "Unable to process the API request due to :" + errorMsg, params, responseType);
HttpUtils.writeHttpResponse(resp, serializedResponse, ApiErrorCode.UNAUTHORIZED2FA.getHttpCode(), responseType, ApiServer.JSONcontentType.value());
verify2FA = false;
}
return verify2FA;
}
protected void setClientAddressForConsoleEndpointAccess(String command, Map<String, Object[]> params, HttpServletRequest req) throws UnknownHostException {
if (org.apache.commons.lang3.StringUtils.isNotBlank(command) &&
command.equalsIgnoreCase(BaseCmd.getCommandNameByClass(CreateConsoleEndpointCmd.class))) {
@ -400,7 +510,7 @@ public class ApiServlet extends HttpServlet {
return true;
}
private boolean invalidateHttpSesseionIfNeeded(HttpServletRequest req, HttpServletResponse resp, StringBuilder auditTrailSb, String responseType, Map<String, Object[]> params, HttpSession session, String account) {
private boolean invalidateHttpSessionIfNeeded(HttpServletRequest req, HttpServletResponse resp, StringBuilder auditTrailSb, String responseType, Map<String, Object[]> params, HttpSession session, String account) {
if (!HttpUtils.validateSessionKey(session, params, req.getCookies(), ApiConstants.SESSIONKEY)) {
String msg = String.format("invalidating session %s for account %s", session.getId(), account);
invalidateHttpSession(session, msg);

View File

@ -77,6 +77,12 @@ public class APIAuthenticationManagerImpl extends ManagerBase implements APIAuth
List<Class<?>> cmdList = new ArrayList<Class<?>>();
cmdList.add(DefaultLoginAPIAuthenticatorCmd.class);
cmdList.add(DefaultLogoutAPIAuthenticatorCmd.class);
cmdList.add(ListUserTwoFactorAuthenticatorProvidersCmd.class);
cmdList.add(ValidateUserTwoFactorAuthenticationCodeCmd.class);
cmdList.add(SetupUserTwoFactorAuthenticationCmd.class);
for (PluggableAPIAuthenticator apiAuthenticator: _apiAuthenticators) {
List<Class<?>> commands = apiAuthenticator.getAuthCommands();
if (commands != null) {

View File

@ -0,0 +1,98 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package com.cloud.api.auth;
import com.cloud.user.Account;
import com.cloud.user.AccountManager;
import org.apache.cloudstack.acl.RoleType;
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.response.UserTwoFactorAuthenticatorProviderResponse;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.auth.UserTwoFactorAuthenticator;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.List;
@APICommand(name = ListUserTwoFactorAuthenticatorProvidersCmd.APINAME,
description = "Lists user two factor authenticator providers",
authorized = {RoleType.Admin, RoleType.DomainAdmin, RoleType.ResourceAdmin, RoleType.User},
responseObject = UserTwoFactorAuthenticatorProviderResponse.class, since = "4.18.0")
public class ListUserTwoFactorAuthenticatorProvidersCmd extends BaseCmd {
public static final String APINAME = "listUserTwoFactorAuthenticatorProviders";
@Inject
private AccountManager accountManager;
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.NAME, type = BaseCmd.CommandType.STRING, description = "List user two factor authenticator provider by name")
private String name;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public String getName() {
return name;
}
@Override
public String getCommandName() {
return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX;
}
@Override
public long getEntityOwnerId() {
return Account.ACCOUNT_ID_SYSTEM;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
private void setupResponse(final List<UserTwoFactorAuthenticator> providers) {
final ListResponse<UserTwoFactorAuthenticatorProviderResponse> response = new ListResponse<>();
final List<UserTwoFactorAuthenticatorProviderResponse> responses = new ArrayList<>();
for (final UserTwoFactorAuthenticator provider : providers) {
if (provider == null || (getName() != null && !provider.getName().equals(getName()))) {
continue;
}
final UserTwoFactorAuthenticatorProviderResponse userTwoFactorAuthenticatorProviderResponse = new UserTwoFactorAuthenticatorProviderResponse();
userTwoFactorAuthenticatorProviderResponse.setName(provider.getName());
userTwoFactorAuthenticatorProviderResponse.setDescription(provider.getDescription());
userTwoFactorAuthenticatorProviderResponse.setObjectName("providers");
responses.add(userTwoFactorAuthenticatorProviderResponse);
}
response.setResponses(responses);
response.setResponseName(getCommandName());
setResponseObject(response);
}
@Override
public void execute() {
List<UserTwoFactorAuthenticator> providers = accountManager.listUserTwoFactorAuthenticationProviders();
setupResponse(providers);
}
}

View File

@ -0,0 +1,96 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package com.cloud.api.auth;
import com.cloud.user.AccountManager;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.UserResponse;
import org.apache.cloudstack.api.response.UserTwoFactorAuthenticationSetupResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.log4j.Logger;
import javax.inject.Inject;
@APICommand(name = SetupUserTwoFactorAuthenticationCmd.APINAME, description = "Setup the 2FA for the user.", authorized = {RoleType.Admin, RoleType.DomainAdmin, RoleType.ResourceAdmin, RoleType.User}, requestHasSensitiveInfo = false,
responseObject = UserTwoFactorAuthenticationSetupResponse.class, entityType = {}, since = "4.18.0")
public class SetupUserTwoFactorAuthenticationCmd extends BaseCmd {
public static final String APINAME = "setupUserTwoFactorAuthentication";
public static final Logger s_logger = Logger.getLogger(SetupUserTwoFactorAuthenticationCmd.class.getName());
@Inject
private AccountManager accountManager;
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.PROVIDER, type = CommandType.STRING, description = "two factor authentication code")
private String provider;
@Parameter(name = ApiConstants.ENABLE, type = CommandType.BOOLEAN, description = "Enabled by default, provide false to disable 2FA")
private Boolean enable = true;
@Parameter(name = ApiConstants.USER_ID, type = CommandType.UUID, entityType = UserResponse.class, description = "optional: the id of the user for which 2FA has to be disabled")
private Long userId;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public String getProvider() {
return provider;
}
public Boolean getEnable() {
return enable;
}
public Long getUserId() {
return userId;
}
@Override
public void execute() throws ServerApiException {
UserTwoFactorAuthenticationSetupResponse response = accountManager.setupUserTwoFactorAuthentication(this);
response.setObjectName("setup2fa");
response.setResponseName(getCommandName());
setResponseObject(response);
}
@Override
public String getCommandName() {
return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX;
}
@Override
public long getEntityOwnerId() {
return CallContext.current().getCallingAccount().getId();
}
@Override
public ApiCommandResourceType getApiResourceType() {
return ApiCommandResourceType.User;
}
}

View File

@ -0,0 +1,145 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package com.cloud.api.auth;
import com.cloud.api.ApiServlet;
import com.cloud.api.response.ApiResponseSerializer;
import com.cloud.exception.CloudTwoFactorAuthenticationException;
import com.cloud.user.AccountManager;
import com.cloud.user.UserAccount;
import com.cloud.user.UserAccountVO;
import com.cloud.utils.exception.CSExceptionErrorCode;
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.ApiServerService;
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.SuccessResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.resourcedetail.UserDetailVO;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.net.InetAddress;
import java.util.List;
import java.util.Map;
@APICommand(name = ValidateUserTwoFactorAuthenticationCodeCmd.APINAME, description = "Checks the 2FA code for the user.", requestHasSensitiveInfo = false,
authorized = {RoleType.Admin, RoleType.DomainAdmin, RoleType.ResourceAdmin, RoleType.User},
responseObject = SuccessResponse.class, entityType = {}, since = "4.18.0")
public class ValidateUserTwoFactorAuthenticationCodeCmd extends BaseCmd implements APIAuthenticator {
public static final String APINAME = "validateUserTwoFactorAuthenticationCode";
public static final Logger s_logger = Logger.getLogger(ValidateUserTwoFactorAuthenticationCodeCmd.class.getName());
@Inject
private AccountManager accountManager;
@Inject
ApiServerService _apiServer;
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.CODE_FOR_2FA, type = CommandType.STRING, description = "two factor authentication code", required = true)
private String codeFor2fa;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public String getCodeFor2fa() {
return codeFor2fa;
}
@Override
public void execute() throws ServerApiException {
throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "This is an authentication api, cannot be used directly");
}
@Override
public String getCommandName() {
return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX;
}
@Override
public long getEntityOwnerId() {
return CallContext.current().getCallingAccount().getId();
}
@Override
public String authenticate(String command, Map<String, Object[]> params, HttpSession session, InetAddress remoteAddress, String responseType, StringBuilder auditTrailSb, HttpServletRequest req, HttpServletResponse resp) throws ServerApiException {
String codeFor2FA = null;
if (params.containsKey(ApiConstants.CODE_FOR_2FA)) {
codeFor2FA = ((String[])params.get(ApiConstants.CODE_FOR_2FA))[0];
}
if (StringUtils.isEmpty(codeFor2FA)) {
throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, "Code for two factor authentication is required");
}
final long currentUserId = (Long) session.getAttribute("userid");
final UserAccount currentUserAccount = _accountService.getUserAccountById(currentUserId);
boolean setupPhase = false;
Map<String, String> userDetails = currentUserAccount.getDetails();
if (userDetails.containsKey(UserDetailVO.Setup2FADetail) && userDetails.get(UserDetailVO.Setup2FADetail).equals(UserAccountVO.Setup2FAstatus.ENABLED.name())) {
setupPhase = true;
}
String serializedResponse = null;
try {
accountManager.verifyUsingTwoFactorAuthenticationCode(codeFor2FA, currentUserAccount.getDomainId(), currentUserId);
SuccessResponse response = new SuccessResponse(getCommandName());
setResponseObject(response);
return ApiResponseSerializer.toSerializedString(response, responseType);
} catch (final CloudTwoFactorAuthenticationException ex) {
if (!setupPhase) {
ApiServlet.invalidateHttpSession(session, "fall through to API key,");
}
String msg = String.format("%s", ex.getMessage() != null ?
ex.getMessage() :
"failed to authenticate user, check if two factor authentication code is correct");
auditTrailSb.append(" " + ApiErrorCode.UNAUTHORIZED2FA + " " + msg);
serializedResponse = _apiServer.getSerializedApiError(ApiErrorCode.UNAUTHORIZED2FA.getHttpCode(), msg, params, responseType);
if (s_logger.isTraceEnabled()) {
s_logger.trace(msg);
}
}
ServerApiException exception = new ServerApiException(ApiErrorCode.UNAUTHORIZED2FA, serializedResponse);
exception.setCSErrorCode(CSExceptionErrorCode.getCSErrCode(CloudTwoFactorAuthenticationException.class.getName()));
throw exception;
}
@Override
public APIAuthenticationType getAPIType() {
return APIAuthenticationType.LOGIN_2FA_API;
}
@Override
public void setAuthenticators(List<PluggableAPIAuthenticator> authenticators) {
}
}

View File

@ -19,6 +19,7 @@ package com.cloud.api.query.dao;
import java.util.List;
import com.cloud.user.AccountManagerImpl;
import org.apache.log4j.Logger;
import org.springframework.stereotype.Component;
@ -72,6 +73,10 @@ public class UserAccountJoinDaoImpl extends GenericDaoBase<UserAccountJoinVO, Lo
userResponse.setApiKey(usr.getApiKey());
userResponse.setSecretKey(usr.getSecretKey());
userResponse.setIsDefault(usr.isDefault());
userResponse.set2FAenabled(usr.isUser2faEnabled());
long domainId = usr.getDomainId();
boolean is2FAmandated = Boolean.TRUE.equals(AccountManagerImpl.enableUserTwoFactorAuthentication.valueIn(domainId)) && Boolean.TRUE.equals(AccountManagerImpl.mandateUserTwoFactorAuthentication.valueIn(domainId));
userResponse.set2FAmandated(is2FAmandated);
// set async job
if (usr.getJobId() != null) {

View File

@ -130,6 +130,9 @@ public class UserAccountJoinVO extends BaseViewVO implements InternalIdentity, I
@Enumerated(value = EnumType.STRING)
private User.Source source;
@Column(name = "is_user_2fa_enabled")
boolean user2faEnabled;
public UserAccountJoinVO() {
}
@ -274,4 +277,8 @@ public class UserAccountJoinVO extends BaseViewVO implements InternalIdentity, I
public User.Source getSource() {
return source;
}
public boolean isUser2faEnabled() {
return user2faEnabled;
}
}

View File

@ -581,6 +581,8 @@ import org.apache.cloudstack.api.command.user.vpn.UpdateVpnConnectionCmd;
import org.apache.cloudstack.api.command.user.vpn.UpdateVpnCustomerGatewayCmd;
import org.apache.cloudstack.api.command.user.vpn.UpdateVpnGatewayCmd;
import org.apache.cloudstack.api.command.user.zone.ListZonesCmd;
import org.apache.cloudstack.auth.UserAuthenticator;
import org.apache.cloudstack.auth.UserTwoFactorAuthenticator;
import org.apache.cloudstack.config.ApiServiceConfiguration;
import org.apache.cloudstack.config.Configuration;
import org.apache.cloudstack.config.ConfigurationGroup;
@ -720,7 +722,6 @@ import com.cloud.projects.Project.ListProjectResourcesCriteria;
import com.cloud.projects.ProjectManager;
import com.cloud.resource.ResourceManager;
import com.cloud.server.ResourceTag.ResourceObjectType;
import com.cloud.server.auth.UserAuthenticator;
import com.cloud.service.ServiceOfferingVO;
import com.cloud.service.dao.ServiceOfferingDao;
import com.cloud.service.dao.ServiceOfferingDetailsDao;
@ -986,6 +987,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
private Map<String, Boolean> _availableIdsMap;
private List<UserAuthenticator> _userAuthenticators;
private List<UserTwoFactorAuthenticator> _userTwoFactorAuthenticators;
private List<UserAuthenticator> _userPasswordEncoders;
protected boolean _executeInSequence;
@ -1030,6 +1032,14 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
_userAuthenticators = authenticators;
}
public List<UserTwoFactorAuthenticator> getUserTwoFactorAuthenticators() {
return _userTwoFactorAuthenticators;
}
public void setUserTwoFactorAuthenticators(final List<UserTwoFactorAuthenticator> userTwoFactorAuthenticators) {
_userTwoFactorAuthenticators = userTwoFactorAuthenticators;
}
public List<UserAuthenticator> getUserPasswordEncoders() {
return _userPasswordEncoders;
}

View File

@ -20,11 +20,14 @@ import java.net.InetAddress;
import java.util.List;
import java.util.Map;
import com.cloud.api.auth.SetupUserTwoFactorAuthenticationCmd;
import org.apache.cloudstack.acl.ControlledEntity;
import org.apache.cloudstack.api.command.admin.account.UpdateAccountCmd;
import org.apache.cloudstack.api.command.admin.user.DeleteUserCmd;
import org.apache.cloudstack.api.command.admin.user.MoveUserCmd;
import org.apache.cloudstack.api.command.admin.user.UpdateUserCmd;
import org.apache.cloudstack.api.response.UserTwoFactorAuthenticationSetupResponse;
import org.apache.cloudstack.auth.UserTwoFactorAuthenticator;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.Configurable;
@ -188,4 +191,10 @@ public interface AccountManager extends AccountService, Configurable {
"This parameter allows the users to enable or disable of showing secret key as a part of response for various APIs. By default it is set to false.", true);
boolean moveUser(long id, Long domainId, Account newAccount);
UserTwoFactorAuthenticator getUserTwoFactorAuthenticator(final Long domainId, final Long userAccountId);
void verifyUsingTwoFactorAuthenticationCode(String code, Long domainId, Long userAccountId);
UserTwoFactorAuthenticationSetupResponse setupUserTwoFactorAuthentication(SetupUserTwoFactorAuthenticationCmd cmd);
}

View File

@ -40,9 +40,6 @@ import javax.crypto.spec.SecretKeySpec;
import javax.inject.Inject;
import javax.naming.ConfigurationException;
import com.cloud.network.security.SecurityGroupService;
import com.cloud.network.security.SecurityGroupVO;
import com.cloud.utils.component.PluggableService;
import org.apache.cloudstack.acl.APIChecker;
import org.apache.cloudstack.acl.ControlledEntity;
import org.apache.cloudstack.acl.QuerySelector;
@ -62,6 +59,10 @@ import org.apache.cloudstack.api.command.admin.user.GetUserKeysCmd;
import org.apache.cloudstack.api.command.admin.user.MoveUserCmd;
import org.apache.cloudstack.api.command.admin.user.RegisterCmd;
import org.apache.cloudstack.api.command.admin.user.UpdateUserCmd;
import org.apache.cloudstack.api.response.UserTwoFactorAuthenticationSetupResponse;
import org.apache.cloudstack.auth.UserAuthenticator;
import org.apache.cloudstack.auth.UserAuthenticator.ActionOnFailedAuthentication;
import org.apache.cloudstack.auth.UserTwoFactorAuthenticator;
import org.apache.cloudstack.config.ApiServiceConfiguration;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
@ -71,6 +72,8 @@ import org.apache.cloudstack.framework.messagebus.MessageBus;
import org.apache.cloudstack.framework.messagebus.PublishScope;
import org.apache.cloudstack.managed.context.ManagedContextRunnable;
import org.apache.cloudstack.region.gslb.GlobalLoadBalancerRuleDao;
import org.apache.cloudstack.resourcedetail.UserDetailVO;
import org.apache.cloudstack.resourcedetail.dao.UserDetailsDao;
import org.apache.cloudstack.utils.baremetal.BaremetalUtils;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.collections.CollectionUtils;
@ -80,6 +83,7 @@ import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import com.cloud.api.ApiDBUtils;
import com.cloud.api.auth.SetupUserTwoFactorAuthenticationCmd;
import com.cloud.api.query.vo.ControlledViewEntity;
import com.cloud.configuration.Config;
import com.cloud.configuration.ConfigurationManager;
@ -103,6 +107,7 @@ import com.cloud.event.ActionEvents;
import com.cloud.event.EventTypes;
import com.cloud.exception.AgentUnavailableException;
import com.cloud.exception.CloudAuthenticationException;
import com.cloud.exception.CloudTwoFactorAuthenticationException;
import com.cloud.exception.ConcurrentOperationException;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.OperationTimedoutException;
@ -112,6 +117,8 @@ import com.cloud.network.IpAddress;
import com.cloud.network.IpAddressManager;
import com.cloud.network.Network;
import com.cloud.network.NetworkModel;
import com.cloud.network.security.SecurityGroupService;
import com.cloud.network.security.SecurityGroupVO;
import com.cloud.network.VpnUserVO;
import com.cloud.network.as.AutoScaleManager;
import com.cloud.network.dao.AccountGuestVlanMapDao;
@ -142,8 +149,6 @@ import com.cloud.projects.ProjectVO;
import com.cloud.projects.dao.ProjectAccountDao;
import com.cloud.projects.dao.ProjectDao;
import com.cloud.region.ha.GlobalLoadBalancingRulesService;
import com.cloud.server.auth.UserAuthenticator;
import com.cloud.server.auth.UserAuthenticator.ActionOnFailedAuthentication;
import com.cloud.storage.VMTemplateVO;
import com.cloud.storage.VolumeApiService;
import com.cloud.storage.VolumeVO;
@ -163,6 +168,7 @@ import com.cloud.utils.Pair;
import com.cloud.utils.Ternary;
import com.cloud.utils.component.Manager;
import com.cloud.utils.component.ManagerBase;
import com.cloud.utils.component.PluggableService;
import com.cloud.utils.concurrency.NamedThreadFactory;
import com.cloud.utils.db.DB;
import com.cloud.utils.db.GlobalLock;
@ -203,6 +209,8 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
@Inject
private UserDao _userDao;
@Inject
private UserDetailsDao _userDetailsDao;
@Inject
private InstanceGroupDao _vmGroupDao;
@Inject
private UserAccountDao _userAccountDao;
@ -293,7 +301,11 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
@Inject
private GlobalLoadBalancingRulesService _gslbService;
@Inject
public AccountService _accountService;
private List<UserAuthenticator> _userAuthenticators;
private List<UserTwoFactorAuthenticator> _userTwoFactorAuthenticators;
protected List<UserAuthenticator> _userPasswordEncoders;
protected List<PluggableService> services;
private List<APIChecker> apiAccessCheckers;
@ -317,6 +329,39 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
private int _cleanupInterval;
private List<String> apiNameList;
protected static Map<String, UserTwoFactorAuthenticator> userTwoFactorAuthenticationProvidersMap = new HashMap<>();
private List<UserTwoFactorAuthenticator> userTwoFactorAuthenticationProviders;
public static ConfigKey<Boolean> enableUserTwoFactorAuthentication = new ConfigKey<>("Advanced",
Boolean.class,
"enable.user.2fa",
"false",
"Determines whether two factor authentication is enabled or not. This can also be configured at domain level.",
true,
ConfigKey.Scope.Domain);
public static ConfigKey<Boolean> mandateUserTwoFactorAuthentication = new ConfigKey<>("Advanced",
Boolean.class,
"mandate.user.2fa",
"false",
"Determines whether to make the two factor authentication mandatory or not. This setting is applicable only when enable.user.2fa is true. This can also be configured at domain level.",
true,
ConfigKey.Scope.Domain);
public static final ConfigKey<String> userTwoFactorAuthenticationIssuer = new ConfigKey<>("Advanced",
String.class,
"user.2fa.issuer",
"CloudStack",
"Name of the issuer of two factor authentication",
true,
ConfigKey.Scope.Domain);
static final ConfigKey<String> userTwoFactorAuthenticationDefaultProvider = new ConfigKey<>("Advanced", String.class,
"user.2fa.default.provider",
"totp",
"The default user two factor authentication provider plugin. Eg. totp, staticpin", true, ConfigKey.Scope.Domain);
protected AccountManagerImpl() {
super();
}
@ -329,6 +374,14 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
_userAuthenticators = authenticators;
}
public List<UserTwoFactorAuthenticator> getUserTwoFactorAuthenticators() {
return _userTwoFactorAuthenticators;
}
public void setUserTwoFactorAuthenticators(List<UserTwoFactorAuthenticator> twoFactorAuthenticators) {
_userTwoFactorAuthenticators = twoFactorAuthenticators;
}
public List<UserAuthenticator> getUserPasswordEncoders() {
return _userPasswordEncoders;
}
@ -402,6 +455,9 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
@Override
public boolean start() {
initializeUserTwoFactorAuthenticationProvidersMap();
if (apiNameList == null) {
long startTime = System.nanoTime();
apiNameList = new ArrayList<String>();
@ -1362,6 +1418,10 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
if (StringUtils.isNotBlank(timezone)) {
user.setTimezone(timezone);
}
Boolean mandate2FA = updateUserCmd.getMandate2FA();
if (mandate2FA != null && mandate2FA) {
user.setUser2faEnabled(true);
}
_userDao.update(user.getId(), user);
return _userAccountDao.findById(user.getId());
}
@ -2378,6 +2438,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
if (userUUID == null) {
userUUID = UUID.randomUUID().toString();
}
UserVO user = _userDao.persist(new UserVO(accountId, userName, encodedPassword, firstName, lastName, email, timezone, userUUID, source));
CallContext.current().putContextParameter(User.class, user.getUuid());
return user;
@ -2650,6 +2711,27 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
return keys;
}
@Override
public List<UserTwoFactorAuthenticator> listUserTwoFactorAuthenticationProviders() {
return userTwoFactorAuthenticationProviders;
}
@Override
public UserTwoFactorAuthenticator getUserTwoFactorAuthenticationProvider(Long domainId) {
final String name = userTwoFactorAuthenticationDefaultProvider.valueIn(domainId);
return getUserTwoFactorAuthenticationProvider(name);
}
public UserTwoFactorAuthenticator getUserTwoFactorAuthenticationProvider(final String name) {
if (StringUtils.isEmpty(name)) {
throw new CloudRuntimeException("Two factor authentication provider name is empty");
}
if (!userTwoFactorAuthenticationProvidersMap.containsKey(name.toLowerCase())) {
throw new CloudRuntimeException(String.format("Failed to find two factor authentication provider by the name: %s.", name));
}
return userTwoFactorAuthenticationProvidersMap.get(name.toLowerCase());
}
@Override
@DB
@ActionEvent(eventType = EventTypes.EVENT_REGISTER_FOR_SECRET_API_KEY, eventDescription = "register for the developer API keys")
@ -3031,7 +3113,11 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
@Override
public UserAccount getUserAccountById(Long userId) {
return _userAccountDao.findById(userId);
UserAccount userAccount = _userAccountDao.findById(userId);
Map<String, String> details = _userDetailsDao.listDetailsKeyPairs(userId);
userAccount.setDetails(details);
return userAccount;
}
@Override
@ -3114,6 +3200,171 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
@Override
public ConfigKey<?>[] getConfigKeys() {
return new ConfigKey<?>[] {UseSecretKeyInResponse};
return new ConfigKey<?>[] {UseSecretKeyInResponse, enableUserTwoFactorAuthentication,
userTwoFactorAuthenticationDefaultProvider, mandateUserTwoFactorAuthentication, userTwoFactorAuthenticationIssuer};
}
public List<UserTwoFactorAuthenticator> getUserTwoFactorAuthenticationProviders() {
return userTwoFactorAuthenticationProviders;
}
public void setUserTwoFactorAuthenticationProviders(final List<UserTwoFactorAuthenticator> userTwoFactorAuthenticationProviders) {
this.userTwoFactorAuthenticationProviders = userTwoFactorAuthenticationProviders;
}
protected void initializeUserTwoFactorAuthenticationProvidersMap() {
if (userTwoFactorAuthenticationProviders != null) {
for (final UserTwoFactorAuthenticator userTwoFactorAuthenticator : userTwoFactorAuthenticationProviders) {
userTwoFactorAuthenticationProvidersMap.put(userTwoFactorAuthenticator.getName().toLowerCase(), userTwoFactorAuthenticator);
}
}
}
@Override
public void verifyUsingTwoFactorAuthenticationCode(final String code, final Long domainId, final Long userAccountId) {
Account caller = CallContext.current().getCallingAccount();
Account owner = _accountService.getActiveAccountById(caller.getId());
checkAccess(caller, null, true, owner);
UserAccount userAccount = _accountService.getUserAccountById(userAccountId);
if (!userAccount.isUser2faEnabled()) {
throw new CloudRuntimeException(String.format("Two factor authentication is not enabled on the user: %s", userAccount.getUsername()));
}
if (StringUtils.isBlank(userAccount.getUser2faProvider()) || StringUtils.isBlank(userAccount.getKeyFor2fa())) {
throw new CloudRuntimeException(String.format("Two factor authentication is not setup for the user: %s, please setup 2FA before verifying", userAccount.getUsername()));
}
UserTwoFactorAuthenticator userTwoFactorAuthenticator = getUserTwoFactorAuthenticator(domainId, userAccountId);
try {
userTwoFactorAuthenticator.check2FA(code, userAccount);
UserDetailVO userDetailVO = _userDetailsDao.findDetail(userAccountId, UserDetailVO.Setup2FADetail);
if (userDetailVO != null) {
userDetailVO.setValue(UserAccountVO.Setup2FAstatus.VERIFIED.name());
_userDetailsDao.update(userDetailVO.getId(), userDetailVO);
}
} catch (CloudTwoFactorAuthenticationException e) {
UserDetailVO userDetailVO = _userDetailsDao.findDetail(userAccountId, "2FAsetupComplete");
if (userDetailVO != null && userDetailVO.getValue().equals(UserAccountVO.Setup2FAstatus.ENABLED.name())) {
disableTwoFactorAuthentication(userAccountId, caller, owner);
}
throw e;
}
}
@Override
public UserTwoFactorAuthenticator getUserTwoFactorAuthenticator(Long domainId, Long userAccountId) {
if (userAccountId != null) {
UserAccount userAccount = _accountService.getUserAccountById(userAccountId);
String user2FAProvider = userAccount.getUser2faProvider();
if (user2FAProvider != null) {
return getUserTwoFactorAuthenticator(user2FAProvider);
}
}
final String name = userTwoFactorAuthenticationDefaultProvider.valueIn(domainId);
return getUserTwoFactorAuthenticator(name);
}
@Override
public UserTwoFactorAuthenticationSetupResponse setupUserTwoFactorAuthentication(SetupUserTwoFactorAuthenticationCmd cmd) {
String providerName = cmd.getProvider();
Account caller = CallContext.current().getCallingAccount();
Account owner = _accountService.getActiveAccountById(caller.getId());
if (Boolean.TRUE.equals(cmd.getEnable())) {
checkAccess(caller, null, true, owner);
Long userId = CallContext.current().getCallingUserId();
return enableTwoFactorAuthentication(userId, providerName);
}
// Admin can disable 2FA of the users
Long userId = cmd.getUserId();
return disableTwoFactorAuthentication(userId, caller, owner);
}
protected UserTwoFactorAuthenticationSetupResponse enableTwoFactorAuthentication(Long userId, String providerName) {
UserAccountVO userAccount = _userAccountDao.findById(userId);
UserVO userVO = _userDao.findById(userId);
Long domainId = userAccount.getDomainId();
if (Boolean.FALSE.equals(enableUserTwoFactorAuthentication.valueIn(domainId)) && Boolean.FALSE.equals(mandateUserTwoFactorAuthentication.valueIn(domainId))) {
throw new CloudRuntimeException("2FA is not enabled for this domain or at global level");
}
if (StringUtils.isEmpty(providerName)) {
providerName = userTwoFactorAuthenticationDefaultProvider.valueIn(domainId);
s_logger.debug(String.format("Provider name is not given to setup 2FA, so using the default 2FA provider %s", providerName));
}
UserTwoFactorAuthenticator provider = getUserTwoFactorAuthenticationProvider(providerName);
String code = provider.setup2FAKey(userAccount);
UserVO user = _userDao.createForUpdate();
user.setKeyFor2fa(code);
user.setUser2faProvider(provider.getName());
user.setUser2faEnabled(true);
_userDao.update(userId, user);
// 2FA setup will be complete only upon successful verification with 2FA code
UserDetailVO setup2FAstatus = new UserDetailVO(userId, UserDetailVO.Setup2FADetail, UserAccountVO.Setup2FAstatus.ENABLED.name());
_userDetailsDao.persist(setup2FAstatus);
UserTwoFactorAuthenticationSetupResponse response = new UserTwoFactorAuthenticationSetupResponse();
response.setId(userVO.getUuid());
response.setUsername(userAccount.getUsername());
response.setSecretCode(code);
return response;
}
protected UserTwoFactorAuthenticationSetupResponse disableTwoFactorAuthentication(Long userId, Account caller, Account owner) {
UserVO userVO = null;
if (userId != null) {
userVO = validateUser(userId, caller.getDomainId());
owner = _accountService.getActiveAccountById(userVO.getAccountId());
} else {
userId = CallContext.current().getCallingUserId();
userVO = _userDao.findById(userId);
}
checkAccess(caller, null, true, owner);
UserVO user = _userDao.createForUpdate();
user.setKeyFor2fa(null);
user.setUser2faProvider(null);
user.setUser2faEnabled(false);
_userDao.update(userVO.getId(), user);
_userDetailsDao.removeDetail(userId, UserDetailVO.Setup2FADetail);
UserTwoFactorAuthenticationSetupResponse response = new UserTwoFactorAuthenticationSetupResponse();
response.setId(userVO.getUuid());
response.setUsername(userVO.getUsername());
return response;
}
private UserVO validateUser(Long userId, Long domainId) {
UserVO user = null;
if (userId != null) {
user = _userDao.findById(userId);
if (user == null) {
throw new InvalidParameterValueException("Invalid user ID provided");
}
if (_accountDao.findById(user.getAccountId()).getDomainId() != domainId) {
throw new InvalidParameterValueException("User doesn't belong to the specified account or domain");
}
}
return user;
}
public UserTwoFactorAuthenticator getUserTwoFactorAuthenticator(final String name) {
if (StringUtils.isEmpty(name)) {
throw new CloudRuntimeException("UserTwoFactorAuthenticator name provided is empty");
}
if (!userTwoFactorAuthenticationProvidersMap.containsKey(name.toLowerCase())) {
throw new CloudRuntimeException(String.format("Failed to find UserTwoFactorAuthenticator by the name: %s.", name));
}
return userTwoFactorAuthenticationProvidersMap.get(name.toLowerCase());
}
}

View File

@ -14,7 +14,7 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package com.cloud.server.auth;
package org.apache.cloudstack.auth;
import java.util.Map;

View File

@ -44,12 +44,15 @@
<bean id="accountManagerImpl" class="com.cloud.user.AccountManagerImpl">
<property name="userAuthenticators"
value="#{userAuthenticatorsRegistry.registered}" />
<property name="userTwoFactorAuthenticators"
value="#{userTwoFactorAuthenticatorsRegistry.registered}" />
<property name="userPasswordEncoders"
value="#{userPasswordEncodersRegistry.registered}" />
<property name="securityCheckers" value="#{securityCheckersRegistry.registered}" />
<property name="querySelectors" value="#{querySelectorsRegistry.registered}" />
<property name="apiAccessCheckers" value="#{apiAclCheckersRegistry.registered}" />
<property name="services" value="#{apiCommandsRegistry.registered}" />
<property name="userTwoFactorAuthenticationProviders" value="#{userTwoFactorAuthenticatorsRegistry.registered}" />
</bean>
<bean id="passwordPolicies" class="com.cloud.user.PasswordPolicyImpl" />
@ -58,6 +61,8 @@
<property name="lockControllerListener" ref="lockControllerListener" />
<property name="userAuthenticators"
value="#{userAuthenticatorsRegistry.registered}" />
<property name="userTwoFactorAuthenticators"
value="#{userTwoFactorAuthenticatorsRegistry.registered}" />
<property name="userPasswordEncoders"
value="#{userPasswordEncodersRegistry.registered}" />
<property name="hostAllocators" value="#{hostAllocatorsRegistry.registered}" />

View File

@ -125,8 +125,8 @@
</adapters>
<adapters key="com.cloud.server.auth.UserAuthenticator">
<adapter name="MD5" class="com.cloud.server.auth.MD5UserAuthenticator" />
<adapters key="org.apache.cloudstack.auth.UserAuthenticator">
<adapter name="MD5" class="org.apache.cloudstack.auth.MD5UserAuthenticator" />
</adapters>
<adapters key="com.cloud.ha.Investigator">
<adapter name="SimpleInvestigator" class="com.cloud.ha.CheckOnAgentInvestigator" />

View File

@ -37,6 +37,8 @@ import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.auth.APIAuthenticationManager;
import org.apache.cloudstack.api.auth.APIAuthenticationType;
import org.apache.cloudstack.api.auth.APIAuthenticator;
import org.apache.cloudstack.api.command.admin.config.ListCfgsByCmd;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
@ -45,10 +47,17 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import com.cloud.api.auth.ListUserTwoFactorAuthenticatorProvidersCmd;
import com.cloud.api.auth.SetupUserTwoFactorAuthenticationCmd;
import com.cloud.api.auth.ValidateUserTwoFactorAuthenticationCodeCmd;
import com.cloud.server.ManagementServer;
import com.cloud.user.Account;
import com.cloud.user.AccountManagerImpl;
import com.cloud.user.AccountService;
import com.cloud.user.User;
import com.cloud.user.UserAccount;
import com.cloud.utils.HttpUtils;
import com.cloud.vm.UserVmManager;
import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
@ -84,6 +93,12 @@ public class ApiServletTest {
@Mock
ManagementServer managementServer;
@Mock
UserAccount userAccount;
@Mock
AccountService accountMgr;
StringWriter responseWriter;
ApiServlet servlet;
@ -115,6 +130,7 @@ public class ApiServletTest {
Field apiServerField = ApiServlet.class.getDeclaredField("apiServer");
apiServerField.setAccessible(true);
apiServerField.set(servlet, apiServer);
}
/**
@ -273,4 +289,136 @@ public class ApiServletTest {
Assert.assertEquals(InetAddress.getByName("127.0.0.1"), ApiServlet.getClientAddress(request));
}
@Test
public void testSkip2FAcheckForAPIs() {
String command = "listZones";
Map<String, Object[]> params = new HashMap<String, Object[]>();
boolean result = servlet.skip2FAcheckForAPIs(command);
Assert.assertEquals(false, result);
command = ListCfgsByCmd.APINAME;
params.put(ApiConstants.NAME, new String[] { UserVmManager.AllowUserExpungeRecoverVm.key() });
result = servlet.skip2FAcheckForAPIs(command);
Assert.assertEquals(false, result);
}
@Test
public void testDoNotSkip2FAcheckForAPIs() {
String[] commands = new String[] {ApiConstants.LIST_IDPS, ApiConstants.LIST_APIS,
ListUserTwoFactorAuthenticatorProvidersCmd.APINAME, SetupUserTwoFactorAuthenticationCmd.APINAME};
Map<String, Object[]> params = new HashMap<String, Object[]>();
for (String cmd: commands) {
boolean result = servlet.skip2FAcheckForAPIs(cmd);
Assert.assertEquals(true, result);
}
}
@Test
public void testSkip2FAcheckForUserWhenAlreadyVerified() {
Mockito.when(session.getAttribute("userid")).thenReturn(1L);
Mockito.when(session.getAttribute(ApiConstants.IS_2FA_VERIFIED)).thenReturn(true);
boolean result = servlet.skip2FAcheckForUser(session);
Assert.assertEquals(true, result);
}
@Test
public void testDoNotSkip2FAcheckForUserWhen2FAEnabled() {
servlet.accountMgr = accountMgr;
HttpSession cuurentSession = Mockito.mock(HttpSession.class);
Mockito.when(cuurentSession.getAttribute("userid")).thenReturn(1L);
Mockito.when(cuurentSession.getAttribute(ApiConstants.IS_2FA_VERIFIED)).thenReturn(false);
Mockito.when(accountMgr.getUserAccountById(1L)).thenReturn(userAccount);
Mockito.when(userAccount.isUser2faEnabled()).thenReturn(true);
boolean result = servlet.skip2FAcheckForUser(cuurentSession);
Assert.assertEquals(false, result);
}
@Test
public void testDoNotSkip2FAcheckForUserWhen2FAMandated() {
servlet.accountMgr = accountMgr;
HttpSession cuurentSession = Mockito.mock(HttpSession.class);
Mockito.when(cuurentSession.getAttribute("userid")).thenReturn(1L);
Mockito.when(cuurentSession.getAttribute(ApiConstants.IS_2FA_VERIFIED)).thenReturn(false);
Mockito.when(accountMgr.getUserAccountById(1L)).thenReturn(userAccount);
Mockito.when(userAccount.getDomainId()).thenReturn(1L);
Mockito.when(userAccount.isUser2faEnabled()).thenReturn(false);
ConfigKey<Boolean> mandateUserTwoFactorAuthentication = Mockito.mock(ConfigKey.class);
AccountManagerImpl.mandateUserTwoFactorAuthentication = mandateUserTwoFactorAuthentication;
Mockito.when(mandateUserTwoFactorAuthentication.valueIn(1L)).thenReturn(false);
boolean result = servlet.skip2FAcheckForUser(cuurentSession);
Assert.assertEquals(true, result);
}
@Test
public void testSkip2FAcheckForUserWhen2FAisNotEnabledAndNotMandated() {
servlet.accountMgr = accountMgr;
HttpSession cuurentSession = Mockito.mock(HttpSession.class);
Mockito.when(cuurentSession.getAttribute("userid")).thenReturn(1L);
Mockito.when(cuurentSession.getAttribute(ApiConstants.IS_2FA_VERIFIED)).thenReturn(false);
Mockito.when(accountMgr.getUserAccountById(1L)).thenReturn(userAccount);
Mockito.when(userAccount.getDomainId()).thenReturn(1L);
Mockito.when(userAccount.isUser2faEnabled()).thenReturn(false);
ConfigKey<Boolean> enableUserTwoFactorAuthentication = Mockito.mock(ConfigKey.class);
AccountManagerImpl.enableUserTwoFactorAuthentication = enableUserTwoFactorAuthentication;
Mockito.when(enableUserTwoFactorAuthentication.valueIn(1L)).thenReturn(true);
ConfigKey<Boolean> mandateUserTwoFactorAuthentication = Mockito.mock(ConfigKey.class);
AccountManagerImpl.mandateUserTwoFactorAuthentication = mandateUserTwoFactorAuthentication;
Mockito.when(mandateUserTwoFactorAuthentication.valueIn(1L)).thenReturn(true);
boolean result = servlet.skip2FAcheckForUser(cuurentSession);
Assert.assertEquals(false, result);
}
@Test
public void testVerify2FA() throws UnknownHostException {
String command = ValidateUserTwoFactorAuthenticationCodeCmd.APINAME;
Mockito.lenient().when(authenticator.authenticate(Mockito.anyString(), Mockito.anyMap(), Mockito.isA(HttpSession.class),
Mockito.same(InetAddress.getByName("127.0.0.1")), Mockito.anyString(), Mockito.isA(StringBuilder.class), Mockito.isA(HttpServletRequest.class), Mockito.isA(HttpServletResponse.class))).thenReturn("{\"Success\":{}");
StringBuilder auditTrailSb = new StringBuilder();
Map<String, Object[]> params = new HashMap<String, Object[]>();
String responseType = HttpUtils.RESPONSE_TYPE_XML;
boolean result = servlet.verify2FA(session, command, auditTrailSb, params, InetAddress.getByName("192.168.1.1"),
responseType, request, response);
Assert.assertEquals(true, result);
}
@Test
public void testVerify2FAWhenAuthenticatorNotFound() throws UnknownHostException {
String command = ValidateUserTwoFactorAuthenticationCodeCmd.APINAME;
Mockito.when(authManager.getAPIAuthenticator(command)).thenReturn(null);
StringBuilder auditTrailSb = new StringBuilder();
Map<String, Object[]> params = new HashMap<String, Object[]>();
String responseType = HttpUtils.RESPONSE_TYPE_XML;
boolean result = servlet.verify2FA(session, command, auditTrailSb, params, InetAddress.getByName("192.168.1.1"),
responseType, request, response);
Assert.assertEquals(false, result);
}
@Test
public void testVerify2FAWhenExpectedCommandIsNotCalled() throws UnknownHostException {
servlet.accountMgr = accountMgr;
String command = "listZones";
Mockito.when(session.getAttribute("userid")).thenReturn(1L);
Mockito.when(accountMgr.getUserAccountById(1L)).thenReturn(userAccount);
Mockito.when(userAccount.isUser2faEnabled()).thenReturn(true);
StringBuilder auditTrailSb = new StringBuilder();
Map<String, Object[]> params = new HashMap<String, Object[]>();
String responseType = HttpUtils.RESPONSE_TYPE_XML;
boolean result = servlet.verify2FA(session, command, auditTrailSb, params, InetAddress.getByName("192.168.1.1"),
responseType, request, response);
Assert.assertEquals(false, result);
}
}

View File

@ -21,11 +21,18 @@ import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import org.apache.cloudstack.acl.SecurityChecker.AccessType;
import org.apache.cloudstack.api.command.admin.user.GetUserKeysCmd;
import org.apache.cloudstack.api.command.admin.user.UpdateUserCmd;
import org.apache.cloudstack.api.response.UserTwoFactorAuthenticationSetupResponse;
import org.apache.cloudstack.auth.UserAuthenticator;
import org.apache.cloudstack.auth.UserAuthenticator.ActionOnFailedAuthentication;
import org.apache.cloudstack.auth.UserTwoFactorAuthenticator;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
@ -34,8 +41,10 @@ import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import static org.mockito.ArgumentMatchers.nullable;
import com.cloud.acl.DomainChecker;
import com.cloud.api.auth.SetupUserTwoFactorAuthenticationCmd;
import com.cloud.domain.Domain;
import com.cloud.domain.DomainVO;
import com.cloud.exception.ConcurrentOperationException;
@ -44,8 +53,6 @@ import com.cloud.exception.PermissionDeniedException;
import com.cloud.exception.ResourceUnavailableException;
import com.cloud.projects.Project;
import com.cloud.projects.ProjectAccountVO;
import com.cloud.server.auth.UserAuthenticator;
import com.cloud.server.auth.UserAuthenticator.ActionOnFailedAuthentication;
import com.cloud.user.Account.State;
import com.cloud.utils.Pair;
import com.cloud.utils.exception.CloudRuntimeException;
@ -92,6 +99,14 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase {
@Mock
PasswordPolicyImpl passwordPolicyMock;
@Mock
ConfigKey<Boolean> enableUserTwoFactorAuthenticationMock;
@Before
public void setUp() throws Exception {
enableUserTwoFactorAuthenticationMock = Mockito.mock(ConfigKey.class);
accountManagerImpl.enableUserTwoFactorAuthentication = enableUserTwoFactorAuthenticationMock;
}
@Before
public void beforeTest() {
@ -211,7 +226,7 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase {
CallContext.register(callingUser, callingAccount); // Calling account is user account i.e normal account
Mockito.when(_listkeyscmd.getID()).thenReturn(1L);
Mockito.when(accountManagerImpl.getActiveUser(1L)).thenReturn(userVoMock);
Mockito.when(accountManagerImpl.getUserAccountById(1L)).thenReturn(userAccountVO);
Mockito.when(userAccountDaoMock.findById(1L)).thenReturn(userAccountVO);
Mockito.when(userAccountVO.getAccountId()).thenReturn(1L);
Mockito.lenient().when(accountManagerImpl.getAccount(Mockito.anyLong())).thenReturn(accountMock); // Queried account - admin account
@ -778,4 +793,185 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase {
accountManagerImpl.updateLoginAttemptsWhenIncorrectLoginAttemptsEnabled(userAccountVO, true, allowedAttempts);
Mockito.verify(accountManagerImpl).updateLoginAttempts(Mockito.eq(accountId), Mockito.eq(allowedAttempts), Mockito.eq(true));
}
@Test(expected = CloudRuntimeException.class)
public void testEnableUserTwoFactorAuthenticationWhenDomainlevelSettingisDisabled() {
Long userId = 1L;
UserAccountVO userAccount = Mockito.mock(UserAccountVO.class);
UserVO userVO = Mockito.mock(UserVO.class);
Mockito.when(userAccountDaoMock.findById(userId)).thenReturn(userAccount);
Mockito.when(userDaoMock.findById(userId)).thenReturn(userVO);
Mockito.when(userAccount.getDomainId()).thenReturn(1L);
ConfigKey<Boolean> enableUserTwoFactorAuthentication = Mockito.mock(ConfigKey.class);
AccountManagerImpl.enableUserTwoFactorAuthentication = enableUserTwoFactorAuthentication;
Mockito.when(enableUserTwoFactorAuthentication.valueIn(1L)).thenReturn(false);
accountManagerImpl.enableTwoFactorAuthentication(userId, "totp");
}
@Test
public void testEnableUserTwoFactorAuthenticationWhenProviderNameIsNullExpectedDefaultProviderTOTP() {
Long userId = 1L;
UserAccountVO userAccount = Mockito.mock(UserAccountVO.class);
UserVO userVO = Mockito.mock(UserVO.class);
Mockito.when(userAccountDaoMock.findById(userId)).thenReturn(userAccount);
Mockito.when(userDaoMock.findById(userId)).thenReturn(userVO);
Mockito.when(userAccount.getDomainId()).thenReturn(1L);
ConfigKey<Boolean> enableUserTwoFactorAuthentication = Mockito.mock(ConfigKey.class);
AccountManagerImpl.enableUserTwoFactorAuthentication = enableUserTwoFactorAuthentication;
Mockito.when(enableUserTwoFactorAuthentication.valueIn(1L)).thenReturn(true);
UserTwoFactorAuthenticator totpProvider = Mockito.mock(UserTwoFactorAuthenticator.class);
Map<String, UserTwoFactorAuthenticator> userTwoFactorAuthenticationProvidersMap = Mockito.mock(HashMap.class);
Mockito.when(userTwoFactorAuthenticationProvidersMap.containsKey("totp")).thenReturn( true);
Mockito.when(userTwoFactorAuthenticationProvidersMap.get("totp")).thenReturn(totpProvider);
AccountManagerImpl.userTwoFactorAuthenticationProvidersMap = userTwoFactorAuthenticationProvidersMap;
Mockito.when(totpProvider.setup2FAKey(userAccount)).thenReturn("EUJEAEDVOURFZTE6OGWVTJZMI54QGMIL");
Mockito.when(userDaoMock.createForUpdate()).thenReturn(userVoMock);
Mockito.when(userDaoMock.update(userId, userVoMock)).thenReturn(true);
UserTwoFactorAuthenticationSetupResponse response = accountManagerImpl.enableTwoFactorAuthentication(userId, null);
Assert.assertEquals("EUJEAEDVOURFZTE6OGWVTJZMI54QGMIL", response.getSecretCode());
}
@Test
public void testEnableUserTwoFactorAuthentication() {
Long userId = 1L;
UserAccountVO userAccount = Mockito.mock(UserAccountVO.class);
UserVO userVO = Mockito.mock(UserVO.class);
Mockito.when(userAccountDaoMock.findById(userId)).thenReturn(userAccount);
Mockito.when(userDaoMock.findById(userId)).thenReturn(userVO);
Mockito.when(userAccount.getDomainId()).thenReturn(1L);
ConfigKey<Boolean> enableUserTwoFactorAuthentication = Mockito.mock(ConfigKey.class);
AccountManagerImpl.enableUserTwoFactorAuthentication = enableUserTwoFactorAuthentication;
Mockito.when(enableUserTwoFactorAuthentication.valueIn(1L)).thenReturn(true);
UserTwoFactorAuthenticator totpProvider = Mockito.mock(UserTwoFactorAuthenticator.class);
Map<String, UserTwoFactorAuthenticator> userTwoFactorAuthenticationProvidersMap = Mockito.mock(HashMap.class);
Mockito.when(userTwoFactorAuthenticationProvidersMap.containsKey("totp")).thenReturn( true);
Mockito.when(userTwoFactorAuthenticationProvidersMap.get("totp")).thenReturn(totpProvider);
AccountManagerImpl.userTwoFactorAuthenticationProvidersMap = userTwoFactorAuthenticationProvidersMap;
Mockito.when(totpProvider.setup2FAKey(userAccount)).thenReturn("EUJEAEDVOURFZTE6OGWVTJZMI54QGMIL");
Mockito.when(userDaoMock.createForUpdate()).thenReturn(userVoMock);
Mockito.when(userDaoMock.update(userId, userVoMock)).thenReturn(true);
UserTwoFactorAuthenticationSetupResponse response = accountManagerImpl.enableTwoFactorAuthentication(userId, "totp");
Assert.assertEquals("EUJEAEDVOURFZTE6OGWVTJZMI54QGMIL", response.getSecretCode());
}
@Test
public void testDisableUserTwoFactorAuthentication() {
Long userId = 1L;
UserVO userVO = Mockito.mock(UserVO.class);
Account caller = Mockito.mock(Account.class);
AccountVO accountMock = Mockito.mock(AccountVO.class);
Mockito.doNothing().when(accountManagerImpl).checkAccess(nullable(Account.class), Mockito.isNull(), nullable(Boolean.class), nullable(Account.class));
Mockito.when(caller.getDomainId()).thenReturn(1L);
Mockito.when(userDaoMock.findById(userId)).thenReturn(userVO);
Mockito.when(userVO.getAccountId()).thenReturn(1L);
Mockito.when(_accountDao.findById(1L)).thenReturn(accountMock);
Mockito.when(accountMock.getDomainId()).thenReturn(1L);
Mockito.when(_accountService.getActiveAccountById(1L)).thenReturn(caller);
userVoMock.setKeyFor2fa("EUJEAEDVOURFZTE6OGWVTJZMI54QGMIL");
userVoMock.setUser2faProvider("totp");
userVoMock.setUser2faEnabled(true);
Mockito.when(userDaoMock.createForUpdate()).thenReturn(userVoMock);
UserTwoFactorAuthenticationSetupResponse response = accountManagerImpl.disableTwoFactorAuthentication(userId, caller, caller);
Assert.assertNull(response.getSecretCode());
Assert.assertNull(userVoMock.getKeyFor2fa());
Assert.assertNull(userVoMock.getUser2faProvider());
}
@Test(expected = CloudRuntimeException.class)
public void testVerify2FAcodeWhen2FAisNotEnabled() {
AccountVO accountMock = Mockito.mock(AccountVO.class);
Account caller = CallContext.current().getCallingAccount();
Mockito.when(caller.getId()).thenReturn(1L);
Mockito.lenient().when(_accountService.getActiveAccountById(1L)).thenReturn(accountMock);
Mockito.when(_accountService.getUserAccountById(1L)).thenReturn(userAccountVO);
Mockito.when(userAccountVO.isUser2faEnabled()).thenReturn(false);
accountManagerImpl.verifyUsingTwoFactorAuthenticationCode("352352", 1L, 1L);
}
@Test(expected = CloudRuntimeException.class)
public void testVerify2FAcodeWhen2FAisNotSetup() {
AccountVO accountMock = Mockito.mock(AccountVO.class);
Account caller = CallContext.current().getCallingAccount();
Mockito.when(caller.getId()).thenReturn(1L);
Mockito.lenient().when(_accountService.getActiveAccountById(1L)).thenReturn(accountMock);
Mockito.when(_accountService.getUserAccountById(1L)).thenReturn(userAccountVO);
Mockito.when(userAccountVO.isUser2faEnabled()).thenReturn(true);
Mockito.when(userAccountVO.getUser2faProvider()).thenReturn(null);
accountManagerImpl.verifyUsingTwoFactorAuthenticationCode("352352", 1L, 1L);
}
@Test
public void testVerify2FAcode() {
AccountVO accountMock = Mockito.mock(AccountVO.class);
Account caller = CallContext.current().getCallingAccount();
Mockito.when(caller.getId()).thenReturn(1L);
Mockito.lenient().when(_accountService.getActiveAccountById(1L)).thenReturn(accountMock);
Mockito.when(_accountService.getUserAccountById(1L)).thenReturn(userAccountVO);
Mockito.when(userAccountVO.isUser2faEnabled()).thenReturn(true);
Mockito.when(userAccountVO.getUser2faProvider()).thenReturn("staticpin");
Mockito.when(userAccountVO.getKeyFor2fa()).thenReturn("352352");
UserTwoFactorAuthenticator staticpinProvider = Mockito.mock(UserTwoFactorAuthenticator.class);
Map<String, UserTwoFactorAuthenticator> userTwoFactorAuthenticationProvidersMap = Mockito.mock(HashMap.class);
Mockito.when(userTwoFactorAuthenticationProvidersMap.containsKey("staticpin")).thenReturn( true);
Mockito.when(userTwoFactorAuthenticationProvidersMap.get("staticpin")).thenReturn(staticpinProvider);
AccountManagerImpl.userTwoFactorAuthenticationProvidersMap = userTwoFactorAuthenticationProvidersMap;
accountManagerImpl.verifyUsingTwoFactorAuthenticationCode("352352", 1L, 1L);
}
@Test
public void testEnable2FAcode() {
SetupUserTwoFactorAuthenticationCmd cmd = Mockito.mock(SetupUserTwoFactorAuthenticationCmd.class);
Mockito.when(cmd.getProvider()).thenReturn("staticpin");
AccountVO accountMock = Mockito.mock(AccountVO.class);
Mockito.when(callingAccount.getId()).thenReturn(1L);
Mockito.when(callingUser.getId()).thenReturn(1L);
CallContext.register(callingUser, callingAccount); // Calling account is user account i.e normal account
Mockito.lenient().when(_accountService.getActiveAccountById(1L)).thenReturn(accountMock);
Mockito.when(userAccountDaoMock.findById(1L)).thenReturn(userAccountVO);
Mockito.when(userDaoMock.findById(1L)).thenReturn(userVoMock);
Mockito.when(userAccountVO.getDomainId()).thenReturn(1L);
Mockito.when(enableUserTwoFactorAuthenticationMock.valueIn(1L)).thenReturn(true);
Mockito.when(cmd.getEnable()).thenReturn(true);
UserTwoFactorAuthenticator staticpinProvider = Mockito.mock(UserTwoFactorAuthenticator.class);
Map<String, UserTwoFactorAuthenticator> userTwoFactorAuthenticationProvidersMap = Mockito.mock(HashMap.class);
Mockito.when(userTwoFactorAuthenticationProvidersMap.containsKey("staticpin")).thenReturn( true);
Mockito.when(userTwoFactorAuthenticationProvidersMap.get("staticpin")).thenReturn(staticpinProvider);
Mockito.when(staticpinProvider.setup2FAKey(userAccountVO)).thenReturn("345543");
Mockito.when(userDaoMock.createForUpdate()).thenReturn(userVoMock);
AccountManagerImpl.userTwoFactorAuthenticationProvidersMap = userTwoFactorAuthenticationProvidersMap;
UserTwoFactorAuthenticationSetupResponse response = accountManagerImpl.setupUserTwoFactorAuthentication(cmd);
Assert.assertEquals("345543", response.getSecretCode());
}
}

View File

@ -23,12 +23,14 @@ import java.util.Map;
import org.apache.cloudstack.acl.SecurityChecker;
import org.apache.cloudstack.affinity.dao.AffinityGroupDao;
import org.apache.cloudstack.auth.UserAuthenticator;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
import org.apache.cloudstack.engine.service.api.OrchestrationService;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.cloudstack.framework.messagebus.MessageBus;
import org.apache.cloudstack.region.gslb.GlobalLoadBalancerRuleDao;
import org.apache.cloudstack.resourcedetail.dao.UserDetailsDao;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@ -60,7 +62,6 @@ import com.cloud.network.vpn.Site2SiteVpnManager;
import com.cloud.projects.ProjectManager;
import com.cloud.projects.dao.ProjectAccountDao;
import com.cloud.projects.dao.ProjectDao;
import com.cloud.server.auth.UserAuthenticator;
import com.cloud.service.dao.ServiceOfferingDao;
import com.cloud.storage.VolumeApiService;
import com.cloud.storage.dao.SnapshotDao;
@ -92,6 +93,8 @@ public class AccountManagetImplTestBase {
@Mock
UserDao userDaoMock;
@Mock
UserDetailsDao userDetailsDaoMock;
@Mock
InstanceGroupDao _vmGroupDao;
@Mock
UserAccountDao userAccountDaoMock;
@ -196,6 +199,8 @@ public class AccountManagetImplTestBase {
AccountManagerImpl accountManagerImpl;
@Mock
UsageEventDao _usageEventDao;
@Mock
AccountService _accountService;
@Before
public void setup() {

View File

@ -22,9 +22,12 @@ import java.net.InetAddress;
import javax.naming.ConfigurationException;
import com.cloud.api.auth.SetupUserTwoFactorAuthenticationCmd;
import org.apache.cloudstack.api.command.admin.account.CreateAccountCmd;
import org.apache.cloudstack.api.command.admin.user.GetUserKeysCmd;
import org.apache.cloudstack.api.command.admin.user.MoveUserCmd;
import org.apache.cloudstack.api.response.UserTwoFactorAuthenticationSetupResponse;
import org.apache.cloudstack.auth.UserTwoFactorAuthenticator;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.springframework.stereotype.Component;
@ -137,6 +140,21 @@ public class MockAccountManagerImpl extends ManagerBase implements Manager, Acco
return false;
}
@Override
public UserTwoFactorAuthenticator getUserTwoFactorAuthenticator(Long domainId, Long userAccountId) {
return null;
}
@Override
public void verifyUsingTwoFactorAuthenticationCode(String code, Long domainId, Long userAccountId) {
}
@Override
public UserTwoFactorAuthenticationSetupResponse setupUserTwoFactorAuthentication(SetupUserTwoFactorAuthenticationCmd cmd) {
return null;
}
@Override
public boolean isAdmin(Long accountId) {
// TODO Auto-generated method stub
@ -434,6 +452,16 @@ public class MockAccountManagerImpl extends ManagerBase implements Manager, Acco
return null;
}
@Override
public List<UserTwoFactorAuthenticator> listUserTwoFactorAuthenticationProviders() {
return null;
}
@Override
public UserTwoFactorAuthenticator getUserTwoFactorAuthenticationProvider(Long domainId) {
return null;
}
@Override
public void checkAccess(User user, ControlledEntity entity)
throws PermissionDeniedException {

View File

@ -0,0 +1,294 @@
# 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.
""" P1 tests for Account
"""
# Import Local Modules
from marvin.cloudstackTestCase import cloudstackTestCase
from marvin.lib.utils import (random_gen,
cleanup_resources,
validateList)
from marvin.cloudstackAPI import *
from marvin.lib.base import (Domain,
Account,
ServiceOffering,
VirtualMachine,
Network,
User,
Template,
Role)
from marvin.lib.common import (get_domain,
get_zone,
get_test_template,
list_accounts,
list_virtual_machines,
list_service_offering,
list_templates,
list_users,
wait_for_cleanup)
from nose.plugins.attrib import attr
from marvin.cloudstackException import CloudstackAPIException
from marvin.codes import PASS
import time
from pyVmomi.VmomiSupport import GetVersionFromVersionUri
class Services:
"""Test Account Services
"""
def __init__(self):
self.services = {
"domain": {
"name": "Domain",
},
"account": {
"email": "test@test.com",
"firstname": "Test",
"lastname": "User",
"username": "test",
# Random characters are appended for unique
# username
"password": "fr3sca",
},
"role": {
"name": "User Role",
"type": "User",
"description": "User Role created by Marvin test"
},
"user": {
"email": "user@test.com",
"firstname": "User",
"lastname": "User",
"username": "User",
# Random characters are appended for unique
# username
"password": "fr3sca",
},
"service_offering": {
"name": "Tiny Instance",
"displaytext": "Tiny Instance",
"cpunumber": 1,
"cpuspeed": 100,
# in MHz
"memory": 128,
# In MBs
},
"virtual_machine": {
"displayname": "Test VM",
"username": "root",
"password": "password",
"ssh_port": 22,
"hypervisor": 'XenServer',
# Hypervisor type should be same as
# hypervisor type of cluster
"privateport": 22,
"publicport": 22,
"protocol": 'TCP',
},
"template": {
"displaytext": "Public Template",
"name": "Public template",
"ostype": 'CentOS 5.6 (64-bit)',
"url": "",
"hypervisor": '',
"format": '',
"isfeatured": True,
"ispublic": True,
"isextractable": True,
"templatefilter": "self"
},
"ostype": 'CentOS 5.6 (64-bit)',
"sleep": 60,
"timeout": 10,
}
class TestUserLogin(cloudstackTestCase):
@classmethod
def setUpClass(cls):
cls.testClient = super(TestUserLogin, cls).getClsTestClient()
cls.api_client = cls.testClient.getApiClient()
cls.services = Services().services
cls.domain = get_domain(cls.api_client)
cls.zone = get_zone(cls.api_client, cls.testClient.getZoneForTests())
cls.services['mode'] = cls.zone.networktype
cls._cleanup = []
return
@classmethod
def tearDownClass(cls):
super(TestUserLogin,cls).tearDownClass()
def setUp(self):
self.apiclient = self.testClient.getApiClient()
self.dbclient = self.testClient.getDbConnection()
self.debug("Enabling 2FA in global setting")
updateConfigurationCmd = updateConfiguration.updateConfigurationCmd()
updateConfigurationCmd.name = "enable.user.2fa"
updateConfigurationCmd.value = "true"
updateConfigurationResponse = self.apiclient.updateConfiguration(
updateConfigurationCmd)
self.cleanup = []
return
def tearDown(self):
self.debug("Disable 2FA in global setting")
updateConfigurationCmd = updateConfiguration.updateConfigurationCmd()
updateConfigurationCmd.name = "enable.user.2fa"
updateConfigurationCmd.value = "false"
updateConfigurationResponse = self.apiclient.updateConfiguration(
updateConfigurationCmd)
super(TestUserLogin,self).tearDown()
@attr(tags=["login", "accounts", "simulator", "advanced",
"advancedns", "basic", "eip", "sg"])
def test_2FA_enabled(self):
"""Test if Login API does not return UUID's
"""
# Steps for test scenario
# 1. create a user account
# 2. login to the user account with given credentials (loginCmd)
# 3. verify login response
# Setup Global settings
self.debug("Mandate 2FA in global setting")
updateConfigurationCmd = updateConfiguration.updateConfigurationCmd()
updateConfigurationCmd.name = "mandate.user.2fa"
updateConfigurationCmd.value = "true"
updateConfigurationResponse = self.apiclient.updateConfiguration(
updateConfigurationCmd)
self.debug("Creating an user account..")
self.account = Account.create(
self.apiclient,
self.services["account"],
domainid=self.domain.id
)
self.cleanup.append(self.account)
self.debug("Logging into the cloudstack with login API")
response = User.login(
self.apiclient,
username=self.account.name,
password=self.services["account"]["password"]
)
self.debug("Login API response: %s" % response)
self.assertEqual(
response.is2faenabled,
"true",
"2FA enabled for user"
)
self.debug("Remove mandating 2FA in global setting")
updateConfigurationCmd = updateConfiguration.updateConfigurationCmd()
updateConfigurationCmd.name = "mandate.user.2fa"
updateConfigurationCmd.value = "false"
updateConfigurationResponse = self.apiclient.updateConfiguration(
updateConfigurationCmd)
return
@attr(tags=["login", "accounts", "simulator", "advanced",
"advancedns", "basic", "eip", "sg"])
def test_2FA_setup(self):
"""Test if Login API does not return UUID's
"""
# Steps for test scenario
# 1. create a user account
# 2. login to the user account with given credentials (loginCmd)
# 3. verify login response for 2fa
# 4. setup 2fa for the user
# 5. verify the code in the setup 2fa response
# 6. test disable 2FA
# Setup Global settings
self.debug("Mandate 2FA in global setting")
updateConfigurationCmd = updateConfiguration.updateConfigurationCmd()
updateConfigurationCmd.name = "mandate.user.2fa"
updateConfigurationCmd.value = "true"
updateConfigurationResponse = self.apiclient.updateConfiguration(
updateConfigurationCmd)
self.debug("Creating an user account..")
self.account = Account.create(
self.apiclient,
self.services["account"],
domainid=self.domain.id
)
self.cleanup.append(self.account)
self.debug("Logging into the cloudstack with login API")
response = User.login(
self.apiclient,
username=self.account.name,
password=self.services["account"]["password"]
)
self.debug("Login API response: %s" % response)
self.assertEqual(
response.is2faenabled,
"true",
"2FA enabled for user"
)
self.user = self.account.user[0]
self.user_apiclient = self.testClient.getUserApiClient(
self.user.username, self.domain.id
)
setup2faCmd = setupUserTwoFactorAuthentication.setupUserTwoFactorAuthenticationCmd()
setup2faCmd.provider = "staticpin"
setup2faResponse = self.user_apiclient.setupUserTwoFactorAuthentication(
setup2faCmd)
self.assertNotEqual(
setup2faResponse.secretcode,
None,
"2FA enabled for user"
)
disable2faCmd = setupUserTwoFactorAuthentication.setupUserTwoFactorAuthenticationCmd()
disable2faCmd.enable = "false"
disable2faResponse = self.user_apiclient.setupUserTwoFactorAuthentication(
disable2faCmd)
self.assertEqual(
disable2faResponse.secretcode,
None,
"2FA disabled for user"
)
self.debug("Remove mandating 2FA in global setting")
updateConfigurationCmd = updateConfiguration.updateConfigurationCmd()
updateConfigurationCmd.name = "mandate.user.2fa"
updateConfigurationCmd.value = "false"
updateConfigurationResponse = self.apiclient.updateConfiguration(
updateConfigurationCmd)
return

64
ui/package-lock.json generated
View File

@ -2840,6 +2840,11 @@
"source-map": "^0.6.1"
}
},
"@types/uuid": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
"integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw=="
},
"@types/webpack": {
"version": "4.41.32",
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.32.tgz",
@ -6973,6 +6978,16 @@
"resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
"integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc="
},
"chart.js": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.9.1.tgz",
"integrity": "sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w=="
},
"chartjs-adapter-moment": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/chartjs-adapter-moment/-/chartjs-adapter-moment-1.0.0.tgz",
"integrity": "sha512-PqlerEvQcc5hZLQ/NQWgBxgVQ4TRdvkW3c/t+SUEQSj78ia3hgLkf2VZ2yGJtltNbEEFyYGm+cA6XXevodYvWA=="
},
"check-types": {
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/check-types/-/check-types-8.0.3.tgz",
@ -18424,6 +18439,11 @@
"integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=",
"dev": true
},
"qrious": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/qrious/-/qrious-4.0.2.tgz",
"integrity": "sha512-xWPJIrK1zu5Ypn898fBp8RHkT/9ibquV2Kv24S/JY9VYEhMBMKur1gHVsOiNUh7PHP9uCgejjpZUHUIXXKoU/g=="
},
"qs": {
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz",
@ -22073,6 +22093,19 @@
"@vue/shared": "3.2.31"
}
},
"vue-chartjs": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-4.1.2.tgz",
"integrity": "sha512-QSggYjeFv/L4jFSBQpX8NzrAvX0B+Ha6nDgxkTG8tEXxYOOTwKI4phRLe+B4f+REnkmg7hgPY24R0cixZJyXBg=="
},
"vue-clipboard2": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/vue-clipboard2/-/vue-clipboard2-0.3.3.tgz",
"integrity": "sha512-aNWXIL2DKgJyY/1OOeITwAQz1fHaCIGvUFHf9h8UcoQBG5a74MkdhS/xqoYe7DNZdQmZRL+TAdIbtUs9OyVjbw==",
"requires": {
"clipboard": "^2.0.0"
}
},
"vue-codemod": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/vue-codemod/-/vue-codemod-0.0.5.tgz",
@ -22345,6 +22378,21 @@
"loader-utils": "^2.0.0"
}
},
"vue-qrious": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/vue-qrious/-/vue-qrious-3.1.0.tgz",
"integrity": "sha512-qC5jw94b/VbUHFxYfumwqhSXKBJNEmaimhpwEmudqOiORMd5yPCFn/mPInnP5nWobvhvcV+S+U3Ger6w2dLyfQ==",
"requires": {
"tslib": "^2.4.0"
},
"dependencies": {
"tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
}
}
},
"vue-router": {
"version": "4.0.14",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.0.14.tgz",
@ -22416,6 +22464,22 @@
"is-plain-object": "3.0.1"
}
},
"vue-uuid": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/vue-uuid/-/vue-uuid-3.0.0.tgz",
"integrity": "sha512-+5DP857xVmTHYd00dMC1c1gVg/nxG6+K4Lepojv9ckHt8w0fDpGc5gQCCttS9D+AkSkTJgb0cekidKjTWu5OQQ==",
"requires": {
"@types/uuid": "^8.3.4",
"uuid": "^8.3.2"
},
"dependencies": {
"uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
}
}
},
"vue-web-storage": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/vue-web-storage/-/vue-web-storage-6.1.0.tgz",

View File

@ -53,12 +53,14 @@
"moment": "^2.26.0",
"npm-check-updates": "^6.0.1",
"nprogress": "^0.2.0",
"qrious": "^4.0.2",
"vue": "^3.2.31",
"vue-chartjs": "^4.0.7",
"vue-clipboard2": "^0.3.1",
"vue-cropper": "^1.0.2",
"vue-i18n": "^9.1.6",
"vue-loader": "^16.2.0",
"vue-qrious": "^3.1.0",
"vue-router": "^4.0.14",
"vue-uuid": "^3.0.0",
"vue-web-storage": "^6.1.0",

View File

@ -109,6 +109,8 @@
"label.action.edit.instance": "Edit instance",
"label.action.edit.iso": "Edit ISO",
"label.action.edit.zone": "Edit zone",
"label.action.enable.two.factor.authentication": "Enabled Two factor authentication",
"label.action.verify.two.factor.authentication": "Verified Two factor authentication",
"label.action.enable.account": "Enable account",
"label.action.enable.cluster": "Enable cluster",
"label.action.enable.maintenance.mode": "Enable maintenance mode",
@ -142,6 +144,7 @@
"label.action.reboot.systemvm": "Reboot system VM",
"label.action.recover.volume": "Recover volume",
"label.action.recurring.snapshot": "Recurring snapshots",
"label.action.disable.2FA.user.auth": "Disable User Two Factor Authentication",
"label.action.register.iso": "Register ISO",
"label.action.register.template": "Register template from URL",
"label.action.release.ip": "Release IP",
@ -160,6 +163,7 @@
"label.action.router.health.checks": "Get health checks result",
"label.action.run.diagnostics": "Run diagnostics",
"label.action.secure.host": "Provision host security keys",
"label.action.setup.2FA.user.auth": "Setup User Two Factor Authentication",
"label.action.start.instance": "Start instance",
"label.action.start.router": "Start router",
"label.action.start.systemvm": "Start system VM",
@ -458,6 +462,7 @@
"label.configuration": "Configuration",
"label.configure": "Configure",
"label.configure.health.monitor": "Configure Health Monitor",
"label.configure.app": "Configure the App",
"label.configure.ldap": "Configure LDAP",
"label.configure.ovs": "Configure Ovs",
"label.configure.sticky.policy": "Configure sticky policy",
@ -776,6 +781,8 @@
"label.endipv6": "IPv6 end IP",
"label.endpoint": "Endpoint",
"label.endport": "End port",
"label.enter.code": "Enter 2FA code to verify",
"label.enter.static.pin": "Enter static PIN to verify",
"label.enter.token": "Enter token",
"label.entityid": "Entity",
"label.entitytype": "Entity Type",
@ -1014,6 +1021,7 @@
"label.iqn": "Target IQN",
"label.is.in.progress": "is in progress",
"label.is.shared": "Is shared",
"label.is2faenabled": "Is 2FA enabled",
"label.isadvanced": "Show advanced settings",
"label.iscsi": "iSCSI",
"label.iscustomized": "Custom disk size",
@ -1703,6 +1711,7 @@
"label.select.ps": "Select primary storage",
"label.select.tier": "Select tier",
"label.select.zones": "Select zones",
"label.select.2fa.provider": "Select the provider",
"label.self": "Mine",
"label.selfexecutable": "Self",
"label.semanticversion": "Semantic version",
@ -1942,6 +1951,10 @@
"label.transportzoneuuid": "Transport zone UUID",
"label.try.again": "Try again",
"label.tuesday": "Tuesday",
"label.two.factor.authentication.secret.key": "Your Two factor authentication secret key",
"label.two.factor.authentication.static.pin": "Your Two factor authentication static PIN",
"label.two.factor.authentication": "Two Factor Authentication",
"label.2FA": "2FA",
"label.tungsten.fabric": "Tungsten Fabric",
"label.tungsten.fabric.provider": "Tungsten Fabric Provider",
"label.tungsten.fabric.routing": "Tungsten Fabric Routing",
@ -2037,6 +2050,7 @@
"label.vcenterpassword": "vCenter password",
"label.vcenterusername": "vCenter username",
"label.vcsdeviceid": "ID",
"label.verify": "Verify",
"label.version": "Version",
"label.versions": "Versions",
"label.vgpu": "VGPU",
@ -2188,6 +2202,8 @@
"message.action.destroy.instance.with.backups": "Please confirm that you want to destroy the instance. There may be backups associated with the instance which will not be deleted.",
"message.action.destroy.systemvm": "Please confirm that you want to destroy the System VM.",
"message.action.destroy.volume": "Please confirm that you want to destroy the volume.",
"message.action.disable.2FA.user.auth": "Please confirm that you want to disable user two factor authentication.",
"message.action.about.mandate.and.disable.2FA.user.auth": "Two factor authentication is mandated for the user, if this is disabled now user will need to setup two factor authentication again during next login. <br><br>Please confirm that you want to disable.",
"message.action.disable.cluster": "Please confirm that you want to disable this cluster.",
"message.action.disable.physical.network": "Please confirm that you want to disable this physical network.",
"message.action.disable.pod": "Please confirm that you want to disable this pod.",
@ -2498,6 +2514,7 @@
"message.error.add.tungsten.routing.policy": "Adding Tungsten-Fabric Routing Policy failed",
"message.error.agent.password": "Please enter agent password.",
"message.error.agent.username": "Please enter agent username.",
"message.error.authentication.code": "Please enter authentication code.",
"message.error.apply.network.policy": "Applying Network Policy failed",
"message.error.apply.tungsten.tag": "Applying Tag failed",
"message.error.binaries.iso.url": "Please enter binaries ISO URL.",
@ -2516,6 +2533,8 @@
"message.error.delete.tungsten.tag": "Removing Tag failed",
"message.error.description": "Please enter description.",
"message.error.discovering.feature": "Exception caught while discovering features.",
"message.error.setup.2fa": "2FA setup failed while verifying the code, please retry.",
"message.error.verifying.2fa": "Unable to verify 2FA, please retry.",
"message.error.display.text": "Please enter display text.",
"message.error.duration.less.than.interval": "The duration in Autoscale policy cannot be less than interval",
"message.error.enable.saml": "Unable to find users IDs to enable SAML single sign on, kindly enable it manually.",
@ -2951,6 +2970,21 @@
"message.update.autoscale.vm.profile.failed": "Failed to update autoscale vm profile",
"message.update.condition.failed": "Failed to update condition",
"message.update.condition.processing": "Updating condition...",
"message.two.factor.authorization.failed": "Unable to verify 2FA with provided code, please retry.",
"message.two.fa.auth": "Open the two-factor authentication app on your mobile device to view your authentication code.",
"message.two.fa.auth.register.account": "Open the two-factor authentication application and scan the QR code add the user account.",
"message.two.fa.static.pin.part1": "If you can't scan the QR code, ",
"message.two.fa.static.pin.part2": "Click here to view the secret code",
"message.two.fa.auth.staticpin": "<br> You have configured 2FA for security verification. <br> Enter the static PIN generated during the 2FA setup to verify.",
"message.two.fa.auth.totp": "<br> You have configured 2FA for security verification. <br> Open the TOTP authenticator application on your device and enter the authentication code.",
"message.two.fa.register.account": "1. Open the TOTP authenticator application on your device. <br>2. Scan the below QR code to add the user. <br>3. If you cannot scan the QR code, enter the setup key manually. <br>4. Verification of the 2FA code is mandatory to complete the 2FA setup.",
"message.two.fa.staticpin": "1. Use the generated static PIN as 2FA code for two factor authentication.<br>2. Save this static PIN / 2FA code and do not share it. This code will be used for subsequent logins.<br>3. Verification of the 2FA code is mandatory to complete the 2FA setup.",
"message.two.fa.register.account.login.page": "1. Open the TOTP authenticator application on your device. <br>2. Scan the below QR code to add the user. <br>3. If you cannot scan the QR code, enter the setup key manually. <br>4. Verify the 2FA code to continue to login.",
"message.two.fa.staticpin.login.page": "1. Use the generated static PIN as 2FA code for two factor authentication.<br>2. Save this static PIN / 2FA code and do not share it. This code will be used for subsequent logins.<br>3. Verify the 2FA code to continue to login.",
"message.two.fa.login.page": "Two factor authentication (2FA) is enabled on your account, you need to select a 2FA provider and setup. 2FA is an extra layer of security to your account. Once setup is done, on every login you will be prompted to enter 2FA code.<br>",
"message.two.fa.setup.page": "Two factor authentication (2FA) is an extra layer of security to your account.<br> Once setup is done, on every login you will be prompted to enter the 2FA code.<br>",
"message.two.fa.view.setup.key": "Click here to view the setup key",
"message.two.fa.view.static.pin": "Click here to view the static PIN",
"message.update.ipaddress.processing": "Updating IP Address...",
"message.update.resource.count": "Please confirm that you want to update resource counts for this account.",
"message.update.resource.count.domain": "Please confirm that you want to update resource counts for this domain.",

View File

@ -17,9 +17,7 @@
<template>
<a-layout class="layout" :class="[device]">
<a-affix style="z-index: 200">
<template v-if="isSideMenu()">
<a-drawer
v-if="isMobile()"

View File

@ -309,6 +309,24 @@ export const constantRouterMap = [
}
]
},
{
path: '/verify2FA',
name: 'VerifyTwoFa',
meta: {
title: 'label.two.factor.authentication',
hidden: true
},
component: () => import('@/views/dashboard/VerifyTwoFa')
},
{
path: '/setup2FA',
name: 'SetupTwoFaAtLogin',
meta: {
title: 'label.two.factor.authentication',
hidden: true
},
component: () => import('@/views/dashboard/SetupTwoFaAtLogin')
},
{
path: '/403',
component: () => import(/* webpackChunkName: "forbidden" */ '@/views/exception/403')

View File

@ -26,7 +26,7 @@ export default {
hidden: true,
permission: ['listUsers'],
columns: ['username', 'state', 'firstname', 'lastname', 'email', 'account'],
details: ['username', 'id', 'firstname', 'lastname', 'email', 'usersource', 'timezone', 'rolename', 'roletype', 'account', 'domain', 'created'],
details: ['username', 'id', 'firstname', 'lastname', 'email', 'usersource', 'timezone', 'rolename', 'roletype', 'is2faenabled', 'account', 'domain', 'created'],
tabs: [
{
name: 'details',
@ -106,6 +106,38 @@ export default {
},
component: shallowRef(defineAsyncComponent(() => import('@/views/iam/ConfigureSamlSsoAuth.vue')))
},
{
api: 'setupUserTwoFactorAuthentication',
icon: 'scan-outlined',
label: 'label.action.setup.2FA.user.auth',
dataView: true,
popup: true,
show: (record, store) => {
return (record.is2faenabled === false && record.id === store.userInfo.id)
},
component: shallowRef(defineAsyncComponent(() => import('@/views/iam/SetupTwoFaAtUserProfile.vue')))
},
{
api: 'setupUserTwoFactorAuthentication',
icon: 'scan-outlined',
label: 'label.action.disable.2FA.user.auth',
message: (record) => { return record.is2famandated === true ? 'message.action.about.mandate.and.disable.2FA.user.auth' : 'message.action.disable.2FA.user.auth' },
dataView: true,
groupAction: true,
popup: true,
args: ['enable', 'userid'],
mapping: {
enable: {
value: (record) => { return false }
},
userid: {
value: (record) => { return record.id }
}
},
show: (record, store) => {
return (record.is2faenabled === true) && (record.id === store.userInfo.id || ['Admin', 'DomainAdmin'].includes(store.userInfo.roletype))
}
},
{
api: 'deleteUser',
icon: 'delete-outlined',

View File

@ -75,6 +75,7 @@ import {
ExclamationCircleOutlined,
EyeInvisibleOutlined,
EyeOutlined,
FieldTimeOutlined,
FileProtectOutlined,
FilterOutlined,
FilterTwoTone,
@ -90,6 +91,7 @@ import {
GithubOutlined,
GlobalOutlined,
GoldOutlined,
GoogleOutlined,
HddOutlined,
HomeOutlined,
IdcardOutlined,
@ -112,6 +114,7 @@ import {
MinusCircleOutlined,
MinusOutlined,
MinusSquareOutlined,
MobileOutlined,
MoreOutlined,
NotificationOutlined,
NumberOutlined,
@ -138,6 +141,7 @@ import {
SaveOutlined,
ScheduleOutlined,
ScissorOutlined,
ScanOutlined,
SearchOutlined,
SettingOutlined,
ShareAltOutlined,
@ -224,6 +228,7 @@ export default {
app.component('ExclamationCircleOutlined', ExclamationCircleOutlined)
app.component('EyeInvisibleOutlined', EyeInvisibleOutlined)
app.component('EyeOutlined', EyeOutlined)
app.component('FieldTimeOutlined', FieldTimeOutlined)
app.component('FileProtectOutlined', FileProtectOutlined)
app.component('FilterOutlined', FilterOutlined)
app.component('FilterTwoTone', FilterTwoTone)
@ -239,6 +244,7 @@ export default {
app.component('GithubOutlined', GithubOutlined)
app.component('GlobalOutlined', GlobalOutlined)
app.component('GoldOutlined', GoldOutlined)
app.component('GoogleOutlined', GoogleOutlined)
app.component('HddOutlined', HddOutlined)
app.component('HomeOutlined', HomeOutlined)
app.component('IdcardOutlined', IdcardOutlined)
@ -261,6 +267,7 @@ export default {
app.component('MinusCircleOutlined', MinusCircleOutlined)
app.component('MinusOutlined', MinusOutlined)
app.component('MinusSquareOutlined', MinusSquareOutlined)
app.component('MobileOutlined', MobileOutlined)
app.component('MoreOutlined', MoreOutlined)
app.component('NotificationOutlined', NotificationOutlined)
app.component('NumberOutlined', NumberOutlined)
@ -286,6 +293,7 @@ export default {
app.component('SafetyOutlined', SafetyOutlined)
app.component('SaveOutlined', SaveOutlined)
app.component('ScheduleOutlined', ScheduleOutlined)
app.component('ScanOutlined', ScanOutlined)
app.component('ScissorOutlined', ScissorOutlined)
app.component('SearchOutlined', SearchOutlined)
app.component('SettingOutlined', SettingOutlined)

View File

@ -59,7 +59,33 @@ router.beforeEach((to, from, next) => {
if (to.path === '/user/login') {
next({ path: '/dashboard' })
NProgress.done()
} else if (to.path === '/verify2FA' || to.path === '/setup2FA') {
const isSAML = JSON.parse(Cookies.get('isSAML') || Cookies.get('isSAML', { path: '/client' }) || false)
const twoFaEnabled = JSON.parse(Cookies.get('twoFaEnabled') || Cookies.get('twoFaEnabled', { path: '/client' }) || false)
const twoFaProvider = Cookies.get('twoFaProvider') || Cookies.get('twoFaProvider', { path: '/client' }) || store.getters.twoFaProvider
if ((store.getters.twoFaEnabled && !store.getters.loginFlag) || (isSAML === true && twoFaEnabled === true)) {
console.log('Do Two-factor authentication')
store.commit('SET_2FA_PROVIDER', twoFaProvider)
next()
} else {
next({ path: '/dashboard' })
NProgress.done()
}
} else {
const isSAML = JSON.parse(Cookies.get('isSAML') || Cookies.get('isSAML', { path: '/client' }) || false)
const twoFaEnabled = JSON.parse(Cookies.get('twoFaEnabled') || Cookies.get('twoFaEnabled', { path: '/client' }) || false)
const twoFaProvider = Cookies.get('twoFaProvider') || Cookies.get('twoFaProvider', { path: '/client' })
if (isSAML === true && !store.getters.loginFlag && to.path !== '/dashboard') {
if (twoFaEnabled === true && twoFaProvider !== '' && twoFaProvider !== undefined) {
next({ path: '/verify2FA' })
return
}
if (twoFaEnabled === true && (twoFaProvider === '' || twoFaProvider === undefined)) {
next({ path: '/setup2FA' })
return
}
store.commit('SET_LOGIN_FLAG', true)
}
if (Object.keys(store.getters.apis).length === 0) {
const cachedApis = vueProps.$localStorage.get(APIS, {})
if (Object.keys(cachedApis).length > 0) {
@ -85,17 +111,19 @@ router.beforeEach((to, from, next) => {
let countNotify = store.getters.countNotify
countNotify++
store.commit('SET_COUNT_NOTIFY', countNotify)
notification.error({
top: '65px',
message: 'Error',
description: i18n.global.t('message.error.discovering.feature'),
duration: 0,
onClose: () => {
let countNotify = store.getters.countNotify
countNotify > 0 ? countNotify-- : countNotify = 0
store.commit('SET_COUNT_NOTIFY', countNotify)
}
})
if (to.path === '/user/login') {
notification.error({
top: '65px',
message: 'Error',
description: i18n.global.t('message.error.discovering.feature'),
duration: 0,
onClose: () => {
let countNotify = store.getters.countNotify
countNotify > 0 ? countNotify-- : countNotify = 0
store.commit('SET_COUNT_NOTIFY', countNotify)
}
})
}
store.dispatch('Logout').then(() => {
next({ path: '/user/login', query: { redirect: to.fullPath } })
})

View File

@ -43,7 +43,11 @@ const getters = {
defaultListViewPageSize: state => state.user.defaultListViewPageSize,
countNotify: state => state.user.countNotify,
customColumns: state => state.user.customColumns,
logoutFlag: state => state.user.logoutFlag
logoutFlag: state => state.user.logoutFlag,
twoFaEnabled: state => state.user.twoFaEnabled,
twoFaProvider: state => state.user.twoFaProvider,
twoFaIssuer: state => state.user.twoFaIssuer,
loginFlag: state => state.user.loginFlag
}
export default getters

View File

@ -58,8 +58,12 @@ const user = {
darkMode: false,
defaultListViewPageSize: 20,
countNotify: 0,
loginFlag: false,
logoutFlag: false,
customColumns: {}
customColumns: {},
twoFaEnabled: false,
twoFaProvider: '',
twoFaIssuer: ''
},
mutations: {
@ -131,6 +135,18 @@ const user = {
},
SET_LOGOUT_FLAG: (state, flag) => {
state.logoutFlag = flag
},
SET_2FA_ENABLED: (state, flag) => {
state.twoFaEnabled = flag
},
SET_2FA_PROVIDER: (state, flag) => {
state.twoFaProvider = flag
},
SET_2FA_ISSUER: (state, flag) => {
state.twoFaIssuer = flag
},
SET_LOGIN_FLAG: (state, flag) => {
state.loginFlag = flag
}
},
@ -172,7 +188,10 @@ const user = {
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()
@ -212,7 +231,7 @@ const user = {
}).catch(error => {
reject(error)
})
} else {
} else if (store.getters.loginFlag) {
const hide = message.loading(i18n.global.t('message.discovering.feature'), 0)
api('listZones').then(json => {
const zones = json.listzonesresponse.zone || []
@ -300,6 +319,10 @@ const user = {
commit('RESET_THEME')
commit('SET_DOMAIN_STORE', {})
commit('SET_LOGOUT_FLAG', true)
commit('SET_2FA_ENABLED', false)
commit('SET_2FA_PROVIDER', '')
commit('SET_2FA_ISSUER', '')
commit('SET_LOGIN_FLAG', false)
vueProps.$localStorage.remove(CURRENT_PROJECT)
vueProps.$localStorage.remove(ACCESS_TOKEN)
vueProps.$localStorage.remove(HEADER_NOTICES)
@ -381,6 +404,9 @@ const user = {
},
SetDarkMode ({ commit }, darkMode) {
commit('SET_DARK_MODE', darkMode)
},
SetLoginFlag ({ commit }, loggedIn) {
commit('SET_LOGIN_FLAG', loggedIn)
}
}
}

View File

@ -77,18 +77,34 @@ const err = (error) => {
}
countNotify++
store.commit('SET_COUNT_NOTIFY', countNotify)
notification.error({
top: '65px',
message: i18n.global.t('label.unauthorized'),
description: i18n.global.t('message.authorization.failed'),
key: 'http-401',
duration: 0,
onClose: () => {
let countNotify = store.getters.countNotify
countNotify > 0 ? countNotify-- : countNotify = 0
store.commit('SET_COUNT_NOTIFY', countNotify)
}
})
if (originalPath === '/verify2FA' || originalPath === '/setup2FA') {
notification.error({
top: '65px',
message: i18n.global.t('label.2FA'),
description: i18n.global.t('message.error.verifying.2fa'),
key: 'http-401',
duration: 0,
onClose: () => {
let countNotify = store.getters.countNotify
countNotify > 0 ? countNotify-- : countNotify = 0
store.commit('SET_COUNT_NOTIFY', countNotify)
}
})
} else {
notification.error({
top: '65px',
message: i18n.global.t('label.unauthorized'),
description: i18n.global.t('message.authorization.failed'),
key: 'http-401',
duration: 0,
onClose: () => {
let countNotify = store.getters.countNotify
countNotify > 0 ? countNotify-- : countNotify = 0
store.commit('SET_COUNT_NOTIFY', countNotify)
}
})
}
store.dispatch('Logout').then(() => {
if (originalPath !== '/user/login') {
router.push({ path: '/user/login', query: { redirect: originalPath } })

View File

@ -119,7 +119,7 @@
:maskClosable="false"
:cancelText="$t('label.cancel')"
style="top: 20px;"
@cancel="closeAction"
@cancel="cancelAction"
:confirmLoading="actionLoading"
:footer="null"
centered
@ -163,7 +163,7 @@
:ok-button-props="getOkProps()"
:cancel-button-props="getCancelProps()"
:confirmLoading="actionLoading"
@cancel="closeAction"
@cancel="cancelAction"
centered
>
<template #title>
@ -1009,6 +1009,10 @@ export default {
this.showAction = false
this.currentAction = {}
},
cancelAction () {
eventBus.emit('action-closing', { action: this.currentAction })
this.closeAction()
},
onRowSelectionChange (selection) {
this.selectedRowKeys = selection
if (selection?.length > 0) {

View File

@ -298,7 +298,14 @@ export default {
loginSuccess (res) {
this.$notification.destroy()
this.$store.commit('SET_COUNT_NOTIFY', 0)
this.$router.push({ path: '/dashboard' }).catch(() => {})
if (store.getters.twoFaEnabled === true && store.getters.twoFaProvider !== '' && store.getters.twoFaProvider !== undefined) {
this.$router.push({ path: '/verify2FA' }).catch(() => {})
} else if (store.getters.twoFaEnabled === true && (store.getters.twoFaProvider === '' || store.getters.twoFaProvider === undefined)) {
this.$router.push({ path: '/setup2FA' }).catch(() => {})
} else {
this.$store.commit('SET_LOGIN_FLAG', true)
this.$router.push({ path: '/dashboard' }).catch(() => {})
}
},
requestFailed (err) {
if (err && err.response && err.response.data && err.response.data.loginresponse) {

View File

@ -35,13 +35,17 @@ import store from '@/store'
import CapacityDashboard from './CapacityDashboard'
import UsageDashboard from './UsageDashboard'
import OnboardingDashboard from './OnboardingDashboard'
import VerifyTwoFa from './VerifyTwoFa'
import SetupTwoFaAtLogin from './SetupTwoFaAtLogin'
export default {
name: 'Dashboard',
components: {
CapacityDashboard,
UsageDashboard,
OnboardingDashboard
OnboardingDashboard,
VerifyTwoFa,
SetupTwoFaAtLogin
},
provide: function () {
return {

View File

@ -0,0 +1,341 @@
// 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.
<template>
<div class="center">
<a-form
:ref="formRef"
:model="form"
:rules="rules">
<img
v-if="$config.banner"
:style="{
width: $config.theme['@banner-width'],
height: $config.theme['@banner-height'],
}"
:src="$config.banner"
class="center-align"
alt="logo">
<br />
<h1 style="text-align: center; font-size: 24px; color: gray"> {{ $t('label.two.factor.authentication') }} </h1>
<p style="font-size: 16px;" v-html="$t('message.two.fa.login.page')"></p>
<h3> {{ $t('label.select.2fa.provider') }} </h3>
<a-row :gutter="24">
<a-col :md="24" :lg="22">
<a-form-item v-ctrl-enter="submitPin" ref="selectedProvider" name="selectedProvider">
<a-select
v-model:value="form.selectedProvider"
optionFilterProp="label"
:filterOption="(input, option) => {
return option.children[0].children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
@change="val => { handleSelectChange(val) }">
<a-select-option
v-for="(opt) in providers"
:key="opt"
:value="opt">
<div>
<span v-if="opt === 'totp'">
<google-outlined />
Google Authenticator
</span>
<span v-if="opt === 'othertotp'">
<field-time-outlined />
Other TOTP Authenticators
</span>
<span v-if="opt === 'staticpin'">
<lock-outlined />
Static PIN
</span>
</div>
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :md="24" :lg="2">
<a-form-item>
<div v-if="selectedProvider">
<a-button ref="submit" type="primary" :disabled="twoFAenabled" @click="setup2FAProvider">{{ $t('label.setup') }}</a-button>
<tooltip-button
tooltipPlacement="top"
:tooltip="$t('label.accept.project.invitation')"
icon="check-outlined"
size="small"
@onClick="setup2FAProvider()"/>
<tooltip-button
tooltipPlacement="top"
:tooltip="$t('label.decline.invitation')"
type="primary"
:danger="true"
icon="close-outlined"
size="small"
@onClick="setup2FAProvider()"/>
</div>
</a-form-item>
</a-col>
</a-row>
<div v-if="twoFAenabled">
<div v-if="form.selectedProvider !== 'staticpin'">
<br />
<p v-html="$t('message.two.fa.register.account.login.page')"></p>
<vue-qrious
class="center-align"
:value="totpUrl"
size="150"
@change="onDataUrlChange"
/>
<div style="text-align: center"> <a @click="showConfiguredPin"> {{ $t('message.two.fa.view.setup.key') }}</a></div>
</div>
<div v-if="form.selectedProvider === 'staticpin'">
<br>
<p v-html="$t('message.two.fa.staticpin.login.page')"></p>
<br>
<div> <a @click="showConfiguredPin"> {{ $t('message.two.fa.view.static.pin') }}</a></div>
</div>
<div v-if="form.selectedProvider">
<br />
<h3> {{ $t('label.enter.code') }} </h3>
<a-row :gutter="24">
<a-col :md="24" :lg="22">
<a-form-item @finish="submitPin" v-ctrl-enter="submitPin" name="code" ref="code">
<a-input-password
v-model:value="form.code"
placeholder="xxxxxx" />
</a-form-item>
</a-col>
<a-col :md="24" :lg="2">
<a-form-item>
<a-button ref="submit" type="primary" :disabled="verifybuttonstate" @click="submitPin">{{ $t('label.verify') }}</a-button>
</a-form-item>
</a-col>
</a-row>
</div>
<a-modal
v-if="showPin"
:visible="showPin"
:title="$t(form.selectedProvider === 'staticpin'? 'label.two.factor.authentication.static.pin' : 'label.two.factor.authentication.secret.key')"
:closable="true"
:footer="null"
@cancel="onCloseModal"
centered
width="450px">
<div> {{ pin }} </div>
</a-modal>
</div>
</a-form>
</div>
</template>
<script>
import { api } from '@/api'
import { ref, reactive, toRaw } from 'vue'
import store from '@/store'
import VueQrious from 'vue-qrious'
import eventBus from '@/config/eventBus'
export default {
name: 'SetupTwoFaAtLogin',
props: {
resource: {
type: Object,
required: true
}
},
components: {
VueQrious
},
data () {
return {
totpUrl: '',
dataUrl: '',
pin: '',
showPin: false,
twoFAenabled: false,
twoFAverified: false,
providers: [],
selectedProvider: null,
verifybuttonstate: false
}
},
mounted () {
this.list2FAProviders()
},
created () {
this.initForm()
eventBus.on('action-closing', (args) => {
if (args.action.api === 'setupUserTwoFactorAuthentication' && this.twoFAenabled && !this.twoFAverified) {
this.disable2FAProvider()
}
})
},
methods: {
initForm () {
this.formRef = ref()
this.form = reactive({})
this.rules = reactive({
code: [{ required: true, message: this.$t('message.error.authentication.code') }]
})
},
onDataUrlChange (dataUrl) {
this.dataUrl = dataUrl
},
handleSelectChange (val) {
if (this.twoFAenabled) {
api('setupUserTwoFactorAuthentication', { enable: 'false' }).then(response => {
this.pin = ''
this.username = ''
this.totpUrl = ''
this.dataUrl = ''
this.showPin = false
this.twoFAenabled = false
this.twoFAverified = false
}).catch(error => {
this.$notification.error({
message: this.$t('message.request.failed'),
description: (error.response && error.response.headers && error.response.headers['x-description']) || error.message
})
})
}
this.selectedProvider = val
this.twoFAenabled = false
},
setup2FAProvider () {
this.formRef.value.validate().then(() => {
const values = toRaw(this.form)
this.selectedProvider = values.selectedProvider
var provider
if (this.selectedProvider === 'othertotp') {
provider = 'totp'
} else {
provider = this.selectedProvider
}
api('setupUserTwoFactorAuthentication', { provider: provider }).then(response => {
this.pin = response.setupusertwofactorauthenticationresponse.setup2fa.secretcode
if (this.selectedProvider === 'totp' || this.selectedProvider === 'othertotp') {
this.username = response.setupusertwofactorauthenticationresponse.setup2fa.username
var issuer = 'CloudStack'
if (store.getters.twoFaIssuer !== '' && store.getters.twoFaIssuer !== undefined) {
issuer = store.getters.twoFaIssuer
}
this.totpUrl = 'otpauth://totp/' + issuer + ':' + this.username + '?secret=' + this.pin + '&issuer=' + issuer
this.showPin = false
}
if (this.selectedProvider === 'staticpin') {
this.showPin = true
}
this.twoFAenabled = true
this.twoFAverified = false
}).catch(error => {
this.$notification.error({
message: this.$t('message.request.failed'),
description: (error.response && error.response.headers && error.response.headers['x-description']) || error.message
})
})
})
},
disable2FAProvider () {
api('setupUserTwoFactorAuthentication', { enable: false }).then(response => {
this.showPin = false
this.twoFAenabled = false
this.twoFAverified = false
}).catch(error => {
this.$notification.error({
message: this.$t('message.request.failed'),
description: (error.response && error.response.headers && error.response.headers['x-description']) || error.message
})
})
},
list2FAProviders () {
api('listUserTwoFactorAuthenticatorProviders', {}).then(response => {
var providerlist = response.listusertwofactorauthenticatorprovidersresponse.providers || []
var providernames = []
for (const provider of providerlist) {
providernames.push(provider.name)
if (provider.name === 'totp') {
providernames.push('othertotp')
}
}
this.providers = providernames
})
},
submitPin () {
this.formRef.value.validate().then(() => {
const values = toRaw(this.form)
if (values.code !== null) {
this.verifybuttonstate = true
}
api('validateUserTwoFactorAuthenticationCode', { codefor2fa: values.code }).then(response => {
this.$message.success({
content: `${this.$t('label.action.enable.two.factor.authentication')}`,
duration: 2
})
this.$notification.destroy()
this.$store.commit('SET_COUNT_NOTIFY', 0)
this.$store.commit('SET_LOGIN_FLAG', true)
this.$router.push({ path: '/dashboard' }).catch(() => {})
this.twoFAverified = true
this.$emit('refresh-data')
}).catch(() => {
this.$notification.error({
message: this.$t('message.request.failed'),
description: this.$t('message.error.setup.2fa')
})
this.$store.dispatch('Logout').then(() => {
this.$router.replace({ path: '/user/login' })
})
})
})
},
closeAction () {
this.$emit('close-action')
},
showConfiguredPin () {
this.showPin = true
},
onCloseModal () {
this.showPin = false
}
}
}
</script>
<style scoped>
.center {
background-color: transparent;
padding: 80px 500px 70px 500px;
}
.center-align {
display: block;
margin-left: auto;
margin-right: auto;
}
.form-align {
display: flex;
flex-direction: row;
}
.top-padding {
padding-top: 35px;
}
.container {
display: flex;
}
</style>

View File

@ -0,0 +1,188 @@
// 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.
<template>
<div class="center">
<a-form>
<img
v-if="$config.banner"
:src="$config.banner"
class="user-layout-logo"
alt="logo">
<h1 style="text-align: center; font-size: 24px; color: gray"> {{ $t('label.two.factor.authentication') }} </h1>
<p v-if="$store.getters.twoFaProvider === 'totp'" style="text-align: center; font-size: 16px;" v-html="$t('message.two.fa.auth.totp')"></p>
<p v-if="$store.getters.twoFaProvider === 'staticpin'" style="text-align: center; font-size: 16px;" v-html="$t('message.two.fa.auth.staticpin')"></p>
<br />
<a-form
:ref="formRef"
:model="form"
:rules="rules"
@finish="handleSubmit"
layout="vertical">
<a-form-item name="code" ref="code" style="text-align: center;">
<a-input-password
style="width: 500px"
v-model:value="form.code"
placeholder="xxxxxx" />
</a-form-item>
<br/>
<div :span="24" class="center-align top-padding">
<a-button
:loading="loading"
ref="submit"
type="primary"
:disabled="buttonstate"
class="center-align"
@click="handleSubmit">{{ $t('label.verify') }}
</a-button>
</div>
</a-form>
</a-form>
</div>
</template>
<script>
import { api } from '@/api'
import { ref, reactive, toRaw } from 'vue'
export default {
name: 'VerifyTwoFa',
data () {
return {
twoFAresponse: false,
buttonstate: false
}
},
created () {
this.initForm()
},
methods: {
initForm () {
this.formRef = ref()
this.form = reactive({})
this.rules = reactive({
code: [{ required: true, message: this.$t('message.error.authentication.code') }]
})
},
handleSubmit () {
this.formRef.value.validate().then(() => {
const values = toRaw(this.form)
if (values.code !== null) {
this.buttonstate = true
}
api('validateUserTwoFactorAuthenticationCode', { codefor2fa: values.code }).then(response => {
this.twoFAresponse = true
if (this.twoFAresponse) {
this.$notification.destroy()
this.$store.commit('SET_COUNT_NOTIFY', 0)
this.$store.commit('SET_LOGIN_FLAG', true)
this.$router.push({ path: '/dashboard' }).catch(() => {})
this.$message.success({
content: `${this.$t('label.action.verify.two.factor.authentication')}`,
duration: 2
})
this.$emit('refresh-data')
}
}).catch(() => {
this.$store.dispatch('Logout').then(() => {
this.$router.replace({ path: '/user/login' })
})
this.$notification.error({
message: this.$t('message.request.failed'),
description: this.$t('message.two.factor.authorization.failed')
})
})
})
}
}
}
</script>
<style lang="less" scoped>
.center {
position: fixed;
top: 42.5%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
background-color: transparent;
padding: 70px 50px 70px 50px;
z-index: 100;
}
.center-align {
display: block;
margin-left: auto;
margin-right: auto;
}
.top-padding {
padding-top: 35px;
}
.note {
text-align: center;
color: grey;
padding-top: 10px;
}
.user-layout {
height: 100%;
&-container {
padding: 3rem 0;
width: 100%;
@media (min-height:600px) {
padding: 0;
position: relative;
top: 50%;
transform: translateY(-50%);
margin-top: -50px;
}
}
&-logo {
border-style: none;
margin: 0 auto 2rem;
display: block;
.mobile & {
max-width: 300px;
margin-bottom: 1rem;
}
}
&-footer {
display: flex;
flex-direction: column;
position: absolute;
bottom: 20px;
text-align: center;
width: 100%;
@media (max-height: 600px) {
position: relative;
margin-top: 50px;
}
label {
width: 368px;
font-weight: 500;
margin: 0 auto;
}
}
}
</style>

View File

@ -0,0 +1,317 @@
// 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.
<template>
<div style="width:500px;height=500px">
<p v-html="$t('message.two.fa.setup.page')"></p>
<h3> {{ $t('label.select.2fa.provider') }} </h3>
<a-form
:ref="formRef"
:model="form"
:rules="rules"
layout="vertical">
<a-row :gutter="12">
<a-col :md="24" :lg="20">
<a-form-item v-ctrl-enter="submitPin" ref="selectedProvider" name="selectedProvider">
<a-select
v-model:value="form.selectedProvider"
optionFilterProp="label"
:filterOption="(input, option) => {
return option.children[0].children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
@change="val => { handleSelectChange(val) }">
<a-select-option
v-for="(opt) in providers"
:key="opt"
:value="opt">
<div>
<span v-if="opt === 'totp'">
<google-outlined />
Google Authenticator
</span>
<span v-if="opt === 'othertotp'">
<field-time-outlined />
Other TOTP Authenticators
</span>
<span v-if="opt === 'staticpin'">
<lock-outlined />
Static PIN
</span>
</div>
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :md="24" :lg="4">
<a-form-item>
<div v-if="selectedProvider">
<a-button ref="submit" type="primary" :disabled="twoFAenabled" @click="setup2FAProvider">{{ $t('label.setup') }}</a-button>
<tooltip-button
tooltipPlacement="top"
:tooltip="$t('label.accept.project.invitation')"
icon="check-outlined"
size="small"
@onClick="setup2FAProvider()"/>
<tooltip-button
tooltipPlacement="top"
:tooltip="$t('label.decline.invitation')"
type="primary"
:danger="true"
icon="close-outlined"
size="small"
@onClick="setup2FAProvider()"/>
</div>
</a-form-item>
</a-col>
</a-row>
<div v-if="twoFAenabled">
<div v-if="form.selectedProvider !== 'staticpin'">
<br />
<p v-html="$t('message.two.fa.register.account')"></p>
<vue-qrious
class="center-align"
:value="totpUrl"
size="200"
@change="onDataUrlChange"
/>
<div style="text-align: center"> <a @click="showConfiguredPin"> {{ $t('message.two.fa.view.setup.key') }}</a></div>
</div>
<div v-if="form.selectedProvider === 'staticpin'">
<br>
<p v-html="$t('message.two.fa.staticpin')"></p>
<br>
<div> <a @click="showConfiguredPin"> {{ $t('message.two.fa.view.static.pin') }}</a></div>
</div>
<div v-if="form.selectedProvider">
<br />
<h3> {{ $t('label.enter.code') }} </h3>
<a-row :gutter="12">
<a-col :md="24" :lg="20">
<a-form-item @finish="submitPin" v-ctrl-enter="submitPin" name="code" ref="code">
<a-input-password
v-model:value="form.code"
placeholder="xxxxxx" />
</a-form-item>
</a-col>
<a-col :md="24" :lg="4">
<a-form-item>
<a-button ref="submit" type="primary" :disabled="verifybuttonstate" @click="submitPin">{{ $t('label.verify') }}</a-button>
</a-form-item>
</a-col>
</a-row>
</div>
<a-modal
v-if="showPin"
:visible="showPin"
:title="$t(form.selectedProvider === 'staticpin'? 'label.two.factor.authentication.static.pin' : 'label.two.factor.authentication.secret.key')"
:closable="true"
:footer="null"
@cancel="onCloseModal"
centered
width="450px">
<div> {{ pin }} </div>
</a-modal>
</div>
</a-form>
</div>
</template>
<script>
import { api } from '@/api'
import { ref, reactive, toRaw } from 'vue'
import store from '@/store'
import VueQrious from 'vue-qrious'
import eventBus from '@/config/eventBus'
export default {
name: 'SetupTwoFaAtUserProfile',
props: {
resource: {
type: Object,
required: true
}
},
components: {
VueQrious
},
data () {
return {
totpUrl: '',
dataUrl: '',
pin: '',
showPin: false,
twoFAenabled: false,
twoFAverified: false,
providers: [],
selectedProvider: null,
verifybuttonstate: false
}
},
mounted () {
this.list2FAProviders()
},
created () {
this.initForm()
eventBus.on('action-closing', (args) => {
if (args.action.api === 'setupUserTwoFactorAuthentication' && this.twoFAenabled && !this.twoFAverified) {
this.disable2FAProvider()
}
})
},
methods: {
initForm () {
this.formRef = ref()
this.form = reactive({})
this.rules = reactive({
code: [{ required: true, message: this.$t('message.error.authentication.code') }]
})
},
onDataUrlChange (dataUrl) {
this.dataUrl = dataUrl
},
handleSelectChange (val) {
if (this.twoFAenabled) {
api('setupUserTwoFactorAuthentication', { enable: 'false' }).then(response => {
this.pin = ''
this.username = ''
this.totpUrl = ''
this.dataUrl = ''
this.showPin = false
this.twoFAenabled = false
this.twoFAverified = false
}).catch(error => {
this.$notification.error({
message: this.$t('message.request.failed'),
description: (error.response && error.response.headers && error.response.headers['x-description']) || error.message
})
})
}
this.selectedProvider = val
this.twoFAenabled = false
},
setup2FAProvider () {
this.formRef.value.validate().then(() => {
const values = toRaw(this.form)
this.selectedProvider = values.selectedProvider
var provider
if (this.selectedProvider === 'othertotp') {
provider = 'totp'
} else {
provider = this.selectedProvider
}
api('setupUserTwoFactorAuthentication', { provider: provider }).then(response => {
this.pin = response.setupusertwofactorauthenticationresponse.setup2fa.secretcode
if (this.selectedProvider === 'totp' || this.selectedProvider === 'othertotp') {
this.username = response.setupusertwofactorauthenticationresponse.setup2fa.username
var issuer = 'CloudStack'
if (store.getters.twoFaIssuer !== '' && store.getters.twoFaIssuer !== undefined) {
issuer = store.getters.twoFaIssuer
}
this.totpUrl = 'otpauth://totp/' + issuer + ':' + this.username + '?secret=' + this.pin + '&issuer=' + issuer
this.showPin = false
}
if (this.selectedProvider === 'staticpin') {
this.showPin = true
}
this.twoFAenabled = true
this.twoFAverified = false
}).catch(error => {
this.$notification.error({
message: this.$t('message.request.failed'),
description: (error.response && error.response.headers && error.response.headers['x-description']) || error.message
})
})
})
},
disable2FAProvider () {
api('setupUserTwoFactorAuthentication', { enable: false }).then(response => {
this.showPin = false
this.twoFAenabled = false
this.twoFAverified = false
}).catch(error => {
this.$notification.error({
message: this.$t('message.request.failed'),
description: (error.response && error.response.headers && error.response.headers['x-description']) || error.message
})
})
},
list2FAProviders () {
api('listUserTwoFactorAuthenticatorProviders', {}).then(response => {
var providerlist = response.listusertwofactorauthenticatorprovidersresponse.providers || []
var providernames = []
for (const provider of providerlist) {
providernames.push(provider.name)
if (provider.name === 'totp') {
providernames.push('othertotp')
}
}
this.providers = providernames
})
},
submitPin () {
this.formRef.value.validate().then(() => {
const values = toRaw(this.form)
if (values.code !== null) {
this.verifybuttonstate = true
}
api('validateUserTwoFactorAuthenticationCode', { codefor2fa: values.code }).then(response => {
this.$message.success({
content: `${this.$t('label.action.enable.two.factor.authentication')}`,
duration: 2
})
this.twoFAverified = true
this.$emit('refresh-data')
}).catch(error => {
this.$notification.error({
message: this.$t('message.request.failed'),
description: (error.response && error.response.headers && error.response.headers['x-description']) || error.message
})
})
this.closeAction()
})
},
closeAction () {
this.$emit('close-action')
},
showConfiguredPin () {
this.showPin = true
},
onCloseModal () {
this.showPin = false
}
}
}
</script>
<style scoped>
.center-align {
display: block;
margin-left: auto;
margin-right: auto;
}
.form-align {
display: flex;
flex-direction: row;
}
.container {
display: flex;
}
</style>

View File

@ -52,7 +52,7 @@ public interface SerialVersionUID {
public static final long DiscoveryException = Base | 0x1b;
public static final long ConflictingNetworkSettingException = Base | 0x1c;
public static final long CloudAuthenticationException = Base | 0x1d;
public static final long AsyncCommandQueued = Base | 0x1e;
public static final long CloudTwoFactorAuthenticationException = Base | 0x1e;
public static final long ResourceUnavailableException = Base | 0x1f;
public static final long ConnectionException = Base | 0x20;
public static final long PermissionDeniedException = Base | 0x21;

View File

@ -46,6 +46,7 @@ public class CSExceptionErrorCode {
ExceptionErrorCodeMap.put("com.cloud.exception.AccountLimitException", 4280);
ExceptionErrorCodeMap.put("com.cloud.exception.AgentUnavailableException", 4285);
ExceptionErrorCodeMap.put("com.cloud.exception.CloudAuthenticationException", 4290);
ExceptionErrorCodeMap.put("com.cloud.exception.CloudTwoFactorAuthenticationException", 4295);
ExceptionErrorCodeMap.put("com.cloud.exception.ConcurrentOperationException", 4300);
ExceptionErrorCodeMap.put("com.cloud.exception.ConflictingNetworkSettingsException", 4305);
ExceptionErrorCodeMap.put("com.cloud.exception.DiscoveredWithErrorException", 4310);