mirror of
https://github.com/apache/cloudstack.git
synced 2025-10-26 08:42:29 +01:00
User two factor authentication (#6924)
Co-authored-by: Rohit Yadav <rohit.yadav@shapeblue.com>
This commit is contained in:
parent
90c92f2710
commit
a3feccf70c
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
}
|
||||
|
||||
@ -90,4 +90,8 @@ public interface User extends OwnedBy, InternalIdentity {
|
||||
public String getExternalEntity();
|
||||
|
||||
public void setExternalEntity(String entity);
|
||||
|
||||
public boolean isUser2faEnabled();
|
||||
|
||||
public String getKeyFor2fa();
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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());
|
||||
|
||||
|
||||
|
||||
@ -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///////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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)));
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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);
|
||||
30
plugins/user-two-factor-authenticators/static-pin/pom.xml
Normal file
30
plugins/user-two-factor-authenticators/static-pin/pom.xml
Normal 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>
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
@ -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>
|
||||
30
plugins/user-two-factor-authenticators/totp/pom.xml
Normal file
30
plugins/user-two-factor-authenticators/totp/pom.xml
Normal 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>
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
@ -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>
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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) {
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
}
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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}" />
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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 {
|
||||
|
||||
294
test/integration/smoke/test_2fa.py
Normal file
294
test/integration/smoke/test_2fa.py
Normal 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
64
ui/package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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.",
|
||||
|
||||
@ -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()"
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 } })
|
||||
})
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 } })
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 {
|
||||
|
||||
341
ui/src/views/dashboard/SetupTwoFaAtLogin.vue
Normal file
341
ui/src/views/dashboard/SetupTwoFaAtLogin.vue
Normal 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>
|
||||
188
ui/src/views/dashboard/VerifyTwoFa.vue
Normal file
188
ui/src/views/dashboard/VerifyTwoFa.vue
Normal 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>
|
||||
317
ui/src/views/iam/SetupTwoFaAtUserProfile.vue
Normal file
317
ui/src/views/iam/SetupTwoFaAtUserProfile.vue
Normal 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>
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user